source: trunk/Resources.py @ 1901

Revision 1901, 49.7 KB checked in by laszlo, 12 years ago (diff)

fixed #1468 spent 26h

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    def isID(self, s):
570        """ Stupid heuristic to decide s is an id or not. """
571        if len(s) != 32:
572            return False
573        for i in range(len(s)):
574            if s[i] not in ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']:
575                return False
576        return True               
577
578    def correctReferences(self, oldBodyText, newBodyText):
579        """ Delete unused references after restore. """
580        oldIDs = [x for x in oldBodyText if self.isID(x)]
581        newIDs = [x for x in newBodyText if self.isID(x)]
582        real_newIDs = [x for x in newIDs if x not in oldIDs]
583        real_oldIDs = [x for x in oldIDs if x not in newIDs]
584        tool = getToolByName(self, 'reference_catalog')
585        for x in real_oldIDs:
586             tool.deleteReference(self, x, 'uses')   
587
588    security.declareProtected(MODIFY_CONTENT, 'restoreAVersion')
589    def restoreAVersion(self, timestamp):
590        """Restores an old version of the resource."""
591        histcopy=list(self.getHistory())
592        for x in histcopy:
593            if round(x['_timestamp']) == round(float(timestamp)):
594                changedFields = []
595                schema = self.Schema()
596                oldBodyText = self.getBodyText()
597                for field in schema.editableFields(self):
598                    if x.has_key(field.getName()):
599                        changedFields.append(field.getName())
600                        self.getField(field.getName()).set(self, x[field.getName()])
601                self.storeInHistory(changedFields)
602                self.correctReferences(oldBodyText, self.getBodyText())       
603        if self.portal_type in MATERIAL_TYPES:
604            return self.REQUEST.RESPONSE.redirect('%s/view' % self.absolute_url())
605        else:
606            return self.REQUEST.RESPONSE.redirect(self.absolute_url())
607
608    def __getLatestHistoricalValueForField(self,fieldname):
609        for entry in self.getHistory():
610            if fieldname in entry.keys():
611                return entry[fieldname]
612        return None
613
614    def _storeInHistory(self, entry, timestamp, by=None, summary=None):
615        entry['_by'] = by or ''
616        entry['_timestamp'] = timestamp
617        entry['_summary'] = summary or ''
618        # Newest first!
619        self.getHistory().insert(0, entry)
620
621    security.declarePrivate('storeInHistory')
622    def storeInHistory(self,fields,summary=None,storeAuthor=True):
623        data = dict((key, self.getField(key).getRaw(self)) for key in fields)
624        self._storeInHistory(data, time.time(), storeAuthor and self.whoami(), summary)
625
626    def getTimeForOldHistory(self, timestamp):
627        """ retruns time for the history """
628        timestamp = float(timestamp)
629        historyTime = time.asctime(time.localtime(timestamp))
630        return historyTime
631
632    def getHistoricalFields(self, timestamp, previous=False):
633        """ get historical fields. changed fields from timestamp(including), if previous=True, return previous entry instead """
634        history = self.getHistory()
635        entry = {}
636        curr_timest = 0
637        rec = 0
638        x_previous=history[0]
639        for x in history:
640
641            if float(timestamp) >= float(x['_timestamp']):  # scroll back in time
642                rec = 1                                 # when requested time is here, start looking for fields
643            if not rec:
644                x_previous=x
645                continue
646            if previous:
647                for k in x_previous.keys():
648                    if k.startswith('_'): continue
649                    if entry.has_key(k): continue
650                    entry[k] = x_previous[k]
651            else:
652                for k in x.keys():
653                    if k.startswith('_'): continue
654                    if entry.has_key(k): continue
655                    entry[k] = x[k]
656
657        fields = self.Schema().editableFields(self)
658        for field in fields:
659            fieldname=field.getName()
660            if entry.has_key(fieldname): continue
661            f = self.getField(fieldname)
662            entry[fieldname] = f.getRaw(self)
663        return entry
664
665    def getFieldHistory(self, field, timestamp):
666        """ get ... """
667        history = self.getHistory()
668        for x in history:
669            timestamp = float(timestamp)
670            if round(x['_timestamp']) == round(timestamp):
671                try:
672                    return x[field]
673                except KeyError:
674                    break
675        # there's nothing, scroll back in time
676        rec = 0
677        for x in history:
678            if float(timestamp) >= float(x['_timestamp']):  # scroll back in time
679                rec = 1
680            if not rec: continue
681            if x.has_key(field): return x[field]
682        return "get error, no history?"
683
684    def getDiffFields(self, old_version):
685        """ will return one version of document by timestamp """
686        obj_x = self.getHistoricalFields(old_version)
687        obj_y = self.getHistoricalFields(old_version, previous=True)
688        keys = []
689        diffs = {}
690        for x in obj_x.keys():                  # I don't think it's necessary to merge but you never know
691            if x not in keys: keys.append(x)
692        for x in obj_y.keys():
693            if x not in keys: keys.append(x)
694        for x in keys:
695            if not obj_x.has_key(x):
696                obj_x[x] = None
697            elif not obj_y.has_key(x):
698                obj_y[x] = None
699
700            if obj_x[x] == obj_y[x]: continue
701            if not obj_x[x] and not obj_y[x]: continue # '' vs. None
702            if x == 'modification_date': continue
703            if x == 'coverImage': continue
704            entry = {}
705            entry['name'] = x
706            entry['old'] = obj_x[x]
707            entry['new'] = obj_y[x]
708            diffs[x] = entry
709        for x in diffs.keys():
710            diffs[x]['niceName'] = self._prettyFieldName(diffs[x]['name'])
711        return diffs
712
713    security.declarePrivate('getHistory')
714    def getHistory(self):
715        try:
716            return self.__history
717        except AttributeError:
718            self.__history=PersistentList()
719            return self.__history
720
721    security.declarePrivate('setHistory')
722    def setHistory(self, history):
723        self.__history=history
724
725
726    def getLastEditor(self):
727        """Returns id of last user who edited this object"""
728        try:
729            return self.getHistory()[0]['_by']
730        except IndexError:
731            #print 'bad history, %s: %s' % (self.getId(), self.getHistory())
732            return ''
733
734    def getLatestEditDate(self):
735        """Returns date of last edit or creation date if fails"""
736        hist=self.getHistory()
737        if len(hist)>0:
738            hist=hist[0]
739            if hist.has_key('_timestamp'):
740                return DateTime(hist['_timestamp'])
741        #print 'bad history, %s: %s' % (self.getId(), self.getHistory())
742        return DateTime(self.CreationDate())
743       
744    def getHistoryEntries(self):
745        """Return list of history entries for viewing."""
746        history=self.getHistory()
747        entries=[]
748        wtool = getToolByName(self,'portal_workflow')
749        sort_history = [(event['_timestamp'],event) for event in history]
750        sort_history.sort()
751        history = [event for ts, event in sort_history]
752        for event, version in zip(history, count(1)):
753            entry = {}
754            entry['version'] = version
755            entry['date']=time.asctime(time.localtime(event['_timestamp']))
756            entry['timestamp'] = event['_timestamp']
757            other_member = self.getMember(event['_by'])
758            entry['author']= other_member and other_member.Creator() or ""
759            if '_summary' in event.keys() and event['_summary']:
760                entry['summary']=event['_summary']
761            elif version == 1:
762                entry['summary']="Resource created"
763            else:
764                fields = [self._prettyFieldName(x) for x in event.keys() if not x.startswith('_')]
765                # this line has thrown UnicodeError
766                # XXX: this can screw up everything.
767                #fields = [self._prettyFieldName(x).decode('latin1').encode('utf-8') for x in event.keys() if not x.startswith('_')]
768                mod = ', '.join(fields)
769                entry['summary']="Modified these: %s" % mod
770            entries.append(entry)
771        entries.reverse()
772        return entries
773
774
775    security.declarePrivate('migrate_history')
776    def migrate_history(self):
777        """ migrate_history """
778        hist = self.getHistory()
779        if len(hist) == 0:
780            return
781
782        prev_timestamp = hist[-1]['_timestamp']
783        prev_user = hist[-1]['_by']
784        changed_fields = set(x for x in hist[-1].iterkeys() if x[0] != '_')
785
786        old_hist = list(hist)
787        del hist[:]
788
789        def collect_fields(hist, fields):
790            fields = set(fields)    # it has to be copied as it's destroyed...
791            result = {}
792            for entry in hist:
793                if not fields:
794                    break
795
796                common_fields = fields & set(entry.iterkeys())
797                assert not sum(key in result.keys() for key in common_fields)
798                result.update((key, entry[key]) for key in common_fields)
799                fields -= common_fields
800
801            assert not fields, fields
802            return result
803
804        def add_to_history(old_history, fields):
805            data = collect_fields(old_history, fields)
806            self._storeInHistory(data, old_history[0]['_timestamp'], old_history[0]['_by'], old_history[0]['_summary'])
807
808        for i, entry in zip(count(1), reversed(old_hist[:-1])):
809            f = set(x for x in entry.iterkeys() if x[0] != '_')
810            if [x for x in f if x not in ('creators', 'modification_date')]:
811                if prev_user != entry['_by'] or entry['_timestamp'] - prev_timestamp >= 3600:
812                    add_to_history(old_hist[-i:], changed_fields)
813                    prev_timestamp = entry['_timestamp']
814                    prev_user = entry['_by']
815                    changed_fields = f
816                else:
817                    changed_fields |= f
818        add_to_history(old_hist, changed_fields)
819
820
821    security.declarePrivate('recalculateAuthors')
822    def recalculateAuthors(self, removeAdmin=''):
823        """ Recalculates author order """
824        creators_field = self.getField('creators')
825        creators=creators_field.get(self)
826
827        if getToolByName(self,'portal_workflow').getInfoFor(self,'review_state',None) == 'draft':
828            #print 'draft'
829            return creators
830
831        hist_entries = self.getHistoryEntries()
832        if not hist_entries:
833            #print 'no history entries'
834            return creators
835
836        l = [(ev['timestamp'], ev['author']) for ev in hist_entries if ev['author']]
837        if not l:
838            #print 'no timestamps'
839            return creators
840        original_creator = min(l)[1]
841        now=time.time()
842
843        diffsort = []
844        for event in hist_entries[:-1]:     # no diff for the original version
845            diff = self.getDiffFields(event['timestamp'])
846            if now-event['timestamp']<3600 and event['author']==removeAdmin:
847                print 'Ignored admin modification in %s, made %s seconds ago.' % (self.getId(), int(now-event['timestamp']))
848            else:                                       
849                if diff.has_key('bodyText'):
850                    # ignore modifications by removeAdmin during last hour
851                    old = diff['bodyText']['old']
852                    new = diff['bodyText']['new']
853                    if type(old)==list:
854                        o2=[]
855                        for o in old:
856                            if type(o)==list:
857                                o='\n'.join(o)
858                            o2.append(o)
859                        old='\n'.join(o2)
860                    old=old.split('\n')
861                    if type(new)==list:
862                        n2=[]
863                        for n in new:
864                            if type(n)==list:
865                                n='\n'.join(n)
866                            n2.append(n)
867                        new='\n'.join(n2)
868                    new=new.split('\n')
869                    d = difflib.unified_diff(old, new)
870                    first_chars = [l[0] for l in list(d)[2:]]
871                    plus = first_chars.count('+')
872                    minus = first_chars.count('-')
873                    context = first_chars.count(' ')
874                    chunk = first_chars.count('@')
875                    diffsort.append(((plus - minus, plus), event['author']))
876
877        diffsort.sort()
878        new_creators = remove_duplicates([creator for cmp_val, creator in diffsort])
879        if original_creator in new_creators:
880            new_creators.remove(original_creator)
881        new_creators.insert(0, original_creator)
882        creators_field.set(self, new_creators)
883        return new_creators
884
885    def getRelatedStories(self):
886        """ Returns good Collections with stories """
887        pc = getToolByName(self, 'portal_catalog')
888        query = {'getRawRelatedContent':self.UID(),
889            'getGoodStory':True,
890            'meta_type':'Collection',
891            'sort_on':'Date',
892            'sort_order':'descending'}
893        results = pc.searchResults(query)
894        results = [x for x in results if x.review_state!='deleted']
895        return results
896
897
898
899
900InitializeClass(Resource)
901
902class LearningResource(Resource):
903    """Superclass for all learning resources (material, activity, tool)."""
904
905    security = ClassSecurityInfo()
906
907    def manage_afterAdd(self, item, container):
908        Resource.manage_afterAdd(self, item, container)
909
910    security.declarePrivate('at_post_create_script')
911    def at_post_create_script(self):
912        # check if this is a translation of some object
913        if hasattr(self, 'getTranslation_of'):
914            trans_of=self.getTranslation_of()
915            if trans_of:
916                trans_of.addToTranslations(self.UID())
917                wftool = getToolByName(self, 'portal_workflow')
918                assert wftool.getInfoFor(self,'review_state',None)=='deleted'
919                transitions = [x['id'] for x in wftool.getTransitionsFor(self)]
920                if 'restore' in transitions:
921                    wftool.doActionFor(self, 'restore')
922                elif 'publish' in transitions:
923                    wftool.doActionFor(self, 'publish')
924        self.at_post_edit_script()
925
926    def __updateCoverImage(self):
927        # Set cover image
928        refs = None
929        try:
930            refs = self.getField('refsToImages').get(self)
931        except AttributeError: # just in case...
932            return
933        cover=self.getField('coverImage')
934        has_cover=self.getField('hasCoverImage')
935        if refs:
936            first=refs[0]
937            value = first.getField('file').get(first)
938            cover.set(self,value)
939            has_cover.set(self,True)
940        else:
941            cover.set(self,None)
942            has_cover.set(self,False)
943
944    def getGroupsEditing(self):
945        """ return a list of groups that are this editing material """
946        local_roles = self.get_local_roles()
947        grouptool = getToolByName(self, 'portal_groups')
948        groups = []
949        for role in local_roles:
950            gr = grouptool.getGroupById(role[0])
951            if gr:
952                groups.append(gr)
953        if groups:
954            return groups[0]
955
956    security.declarePublic('canIEdit')
957    def canIEdit(self):
958        mtool = getToolByName(self, 'portal_membership')
959        lmtool = getToolByName(self, 'lemill_usertool')
960        group = self.getField('groups').get(self)
961        if group=='no_group' or not group:
962            return True # This should happen often
963        member = mtool.getAuthenticatedMember()
964        if not member:
965            return False # This shouldn't happen at all
966        membersgroups=member.getGroups()
967        if not membersgroups: # This happens for admins from lower acl_users, they have to try memberfolder for correct list of groups           
968            mf= lmtool.getLeMillMemberFolder(member.id)
969            membersgroups=mf.getGroups()
970        return group in membersgroups
971
972       
973    security.declareProtected(MODIFY_CONTENT, 'setGroupsShared')
974    def setGroupsShared(self, value):
975        """ share a material/resource with a group(s) """
976        # adjust security to members of portal
977        create_new = self.REQUEST.get('new_group_name', '')
978        if create_new and value=='__new_group':
979            # create a new group here
980            from Products.CMFPlone.utils import normalizeString
981            new_group_id = normalizeString(create_new, context=self)
982            gtool = getToolByName(self, 'portal_groups')
983            try:
984                gtool.addGroup(new_group_id, (), ())
985            except KeyError:
986                # duplicate it
987                return
988            blog = gtool.getGroupareaFolder(new_group_id)
989            blog.join_group()
990            blog.setProperties(id=new_group_id, title=create_new, description="")
991            value = new_group_id
992        if value == '__new_group':
993            return
994        f = self.getField('groups')
995        old_value = f.get(self)
996        if old_value:
997            if old_value!='no_group':
998                self.manage_delLocalRoles((old_value,))
999        if value:
1000            if value!='no_group':
1001                self.manage_setLocalRoles(value, ('CoAuthor',))
1002            else:
1003                # make sure that all CoAuthors are deleted
1004                roles=dict(self.get_local_roles())
1005                for (key,dvalue) in roles.items():
1006                    if not 'Owner' in dvalue: self.manage_delLocalRoles((key,))
1007            f.set(self, value)
1008
1009
1010    #########################
1011    ### Translation stuff ###
1012    #########################
1013
1014    # also in LargeSectionFolder:
1015    # def start_translation(self, objId=None):
1016
1017    # and scripts:
1018    # createTranslation.cpy
1019
1020    # getTranslations, getTranslation_of and setTranslations, setTranslation_of are basic archetype generated methods
1021
1022    security.declarePublic('languagesNotTranslated')
1023    def languagesNotTranslated(self):
1024        """ List of languages minus list of existing translations """
1025        transcodes = [x.Language() for x in self.getTranslationsOfOriginal()]
1026        # make sure that original language isn't one of translation options
1027        mother=self.getOriginal()
1028        if mother:
1029            transcodes.append(mother.Language())
1030        else:
1031            transcodes.append(self.Language())
1032        langs = self.availableLanguages()[1:]
1033        return [x for x in langs if x[0] not in transcodes and x[0] != self.Language()]
1034
1035
1036    security.declarePublic('getFieldsToTranslate')
1037    def getFieldsToTranslate(self):
1038        """ Returns fields to show from original when translating, uses list of untranslateables as guide """
1039        fields= self.Schema().filterFields(isMetadata=0)
1040        lif= LANGUAGE_INDEPENDENT_FIELDS+['language', 'translation_of', 'translations']
1041        fields = [x for x in fields if x.__name__ not in lif]
1042        return fields
1043       
1044    security.declarePublic('getTranslationsOfOriginal')
1045    def getTranslationsOfOriginal(self, include_self=True):
1046        """ Returns translations from mother or if no such thing, translations from self. """
1047        if not hasattr(self, 'getTranslation_of'):
1048            return []
1049        wtool = getToolByName(self,'portal_workflow')
1050        mother=self.getTranslation_of()
1051        if mother:
1052            listoft= mother.getField('translations').get(mother)
1053            if include_self:
1054                listoft.append(mother)
1055        else:
1056            listoft= self.getField('translations').get(self)
1057            if include_self:
1058                listoft.append(self)
1059        goodlist=[]
1060        goodlist=[x for x in listoft if wtool.getInfoFor(x,'review_state',None)!='deleted' and x]
1061        if goodlist!=listoft:
1062            if mother:
1063                mother.setTranslations(goodlist)
1064            else:
1065                self.setTranslations(goodlist)
1066        return goodlist
1067
1068    def getOriginal(self):
1069        """ accessor plus check if deleted """
1070        wtool = getToolByName(self,'portal_workflow')
1071        if hasattr(self, 'getTranslation_of'):
1072            mother=self.getTranslation_of()
1073            if mother:
1074                if wtool.getInfoFor(mother,'review_state',None)!='deleted':
1075                    return mother
1076        return False
1077
1078    def prefill_translation(self, to_language='en', base_obj=None):
1079        """ Copy values of some fields to new object and set translation-references """
1080        if base_obj is None:
1081            raise 'base object for translation is not found (prefill_translation)'
1082        mtool = getToolByName(self, 'portal_membership')
1083        wftool = getToolByName(self, 'portal_workflow')
1084        portal_url = getToolByName(self, 'portal_url')
1085        lif=LANGUAGE_INDEPENDENT_FIELDS       
1086
1087        new=self
1088        wftool.doActionFor(new, 'delete')
1089
1090        import copy
1091        for k in base_obj.schema.keys():
1092            # list all fields here that shouldn't be copyied to new object
1093            if k not in lif:
1094                continue
1095            old_accessor = base_obj.schema[k].getEditAccessor(base_obj)
1096            new_mutator = new.schema[k].getMutator(new)
1097            if not old_accessor:
1098                continue
1099            val = old_accessor()
1100            copied_val = None
1101            try:
1102                copied_val = copy.copy(val)
1103            except TypeError:
1104                copied_val = copy.copy(val.aq_base)
1105            new_mutator(copied_val)
1106
1107        realbase=base_obj.getTranslation_of() # if this is a translation of translation
1108        if not realbase:
1109            realbase=base_obj
1110        baseuid=realbase.UID()
1111        realbase.addToTranslations(new.UID())
1112        new.setTranslation_of(baseuid)
1113        new.setLanguage(to_language)
1114        author=mtool.getAuthenticatedMember().getId()
1115        new.setCreators([author])
1116        new.storeInHistory({}, summary='Translation of %s created' % base_obj.title_or_id())
1117               
1118        # give points for original author
1119        authid=base_obj.getAuthorsNames()
1120        memberfolder=mtool.getHomeFolder(authid[0])
1121        if memberfolder!=None:
1122            memberfolder.note_action(base_obj.UID(), base_obj.portal_type, 'new translation')       
1123        return new       
1124
1125    def addToTranslations(self, UID):
1126        """ Makes sure that obj UID is in translations, but doesn't check if this is mother of translations"""
1127        #print 'add to translations with obj %s and UID %s' % (self, UID)
1128        trans_list = self.getTranslations()
1129        trans_list = [x.UID() for x in trans_list if x!=None]
1130        if UID not in trans_list:
1131            trans_list.append(UID)
1132            self.setTranslations(trans_list)       
1133
1134    def removeFromTranslations(self, UID):
1135        """ remove """
1136        trans_list = self.getTranslations()
1137        trans_list = [x.UID() for x in trans_list if x!=None]
1138        if UID in trans_list:
1139            trans_list.remove(UID)
1140            self.setTranslations(trans_list)       
1141
1142
1143    security.declareProtected(MANAGE_PORTAL, 'manage_form_setTranslationOf')
1144    def manage_form_setTranslationOf(self, REQUEST):
1145        """ Allows managers to reorganize which is translated by whom """
1146        ptool = getToolByName(self, "plone_utils")
1147        mothers_id = REQUEST.get('mother_field')
1148        folder = self.getSectionFolder()
1149        old_mother=self.getTranslation_of()
1150        if mothers_id=='' and old_mother:
1151            self.setTranslation_of('')
1152            old_mother.removeFromTranslations(self.UID)
1153        if hasattr(folder, mothers_id):
1154            new_mother = getattr(folder, mothers_id)
1155            if new_mother.portal_type == self.portal_type:
1156                self.setTranslation_of(new_mother)
1157                new_mother.addToTranslations(self.UID())
1158            else:
1159                ptool.addPortalMessage(_(u"Type mismatch, cannot be translation of that kind of object."))
1160        else:
1161            ptool.addPortalMessage(_(u"Object not found."))       
1162        return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
1163
1164    security.declareProtected(MANAGE_PORTAL, 'manage_form_setTranslations')
1165    def manage_form_setTranslations(self, REQUEST):
1166        """ Allows managers to reorganize which is translated by whom """
1167        ptool = getToolByName(self, "plone_utils")
1168        oldlist=self.getTranslations()
1169        oldids=[x.id for x in oldlist]
1170        folder = self.getSectionFolder()
1171       
1172        for n in range(len(oldlist)):
1173            new_id = REQUEST.get('obj_translation%s' % n)
1174            old_id = REQUEST.get('obj_translation_old%s' % n)
1175            if new_id != old_id:
1176                if hasattr(folder, new_id):
1177                    new = getattr(folder, new_id)
1178                    if new.portal_type == self.portal_type:
1179                        self.addToTranslations(new.UID())
1180                        new.setTranslation_of(self)
1181                    else:
1182                        ptool.addPortalMessage(_(u"Type mismatch, cannot be translation of that kind of object."))
1183                else:
1184                    ptool.addPortalMessage(_(u"Object not found."))       
1185                if hasattr(folder, old_id):
1186                    old = getattr(folder, old_id)
1187                    self.removeFromTranslations(old.UID())
1188                    old.setTranslation_of('')
1189
1190        return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
1191
1192
1193
1194
1195    security.declareProtected(MANAGE_PORTAL, 'manage_form_addTranslation')
1196    def manage_form_addTranslation(self, REQUEST):
1197        """ Allows managers to reorganize which is translated by whom """
1198        ptool = getToolByName(self, "plone_utils")
1199        new_addition = REQUEST.get('new_translation')
1200        if not new_addition:
1201            return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
1202        folder = self.getSectionFolder()
1203        if hasattr(folder, new_addition):
1204            new = getattr(folder, new_addition)
1205            if new.portal_type == self.portal_type:
1206                self.addToTranslations(new.UID())
1207                new.setTranslation_of(self)
1208            else:
1209                ptool.addPortalMessage(_(u"Type mismatch, cannot be translation of that kind of object."))
1210        else:
1211            ptool.addPortalMessage(_(u"Object not found."))       
1212        return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
1213
1214    def recalculateScore(self):
1215        """  Recalculates score for LearningResources according to specifications"""
1216        score = 0
1217        collections = self.getCollections()
1218        stories = self.getRelatedStories()
1219        different_members = []
1220
1221        for c in collections:
1222            if c.Creator() not in different_members:
1223                different_members.append(c.Creator())
1224
1225        for member in different_members:
1226            score = score + 1
1227
1228        for story in stories:
1229            score = score + 10
1230
1231        # Let's make sure that score is at least 1
1232        if score<1:
1233            score = 1
1234        # Set the value for field score
1235        self.setScore(score)
1236
1237
1238InitializeClass(LearningResource)
1239
1240class Redirector(BaseContent,CommonMixIn):
1241    """Redirects to new URLs of renamed resources."""
1242
1243    meta_type = "Redirector"
1244    archetype_name = "Redirector"
1245
1246    aliases = {
1247        '(Default)' : 'redirect',
1248        'view'      : 'redirect',
1249    #    'edit'      : 'redirect',
1250    #    'base_view' : 'redirect',
1251    #    'history_view': 'redirect',
1252    }
1253
1254    def __bobo_traverse__(self, REQUEST, entry_name=None):
1255        """ redirect to correct object """
1256        obj = self.redirect()
1257        if entry_name is None:
1258            return obj
1259        try:
1260            l = getattr(obj, entry_name)
1261        except AttributeError:
1262            return BaseContent.__bobo_traverse__(self, REQUEST, entry_name)
1263        if hasattr(l, 'im_func'):
1264            return l()
1265        return l
1266
1267    def redirect(self, REQUEST=None):
1268        """Redirect to current location."""
1269        rc = getToolByName(self, 'reference_catalog')
1270        return rc.lookupObject(self.redirect_to, REQUEST)
1271
1272registerType(Redirector)
Note: See TracBrowser for help on using the repository browser.