source: trunk/Collection.py @ 1833

Revision 1833, 24.5 KB checked in by jukka, 12 years ago (diff)

Closed #1423, spent 5h. Adds field 'score' to every resource type. If you have lemill.net's Data.fs it is better to do archetype's update schema on resource type at a time. Didn't work for me if tried to do all at once. After archetype update it is better to reindex catalog and restart zope, as it has probably reserved huge amounts of memory.

Line 
1# Copyright 2006 by the LeMill Team (see AUTHORS)
2#
3# This file is part of LeMill.
4#
5# LeMill is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# LeMill is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with LeMill; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18
19from Products.Archetypes.public import *
20from Products.ATReferenceBrowserWidget.ATReferenceBrowserWidget import ReferenceBrowserWidget
21from Products.CMFCore.permissions import ModifyPortalContent
22from Products.Archetypes.public import BaseContent, registerType, BaseFolder
23from Products.Archetypes.public import StringField
24from Products.Archetypes.public import TextAreaWidget
25from Products.Archetypes.atapi import DisplayList
26from Globals import InitializeClass
27from Products.CMFCore.utils import getToolByName
28from AccessControl import ClassSecurityInfo, Unauthorized
29from SharedMetadata import score
30
31from config import PROJECTNAME, ALL_CONTENT_TYPES, CONTENT_TYPES, DEFAULT_ICONS, ACTIVITY_TYPES, TOOLS_TYPES
32from Resources import Resource
33from permissions import MODIFY_CONTENT
34
35import sre
36from cStringIO import StringIO
37import zipfile
38from urllib2 import urlopen, HTTPError
39
40schema = BaseSchema + score + Schema((
41
42    StringField('description',
43        accessor='Description',
44        widget=TextAreaWidget(
45            label="Learning and teaching story",
46            description="",
47            label_msgid='label_learning_story',
48            description_msgid='description_description',
49            i18n_domain = "lemill",
50            visible={'view':'invisible','edit':'visible'},
51        ),
52    ),
53   
54    ReferenceField('refsToResources',
55        accessor = 'getRefsToResources',
56        relationship = 'References',
57        mutator = 'addRefsToResources',
58        allowed_types = ALL_CONTENT_TYPES,
59        multiValued = True,
60        widget = ReferenceBrowserWidget(
61            visible = {'view':'invisible', 'edit':'invisible'},
62            ),
63    ),
64    ReferenceField('relatedContent',
65        relationship = 'relatesToContent',
66        multiValued = True,
67        isMetadata = True,
68        languageIndependent = False,
69        index = 'KeywordIndex',
70        write_permission = ModifyPortalContent,
71        allowed_types= ALL_CONTENT_TYPES,
72        widget = ReferenceBrowserWidget(
73            allow_search = True,
74            allow_browse = True,
75            show_indexes = False,
76            force_close_on_insert = True,
77            startup_directory = "content",
78            size = 4,
79            i18n_domain = "lemill",
80            label = "Related Content",
81            label_msgid = "label_related_content",
82            description = "",
83            description_msgid = "help_story_related_content",
84            visible = {'edit' : 'invisible', 'view' : 'invisible' }
85            )
86        ),
87    ReferenceField('relatedMethods',
88        relationship = 'relatesToMethods',
89        multiValued = True,
90        isMetadata = True,
91        languageIndependent = False,
92        index = 'KeywordIndex',
93        write_permission = ModifyPortalContent,
94        allowed_types=('Document','Activity',),
95        widget = ReferenceBrowserWidget(
96            allow_search = True,
97            allow_browse = True,
98            show_indexes = False,
99            force_close_on_insert = True,
100            startup_directory = "methods",
101            size = 4,
102            i18n_domain = "lemill",
103            label = "Related Methods",
104            label_msgid = "label_related_methods",
105            description = "",
106            description_msgid = "help_story_related_methods",
107            visible = {'edit' : 'invisible', 'view' : 'invisible' }
108            )
109        ),
110    ReferenceField('relatedTools',
111        relationship = 'relatesToTools',
112        multiValued = True,
113        isMetadata = True,
114        languageIndependent = False,
115        index = 'KeywordIndex',
116        write_permission = ModifyPortalContent,
117        allowed_types=('Document','Tool',),
118        widget = ReferenceBrowserWidget(
119            allow_search = True,
120            allow_browse = True,
121            show_indexes = False,
122            force_close_on_insert = True,
123            startup_directory = "tools",
124            size = 4,
125
126            i18n_domain = "lemill",
127            label = "Related Tools",
128            label_msgid = "label_related_tools",
129            description = "",
130            description_msgid = "help_story_related_tools",
131            visible = {'edit' : 'invisible', 'view' : 'invisible' }
132            )
133        ),
134    ComputedField('goodStory',
135        index = 'FieldIndex',
136        expression = 'here.isThisGoodStory()',
137        isMetadata = True
138        )
139))
140
141schema = schema.copy()
142
143class Collection(Resource):
144    """Collection"""
145   
146    schema = schema
147    actions= (
148    {
149    'id':'view',
150    'name':'view',
151    'action':'string:${object_url}/collection_view',
152    'permission':('View',),
153    },
154    {
155    'id':'edit',
156    'name':'Edit',
157    'action':'string:${object_url}/base_edit',
158    'permission':('View',),
159    },
160    )
161    meta_type = "Collection"
162    archetype_name = "Collection" 
163    typeDescription="Collection of resources."
164    typeDescMsgId='description_collection'
165    global_allow = 1
166    security = ClassSecurityInfo()
167    _at_rename_after_creation = True
168
169    def at_post_create_script(self):
170        self.at_post_edit_script()
171
172    def at_post_edit_script(self):
173        self._renameAfterCreation()
174        self.setGoodStory()
175
176    def after_add_rename(self):
177        self._renameAfterCreation()
178       
179    def setGoodStory(self):
180        """ Update goodStory -flag """
181        # after trying to get field and then setting the field by recalculating self.isThisGoodStory()
182        # it appears to be *always* up-to-date. (field.get()==self.isThisGoodStory())
183        # so the problem is indexing.
184        self.reindexObject()
185               
186#        field = self.getField('goodStory')
187#        wasgood=field.get(self)
188#        isgood= self.isThisGoodStory()
189#        print 'setting story %s. wasgood:%s isgood:%s' % (self.getId(), wasgood, isgood)
190#        ct = getToolByName(self,'portal_catalog')
191#        print ct({'getGoodStory':True})
192#        self.reindexObject()
193#        print ct({'getGoodStory':True})
194#        if wasgood!=isgood:
195#            field.set(self, isgood)
196#            self.reindexObject()
197#            ct = self.getToolByName('portal_catalog')
198           
199
200    def getLargestCount(self, reftype='relatedContent'):
201        field = self.Schema().get(reftype)
202        rtype=field.relationship
203        f = self.getReferenceImpl(relationship=rtype)
204
205        highest = 1001
206        for a in f:
207            try:
208                highest = a.collection_position
209            except AttributeError:
210                pass
211        highest += 100
212        return highest
213
214
215    security.declareProtected(MODIFY_CONTENT,'addRelatedContent')
216    def addRelatedContent(self, ref, empty=0):
217        """ add reference to content """
218        self.addRefsToResources(ref, empty, reftype="relatedContent")
219        self.setGoodStory()
220
221    security.declareProtected(MODIFY_CONTENT,'addRelatedMethods')
222    def addRelatedMethods(self, ref, empty=0):
223        """ add reference to methods """
224        self.addRefsToResources(ref, empty, reftype="relatedMethods")
225        self.setGoodStory()
226
227    security.declareProtected(MODIFY_CONTENT,'addRelatedTools')
228    def addRelatedTools(self, ref, empty=0):
229        """ add reference to tools """
230        self.addRefsToResources(ref, empty, reftype="relatedTools")
231        self.setGoodStory()
232
233    security.declareProtected(MODIFY_CONTENT,'addRefsToResources')
234    def addRefsToResources(self, ref, empty=0, reftype=None):
235        """ add reference to resource """
236        from types import ListType, TupleType
237        if reftype:
238            field = self.Schema().get(reftype)
239        else:
240            if type(ref) == ListType or type(ref) == TupleType:
241                for x in ref: # if entry is a list of mixed type entries, run this method again for each component.
242                    self.addRefsToResources(x)
243                return 1
244            uc=getToolByName(self, 'uid_catalog')
245            obj=uc(UID=ref)
246            objtype=obj[0].portal_type
247            if objtype in CONTENT_TYPES:
248                reftype='relatedContent'
249            elif objtype in ACTIVITY_TYPES:
250                reftype='relatedMethods'
251            elif objtype in TOOLS_TYPES:
252                reftype='relatedTools'
253            else:
254                reftype='refsToResources'
255            field = self.Schema().get(reftype)
256
257        prev = field.getRaw(self)
258        new_value = []
259        if type(ref) == ListType or type(ref) == TupleType:
260            new_value = prev
261            if empty:
262                new_value = []
263            for x in ref:
264                new_value.append(x)
265        else:
266            new_value = prev+[ref,]
267        field.set(self, new_value, collection_position=self.getLargestCount(reftype))
268        self.setGoodStory()
269
270    def isThisGoodStory(self):
271        """ Check if there is content,method,tool and description """       
272        desc=self.Description()
273        c=self.getRelatedContent()
274        m=self.getRelatedMethods()
275        t=self.getRelatedTools()
276        wtool=getToolByName(self, 'portal_workflow')
277        r= wtool.getInfoFor(self,'review_state',None)!='deleted'
278        if desc and c and m and t and r:
279            return True
280        else:
281            return False
282
283    def getItemCount(self):
284        """ return how many items are in collection """
285        return len(self.getRelatedMethods())+len(self.getRelatedTools())+len(self.getRelatedContent())
286
287
288    def getRelatedContent(self):
289        """ make sure that related content comes in correct order even when programmer forgets to ask for it"""
290        return self.getResources()
291
292    def getSortedList(self, reftype='relatedContent'):
293        #f = self.getField('refsToResources')
294        field = self.Schema().get(reftype)
295        rtype=field.relationship
296        f = self.getReferenceImpl(relationship=rtype)
297        arr = []
298        err_base = 12345123
299        for a in f:
300            try:
301                i = a.collection_position
302            except AttributeError:
303                i = err_base + 100
304                a.collection_position = i
305            arr.append([i, a])
306        arr.sort()
307        return arr
308
309    def getBlurp(self):
310        """ Some 1000 chars or something FIXME TO FIND NICE PLACES TO CUT (<p><br> etc.)"""
311        lt = getToolByName(self, 'lemill_tool')
312        return lt.split_at_p_or_br(lt.shorten_link_names(lt.htmlify(self.Description())))
313
314    def getResources(self, reftype='relatedContent'):
315        """ ... """
316        #return self.getRefsToResources()
317        sorted = self.getSortedList(reftype)
318        arr = []
319        for x in sorted:
320            arr.append(x[1].getTargetObject())
321        # Only Tools and Methods should get sorted by name
322        if reftype != 'relatedContent':
323            mod_resources = [(x.Title(), x) for x in arr]
324            mod_resources.sort()
325            arr = [x[1] for x in mod_resources]
326        return arr
327
328
329    security.declareProtected(MODIFY_CONTENT,'delContent')
330    def delContent(self, objid,REQUEST=None):
331        """ front for delResources """
332        if objid:
333            self.delResources(objid, reftype='relatedContent')
334        return REQUEST.RESPONSE.redirect(self.absolute_url() + "/collection_edit")
335
336    security.declareProtected(MODIFY_CONTENT,'delMethods')
337    def delMethods(self, objid,REQUEST=None):
338        """ front for delResources """
339        if objid:
340            self.delResources(objid, reftype='relatedMethods')
341        return REQUEST.RESPONSE.redirect(self.absolute_url() + "/collection_edit")
342
343    security.declareProtected(MODIFY_CONTENT,'delTools')
344    def delTools(self, objid,REQUEST=None):
345        """ front for delResources """
346        if objid:
347            self.delResources(objid, reftype='relatedTools')
348        return REQUEST.RESPONSE.redirect(self.absolute_url() + "/collection_edit")
349
350    security.declareProtected(MODIFY_CONTENT,'delResources')
351    def delResources(self, objid, reftype='relatedContent'):
352        """ delete selected resource """
353        field = self.Schema().get(reftype)
354        objlist = field.get(self)
355
356        newlist = [o.UID() for o in objlist if o.id!=objid]
357        field.set(self, newlist)
358        # if collection is empty, delete it too
359#        if new == []:
360#            self.collections.delCollection(self.getId())
361#            return True
362#        else:
363#            return False
364        self.setGoodStory()
365        return False
366
367    def getCollectionContent(self):
368        """ Returns a list of content, methods and tools, ordered by their modification date """
369        objects=self.getRelatedContent()+self.getRelatedMethods()+self.getRelatedTools()
370        objects = [(x.ModificationDate(), x) for x in objects]
371        objects.sort()
372        return [x[1] for x in objects]
373       
374
375
376    security.declareProtected(MODIFY_CONTENT,'moveUpContent')
377    def moveUpContent(self, objid,REQUEST=None):
378        """ Front for more general moveUpResources """
379        if objid:
380            self.moveUpResources(objid, reftype='relatedContent')
381        return REQUEST.RESPONSE.redirect(self.absolute_url() + "/collection_edit")
382
383    security.declareProtected(MODIFY_CONTENT,'moveUpResources')
384    def moveUpResources(self, objid, reftype='relatedContent'):
385        """ move up """
386        field = self.getField(reftype)
387        sorted = self.getSortedList(reftype)
388        cur_pos = 0
389        count = 0
390        for x in sorted:
391            if x[1].getTargetObject().id == objid:
392                cur_pos = count
393                break
394            count += 1
395        if cur_pos > 0:
396            prev_uid = sorted[cur_pos-1][1].getTargetObject().UID()
397            prev_pos = sorted[cur_pos-1][1].collection_position
398            curr_uid = sorted[cur_pos][1].getTargetObject().UID()
399            curr_pos = sorted[cur_pos][1].collection_position
400
401            sorted.remove(sorted[cur_pos])
402            sorted.remove(sorted[cur_pos-1])
403            new_list = []
404            [ new_list.append(x[1].getTargetObject()) for x in sorted ]
405           
406            field.set(self, new_list)
407            new_list.append(curr_uid)
408            field.set(self, new_list, collection_position=prev_pos)
409            new_list.append(prev_uid)
410            field.set(self, new_list, collection_position=curr_pos)
411            #new_list = []
412            #for x in sorted:
413            #    new_list.append(x[1].getTargetObject())
414            #field.set(self, new_list)
415
416
417    security.declareProtected(MODIFY_CONTENT,'moveDownContent')
418    def moveDownContent(self, objid,REQUEST=None):
419        """ Front for more general moveDownResources """
420        if objid:
421            self.moveDownResources(objid, reftype='relatedContent')
422        return REQUEST.RESPONSE.redirect(self.absolute_url() + "/collection_edit")
423
424
425    security.declareProtected(MODIFY_CONTENT,'moveDownResources')
426    def moveDownResources(self, objid, reftype='relatedContent'):
427        """ move down """
428        field = self.getField(reftype)
429        sorted = self.getSortedList(reftype=reftype)
430        cur_pos = 0
431        count = 0
432        for x in sorted:
433            if x[1].getTargetObject().id == objid:
434                cur_pos = count
435                break
436            count += 1
437        if cur_pos+1 < len(sorted):
438            next_uid = sorted[cur_pos+1][1].getTargetObject().UID()
439            next_pos = sorted[cur_pos+1][1].collection_position
440            curr_uid = sorted[cur_pos][1].getTargetObject().UID()
441            curr_pos = sorted[cur_pos][1].collection_position
442
443            sorted.remove(sorted[cur_pos+1])
444            sorted.remove(sorted[cur_pos])
445            new_list = []
446            [ new_list.append(x[1].getTargetObject()) for x in sorted ]
447           
448            field.set(self, new_list)
449            new_list.append(curr_uid)
450            field.set(self, new_list, collection_position=next_pos)
451            new_list.append(next_uid)
452            field.set(self, new_list, collection_position=curr_pos)
453
454    def amIOwner(self):
455        """ check owner of object """
456        roles = self.portal_membership.getAuthenticatedMember().getRolesInContext(self)
457        return 'Owner' in roles
458
459
460    def manage_afterAdd(self, item, container):
461        #Replaces the left side portlets with the content type's own action portlet.
462        BaseContent.manage_afterAdd(self, item, container)
463        if not hasattr(item.aq_base, 'left_slots'):
464            self._setProperty('left_slots', ['here/portlet_%s_actions/macros/portlet' % item.meta_type.lower(),], 'lines')
465
466    security.declareProtected(MODIFY_CONTENT,'cleanRefsToResources')
467    def cleanRefsToResources(self):
468        """ Check if (deprecated) RefsToResources contains stuff that should be in relatedContent etc. """
469        if not self.Schema().get('relatedContent'): # update schema if old type collection
470            #print 'schema update required for %s' % self.getId()
471            self._updateSchema()
472        objlist=self.getRefsToResources()
473        field=self.getField('refsToResources')
474        newlist=[]
475        for o in objlist:
476            objtype=o.meta_type
477            if objtype in CONTENT_TYPES:
478                reftype='relatedContent'
479            elif objtype in ACTIVITY_TYPES:
480                reftype='relatedMethods'
481            elif objtype in TOOLS_TYPES:
482                reftype='relatedTools'
483            else:
484                reftype='keeper'
485
486            if reftype=='keeper':
487                newlist.append(o.UID())
488            else:
489                self.addRefsToResources(o.UID(),reftype=reftype)
490        field.set(self, newlist)
491
492    def getLearningStoryText(self):
493        """ will return the nice version for the story text """
494        lt = getToolByName(self, 'lemill_tool')
495        return lt.shorten_link_names(lt.htmlify(self.Description()))
496
497    def _buildZIP(self):
498        print '-------------- _buildZIP -------------'
499
500        def do_stuff(html, cache):  # change src urls, download and put them in zip
501            # get and remove base url from html
502            split_html = sre.split('(<base href=")(.*?)(".*?/>)', html)
503            base = split_html[2]
504            del split_html[1:4]
505            html = ''.join(split_html)
506
507            split_html = sre.split('(<[^>]+)src="(.*?)"', html)
508
509            srcs = {}
510            newnames = {}
511            src_urls = split_html[2::3]
512            for url in src_urls:
513                if not srcs.has_key(url):
514                    if cache.has_key(url):
515                        filename, content = cache[url]
516                        srcs[url] = (filename, content)
517                        print 'From cache:', url
518                    else:
519                        xurl = url
520                        try:
521                            if not url.startswith('http://'):
522                                xurl = base + '/' + url
523                            src = urlopen(xurl)
524                            print xurl
525                            media_type, subtype = src.headers.getheader('Content-Type').split('/')
526                            if media_type == 'image':
527                                tmp_url = src.url
528                                if src.url.startswith(self.portal_url()):
529                                    for s in ('/coverImage', '/image_large'):
530                                        i = src.url.find(s)
531                                        if i != -1:
532                                            tmp_url = list(src.url)
533                                            tmp_url[i] = '_'
534                                            tmp_url = ''.join(tmp_url[:i + len(s)])
535                                            break
536                                basename = tmp_url.split('/')[-1].rsplit('.', 1)[0]
537                                filename = '%s.%s' % (basename, subtype.split(';', 1)[0])
538                                newnames[filename] = newnames.get(filename, 0) + 1
539                                if newnames[filename] > 1:
540                                    filename = '%s-%d.%s' % (basename, newnames[filename] - 1, subtype)
541                            else:
542                                filename = src.url.split('/')[-1]
543                            url_content = src.read()
544                            srcs[url] = (filename, url_content)
545                            cache[url] = (filename, url_content)
546                        except HTTPError, e:
547                            print "Failed to download %s: %s" % (xurl, e)
548
549            return srcs, split_html
550
551        zipcontent = []
552
553        main_dir = self.getId()
554        url_cache = {}
555        zipcontent.append((main_dir, do_stuff(self.standalone_view(), url_cache)))
556        for content in self.getResources(reftype='relatedContent'):
557            zipcontent.append(('%s/content/%s' % (main_dir, content.getId()), do_stuff(content.standalone_view(), url_cache)))
558
559        for method in self.getResources(reftype='relatedMethods'):
560            zipcontent.append(('%s/methods/%s' % (main_dir, method.getId()), do_stuff(method.standalone_view(), url_cache)))
561
562        for tool in self.getResources(reftype='relatedTools'):
563            zipcontent.append(('%s/tools/%s' % (main_dir, tool.getId()), do_stuff(tool.standalone_view(), url_cache)))
564
565        file_ref_count = {}
566        for dir_, (srcs, split_html) in zipcontent:
567            for url in srcs.iterkeys():
568                file_ref_count[url] = file_ref_count.get(url, 0) + 1
569
570        s = StringIO()
571        zf = zipfile.ZipFile(s, 'w', compression=zipfile.ZIP_DEFLATED)
572
573        common_urls = [key for key, val in file_ref_count.iteritems() if val > 1]
574        for dir_, (srcs, split_html) in zipcontent:
575            for url, (newname, content) in srcs.items():
576                if url in common_urls:
577                    srcs[url] = ('%s/common_files/%s' % (main_dir, newname), content)
578                else:
579                    srcs[url] = ('%s/%s' % (dir_, newname), content)
580
581            def calc_url(link):
582                url = srcs.get(link, (link,))[0]
583                depth = dir_.count('/')
584                return "../" * depth + url.split('/', 1)[1]
585
586            new_html_l = ['%s%ssrc="%s"' % (stuff, tag, calc_url(link)) for stuff, tag, link in zip(split_html[::3], split_html[1::3], split_html[2::3])]
587            new_html_l.append(split_html[-1])
588            zf.writestr(dir_ + '/index.html', ''.join(new_html_l))
589
590        in_zip = {}
591        for dir_, (srcs, split_html) in zipcontent:
592            for url, (newname, content) in srcs.items():
593                if not in_zip.has_key(newname):
594                    in_zip[newname] = content
595                    zf.writestr(newname, content)
596                else:
597                    assert in_zip[newname] == content
598
599        print '-------------- _buildZIP done ------------'
600        return s, zf
601
602    def getZIP(self):
603        s, zf = self._buildZIP()
604        zf.close()
605        response = self.REQUEST.RESPONSE
606        response.setHeader('Content-Type', 'application/zip')
607        response.setHeader('Content-Disposition', 'attachment; filename="%s.zip"' % self.getId())
608        return s.getvalue()
609
610    def getSCORM(self):
611        """ hguriehguirehgre """
612        s, zf = self._buildZIP()
613        zf.close()
614        response = self.REQUEST.RESPONSE
615        response.setHeader('Content-Type', 'application/zip')
616        response.setHeader('Content-Disposition', 'attachment; filename="%s.zip"' % self.getId())
617        return s.getvalue()
618
619    #XXX These additional meta-type='' and obj=None might not be needed, but I will leave them here just to be on the safe side.
620    def getDefaultIcon(self, meta_type='', obj=None):
621        """ Will return a portrait of the creator or his default icon """
622        owner = self.Creator()
623        mtool = getToolByName(self, 'portal_membership')
624        member_folder = mtool.getHomeFolder(owner)
625        given_image = member_folder.getCoverImageURL()
626        return given_image
627       
628
629registerType(Collection, PROJECTNAME)
630
631class CollectionsFolder(BaseFolder):
632    """ container for collection """
633    schema = BaseSchema
634    id = "collections"
635    meta_type = "CollectionsFolder"
636    archetype_name = "CollectionsFolder" 
637
638    actions= (
639    {
640    'id':'view',
641    'name':'view',
642    'action':'string:${object_url}/collections_list',
643    'permission':('View',),
644    },
645    )
646               
647registerType(CollectionsFolder, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.