source: trunk/FieldsWidgets.py @ 3098

Revision 3098, 48.3 KB checked in by jukka, 9 years ago (diff)

Fixed bugs found from error_log, mostly to do about strange pieces.

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