source: trunk/Resources.py @ 1886

Revision 1886, 48.8 KB checked in by laszlo, 12 years ago (diff)

fixed #1460 spent 4h

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
20import Globals
21import os.path
22from Globals import InitializeClass
23from Products.CMFCore.utils import getToolByName
24from AccessControl import ClassSecurityInfo, Unauthorized
25from config import PROJECTNAME, MATERIAL_TYPES, LANGUAGE_INDEPENDENT_FIELDS, DEFAULT_ICONS, TYPE_ABBREVIATIONS
26from persistent.list import PersistentList
27from Products.LeMill import LeMillMessageFactory as _
28from Products.CMFPlone import PloneMessageFactory as PMF
29from permissions import ModerateContent, MODIFY_CONTENT, DELETE_CONTENT, MANAGE_PORTAL
30import datetime
31from DateTime import DateTime
32
33import time, re
34from itertools import count
35import difflib
36import traceback
37import sys
38from types import StringTypes
39
40# retains the first occurence of an object and drops the others
41def _remove_duplicates(d):      # XXX both _remove_duplicates and remove_duplicates does the same (to catch some bugs early)
42    d = list(d)
43    d.reverse()
44    creators_pos = dict([(c, -i) for i, c in enumerate(d)])
45    creators_pos_rev = dict([(v, k) for k, v in creators_pos.iteritems()])
46    return [v for k, v in sorted(creators_pos_rev.iteritems())]
47
48def remove_duplicates(d):
49    creator_positions = {}
50    for pos, creator in enumerate(d):
51        creator_positions.setdefault(creator, []).append(pos)
52
53    best_pos = sorted(((min(positions), creator) for creator, positions in creator_positions.iteritems()))
54    assert len(set((pos for pos, creator in best_pos))) == len(best_pos)
55    assert len(set((creator for pos, creator in best_pos))) == len(best_pos)
56
57    result = [creator for pos, creator in best_pos]
58    assert result == _remove_duplicates(d), "%s, %s" % (result, _remove_duplicates(d))
59    return result
60assert remove_duplicates((1, 2, 1, 7, 4, 2, 4, 6, 7, 8, 3, 2, 1)) == [1, 2, 7, 4, 6, 8, 3]
61
62class CommonMixIn:
63    """Superclass for all objects."""
64
65    def getDefaultIcon(self, meta_type='', obj=None):
66        """ general method for getting proper icon for object, used when only catalog-metadata is available """
67        # this combines folderish getDefaultIcon(for-this-type, object) and resource-specific object.getDefaultIcon()
68        # folderish behaviour is needed because members have these created resources-pages. 
69        if meta_type=='':
70            return  DEFAULT_ICONS[self.meta_type]
71        else:     
72            address=DEFAULT_ICONS[meta_type]
73            if address!='piece':
74                return address
75            else:
76                obj=obj.getObject()
77                return obj.getDefaultIcon()
78
79    def canDeleteOnCancel(self):
80        """ Check if object is just created and generally boring"""
81        obj_type=self.meta_type.lower()
82        obj_title=self.Title()
83        if obj_title:
84            obj_title.lower()
85        else:
86            obj_title=''
87        stupidname= self.getId().lower().startswith(obj_type)
88        notitle= not obj_title.startswith(obj_type)
89        timediff=DateTime()-DateTime(self.CreationDate())
90        if timediff<0.1 and stupidname and notitle:
91            return True
92        else:
93            return False
94
95    def getBodyText(self):
96        """ get body text, do nothing"""
97        field = self.getField('bodyText')
98        if not field:
99            return None
100        return field.get(self)
101
102    def getCookedBodyText(self):
103        """ get body text, run it through parsers"""
104        lt = getToolByName(self, 'lemill_tool')
105        bodytextfield = self.getField('bodyText')
106        if not bodytextfield:
107            return None
108
109        bodytext = bodytextfield.get(self)
110        if type(bodytext) in StringTypes:
111            return lt.shorten_link_names(lt.htmlify(bodytext))
112        else:
113            if bodytext and reduce(lambda x, y: x and y, (type(s) in StringTypes for s in bodytext), True):
114                return [lt.shorten_link_names(lt.htmlify(s)) for s in bodytext]
115            else:
116                return bodytext
117
118    def getRawBodyText(self):
119        """ get body text, do nothing"""
120        field = self.getField('bodyText')
121        if not field:
122            return None
123        return field.get(self)
124
125    def pretty_title_or_id(self):
126        """ add type abbreviation like [MP] before title """
127        if TYPE_ABBREVIATIONS.has_key(self.portal_type):
128            return '[%s] %s' % (TYPE_ABBREVIATIONS[self.portal_type], self.title_or_id())
129        else:
130            return '[%s] %s' % (self.portal_type, self.title_or_id())       
131
132    def recalculateScore(self):
133        """ fallback method if resource doesn't have this method implemented, do nothing """
134        pass
135
136    def getRelatedStories(self):
137        """ fallback method if resource doesn't have this method implemented, return empty array"""
138        return []
139
140InitializeClass(CommonMixIn)
141
142class CoverImageMixIn:
143    """Mix-in class for resources with cover images."""
144    security = ClassSecurityInfo()
145    def getCoverOrDefault(self):
146        wtool = getToolByName(self,'portal_workflow')
147        if self.getField('hasCoverImage').get(self)==True and \
148               wtool.getInfoFor(self,'review_state',None)!='draft':
149            return self.coverImage
150        return eval('default_%s.png' % self.meta_type.lower())
151       
152       
153    def getCoverImageURL(self, drafts=False):
154        """Returns the URL for the cover image. If drafts=True, also allow drafts to show cover image"""
155        wtool = getToolByName(self,'portal_workflow')
156        field = self.getField('hasCoverImage')
157        timestamp = time.time()
158        if field and field.get(self)==True and \
159               (wtool.getInfoFor(self,'review_state',None)!='draft' or drafts):
160            return self.absolute_url()+'/coverImage?newest='+str(timestamp)
161        return self.getDefaultIcon()
162
163    security.declareProtected(MODIFY_CONTENT,'setCoverImage')
164    def setCoverImage(self, value, **kwargs):
165        """ Normal mutator, but flags object to have a coverImage (hasCoverImage = True) """
166        cover=self.getField('coverImage')
167        cover.set(self,value,**kwargs)
168        has_cover=self.getField('hasCoverImage')
169        if value==None:
170            has_cover.set(self,False)
171        else:
172            has_cover.set(self,True)
173        self.reindexObject()
174
175    security.declareProtected(MODIFY_CONTENT,'delCoverImage')
176    def delCoverImage(self):
177        """ Reverse of setCoverImage """
178        cover=self.getField('coverImage')
179        cover.set(self, "DELETE_IMAGE")
180        orgCover = self.getField('originalCoverImage')
181        orgCover.set(self, "DELETE_IMAGE")
182        has_cover=self.getField('hasCoverImage')
183        has_cover.set(self,False)
184        self.reindexObject()
185
186InitializeClass(CoverImageMixIn)
187
188
189default_ir_regexp = re.compile(r'^[A-Z]?[a-z]+[-.]?[-.0-9]+[0-9]+$')
190def is_id_in_default_format(id):
191    return not not re.match(default_ir_regexp,id)
192
193class Resource(BaseContent,CommonMixIn,CoverImageMixIn):
194    """Superclass for all resources (pieces and learning resources)."""
195    global_allow = 1
196    _at_rename_after_creation = True
197
198    security = ClassSecurityInfo()
199
200    security.declarePrivate('manage_afterAdd')
201    def manage_afterAdd(self, item, container):
202        # Replaces the left side portlets with the content type's own action portlet.
203        try:
204            BaseContent.manage_afterAdd(self, item, container)
205        except TypeError:
206            pass # ZEXP import problem
207        mtool = getToolByName(self, 'portal_membership')
208        memberfolder=mtool.getHomeFolder()
209        if memberfolder!=None:
210            try:
211                memberfolder.note_action(item.UID(), item.portal_type, 'afterAdd')
212            except AttributeError:
213                pass # this happens when importing objects from ZEXP files
214        if not hasattr(item.aq_base, 'left_slots'):
215            self._setProperty('left_slots', ['here/portlet_%s_actions/macros/portlet' % item.meta_type.lower(),], 'lines')
216
217    security.declarePrivate('at_post_create_script')
218    def at_post_create_script(self):
219        self.at_post_edit_script()
220
221    security.declarePrivate('at_post_edit_script')
222    def at_post_edit_script(self):
223        self.post_edit_rename()
224        self.post_edit_update_history()
225        self.post_edit_credit_author()
226        self.recalculateAuthors()
227        self.recalculateScore()
228        self.reindexObject()
229
230
231    security.declarePrivate('post_edit_credit_author')
232    def post_edit_credit_author(self):
233        mtool = getToolByName(self, 'portal_membership')
234        memberfolder=mtool.getHomeFolder()
235        if memberfolder!=None:
236            memberfolder.note_action(self.UID(), self.portal_type, 'post_edit')
237
238    security.declarePrivate('post_edit_rename')
239    def post_edit_rename(self):
240        # Store current ID
241        old_id = self.getId();
242        # See what would be the optimal ID currently
243        optimal_new_id = self.generateNewId()
244        if optimal_new_id and optimal_new_id != old_id:
245            execute_rename=True
246            if old_id.startswith(optimal_new_id):
247                # Check that the old_id isn't optimal + "-n" suffix
248                suffix = old_id[len(optimal_new_id):]
249                if suffix[0]=='-' and suffix[1:].isdigit():
250                    execute_rename=False
251            # Attempt rename, adding "-n" so that no duplicates occur
252            if execute_rename:
253                # Ff the new name is a redirector for this object, delete it
254                ob = getattr(self.aq_inner.aq_parent,optimal_new_id,None)
255                if ob and ob.meta_type == 'Redirector':
256                    if ob.redirect_to == self.UID():
257                        self.aq_inner.aq_parent._delObject(optimal_new_id)
258                # Execute rename
259                self._renameAfterCreation()
260                new_id = self.getId();
261                # If the id did change
262                if old_id != new_id and not is_id_in_default_format(old_id):
263                    # Need to create redirection from old URL
264                    red = Redirector(old_id)
265                    red.redirect_to = self.UID()
266                    self.aq_inner.aq_parent._setObject(old_id,red)
267   
268    def dumpme(self,item):
269        """ print """
270        print 'dump: %s' % item
271
272    def amIListType(self, s):
273        """ Checks if s is a list """
274        if type(s) == list:
275            return True
276        else:
277            return False
278
279    def checkTitle(self, obj=None ,title='', objtype=''):
280        """ check if title is not used anywhere in not(deleted, redirector) object, return false if it is """
281        lt=getToolByName(self, 'lemill_tool')
282        return lt.checkTitle(self,obj=obj, title=title, objtype=objtype)
283
284    def shortenedTitle(self, max_length):
285        title = self.Title()
286        if len(title)<=max_length:
287            return title
288        while len(title)>max_length:
289            title=title[:title.rfind(' ')]
290        return title+"..."           
291
292    security.declareProtected(MANAGE_PORTAL, 'refresh_author')
293    def refresh_author(self, REQUEST):
294        """run private recalc script"""
295        self.recalculateAuthors()
296        return REQUEST.RESPONSE.redirect(self.absolute_url())
297
298
299    def Creator(self):
300        """This method is part of Plone Dublin Core.
301        Overridden to give correct values."""
302        auth = self.getAuthors()
303        if len(auth)>0:
304            return self.getAuthors()[0]
305        else:
306            return ''
307
308    def Creators(self):
309        """This is another base method that should provide good values."""
310        return self.getAuthors()
311
312    def Contributors(self):
313        """This is another base method that should provide good values
314        (everyone except first author)"""
315        auth = self.getAuthors()
316        if len(auth)>0:
317            return self.getAuthors()[1:]
318        else:
319            return []
320
321    def amIMaterial(self):
322        """ Returns True if it's a material """
323        return False
324
325    def amIOwner(self):
326        """ check owner of object """
327        roles = self.portal_membership.getAuthenticatedMember().getRolesInContext(self)
328        return 'Owner' in roles
329
330    def canIModerate(self):
331        roles = self.portal_membership.getAuthenticatedMember().getRolesInContext(self)
332        return 'Manager' in roles or 'Reviewer' in roles
333 
334    def getObjectByUID(self, UID=None):
335        """ Return object with this UID or None """
336        uc=getToolByName(self, 'uid_catalog')
337        objlist=uc({'UID':UID})
338        if objlist:
339            return objlist[0].getObject()
340        return None
341
342    def getAuthors(self):
343        """ used to get the list of authors """
344        return self.getField('creators').get(self)
345
346    def getUserGroups(self, allow_none=True):
347        """ this is vocabulary for groups field """
348        req = self.REQUEST
349        ut = getToolByName(self, 'lemill_usertool')
350        groups = ut.getGroupsList(str(req.AUTHENTICATED_USER))
351        if allow_none:
352            mess_not_assigned = _(u'Not assigned to any group')
353            result = [('no_group', mess_not_assigned)]
354        else:
355            result = []
356        if groups:
357            pg = getToolByName(self, 'portal_groups')
358            for g in groups:
359                group_id = g.getGroupId()
360                ga = pg.getGroupareaFolder(group_id)
361                result.append((group_id, ga.TitleOrId()))
362        mess_create_new_group = _(u'...or create a new group:')
363        result.append(('__new_group', mess_create_new_group))
364        return result
365
366
367    def getAuthorsNames(self):
368        """ Get nice user names of authors. """
369        names = []
370        authors = self.getAuthors()
371        for author in authors:
372            auth = author.split(',')
373            for a in auth:
374                try:
375                    member=self.getMember(a)
376                    name = member.NiceName()
377                    names.append(name)
378                except AttributeError:
379                    names.append(a)
380        if not names:
381            return ""
382        return ', '.join(names)
383
384    def getLanguagelist(self):
385        languagelist=self.availableLanguages()
386        return DisplayList(languagelist)
387
388    security.declarePublic('defaultLanguage')
389    def defaultLanguage(self):
390        """ Get logged users default language """
391        member = self.getMember()
392        if member:
393            #lang=member.getLanguage_skills()
394            try:
395                lang=member.getField('language_skills').get(member)
396                if lang:
397                    return lang[0]
398            except AttributeError:
399                raise 'Invalid User',"%s - %s" % (self.whoami(),str(member))
400        return ''
401
402
403    def _prettyFieldName(self, fieldname):
404        """ return a widget's label """
405        f = self.getField(fieldname)
406        if f is None:
407            return fieldname
408        return f.widget.Label(self)
409
410
411
412    security.declareProtected(ModerateContent, 'publish')
413    def publish(self):
414        """ Publish object. Called by script_changeCoverImage.cpy """
415        wtool = getToolByName(self, 'portal_workflow')
416        self.content_status_modify(workflow_action='publish',msg='Resource published')
417        # When publishing a learning resource, the media pieces will inherit the title and tags of their parent learning resource
418        for bodyElement in self.getBodyText():
419            if self.isUid(bodyElement):
420                obj = self.getObjectByUID(bodyElement)
421                if obj:
422                    newTags = obj.getTags()
423                    for tag in self.getTags():
424                        if not tag in newTags:
425                            newTags += (tag,)
426                    if obj.title_or_id().startswith(self.title_or_id() + " - "):
427                        newTitle = obj.title_or_id()
428                    else:
429                        newTitle = self.title_or_id() + " - " + obj.title_or_id()
430                    obj.edit(tags = newTags, title = newTitle)
431
432    security.declareProtected(ModerateContent, 'retract')
433    def retract(self):
434        """Retract published, but keep coverimage as it is"""
435        self.content_status_modify(workflow_action='retract', msg='Resource changed back to draft status')
436        return self
437
438
439    security.declareProtected(MANAGE_PORTAL, 'permaDelete')
440    def permaDelete(self, REQUEST):
441        """Move to trash"""
442        portal_workflow=getToolByName(self, 'portal_workflow')
443        if not portal_workflow.getInfoFor(self,'review_state',None) == 'deleted':
444            return self
445        portal_url = getToolByName(self, 'portal_url')
446        id=self.id
447        context=self.aq_parent
448        portal = portal_url.getPortalObject()
449        trash=portal.trash
450        trash.manage_pasteObjects(context.manage_cutObjects(id))
451        moved=getattr(trash, id)
452        moved.setId(moved.UID())
453        moved.unindexObject()
454        plone_utils=getToolByName(self, 'plone_utils')
455        plone_utils.addPortalMessage(PMF(u'Moved item to trash.'))
456        return REQUEST.RESPONSE.redirect(context.absolute_url())
457
458
459    security.declareProtected(ModerateContent, 'deleteResource')
460    def deleteResource(self, reason=''):
461        """Set reason for deletion, set state to deleted and update catalog"""
462        f = self.getField('deletionReason')
463        f.set(self, reason)
464        self.content_status_modify(workflow_action='delete', msg='Resource deleted')
465        if self.meta_type in MATERIAL_TYPES:
466            self.aliases['(Default)']='base_view'
467        self.reindexObject()
468
469    security.declareProtected(ModerateContent, 'rescue')
470    def rescue(self):
471        """Undelete a resource """
472        if self.meta_type in MATERIAL_TYPES:
473            self.aliases['(Default)']='fullscreen_view'
474        self.undeleteResource()
475
476    security.declareProtected(ModerateContent, 'undeleteResource')
477    def undeleteResource(self):
478        f=self.getField('deletionReason')
479        f.set(self, None)
480        portal_workflow=getToolByName(self, 'portal_workflow')
481        transitions = [x['id'] for x in portal_workflow.getTransitionsFor(self)]
482        if 'restore' in transitions:
483            self.content_status_modify(workflow_action='restore', msg='Resource restored1')
484        elif 'publish' in transitions:
485            self.content_status_modify(workflow_action='publish', msg='Resource restored2')
486        else:
487            plone_utils=getToolByName(self, 'plone_utils')
488            plone_utils.addPortalMessage(PMF(u'Restoration failed.'))
489        self.reindexObject()
490
491
492
493    def download(self, REQUEST, RESPONSE, field):
494        """ download a file """
495        from Products.Archetypes.utils import contentDispositionHeader
496        org_filename = field.getFilename(self)
497        filename = self.Title()
498        pu = getToolByName(self, 'plone_utils')
499        filename = pu.normalizeString(filename)
500        extension = ''
501        if org_filename:        # extract .doc .pdf or something
502            extension = org_filename[org_filename.rfind('.'):]
503            if extension == -1: extension = ''
504        else:                   # try to guess extension
505            ct = field.getContentType(self)
506            mr = getToolByName(self, 'mimetypes_registry')
507            mt = mr.lookup(ct)
508            if mt:              # mt is something like (<mimetype text/plain>,) so we'll take first one
509                extension = mt[0].extensions[0]       # and take first one from here too
510                extension = '.'+extension
511        if extension:
512            filename += extension
513        header_value = contentDispositionHeader('attachment', self.getCharset(), filename=filename)
514        RESPONSE.setHeader("Content-disposition", header_value)
515        file = field.get(self)
516        return file.index_html(REQUEST, RESPONSE)
517
518    def getCollections(self):
519        """ Show collections where object is used """
520        obj_uid = self.UID()
521        res = []
522        q = { 'targetUID': obj_uid }
523        qres = self.reference_catalog(q)
524        for q in qres:
525            v = self.reference_catalog.lookupObject(q.UID)
526            source = v.getSourceObject()
527            if source.meta_type == 'Collection':
528                 res.append(source)
529        return res
530   
531   
532    def isUid(self, chapter):
533        """This is a wrapper to reach ChapterField.isUid()"""
534        return self.getField('bodyText').isUid(chapter)
535
536
537    #########################
538    ###   History  stuff  ###
539    #########################
540
541    security.declarePrivate('post_edit_update_history')
542    def post_edit_update_history(self):
543        history=self.getHistory()
544        changedFields=[]
545        schema=self.Schema()
546
547        if not history:
548            # First edit - object just created
549            for field in schema.editableFields(self):
550                fieldname=field.getName()
551                #if fieldname in form.keys():
552                changedFields.append(fieldname)
553        else:
554            # Subsequent edit - history exists
555            for field in schema.editableFields(self):
556                fieldname=field.getName()
557                #if fieldname in form.keys():
558                if field.getRaw(self) != \
559                       self.__getLatestHistoricalValueForField(fieldname):
560                    changedFields.append(fieldname)
561        if [x for x in changedFields if x not in ('creators', 'modification_date')]:
562            hist = self.getHistory()
563            if hist and hist[0]['_by'] == self.whoami() and time.time() - hist[0]['_timestamp'] < 3600:   # within one hour
564                prev_changes = hist[0].keys()
565                del hist[0]
566                changedFields = [x for x in set(changedFields + prev_changes) if x[0] != '_']
567            self.storeInHistory(changedFields)
568
569    security.declareProtected(MODIFY_CONTENT, 'restoreAVersion')
570    def restoreAVersion(self, timestamp):
571        """Restores an old version of the resource."""
572        histcopy=list(self.getHistory())
573        for x in histcopy:
574            if round(x['_timestamp']) == round(float(timestamp)):
575                changedFields = []
576                schema = self.Schema()
577                for field in schema.editableFields(self):
578                    if x.has_key(field.getName()):
579                        changedFields.append(field.getName())
580                        self.getField(field.getName()).set(self, x[field.getName()])
581                self.storeInHistory(changedFields)
582        if self.portal_type in MATERIAL_TYPES:
583            return self.REQUEST.RESPONSE.redirect('%s/view' % self.absolute_url())
584        else:
585            return self.REQUEST.RESPONSE.redirect(self.absolute_url())
586
587    def __getLatestHistoricalValueForField(self,fieldname):
588        for entry in self.getHistory():
589            if fieldname in entry.keys():
590                return entry[fieldname]
591        return None
592
593    def _storeInHistory(self, entry, timestamp, by=None, summary=None):
594        entry['_by'] = by or ''
595        entry['_timestamp'] = timestamp
596        entry['_summary'] = summary or ''
597        # Newest first!
598        self.getHistory().insert(0, entry)
599
600    security.declarePrivate('storeInHistory')
601    def storeInHistory(self,fields,summary=None,storeAuthor=True):
602        data = dict((key, self.getField(key).getRaw(self)) for key in fields)
603        self._storeInHistory(data, time.time(), storeAuthor and self.whoami(), summary)
604
605    def getTimeForOldHistory(self, timestamp):
606        """ retruns time for the history """
607        timestamp = float(timestamp)
608        historyTime = time.asctime(time.localtime(timestamp))
609        return historyTime
610
611    def getHistoricalFields(self, timestamp, previous=False):
612        """ get historical fields. changed fields from timestamp(including), if previous=True, return previous entry instead """
613        history = self.getHistory()
614        entry = {}
615        curr_timest = 0
616        rec = 0
617        x_previous=history[0]
618        for x in history:
619
620            if float(timestamp) >= float(x['_timestamp']):  # scroll back in time
621                rec = 1                                 # when requested time is here, start looking for fields
622            if not rec:
623                x_previous=x
624                continue
625            if previous:
626                for k in x_previous.keys():
627                    if k.startswith('_'): continue
628                    if entry.has_key(k): continue
629                    entry[k] = x_previous[k]
630            else:
631                for k in x.keys():
632                    if k.startswith('_'): continue
633                    if entry.has_key(k): continue
634                    entry[k] = x[k]
635
636        fields = self.Schema().editableFields(self)
637        for field in fields:
638            fieldname=field.getName()
639            if entry.has_key(fieldname): continue
640            f = self.getField(fieldname)
641            entry[fieldname] = f.getRaw(self)
642        return entry
643
644    def getFieldHistory(self, field, timestamp):
645        """ get ... """
646        history = self.getHistory()
647        for x in history:
648            timestamp = float(timestamp)
649            if round(x['_timestamp']) == round(timestamp):
650                try:
651                    return x[field]
652                except KeyError:
653                    break
654        # there's nothing, scroll back in time
655        rec = 0
656        for x in history:
657            if float(timestamp) >= float(x['_timestamp']):  # scroll back in time
658                rec = 1
659            if not rec: continue
660            if x.has_key(field): return x[field]
661        return "get error, no history?"
662
663    def getDiffFields(self, old_version):
664        """ will return one version of document by timestamp """
665        obj_x = self.getHistoricalFields(old_version)
666        obj_y = self.getHistoricalFields(old_version, previous=True)
667        keys = []
668        diffs = {}
669        for x in obj_x.keys():                  # I don't think it's necessary to merge but you never know
670            if x not in keys: keys.append(x)
671        for x in obj_y.keys():
672            if x not in keys: keys.append(x)
673        for x in keys:
674            if not obj_x.has_key(x):
675                obj_x[x] = None
676            elif not obj_y.has_key(x):
677                obj_y[x] = None
678
679            if obj_x[x] == obj_y[x]: continue
680            if not obj_x[x] and not obj_y[x]: continue # '' vs. None
681            if x == 'modification_date': continue
682            if x == 'coverImage': continue
683            entry = {}
684            entry['name'] = x
685            entry['old'] = obj_x[x]
686            entry['new'] = obj_y[x]
687            diffs[x] = entry
688        for x in diffs.keys():
689            diffs[x]['niceName'] = self._prettyFieldName(diffs[x]['name'])
690        return diffs
691
692    security.declarePrivate('getHistory')
693    def getHistory(self):
694        try:
695            return self.__history
696        except AttributeError:
697            self.__history=PersistentList()
698            return self.__history
699
700    security.declarePrivate('setHistory')
701    def setHistory(self, history):
702        self.__history=history
703
704
705    def getLastEditor(self):
706        """Returns id of last user who edited this object"""
707        try:
708            return self.getHistory()[0]['_by']
709        except IndexError:
710            #print 'bad history, %s: %s' % (self.getId(), self.getHistory())
711            return ''
712
713    def getLatestEditDate(self):
714        """Returns date of last edit or creation date if fails"""
715        hist=self.getHistory()
716        if len(hist)>0:
717            hist=hist[0]
718            if hist.has_key('_timestamp'):
719                return DateTime(hist['_timestamp'])
720        #print 'bad history, %s: %s' % (self.getId(), self.getHistory())
721        return DateTime(self.CreationDate())
722       
723    def getHistoryEntries(self):
724        """Return list of history entries for viewing."""
725        history=self.getHistory()
726        entries=[]
727        wtool = getToolByName(self,'portal_workflow')
728        sort_history = [(event['_timestamp'],event) for event in history]
729        sort_history.sort()
730        history = [event for ts, event in sort_history]
731        for event, version in zip(history, count(1)):
732            entry = {}
733            entry['version'] = version
734            entry['date']=time.asctime(time.localtime(event['_timestamp']))
735            entry['timestamp'] = event['_timestamp']
736            other_member = self.getMember(event['_by'])
737            entry['author']= other_member and other_member.Creator() or ""
738            if '_summary' in event.keys() and event['_summary']:
739                entry['summary']=event['_summary']
740            elif version == 1:
741                entry['summary']="Resource created"
742            else:
743                fields = [self._prettyFieldName(x) for x in event.keys() if not x.startswith('_')]
744                # this line has thrown UnicodeError
745                # XXX: this can screw up everything.
746                #fields = [self._prettyFieldName(x).decode('latin1').encode('utf-8') for x in event.keys() if not x.startswith('_')]
747                mod = ', '.join(fields)
748                entry['summary']="Modified these: %s" % mod
749            entries.append(entry)
750        entries.reverse()
751        return entries
752
753
754    security.declarePrivate('migrate_history')
755    def migrate_history(self):
756        """ migrate_history """
757        hist = self.getHistory()
758        if len(hist) == 0:
759            return
760
761        prev_timestamp = hist[-1]['_timestamp']
762        prev_user = hist[-1]['_by']
763        changed_fields = set(x for x in hist[-1].iterkeys() if x[0] != '_')
764
765        old_hist = list(hist)
766        del hist[:]
767
768        def collect_fields(hist, fields):
769            fields = set(fields)    # it has to be copied as it's destroyed...
770            result = {}
771            for entry in hist:
772                if not fields:
773                    break
774
775                common_fields = fields & set(entry.iterkeys())
776                assert not sum(key in result.keys() for key in common_fields)
777                result.update((key, entry[key]) for key in common_fields)
778                fields -= common_fields
779
780            assert not fields, fields
781            return result
782
783        def add_to_history(old_history, fields):
784            data = collect_fields(old_history, fields)
785            self._storeInHistory(data, old_history[0]['_timestamp'], old_history[0]['_by'], old_history[0]['_summary'])
786
787        for i, entry in zip(count(1), reversed(old_hist[:-1])):
788            f = set(x for x in entry.iterkeys() if x[0] != '_')
789            if [x for x in f if x not in ('creators', 'modification_date')]:
790                if prev_user != entry['_by'] or entry['_timestamp'] - prev_timestamp >= 3600:
791                    add_to_history(old_hist[-i:], changed_fields)
792                    prev_timestamp = entry['_timestamp']
793                    prev_user = entry['_by']
794                    changed_fields = f
795                else:
796                    changed_fields |= f
797        add_to_history(old_hist, changed_fields)
798
799
800    security.declarePrivate('recalculateAuthors')
801    def recalculateAuthors(self, removeAdmin=''):
802        """ Recalculates author order """
803        creators_field = self.getField('creators')
804        creators=creators_field.get(self)
805
806        if getToolByName(self,'portal_workflow').getInfoFor(self,'review_state',None) == 'draft':
807            #print 'draft'
808            return creators
809
810        hist_entries = self.getHistoryEntries()
811        if not hist_entries:
812            #print 'no history entries'
813            return creators
814
815        l = [(ev['timestamp'], ev['author']) for ev in hist_entries if ev['author']]
816        if not l:
817            #print 'no timestamps'
818            return creators
819        original_creator = min(l)[1]
820        now=time.time()
821
822        diffsort = []
823        for event in hist_entries[:-1]:     # no diff for the original version
824            diff = self.getDiffFields(event['timestamp'])
825            if now-event['timestamp']<3600 and event['author']==removeAdmin:
826                print 'Ignored admin modification in %s, made %s seconds ago.' % (self.getId(), int(now-event['timestamp']))
827            else:                                       
828                if diff.has_key('bodyText'):
829                    # ignore modifications by removeAdmin during last hour
830                    old = diff['bodyText']['old']
831                    new = diff['bodyText']['new']
832                    if type(old)==list:
833                        o2=[]
834                        for o in old:
835                            if type(o)==list:
836                                o='\n'.join(o)
837                            o2.append(o)
838                        old='\n'.join(o2)
839                    old=old.split('\n')
840                    if type(new)==list:
841                        n2=[]
842                        for n in new:
843                            if type(n)==list:
844                                n='\n'.join(n)
845                            n2.append(n)
846                        new='\n'.join(n2)
847                    new=new.split('\n')
848                    d = difflib.unified_diff(old, new)
849                    first_chars = [l[0] for l in list(d)[2:]]
850                    plus = first_chars.count('+')
851                    minus = first_chars.count('-')
852                    context = first_chars.count(' ')
853                    chunk = first_chars.count('@')
854                    diffsort.append(((plus - minus, plus), event['author']))
855
856        diffsort.sort()
857        new_creators = remove_duplicates([creator for cmp_val, creator in diffsort])
858        if original_creator in new_creators:
859            new_creators.remove(original_creator)
860        new_creators.insert(0, original_creator)
861        creators_field.set(self, new_creators)
862        return new_creators
863
864    def getRelatedStories(self):
865        """ Returns good Collections with stories """
866        pc = getToolByName(self, 'portal_catalog')
867        query = {'getRawRelatedContent':self.UID(),
868            'getGoodStory':True,
869            'meta_type':'Collection',
870            'sort_on':'Date',
871            'sort_order':'descending'}
872        results = pc.searchResults(query)
873        results = [x for x in results if x.review_state!='deleted']
874        return results
875
876
877
878
879InitializeClass(Resource)
880
881class LearningResource(Resource):
882    """Superclass for all learning resources (material, activity, tool)."""
883
884    security = ClassSecurityInfo()
885
886    def manage_afterAdd(self, item, container):
887        Resource.manage_afterAdd(self, item, container)
888
889    security.declarePrivate('at_post_create_script')
890    def at_post_create_script(self):
891        # check if this is a translation of some object
892        if hasattr(self, 'getTranslation_of'):
893            trans_of=self.getTranslation_of()
894            if trans_of:
895                trans_of.addToTranslations(self.UID())
896                wftool = getToolByName(self, 'portal_workflow')
897                assert wftool.getInfoFor(self,'review_state',None)=='deleted'
898                transitions = [x['id'] for x in wftool.getTransitionsFor(self)]
899                if 'restore' in transitions:
900                    wftool.doActionFor(self, 'restore')
901                elif 'publish' in transitions:
902                    wftool.doActionFor(self, 'publish')
903        self.at_post_edit_script()
904
905    def __updateCoverImage(self):
906        # Set cover image
907        refs = None
908        try:
909            refs = self.getField('refsToImages').get(self)
910        except AttributeError: # just in case...
911            return
912        cover=self.getField('coverImage')
913        has_cover=self.getField('hasCoverImage')
914        if refs:
915            first=refs[0]
916            value = first.getField('file').get(first)
917            cover.set(self,value)
918            has_cover.set(self,True)
919        else:
920            cover.set(self,None)
921            has_cover.set(self,False)
922
923    def getGroupsEditing(self):
924        """ return a list of groups that are this editing material """
925        local_roles = self.get_local_roles()
926        grouptool = getToolByName(self, 'portal_groups')
927        groups = []
928        for role in local_roles:
929            gr = grouptool.getGroupById(role[0])
930            if gr:
931                groups.append(gr)
932        if groups:
933            return groups[0]
934
935    security.declarePublic('canIEdit')
936    def canIEdit(self):
937        mtool = getToolByName(self, 'portal_membership')
938        lmtool = getToolByName(self, 'lemill_usertool')
939        group = self.getField('groups').get(self)
940        if group=='no_group' or not group:
941            return True # This should happen often
942        member = mtool.getAuthenticatedMember()
943        if not member:
944            return False # This shouldn't happen at all
945        membersgroups=member.getGroups()
946        if not membersgroups: # This happens for admins from lower acl_users, they have to try memberfolder for correct list of groups           
947            mf= lmtool.getLeMillMemberFolder(member.id)
948            membersgroups=mf.getGroups()
949        return group in membersgroups
950
951       
952    security.declareProtected(MODIFY_CONTENT, 'setGroupsShared')
953    def setGroupsShared(self, value):
954        """ share a material/resource with a group(s) """
955        # adjust security to members of portal
956        create_new = self.REQUEST.get('new_group_name', '')
957        if create_new and value=='__new_group':
958            # create a new group here
959            from Products.CMFPlone.utils import normalizeString
960            new_group_id = normalizeString(create_new, context=self)
961            gtool = getToolByName(self, 'portal_groups')
962            try:
963                gtool.addGroup(new_group_id, (), ())
964            except KeyError:
965                # duplicate it
966                return
967            blog = gtool.getGroupareaFolder(new_group_id)
968            blog.join_group()
969            blog.setProperties(id=new_group_id, title=create_new, description="")
970            value = new_group_id
971        if value == '__new_group':
972            return
973        f = self.getField('groups')
974        old_value = f.get(self)
975        if old_value:
976            if old_value!='no_group':
977                self.manage_delLocalRoles((old_value,))
978        if value:
979            if value!='no_group':
980                self.manage_setLocalRoles(value, ('CoAuthor',))
981            else:
982                # make sure that all CoAuthors are deleted
983                roles=dict(self.get_local_roles())
984                for (key,dvalue) in roles.items():
985                    if not 'Owner' in dvalue: self.manage_delLocalRoles((key,))
986            f.set(self, value)
987
988
989    #########################
990    ### Translation stuff ###
991    #########################
992
993    # also in LargeSectionFolder:
994    # def start_translation(self, objId=None):
995
996    # and scripts:
997    # createTranslation.cpy
998
999    # getTranslations, getTranslation_of and setTranslations, setTranslation_of are basic archetype generated methods
1000
1001    security.declarePublic('languagesNotTranslated')
1002    def languagesNotTranslated(self):
1003        """ List of languages minus list of existing translations """
1004        transcodes = [x.Language() for x in self.getTranslationsOfOriginal()]
1005        # make sure that original language isn't one of translation options
1006        mother=self.getOriginal()
1007        if mother:
1008            transcodes.append(mother.Language())
1009        else:
1010            transcodes.append(self.Language())
1011        langs = self.availableLanguages()[1:]
1012        return [x for x in langs if x[0] not in transcodes and x[0] != self.Language()]
1013
1014
1015    security.declarePublic('getFieldsToTranslate')
1016    def getFieldsToTranslate(self):
1017        """ Returns fields to show from original when translating, uses list of untranslateables as guide """
1018        fields= self.Schema().filterFields(isMetadata=0)
1019        lif= LANGUAGE_INDEPENDENT_FIELDS+['language', 'translation_of', 'translations']
1020        fields = [x for x in fields if x.__name__ not in lif]
1021        return fields
1022       
1023    security.declarePublic('getTranslationsOfOriginal')
1024    def getTranslationsOfOriginal(self, include_self=True):
1025        """ Returns translations from mother or if no such thing, translations from self. """
1026        if not hasattr(self, 'getTranslation_of'):
1027            return []
1028        wtool = getToolByName(self,'portal_workflow')
1029        mother=self.getTranslation_of()
1030        if mother:
1031            listoft= mother.getField('translations').get(mother)
1032            if include_self:
1033                listoft.append(mother)
1034        else:
1035            listoft= self.getField('translations').get(self)
1036            if include_self:
1037                listoft.append(self)
1038        goodlist=[]
1039        goodlist=[x for x in listoft if wtool.getInfoFor(x,'review_state',None)!='deleted' and x]
1040        if goodlist!=listoft:
1041            if mother:
1042                mother.setTranslations(goodlist)
1043            else:
1044                self.setTranslations(goodlist)
1045        return goodlist
1046
1047    def getOriginal(self):
1048        """ accessor plus check if deleted """
1049        wtool = getToolByName(self,'portal_workflow')
1050        if hasattr(self, 'getTranslation_of'):
1051            mother=self.getTranslation_of()
1052            if mother:
1053                if wtool.getInfoFor(mother,'review_state',None)!='deleted':
1054                    return mother
1055        return False
1056
1057    def prefill_translation(self, to_language='en', base_obj=None):
1058        """ Copy values of some fields to new object and set translation-references """
1059        if base_obj is None:
1060            raise 'base object for translation is not found (prefill_translation)'
1061        mtool = getToolByName(self, 'portal_membership')
1062        wftool = getToolByName(self, 'portal_workflow')
1063        portal_url = getToolByName(self, 'portal_url')
1064        lif=LANGUAGE_INDEPENDENT_FIELDS       
1065
1066        new=self
1067        wftool.doActionFor(new, 'delete')
1068
1069        import copy
1070        for k in base_obj.schema.keys():
1071            # list all fields here that shouldn't be copyied to new object
1072            if k not in lif:
1073                continue
1074            old_accessor = base_obj.schema[k].getEditAccessor(base_obj)
1075            new_mutator = new.schema[k].getMutator(new)
1076            if not old_accessor:
1077                continue
1078            val = old_accessor()
1079            copied_val = None
1080            try:
1081                copied_val = copy.copy(val)
1082            except TypeError:
1083                copied_val = copy.copy(val.aq_base)
1084            new_mutator(copied_val)
1085
1086        realbase=base_obj.getTranslation_of() # if this is a translation of translation
1087        if not realbase:
1088            realbase=base_obj
1089        baseuid=realbase.UID()
1090        realbase.addToTranslations(new.UID())
1091        new.setTranslation_of(baseuid)
1092        new.setLanguage(to_language)
1093        author=mtool.getAuthenticatedMember().getId()
1094        new.setCreators([author])
1095        new.storeInHistory({}, summary='Translation of %s created' % base_obj.title_or_id())
1096               
1097        # give points for original author
1098        authid=base_obj.getAuthorsNames()
1099        memberfolder=mtool.getHomeFolder(authid[0])
1100        if memberfolder!=None:
1101            memberfolder.note_action(base_obj.UID(), base_obj.portal_type, 'new translation')       
1102        return new       
1103
1104    def addToTranslations(self, UID):
1105        """ Makes sure that obj UID is in translations, but doesn't check if this is mother of translations"""
1106        #print 'add to translations with obj %s and UID %s' % (self, UID)
1107        trans_list = self.getTranslations()
1108        trans_list = [x.UID() for x in trans_list if x!=None]
1109        if UID not in trans_list:
1110            trans_list.append(UID)
1111            self.setTranslations(trans_list)       
1112
1113    def removeFromTranslations(self, UID):
1114        """ remove """
1115        trans_list = self.getTranslations()
1116        trans_list = [x.UID() for x in trans_list if x!=None]
1117        if UID in trans_list:
1118            trans_list.remove(UID)
1119            self.setTranslations(trans_list)       
1120
1121
1122    security.declareProtected(MANAGE_PORTAL, 'manage_form_setTranslationOf')
1123    def manage_form_setTranslationOf(self, REQUEST):
1124        """ Allows managers to reorganize which is translated by whom """
1125        ptool = getToolByName(self, "plone_utils")
1126        mothers_id = REQUEST.get('mother_field')
1127        folder = self.getSectionFolder()
1128        old_mother=self.getTranslation_of()
1129        if mothers_id=='' and old_mother:
1130            self.setTranslation_of('')
1131            old_mother.removeFromTranslations(self.UID)
1132        if hasattr(folder, mothers_id):
1133            new_mother = getattr(folder, mothers_id)
1134            if new_mother.portal_type == self.portal_type:
1135                self.setTranslation_of(new_mother)
1136                new_mother.addToTranslations(self.UID())
1137            else:
1138                ptool.addPortalMessage(_(u"Type mismatch, cannot be translation of that kind of object."))
1139        else:
1140            ptool.addPortalMessage(_(u"Object not found."))       
1141        return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
1142
1143    security.declareProtected(MANAGE_PORTAL, 'manage_form_setTranslations')
1144    def manage_form_setTranslations(self, REQUEST):
1145        """ Allows managers to reorganize which is translated by whom """
1146        ptool = getToolByName(self, "plone_utils")
1147        oldlist=self.getTranslations()
1148        oldids=[x.id for x in oldlist]
1149        folder = self.getSectionFolder()
1150       
1151        for n in range(len(oldlist)):
1152            new_id = REQUEST.get('obj_translation%s' % n)
1153            old_id = REQUEST.get('obj_translation_old%s' % n)
1154            if new_id != old_id:
1155                if hasattr(folder, new_id):
1156                    new = getattr(folder, new_id)
1157                    if new.portal_type == self.portal_type:
1158                        self.addToTranslations(new.UID())
1159                        new.setTranslation_of(self)
1160                    else:
1161                        ptool.addPortalMessage(_(u"Type mismatch, cannot be translation of that kind of object."))
1162                else:
1163                    ptool.addPortalMessage(_(u"Object not found."))       
1164                if hasattr(folder, old_id):
1165                    old = getattr(folder, old_id)
1166                    self.removeFromTranslations(old.UID())
1167                    old.setTranslation_of('')
1168
1169        return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
1170
1171
1172
1173
1174    security.declareProtected(MANAGE_PORTAL, 'manage_form_addTranslation')
1175    def manage_form_addTranslation(self, REQUEST):
1176        """ Allows managers to reorganize which is translated by whom """
1177        ptool = getToolByName(self, "plone_utils")
1178        new_addition = REQUEST.get('new_translation')
1179        if not new_addition:
1180            return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
1181        folder = self.getSectionFolder()
1182        if hasattr(folder, new_addition):
1183            new = getattr(folder, new_addition)
1184            if new.portal_type == self.portal_type:
1185                self.addToTranslations(new.UID())
1186                new.setTranslation_of(self)
1187            else:
1188                ptool.addPortalMessage(_(u"Type mismatch, cannot be translation of that kind of object."))
1189        else:
1190            ptool.addPortalMessage(_(u"Object not found."))       
1191        return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
1192
1193    def recalculateScore(self):
1194        """  Recalculates score for LearningResources according to specifications"""
1195        score = 0
1196        collections = self.getCollections()
1197        stories = self.getRelatedStories()
1198        different_members = []
1199
1200        for c in collections:
1201            if c.Creator() not in different_members:
1202                different_members.append(c.Creator())
1203
1204        for member in different_members:
1205            score = score + 1
1206
1207        for story in stories:
1208            score = score + 10
1209
1210        # Let's make sure that score is at least 1
1211        if score<1:
1212            score = 1
1213        # Set the value for field score
1214        self.setScore(score)
1215
1216
1217InitializeClass(LearningResource)
1218
1219class Redirector(BaseContent,CommonMixIn):
1220    """Redirects to new URLs of renamed resources."""
1221
1222    meta_type = "Redirector"
1223    archetype_name = "Redirector"
1224
1225    aliases = {
1226        '(Default)' : 'redirect',
1227        'view'      : 'redirect',
1228    #    'edit'      : 'redirect',
1229    #    'base_view' : 'redirect',
1230    #    'history_view': 'redirect',
1231    }
1232
1233    def __bobo_traverse__(self, REQUEST, entry_name=None):
1234        """ redirect to correct object """
1235        obj = self.redirect()
1236        if entry_name is None:
1237            return obj
1238        try:
1239            l = getattr(obj, entry_name)
1240        except AttributeError:
1241            return BaseContent.__bobo_traverse__(self, REQUEST, entry_name)
1242        if hasattr(l, 'im_func'):
1243            return l()
1244        return l
1245
1246    def redirect(self, REQUEST=None):
1247        """Redirect to current location."""
1248        rc = getToolByName(self, 'reference_catalog')
1249        return rc.lookupObject(self.redirect_to, REQUEST)
1250
1251registerType(Redirector)
Note: See TracBrowser for help on using the repository browser.