source: trunk/LearningResource.py @ 3115

Revision 3115, 16.8 KB checked in by jukka, 9 years ago (diff)

Working with pdf collections.

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 Globals import InitializeClass
20from Products.CMFCore.utils import getToolByName
21from AccessControl import ClassSecurityInfo, Unauthorized
22from messagefactory_ import i18nme as _
23from permissions import ModerateContent, MODIFY_CONTENT, DELETE_CONTENT, MANAGE_PORTAL, ACCESS_CONTENT, VIEW, ADD_CONTENT_PERMISSION
24from Products.CMFPlone.utils import transaction_note
25from Acquisition import aq_inner, aq_parent
26import re, copy
27from Resource import Resource
28from Discussable import Discussable
29from config import LANGUAGES
30
31# This is used by pdf cleaning method prepareForPDF
32#visualNoPrint = re.compile('(<h1>.*?)<div class="visualNoPrint">', re.DOTALL | re.IGNORECASE | re.MULTILINE)
33visualNoPrint = re.compile('<div class="visualNoPrint">.*?</div>', re.DOTALL | re.IGNORECASE | re.MULTILINE)
34visualClear = re.compile('<div class="visualClear">.*?</div>', re.DOTALL | re.IGNORECASE | re.MULTILINE)
35embedContent = re.compile('<div class="embed_content">.*?</div>',re.DOTALL | re.IGNORECASE | re.MULTILINE)
36
37t_license_pattern = re.compile('<p id="license_text_box">.*?</p>', re.MULTILINE | re.DOTALL | re.IGNORECASE)
38license_pattern = re.compile('<!--Creative Commons License-->.*?<!--/Creative Commons License-->', re.DOTALL | re.IGNORECASE | re.MULTILINE)
39
40
41################## Learning Resource #################################################
42
43class LearningResource(Discussable, Resource):
44    """Superclass for all learning resources (material, activity, tool)."""
45
46    security = ClassSecurityInfo()
47    allow_discussion = True
48
49    ################ Messages #################################################
50
51    def getMessages(self):
52        """ Returns permanent messages related to this resource (draft, is missing a language etc.) """
53        messages=Resource.getMessages(self)
54        if self.isDraft():
55            messages.append('draft')
56        if self.isPrivate():
57            messages.append('private')
58        return messages
59
60
61    ################ After add/edit #################################################
62
63    def manage_afterAdd(self, item, container):
64        Resource.manage_afterAdd(self, item, container)
65
66
67    security.declarePrivate('manage_beforeDelete')
68    def manage_beforeDelete(self, item, container):
69        """ When deleting, remove self from lists of translations and remove related discussions. """       
70        storeRefs = getattr(item, '_v_cp_refs', None)
71        if self.Schema().has_key('translation_of'):
72            translation_parent= self.getTranslation_of()
73            if translation_parent and not storeRefs:
74                translation_parent.removeTranslation(self.UID())
75        # Currently it is a bit improbable that just created
76        # object has discussions, but keep it robust for further changes.
77        discussion=self.getDiscussion(do_create=False)
78        if discussion and not storeRefs:
79            discussion.aq_parent.manage_delObjects([discussion.id])
80        # Groups do not refer back to objects, only objects refer to groups, so that doesn't
81        # require additional work
82        Resource.manage_beforeDelete(self, item, container)
83
84    security.declarePrivate('at_post_create_script')
85    def at_post_create_script(self):
86        # check if this is a translation of some object
87        if self.Schema().has_key('translation_of'):
88            trans_of=self.getTranslation_of()
89            if trans_of:
90                trans_of.addToTranslations(self.UID())
91                # by default translation will get state draft and resource is visible to others
92                if self.meta_type in self.getMaterialTypes():
93                    self.setState('draft')
94                    self.setHideDrafts(False)
95                else:
96                    self.setState('public')
97        # check if this is a branch of some object
98        if self.Schema().has_key('branch_of'):
99            branch_of = self.getBranch_of()
100            if branch_of:
101                branch_of.addToBranches(self.UID())
102        self.at_post_edit_script()
103        if self.getState()=='public':
104            self.notifyPublishedResource()
105
106
107    ############ Cover image        #############################################
108
109    def __updateCoverImage(self):
110        # Set cover image
111        refs = None
112        try:
113            refs = self.getField('refsToImages').get(self)
114        except AttributeError: # just in case...
115            return
116        cover=self.getField('coverImage')
117        has_cover=self.getField('hasCoverImage')
118        if refs:
119            first=refs[0]
120            value = first.getField('file').get(first)
121            cover.set(self,value)
122            has_cover.set(self,True)
123        else:
124            cover.set(self,None)
125            has_cover.set(self,False)
126
127
128
129
130    ############ Workflow methods #################################
131
132    def canDeleteOnCancel(self):
133        """ LearningResources have translations that should be deleted if they have been just created,
134            also anything that has no history past the author's changes in ten minutes. """
135        if self.isJustCreated():
136            return True
137        history=self.getHistory()
138        if not history:
139            return True
140        summary=history[0].get('_summary','')
141        if summary.startswith('Translation of '):  # this is a newly created translation
142            return True 
143        return Resource.canDeleteOnCancel(self)
144
145
146    ################# Authorship and groups  ###################################################################
147
148    security.declarePublic('canIEdit')
149    def canIEdit(self, member=None, group=None):
150        """ Optimized version, doesn't use easier get-methods as this gets called so often """
151        lutool = getToolByName(self, 'lemill_usertool')
152        if lutool.isAnonymousUser():
153            return False       
154        if member:
155            member_id=member.getId()
156        else:
157            member_id = lutool.getAuthenticatedId()
158        if self.isBranchable():
159            if not self.branchableEditingCheck():
160                return False
161        if not getattr(self, 'getGroupEditing', False):
162            return True
163        if group:
164            return member_id in group.getGroupMembers
165        group_md=self.getGroupInfo()
166        if not group_md:
167            return True
168        for group in group_md:
169            if member_id in group.getGroupMembers:
170                return True
171        return False
172
173    def getGroupInfo(self):
174        """ Returns catalog metadata of groups editing this content """
175        field=self.getField('groupEditing')
176        if not field:
177            return []
178        uid_list=field.getRaw(self)       
179        if not uid_list:
180            return []
181        pc = getToolByName(self, 'portal_catalog')
182        return pc(UID=uid_list, portal_type='GroupBlog', sort_on='sortable_title')
183               
184    def joinGroupEditing(self, group_uid=None):
185        """ join to a group working on this material """
186        group=None
187        if group_uid:
188            utool = getToolByName(self, 'uid_catalog')
189            results=utool({'UID':group_uid})
190            if results:
191                group=results[0].getObject()           
192        else:
193            groups=self.getGroupEditing()
194            if groups:
195                group=groups[0]
196           
197        if group:
198            group.join_group()
199        return self.REQUEST.RESPONSE.redirect('%s/edit' % self.absolute_url())
200
201
202    ############## Translations  ##############################################################
203
204    # getTranslations, getTranslation_of and setTranslations, setTranslation_of are basic archetype generated methods
205
206    security.declareProtected(ADD_CONTENT_PERMISSION, 'createTranslation')
207    def createTranslation(self, REQUEST):
208        lutool=getToolByName(self, 'lemill_usertool')
209        parent = aq_parent(aq_inner(self))
210        new = parent.lemill_invokeFactory(parent, self.meta_type, id=self.meta_type.lower()+str(id(self)), do_create=True)
211        to_language=REQUEST.form.get('translanguage','')
212        new.setState('deleted')
213        base_obj=self
214
215        for field in base_obj.schema.values():
216            # list all fields here that shouldn't be copyied to new object
217            if not getattr(field, 'copied_in_translation', False) or field.getName() in ['id', 'groupEditing']:
218                continue
219            old_accessor = field.getEditAccessor(base_obj)
220            new_mutator = field.getMutator(new)
221            if not old_accessor:
222                continue
223            val = old_accessor()
224            copied_val = None
225            try:
226                copied_val = copy.copy(val)
227            except TypeError:
228                copied_val = copy.copy(val.aq_base)
229            new_mutator(copied_val)
230
231        realbase=base_obj.getTranslation_of() # if this is a translation of translation
232        if not realbase:
233            realbase=base_obj
234        baseuid=realbase.UID()
235        realbase.addToTranslations(new.UID())
236        new.setTranslation_of(baseuid)
237        new.setLanguage(to_language)
238        author=lutool.getAuthenticatedId()
239        new.setCreators([author])
240        new.storeInHistory({}, summary='Translation of %s created' % base_obj.title_or_id())
241               
242        return REQUEST.RESPONSE.redirect('%s/base_translate' % new.absolute_url())       
243
244
245    security.declarePublic('languagesNotTranslated')
246    def languagesNotTranslated(self):
247        """ List of languages minus list of existing translations """
248        transcodes = set([x.Language() for x in self.getTranslationsOfOriginal(include_original=True)])
249        return [x for x in LANGUAGES[1:] if x[0] not in transcodes]
250
251
252    security.declarePublic('getTranslationsOfOriginal')
253    def getTranslationsOfOriginal(self, include_original=True):
254        """ Returns translations from mother or if no such thing, translations from self. """
255        if not hasattr(self, 'getTranslation_of'):
256            return []
257        original=self.getTranslation_of() or self
258        translations=original.getTranslations()       
259        good_translations = [tr for tr in translations if tr and tr.state!='deleted']
260        # fix lists if there were problems
261        if len(good_translations)<len(translations):
262            original.setTranslations(good_translations)               
263        # include original or self into results
264        if include_original:
265            good_translations.append(original)
266        return good_translations
267
268    def getOriginal(self):
269        """ accessor plus check if deleted """
270        if hasattr(self, 'getTranslation_of'):
271            mother=self.getTranslation_of()
272            if (mother and mother.state != 'deleted'):
273                return mother
274        return False
275
276    def addToTranslations(self, UID):
277        """ Makes sure that obj UID is in translations, but doesn't check if this is mother of translations"""
278        #print 'add to translations with obj %s and UID %s' % (self, UID)
279        trans_list = self.getRawTranslations()
280        if UID not in trans_list:
281            trans_list.append(UID)
282            self.setTranslations(trans_list)       
283
284    def removeFromTranslations(self, UID):
285        """ remove """
286        trans_list = self.getRawTranslations()
287        if UID in trans_list:
288            trans_list.remove(UID)
289            self.setTranslations(trans_list)       
290
291    security.declareProtected(MANAGE_PORTAL, 'manage_form_setTranslationOf')
292    def manage_form_setTranslationOf(self, REQUEST):
293        """ Allows managers to reorganize which is translated by whom """
294        lt = getToolByName(self, "lemill_tool")
295        mothers_id = REQUEST.get('mother_field')
296        folder = self.getSectionFolder()
297        old_mother=self.getTranslation_of()
298        if not mothers_id and old_mother: # Removing mother
299            self.setTranslation_of('')
300            old_mother.removeFromTranslations(self.UID())
301        elif ((not old_mother) or mothers_id!=old_mother.getId()) and hasattr(folder, mothers_id): # Changing mother
302            new_mother = getattr(folder, mothers_id)
303            if new_mother.portal_type == self.portal_type:
304                self.setTranslation_of(new_mother)
305                new_mother.addToTranslations(self.UID())
306                if old_mother:
307                    old_mother.removeFromTranslations(self.UID())
308            else:
309                lt.addPortalMessage("Type mismatch, cannot be translation of that kind of object.")
310        else:
311            lt.addPortalMessage("Object not found.")       
312        return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
313
314    security.declareProtected(MANAGE_PORTAL, 'manage_form_removeTranslations')
315    def manage_form_removeTranslations(self, REQUEST):
316        """ Allows managers to reorganize which is translated by whom """
317        lt = getToolByName(self, "lemill_tool")
318        oldlist=self.getTranslations()       
319        for n, obj in enumerate(oldlist):
320            checked = REQUEST.get('obj_translation%s' % n)
321            if checked:
322                self.removeFromTranslations(obj.UID())
323                obj.setTranslation_of('')
324        return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
325
326    security.declareProtected(MANAGE_PORTAL, 'manage_form_addTranslation')
327    def manage_form_addTranslation(self, REQUEST):
328        """ Allows managers to reorganize which is translated by whom """
329        lt = getToolByName(self, "lemill_tool")
330        new_addition = REQUEST.get('new_translation')
331        if not new_addition:
332            return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
333        folder = self.getSectionFolder()
334        new = getattr(folder, new_addition, None)
335        if new:
336            if new.portal_type == self.portal_type:
337                self.addToTranslations(new.UID())
338                new.setTranslation_of(self)
339            else: 
340                lt.addPortalMessage("text_message_type_mismatch_cant_translate", default="Type mismatch, cannot be translation of that kind of object.")
341        else:
342            lt.addPortalMessage("Object not found.")       
343        return REQUEST.RESPONSE.redirect('%s/manage_translations' % self.absolute_url())
344
345
346    ############ Scoring         #####################################
347
348    def recalculateScore(self):
349        """ Recalculates score for LearningResources according to specifications """
350        # every resource get's score at least one for creating it
351        score = 1
352        # +1 if resource is also published
353        if self.getState() == 'public' and self.getHasCoverImage():
354            score += 1
355        # +5 every additional author
356        authors = self.listCreators()
357        creator = self.Creator()
358        for author in authors:
359            if author != creator:
360                score += 5
361        # +1 every additional change
362        history=self.getHistory()
363        if len(history) > 0:
364            score += len(history) - 1
365        # +1 if resource is in owner's collection
366        # +10 if resource is someone else's collection
367        collections = self.getCollections()
368        for collection in collections:
369            if collection.Creator() == creator:
370                score += 1
371            else:
372                score += 10
373        # +1 if assigned to group
374        group_editing = hasattr(self, 'getGroupEditing')
375        if group_editing:
376            if self.getGroupEditing():
377                score += 1
378        # if resource is webpage or exercise
379        # add +1 for every used media piece and embed block
380        if self.portal_type in ['MultimediaMaterial', 'ExerciseMaterial']:
381            bodytext = self.getBodyText()
382            for chapter in bodytext:
383                if chapter['type'] in ['media_piece', 'embed_block']:
384                    score += 1
385        # Set the value for field score
386        self.setScore(score)
387
388
389
390
391    ############ PDF view ########################################
392
393
394    def prepareForPDF(self):
395        """ Removes license and visualNoPrint"""       
396        text=Resource.prepareForPDF(self)
397        text = re.sub(visualClear, '', text)
398        text = re.sub(embedContent, '', text)
399        text = re.sub(visualNoPrint, '', text)
400        text = re.sub(t_license_pattern, '', text)
401        text = re.sub(license_pattern, '', text)
402        return text
403
404
405InitializeClass(LearningResource)
406
407
Note: See TracBrowser for help on using the repository browser.