root/trunk/GroupBlog.py

Revision 3195, 21.5 kB (checked in by jukka, 1 week ago)

Fixed #2046. Last commit was from wrong branch.

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