source: trunk/LearningResource.py @ 3065

Revision 3065, 16.6 KB checked in by jukka, 9 years ago (diff)

Fixing stylesheets for edit pages.

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