source: trunk/GroupBlog.py @ 1930

Revision 1930, 14.9 KB checked in by jukka, 12 years ago (diff)

Fixed tests and other stuff, fixed #1376, closed #1420.

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 SharedMetadata import description, tags
20from Products.PythonScripts.standard import url_quote
21from Products.Archetypes.public import *
22from Products.ATContentTypes.content.folder import ATFolder
23from Products.ATReferenceBrowserWidget.ATReferenceBrowserWidget import ReferenceBrowserWidget
24from Products.CMFCore.permissions import ModifyPortalContent
25from Products.Archetypes.public import BaseFolder, BaseFolderSchema, registerType
26from Products.Archetypes.atapi import DisplayList
27from Globals import InitializeClass
28from Products.CMFCore.utils import getToolByName
29from AccessControl import ClassSecurityInfo, Unauthorized
30from Products.LeMill import LeMillMessageFactory as _
31
32from config import PROJECTNAME, MODIFY_CONTENT, VIEW, DEFAULT_ICONS
33from permissions import MODIFY_CONTENT
34from SharedMetadata import *
35from Resources import CommonMixIn
36
37from DateTime import DateTime
38import datetime
39import operator
40
41import time
42
43# Same thing as with MemberFolder but easier: MemberBlogs contain the values of groups itself in indexable storage.
44
45schema = BaseFolderSchema + description + tags + coverImage + deletionReason + score + subject_area_schema + latest_edit_schema + Schema((
46    StringField('title',
47        required=True,
48        searchable = True,
49    ),
50    StringField('description',
51        schemata='default',
52        widget=TextAreaWidget(
53            rows=5,
54            cols=40,
55            label='Description',
56            label_msgid='label_description',
57            )
58    ),
59
60    LinesField('language_skills',
61        default = [],
62        index = 'KeywordIndex:schema',
63        vocabulary = 'getLanguagelist',
64        widget = PicklistWidget(           
65             label = 'Languages',
66             label_msgid = 'label_group_language_skills',
67             description = "Choose languages that the group is willing to use. This affects only when people try to find groups based on a language.",
68             description_msgid = 'help_group_language_skills',
69             i18n_domain = "lemill"
70             )
71        ),       
72    LinesField('recent_posts',
73        default= [],
74        widget = LinesWidget(
75            visible = {'view':'invisible', 'edit':'invisible'},
76            )
77        ),
78    LinesField('groupMembers',
79        default=[],
80        index = 'KeywordIndex:schema',
81        widget = LinesWidget(
82            visible = {'view':'invisible', 'edit':'invisible'},
83            ),
84        ),
85
86 ))
87
88schema = schema.copy()
89schema['allowDiscussion'].widget.visible = {'edit':'invisible', 'view':'invisible'}
90schema['subject'].widget.visible = {'edit':'invisible', 'view':'invisible'}
91schema['contributors'].widget.visible = {'edit':'invisible', 'view':'invisible'}
92schema['creators'].widget.visible = {'edit':'invisible', 'view':'invisible'}
93schema['language'].widget.visible = {'edit':'invisible', 'view':'invisible'}
94schema['rights'].widget.visible = {'edit':'invisible', 'view':'invisible'}
95schema['tags'].schemata='default'
96schema['subject_area'].schemata='default'
97schema.moveField('subject_area', after='language_skills')
98schema['subject_area'].searchable = True
99
100
101class GroupBlog(BaseFolder,CommonMixIn):
102    """Group blog"""
103
104    meta_type = "GroupBlog"
105    archetype_name = "GroupBlog" 
106    global_allow = 1
107
108    allowed_content_types = ('BlogPost')
109    default_view = ('groupblog_view')
110    filter_content_types = True
111    security = ClassSecurityInfo()
112    schema = schema
113
114    actions = ({ 'id':'view',
115                 'name': 'View',
116                 'action': 'string:${object_url}/groupblog_view',
117                 'permissions': (VIEW,)
118               },
119               { 'id':'edit',
120                 'name': 'Edit',
121                 'action': 'string:${object_url}/base_edit',
122                 'permissions': (MODIFY_CONTENT,)
123               },
124               { 'id':'edit_links',
125                 'name': 'Edit links',
126                 'action': 'string:${object_url}/base_metadata',
127                 'permissions': (MODIFY_CONTENT,)
128               },
129               { 'id':'edit_categories',
130                 'name': 'Edit categories',
131                 'action': 'string:${object_url}/base_metadata',
132                 'permissions': (MODIFY_CONTENT,)
133               },
134               )
135    aliases = {
136        '(Default)' : '',
137        'view'      : 'groupblog_view',
138        'edit'      : 'base_edit',
139        'edit_categories' : 'base_metadata',
140        'edit_links' : 'base_metadata',
141    }
142
143    def manage_afterAdd(self, item, container):
144        # Replaces the left side portlets with the content type's own action portlet.
145        BaseFolder.manage_afterAdd(self, item, container)
146        mtool = getToolByName(self, 'portal_membership')
147        memberfolder=mtool.getHomeFolder()
148        if memberfolder!=None and type(memberfolder)=='MemberFolder':
149            memberfolder.recalculateScore()
150        if not hasattr(item.aq_base, 'left_slots'):
151            self._setProperty('left_slots', ['here/portlet_%s_actions/macros/portlet' % item.meta_type.lower(),], 'lines')
152        # Enable topic syndication by default
153        syn_tool = getToolByName(self, 'portal_syndication', None)
154        if syn_tool is not None:
155            if syn_tool.isSiteSyndicationAllowed():
156                try:
157                    syn_tool.enableSyndication(self)
158                except: # might get 'Syndication Information Exists'
159                    pass
160
161
162    def at_post_edit_script(self):
163        self.recalculateScore()
164           
165    def getGroupMaterials(self, n=False):
166        """ Get resources edited by this group """
167        pc=getToolByName(self, 'portal_catalog')       
168        results=pc({'getRawGroupEditing':self.UID()})
169        if n:
170            return len(results)
171        else:
172            return results
173
174    def getGroupMembersNamesAndUrls(self):
175        memberids= self.getGroupMembers()
176        reslist=[]
177        for mid in memberids:
178            memberfolder=self.getMemberFolderById(mid)
179            if memberfolder:
180                reslist.append((memberfolder.NiceName(), memberfolder.absolute_url()))
181        return reslist
182
183    def getLatestEditDate(self):
184        """Returns creation date, we don't want to notify about every edit"""
185        return DateTime(self.CreationDate())
186
187    def getSamples(self):
188        """ get n number of samples """
189        all = self.getGroupMaterials()
190        all = [a for a in all if a.review_state=='public' and a.portal_type in self.getFeaturedTypes() and a.getHasCoverImage==True]
191        import random
192        n = min(3, len(all))
193        return random.sample(all,n)
194
195    def getCoverImageURL(self, drafts=False):
196        """Returns the URL for the cover image."""
197        timestamp = time.time()
198        if self.getField('hasCoverImage').get(self)==True:
199            return self.absolute_url()+'/coverImage?newest='+str(timestamp)
200        return 'images/default_group.png'
201       
202    def isPost(self):
203        return False       
204       
205    def NiceName(self):
206        """ To make GroupBlog compatible with MemberFolders """
207        return self.getTitle()
208
209    def getLanguagelist(self):
210        languagelist=self.availableLanguages()
211        return DisplayList(languagelist)
212
213    def isMember(self, memberid):
214        return memberid in self.getGroupMembers()
215
216    def canIModerate(self):
217        roles = self.portal_membership.getAuthenticatedMember().getRolesInContext(self)
218        return 'Manager' in roles or 'Reviewer' in roles
219       
220    def getPosts(self, batch=True, b_size=30, b_start=0):
221        """ Return get posts as full objects, sort them by date and batch them """
222        wtool = getToolByName(self,'portal_workflow')
223        results= [(obj.CreationDate(), obj) for obj in self.objectValues() if obj.portal_type=='BlogPost' and wtool.getInfoFor(obj,'review_state',None)!='deleted']
224
225        results.sort(key=operator.itemgetter(0))
226        results.reverse()
227        results =[obj for (d,obj) in results]
228        if batch:
229            from Products.CMFPlone import Batch
230            results = Batch(results, b_size, int(b_start), orphan=0)       
231        return results
232
233    def getFullForumRSS(self):
234        """ Returns forum topics and comments for them """
235        full_results=[]
236        wtool = getToolByName(self,'portal_workflow')
237        results= [(obj.CreationDate(), obj) for obj in self.objectValues() if obj.portal_type=='BlogPost' and wtool.getInfoFor(obj,'review_state',None)!='deleted']
238        results.sort(key=operator.itemgetter(0))
239        results.reverse()
240        results =[obj for (d,obj) in results]
241        for result in results:
242            full_results.append(result)
243            for x in result.getComments():
244                full_results.append(x)
245        return full_results
246
247    def getBlog(self):
248        return self
249       
250    security.declareProtected(MODIFY_CONTENT,'addRecent_post')
251    def addRecent_post(self,postid):
252        """ Recent posts are lists of id's, because then picking the post objects when inside group is easy and quick """
253        # sometimes this method gets called before new blogposts get their proper id:s, so escape then:
254        if postid.startswith('blogpost.'):
255            return None
256        postlist= list(self.getField('recent_posts').get(self))
257        if postid in postlist:
258            postlist.remove(postid)
259        postlist=[postid]+postlist
260        self.setRecent_posts(postlist[:10])           
261
262    security.declareProtected(MODIFY_CONTENT,'removeRecent_post')
263    def removeRecent_post(self,postid):
264        postlist= list(self.getField('recent_posts').get(self))
265        if postid in postlist:
266            postlist.remove(postid)
267        self.setRecent_posts(postlist)           
268
269    security.declareProtected(MODIFY_CONTENT,'replaceRecent_post')
270    def replaceRecent_post(self, old, new):
271        postlist= list(self.getField('recent_posts').get(self))
272        postlist[postlist.index(old)]=new
273        self.setRecent_posts(postlist)           
274
275    def addMember(self, memberid):
276        mtool = getToolByName(self, 'portal_membership')
277        memberfolder=mtool.getHomeFolder(memberid)
278        if not memberfolder:
279            print "We have a problem: %s cannot be found and is not added to group" % memberid
280            return False
281        mid=memberfolder.getMemberId()
282        users=list(self.getGroupMembers())
283        if mid in users:
284            return False
285        self.setGroupMembers(users+[mid])
286        self.reindexObject()
287        return True
288           
289    def removeMember(self, memberid):
290        mtool = getToolByName(self, 'portal_membership')
291        memberfolder=mtool.getHomeFolder(memberid)
292        if memberfolder:
293            mid=memberfolder.getMemberId()
294        else:
295            print "We have a problem: %s cannot be found. removing him from group anyway" % memberid
296            mid=memberid
297        users=list(self.getGroupMembers())
298        if mid not in users:
299            return False
300        users.remove(mid)
301        self.setGroupMembers(users)
302        if len(users)==0:
303            # Deleting group.
304            # safe but uncertain method: some links can still work, especially after reindexing
305            self.content_status_modify(workflow_action='delete', msg='Resource deleted')
306            self.unindexObject()
307        return True     
308       
309       
310           
311    def join_group(self, redirect=True):
312        """ The logged in user joins the current group """
313        mtool = getToolByName(self, 'portal_membership')
314        putils= getToolByName(self, 'plone_utils')
315        user = mtool.getAuthenticatedMember()       
316        userid = user.getId()
317        if self.addMember(userid):
318            msg = _("You have joined the group '${title}'.", mapping={u'title' : self.title_or_id()})
319        else:
320            msg= _(u"You are already member in this group.")
321        putils.addPortalMessage(msg)
322        if redirect:
323            return self.REQUEST.RESPONSE.redirect(self.absolute_url())
324   
325    def leave_group(self):
326        """ The logged in user leaves the current group """
327        REQUEST=self.REQUEST
328        mtool = getToolByName(self, 'portal_membership')
329        putils= getToolByName(self, 'plone_utils')
330        user = mtool.getAuthenticatedMember()
331        userid = user.getId()
332        if self.removeMember(userid):
333            msg = _("You have left the group '${title}'.", mapping={u'title' : self.title_or_id()})
334        else:
335            msg= _(u"You are not member in this group.")
336        putils.addPortalMessage(msg)
337        return REQUEST.RESPONSE.redirect(self.community.absolute_url())
338
339 
340       
341
342    # These have to be duplicated since its difficult to base these folderish objects on non-folderish Resources
343
344    security.declareProtected(MODIFY_CONTENT,'setCoverImage')
345    def setCoverImage(self, value, **kwargs):
346        """ Normal mutator, but flags object to have a coverImage (hasCoverImage = True) """
347        cover=self.getField('coverImage')
348        cover.set(self,value,**kwargs)
349        has_cover=self.getField('hasCoverImage')
350        has_cover.set(self,True)
351        self.reindexObject()
352
353    security.declareProtected(MODIFY_CONTENT,'delCoverImage')
354    def delCoverImage(self):
355        """ Reverse of setCoverImage """
356        cover=self.getField('coverImage')
357        cover.set(self,None)
358        has_cover=self.getField('hasCoverImage')
359        has_cover.set(self,False)
360        self.reindexObject()
361
362
363    def recalculateScore(self):
364        """ Calculates the score for Group Blog according to specifications """
365        lt = getToolByName(self, 'lemill_tool')
366        score = float(0)
367        members_score = len(self.getGroupMembers())
368        materials = self.getGroupMaterials()
369        posts = self.getPosts(batch=False)
370
371        score = score + members_score
372
373        for m in materials:
374            months_old = lt.getTimeDifference(m.getLatestEdit)
375            months_old = months_old['months']
376            score = score + 1 / float(months_old + 1)
377
378        for p in posts:
379            months_old = lt.getTimeDifference(p.getLatestEditDate())
380            months_old = months_old['months']
381            score = score + 1 / float(months_old + 1)
382
383        score = int(round(score))
384
385        # This one is not really needed, because Group must have at least one member
386        if score<1:
387            score = 1
388        # Now to set the value for field score
389        self.setScore(score)
390
391
392       
393
394registerType(GroupBlog, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.