source: trunk/GroupBlog.py @ 3094

Revision 3094, 21.3 KB checked in by jukka, 9 years ago (diff)

Restored and refactored RSS support.

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.CMFCore.permissions import ModifyPortalContent
21from Products.Archetypes.public import BaseFolder, BaseFolderSchema, registerType
22from Products.Archetypes.atapi import DisplayList
23from Globals import InitializeClass
24from Products.CMFCore.utils import getToolByName
25from AccessControl import ClassSecurityInfo, Unauthorized
26from messagefactory_ import i18nme as _
27from LargeSectionFolder import LeMillFolder
28from Products.CMFPlone.PloneBatch import Batch
29
30from config import PROJECTNAME, MODIFY_CONTENT, VIEW, DEFAULT_ICONS, MATERIAL_TYPES, to_unicode, LANGUAGES
31from permissions import MODIFY_CONTENT
32from Schemata import tags, coverImage, deletionReason, score, subject_area_schema, latest_edit_schema, state
33from CommonMixIn import CommonMixIn
34
35from DateTime import DateTime
36import random, time
37
38# Same thing as with MemberFolder but easier: MemberBlogs contain the values of groups itself in indexable storage.
39
40groupblog_schema = Schema((
41    StringField('title',
42        required=True,
43        searchable = True,
44    ),
45    StringField('description',
46        schemata='default',
47        widget=TextAreaWidget(
48            rows=5,
49            cols=40,
50            label='Description',
51            label_msgid='label_description',
52            i18n_domain='lemill'
53            )
54    ),
55    LinesField('language_skills',
56        default = [],
57        index = 'KeywordIndex:schema',
58        vocabulary = 'getLanguagelist',
59        widget = PicklistWidget(           
60             label = 'Languages',
61             label_msgid = 'label_group_language_skills',
62             description = "Choose languages that the group is willing to use. This affects only when people try to find groups based on a language.",
63             description_msgid = 'help_group_language_skills',
64             i18n_domain = "lemill"
65             )
66        ),       
67    LinesField('recent_posts',
68        default= [],
69        widget = LinesWidget(
70            visible = {'view':'invisible', 'edit':'invisible'},
71            )
72        ),
73    LinesField('groupMembers',
74        default=[],
75        index = 'KeywordIndex:schema',
76        widget = LinesWidget(
77            visible = {'view':'invisible', 'edit':'invisible'},
78            ),
79        ),
80 ))
81
82schema = BaseFolderSchema + tags + coverImage + deletionReason + score + subject_area_schema + latest_edit_schema + state + groupblog_schema
83
84schema = schema.copy()
85schema['allowDiscussion'].widget.visible = {'edit':'invisible', 'view':'invisible'}
86schema['subject'].widget.visible = {'edit':'invisible', 'view':'invisible'}
87schema['contributors'].widget.visible = {'edit':'invisible', 'view':'invisible'}
88schema['creators'].widget.visible = {'edit':'invisible', 'view':'invisible'}
89schema['language'].widget.visible = {'edit':'invisible', 'view':'invisible'}
90schema['rights'].widget.visible = {'edit':'invisible', 'view':'invisible'}
91schema['tags'].schemata='default'
92schema['tags'].widget.description = "Enter descriptive tags for this group, separated by commas."
93schema['tags'].widget.description_msgid = 'description_group_tags'
94schema['subject_area'].schemata='default'
95schema.moveField('subject_area', after='language_skills')
96schema['subject_area'].searchable = True
97schema['subject_area'].widget.description = "Choose curriculum subject areas that are appropriate for this group."
98schema['subject_area'].widget.description_msgid = 'description_group_subject_area'
99
100
101class GroupBlog(CommonMixIn, LeMillFolder, BaseFolder):
102    """Group blog"""
103
104    meta_type = "GroupBlog"
105    archetype_name = "GroupBlog" 
106    default_location = 'community/groups'
107
108    global_allow = 1
109
110    portlet = 'here/portlet_groupblog_actions/macros/portlet'
111    allowed_content_types = ('BlogPost')
112    default_view = ('groupblog_view')
113    filter_content_types = True
114    security = ClassSecurityInfo()
115    schema = schema
116
117    actions = ({ 'id':'view',
118                 'name': 'View',
119                 'action': 'string:${object_url}/groupblog_view',
120                 'permissions': (VIEW,)
121               },
122               { 'id':'edit',
123                 'name': 'Edit',
124                 'action': 'string:${object_url}/base_edit',
125                 'permissions': (MODIFY_CONTENT,)
126               },
127               { 'id':'edit_links',
128                 'name': 'Edit links',
129                 'action': 'string:${object_url}/base_metadata',
130                 'permissions': (MODIFY_CONTENT,)
131               },
132               { 'id':'edit_categories',
133                 'name': 'Edit categories',
134                 'action': 'string:${object_url}/base_metadata',
135                 'permissions': (MODIFY_CONTENT,)
136               },
137               )
138    aliases = {
139        '(Default)' : '',
140        'view'      : 'groupblog_view',
141        'edit'      : 'base_edit',
142        'edit_categories' : 'base_metadata',
143        'edit_links' : 'base_metadata',
144    }
145
146    def manage_afterAdd(self, item, container):
147        BaseFolder.manage_afterAdd(self, item, container)
148        # We need this to get at_post_edit script running, as we are creating groups automatically
149        # This way processForm does not run
150        self.at_post_create_script()
151
152    def helper_updateCatalogUID(self):
153        self._catalogUID(self)
154
155    def at_post_create_script(self):
156        lutool = getToolByName(self, 'lemill_usertool')
157        memberfolder=lutool.getMemberFolder()
158        if memberfolder:
159            memberfolder.recalculateScore()
160        # Enable topic syndication by default
161        syn_tool = getToolByName(self, 'portal_syndication', None)
162        if syn_tool is not None:
163            if syn_tool.isSiteSyndicationAllowed():
164                try:
165                    syn_tool.enableSyndication(self)
166                except: # might get 'Syndication Information Exists'
167                    pass
168       
169
170    def at_post_edit_script(self):
171        old_id=self.id
172        self._renameAfterCreation()
173        # If id has changed, update posts to have correct parent's id
174        if old_id!=self.id:
175            for post in self.objectValues('BlogPost'):
176                post.setParentBlog(self.id)
177                post.reindexObject(['getParentBlog'])
178        self.recalculateScore()
179        self.reindexObject()
180        # See if we need to reindex Title in the uid_catalog
181        uid = self.UID()
182        utool = getToolByName(self, 'uid_catalog')
183        objlist = utool({'UID':uid})
184        if objlist:
185            if self.Title() != objlist[0].Title:
186                self.helper_updateCatalogUID()
187
188    def getMetaDescription(self):
189        """ Use group description for header metadata   """
190        return self.getDescription()
191           
192    def getGroupMaterials(self, n=False, as_dict=False):
193        """ Get resources edited by this group """
194        pc=getToolByName(self, 'portal_catalog')       
195        results=pc({'getRawGroupEditing':self.UID(), 'getState':('public','draft')})
196        if as_dict:
197            # Make a dictionary with following keys: 'Content','Activities','Tools'
198            dict={'Content':[],'Activities':[],'Tools':[]}
199            for res in results:
200                if res.meta_type in MATERIAL_TYPES:
201                    dict['Content'].append(res)
202                elif res.meta_type=='Activity':
203                    dict['Activities'].append(res)
204                elif res.meta_type=='Tool':
205                    dict['Tools'].append(res)
206            if n:
207                for key in dict.keys():
208                    dict[key]=len(dict[key])
209            return dict                   
210
211        if n:
212            return len(results)
213        else:
214            return results
215
216    def getGroupMembersNamesAndUrls(self):
217        """ Returns a list of tuples, where first element is nice name and second element is url """
218        memberids= self.getGroupMembers()
219        lutool=getToolByName(self, 'lemill_usertool')
220        if memberids:       
221            return [(md.getNicename, md.getURL()) for md in lutool.getMemberFolderMetadata(memberid=memberids)]
222        else:
223            return []
224
225    def getLatestEditDate(self):
226        """Returns creation date, we don't want to notify about every edit"""
227        return DateTime(self.CreationDate())
228
229    def getLastEditor(self):
230        """ Returns creator, we don't have any history for this one """
231        return self.Creator()
232
233    def getSamples(self):
234        """ get n number of samples """
235        pc=getToolByName(self, 'portal_catalog')       
236        results=pc({'getRawGroupEditing':self.UID(), 'getState':'public', 'portal_type':tuple(self.getFeaturedTypes()), 'getHasCoverImage':True})
237        n = min(4, len(results))
238        return random.sample(results,n)
239
240    def getCoverImageURL(self, drafts=False):
241        """Returns the URL for the cover image."""
242        if self.getField('hasCoverImage').get(self)==True:
243            return self.absolute_url()+'/coverImage'
244        portal_url=getToolByName(self, 'portal_url')
245        return portal_url()+'/images/default_group.png'
246       
247    def isPost(self):
248        return False       
249       
250    def getNicename(self):
251        """ To make GroupBlogs compatible with MemberFolders """
252        return self.Title()
253
254    def getLanguagelist(self):
255        return DisplayList(LANGUAGES)
256
257    def canIEdit(self, member=None):
258        """ Override resources canIEdit: in groups only group members can edit (and managers, of course) """
259        if not member:
260            lutool=getToolByName(self, 'lemill_usertool')       
261            member=lutool.getAuthenticatedMember()     
262        return self.isMember(member.getId()) or self.canIManage()
263
264    def isMember(self, memberid):
265        return memberid in self.getGroupMembers()
266
267    def canIModerate(self):
268        lutool=getToolByName(self, 'lemill_usertool')       
269        roles = lutool.getAuthenticatedMember().getRolesInContext(self)
270        return 'Manager' in roles or 'Reviewer' in roles
271       
272    def getPosts(self, batch=True, b_size=30, b_start=0, objects=True, limit=0, remove_zeros=False):
273        """ Return get posts as *metadata* objects, sort them by date and batch them """
274        pc= getToolByName(self,'portal_catalog')
275        blogposts=pc({'portal_type':'BlogPost', 'getState':'public', 'path':'/'.join(self.getPhysicalPath()[1:])})
276        resources=pc({'getRawGroupEditing':self.UID(), 'getState':('public','draft')})
277        if remove_zeros:       
278            resources = filter(lambda x: x.postCount, resources)
279            blogposts = list(blogposts) # this is required as the filter operation changes lazy catalog object to list
280            # and blogposts need to be list too so that they can be concatenated
281        results=[(int(DateTime(r.getLastCommentDate).timeTime()), r.aq_base, r) for r in blogposts+resources if r.getLastCommentDate]
282        results.sort()
283        results.reverse()       
284        results=[c for a,b,c in results]
285        if limit:
286            results = results[:limit]
287        if batch:
288            results = Batch(results, b_size, int(b_start), orphan=0)
289        if objects:
290            results = [r.getObject() for r in results]
291        return results
292
293
294    def getBlog(self):
295        return self
296 
297 
298    def linkToLatestComment(self, post_md):
299        """ Takes a catalog metadata object and returns an url that links either
300         to blogpost's last anchor or resource's discussion page """
301        if post_md.portal_type=='BlogPost':
302            url=post_md.getURL()
303        else:
304            url='%s/discussion' % post_md.getURL()
305        if not post_md.postCount:
306            return url
307        anchor=int(DateTime(post_md.getLastCommentDate).timeTime())
308        return '%s#%s' % (url,anchor)
309
310       
311    security.declareProtected(MODIFY_CONTENT,'addRecent_post')
312    def addRecent_post(self,postid):
313        """ Recent posts are lists of id's, because then picking the post objects when inside group is easy and quick """
314        # sometimes this method gets called before new blogposts get their proper id:s, so escape then:
315        if postid.startswith('blogpost.'):
316            return None
317        postlist= list(self.getField('recent_posts').get(self))
318        if postid in postlist:
319            postlist.remove(postid)
320        postlist=[postid]+postlist
321        self.setRecent_posts(postlist[:10])           
322
323    security.declareProtected(MODIFY_CONTENT,'removeRecent_post')
324    def removeRecent_post(self,postid):
325        postlist= [post_id for post_id in self.getRecent_posts() if not post_id==postid]
326        self.setRecent_posts(postlist)           
327
328    security.declareProtected(MODIFY_CONTENT,'replaceRecent_post')
329    def replaceRecent_post(self, old, new):
330        postlist= list(self.getRecent_posts())
331        postlist[postlist.index(old)]=new
332        self.setRecent_posts(postlist)           
333
334    def addMember(self, memberid):
335        users=list(self.getGroupMembers())
336        if memberid in users:
337            return False
338        self.setGroupMembers(users+[memberid])
339        self.reindexObject()
340        return True
341           
342    def removeMember(self, memberid):
343        users=list(self.getGroupMembers())
344        if memberid not in users:
345            return False
346        users.remove(memberid)
347        self.setGroupMembers(users)
348        self.reindexObject()
349        if len(users)==0:
350            self.community.groups._delObject(self.id)
351        return True
352       
353           
354    def join_group(self, redirect=True):
355        """ The logged in user joins the current group """
356        lutool = getToolByName(self, 'lemill_usertool')
357        ltool= getToolByName(self, 'lemill_tool')
358        if lutool.isAnonymousUser():
359            ltool.addPortalMessage(_("You have to be logged in to join a group."))
360            return self.REQUEST.RESPONSE.redirect(self.absolute_url())           
361        userid = lutool.getAuthenticatedId()               
362        if self.addMember(userid):
363            ltool.addPortalMessage(_("text_message_joined_the_group"), default="You have joined the group '${title}'.", mapping={'title':self.title_or_id()})
364            self.notifyGroupJoin()
365        else:
366            ltool.addPortalMessage(_("You are already member in this group."))
367        if redirect:
368            return self.REQUEST.RESPONSE.redirect(self.absolute_url())
369   
370    def leave_group(self):
371        """ The logged in user leaves the current group """
372        REQUEST=self.REQUEST
373        lutool = getToolByName(self, 'lemill_usertool')
374        ltool= getToolByName(self, 'lemill_tool')
375        userid = lutool.getAuthenticatedId()       
376        if self.removeMember(userid):
377            ltool.addPortalMessage(_("text_message_left_the_group"), default="You have left the group '${title}'." , mapping={'title':self.title_or_id()})
378        else:
379            ltool.addPortalMessage(_("You are not member in this group."))
380        return REQUEST.RESPONSE.redirect(self.community.absolute_url())
381
382 
383       
384
385    # These have to be duplicated since its difficult to base these folderish objects on non-folderish Resources
386
387    security.declareProtected(MODIFY_CONTENT,'setCoverImage')
388    def setCoverImage(self, value, **kwargs):
389        """ Normal mutator, but flags object to have a coverImage (hasCoverImage = True) """
390        cover=self.getField('coverImage')
391        cover.set(self,value,**kwargs)
392        has_cover=self.getField('hasCoverImage')
393        has_cover.set(self,True)
394
395    security.declareProtected(MODIFY_CONTENT,'delCoverImage')
396    def delCoverImage(self):
397        """ Reverse of setCoverImage """
398        cover=self.getField('coverImage')
399        cover.set(self,None)
400        has_cover=self.getField('hasCoverImage')
401        has_cover.set(self,False)
402
403
404    def recalculateScore(self):
405        """ Calculates the score for Group Blog according to specifications """
406        lt = getToolByName(self, 'lemill_tool')
407        score = float(0)
408        members_score = len(self.getGroupMembers())
409        materials = self.getGroupMaterials()
410        posts = self.getPosts(batch=False)
411
412        score = score + members_score
413
414        for m in materials:
415            months_old = lt.getTimeDifference(m.getLatestEdit)
416            months_old = months_old['months']
417            score = score + 1 / float(months_old + 1)
418
419        for p in posts:
420            months_old = lt.getTimeDifference(p.getLatestEditDate())
421            months_old = months_old['months']
422            score = score + 1 / float(months_old + 1)
423
424        score = int(round(score))
425
426        # This one is not really needed, because Group must have at least one member
427        if score<1:
428            score = 1
429        # Now to set the value for field score
430        self.setScore(score)
431
432    def portfolioFilter(self):
433        """ Used for filtering group portfolio to show only resources where this group is editing """
434        return ('getRawGroupEditing',self.UID())
435
436
437    def getPortletDetails(self, REQUEST, isAnon, member):
438        """ Return an object of booleans to determine what to show and what to hide in portlet view """
439
440        v={'canChangeCoverImage':False,
441            'mainView':True,
442            'manageGroup':False,
443            'joinGroup':'',
444            'leaveGroup':False,
445            'groupMembers':[],
446            'groupResources':{}
447        }
448       
449        ################ Mandatory flags   
450
451        canEdit= self.canIEdit(member)
452        view_id=REQUEST['URL'].split('/')[-1]
453        url=self.absolute_url()
454        isDeleted = self.isDeleted()
455
456        v['mainView'] = mainView = view_id!='lemill_portfolio_view'
457
458        v['canChangeCoverImage'] = mainView and canEdit and not isDeleted
459        v['manageGroup'] = canEdit
460        if isAnon:
461            v['joinGroup'] = 'login_form'
462        elif member.getId() not in self.getGroupMembers():
463            v['joinGroup'] = 'join_group'             
464        v['leaveGroup'] = not v['joinGroup']
465        v['groupMembers'] = self.getGroupMembersNamesAndUrls()
466        v['groupResources'] = self.getGroupMaterials(n=True, as_dict=True)
467        return v
468
469    security.declarePrivate('notifyGroupJoin')
470    def notifyGroupJoin(self):
471        lutool = getToolByName(self, 'lemill_usertool')
472        auth_mf = lutool.getMemberFolder()
473        if self.Creator() != lutool.getAuthenticatedId():
474            mf = lutool.getMemberFolder(self.Creator())
475            if mf.canNotify('group_joined'):
476                ltool=getToolByName(self, 'lemill_tool')
477                language=lutool.getCommunicationLanguage(mf)
478                dict={'name':to_unicode(auth_mf.getNicename()),
479                    'title':to_unicode(self.Title()),
480                    'url':to_unicode(self.absolute_url())}               
481                msg = self.translate("'%(name)s has joined your group '%(title)s': %(url)s",domain="lemill",target_language=language) % dict
482                ltool.mailNotification(msg, recipient=mf.getEmail(), language=language)
483
484    security.declarePrivate('notifyNewThread')
485    def notifyNewThread(self):
486        lutool = getToolByName(self, 'lemill_usertool')
487        auth_mf = lutool.getMemberFolder()
488        if self.Creator() != lutool.getAuthenticatedId():
489            mf = lutool.getMemberFolder(self.Creator())
490            if mf.canNotify('group_message_posted'):
491                language=lutool.getCommunicationLanguage(mf)
492                ltool = getToolByName(self, 'lemill_tool')
493                dict={'name':to_unicode(auth_mf.getNicename()),
494                    'title':to_unicode(self.Title()),
495                    'url':to_unicode(self.absolute_url())}               
496                msg = self.translate("%(name)s has posted a message in your group forum '%(title)s': %(url)s",domain="lemill",target_language=language) % dict
497                ltool.mailNotification(msg=msg, recipient=mf.getEmail(), language=language)
498
499    ################# RSS ######################################
500
501    def getRSSResults(self, max=30):
502        """ Returns latest forum topics and comments
503        metadata returned:
504        'getURL', 'getLatestEdit', 'Title', 'listCreators','Rights'
505        """
506        results=self.getPosts(batch=False, objects=True, limit=10)
507        sortable_results=[]
508        for result in results:
509            r_dict={'getURL':result.absolute_url(), 'getLatestEdit':result.created(), 'Title':result.Title(), 'listCreators':[result.Creator()], 'Rights':'CC-SA 2.5'}
510            sortable_results.append((result.created(), r_dict))
511            url_base=result.absolute_url()
512            title='re:%s' % result.Title()               
513            for reply in result.getDiscussion().values():
514                r_dict={'getURL':'#'.join((url_base, reply.id)), 'getLatestEdit':reply.created, 'Title':title, 'listCreators':[reply.Creator()],'Rights':'CC-SA 2.5'}
515                sortable_results.append((reply.created, r_dict))
516        sortable_results.sort(reverse=True)
517        rez= [x[1] for x in sortable_results[:max]]
518        return rez   
519
520       
521
522registerType(GroupBlog, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.