source: trunk/GroupBlog.py @ 1876

Revision 1876, 23.9 KB checked in by jukka, 12 years ago (diff)

Added groups to recents, removed i18n-strings from management screen buttons and added a check so that spiderbots don't generate error messages by trying to view portfolio of None.

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
43monthNames = {'01':'month_jan', '02':'month_feb', '03':'month_mar', '04':'month_apr', '05':'month_may', '06':'month_jun', '07':'month_jul', '08':'month_aug', '09':'month_sep', '10':'month_oct', '11':'month_nov', '12':'month_dec'}
44
45# Same thing as with MemberFolder but easier: MemberBlogs contain the values of groups itself in indexable storage.
46
47schema = BaseFolderSchema + description + tags + coverImage + deletionReason + score + subject_area_schema + latest_edit_schema + Schema((
48    StringField('title',
49        default_method='prefill_title',
50        searchable = True,
51    ),
52    StringField('description',
53        default_method='prefill_description',
54        schemata='default',
55        widget=TextAreaWidget(
56            rows=5,
57            cols=40,
58            label='Description',
59            label_msgid='label_description',
60            )
61    ),
62
63    LinesField('language_skills',
64        default = [],
65        index = 'KeywordIndex:schema',
66        vocabulary = 'getLanguagelist',
67        widget = PicklistWidget(           
68             label = 'Languages',
69             label_msgid = 'label_group_language_skills',
70             description = "Choose languages that the group is willing to use. This affects only when people try to find groups based on a language.",
71             description_msgid = 'help_group_language_skills',
72             i18n_domain = "lemill"
73             )
74        ),       
75    LinesField('recent_posts',
76        default= [],
77        widget = LinesWidget(
78            visible = {'view':'invisible', 'edit':'invisible'},
79            )
80        ),
81    LinesField('recent_activity',
82        default= [],
83        widget = LinesWidget(
84            visible = {'view':'invisible', 'edit':'invisible'},
85            ),
86        ),
87
88    LinesField('moderators',
89        default = [],
90        vocabulary = 'getMembersList',
91        widget = PicklistWidget(           
92             label = 'Moderators',
93             label_msgid = 'label_group_moderators',
94             description = "Trusted members of group can help you to remove offensive posts and keep the group together.",
95             description_msgid = 'help_group_moderators',
96             i18n_domain = "lemill",
97             visible = {'view':'invisible', 'edit':'visible'},
98             )
99        ),       
100
101
102    TagsField('banned',
103        default=[],
104        widget = TagsWidget(
105            label = 'Ban members',
106            label_msgid = 'label_ban_members',
107            description = "If someone repeatedly offends against group, group founder can ban him and prevent from joining the group again. Separate user id's with commas. User id's can be found by going to offending members member page and noticing the address in the browser. Id is the last part of address, after /community/.",
108            descriptio_msgid = 'help_ban_members',
109            i18n_domain = "lemill",
110            visible = {'view':'invisible', 'edit':'invisible'},
111            ),
112        ),
113))
114
115schema = schema.copy()
116schema['allowDiscussion'].widget.visible = {'edit':'invisible', 'view':'invisible'}
117schema['subject'].widget.visible = {'edit':'invisible', 'view':'invisible'}
118schema['contributors'].widget.visible = {'edit':'invisible', 'view':'invisible'}
119schema['creators'].widget.visible = {'edit':'invisible', 'view':'invisible'}
120schema['language'].widget.visible = {'edit':'invisible', 'view':'invisible'}
121schema['rights'].widget.visible = {'edit':'invisible', 'view':'invisible'}
122schema['tags'].schemata='default'
123schema['subject_area'].schemata='default'
124schema.moveField('subject_area', after='language_skills')
125schema['subject_area'].searchable = True
126
127
128class GroupBlog(BaseFolder,CommonMixIn):
129    """Group blog"""
130
131    meta_type = "GroupBlog"
132    archetype_name = "GroupBlog" 
133    typeDescription="Shared blog for a group"
134    typeDescMsgId='description_groupblog'
135    global_allow = 1
136
137    allowed_content_types = ('BlogPost')
138    default_view = ('groupblog_view')
139    filter_content_types = True
140    security = ClassSecurityInfo()
141    schema = schema
142
143    actions = ({ 'id':'view',
144                 'name': 'View',
145                 'action': 'string:${object_url}/groupblog_view',
146                 'permissions': (VIEW,)
147               },
148               { 'id':'edit',
149                 'name': 'Edit',
150                 'action': 'string:${object_url}/base_edit',
151                 'permissions': (MODIFY_CONTENT,)
152               },
153               { 'id':'edit_links',
154                 'name': 'Edit links',
155                 'action': 'string:${object_url}/base_metadata',
156                 'permissions': (MODIFY_CONTENT,)
157               },
158               { 'id':'edit_categories',
159                 'name': 'Edit categories',
160                 'action': 'string:${object_url}/base_metadata',
161                 'permissions': (MODIFY_CONTENT,)
162               },
163               )
164    aliases = {
165        '(Default)' : '',
166        'view'      : 'groupblog_view',
167        'edit'      : 'base_edit',
168        'edit_categories' : 'base_metadata',
169        'edit_links' : 'base_metadata',
170    }
171
172    def manage_afterAdd(self, item, container):
173        # Replaces the left side portlets with the content type's own action portlet.
174        BaseFolder.manage_afterAdd(self, item, container)
175        mtool = getToolByName(self, 'portal_membership')
176        memberfolder=mtool.getHomeFolder()
177        me = mtool.getAuthenticatedMember()
178        if memberfolder!=None and type(memberfolder)=='MemberFolder':
179            memberfolder.note_action(item.UID(), item.portal_type, 'afterAdd')
180        self.content_status_modify(workflow_action='publish', msg='silent')
181        if not hasattr(item.aq_base, 'left_slots'):
182            self._setProperty('left_slots', ['here/portlet_%s_actions/macros/portlet' % item.meta_type.lower(),], 'lines')
183        # Enable topic syndication by default
184        syn_tool = getToolByName(self, 'portal_syndication', None)
185        if syn_tool is not None:
186            if syn_tool.isSiteSyndicationAllowed():
187                try:
188                    syn_tool.enableSyndication(self)
189                except: # might get 'Syndication Information Exists'
190                    pass
191
192
193    def at_post_edit_script(self):
194        mtool = getToolByName(self, 'portal_membership')
195        ltool = getToolByName(self, 'lemill_tool')
196        memberfolder=mtool.getHomeFolder()
197        if memberfolder!=None:
198            memberfolder.note_action(self.UID(), self.portal_type, 'post_edit')
199
200        self.recalculateScore()
201        if hasattr(ltool,'allow_banning'):
202            if ltool.allow_banning:
203                if self.getField('banned').widget.visible['edit']!='visible':
204                    self.getField('banned').widget.visible['edit']='visible'
205            else:
206                if self.getField('banned').widget.visible['edit']!='invisible':
207                    self.getField('banned').widget.visible['edit']='invisible'
208
209
210    def setMeAsOwner(self):
211        mtool = getToolByName(self, 'portal_membership')
212        me = mtool.getAuthenticatedMember()
213        if [self.getId()] == self.users_with_local_role('Owner'):
214            self.manage_setLocalRoles(me.getId(), ['Owner'])
215            self.manage_addLocalRoles(me.getId(), ['Reviewer'])
216            #print self.users_with_local_role('Owner')
217           
218
219    def getGroupMaterials(self, n=False):
220        """ Get resources used by group, uses portfolio-topic """
221        results=self.portfolio.queryCatalog(getGroupsShared=self.getId())
222        if n:
223            return len(results)
224        return results
225
226    def getLatestEditDate(self):
227        """Returns creation date, we don't want to notify about every edit"""
228        return DateTime(self.CreationDate())
229
230    def getSamples(self):
231        """ get n number of samples """
232        q = {'review_state':'public', 'meta_type': self.getFeaturedTypes(), 'getHasCoverImage':True, 'getGroupsShared': self.getId()}
233        all = self.portfolio.queryCatalog(q)
234        import random
235        n = min(3, len(all))
236        return random.sample(all,n)
237
238
239
240    def prefill_title(self):
241        # When GroupBlog is created we need to get same values as group itself here
242        # It seems that the marching order in creating a group sets these to 'x:s workspace' after creating object.
243        groupid = self.getId()
244        grouptool = getToolByName(self, 'portal_groups')
245        group=grouptool.getGroupById(groupid)
246        value= group.getTitle()
247        return value
248
249    def prefill_description(self):
250        # When GroupBlog is created we need to get same values as group itself here
251        groupid = self.getId()
252        grouptool = getToolByName(self, 'portal_groups')
253        group=grouptool.getGroupById(groupid)
254        value= group.getDescription()
255        return value
256
257    def getCoverImageURL(self, drafts=False):
258        """Returns the URL for the cover image."""
259        timestamp = time.time()
260        if self.getField('hasCoverImage').get(self)==True:
261            return self.absolute_url()+'/coverImage?newest='+str(timestamp)
262        return 'images/default_group.png'
263       
264    def amIOwner(self):
265        """ check owner of object """
266        roles = self.portal_membership.getAuthenticatedMember().getRolesInContext(self)
267        return 'Owner' in roles
268
269    def isPost(self):
270        return False
271
272    def getMembersList(self, no_self=True):
273        groupid = self.getId()
274        grouptool = getToolByName(self, 'portal_groups')
275        usertool = getToolByName(self, 'lemill_usertool')
276        group=grouptool.getGroupById(groupid)
277        if no_self:
278            dll = [usertool.getLeMillMemberFolder(y.getId()) for y in group.getGroupMembers() if y.getId()!=self.Creator()]
279        else:
280            dll = [usertool.getLeMillMemberFolder(y.getId()) for y in group.getGroupMembers()]
281        dl = [(x.getId(), x.NiceName()) for x in dll]
282        return DisplayList(dl)
283
284    def getGroupMembers(self):
285        """ Public access to memberlist, returns tuple where (nicename, role, url) """
286        groupid = self.getId()
287        grouptool = getToolByName(self, 'portal_groups')
288        usertool = getToolByName(self, 'lemill_usertool')
289        group=grouptool.getGroupById(groupid)
290        if group:
291            dll = [usertool.getLeMillMemberFolder(y.getId()) for y in group.getGroupMembers()]
292            return [(x.NiceName(), self.get_local_roles_for_userid(x.getId()),x.absolute_url()) for x in dll if dll]
293            return []
294        else:
295            return []
296       
297       
298    def NiceName(self):
299        """ To make GroupBlog compatible with MemberFolders """
300        return self.getTitle()
301
302    def getLanguagelist(self):
303        languagelist=self.availableLanguages()
304        return DisplayList(languagelist)
305
306    def isMember(self, memberid):
307        groupid = self.getId()
308        grouptool = getToolByName(self, 'portal_groups')
309        group=grouptool.getGroupById(groupid)
310        return memberid in [x.getId() for x in group.getGroupMembers()]
311
312
313    def canIModerate(self):
314        roles = self.getRoles()
315        return 'Manager' in roles or 'Reviewer' in roles
316
317
318    def getRoles(self, memberid=None):
319        # Helper method to get roles
320        if memberid:
321            return self.get_local_roles_for_userid(memberid)
322        else:
323            mtool = getToolByName(self, 'portal_membership')
324            user = mtool.getAuthenticatedMember()       
325            return self.get_local_roles_for_userid(user.getId())
326       
327    def getPosts(self, batch=True, b_size=30, b_start=0):
328        """ Return get posts as full objects, sort them by date and batch them """
329        wtool = getToolByName(self,'portal_workflow')
330        results= [(obj.CreationDate(), obj) for obj in self.objectValues() if obj.portal_type=='BlogPost' and wtool.getInfoFor(obj,'review_state',None)!='deleted']
331
332        results.sort(key=operator.itemgetter(0))
333        results.reverse()
334        results =[obj for (d,obj) in results]
335        if batch:
336            from Products.CMFPlone import Batch
337            results = Batch(results, b_size, int(b_start), orphan=0)       
338        return results
339
340    def getFullForumRSS(self):
341        """ Returns forum topics and comments for them """
342        full_results=[]
343        wtool = getToolByName(self,'portal_workflow')
344        results= [(obj.CreationDate(), obj) for obj in self.objectValues() if obj.portal_type=='BlogPost' and wtool.getInfoFor(obj,'review_state',None)!='deleted']
345        results.sort(key=operator.itemgetter(0))
346        results.reverse()
347        results =[obj for (d,obj) in results]
348        for result in results:
349            full_results.append(result)
350            for x in result.getComments():
351                full_results.append(x)
352        return full_results
353
354    def getBlog(self):
355        return self
356
357    def getRecent_activity(self):
358        """ Because I prefer lists, not tuples """
359        return list(self.getField('recent_activity').get(self))
360
361    security.declareProtected(MODIFY_CONTENT,'addRecent_activity')
362    def addRecent_activity(self, obj_uid, act_type):
363        """ Recent activity is a list of (obj_UID, date, activity type {'modified piece', 'created' etc.})
364         that tracks member activities for all kinds of calculations """
365
366        current_date = DateTime()
367        acts= self.getRecent_activity()
368        acts=[(obj_uid,current_date,act_type)]+acts
369        # pop out old collaboration proposals from tail of the list
370        while acts[-1][1] < (current_date-31):
371            acts.pop()
372        acts_field=self.getField('recent_activity')
373        acts_field.set(self, acts)
374       
375    security.declareProtected(MODIFY_CONTENT,'addRecent_post')
376    def addRecent_post(self,postid):
377        """ Recent posts are lists of id's, because then picking the post objects when inside group is easy and quick """
378        # sometimes this method gets called before new blogposts get their proper id:s, so escape then:
379        if postid.startswith('blogpost.'):
380            return None
381        postlist= list(self.getField('recent_posts').get(self))
382        if postid in postlist:
383            postlist.remove(postid)
384        postlist=[postid]+postlist
385        self.setRecent_posts(postlist[:10])           
386
387    security.declareProtected(MODIFY_CONTENT,'removeRecent_post')
388    def removeRecent_post(self,postid):
389        postlist= list(self.getField('recent_posts').get(self))
390        if postid in postlist:
391            postlist.remove(postid)
392        self.setRecent_posts(postlist)           
393
394    security.declareProtected(MODIFY_CONTENT,'replaceRecent_post')
395    def replaceRecent_post(self, old, new):
396        postlist= list(self.getField('recent_posts').get(self))
397        postlist[postlist.index(old)]=new
398        self.setRecent_posts(postlist)           
399
400    def do_joining(self, userid, groupid):
401        try:
402            self.acl_users.source_groups.addPrincipalToGroup(userid, groupid)
403        except:
404            return 1
405        return 0
406
407       
408    def do_leaving(self, userid, groupid):
409        try:
410            self.acl_users.source_groups.removePrincipalFromGroup(userid, groupid)
411            if len(self.getGroupMembers()) <= 0:
412                for x in self.getGroupMaterials():
413                    x.getObject().setGroupsShared('no_group')
414                groupTool = getToolByName(self, 'portal_groups')
415                groupTool.removeGroups([self.getId()])
416        except:
417            return 1
418        return 0
419           
420    def join_group(self):
421        """ The logged in user joins the current group """
422        REQUEST=self.REQUEST
423        mtool = getToolByName(self, 'portal_membership')
424        putils= getToolByName(self, 'plone_utils')
425        user = mtool.getAuthenticatedMember()       
426        userid = user.getId()
427        groupid = self.getId()
428        banned=self.getField('banned').get(self)       
429        if userid in banned:
430            putils.addPortalMessage( _(u"You have been banned from this group and cannot join anymore."))
431            return REQUEST.RESPONSE.redirect(self.community.absolute_url())
432       
433        join_res = 0
434        join_res = self.do_joining(userid, groupid)
435        if not join_res:
436            msg = _("You have joined the group '${title}'.", mapping={u'title' : self.title_or_id()})
437        else:
438            msg= _(u"Joining the group failed for some reason.")
439        putils.addPortalMessage(msg)
440        return REQUEST.RESPONSE.redirect(self.absolute_url())
441   
442    def leave_group(self):
443        """ The logged in user leaves the current group """
444        REQUEST=self.REQUEST
445        mtool = getToolByName(self, 'portal_membership')
446        putils= getToolByName(self, 'plone_utils')
447        user = mtool.getAuthenticatedMember()
448        userid = user.getId()
449        groupid = self.getId()
450        leave_res = 0
451        leave_res = self.do_leaving(userid, groupid)
452        if not leave_res:
453            msg = _("You have left the group '${title}'.", mapping={u'title' : self.title_or_id()})
454        else:
455            msg= _(u"Leaving the group failed for some reason.")
456        putils.addPortalMessage(msg)
457        return REQUEST.RESPONSE.redirect(self.community.absolute_url())
458
459
460### Mutators = default mutator + set group properties
461
462   
463    security.declareProtected(MODIFY_CONTENT,'setProperties')
464    def setProperties(self, newprops=None, **kw):
465        """ Try to get mutator for each value and if there isn't one then change groups properties """
466        if newprops is None:
467            newprops = kw
468        grouptool = getToolByName(self, 'portal_groups')
469        groupid=self.getId()
470        #print groupid +' changes properties:' + str(newprops)
471        for (key, value) in newprops.items():
472            accessor='get'+str(key.capitalize())
473            mutator='set'+str(key.capitalize())
474
475            if hasattr(self, accessor):
476                if eval('self.'+accessor+'()') != value:
477                    eval('self.'+mutator+'(value)')
478                    #print 'changing attribute with self.'+mutator+'(value), value='+str(value)
479                else:
480                    group=grouptool.getGroupById(groupid)
481                    group.setProperties({key:value})
482        self.reindexObject()         
483
484    def setModerators(self, value, **kwargs):
485        field=self.getField('moderators')
486        old_mods=field.get(self)
487        if old_mods==None or old_mods=='' or old_mods==['']: old_mods=[]
488        if value==None or value=='' or value==['']: value=[]
489        additions= [x for x in value if x not in old_mods]
490        for member in additions:
491            if member != '':
492                self.manage_setLocalRoles(member,['Reviewer'])
493        removals= [x for x in old_mods if x not in value]
494        for member in removals:
495            if 'Owner' not in self.get_local_roles_for_userid(member):
496                self.manage_delLocalRoles(member)           
497        field.set(self, value, **kwargs)
498       
499    def setBanned(self, value, **kwargs):
500        field=self.getField('banned')
501        if type(value)==list or type(value)==tuple:
502            banlist=list(value)
503        else:
504            banlist= [x.strip() for x in value.split(',')]
505        if banlist==['']: banlist=[]
506        grouptool = getToolByName(self, 'portal_groups')
507        groupid=self.getId()
508        group=grouptool.getGroupById(groupid)
509        groupmembers= group.getGroupMemberIds()
510        those_who_need_a_kick= [x for x in banlist if x in groupmembers]
511        for kickee in those_who_need_a_kick:           
512            if 'Owner' not in self.get_local_roles_for_userid(kickee): # can't kick yourself
513                self.manage_delLocalRoles(kickee)                       
514                self.do_leaving(kickee, groupid) # kick!
515        field.set(self, value, **kwargs)
516       
517
518    # These have to be duplicated since its difficult to base these folderish objects on non-folderish Resources
519
520    security.declareProtected(MODIFY_CONTENT,'setCoverImage')
521    def setCoverImage(self, value, **kwargs):
522        """ Normal mutator, but flags object to have a coverImage (hasCoverImage = True) """
523        cover=self.getField('coverImage')
524        cover.set(self,value,**kwargs)
525        has_cover=self.getField('hasCoverImage')
526        has_cover.set(self,True)
527        self.reindexObject()
528
529    security.declareProtected(MODIFY_CONTENT,'delCoverImage')
530    def delCoverImage(self):
531        """ Reverse of setCoverImage """
532        cover=self.getField('coverImage')
533        cover.set(self,None)
534        has_cover=self.getField('hasCoverImage')
535        has_cover.set(self,False)
536        self.reindexObject()
537
538    def getArchDates(self):
539        """ Get years for Archives  """
540        dates = []
541        # The result should be something like this (but in reverse sorted order):
542        #['2006-10', '2006-01', '2007-11', '2006-07', '2007-04', '2007-02']
543        results = self.objectValues('BlogPost')
544        for x in results:
545            cDate = x.CreationDate()[:7]
546            if cDate not in dates:
547                dates.append(cDate)
548        dates.sort()
549        dates.reverse()
550       
551        return dates
552
553
554    def interpritArchDates(self):
555        """ Interprit arch Dates given  """
556        preresults = []
557        results = []
558        # This function splits the dates array elements by '-' in 2. And changes the code of the month by its name.
559        gotDates = self.getArchDates()
560        for y in gotDates:
561            preresults.append(y.split("-"))
562        for z in preresults:
563            item = z[1]
564            if item in monthNames.keys():
565                z[1] = monthNames[item]
566                results.append(z)
567       
568        return results
569
570    def recalculateScore(self):
571        """ Calculates the score for Group Blog according to specifications """
572        lt = getToolByName(self, 'lemill_tool')
573        score = float(0)
574        members_score = len(self.getGroupMembers())
575        materials = self.getGroupMaterials()
576        posts = self.getPosts(batch=False)
577
578        score = score + members_score
579
580        for m in materials:
581            months_old = lt.getTimeDifference(m.getLatestEdit)
582            months_old = months_old['months']
583            score = score + 1 / float(months_old + 1)
584
585        for p in posts:
586            months_old = lt.getTimeDifference(p.getLatestEditDate())
587            months_old = months_old['months']
588            score = score + 1 / float(months_old + 1)
589
590        score = int(round(score))
591
592        # This one is not really needed, because Group must have at least one member
593        if score<1:
594            score = 1
595        # Now to set the value for field score
596        self.setScore(score)
597
598
599       
600
601registerType(GroupBlog, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.