source: trunk/Resource.py @ 3087

Revision 3087, 45.1 KB checked in by jukka, 9 years ago (diff)

Numerous fixes

Line 
1#
2# This file is part of LeMill.
3#
4# LeMill is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8#
9# LeMill is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with LeMill; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18from Products.Archetypes.public import *
19from Products.Archetypes.atapi import DisplayList
20from Products.Archetypes.utils import mapply
21from Globals import InitializeClass
22from Products.CMFCore.utils import getToolByName
23from AccessControl import ClassSecurityInfo, Unauthorized
24from config import MATERIAL_TYPES, TARGET_GROUP_TO_LRE_MAPPING, SUBJECT_AREA_TO_LRE_MAPPING, PORTAL_TYPE_TO_LRE_MAPPING, to_unicode, LANGUAGES
25from persistent.list import PersistentList
26from messagefactory_ import i18nme as _
27from permissions import ModerateContent, MODIFY_CONTENT, DELETE_CONTENT, MANAGE_PORTAL, ACCESS_CONTENT, VIEW, ADD_CONTENT_PERMISSION
28from DateTime import DateTime
29from Acquisition import aq_inner, aq_parent
30import time, re, difflib, traceback
31from itertools import count
32from zope import event
33from zope.app.event import objectevent
34from CommonMixIn import CoverImageMixIn, CommonMixIn, Redirector
35from types import MethodType as instancemethod
36import time
37
38# regexp to recognize default ids, f.ex. activity.2008-05-19.1133294569
39default_ir_regexp = re.compile(r'^[A-Z]?[a-z]+[-.]?[-.0-9]+[0-9]+$')
40
41# empty marker definition, used by _processForm
42_marker = []
43
44##################### Resource #####################################################
45
46class Resource(CoverImageMixIn, CommonMixIn, BaseContent):
47    """Superclass for all resources (pieces and learning resources)."""
48    global_allow = 1
49    _at_rename_after_creation = True
50
51    security = ClassSecurityInfo()
52
53
54
55
56    ################ Messages #################################################
57
58    def getMessages(self):
59        """ Returns permanent messages related to this resource (draft, is missing a language etc.) """
60        messages=CommonMixIn.getMessages(self)
61        if (not messages) and (not self.Language()) and self.canIEdit():
62            messages.append('language_missing')
63        if self.REQUEST.has_key('version'):
64            messages=['old_version'] # overwrite other messages, this has the priority
65        return messages
66
67
68    ############ Workflow methods #################################
69
70    def canDeleteOnCancel(self):
71        """ Resources haven't got any special conditions """
72        return CommonMixIn.canDeleteOnCancel(self)
73
74
75    ################ After add/edit #################################################
76
77
78    security.declareProtected(MODIFY_CONTENT, 'processForm')
79    def processForm(self, data=1, metadata=0, REQUEST=None, values=None):
80        """Processes the schema looking for data in the form, taken from Archetypes BaseObject
81        """
82        is_new_object = self.checkCreationFlag()
83        self._processForm(data=data, metadata=metadata,
84                          REQUEST=REQUEST, values=values)
85        self.unmarkCreationFlag()
86        # Post create/edit hooks
87        if is_new_object:
88            self.at_post_create_script()
89        else:
90            self.at_post_edit_script()
91        event.notify(objectevent.ObjectModifiedEvent(self))
92
93    security.declarePrivate('at_post_create_script')
94    def at_post_create_script(self):
95        self.at_post_edit_script()
96
97    security.declarePrivate('at_post_edit_script')
98    def at_post_edit_script(self):
99        self.post_edit_rename()
100        self.post_edit_update_history()
101        self.post_edit_credit_author()
102        self.recalculateAuthors()
103        self.recalculateScore()
104        if self.hasComplexWorkflow():
105            if self.getHideDrafts():
106                if self.state == 'draft':
107                    self.setState('private')
108            else:
109                if self.state == 'private':
110                    self.setState('draft')
111        self.reindexObject()
112
113    security.declarePrivate('post_edit_credit_author')
114    def post_edit_credit_author(self):
115        lutool = getToolByName(self, 'lemill_usertool')
116        #memberfolder=lutool.getMemberFolder()
117        #if memberfolder:
118        #    memberfolder.recalculateScore()
119        #    memberfolder.reindexObject(['getScore'])
120
121    security.declarePrivate('post_edit_rename')
122    def post_edit_rename(self):
123        # Store current ID
124        old_id = self.getId();
125        # See what would be the optimal ID currently
126        optimal_new_id = str(self.generateNewId())
127        if optimal_new_id and optimal_new_id != old_id:
128            execute_rename=True
129            if old_id.startswith(optimal_new_id):
130                # Check that the old_id isn't optimal + "-n" suffix
131                suffix = old_id[len(optimal_new_id):]
132                if suffix[0]=='-' and suffix[1:].isdigit():
133                    execute_rename=False
134            # Attempt rename, adding "-n" so that no duplicates occur
135            if execute_rename:
136                # Ff the new name is a redirector for this object, delete it
137                ob = getattr(self.aq_inner.aq_parent,optimal_new_id,None)
138                if ob and ob.meta_type == 'Redirector':
139                    if ob.redirect_to == self.UID():
140                        self.aq_inner.aq_parent._delObject(optimal_new_id)
141                # Execute rename
142                self._renameAfterCreation()
143                new_id = self.getId()
144                # If the id did change
145                if old_id != new_id and not re.match(default_ir_regexp,old_id):
146                    # Need to create redirection from old URL
147                    #print 'creating a redirector: %s' % old_id
148                    red = Redirector(old_id)
149                    red.redirect_to = self.UID()
150                    self.aq_inner.aq_parent._setObject(old_id,red)
151
152
153    security.declarePrivate('_processForm')
154    def _processForm(self, data=1, metadata=None, REQUEST=None, values=None):
155
156        # from Archetypes/BaseObject minus reindexObject at the end
157        request = REQUEST or self.REQUEST
158        if values:
159            form = values
160        else:
161            form = request.form
162        fieldset = form.get('fieldset', None)
163        schema = self.Schema()
164        schemata = self.Schemata()
165        fields = []
166
167        if fieldset is not None:
168            fields = schemata[fieldset].fields()
169        else:
170            if data: fields += schema.filterFields(isMetadata=0)
171            if metadata: fields += schema.filterFields(isMetadata=1)
172
173        form_keys = form.keys()
174        for field in fields:
175            ## Delegate to the widget for processing of the form
176            ## element.  This means that if the widget needs _n_
177            ## fields under a naming convention it can handle this
178            ## internally.  The calling API is process_form(instance,
179            ## field, form) where instance should rarely be needed,
180            ## field is the field object and form is the dict. of
181            ## kv_pairs from the REQUEST
182            ##
183            ## The product of the widgets processing should be:
184            ##   (value, **kwargs) which will be passed to the mutator
185            ##   or None which will simply pass
186
187            if not field.writeable(self):
188                # If the field has no 'w' in mode, or the user doesn't
189                # have the required permission, or the mutator doesn't
190                # exist just bail out.
191                continue
192
193            result = field.widget.process_form(self, field, form,
194                                               empty_marker=_marker)
195            try:
196                # Pass validating=False to inform the widget that we
197                # aren't in the validation phase, IOW, the returned
198                # data will be forwarded to the storage
199                result = field.widget.process_form(self, field, form,
200                                                   empty_marker=_marker,
201                                                   validating=False)
202            except TypeError:
203                # Support for old-style process_form methods
204                result = field.widget.process_form(self, field, form,
205                                                   empty_marker=_marker)
206
207            if result is _marker or result is None:
208                continue
209            # Set things by calling the mutator
210            mutator = field.getMutator(self)
211            __traceback_info__ = (self, field, mutator)
212            result[1]['field'] = field.__name__
213            mapply(mutator, result[0], **result[1])
214
215   
216
217    ############## Getters and feature checks  ##################################################
218
219
220    def getPhysicalPath(self):
221        """ For some reasons we get a lot of unicode strings as paths. This causes problems in catalog """
222        return tuple(map(str, BaseContent.getPhysicalPath(self)))
223
224    def dumpme(self,item):
225        """ print """
226
227        print 'dump: %s' % item
228
229    def checkTitle(self, obj=None ,title='', objtype='', container=None):
230        """ check if title is not used anywhere in not(deleted, redirector) object, return false if it is """
231        lt=getToolByName(self, 'lemill_tool')
232        return lt.checkTitle(self,obj=obj, title=title, objtype=objtype, container=container)
233
234    def shortenedTitle(self, max_length):
235        title = self.Title()
236        if len(title)<=max_length:
237            return title
238        while len(title)>max_length:
239            title=title[:title.rfind(' ')]
240        return title+"..."           
241
242    def amIMaterial(self):
243        """ Returns True if it's a material """
244        return False
245
246    def getObjectByUID(self, UID=None):
247        """ Return object with this UID or None """
248        uc=getToolByName(self, 'uid_catalog')
249        if not UID:
250            return None
251        objlist=uc({'UID':UID})
252        if objlist:
253            # getObject verified
254            return objlist[0].getObject()
255        return None
256
257    def getLanguagelist(self):
258        return DisplayList(LANGUAGES)
259
260    security.declarePublic('defaultLanguage')
261    def defaultLanguage(self):
262        """ Get logged users default language """       
263        lutool = getToolByName(self, 'lemill_usertool')
264        mf = lutool.getMemberFolder()       
265        if mf:
266            lang=mf.getLanguage_skills()
267            if lang:
268                return lang[0]
269        return ''
270
271    def getLatestEditDate(self):
272        """Returns date of last edit or creation date if fails"""
273        hist=self.getHistory()
274        if len(hist)>0:
275            hist=hist[0]
276            if hist.has_key('_timestamp'):
277                return DateTime(hist['_timestamp'])
278        #print 'bad history, %s: %s' % (self.getId(), self.getHistory())
279        return DateTime(self.CreationDate())
280
281    def _prettyFieldName(self, fieldname):
282        """ return a widget's label """
283        f = self.getField(fieldname)
284        if f is None:
285            return fieldname
286        return f.widget.Label(self)
287
288    def isUid(self, chapter):
289        """This is a wrapper to reach ChapterField.isUid()"""
290        return self.getField('bodyText').isUid(chapter)
291
292
293    def addTags(self, tags):
294        """ Process and add tags (do not remove existing, ignore if tag already exists). Returns True if tags get changed  """
295        f = self.getField('tags')
296        if not (f and tags):
297            return
298        existing_tags=list(f.get(self))
299        starting_length=len(existing_tags)
300        if not isinstance(tags, list):
301            tags=[t.strip() for t in tags.split(',')]
302        for tag in tags:
303            if tag not in existing_tags:
304                existing_tags.append(tag)
305        if len(existing_tags)>starting_length:
306            f.set(self, existing_tags)
307            return True
308        return False
309
310
311    def getMetaDescription(self):
312        """ Description for html header. """
313        # If resource has description field, use it           
314        if hasattr(self, 'description'):
315            try:
316                desc=self.getDescription()
317            except AttributeError:
318                try:
319                    desc=self.Description()
320                except AttributeError:
321                    desc=''
322            if desc:
323                return desc
324        # try bodytext
325        if hasattr(self, 'getRawBodyText'):           
326            desc=self.getRawBodyText()
327            if desc:
328                if isinstance(desc, list) or isinstance(desc, tuple):
329                    desc=desc[0]
330                if isinstance(desc, dict) and 'text' in desc:
331                    desc=desc['text']
332            if not desc:
333                return "LeMill %s" % self.portal_type
334            desc_len=0
335            desc_result=[]
336            ltool = getToolByName(self, 'lemill_tool')       
337            desc=ltool.stripHTML(desc)
338            # Add paragraphs until total is over 140 characters
339            for p in desc.split('\n'):
340                if p: # ignore empty lines
341                    desc_len+=len(p)
342                    desc_result.append(p)
343                if desc_len > 140:
344                    break
345            return '\n'.join(desc_result)
346        # Else give some basic info
347        return "LeMill %s by %s" % (self.portal_type, self.Creator())
348       
349
350    def getLREMetadata(self):
351        """ This returns dictionary with following LRE LOM fields:
352            'type' from PORTAL_TYPE_TO_LRE_MAPPING
353            'typicalAgeRange': from TARGET_GROUP_TO_LRE_MAPPING
354            'intendedEndUserRoleSource': ''|'LOMv1.0'
355            'intendedEndUserRole': 'learner'|'teacher'
356            'contextSource': '' | 'LOMv1.0' | 'LREv3.0'
357            'contextValue': from TARGET_GROUP_TO_LRE_MAPPING
358            'taxonomy_list':[('id','string')]
359        """
360        dict={'type': self.portal_type,
361            'typicalAgeRange_list':[],
362            'intendedEndUserRoleSource': 'LOMv1.0',
363            'intendedEndUserRole': 'learner',
364            'contextSource': '',
365            'contextValue': '',
366            'taxonomy_list':[]
367            }
368
369        dict['type']=PORTAL_TYPE_TO_LRE_MAPPING.get(self.portal_type, self.portal_type)
370
371        if hasattr(self, 'getTarget_group'):
372            tg=self.getTarget_group()
373            mapd=TARGET_GROUP_TO_LRE_MAPPING
374            tgs= [mapd[key] for key in tg if mapd.has_key(key)]
375            dict['typicalAgeRange_list']=[t[0] for t in tgs]
376            if 'teacher' in [t[2] for t in tgs]:
377                dict['intendedEndUserRole'] = 'teacher'
378            if 'LREv3.0' in [t[3] for t in tgs]:
379                dict['contextSource'] = 'LREv3.0'
380            elif 'LOMv1.0' in [t[3] for t in tgs]:
381                dict['contextSource'] = 'LOMv1.0'
382            contexts= [t[4] for t in tgs]
383            for place in ['continuing education', 'special education', 'higher education', 'compulsory education', 'pre-school']:
384                if place in contexts:
385                    dict['contextValue']=place
386                    break
387        if hasattr(self, 'getSubject_area'):
388            sa=self.getSubject_area()
389            mapd=SUBJECT_AREA_TO_LRE_MAPPING
390            dict['taxonomy_list']= [mapd[key] for key in sa if mapd.has_key(key)]               
391        return dict
392       
393
394    ################# Authorship info ###################################################################
395
396    def Creator(self):
397        """This method is part of Plone Dublin Core.
398        Overridden to give correct values."""
399        auth = self.getAuthors()
400        if len(auth)>0:
401            return self.getAuthors()[0]
402        else:
403            return ''
404
405    def Creators(self):
406        """This is another base method that should provide good values."""
407        return self.getAuthors()
408
409    def Contributors(self):
410        """This is another base method that should provide good values
411        (everyone except first author)"""
412        auth = self.getAuthors()
413        if len(auth)>0:
414            return self.getAuthors()[1:]
415        else:
416            return []
417
418    def getAuthors(self):
419        """ used to get the list of authors """
420        return self.getField('creators').get(self)
421
422    def getUserGroups(self):
423        """ this is vocabulary for groups field """
424        return [] # only LearningResources can have group editing
425
426    def getGroupInfo(self):
427        """ Returns uid_catalog metadata for group editing this content """
428        return None # only LearningResources can have group editing
429
430    def getAuthorsNames(self):
431        """ Get nice user names of authors. """
432        lutool = getToolByName(self, 'lemill_usertool')
433        names = []
434        authors = self.getAuthors()
435        for author in authors:
436            auth = author.split(',')
437            for a in auth:
438                try:
439                    md=self.getMemberFolderMetaData(a)
440                    name = md.getNicename
441                    names.append(name)
442                except AttributeError:
443                    names.append(a)
444        if not names:
445            return ""
446        return ', '.join(names)
447
448    def getLastEditor(self):
449        """Returns id of last user who edited this object"""
450        try:
451            return self.getHistory()[0]['_by']
452        except IndexError:
453            #print 'bad history, %s: %s' % (self.getId(), self.getHistory())
454            return ''
455
456
457    security.declareProtected(MANAGE_PORTAL, 'resetFirstAuthor')
458    def resetFirstAuthor(self, userid):
459        """ If after copying, updating or some other maintenance act the first author of a resource has changed
460            this maintenance method helps to set first author to userid """
461        history=self.getHistory()
462        history[-1]['_by']=userid
463        self.setHistory(history)
464        self.recalculateAuthors()       
465
466    security.declareProtected(MANAGE_PORTAL, 'removeHistoryEntry')
467    def removeHistoryEntry(self, index):
468        """ If after copying, updating or some other maintenance creates a misleading history entry
469            this maintenance method removes it. """
470        history=self.getHistory()
471        del history[int(index)]
472        self.setHistory(history)
473        self.recalculateAuthors()       
474
475    security.declarePrivate('recalculateAuthors')
476    def recalculateAuthors(self, removeAdmin=''):
477        """ Recalculates author order.
478            This is done by getting all history entries for object, combining diffs f
479         """
480
481        def recursive_join(item):
482            """ Method to join complicated list-structures to one string, separated by \n:s """
483            if type(item)==list or type(item)==tuple:
484                new=[]
485                for i in item:
486                    new.append(recursive_join(i))
487                return '\n'.join(new)
488            else:
489                return str(item)
490
491        creators_field = self.getField('creators')
492        creators=creators_field.get(self)
493
494        if self.state == 'private':# Only creator or manager can modify private resources
495            return creators
496
497        history_entries = self.getHistoryEntries()
498        if not history_entries:
499            return creators
500       
501        i=-1
502        original_creator=''
503        # If, for some reason the last entry doesn't have author, try next until one is found
504        while (not original_creator) and i+len(history_entries)>=0:
505            original_creator = history_entries[i].get('author','')
506            i-=1
507
508        diffsort = []
509        for entry in history_entries[:-1]:     # no diff for the original version
510            diff_fields = self.getDiffFields(entry['timestamp'])
511            if removeAdmin and now - entry['timestamp']<3600 and entry['author']==removeAdmin:
512                print 'Ignored admin modification in %s, made %s seconds ago.' % (self.getId(), int(now - entry['timestamp']))
513            elif entry['minor_edit']: # we ignore any case with minor_edit beinf True
514                print 'Ignored minor-edit in %s by %s' % (self.getId(), entry['author'])
515            else:                                       
516                if diff_fields.has_key('bodyText'):
517                    old_body = diff_fields['bodyText']['old']
518                    new_body = diff_fields['bodyText']['new']
519                    old_body = recursive_join(old_body)
520                    old_body = old_body.split('\n')
521                    new_body = recursive_join(new_body)
522                    new_body = new_body.split('\n')
523                    differences = difflib.unified_diff(old_body, new_body)
524                    first_chars = [change_note[0] for change_note in list(differences)[2:]]
525                    changed_lines = first_chars.count('+')
526                    removed_lines = first_chars.count('-')
527                    #context = first_chars.count(' ')
528                    #chunk = first_chars.count('@')
529                    new_lines = changed_lines - removed_lines
530                    diffsort.append(((new_lines, changed_lines), entry['author']))
531
532        # Author who has added most lines is the first author
533        diffsort.sort()
534        # Make sure that original creator is first
535        new_creators=[original_creator]
536        # Add other authors from ordered list, ignoring duplicates
537        for comparison_tuple, creator in diffsort:
538            if creator not in new_creators:
539                new_creators.append(creator)
540        # Set field and finish.
541        if creators!=new_creators:
542            creators_field.set(self, new_creators)
543        return new_creators
544
545
546    #################    History        #####################################################
547    #
548    # getHistory -- gives list of dictionaries, assume that list is ordered latest '_timestamp' first
549    # setHistory -- !! only if you know what you are doing !!
550    # getHistoryEntries -- gets a simpler dictionary for history_view
551    # post_edit_update_history -- after editing this gets called, creates a new history entry if necessary
552    # storeInHistory -- a method to add a new entry. Assumes that new entry is _new_, so it goes first in list. Called by post_edit_update_history
553    # _storeInHistory -- use storeInHistory instead
554    # restoreAVersion -- finds a version corresponding to a timestamp and fetches field values from that time and rewrites fields and calls storeInHistory
555    # __getLatestHistoricalValueForField -- gets the latest value from history, this makes sense if the object is modified, but modifications
556    #                                       are not yet stored in history. (Like in post_edit_update_history)
557    # getTimeForOldHistory -- used to get localized time to display instead of timestamp
558    # getHistoricalFields -- gets all fields from an older version or the previous version of that. Called by getDiffFields
559    # getFieldHistory -- gets one field from a certain timestamp. Used by fields from FieldsWidgets.
560    # getDiffFields -- gets a dictionary where new (from timestamp) and old (next older) fields can be compared
561    #                  used by recalculateAuthors
562    # migrate_history -- deprecated, used to move objects from another portal
563    # isMinorEditHappening -- tries to guess if editor is making a minor edit, the checkbox gets ticked if yes. 
564
565
566    security.declarePrivate('getHistory')
567    def getHistory(self):
568        """This is the base for all other history methods and we assume that history is stored *latest first*."""
569        try:
570            return self.__history
571        except AttributeError:
572            self.__history=PersistentList()
573            return self.__history
574
575    security.declarePrivate('setHistory')
576    def setHistory(self, history):
577        self.__history=history
578
579    def getHistorySize(self):
580        """ Returns an estimation of size of history of this object """
581        def recursive_size(value):
582            size=0
583            if hasattr(value, 'get_size'):
584                size=value.get_size()
585            elif type(value)==list or type(value)==tuple:
586                for v in value:
587                    size+=recursive_size(v)
588            elif type(value)==dict:
589                for k, v in value.items():
590                    size+=k
591                    size+=recursive_size(v)
592            elif type(value)==bool:
593                size+=1
594            elif hasattr(value, '__len__'):
595                size=len(value)
596            else:
597                print "Weird: can't calculate size: %s" % value
598                size=len(str(value))
599            return size
600           
601        hist=self.getHistory()
602        size=0
603        for event in hist:
604            for key, value in event.items():
605                size+=len(key)
606                size+=recursive_size(value)
607        return size
608       
609    def getHistoryEntries(self):
610        """Returns a list of user-friendly history entries that can be used by display methods."""
611        history=self.getHistory()
612        history_entries=[]
613        version_number=len(history) # since we start from latest, it has the largest version number
614        for entry in history:
615            result_entry = {}
616            result_entry['version'] = version_number
617            result_entry['date']=time.asctime(time.localtime(entry['_timestamp']))
618            result_entry['timestamp'] = entry['_timestamp']
619            result_entry['minor_edit'] = entry.get('_minor_edit', False)
620            result_entry['author']= entry['_by']
621            summary = entry.get('_summary', '')
622            if version_number==1:
623                summary = "Resource created"
624            if not summary:
625                fields = [self._prettyFieldName(field_name) for field_name in entry.keys() if not field_name.startswith('_')]
626                summary = "Modified these: %s" % ', '.join(fields)
627            result_entry['summary'] = summary
628            history_entries.append(result_entry)
629            version_number-=1
630        return history_entries
631
632
633    security.declarePrivate('post_edit_update_history')
634    def post_edit_update_history(self):
635        """ Check which fields have changed and send a list of field names to history building method """
636        history=self.getHistory()
637        changedFields=[]
638        editable_fields=self.Schema().editableFields(self)
639
640        for field in editable_fields:
641            fieldname=field.getName()
642            if fieldname!='creators' and fieldname!='modification_date' and field.getStorageName()!='FileSystemStorage':
643                last_value=self.__getLatestHistoricalValueForField(fieldname)
644                current_value=field.getRaw(self)
645                if (not last_value) or last_value!=current_value:
646                    changedFields.append(fieldname)
647                   
648        if not changedFields:
649            return
650
651        if history:
652            if self.isMinorEditHappening():
653                last_entry = history[0]
654                prev_changes = last_entry.keys()
655                # Merge this change to previous change
656                changedFields = [fieldname for fieldname in set(changedFields + prev_changes) if not fieldname.startswith('_')]
657                del history[0]
658                self.setHistory(history)
659            elif self.meta_type in self.getFeaturedTypes():
660                # If changes are sufficiently large and this is in featured types, we send notification email
661                self.notifyEditedResource()
662        self.storeInHistory(changedFields)
663
664    security.declarePrivate('storeInHistory')
665    def storeInHistory(self,fields_list,summary=None):
666        """ This is the only method to add to history. New entries are inserted to the beginning of history, index 0 """
667        lutool = getToolByName(self, 'lemill_usertool')
668        member_id = lutool.getAuthenticatedId()
669        data={}
670        for fieldname in fields_list:
671            f=self.getField(fieldname)
672            if f:
673                data[fieldname]=f.getRaw(self)
674        self._storeInHistory(data, time.time(), member_id, summary)
675       
676
677    def _storeInHistory(self, entry, timestamp, by=None, summary=None):
678        entry['_by'] = by or ''
679        entry['_timestamp'] = timestamp
680        entry['_summary'] = summary or ''
681        # See if the REQUEST has minor_edit in it and decide the value for the flag
682        entry['_minor_edit'] = self.REQUEST.get('minor_edit', False)
683        # Newest first!
684        history=self.getHistory()
685        history.insert(0, entry)
686        self.setHistory(history)
687       
688
689    security.declareProtected(MODIFY_CONTENT, 'restoreAVersion')
690    def restoreAVersion(self, timestamp):
691        """Restores an old version of the resource."""
692        history = self.getHistory()
693        changedFields = []
694        entries_old_enough=[]
695        # because easch history entry stores only those fields that have changed,
696        # you cannot rely that one history entry can give all required fields,
697        # you'll need to have a list of old-enough-history entries
698        for entry in history: # only look history beyond given timestamp
699            if round(entry['_timestamp']) <= round(float(timestamp)):
700                entries_old_enough.append(entry)
701        editable_fields = [field for field in self.Schema().editableFields(self) if field.getName()!='id']
702        for field in editable_fields:
703            fieldname = field.getName()
704            for entry in entries_old_enough: # looking for a historical value
705                if entry.has_key(fieldname):
706                    field.set(self, entry[fieldname])
707                    changedFields.append(fieldname)
708                    break
709            # Update history
710        if changedFields:
711            if 'title' in changedFields:
712                self.post_edit_rename()
713            self.storeInHistory(changedFields)                       
714            self.recalculateScore()
715            self.reindexObject()
716        if self.portal_type in MATERIAL_TYPES:
717            return self.REQUEST.RESPONSE.redirect('%s/view' % self.absolute_url())
718        else:
719            return self.REQUEST.RESPONSE.redirect(self.absolute_url())
720
721    def __getLatestHistoricalValueForField(self,fieldname):
722        for entry in self.getHistory():
723            if entry.has_key(fieldname):
724                return entry[fieldname]
725        return None
726
727    def getTimeForOldHistory(self, timestamp):
728        """ returns time for the history """
729        timestamp = float(timestamp)
730        return time.asctime(time.localtime(timestamp))
731
732
733    def getHistoricalFields(self, timestamp, take_previous_version=False):
734        """ get historical fields from timestamp or the immediate next entry before timestamp (previous version) """
735        history = self.getHistory()
736        result_entry = {}
737        waiting=False
738        begin_copying=False
739        # Wait for the latest entry before/at timestamp and copy all of its fields
740        # then continue back in history and try to find fields that were not changed in latest entry, but
741        # have been changed earlier
742        for entry in history:
743            if waiting:
744                begin_copying=True
745            if float(timestamp) >= float(entry['_timestamp']) and not begin_copying:  # scroll back in time
746                if take_previous_version:
747                    waiting=True
748                else:
749                    begin_copying=True
750            if begin_copying:
751                for fieldname in entry.keys():
752                    if not (fieldname.startswith('_') or result_entry.has_key(fieldname)):
753                        result_entry[fieldname]=entry[fieldname]
754
755        # Use current values for those fields that don't have historical values
756        for field in self.Schema().editableFields(self):
757            fieldname=field.getName()
758            if not result_entry.has_key(fieldname):
759                result_entry[fieldname] = self.getField(fieldname).getRaw(self)
760        return result_entry
761
762    def getFieldHistory(self, fieldname, timestamp):
763        """ get field from an older version, used by Fields from FieldsWidgets.py """
764        if not timestamp: # page templates call this even when they intend not to use result, in test(condition, act1, act2) -formulations
765            return None
766        history = self.getHistory()
767        timestamp = float(timestamp)
768        for entry in history:
769            if timestamp >= float(entry['_timestamp']):
770                if entry.has_key(fieldname):
771                    return entry[fieldname]
772        return None
773
774    def getDiffFields(self, timestamp):
775        """ returns a dictionary where each field has value from a certain timestamp and its previous version """
776        diffs_dict={}
777        fields_this = self.getHistoricalFields(timestamp)
778        fields_previous = self.getHistoricalFields(timestamp, take_previous_version=True)
779        fieldnames = set(fields_this.keys()+fields_previous.keys())
780        for fieldname in fieldnames:
781            this=fields_this.get(fieldname, None) or None # 'or None' assigns None for all of those cases when field equals empty =(False, '', 0, [])
782            previous=fields_previous.get(fieldname, None) or None
783            if this==previous: continue
784            if fieldname == 'modification_date' or fieldname=='coverImage': continue
785            entry = {}
786            entry['name'] = fieldname
787            entry['old'] = fields_previous.get(fieldname, None)
788            entry['new'] = fields_this.get(fieldname, None)
789            entry['niceName'] = self._prettyFieldName(fieldname)
790            diffs_dict[fieldname] = entry
791        return diffs_dict
792
793
794    security.declarePublic('isJustCreated')
795    def isJustCreated(self):
796        """ Plone's normal 'just created' detection methods do not work with chapterized resources.
797            let's use history to find if recent changes are from the same author and there aren't many of them. """
798        history = self.getHistory()
799        if not history:
800            return True
801        lutool = getToolByName(self, 'lemill_usertool')
802        auth_id = lutool.getAuthenticatedId()
803        now = time.time()
804        for entry in history:
805            if auth_id!=entry['_by'] or now - entry['_timestamp'] > 600:
806                return False
807        return True
808
809    security.declarePublic('isMinorEditHappening')
810    def isMinorEditHappening(self):
811        """ See if the current editor is the author of latest entry (not older than 1 hour)
812            and try to guess the value for minor_edit checkbox"""
813        history = self.getHistory()
814        if not history:  # return False if there is a problem getting latest history entry
815            return False
816        last_entry = history[0]       
817        lutool = getToolByName(self, 'lemill_usertool')
818        auth_id = lutool.getAuthenticatedId()
819        now = time.time()
820        return now - last_entry['_timestamp']<3600 and last_entry['_by'] == auth_id
821
822
823    ################   Workflow operations   ###########################################
824
825
826    security.declareProtected(MODIFY_CONTENT, 'publish')
827    def publish(self):
828        """ Publish object. Called by script_changeCoverImage.cpy """
829        self.setState('public')
830        self.notifyPublishedResource()
831        # When publishing a learning resource, the media pieces will inherit the title and tags of their parent learning resource
832        if hasattr(self, 'getBodyText'):
833            bodyText=self.getBodyText()
834            if type(bodyText)==list:
835                for bodyElement in bodyText:
836                    if type(bodyElement)==tuple and bodyElement[1] in ["media_piece","image_piece","audio_piece"] and self.isUid(bodyElement[0]):
837                        obj = self.getObjectByUID(bodyElement[0])
838                        if obj and not obj.getTags(): # Only change tags or title for pieces that don't have any tags.
839                            newTags = list(self.getTags())
840                            if obj.title_or_id().startswith("%s - " % self.title_or_id()):
841                                newTitle = obj.title_or_id()
842                            else:
843                                newTitle = "%s - %s" % (self.title_or_id(), obj.title_or_id())
844                            obj.edit(tags = newTags, title = newTitle)
845        self.recalculateScore()
846        self.reindexObject(['getState','getScore'])
847        self.reindexCollections()
848
849    security.declareProtected(MODIFY_CONTENT, 'retract')
850    def retract(self):
851        """Retract published, but keep coverimage as it is"""
852        REQUEST = self.REQUEST
853        if not self.hasComplexWorkflow():
854            return REQUEST.RESPONSE.redirect(self.absolute_url())
855        if self.getHideDrafts():
856            self.setState('private')
857        else:
858            self.setState('draft')
859        self.recalculateScore()
860        self.reindexObject(['getState', 'getScore'])
861        self.reindexCollections()
862        return REQUEST.RESPONSE.redirect(self.absolute_url())
863
864    security.declareProtected(MANAGE_PORTAL, 'permaDelete')
865    def permaDelete(self, REQUEST):
866        """Move to trash"""
867        if not self.state == 'deleted':
868            return self
869        portal_url = getToolByName(self, 'portal_url')
870        id=str(self.id)
871        context=self.aq_parent
872        portal = portal_url.getPortalObject()
873        trash=portal.trash
874        obj = getattr(context, id)
875        new_id=str(obj.UID())
876        trash._setObject(new_id, obj)
877        context._delObject(id)
878        moved=getattr(trash, new_id)
879        moved.unindexObject()
880        lt=getToolByName(self, 'lemill_tool')
881        lt.addPortalMessage(_('Moved item to trash.'))
882        return REQUEST.RESPONSE.redirect(context.absolute_url())
883
884
885    security.declareProtected(ModerateContent, 'deleteResource')
886    def deleteResource(self, reason=''):
887        """Set reason for deletion, set state to deleted and update catalog"""
888        f = self.getField('deletionReason')
889        if f:
890            f.set(self, reason)
891        self.setState('deleted')
892        self.aliases['(Default)']='base_view'
893        try:
894            self.reindexObject(['getState'])
895        except CatalogError:
896            print "Problem reindexing %s" % self.id
897        self.reindexCollections()
898
899    security.declareProtected(ModerateContent, 'rescue')
900    def rescue(self, REQUEST):
901        """Undelete a resource """
902        self.aliases['(Default)']=self.__class__.aliases['(Default)']
903        self.undeleteResource()
904        return REQUEST.RESPONSE.redirect(self.absolute_url())
905
906    security.declareProtected(ModerateContent, 'undeleteResource')
907    def undeleteResource(self):
908        f=self.getField('deletionReason')
909        f.set(self, None)
910        if self.hasComplexWorkflow():
911            if self.getHideDrafts():
912                self.setState('private')
913            else:
914                self.setState('draft')
915        else:
916            self.setState('public')
917        self.reindexObject(['getState'])
918        self.reindexCollections()
919       
920
921    ################### Download     ######################################
922
923    def download(self, REQUEST, RESPONSE, field):
924        """ download a file """
925        from Products.Archetypes.utils import contentDispositionHeader
926        if not hasattr(field, 'getFilename'): # something is wrong here
927            return None
928        org_filename = field.getFilename(self)
929        filename = self.Title()
930        lt = getToolByName(self, 'lemill_tool')
931        filename = lt.normalizeString(filename)
932        extension = ''
933        if org_filename:        # extract .doc .pdf or something
934            extension = org_filename[org_filename.rfind('.'):]
935            if extension == -1: extension = ''
936        else:                   # try to guess extension
937            ct = field.getContentType(self)
938            mr = getToolByName(self, 'mimetypes_registry')
939            mt = mr.lookup(ct)
940            if mt:              # mt is something like (<mimetype text/plain>,) so we'll take first one
941                extension = mt[0].extensions[0]       # and take first one from here too
942                extension = '.'+extension
943        # Handling the special KML case
944        if extension == '.xml':
945            m_file = field.get(self)
946            if lt.checkIsKML(str(m_file)):
947                extension = '.kml'
948        if extension and not filename.endswith(extension):
949            filename += extension
950        header_value = contentDispositionHeader('attachment', self.getCharset(), filename=filename)
951        RESPONSE.setHeader("Content-disposition", header_value)
952        file = field.get(self)
953        return file.index_html(REQUEST, RESPONSE)
954
955
956    ################# Collections & Stories      ###########################
957
958    def getCollections(self):
959        """ Show collections where object is used """
960        obj_uid = self.UID()
961        res = []
962        q = { 'targetUID': obj_uid }
963        qres = self.reference_catalog(q)
964        for q in qres:
965            v = self.reference_catalog.lookupObject(q.UID)
966            if v:
967                source = v.getSourceObject()
968                if source and source.meta_type == 'Collection':
969                     res.append(source)
970        sortable_res = [(x.Title(), x) for x in res]
971        sortable_res.sort()
972        return [x[1] for x in sortable_res]
973
974    def getRelatedStories(self):
975        """ Returns good Collections with stories """
976        pc = getToolByName(self, 'portal_catalog')
977        rc = getToolByName(self, 'reference_catalog')
978        r_query = {'targetUID':self.UID(), 'relationship':('relatesToContent','relatesToMethods','relatesToTools')}       
979        uids=[r.sourceUID for r in rc(r_query)]
980        query = {'UID':uids,
981            'getGoodStory':True,
982            'meta_type':'Collection',
983            'sort_on':'getLatestEdit',
984            'sort_order':'descending',
985            'getState':'public'}
986        return pc(query)
987       
988    def reindexCollections(self):
989        """ updating resource collections getGoodStory index in catalog """
990        collections = self.getCollections()
991        for collection in collections:
992            collection.reindexObject(['getGoodStory'])
993
994    ################ Notifications ##############################
995    security.declarePrivate('notifyEditedResource')
996    def notifyEditedResource(self):
997        ltool = getToolByName(self, 'lemill_tool')
998        lutool = getToolByName(self, 'lemill_usertool')
999        auth_mf=lutool.getMemberFolder()
1000        if not auth_mf: return # When portal is created this method gets called but there are no memberfolders yet.                   
1001        if self.Creator() != lutool.getAuthenticatedId():
1002            mf = lutool.getMemberFolder(self.Creator())
1003            # See if notification is checked
1004            if mf.canNotify('resource_edited'):
1005                language=lutool.getCommunicationLanguage(mf)
1006                dict={'name':to_unicode(auth_mf.getNicename()),
1007                    'title':to_unicode(self.Title()),
1008                    'url':to_unicode(self.absolute_url())}               
1009                msg = self.translate("%(name)s has edited your resource '%(title)s': %(url)s", domain="lemill", target_language=language) % dict
1010                ltool.mailNotification(msg, recipient=mf.getEmail(), language=language)   
1011
1012
1013    security.declarePrivate('notifyPublishedResource')
1014    def notifyPublishedResource(self):
1015        ltool = getToolByName(self, 'lemill_tool')
1016        lutool = getToolByName(self, 'lemill_usertool')
1017        auth_mf = lutool.getMemberFolder()
1018        contacts = [lutool.getMemberFolder(c.getId) for c in auth_mf.getRelatedContacts()]
1019        recipients= [c for c in contacts if c.canNotify('contact_published_resource')]
1020        for recip in recipients:
1021            language=lutool.getCommunicationLanguage(recip)
1022            dict={'name':to_unicode(auth_mf.getNicename()),
1023                'title':to_unicode(self.Title()),
1024                'url':to_unicode(self.absolute_url())}               
1025            msg= self.translate("Your contact %(name)s has published a new resource '%(title)s': %(url)s", domain="lemill",target_language=language) % dict
1026            ltool.mailNotification(msg, recipient=recip.getEmail(), language=language)   
1027
1028
1029    ############# Conditions ################################
1030    def isMinorEditShown(self):
1031        """ Used to decide if minorEdit checkbox will be shown """
1032        lutool = getToolByName(self, 'lemill_usertool')
1033        current_member_id = lutool.getAuthenticatedId()
1034        if current_member_id not in self.Creators():
1035            return True
1036        return False
1037
1038    ############ PDF view ########################################
1039
1040
1041    def prepareForPDF(self):
1042        """  """       
1043        source=self.standalone_view()
1044        begin=source.find("material-content")-9
1045        end=source.find("material-content-ends")-5
1046        return source[begin:end]
1047
1048
1049    ############# Additional editing ######################################
1050   
1051    security.declareProtected(MODIFY_CONTENT, 'updateLanguage')
1052    def updateLanguage(self,lang):
1053        """ Just change language, called from javascript """
1054        self.setLanguage(lang)
1055        self.at_post_edit_script()
1056        lt=getToolByName(self, 'lemill_tool')
1057        lt.addPortalMessage(_('Thank you!'))
1058        return self.REQUEST.RESPONSE.redirect(self.absolute_url()+'/base_view')
1059
1060    security.declareProtected(MODIFY_CONTENT, 'updateTags')
1061    def updateTags(self, tags):
1062        """ Just add tags, called from javascript """
1063        self.setTags(tags)
1064        self.at_post_edit_script()
1065        lt=getToolByName(self, 'lemill_tool')
1066        lt.addPortalMessage(_('Thank you!'))
1067        return self.REQUEST.RESPONSE.redirect(self.absolute_url()+'/base_view')
1068
1069
1070InitializeClass(Resource)
Note: See TracBrowser for help on using the repository browser.