root/trunk/FieldsWidgets.py

Revision 3196, 60.5 kB (checked in by jukka, 1 week ago)

Fixed #2036, fixed #2037, fixed #2035. Input field for points earned from exercise.

  • 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
19 from AccessControl import ClassSecurityInfo
20 from Products.Archetypes.config import REFERENCE_CATALOG
21 from Products.Archetypes.Registry import registerField, registerWidget
22 from Products.Archetypes.Field import StringField, LinesField, ReferenceField, ObjectField, FileField
23 from Products.Archetypes.Widget import TextAreaWidget, StringWidget, SelectionWidget, TypesWidget, MultiSelectionWidget, VisualWidget, FileWidget
24 from Products.Archetypes.ReferenceEngine import Reference
25 from config import to_unicode
26 from Products.Archetypes import config
27 from string import letters, punctuation
28 from Products.CMFCore.utils import getToolByName
29 from ZODB.PersistentMapping import PersistentMapping
30 from Products.Archetypes.utils import shasattr
31 from Acquisition import aq_base
32 from mp3tool import get_length
33 from types import ListType, TupleType, StringType, UnicodeType, InstanceType
34 from messagefactory_ import i18nme as _
35 from random import shuffle
36 from copy import copy
37 from xml.dom.minidom import parseString as parse_xml
38 from urlparse import urlparse
39 from xml.sax.saxutils import unescape
40 import 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
45 class 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
65 registerWidget(LeVisualWidget,
66     title='Visual Widget',
67     description='Visual Widget'
68 )   
69
70 class LeStringWidget(StringWidget):
71     _properties = StringWidget._properties.copy()
72     _properties.update({
73         'macro' : 'widget_string'
74     })
75
76 registerWidget(LeStringWidget,
77     title='String Widget',
78     description='String Widget'
79 )
80
81
82 class LeTextAreaWidget(TextAreaWidget):
83     _properties = TextAreaWidget._properties.copy()
84     _properties.update({
85         'macro' : 'widget_textarea'
86     })
87
88 registerWidget(LeTextAreaWidget,
89     title='Textarea Widget',
90     description='Textarea Widget'
91 )
92
93 class TagsWidget(StringWidget):
94     _properties = StringWidget._properties.copy()
95     _properties.update({
96         'macro' : 'widget_tags',
97     })
98    
99 registerWidget(TagsWidget,
100     title='Tags Widget',
101     description='Tags Widget',
102     used_for=('Products.LeMill.TagsField.TagsField',)
103 )
104
105 class 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    
128 registerWidget(HTMLLinkWidget,
129     title='HTML Link Widget',
130     description='Generated <a> tag Widget',
131     used_for=('Products.LeMill.FieldsWidgets.HTMLLinkWidget',)
132 )
133
134 class AuthorsWidget(StringWidget):
135     _properties = StringWidget._properties.copy()
136     _properties.update({
137         'macro' : 'widget_authors',
138     })
139
140     def process_form(self, instance, field, form, empty_marker=None, emptyReturnsMarker=False, validating=True):
141         """ Check if the field should use computed values or custom entry. If custom entry, then create the list by replacing
142             author names with their ids if possible.
143             
144             Since the field cannot store extra info about if its custom or computed, first member of the field is '*!*'
145             if the value is customized. This must be removed when displaying field values. 
146             
147             !!! stop !!! what to do with searches and comparisons, if there is *!* messing things up?
148             
149         """
150
151         name=field.getName()
152         is_computed=form.get(name+'_are_computed','')=='computed'
153         computed_authors= instance.recalculateAuthors(set_field=False, force=True)
154         if is_computed:
155             authors_list=computed_authors
156         else:
157             value_string=form.get(name+'_custom','')
158             names=value_string.split(',')
159             names=[n.strip() for n in names]
160             if names:
161                 utool=getToolByName(instance, 'lemill_usertool')
162                 # find matching users for names
163                 authors_list=['*!*']
164                 for name in names:
165                     user_id=utool.getMemberByFullName(name, return_id=True, candidates=computed_authors) or name
166                     authors_list.append(user_id)
167             else:
168                 authors_list=instance.recalculateAuthors(set_field=False, force=True)
169         return authors_list, {}               
170
171     def displayString(self, instance, values, as_links=False):
172         """ Linearize list of ids to string of user names and omit custom marker """
173         if not values:
174             return values
175         utool=getToolByName(instance, 'lemill_usertool')
176         if values[0]=='*!*':
177             values=values[1:] 
178         names=[utool.linkTo(user_id, disabled=not as_links) for user_id in values]
179         return ", ".join(names)
180            
181     def getComputedAuthors(self,instance,as_string=True, include_user=False):
182         """ Get updated authors list to show as an option when editing authors field """
183         authors=instance.recalculateAuthors(set_field=False)
184         if include_user:
185             utool=getToolByName(instance, 'lemill_usertool')
186             user_id=utool.getAuthenticatedId()
187             if user_id not in authors:
188                 authors.append(user_id)           
189         if as_string:
190             return self.displayString(instance, authors, as_links=False)
191         else:
192             return authors
193
194            
195 registerWidget(AuthorsWidget,
196     title='Authors Widget',
197     description='Authors Widget',
198     used_for=('Products.Archetypes.Field.LinesField',)
199 )
200
201
202
203
204 class MessengerWidget(TypesWidget):
205     _properties = TypesWidget._properties.copy()
206     _properties.update({
207         'macro' : 'widget_messenger',
208     },)
209
210     security = ClassSecurityInfo()
211
212     security.declarePublic('process_form')
213     def process_form(self, instance, field, form, empty_marker=None,
214                      emptyReturnsMarker=False, validating=True):
215         """Concatenates address + messenger type, but if address already contains something like prefix (word:) then removes it.
216         """
217         name = field.getName()
218         otherName = "%s_other" % name
219         value = form.get(name, empty_marker)
220         othervalue = form.get(otherName, empty_marker)
221         if not value:
222             return '', {}
223         if not othervalue == empty_marker:
224             value = "%s:%s" % (othervalue,value)
225         return value, {}
226
227
228 registerWidget(MessengerWidget,
229     title='Messenger Widget',
230     description='Messenger Widget',
231     used_for=('Products.Archetypes.Field.StringField',)
232 )
233
234
235 class MobileWidget(StringWidget):
236     _properties = StringWidget._properties.copy()
237     _properties.update({
238         'macro' : 'widget_mobile',
239     })
240
241     security = ClassSecurityInfo()
242
243     security.declarePublic('process_form')
244     def process_form(self, instance, field, form, empty_marker=None,
245                      emptyReturnsMarker=False, validating=True):
246         name = field.getName()
247         otherName = "%s_checkbox" % name
248         value = form.get(name, empty_marker)
249         othervalue = form.get(otherName, empty_marker)
250         if value == empty_marker and emptyReturnsMarker:
251             return empty_marker
252         if othervalue=="1":
253             value = "*SMS*%s" % value
254         return value, {}
255
256    
257 registerWidget(MobileWidget,
258     title='Mobile Widget',
259     description='Mobile Widget',
260     used_for=('Products.Archetypes.Field.StringField',)
261 )
262
263
264 class CopyrightWidget(SelectionWidget):
265     _properties = SelectionWidget._properties.copy()
266     _properties.update({
267         'macro' : 'widget_copyright',
268     })
269    
270 registerWidget(CopyrightWidget,
271     title='Copyright Widget',
272     description='Copyright selection Widget',
273     used_for=('Products.LeMill.StringField.StringField',)
274 )
275
276 class TwoColumnMultiSelectionWidget(MultiSelectionWidget):
277     _properties = MultiSelectionWidget._properties.copy()
278     _properties.update({
279         'macro' : 'widget_twocolumn',
280     })
281    
282 registerWidget(TwoColumnMultiSelectionWidget,
283     title='Two column widget',
284     description='Two column multiselection widget',
285     used_for=('Products.Archetypes.Field.MultiSelectionField',)
286 )
287
288
289 class GroupWidget(SelectionWidget):
290     _properties = SelectionWidget._properties.copy()
291     _properties.update({
292         'macro' : 'widget_group',
293         'format' : 'checkbox',
294     })
295     def getGroupVocabulary(self, instance):
296         """ this builds vocabulary tuples for groups field:
297         [(group Title, group UID, is_checked, is_disabled), ... ]
298          """
299         lutool = getToolByName(instance, 'lemill_usertool')
300         mf = lutool.getMemberFolder()
301         user_groups=[]
302         if mf:
303             user_groups=mf.getGroups()
304         resource_groups=instance.getGroupInfo()       
305         resource_uids=[g.UID for g in resource_groups]
306         user_uids=[g.UID for g in user_groups]
307         disabled=[(g.Title.lower(), g.Title, g.UID, True, True) for g in resource_groups if (g.UID not in user_uids) and isinstance(g.Title, StringType)]
308         disabled.sort()
309         other=[(g.Title.lower(), g.Title, g.UID, g.UID in resource_uids, False) for g in user_groups if isinstance(g.Title, StringType)]
310         other.sort()
311         groups=[(g[1],g[2],g[3],g[4]) for g in disabled+other]
312         return groups+[('...or create a new group:','__new_group',False,False)]
313        
314     def process_form(self, instance, field, form, empty_marker=[],
315                      emptyReturnsMarker=False, validating=True):
316         """Basic impl for form processing in a widget"""
317         value = form.get(field.getName(), empty_marker)
318         if value is empty_marker:
319             return empty_marker
320         if emptyReturnsMarker and value == '':
321             return empty_marker
322         if '__new_group' in value:
323             new_group_name = form.get('new_group_name', '')
324             if new_group_name:
325                 # create a new group here
326                 blog=instance.community.groups.addGroup(new_group_name)
327                 value.remove('__new_group')
328                 value.append(blog.UID())
329         return value, {}
330
331 class TranslationWidget(SelectionWidget):
332     _properties = SelectionWidget._properties.copy()
333     _properties.update({
334         'macro' : 'widget_translation',
335     })
336     def getTranslationsData(self, instance):
337         """ Get {uid, url, title, language, pretty_language} for  translations of this instance """
338         pc=getToolByName(instance, 'portal_catalog')
339         ltool=getToolByName(instance, 'lemill_tool')
340         my_uid=instance.UID()
341         md_results=pc(getOther_languages=my_uid, getState=['public','draft'])
342         results=[]
343         for md in self.fastMetadata(md_results, ['getUID','rid','getNicename','Language']):
344             # try
345             data={'uid':md[0], 'url':pc.getUrlPath(md[1]), 'title':md[2], 'lang':md[3], 'pretty_lang':ltool.getPrettyLanguage(md[3])}
346             results.append(data)
347             # except:
348             # pass
349         results.sort(key=lambda x: x['lang'])
350         return results
351
352     def process_form(self, instance, field, form, empty_marker=[],
353                      emptyReturnsMarker=False, validating=True):
354         """Basic impl for form processing in a widget"""
355         value=True
356         uids=[]
357         index=0
358         while value:
359             value=form.get('%s_%s' % (field.getName(), index), '')
360             if value:
361                 uids.append(value)
362             index+=1
363         return value, {}
364
365 class ChapterWidget(SelectionWidget):
366     _properties = SelectionWidget._properties.copy()
367     _properties.update({
368         'macro' : 'widget_chapter',
369         'collections_only' : False,
370         'helper_js' : ('AC_RunActiveContent.js',),
371         'show_added' : True,
372         'no_label' : True
373     })
374     security = ClassSecurityInfo()
375
376     security.declarePublic('process_form')
377     def process_form(self, instance, field, form, empty_marker=None,
378             emptyReturnsMarker=False, validating=True):
379         """ goes through the form, reorganizes chapters if necessary and delivers results to field writer (as dict). """
380         chapter_type=form.get('chapter_type')
381         if not chapter_type:
382             return {},{}
383         changes = {}
384         changes['edited']=int(form.get('chapter_last_edited', 0))
385         count=int(form.get('chapter_count',0))
386         changes['count']=count
387         new_order=[]
388         reorder=False
389         # Orderings and deletions
390         
391         for index in range(0,count):
392             order=int(form.get('chapter_order_%s' % index, 0))
393             new_order.append(order)
394             if order!=index:
395                 reorder=True
396         if reorder:
397             changes['new_order']=new_order
398         if form.has_key('delete_chapter'):
399             changes['delete_chapter']=int(form.get('delete_chapter'))
400         # Changes
401         changes['chapter_type']=chapter_type
402         if chapter_type=='media_piece':
403             changes['uid']=form.get('piece_uid')
404             changes['caption']=form.get('caption','')
405         elif chapter_type=='text_block':
406             changes['text']=form.get('chapter_textarea')
407         elif chapter_type=='embed_block':
408             changes['text']=''
409             changes['embed']=form.get('embed_field','')
410             changes['embed_old']=form.get('embed_field_old', '')
411         # Save & edit buttons
412         if form.get('saveChapter', None):
413             form.update({'chapter_edited':-1, 'chapter_anchor':changes['edited'], 'stay':1})       
414         elif form.get('editChapter', None):
415             form.update({'chapter_anchor':form['chapter_edited'], 'stay':1})       
416         # Add-buttons
417         elif form.get('add_text_block', None):
418             changes['new_chapters']=[{'text':'','type':'text_block'}]
419             form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
420         elif form.get('add_media_piece', None):
421             changes['new_chapters']=[{'text':'','type':'media_piece','uid':''}]
422             form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
423         elif form.get('add_embed_block', None):
424             changes['new_chapters']=[{'text':'','type':'embed_block', 'embed':''}]
425             form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
426         return changes, {}
427
428 registerWidget(ChapterWidget,
429     title='Chapter Widget',
430     description='Chapter editor Widget',
431     used_for=('Products.LeMill.ChapterField',)
432 )
433
434 class SlideWidget(ChapterWidget):
435     _properties = ChapterWidget._properties.copy()
436     _properties.update({
437         'macro' : 'widget_slides',
438         'collections_only' : False,
439         'show_added' : True,
440         'chapter_count' : 5,
441     })
442
443 registerWidget(SlideWidget,
444     title='Slide Widget',
445     description='Slide and caption viewer Widget',
446     used_for=('Products.LeMill.ChapterField',)
447 )
448
449 class PilotWidget(ChapterWidget):
450     _properties = ChapterWidget._properties.copy()
451     _properties.update({
452         'macro' : 'widget_pilot',
453         'collections_only' : False,
454         'helper_js' : ('AC_RunActiveContent.js',),
455         'show_added' : True,
456         'chapter_count' : 3,
457     })
458     security = ClassSecurityInfo()
459
460     security.declarePublic('process_form')
461     def process_form(self, instance, field, form, empty_marker=None,
462             emptyReturnsMarker=False, validating=True):
463         """ goes through the form and sends changes forward to field writer """
464         changes, empty= ChapterWidget.process_form(self, instance, field, form)
465         if not changes:
466             return changes,empty
467         chapter_type=changes['chapter_type']
468         if chapter_type=='pilot_scene':
469             audio_file=form.get('audio_file', None)
470             if audio_file and hasattr(audio_file, 'filename'):
471                 changes['audio_file']=audio_file
472             else:
473                 changes['audio_uid']=form.get('audio_uid')
474             image_file=form.get('image_file', None)
475             if image_file and hasattr(image_file, 'filename'):
476                 changes['image_file']=image_file
477             else:
478                 changes['image_uid']=form.get('image_uid')
479             kws=[]
480             for i in range(0, 3):
481                 kw=form.get('keywords_%s' % i, '')
482                 if kw:
483                     kws.append(kw)
484             if kws:
485                 changes['keywords']=kws
486             qs=[]
487             for i in range(0, 7):
488                 q=form.get('question_%s' % i, '')
489                 if q:
490                     qs.append(q)
491             if qs:
492                 changes['questions']=qs
493         # Buttons
494         if form.get('add_pilot_section', None):
495             changes['new_chapters']={'text':'','type':'pilot_scene','audio_uid':'', 'image_uid':'', 'keywords':[], 'questions':[]},
496             form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
497         return changes, {}
498
499 registerWidget(PilotWidget,
500     title='Pilot Widget',
501     description='Scene editor Widget',
502     used_for=('Products.LeMill.ChapterField',)
503 )
504
505 class ExerciseWidget(ChapterWidget):
506     _properties = ChapterWidget._properties.copy()
507     _properties.update({
508         'macro' : 'widget_exercise',
509         'collections_only' : False,
510         'helper_js' : ('AC_RunActiveContent.js',),
511         'show_added' : True,
512         'chapter_count' : 1,
513     })
514     security = ClassSecurityInfo()
515
516     security.declarePublic('process_form')
517     def process_form(self, instance, field, form, empty_marker=None,
518             emptyReturnsMarker=False, validating=True):
519         """ 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. """
520         changes, empty= ChapterWidget.process_form(self, instance, field, form)
521         if not changes:
522             return changes,empty
523         chapter_type=changes['chapter_type']               
524         if chapter_type=='guidelines':
525             # this should be deprecated, but I keep this still here because I cannot now think if there
526             # are any bad consequences from removing it. Too tired.
527             changes['text']=form.get('chapter')
528         elif chapter_type=='exercise':
529             # exercises are question chapters that don't have yet chosen any specific type for themselves. They should be empty.
530             changes['text']=''           
531         elif chapter_type in ['choice','multiple_choices','poll']:
532             question=form.get('choice_question')
533             answers=[]
534             answer_count=int(form.get('choice_answer_count',0))
535             for i in range(0, answer_count):
536                 answer={}
537                 answer['text']=form.get('choice_answer_%s' % i, '')
538                 answer['is_correct']= chapter_type=='poll' or int(form.get('answer_%s_correct' % i, ''))
539                 answer['order']=int(form.get('choice_order_%s' % i, 0))
540                 if answer['text']:  # empty answers are deleted by ignoring them
541                     answers.append(answer)
542             answers.sort(cmp=lambda x,y: cmp(x['order'], y['order']))
543             changes['question']=question
544             changes['answers']=answers or [{'text':'','is_correct':1,'order':0}]
545             try:
546                 max_points=float(form.get('max_points_mc', 1))
547             except ValueError:
548                 max_points=1
549             changes['max_points']=max_points
550         elif chapter_type=='fill_in_the_blanks':
551             changes['text']=form.get('fill_blanks','')
552             try:
553                 max_points=float(form.get('max_points_fib', 1))
554             except ValueError:
555                 max_points=1
556             changes['max_points']=max_points
557         elif chapter_type=='open_ended':
558             changes['text']=form.get('open_field','')
559             try:
560                 max_points=float(form.get('max_points_ff', 1))
561             except ValueError:
562                 max_points=1
563             changes['max_points']=max_points
564
565         # Buttons
566         if form.get('add_exercise', None):
567             changes['new_chapters']=[{'text':'','type':'exercise'}]
568             count=changes['count']
569             form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
570         elif form.get('import_potatoes', None):
571             file=form.get('import_file', None)
572             if file and hasattr(file, 'filename'):
573                 if file.filename.endswith('jcl'):
574                     text=self.importJClozeQuestion(file)
575                     changes['chapter_type']='fill_in_the_blanks'
576                     changes['text']=text
577                     form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
578                 elif file.filename.endswith('jcz'):
579                     new_chapters=self.importJQuizQuestions(file)
580                     if new_chapters:
581                         this_chapter= new_chapters.pop(0)
582                         changes['chapter_type']=this_chapter['type']
583                         changes['answers']=this_chapter['answers']
584                         changes['question']=this_chapter['text']
585                         if new_chapters:
586                             changes['new_chapters']=new_chapters
587                             form.update({'chapter_edited':count, 'chapter_anchor':count, 'stay':1})
588         return changes, {}
589    
590     def importJQuizQuestions(self, file):
591         """ import hot potatoes questions made with jquiz """       
592         dom = parse_xml(file.read())
593         questions = dom.getElementsByTagName("question-record")
594         result=[]
595         for ques in questions:
596             questype = int(ques.getElementsByTagName("question-type")[0].childNodes[0].data)
597             if not (questype==1 or questype==4):
598                 return           
599             question = ques.getElementsByTagName("question")[0].childNodes[0].data.encode('utf-8')
600             # if jquiz question type is multiple choice(1) or multiple correct(4)
601             answers = ques.getElementsByTagName("answer")
602             # calculate how many correct answers (is it choice or choice_multiple type)
603             corrects=0
604             made_answers=[]
605             for answer in answers:
606                 correct = int(answer.getElementsByTagName("correct")[0].childNodes[0].data) # 1-correct 0-false
607                 if correct:
608                     corrects+=1
609                 answertext = answer.getElementsByTagName("text")[0].childNodes[0].data.encode('utf-8')
610                 if answertext:
611                     made_answers.append({'text':answertext, 'is_correct':correct})
612             if corrects==len(result):
613                 qtype='poll'
614             elif corrects==1:
615                 qtype='choice'
616             else:
617                 qtype='multiple_choices'
618             result.append({'type':qtype,'text':question,'answers':made_answers})       
619         dom.unlink()
620         return result
621    
622     def importJClozeQuestion(self, file):
623         """ import hot potatoes question made with JCloze """       
624         dom = parse_xml(file.read())
625         gap_fill = dom.getElementsByTagName("gap-fill")[0]
626         result=[]
627         for node in gap_fill.childNodes:
628             if node.nodeType==node.TEXT_NODE:
629                 result.append(node.data)
630             else:
631                 for answer in node.getElementsByTagName("answer"):
632                     text_tag=answer.getElementsByTagName("text")[0]
633                     text=text_tag.childNodes[0].data
634                     result.append('{%s}' % text)
635         result=u''.join(result)
636         #result=unescape(u''.join(result), {'&#x003C;':'<','&#x003E;':'>'})
637         dom.unlink()
638         return result
639
640 registerWidget(ExerciseWidget,
641     title='Exercise Widget',
642     description='Exercise editor Widget',
643     used_for=('Products.LeMill.ChapterField',)
644 )
645
646 class AudioWidget(FileWidget):
647     _properties = FileWidget._properties.copy()
648     _properties.update({
649     'macro' : 'widget_audio',
650     'helper_js' : ('AC_RunActiveContent.js',),
651     })
652
653 registerWidget(AudioWidget,
654     title='Audio Widget',
655     description='Widget that uses flashplayer for mp3:s',
656     used_for=('Products.LeMill.ChapterField',)
657 )
658
659 class LeMillLinksWidget(LeVisualWidget):
660     _properties = LeVisualWidget._properties.copy()
661     _properties.update({
662             'macro' : 'widget_lemilllinks',
663     })
664        
665     def process_form(self, instance, field, form, empty_marker=None,
666                      emptyReturnsMarker=False, validating=True):
667         """ Replaces internal links with {uid:UID} and external links with {url} """
668         fieldname = field.getName()
669         value = form.get(fieldname, empty_marker)
670        
671         if value.strip() == '':
672             return None, {}
673            
674         portal_url = getToolByName(instance, 'portal_url')
675         portal_url = portal_url()
676         pc = getToolByName(instance, 'portal_catalog')
677         from LargeSectionFolder import DEFAULT_LOCATIONS
678         from config import CONTENT_TYPES, ACTIVITY_TYPES, TOOLS_TYPES
679         suitable_types = CONTENT_TYPES + ACTIVITY_TYPES + TOOLS_TYPES
680         locations = {}
681         # Only some locations are needed
682         for d in DEFAULT_LOCATIONS.keys():
683             if d in suitable_types:
684                 locations[d] = DEFAULT_LOCATIONS[d]
685         # array of inserted data
686         rows = value.split('\n'
687
688         data = ""
689         for row in rows:
690             r = row
691             row = row.strip()
692             words = []
693             if row != '':
694                 words = row.split(' ')
695             for word in words:
696                 index = words.index(word)
697                 # control is it link to LeMill resource
698                 if (word.lower().startswith(portal_url)):
699                     isMaterial = False
700                     for location, url in locations.items():
701                         if url in word:
702                             # Now the type is known, getting the id
703                             temp = word.split(url)
704                             temp = temp[1]
705                             temp = temp.split('/')
706                             id = temp[1]
707                             # Now to make a catalog search
708                             query = {'getId':id,'meta_type':location}
709                             results = pc(query)
710                             if len(results)>0:
711                                 words[index] = '{uid:'+results[0].UID+'}'
712                                 #value = value.replace(word,'{uid:'+results[0].UID+'}')
713                                 isMaterial = True
714                     # if we dont find LeMill resource, then treat this url as any other link           
715                     if not isMaterial:
716                         words[index] = '{'+word+'}'
717                 # if it is external link
718                 elif (word.lower().startswith("http://") or word.lower().startswith("https://")):
719                     words[index] = '{'+word+'}'
720             if words != []:
721                 data += ' '.join(words) + '\n'
722         return data, {}
723
724 registerWidget(LeMillLinksWidget,
725     title='LeMill Links Widget',
726     description='LeMill Links Widget',
727     used_for=('Products.LeMill.FieldsWidgets.LeMillLinksField',)
728 )
729
730
731 class EmbedWidget(StringWidget):
732     _properties = StringWidget._properties.copy()
733     _properties.update({
734         'macro' : 'widget_embed'
735     })
736
737 registerWidget(EmbedWidget,
738     title='Embed Widget',
739     description='Embed Widget, not modifiable by common means, only displays embed code'
740 )
741
742
743 ####                            ####
744 ####            FIELDS          ####
745 ####                            ####
746
747
748 class TranslationField(LinesField):
749     """ A field that stores tags """
750     __implements__ = LinesField.__implements__
751     _properties = LinesField._properties.copy()
752     _properties.update({
753         'widget' : TranslationWidget,
754     })
755
756     security = ClassSecurityInfo()
757
758     def setDisabled(self, instance, value, **kwargs):
759         value=list(value)
760         old_value=list(self.get(instance))
761         if value and value==old_value:
762             return
763         LinesField.set(self, instance, value, **kwargs)
764         if kwargs.get('asymmetric', False) or True: # Let's disable this for a moment
765             return
766         # Make sure that changes here are reflected to other translations too
767         uc=getToolByName(instance, 'uid_catalog')
768         my_uid=instance.UID()
769         # what has been removed?
770         removed=set(old_value)-set(value)
771         for md in uc(UID=list(removed)):
772             obj=md.getObject()
773             field=obj.getField(self.getName())
774             field.remove(my_uid)
775         # what has been added?
776         added=set(value)-set(old_value)
777         for md in uc(UID=list(added)):
778             obj=md.getObject()
779             field=obj.getField(self.getName())
780             field.add(my_uid)
781
782     def add(self, instance, items):         
783         """ local helper method, doesn't traverse to linked objects """
784         items=list(items)
785         old_value=list(self.get(instance))
786         value=list(old_value)     
787         for item in items:
788             if item not in value:
789                 value.append(item)
790         if value!=old_value:
791             LinesField.set(self, instance, value)
792        
793     def remove(self, instance, items):         
794         """ local helper method, doesn't traverse to linked objects """
795         items=list(items)
796         old_value=list(self.get(instance))
797         value=list(old_value)     
798         for item in items:
799             if item in value:
800                 value.remove(item)
801         if value!=old_value:
802             LinesField.set(self, instance, value)
803
804     security.declarePrivate('get')
805     def get(self, instance, **kwargs):
806         val=LinesField.get(self, instance, **kwargs)
807         if callable(val):
808             val=val()
809         return val
810
811     security.declarePrivate('getRaw')
812     def getRaw(self, instance, **kwargs):
813         val=LinesField.getRaw(self, instance, **kwargs)
814         if val and callable(val):
815             val=val()
816         return val
817
818                        
819 registerField(TranslationField,
820                title='Translation Field',
821                description=('Translation Field'),
822                )
823
824
825
826 class TagsField(LinesField):
827     """ A field that stores tags """
828     __implements__ = LinesField.__implements__
829     _properties = LinesField._properties.copy()
830     _properties.update({
831         'widget' : TagsWidget,
832     })
833
834     security = ClassSecurityInfo()
835
836     def set(self, instance, value, **kwargs):
837         if not value:
838             value=''
839         if isinstance(value,str):
840             value = value.lower()
841             value = value.split(',')
842         if isinstance(value, tuple) or isinstance(value, list):
843             value=[t.strip().lower() for t in value] # strip and clean individual tags
844             value=list(set(value)) # remove duplicates
845             value= '\n'.join(value) # merge back to string
846         LinesField.set(self, instance, value, **kwargs)
847                        
848     def getRaw(self, instance, **kwargs):
849         version = None
850         try:
851             version = instance.REQUEST.get('version',None)
852         except AttributeError:
853             pass
854         if version:
855             value = instance.getFieldHistory(self.getName(), version)
856             if value and callable(value):
857                 value=value()
858             value = value.split(',')
859         else:
860             value = LinesField.get(self, instance, **kwargs)
861             if value and callable(value):
862                 value=value()
863         return ', '.join(value)
864
865     def get(self, instance, **kwargs):
866         version = None
867         try:
868             version = instance.REQUEST.get('version', None)
869         except AttributeError:
870             pass
871         if version:
872             value = instance.getFieldHistory(self.getName(), version) or ''
873             if value and callable(value):
874                 value=value()
875             if value:
876                 value = value.split(',')             
877         else:
878             value = LinesField.get(self, instance, **kwargs)
879             if value and callable(value):
880                 value=value()
881         return value
882
883 registerField(TagsField,
884                title='Tags Field',
885                description=('Tags Field'),
886                )
887
888 class WYSIWYMField(StringField):
889     """A field that stores WYSIWYM strings"""
890     _properties = StringField._properties.copy()
891     _properties.update({
892         'widget' : LeTextAreaWidget,
893         'default_output_type' : 'text/x-html-captioned',
894         'default_content_type' : 'text/html',
895     })   
896    
897     security = ClassSecurityInfo()
898    
899    
900     def getRaw(self, instance, **kwargs):
901         """ Gets raw version for editing """
902         version = None
903         try:
904             version = instance.REQUEST.get('version',None)
905         except AttributeError:
906             pass
907         if version:
908             value = instance.getFieldHistory(self.getName(), version)
909         else:
910             value = StringField.get(self, instance,**kwargs)
911         return value
912    
913     def get(self, instance, **kwargs):
914         """ Gets cleaned version for display """
915         version = None
916         try:
917             version = instance.REQUEST.get('version',None)
918         except AttributeError:
919             pass
920         value = self.getRaw(instance, **kwargs)
921         cleaned_text = getattr(instance, 'cleaned_%s' % self.getName(), None)
922         if cleaned_text and not version:
923             return cleaned_text       
924         ltool = getToolByName(instance, 'lemill_tool')
925         cleaned_text = ltool.parse_text(value)
926         if not version:
927             setattr(instance, 'cleaned_%s' % self.getName(), cleaned_text)
928         return cleaned_text
929
930     def set(self, instance, value, **kwargs):
931         """ update the cleaned_text and then call original set """
932
933         # update cleaned text
934         ltool = getToolByName(instance, 'lemill_tool')
935         cleaned_text = ltool.parse_text(value)
936         setattr(instance, 'cleaned_%s' % self.getName(), cleaned_text)
937
938         # set value
939         StringField.set(self, instance, value, **kwargs)
940        
941
942
943     # render -method has moved to LeMillTools, because it is needed also outside the wysiwym field.
944
945 registerField(WYSIWYMField,
946                title='WYSIWYM Field',
947                description=('WYSIWYM Field'),
948                )
949
950
951 class ChapterField(ObjectField):
952     """ chapters are stored as lists, where chapter can either be a string or UID. If UID, view widget should fetch the object. """
953     _properties = ObjectField._properties.copy()
954     _properties.update({
955         'widget' : ChapterWidget,
956         'relationship' : 'uses', # required
957         'referenceClass' : Reference,
958         'referenceReferences' : False,
959         'index_method':'indexValue'})
960    
961     security = ClassSecurityInfo()
962     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')}
963
964     def getChapterNames(self):
965         """ Return readable versions of different chapter types """       
966         return ChapterField.chapter_names
967
968
969     def getRaw(self, instance, version=None, **kwargs):
970         """ Gets raw version for editing, as list """
971         if version:
972             value = instance.getFieldHistory(self.getName(), version) or []
973         elif '_initializing_' in kwargs:
974             value = self.getDefault(instance) or []   
975         else:
976             value = ObjectField.get(self, instance, **kwargs) or []
977         if not isinstance(value, list):
978             value = []
979         if value and callable(value):
980             value=value()
981         for chapter in value:
982             if isinstance(chapter, tuple):
983                 value = self.updateChapterList(value)
984                 break
985             elif isinstance(chapter, dict) and chapter.get('type','')=='choice' and isinstance(chapter.get('text',[]), list):
986                 value=self.fixBrokenChoice(value)
987                 break
988         return value
989
990     def get(self, instance, **kwargs):
991         """ Get a cleaned version for display """
992         REQUEST=getattr(instance,'REQUEST',None)
993         if REQUEST:
994             version=REQUEST.get('version',None)
995             translation=REQUEST.get('translation',None)
996         else:
997             version=None
998             translation=None
999
1000         fix_choice=False
1001         convert=False
1002         if version or translation: # don't cache cleaned version
1003             value = self.getRaw(instance, version=version, **kwargs)
1004             value=self.cleanChapters(instance, value)
1005             if translation:
1006                 value=self.mergeTranslationSource(instance,value)
1007             return value
1008         cleaned_value = getattr(instance, 'cleaned_%s' % self.getName(), None)
1009         if cleaned_value:
1010             for chapter in cleaned_value:
1011                 if isinstance(chapter, tuple):
1012                     convert=True
1013                     break
1014                 elif isinstance(chapter, dict) and 'type' in chapter and chapter['type']=='choice' and isinstance(chapter['text'], list):
1015                     fix_choice=True
1016                     break
1017             if convert:
1018                 cleaned_value=self.updateChapterList(cleaned_value)
1019                 cleaned_value=self.updateCleanedChapters(instance, cleaned_value)
1020                 return cleaned_value
1021             if fix_choice:
1022                 cleaned_value=self.fixBrokenChoice(cleaned_value)
1023                 cleaned_value=self.updateCleanedChapters(instance, cleaned_value)
1024                 return cleaned_value
1025             else:
1026                 return cleaned_value
1027         value=self.getRaw(instance, **kwargs)
1028         cleaned_value=self.updateCleanedChapters(instance, value)
1029         return cleaned_value
1030
1031
1032     def textOnly(self, instance, **kwargs):
1033         chapters=self.get(instance)
1034         if chapters and isinstance(chapters, list):
1035             res=[]
1036             for ch in chapters:
1037                 if ch:
1038                     try:
1039                         res.append(to_unicode(ch['text']))
1040                     except UnicodeDecodeError:
1041                         pass
1042             return u'\n'.join(res)
1043         elif chapters:
1044             return str(chapters)       
1045         else:
1046             return ''
1047
1048     def mergeTranslationSource(self, instance, chapter_list):
1049         """ Adds original source texts to translated chapters, under key 'original' """
1050         return chapter_list
1051         #trans_source=instance.get
1052         #for chapter in chapter_list:
1053         
1054
1055     def cleanChapters(self, instance, chapters):
1056         ltool = getToolByName(instance, 'lemill_tool')
1057         cleaned_chapters=[]
1058         parsed_fields=['text','keywords','questions','answers']
1059         for chapter in chapters:
1060             new_chapter={}
1061             new_chapter=copy(chapter)
1062             for key, value in chapter.items():
1063                 if key in parsed_fields:
1064                     new_chapter[key]=ltool.parse_text(value, start_with_p=False)
1065             cleaned_chapters.append(new_chapter)
1066         return cleaned_chapters
1067
1068
1069     def updateCleanedChapters(self, instance, chapters):
1070         cleaned_chapters=self.cleanChapters(instance, chapters)
1071         setattr(instance, 'cleaned_%s' % self.getName(), cleaned_chapters)
1072         return cleaned_chapters
1073        
1074
1075     def _updatePilot(self, old_values):
1076         """ in new dictionary based chapters it makes sense to use one chapter per slide, instead of three chapters
1077         this method converts old chapter lists into new format """       
1078         new_scene=True
1079         updated_values=[]
1080         for cvalue, ctype in old_values:
1081             if new_scene:
1082                 scene={'text':'', 'image_uid':'', 'audio_uid':'', 'keywords':[], 'questions':[], 'type':'pilot_scene'}
1083                 new_scene=False
1084             if ctype=='audio_piece':
1085                 scene['audio_uid']=cvalue
1086             elif ctype=='image_piece':
1087                 scene['image_uid']=cvalue
1088             elif ctype=='pilot_keywords':
1089                 scene['keywords']=cvalue
1090                 updated_values.append(scene)
1091                 new_scene=True
1092             elif ctype=='pilot_questions':
1093                 scene['questions']=cvalue
1094                 updated_values.append(scene)
1095                 new_scene=True
1096         return updated_values           
1097        
1098     def fixBrokenChoice(self, old_values):
1099         new_values=[]
1100         for vald in old_values:
1101             if vald['type']=='choice' and isinstance(vald['text'], list):
1102                  # [text, [correct_answers], [wrong_answers]]
1103                  print vald
1104                  text_list=vald['text']
1105                  vald['text']=text_list[0]
1106                  correct_choices=text_list[1]
1107                  if not isinstance(correct_choices, list):
1108                     correct_choices=[correct_choices]
1109                  wrong_choices=text_list[2]
1110                  if not isinstance(wrong_choices, list):
1111                     wrong_choices=[wrong_choices]
1112                  choices=[]
1113                  for item in correct_choices:
1114                     choices.append((item, 1))
1115                  for item in wrong_choices:
1116                     choices.append((item, 0))                               
1117                  vald['choices']=choices
1118                  vald['type']='multiple_choices'
1119             new_values.append(vald)
1120         return new_values
1121
1122
1123     def updateChapterList(self, old_values):
1124         """ Backward compatibility:
1125             Converts a chapterlist to use dicts instead of tuples. Returns updated list """
1126         updated_values=[]
1127         if 'pilot_keywords' in [ct for cv, ct in old_values]: # Pilots need little more work
1128             return self._updatePilot(old_values)
1129         for cvalue, ctype in old_values:
1130             if ctype in ['multiple_choices','poll','choices', 'choice']:
1131                 q,correct,wrong=cvalue
1132                 if ctype=='multiple_choices':
1133                     choices=[(c, 1) for c in correct]
1134                     choices+=[(w, 0) for w in wrong]
1135                     shuffle(choices)
1136                 elif ctype=='choices' or ctype=='choice':
1137                     if correct and isinstance(correct, list):
1138                         correct=correct[0]
1139                     choices=[(correct,1)]
1140                     choices+=[(w, 0) for w in wrong]
1141                     shuffle(choices)
1142                     ctype='multiple_choices'
1143                 elif ctype=='poll':
1144                     choices=[(c,1) for c in correct+wrong]
1145                     ctype='multiple_choices'
1146                 new_chapter={'text':q, 'type':ctype, 'choices':choices, 'max_points':1.0}
1147             elif ctype in ['fill_in_the_blanks', 'open_ended']:
1148                 new_chapter={'text':cvalue, 'type':ctype, 'max_points':1.0}
1149             elif ctype in ['image_piece','audio_piece','media_piece']:
1150                 new_chapter={'text':'', 'type':ctype, 'uid':cvalue}
1151             elif ctype=='embed_block':
1152                 new_chapter={'text':'', 'type':'embed_block', 'embed':cvalue}
1153             elif ctype=='guidelines':
1154                 new_chapter={'text':cvalue, 'type':'text_block'}
1155             else:
1156                 new_chapter={'text':cvalue, 'type':ctype}
1157             changed=True
1158             updated_values.append(new_chapter)
1159         return updated_values
1160
1161     def set(self, instance, value, **kwargs):
1162         if not value:
1163             return
1164         lt = getToolByName(instance, 'lemill_tool')
1165         changed=False
1166         if kwargs.get('_initializing_', False):
1167             old_values=self.getDefault(instance)
1168             changed=True
1169         else:
1170             old_values=self.getRaw(instance, **kwargs)
1171         references_to_add=[]
1172         references_to_remove=[]       
1173         ### The normal case: widget gives a dictionary ###   
1174         if isinstance(value, dict): 
1175             edited=value['edited']
1176             new_values=copy(old_values)
1177
1178             if edited!=-1: # one chapter has been edited
1179                 ctype=value['chapter_type']
1180                 old_chapter=old_values[edited]           
1181                 # build _one_ chapter based on input
1182                 new_chapter={'type':ctype}
1183                 if ctype in ['multiple_choices','choices','poll']:
1184                     new_chapter['text']=value['question']
1185                     choices=[]
1186                     for answer in value['answers']:
1187                         choices.append((answer['order'], answer['text'], answer['is_correct']))
1188                     choices.sort()
1189                     choices=[(y,z) for (x,y,z) in choices]
1190                     new_chapter['choices']=choices
1191                     new_chapter['max_points']=value.get('max_points',1.0)
1192                 elif ctype=='pilot_scene':
1193                     new_chapter['text']=u'\n'.join(value['keywords'] or value['questions'])
1194                     if value['audio_file']:
1195                         new_piece = lt.createPieceFromFile(value['audio_file'], instance)
1196                         uid=new_piece.UID()
1197                     else:
1198                         uid=value['audio_uid']
1199                     old_uid=old_chapter.get('audio_uid','')
1200                     if old_uid:
1201                         if old_uid!=uid:
1202                             references_to_remove.append(old_uid)
1203                             references_to_add.append(uid)
1204                     else:
1205                         references_to_add.append(uid)
1206                     new_chapter['audio_uid']=uid               
1207                     if value['image_file']:
1208                         new_piece = lt.createPieceFromFile(value['image_file'], instance)
1209                         uid=new_piece.UID()
1210                     else:
1211                         uid=value['image_uid']
1212                     old_uid=old_chapter.get('image_uid','')
1213                     if old_uid:
1214                         if old_uid!=uid:
1215                             references_to_remove.append(old_uid)
1216                             references_to_add.append(uid)
1217                     else:
1218                         references_to_add.append(uid)
1219                     new_chapter['image_uid']=uid
1220                     new_chapter['questions']=value['questions']
1221                     new_chapter['keywords']=value['keywords']
1222                 elif ctype in ['image_piece','audio_piece','media_piece']:
1223                     #new_chapter['text']=''
1224                     uid=value['uid']
1225                     old_uid=old_chapter.get('uid','')
1226                     if old_uid:
1227                         if old_uid!=uid:
1228                             references_to_remove.append(old_uid)
1229                             if uid:
1230                                 references_to_add.append(uid)
1231                     elif uid:
1232                         references_to_add.append(uid)
1233                     new_chapter['uid']=uid
1234                     new_chapter['text']=value.get('caption','')
1235                 elif ctype == 'embed_block':
1236                     new_chapter['text']=''
1237                     if 'embed_old' in value and value['embed_old'] and not value['embed']:
1238                         new_chapter['embed']=value['embed_old']
1239                     else:
1240                         new_chapter['embed']=value['embed']
1241                 elif ctype in ['fill_in_the_blanks', 'open_ended']:
1242                     new_chapter['text']=value['text']
1243                     new_chapter['max_points']=value.get('max_points',1.0)
1244                 else:           
1245                     new_chapter['text']=value['text']
1246                 if new_chapter!=old_chapter:
1247                     changed=True
1248                 new_values[edited]=new_chapter
1249             ## Deleting ##
1250             delete=value.has_key('delete_chapter')
1251             n_o=value.get('new_order',[])
1252             if delete:
1253                 del_i=value['delete_chapter']
1254                 if del_i<len(new_values):
1255                     removed=new_values.pop(del_i)
1256                     if removed.get('uid',''):
1257                         references_to_remove.append(removed['uid'])
1258                     changed=True
1259                 if n_o and del_i in n_o:
1260                     n_o.pop(n_o.index(del_i))
1261                     changed=True
1262             ## Sorting ##
1263             if n_o:
1264                 sortable=zip(n_o, new_values)
1265                 sortable.sort()
1266                 new_values=[val for o, val in sortable]
1267                 changed=True                       
1268             ## Adding new chapters ##
1269             new_chapters=value.get('new_chapters',[])
1270             if new_chapters:
1271                 new_values+=new_chapters
1272                 changed=True
1273         ### The case where field's get-method feeds field's set-method (archetype update, copying)
1274         elif isinstance(value, list):
1275             references_new=[]
1276             for chapter in value:
1277                 uid=chapter.get('uid','')
1278                 if uid:
1279                     references_new.append(uid)
1280             references_old=[]
1281             for chapter in old_values:
1282                 uid=chapter.get('uid','')
1283                 if uid:
1284                     references_old.append(uid)
1285             references_to_add=[uid for uid in references_new if uid not in references_old]
1286             references_to_remove=[uid for uid in references_old if uid not in references_new]
1287             if value!=old_values:
1288                 changed=True
1289             new_values=value
1290         ### update references if necessary
1291         if references_to_add or references_to_remove:
1292             tool = instance.reference_catalog
1293             for uid in references_to_add:
1294                 tool.addReference(instance, uid, self.relationship)
1295             for uid in references_to_remove:
1296                 tool.deleteReference(instance, uid, self.relationship)
1297         if changed:
1298             # update cleaned text
1299             self.updateCleanedChapters(instance, new_values)       
1300             # set new value
1301             ObjectField.set(self, instance, new_values, **kwargs)
1302
1303     def getObjectByUID(self, instance, uid, catalog_only=False):
1304         """ Helper method for fetching mediapieces from references """
1305         uid_catalog = getToolByName(instance, 'uid_catalog')
1306         if not uid:
1307             return None # this is just a newly created placeholder
1308         results=uid_catalog(UID=uid)
1309         try:
1310             return results[0].getObject()
1311         except IndexError:
1312             return None
1313
1314
1315     def isOEmbedChapter(self, chapter):
1316         """ Check if this chapter should use oembed query or just paste embed code """
1317         if not ('type' in chapter and chapter['type']=='embed_block'):
1318             return False
1319         code=chapter['embed']
1320         try:
1321             parsetuple=urlparse(code)
1322             return parsetuple[0]=='http' or parsetuple[0]=='https'
1323         except TypeError:
1324             return False
1325         return False
1326
1327     def isGoogleDocs(self, chapter):
1328         """ OEmbed doesn't recognize google docs, but we can fill them in manually """
1329         def _getParam(query, key):
1330             query_list=query.split('&')
1331             for pair in query_list:
1332                 k,v = pair.split('=')
1333                 if k==key:
1334                     return v
1335             return ''
1336         code=chapter['embed']
1337         try:
1338             adscheme, network, path, params, query, frag = urlparse(code)
1339         except TypeError:
1340             return ''
1341         if network=='docs.google.com' or (network.startswith('docs') and network.endswith('.google.com')):
1342             try:
1343                 docid=''
1344                 path_list=path.split('/')
1345                 if 'present' in path_list: # https://docs.google.com/present/view?id=d-----------n
1346                     docid=_getParam(query,'id')                   
1347                     if docid:
1348                         return '<iframe src="https://docs.google.com/present/embed?id=%s" frameborder="0" width="700" height="559"></iframe>' % docid                   
1349                 elif 'drawings' in path_list: # https://docs.google.com/drawings/pub?id=12---------------------Z6U&w=960&h=720
1350                     docid=_getParam(query,'id')
1351                     try:
1352                         w=int(_getParam(query, 'w') or 960)
1353                         h=int(_getParam(query, 'h') or 720)
1354                     except ValueError:
1355                         w=960
1356                         h=720                                                               
1357                     if docid:                       
1358                         return '<img src="https://docs.google.com/drawings/pub?id=%s&amp;w=%s&amp;h=%s">' % (docid, w,h)                   
1359                 elif 'Doc' in path_list: # https://docs.google.com/Doc?docid=0Acv---------------------mZA&hl=en
1360                     docid=_getParam(query,'docid')
1361                 elif 'd' in path_list: # https://docs.google.com/document/d/1k----------------2ttA0AnSurA/edit?hl=en&authkey=CJ----D
1362                     docid=path_list[2]
1363                 elif 'pub' in path_list: # https://docs.google.com/document/pub?id=1k---------------------------SurA
1364                     docid=_getParam(query,'id')                   
1365             except IndexError:
1366                 return ''
1367             if docid:
1368                 return '<iframe src="https://docs.google.com/document/pub?id=%s&amp;embedded=true" width="760" height="564"></iframe>' % docid
1369         elif network=='spreadsheets.google.com' or (network.startswith('spreadsheets') and network.endswith('.google.com')):
1370             path_list=path.split('/')
1371             if 'viewform' in path_list:
1372                 docid=_getParam(query, 'formkey')     
1373                 return '<iframe src="https://spreadsheets.google.com/embeddedform?formkey=%s" width="760" height="564" frameborder="0" marginheight="0" marginwidth="0">Loading...</iframe>' % docid
1374             elif 'gform' in path_list:
1375                 docid=_getParam(query, 'key')     
1376                 return '<iframe src="https://spreadsheets.google.com/embeddedform?formkey=%s" width="760" height="564" frameborder="0" marginheight="0" marginwidth="0">Loading...</iframe>' % docid               
1377             else:
1378                 docid=_getParam(query, 'key')     
1379                 return "<iframe width='760' height='564' frameborder='0' src='https://spreadsheets.google.com/pub?key=%s&amp;output=html&amp;widget=true'></iframe>" % docid
1380         return ''
1381
1382     def getLength(self, piece):
1383         as_time=piece.getLength()
1384         return {'hour':as_time/3600,'minute':(as_time % 3600)/60,'second':as_time % 60}           
1385
1386     def hasOneCorrectChoice(self, chapter):
1387         """ Check if this exercise is multiple choice (-> use checkboxes) or has only one correct answer (-> use radio buttons) """
1388         corrects=0
1389         for q,c in chapter['choices']:
1390             if c:
1391                 corrects+=1
1392         return corrects==1
1393                
1394
1395
1396 registerField(ChapterField,
1397     title='Chapter field',
1398     description=('Chapter field'),
1399 )
1400
1401 class AudioField(FileField):
1402     """ field for mp3:s """
1403     _properties = FileField._properties.copy()
1404     _properties.update({
1405         'widget' : AudioWidget,
1406     })
1407
1408     def getLength(self, instance):
1409         i = aq_base(instance)
1410         file = AudioField.get(self,instance)
1411         if not shasattr(i, '_mp3length'):
1412             instance._mp3length=get_length(str(file))
1413         if instance._mp3length==0:
1414             instance._mp3length=get_length(str(file))
1415         return {'hour':instance._mp3length/3600,'minute':(instance._mp3length % 3600)/60,'second':instance._mp3length % 60}
1416
1417     def set(self, instance, value, **kwargs):
1418         """ allow only audio here """
1419         FileField.set(self, instance, value, **kwargs)
1420         file=self.get(instance)
1421         if self.getContentType(instance).startswith('audio/'):
1422             instance._mp3length=get_length(str(file))
1423         elif str(file)!='':
1424             FileField.set(self, instance, "DELETE_FILE", **kwargs)
1425
1426 registerField(AudioField,
1427     title='Audio field',
1428     description=('Field that accepts only mp3:s'),
1429 )
1430
1431 class LeMillLinksField(ObjectField):
1432     """ A field that stores LeMill internal links """
1433     __implements__ = ObjectField.__implements__
1434     _properties = ObjectField._properties.copy()
1435     _properties.update({
1436         'widget' : LeMillLinksWidget,
1437     })
1438
1439     security = ClassSecurityInfo()
1440
1441     def set(self, instance, value, **kwargs):
1442         ObjectField.set(self, instance, value, **kwargs)
1443
1444     def getRaw(self, instance, **kwargs):
1445         version = None
1446         try:
1447             version = instance.REQUEST.get('version', None)
1448         except AttributeError:
1449             pass
1450         if version:
1451             value = instance.getFieldHistory(self.getName(), version)
1452         else:
1453             value = ObjectField.get(self, instance, **kwargs)
1454         if value and callable(value):
1455             value=value()
1456         return value
1457
1458     def getRawEdit(self, instance, **kwargs):
1459         """ Returns a text for editing """
1460         pc = getToolByName(instance, 'portal_catalog')
1461         field_data = ObjectField.get(self, instance, **kwargs)#XXX See if any version control is needed here
1462         if field_data == None:
1463             return ""
1464         # find uids and links - replace them with urls
1465         links = re.findall('\{.*?\}', field_data)
1466         for link in links:
1467             linktext = link[1:-1]
1468             if linktext.lower().startswith('http://') or linktext.lower().startswith('https://'):
1469                 field_data = field_data.replace(link, linktext)
1470             elif linktext.lower().startswith('uid:'):
1471                 uid = linktext[4:]
1472                 results = pc({'UID':uid})
1473                 if len(results)>0:
1474                     field_data = field_data.replace(link, results[0].getURL())
1475                 else:
1476                     field_data = field_data.replace(link, '')
1477         return field_data
1478
1479     def get(self, instance, **kwargs):
1480         pc = getToolByName(instance, 'portal_catalog')
1481         letool = getToolByName(instance,'lemill_tool')
1482         version = None
1483         try:
1484             version = instance.REQUEST.get('version',None)
1485         except AttributeError:
1486             pass
1487         if version:
1488             field_data = instance.getFieldHistory(self.getName(), version)
1489         else:
1490             field_data = ObjectField.get(self, instance, **kwargs)
1491         if not field_data:
1492             return ""
1493         if callable(field_data):
1494             field_data=field_data()
1495         links = re.findall('\{.*?\}', field_data)
1496         for link in links:
1497             linktext = link[1:-1]
1498             if linktext.lower().startswith('http://') or linktext.lower().startswith('https://'):
1499                 repl='<a href="%s">%s</a>' % (linktext, letool.shorten_url(linktext))
1500                 field_data = field_data.replace(link, repl)
1501             elif linktext.lower().startswith('uid:'):
1502                 uid = linktext[4:]
1503                 results = pc({'UID':uid})
1504                 repl=''
1505                 if results:
1506                     repl='<a href="%s">%s</a>' % (results[0].getURL(), results[0].Title)
1507                 field_data = field_data.replace(link, repl)
1508         field_data = field_data.strip()
1509         value = field_data.split('\n')
1510         return value
1511
1512 registerField(LeMillLinksField,
1513     title='LeMill Links Field',
1514     description=('LeMill Links Field'),
1515 )
Note: See TracBrowser for help on using the browser.