source: trunk/FieldsWidgets.py @ 3090

Revision 3090, 47.4 KB checked in by jukka, 9 years ago (diff)

Few more fixes

  • Property svn:eol-style set to native
Line 
1# Copyright 2006 by the LeMill Team (see AUTHORS)
2#
3# This file is part of LeMill.
4#
5# LeMill is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# LeMill is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with LeMill; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18
19from AccessControl import ClassSecurityInfo
20from Products.Archetypes.config import REFERENCE_CATALOG
21from Products.Archetypes.Registry import registerField, registerWidget
22from Products.Archetypes.Field import StringField, LinesField, ReferenceField, ObjectField, FileField
23from Products.Archetypes.Widget import TextAreaWidget, StringWidget, SelectionWidget, TypesWidget, MultiSelectionWidget, VisualWidget, FileWidget
24from Products.Archetypes.ReferenceEngine import Reference
25from config import to_unicode
26from Products.Archetypes import config
27from string import letters, punctuation
28from Products.CMFCore.utils import getToolByName
29from ZODB.PersistentMapping import PersistentMapping
30from Products.Archetypes.utils import shasattr
31from Acquisition import aq_base
32from mp3tool import get_length
33from types import ListType, TupleType, StringType, UnicodeType, InstanceType
34from messagefactory_ import i18nme as _
35from random import shuffle
36from copy import copy
37from xml.dom.minidom import parseString as parse_xml
38from xml.sax.saxutils import unescape
39import re, os, shutil
40
41#StringWidget and TextAreaWidget need to update their macro, otherwise they try to use archetypes/skins/widgets -path only
42#Unfortunately this can't be done but by making new Widgets: LeStringWidget and LeTextAreaWidget
43
44class LeVisualWidget(VisualWidget):
45    _properties = VisualWidget._properties.copy()
46    _properties.update({
47        'macro' : 'widget_visual',
48        'rows'  : 18,      #rows of TextArea if VE is not available
49        'cols'  : 80,      #same for cols
50        'width' : '507px', #width of VE frame (if VE is avalilable)
51        'height': '260px', #same for height
52    })
53    # material types use versions of chapter_widgets and are not affected by this
54
55    def process_form(self, instance, field, form, empty_marker=None, emptyReturnsMarker=False):
56        """Basic impl for form processing in a widget"""
57        value = form.get(field.getName(), empty_marker)
58        if value is empty_marker:
59            return empty_marker
60        if emptyReturnsMarker and value == '':
61            return empty_marker
62        return value, {}
63
64registerWidget(LeVisualWidget,
65    title='Visual Widget',
66    description='Visual Widget'
67)   
68
69class LeStringWidget(StringWidget):
70    _properties = StringWidget._properties.copy()
71    _properties.update({
72        'macro' : 'widget_string'
73    })
74
75registerWidget(LeStringWidget,
76    title='String Widget',
77    description='String Widget'
78)
79
80
81class LeTextAreaWidget(TextAreaWidget):
82    _properties = TextAreaWidget._properties.copy()
83    _properties.update({
84        'macro' : 'widget_textarea'
85    })
86
87registerWidget(LeTextAreaWidget,
88    title='Textarea Widget',
89    description='Textarea Widget'
90)
91
92class TagsWidget(StringWidget):
93    _properties = StringWidget._properties.copy()
94    _properties.update({
95        'macro' : 'widget_tags',
96    })
97   
98registerWidget(TagsWidget,
99    title='Tags Widget',
100    description='Tags Widget',
101    used_for=('Products.LeMill.TagsField.TagsField',)
102)
103
104class HTMLLinkWidget(StringWidget):
105    _properties = StringWidget._properties.copy()
106    _properties.update({
107        'macro' : 'widget_htmllink',
108    })
109
110    def getShortLinkName(self, link):
111        """Returns the shortened version of the link pointing to the reference's location. This shortened version goes into the content of the <a> tag."""     
112        lt = getToolByName(self, 'lemill_tool')
113        return lt.parse_text(link, start_with_p=False)
114
115    def process_form(self, instance, field, form, empty_marker=None, emptyReturnsMarker=False):
116        """Basic impl for form processing in a widget"""
117        value = form.get(field.getName(), empty_marker)
118        if value is empty_marker:
119            return empty_marker
120        if emptyReturnsMarker and value == '':
121            return empty_marker
122        if field.getName() == 'home_page':
123            if not (value.lower().startswith('http://') or value.lower().startswith('https://')) and value != '':
124                value = 'http://' + value
125        return value, {}
126   
127registerWidget(HTMLLinkWidget,
128    title='HTML Link Widget',
129    description='Generated <a> tag Widget',
130    used_for=('Products.LeMill.FieldsWidgets.HTMLLinkWidget',)
131)
132
133class MessengerWidget(TypesWidget):
134    _properties = TypesWidget._properties.copy()
135    _properties.update({
136        'macro' : 'widget_messenger',
137    },)
138
139    security = ClassSecurityInfo()
140
141    security.declarePublic('process_form')
142    def process_form(self, instance, field, form, empty_marker=None,
143                     emptyReturnsMarker=False):
144        """Concatenates address + messenger type, but if address already contains something like prefix (word:) then removes it.
145        """
146        name = field.getName()
147        otherName = "%s_other" % name
148        value = form.get(name, empty_marker)
149        othervalue = form.get(otherName, empty_marker)
150        if not value:
151            return '', {}
152        if not othervalue == empty_marker:
153            value = "%s:%s" % (othervalue,value)
154        return value, {}
155
156
157registerWidget(MessengerWidget,
158    title='Messenger Widget',
159    description='Messenger Widget',
160    used_for=('Products.Archetypes.Field.StringField',)
161)
162
163
164class MobileWidget(StringWidget):
165    _properties = StringWidget._properties.copy()
166    _properties.update({
167        'macro' : 'widget_mobile',
168    })
169
170    security = ClassSecurityInfo()
171
172    security.declarePublic('process_form')
173    def process_form(self, instance, field, form, empty_marker=None,
174                     emptyReturnsMarker=False):
175        name = field.getName()
176        otherName = "%s_checkbox" % name
177        value = form.get(name, empty_marker)
178        othervalue = form.get(otherName, empty_marker)
179        if value == empty_marker and emptyReturnsMarker:
180            return empty_marker
181        if othervalue=="1":
182            value = "*SMS*%s" % value
183        return value, {}
184
185   
186registerWidget(MobileWidget,
187    title='Mobile Widget',
188    description='Mobile Widget',
189    used_for=('Products.Archetypes.Field.StringField',)
190)
191
192
193class CopyrightWidget(SelectionWidget):
194    _properties = SelectionWidget._properties.copy()
195    _properties.update({
196        'macro' : 'widget_copyright',
197    })
198   
199registerWidget(CopyrightWidget,
200    title='Copyright Widget',
201    description='Copyright selection Widget',
202    used_for=('Products.LeMill.StringField.StringField',)
203)
204
205class TwoColumnMultiSelectionWidget(MultiSelectionWidget):
206    _properties = MultiSelectionWidget._properties.copy()
207    _properties.update({
208        'macro' : 'widget_twocolumn',
209    })
210   
211registerWidget(TwoColumnMultiSelectionWidget,
212    title='Two column widget',
213    description='Two column multiselection widget',
214    used_for=('Products.Archetypes.Field.MultiSelectionField',)
215)
216
217
218class GroupWidget(SelectionWidget):
219    _properties = SelectionWidget._properties.copy()
220    _properties.update({
221        'macro' : 'widget_group',
222        'format' : 'checkbox',
223    })
224    def getGroupVocabulary(self, instance):
225        """ this builds a vocabulary for groups field """
226        lutool = getToolByName(instance, 'lemill_usertool')
227        mf = lutool.getMemberFolder()
228        user_groups=[]
229        if mf:
230            user_groups=mf.getGroups()
231        resource_groups=instance.getGroupInfo()       
232        resource_uids=[g.UID for g in resource_groups]
233        user_uids=[g.UID for g in user_groups]
234        disabled=[(g.UID, g.Title, True, True) for g in resource_groups if g.UID not in user_uids]
235        other=[(g.UID, g.Title, g.UID in resource_uids, False) for g in user_groups]
236        return disabled+other+[('__new_group','...or create a new group:',False,False)]
237       
238    def process_form(self, instance, field, form, empty_marker=[],
239                     emptyReturnsMarker=False, validating=True):
240        """Basic impl for form processing in a widget"""
241        value = form.get(field.getName(), empty_marker)
242        if value is empty_marker:
243            return empty_marker
244        if emptyReturnsMarker and value == '':
245            return empty_marker
246        if '__new_group' in value:
247            new_group_name = form.get('new_group_name', '')
248            if new_group_name:
249                # create a new group here
250                blog=instance.community.groups.addGroup(new_group_name)
251                value.remove('__new_group')
252                value.append(blog.UID())
253        return value, {}
254
255
256class ChapterWidget(SelectionWidget):
257    _properties = SelectionWidget._properties.copy()
258    _properties.update({
259        'macro' : 'widget_chapter',
260        'collections_only' : False,
261        'helper_js' : ('AC_RunActiveContent.js',),
262        'show_added' : True,
263        'no_label' : True
264    })
265    security = ClassSecurityInfo()
266
267    security.declarePublic('process_form')
268    def process_form(self, instance, field, form, empty_marker=None,
269            emptyReturnsMarker=False):
270        """ goes through the form, reorganizes chapters if necessary and delivers results to field writer (as dict). """
271        chapter_type=form.get('chapter_type')
272        if not chapter_type:
273            return {},{}
274        changes = {}
275        changes['edited']=int(form.get('chapter_last_edited', 0))
276        count=int(form.get('chapter_count',0))
277        changes['count']=count
278        new_order=[]
279        reorder=False
280        deleted=[]
281        # Orderings and deletions
282        for index in range(0,count):
283            order=int(form.get('chapter_order_%s' % index, 0))
284            new_order.append(order)
285            if order!=index:
286                reorder=True
287            this_deleted = int(form.get('chapter_deleted_%s' % index, 0))
288            if this_deleted:
289                deleted.append(index)
290        if reorder:
291            changes['new_order']=new_order
292        if deleted:
293            changes['deleted']=deleted
294        # Changes
295        changes['chapter_type']=chapter_type
296        if chapter_type=='media_piece':
297            file=form.get('file', None)
298            if file and hasattr(file, 'filename'):
299                changes['file']=file
300            else:
301                changes['uid']=form.get('piece_uid')
302        elif chapter_type=='text_block':
303            changes['text']=form.get('chapter_textarea')
304        elif chapter_type=='embed_block':
305            changes['text']=''
306            changes['embed']=form.get('embed_textarea')
307        # Save & edit buttons
308        if form.get('saveChapter', None):
309            form.update({'chapter_edited':-1, 'chapter_anchor':changes['edited'], 'stay':1})       
310        elif form.get('editChapter', None):
311            form.update({'chapter_anchor':form['chapter_edited'], 'stay':1})       
312        # Add-buttons
313        elif form.get('add_text_block', None):
314            changes['new_chapters']=[{'text':'','type':'text_block'}]
315            form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
316        elif form.get('add_media_piece', None):
317            changes['new_chapters']=[{'text':'','type':'media_piece','uid':''}]
318            form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
319        elif form.get('add_embed_block', None):
320            changes['new_chapters']=[{'text':'','type':'embed_block', 'embed':''}]
321            form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
322        return changes, {}
323
324registerWidget(ChapterWidget,
325    title='Chapter Widget',
326    description='Chapter editor Widget',
327    used_for=('Products.LeMill.ChapterField',)
328)
329
330class SlideWidget(ChapterWidget):
331    _properties = ChapterWidget._properties.copy()
332    _properties.update({
333        'macro' : 'widget_slides',
334        'collections_only' : False,
335        'show_added' : True,
336        'chapter_count' : 5,
337    })
338
339registerWidget(SlideWidget,
340    title='Slide Widget',
341    description='Slide and caption viewer Widget',
342    used_for=('Products.LeMill.ChapterField',)
343)
344
345class PilotWidget(ChapterWidget):
346    _properties = ChapterWidget._properties.copy()
347    _properties.update({
348        'macro' : 'widget_pilot',
349        'collections_only' : False,
350        'helper_js' : ('AC_RunActiveContent.js',),
351        'show_added' : True,
352        'chapter_count' : 3,
353    })
354    security = ClassSecurityInfo()
355
356    security.declarePublic('process_form')
357    def process_form(self, instance, field, form, empty_marker=None,
358            emptyReturnsMarker=False):
359        """ goes through the form and sends changes forward to field writer """
360        changes, empty= ChapterWidget.process_form(self, instance, field, form)
361        if not changes:
362            return changes,empty
363        chapter_type=changes['chapter_type']
364        if chapter_type=='pilot_scene':
365            audio_file=form.get('audio_file', None)
366            if audio_file and hasattr(audio_file, 'filename'):
367                changes['audio_file']=audio_file
368            else:
369                changes['audio_uid']=form.get('audio_uid')
370            image_file=form.get('image_file', None)
371            if image_file and hasattr(image_file, 'filename'):
372                changes['image_file']=image_file
373            else:
374                changes['image_uid']=form.get('image_uid')
375            kws=[]
376            for i in range(0, 3):
377                kw=form.get('keywords_%s' % i, '')
378                if kw:
379                    kws.append(kw)
380            if kws:
381                changes['keywords']=kws
382            qs=[]
383            for i in range(0, 7):
384                q=form.get('question_%s' % i, '')
385                if q:
386                    qs.append(q)
387            if qs:
388                changes['questions']=qs
389        # Buttons
390        if form.get('add_pilot_section', None):
391            changes['new_chapters']={'text':'','type':'pilot_scene','audio_uid':'', 'image_uid':'', 'keywords':[], 'questions':[]},
392            form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
393        return changes, {}
394
395registerWidget(PilotWidget,
396    title='Pilot Widget',
397    description='Scene editor Widget',
398    used_for=('Products.LeMill.ChapterField',)
399)
400
401class ExerciseWidget(ChapterWidget):
402    _properties = ChapterWidget._properties.copy()
403    _properties.update({
404        'macro' : 'widget_exercise',
405        'collections_only' : False,
406        'helper_js' : ('AC_RunActiveContent.js',),
407        'show_added' : True,
408        'chapter_count' : 1,
409    })
410    security = ClassSecurityInfo()
411
412    security.declarePublic('process_form')
413    def process_form(self, instance, field, form, empty_marker=None,
414            emptyReturnsMarker=False):
415        """ goes through the form and tries to merge all text inputs to lists, find references to pieces and if file objects are uploaded, just send them forward to field. """
416        changes, empty= ChapterWidget.process_form(self, instance, field, form)
417        if not changes:
418            return changes,empty
419        chapter_type=changes['chapter_type']               
420        if chapter_type=='guidelines':
421            # this should be deprecated, but I keep this still here because I cannot now think if there
422            # are any bad consequences from removing it. Too tired.
423            changes['text']=form.get('chapter')
424        elif chapter_type=='exercise':
425            # exercises are question chapters that don't have yet chosen any specific type for themselves. They should be empty.
426            changes['text']=''           
427        elif chapter_type in ['choice','multiple_choices','poll']:
428            question=form.get('choice_question')
429            answers=[]
430            answer_count=int(form.get('choice_answer_count',0))
431            for i in range(0, answer_count):
432                answer={}
433                answer['text']=form.get('choice_answer_%s' % i, '')
434                answer['is_correct']= chapter_type=='poll' or int(form.get('answer_%s_correct' % i, ''))
435                answer['order']=int(form.get('choice_order_%s' % i, 0))
436                if answer['text']:  # empty answers are deleted by ignoring them
437                    answers.append(answer)
438            answers.sort(cmp=lambda x,y: cmp(x['order'], y['order']))
439            changes['question']=question
440            changes['answers']=answers or [{'text':'','is_correct':1,'order':0}]
441        elif chapter_type=='fill_in_the_blanks':
442            changes['text']=form.get('fill_blanks','')
443        elif chapter_type=='open_ended':
444            changes['text']=form.get('open_field','')
445        # Buttons
446        if form.get('add_exercise', None):
447            changes['new_chapters']=[{'text':'','type':'exercise'}]
448            count=changes['count']
449            form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
450        elif form.get('import_potatoes', None):
451            file=form.get('import_file', None)
452            if file and hasattr(file, 'filename'):
453                if file.filename.endswith('jcl'):
454                    text=self.importJClozeQuestion(file)
455                    changes['chapter_type']='fill_in_the_blanks'
456                    changes['text']=text
457                    form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
458                elif file.filename.endswith('jcz'):
459                    new_chapters=self.importJQuizQuestions(file)
460                    if new_chapters:
461                        this_chapter= new_chapters.pop(0)
462                        changes['chapter_type']=this_chapter['type']
463                        changes['answers']=this_chapter['answers']
464                        changes['question']=this_chapter['text']
465                        if new_chapters:
466                            changes['new_chapters']=new_chapters
467                            form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
468        return changes, {}
469   
470    def importJQuizQuestions(self, file):
471        """ import hot potatoes questions made with jquiz """       
472        dom = parse_xml(file.read())
473        questions = dom.getElementsByTagName("question-record")
474        result=[]
475        for ques in questions:
476            questype = int(ques.getElementsByTagName("question-type")[0].childNodes[0].data)
477            if not (questype==1 or questype==4):
478                return           
479            question = ques.getElementsByTagName("question")[0].childNodes[0].data.encode('utf-8')
480            # if jquiz question type is multiple choice(1) or multiple correct(4)
481            answers = ques.getElementsByTagName("answer")
482            # calculate how many correct answers (is it choice or choice_multiple type)
483            corrects=0
484            made_answers=[]
485            for answer in answers:
486                correct = int(answer.getElementsByTagName("correct")[0].childNodes[0].data) # 1-correct 0-false
487                if correct:
488                    corrects+=1
489                answertext = answer.getElementsByTagName("text")[0].childNodes[0].data.encode('utf-8')
490                if answertext:
491                    made_answers.append({'text':answertext, 'is_correct':correct})
492            if corrects==len(result):
493                qtype='poll'
494            elif corrects==1:
495                qtype='choice'
496            else:
497                qtype='multiple_choices'
498            result.append({'type':qtype,'text':question,'answers':made_answers})       
499        dom.unlink()
500        return result
501   
502    def importJClozeQuestion(self, file):
503        """ import hot potatoes question made with JCloze """       
504        dom = parse_xml(file.read())
505        gap_fill = dom.getElementsByTagName("gap-fill")[0]
506        result=[]
507        for node in gap_fill.childNodes:
508            if node.nodeType==node.TEXT_NODE:
509                result.append(node.data)
510            else:
511                for answer in node.getElementsByTagName("answer"):
512                    text_tag=answer.getElementsByTagName("text")[0]
513                    text=text_tag.childNodes[0].data
514                    result.append('{%s}' % text)
515        result=u''.join(result)
516        #result=unescape(u''.join(result), {'&#x003C;':'<','&#x003E;':'>'})
517        dom.unlink()
518        return result
519
520registerWidget(ExerciseWidget,
521    title='Exercise Widget',
522    description='Exercise editor Widget',
523    used_for=('Products.LeMill.ChapterField',)
524)
525
526class AudioWidget(FileWidget):
527    _properties = FileWidget._properties.copy()
528    _properties.update({
529    'macro' : 'widget_audio',
530    'helper_js' : ('AC_RunActiveContent.js',),
531    })
532
533registerWidget(AudioWidget,
534    title='Audio Widget',
535    description='Widget that uses flashplayer for mp3:s',
536    used_for=('Products.LeMill.ChapterField',)
537)
538
539class LeMillLinksWidget(LeVisualWidget):
540    _properties = LeVisualWidget._properties.copy()
541    _properties.update({
542            'macro' : 'widget_lemilllinks',
543    })
544       
545    def process_form(self, instance, field, form, empty_marker=None,
546                     emptyReturnsMarker=False):
547        """ Replaces internal links with {uid:UID} and external links with {url} """
548        fieldname = field.getName()
549        value = form.get(fieldname, empty_marker)
550       
551        if value.strip() == '':
552            return None, {}
553           
554        portal_url = getToolByName(instance, 'portal_url')
555        portal_url = portal_url()
556        pc = getToolByName(instance, 'portal_catalog')
557        from LargeSectionFolder import DEFAULT_LOCATIONS
558        from config import CONTENT_TYPES, ACTIVITY_TYPES, TOOLS_TYPES
559        suitable_types = CONTENT_TYPES + ACTIVITY_TYPES + TOOLS_TYPES
560        locations = {}
561        # Only some locations are needed
562        for d in DEFAULT_LOCATIONS.keys():
563            if d in suitable_types:
564                locations[d] = DEFAULT_LOCATIONS[d]
565        # array of inserted data
566        rows = value.split('\n') 
567
568        data = ""
569        for row in rows:
570            r = row
571            row = row.strip()
572            words = []
573            if row != '':
574                words = row.split(' ')
575            for word in words:
576                index = words.index(word)
577                # control is it link to LeMill resource
578                if (word.lower().startswith(portal_url)):
579                    isMaterial = False
580                    for location, url in locations.items():
581                        if url in word:
582                            # Now the type is known, getting the id
583                            temp = word.split(url)
584                            temp = temp[1]
585                            temp = temp.split('/')
586                            id = temp[1]
587                            # Now to make a catalog search
588                            query = {'getId':id,'meta_type':location}
589                            results = pc(query)
590                            if len(results)>0:
591                                words[index] = '{uid:'+results[0].UID+'}'
592                                #value = value.replace(word,'{uid:'+results[0].UID+'}')
593                                isMaterial = True
594                    # if we dont find LeMill resource, then treat this url as any other link           
595                    if not isMaterial:
596                        words[index] = '{'+word+'}'
597                # if it is external link
598                elif (word.lower().startswith("http://") or word.lower().startswith("https://")):
599                    words[index] = '{'+word+'}'
600            if words != []:
601                data += ' '.join(words) + '\n'
602        return data, {}
603
604registerWidget(LeMillLinksWidget,
605    title='LeMill Links Widget',
606    description='LeMill Links Widget',
607    used_for=('Products.LeMill.FieldsWidgets.LeMillLinksField',)
608)
609
610
611class EmbedWidget(StringWidget):
612    _properties = StringWidget._properties.copy()
613    _properties.update({
614        'macro' : 'widget_embed'
615    })
616
617registerWidget(EmbedWidget,
618    title='Embed Widget',
619    description='Embed Widget, not modifiable by common means, only displays embed code'
620)
621
622
623####                            ####
624####            FIELDS          ####
625####                            ####
626
627
628class TagsField(LinesField):
629    """ A field that stores tags """
630    __implements__ = LinesField.__implements__
631    _properties = LinesField._properties.copy()
632    _properties.update({
633        'widget' : TagsWidget,
634    })
635
636    security = ClassSecurityInfo()
637
638    def set(self, instance, value, **kwargs):
639        if not value:
640            value=''
641        if isinstance(value,str):
642            value = value.lower()
643            value = value.split(',')
644        if isinstance(value, tuple) or isinstance(value, list):
645            value=[t.strip().lower() for t in value] # strip and clean individual tags
646            value=list(set(value)) # remove duplicates
647            value= '\n'.join(value) # merge back to string
648        LinesField.set(self, instance, value, **kwargs)
649                       
650    def getRaw(self, instance, **kwargs):
651        version = None
652        try:
653            version = instance.REQUEST.get('version',None)
654        except AttributeError:
655            pass
656        if version:
657            value = instance.getFieldHistory(self.getName(), version)
658            if value and callable(value):
659                value=value()
660            value = value.split(',')
661        else:
662            value = LinesField.get(self, instance, **kwargs)
663        return ', '.join(value)
664
665    def get(self, instance, **kwargs):
666        version = None
667        try:
668            version = instance.REQUEST.get('version', None)
669        except AttributeError:
670            pass
671        if version:
672            value = instance.getFieldHistory(self.getName(), version) or ''
673            if value and callable(value):
674                value=value()
675            if value:
676                value = value.split(',')             
677        else:
678            value = LinesField.get(self, instance, **kwargs)
679        return value
680
681registerField(TagsField,
682               title='Tags Field',
683               description=('Tags Field'),
684               )
685
686class WYSIWYMField(StringField):
687    """A field that stores WYSIWYM strings"""
688    _properties = StringField._properties.copy()
689    _properties.update({
690        'widget' : LeTextAreaWidget,
691        'edit_accessor' :  "getRaw",
692        'default_output_type' : 'text/x-html-captioned',
693        'default_content_type' : 'text/html',
694    })   
695   
696    security = ClassSecurityInfo()
697   
698   
699    def getRaw(self, instance, **kwargs):
700        """ Gets raw version for editing """
701        version = None
702        try:
703            version = instance.REQUEST.get('version',None)
704        except AttributeError:
705            pass
706        if version:
707            value = instance.getFieldHistory(self.getName(), version)
708        else:
709            value = StringField.get(self, instance,**kwargs)
710        return value
711   
712    def get(self, instance, **kwargs):
713        """ Gets cleaned version for display """
714        version = None
715        try:
716            version = instance.REQUEST.get('version',None)
717        except AttributeError:
718            pass
719        value = self.getRaw(instance, **kwargs)
720        cleaned_text = getattr(instance, 'cleaned_%s' % self.getName(), None)
721        if cleaned_text and not version:
722            return cleaned_text       
723        ltool = getToolByName(instance, 'lemill_tool')
724        cleaned_text = ltool.parse_text(value)
725        if not version:
726            setattr(instance, 'cleaned_%s' % self.getName(), cleaned_text)
727        return cleaned_text
728
729    def set(self, instance, value, **kwargs):
730        """ update the cleaned_text and then call original set """
731
732        # update cleaned text
733        ltool = getToolByName(instance, 'lemill_tool')
734        cleaned_text = ltool.parse_text(value)
735        setattr(instance, 'cleaned_%s' % self.getName(), cleaned_text)
736
737        # set value
738        StringField.set(self, instance, value, **kwargs)
739       
740
741
742    # render -method has moved to LeMillTools, because it is needed also outside the wysiwym field.
743
744registerField(WYSIWYMField,
745               title='WYSIWYM Field',
746               description=('WYSIWYM Field'),
747               )
748
749
750class ChapterField(ObjectField):
751    """ chapters are stored as lists, where chapter can either be a string or UID. If UID, view widget should fetch the object. """
752    _properties = ObjectField._properties.copy()
753    _properties.update({
754        'widget' : ChapterWidget,
755        'relationship' : 'uses', # required
756        'referenceClass' : Reference,
757        'referenceReferences' : False,
758        'index_method':'indexValue'})
759   
760    security = ClassSecurityInfo()
761    chapter_names={'text_block':_('text chapter'), 'media_piece':_('media piece'), 'embed_block':_('embedded content'), 'multiple_choices':_('multiple choices'), 'choices':_('choices'), 'fill_in_the_blanks':_('fill in the blanks'), 'open_ended':_('open ended question'), 'exercise':_('new exercise'), 'guidelines':_('text chapter')}
762
763    def getChapterNames(self):
764        """ Return readable versions of different chapter types """       
765        return ChapterField.chapter_names
766
767
768    def getRaw(self, instance, version=None, **kwargs):
769        """ Gets raw version for editing, as list """
770        if version:
771            value = instance.getFieldHistory(self.getName(), version) or []
772        elif '_initializing_' in kwargs:
773            value = self.getDefault(instance) or []   
774        else:
775            value = ObjectField.get(self, instance, **kwargs) or []
776        if not isinstance(value, list):
777            value = []
778        for chapter in value:
779            if isinstance(chapter, tuple):
780                value = self.updateChapterList(value)
781                break
782            elif isinstance(chapter, dict) and chapter['type']=='choice' and isinstance(chapter['text'], list):
783                value=self.fixBrokenChoice(value)
784                break
785        return value
786
787    def get(self, instance, **kwargs):
788        """ Get a cleaned version for display """
789        REQUEST=getattr(instance,'REQUEST',None)
790        if REQUEST:
791            version=REQUEST.get('version',None)
792            translation=REQUEST.get('translation',None)
793        else:
794            version=None
795            translation=None
796
797        fix_choice=False
798        convert=False
799        if version or translation: # don't cache cleaned version
800            value = self.getRaw(instance, version=version, **kwargs)
801            value=self.cleanChapters(instance, value)
802            if translation:
803                value=self.mergeTranslationSource(instance,value)
804            return value
805        cleaned_value = getattr(instance, 'cleaned_%s' % self.getName(), None)
806        if cleaned_value:
807            for chapter in cleaned_value:
808                if isinstance(chapter, tuple):
809                    convert=True
810                    break
811                elif isinstance(chapter, dict) and 'type' in chapter and chapter['type']=='choice' and isinstance(chapter['text'], list):
812                    fix_choice=True
813                    break
814            if convert:
815                cleaned_value=self.updateChapterList(cleaned_value)
816                cleaned_value=self.updateCleanedChapters(instance, cleaned_value)
817                return cleaned_value
818            if fix_choice:
819                cleaned_value=self.fixBrokenChoice(cleaned_value)
820                cleaned_value=self.updateCleanedChapters(instance, cleaned_value)
821                return cleaned_value
822            else:
823                return cleaned_value
824        value=self.getRaw(instance, **kwargs)
825        cleaned_value=self.updateCleanedChapters(instance, value)
826        return cleaned_value
827
828    def textOnly(self, instance, **kwargs):
829        listofdicts= getattr(instance, 'cleaned_%s' % self.getName(), [])
830        if not listofdicts:
831            listofdicts=self.get(instance)
832        if listofdicts and isinstance(listofdicts, list):
833            return '\n'.join([x['text'] for x in listofdicts])
834        return ''
835       
836
837    def mergeTranslationSource(self, instance, chapter_list):
838        """ Adds original source texts to translated chapters, under key 'original' """
839        return chapter_list
840        #trans_source=instance.get
841        #for chapter in chapter_list:
842       
843
844    def cleanChapters(self, instance, chapters):
845        ltool = getToolByName(instance, 'lemill_tool')
846        cleaned_chapters=[]
847        parsed_fields=['text','keywords','questions','answers']
848        for chapter in chapters:
849            new_chapter={}
850            new_chapter=copy(chapter)
851            for key, value in chapter.items():
852                if key in parsed_fields:
853                    new_chapter[key]=ltool.parse_text(value, start_with_p=False)
854            cleaned_chapters.append(new_chapter)
855        return cleaned_chapters
856
857
858    def updateCleanedChapters(self, instance, chapters):
859        cleaned_chapters=self.cleanChapters(instance, chapters)
860        setattr(instance, 'cleaned_%s' % self.getName(), cleaned_chapters)
861        return cleaned_chapters
862       
863
864    def _updatePilot(self, old_values):
865        """ in new dictionary based chapters it makes sense to use one chapter per slide, instead of three chapters
866        this method converts old chapter lists into new format """       
867        new_scene=True
868        updated_values=[]
869        for cvalue, ctype in old_values:
870            if new_scene:
871                scene={'text':'', 'image_uid':'', 'audio_uid':'', 'keywords':[], 'questions':[], 'type':'pilot_scene'}
872                new_scene=False
873            if ctype=='audio_piece':
874                scene['audio_uid']=cvalue
875            elif ctype=='image_piece':
876                scene['image_uid']=cvalue
877            elif ctype=='pilot_keywords':
878                scene['keywords']=cvalue
879                updated_values.append(scene)
880                new_scene=True
881            elif ctype=='pilot_questions':
882                scene['questions']=cvalue
883                updated_values.append(scene)
884                new_scene=True
885        return updated_values           
886       
887    def fixBrokenChoice(self, old_values):
888        for vald in old_values:
889            if vald['type']=='choice' and isinstance(vald['text'], list):
890                 # [text, [correct_answers], [wrong_answers]]
891                 broken_text=vald['text']
892                 vald['text']=broken_text[0]
893                 choices=[(broken_text[1][0], 1)]
894                 for item in broken_text[2]:
895                    choices.append((item, 0))
896                 vald['choices']=choices
897                 vald['type']='multiple_choices'
898            new_values.append(vald)
899        return new_values
900
901
902    def updateChapterList(self, old_values):
903        """ Backward compatibility:
904            Converts a chapterlist to use dicts instead of tuples. Returns updated list """
905        updated_values=[]
906        if 'pilot_keywords' in [ct for cv, ct in old_values]: # Pilots need little more work
907            return self._updatePilot(old_values)
908        for cvalue, ctype in old_values:
909            if ctype in ['multiple_choices','poll','choices', 'choice']:
910                q,correct,wrong=cvalue
911                if ctype=='multiple_choices':
912                    choices=[(c, 1) for c in correct]
913                    choices+=[(w, 0) for w in wrong]
914                    shuffle(choices)
915                elif ctype=='choices' or ctype=='choice':
916                    if correct and isinstance(correct, list):
917                        correct=correct[0]
918                    choices=[(correct,1)]
919                    choices+=[(w, 0) for w in wrong]
920                    shuffle(choices)
921                    ctype='multiple_choices'
922                elif ctype=='poll':
923                    choices=[(c,1) for c in correct+wrong]
924                    ctype='multiple_choices'
925                new_chapter={'text':q, 'type':ctype, 'choices':choices}
926            elif ctype in ['image_piece','audio_piece','media_piece']:
927                new_chapter={'text':'', 'type':ctype, 'uid':cvalue}
928            elif ctype=='embed_block':
929                new_chapter={'text':'', 'type':'embed_block', 'embed':cvalue}
930            elif ctype=='guidelines':
931                new_chapter={'text':cvalue, 'type':'text_block'}
932            else:
933                new_chapter={'text':cvalue, 'type':ctype}
934            changed=True
935            updated_values.append(new_chapter)
936        return updated_values
937
938    def set(self, instance, value, **kwargs):
939        if not value:
940            return
941        lt = getToolByName(instance, 'lemill_tool')
942        changed=False
943        if kwargs.get('_initializing_', False):
944            old_values=self.getDefault(instance)
945            changed=True
946        else:
947            old_values=self.getRaw(instance, **kwargs)
948        references_to_add=[]
949        references_to_remove=[]       
950        ### The normal case: widget gives a dictionary ###   
951        if isinstance(value, dict): 
952            edited=value['edited']
953            new_values=copy(old_values)
954
955            if edited!=-1: # one chapter has been edited
956                ctype=value['chapter_type']
957                old_chapter=old_values[edited]           
958                # build _one_ chapter based on input
959                new_chapter={'type':ctype}
960                if ctype in ['multiple_choices','choices','poll']:
961                    new_chapter['text']=value['question']
962                    choices=[]
963                    for answer in value['answers']:
964                        choices.append((answer['order'], answer['text'], answer['is_correct']))
965                    choices.sort()
966                    choices=[(y,z) for (x,y,z) in choices]
967                    new_chapter['choices']=choices
968                elif ctype=='pilot_scene':
969                    new_chapter['text']=u'\n'.join(value['keywords'] or value['questions'])
970                    if value['audio_file']:
971                        new_piece = lt.createPieceFromFile(value['audio_file'], instance)
972                        uid=new_piece.UID()
973                    else:
974                        uid=value['audio_uid']
975                    old_uid=old_chapter.get('audio_uid','')
976                    if old_uid:
977                        if old_uid!=uid:
978                            references_to_remove.append(old_uid)
979                            references_to_add.append(uid)
980                    else:
981                        references_to_add.append(uid)
982                    new_chapter['audio_uid']=uid               
983                    if value['image_file']:
984                        new_piece = lt.createPieceFromFile(value['image_file'], instance)
985                        uid=new_piece.UID()
986                    else:
987                        uid=value['image_uid']
988                    old_uid=old_chapter.get('image_uid','')
989                    if old_uid:
990                        if old_uid!=uid:
991                            references_to_remove.append(old_uid)
992                            references_to_add.append(uid)
993                    else:
994                        references_to_add.append(uid)
995                    new_chapter['image_uid']=uid
996                    new_chapter['questions']=value['questions']
997                    new_chapter['keywords']=value['keywords']
998                elif ctype in ['image_piece','audio_piece','media_piece']:
999                    file=value.get('file','')
1000                    new_chapter['text']=''
1001                    if file:
1002                        new_piece = lt.createPieceFromFile(file, instance)
1003                        uid=new_piece.UID()
1004                    else:
1005                        uid=value['uid']
1006                    old_uid=old_chapter.get('uid','')
1007                    if old_uid:
1008                        if old_uid!=uid:
1009                            references_to_remove.append(old_uid)
1010                            if uid:
1011                                references_to_add.append(uid)
1012                    elif uid:
1013                        references_to_add.append(uid)
1014                    new_chapter['uid']=uid
1015                elif ctype == 'embed_block':
1016                    new_chapter['text']=''
1017                    new_chapter['embed']=value['embed']
1018                else:           
1019                    new_chapter['text']=value['text']
1020                if new_chapter!=old_chapter:
1021                    changed=True
1022                new_values[edited]=new_chapter
1023            ## Deleting ##
1024            deleted=value.get('deleted',[])
1025            n_o=value.get('new_order',[])
1026            if deleted:
1027                vals=[]
1028                for i, val in enumerate(new_values):
1029                    if i in deleted:
1030                        uid=val.get('uid','')
1031                        if uid:
1032                            references_to_remove.append(uid)
1033                    else:
1034                        vals.append(val)
1035                new_values=vals
1036                changed=True
1037                if n_o:
1038                    n_o=[val for i, val in enumerate(n_o) if i not in deleted]           
1039            ## Sorting ##
1040            if n_o:
1041                sortable=zip(n_o, new_values)
1042                sortable.sort()
1043                new_values=[val for o, val in sortable]
1044                changed=True                       
1045            ## Adding new chapters ##
1046            new_chapters=value.get('new_chapters',[])
1047            if new_chapters:
1048                new_values+=new_chapters
1049                changed=True
1050        ### The case where field's get-method feeds field's set-method (archetype update, copying)
1051        elif isinstance(value, list):
1052            references_new=[]
1053            for chapter in value:
1054                uid=chapter.get('uid','')
1055                if uid:
1056                    references_new.append(uid)
1057            references_old=[]
1058            for chapter in old_values:
1059                uid=chapter.get('uid','')
1060                if uid:
1061                    references_old.append(uid)
1062            references_to_add=[uid for uid in references_new if uid not in references_old]
1063            references_to_remove=[uid for uid in references_old if uid not in references_new]
1064            if value!=old_values:
1065                changed=True
1066            new_values=value
1067        ### update references if necessary
1068        if references_to_add or references_to_remove:
1069            tool = instance.reference_catalog
1070            for uid in references_to_add:
1071                tool.addReference(instance, uid, self.relationship)
1072            for uid in references_to_remove:
1073                tool.deleteReference(instance, uid, self.relationship)
1074        if changed:
1075            # update cleaned text
1076            self.updateCleanedChapters(instance, new_values)       
1077            # set new value
1078            ObjectField.set(self, instance, new_values, **kwargs)
1079
1080    def getObjectByUID(self, instance, uid, catalog_only=False):
1081        """ Helper method for fetching mediapieces from references """
1082        uid_catalog = getToolByName(instance, 'uid_catalog')
1083        if not uid:
1084            return None # this is just a newly created placeholder
1085        results=uid_catalog(UID=uid)
1086        try:
1087            return results[0].getObject()
1088        except IndexError:
1089            return None
1090
1091    def getLength(self, piece):
1092        as_time=piece.getLength()
1093        return {'hour':as_time/3600,'minute':(as_time % 3600)/60,'second':as_time % 60}           
1094
1095registerField(ChapterField,
1096    title='Chapter field',
1097    description=('Chapter field'),
1098)
1099
1100class AudioField(FileField):
1101    """ field for mp3:s """
1102    _properties = FileField._properties.copy()
1103    _properties.update({
1104        'widget' : AudioWidget,
1105    })
1106
1107    def getLength(self, instance):
1108        i = aq_base(instance)
1109        file = AudioField.get(self,instance)
1110        if not shasattr(i, '_mp3length'):
1111            instance._mp3length=get_length(str(file))
1112        if instance._mp3length==0:
1113            instance._mp3length=get_length(str(file))
1114        return {'hour':instance._mp3length/3600,'minute':(instance._mp3length % 3600)/60,'second':instance._mp3length % 60}
1115
1116    def set(self, instance, value, **kwargs):
1117        """ allow only audio here """
1118        FileField.set(self, instance, value, **kwargs)
1119        file=self.get(instance)
1120        if self.getContentType(instance).startswith('audio/'):
1121            instance._mp3length=get_length(str(file))
1122        elif str(file)!='':
1123            FileField.set(self, instance, "DELETE_FILE", **kwargs)
1124
1125registerField(AudioField,
1126    title='Audio field',
1127    description=('Field that accepts only mp3:s'),
1128)
1129
1130class LeMillLinksField(ObjectField):
1131    """ A field that stores LeMill internal links """
1132    __implements__ = ObjectField.__implements__
1133    _properties = ObjectField._properties.copy()
1134    _properties.update({
1135        'widget' : LeMillLinksWidget,
1136    })
1137
1138    security = ClassSecurityInfo()
1139
1140    def set(self, instance, value, **kwargs):
1141        ObjectField.set(self, instance, value, **kwargs)
1142
1143    def getRaw(self, instance, **kwargs):
1144        version = None
1145        try:
1146            version = instance.REQUEST.get('version', None)
1147        except AttributeError:
1148            pass
1149        if version:
1150            value = instance.getFieldHistory(self.getName(), version)
1151        else:
1152            value = ObjectField.get(self, instance, **kwargs)
1153        return value
1154
1155    def getRawEdit(self, instance, **kwargs):
1156        """ Returns a text for editing """
1157        pc = getToolByName(instance, 'portal_catalog')
1158        field_data = ObjectField.get(self, instance, **kwargs)#XXX See if any version control is needed here
1159        if field_data == None:
1160            return ""
1161        # find uids and links - replace them with urls
1162        links = re.findall('\{.*?\}', field_data)
1163        for link in links:
1164            linktext = link[1:-1]
1165            if linktext.lower().startswith('http://') or linktext.lower().startswith('https://'):
1166                field_data = field_data.replace(link, linktext)
1167            elif linktext.lower().startswith('uid:'):
1168                uid = linktext[4:]
1169                results = pc({'UID':uid})
1170                if len(results)>0:
1171                    field_data = field_data.replace(link, results[0].getURL())
1172                else:
1173                    field_data = field_data.replace(link, '')
1174        return field_data
1175
1176    def get(self, instance, **kwargs):
1177        pc = getToolByName(instance, 'portal_catalog')
1178        letool = getToolByName(instance,'lemill_tool')
1179        version = None
1180        try:
1181            version = instance.REQUEST.get('version',None)
1182        except AttributeError:
1183            pass
1184        if version:
1185            field_data = instance.getFieldHistory(self.getName(), version)
1186        else:
1187            field_data = ObjectField.get(self, instance, **kwargs)
1188        if not field_data:
1189            return ""
1190        links = re.findall('\{.*?\}', field_data)
1191        for link in links:
1192            linktext = link[1:-1]
1193            if linktext.lower().startswith('http://') or linktext.lower().startswith('https://'):
1194                repl='<a href="%s">%s</a>' % (linktext, letool.shorten_url(linktext))
1195                field_data = field_data.replace(link, repl)
1196            elif linktext.lower().startswith('uid:'):
1197                uid = linktext[4:]
1198                results = pc({'UID':uid})
1199                repl=''
1200                if results:
1201                    repl='<a href="%s">%s</a>' % (results[0].getURL(), results[0].Title)
1202                field_data = field_data.replace(link, repl)
1203        field_data = field_data.strip()
1204        value = field_data.split('\n')
1205        return value
1206
1207registerField(LeMillLinksField,
1208    title='LeMill Links Field',
1209    description=('LeMill Links Field'),
1210)
Note: See TracBrowser for help on using the repository browser.