source: trunk/MemberFolder.py @ 1213

Revision 1213, 22.4 KB checked in by jukka, 13 years ago (diff)

Closed #1028, spent 2h.

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.ATContentTypes.content.folder import ATFolder
21from Products.ATReferenceBrowserWidget.ATReferenceBrowserWidget import ReferenceBrowserWidget
22from Products.CMFCore.permissions import ModifyPortalContent
23from Products.Archetypes.public import BaseFolder, BaseFolderSchema, registerType
24from Products.Archetypes.atapi import DisplayList
25from Globals import InitializeClass
26from Products.CMFCore.utils import getToolByName
27from AccessControl import ClassSecurityInfo, Unauthorized
28from config import *
29from Products.PloneLanguageTool.availablelanguages import countries
30try:
31    from Products.PloneLanguageTool.availablelanguages import languages_english
32except ImportError:
33    from Products.PloneLanguageTool.availablelanguages import languages
34    languages_english = dict([(key, val['english']) for (key, val) in languages.iteritems()])
35    del languages
36
37from FieldsWidgets import TagsField, TagsWidget, MessengerWidget, MobileWidget, LeTextAreaWidget
38from Resources import CommonMixIn
39
40from DateTime import DateTime
41import datetime
42
43
44messenger_vocabulary = [(x,x) for x in INSTANT_MESSENGERS]
45
46# if it is plausible that just this field is searched somewhere, then field can have its own index. Otherwise it should be just 'searchable=True'. Those are all kept in one big text index.
47
48schema = BaseFolderSchema + Schema((
49    StringField('title',
50        mode ='r',
51        # default_method is called only once, when first needed; after that, getXxx is used
52        default_method = 'prefill_title',
53        widget=StringWidget(
54            label_msgid='label_title',
55            visible={'edit' : 'invisible'},           
56        ),
57    ),
58    TextField('firstname',
59        default_method = 'prefill_firstname',
60        widget = StringWidget(
61             label = 'First name',
62             label_msgid = 'label_firstname',
63             i18n_domain = "lemill"
64             )
65        ),
66    TextField('lastname',
67        index = 'FieldIndex:schema',
68        default_method = 'prefill_lastname',
69        widget = StringWidget(
70             label = 'Last name',
71             label_msgid = 'label_lastname',
72             i18n_domain = "lemill"
73             )
74        ),
75    ComputedField('fullname',
76        index = 'FieldIndex:schema',
77        searchable = True,
78        expression = 'here.getFullname()',
79        default_method = 'prefill_fullname',
80        ),
81    TextField('nickname',
82        index = 'FieldIndex:schema',
83        searchable = True,
84        widget = StringWidget(
85             label = 'Nick name',
86             label_msgid = 'label_nickname',
87             description = 'If you enter a nick name, it will be visible instead of your full name.',
88             description_msgid = 'help_nickname',
89             i18n_domain = "lemill"
90             )
91        ),
92    ComputedField('nicename', # so that displayed name can be pulled from catalog metadata
93        index= 'FieldIndex:schema',
94        expression = 'here.getNicename()',
95        default_method = 'prefill_fullname',
96        ),
97    ComputedField('sortable_nicename', # so that displayed names can be ordered alphabetically
98        index= 'FieldIndex:schema',
99        expression = 'here.getSortable_nicename',
100        default_method = 'prefill_lastname',
101        ),
102       
103    TextField('email',
104        searchable = True,
105        index = 'FieldIndex:schema',
106        default_method = 'prefill_email',
107        validators = 'isEmail',
108        widget = StringWidget(
109             label = 'Email address',
110             label_msgid = 'label_email',
111             i18n_domain = "lemill"
112             )
113        ),
114
115    TextField('mobile',
116        widget = MobileWidget(
117            label = 'Phone',
118            label_msgid = 'label_phone',
119            description = 'Check the box if you approve community members sending text messages.',
120            description_msgid = 'help_mobile',
121            i18n_domain ='lemill',
122            )
123    ),         
124
125    TextField('messenger1',
126        vocabulary = DisplayList(messenger_vocabulary),
127        widget = MessengerWidget(
128             label = 'Instant messengers',
129             label_msgid = 'label_messenger',
130             i18n_domain = "lemill"
131             )
132        ),
133    TextField('messenger2',
134        vocabulary = DisplayList(messenger_vocabulary),
135        widget = MessengerWidget(
136             label = ' ',
137             )
138        ),
139    TextField('messenger3',
140        vocabulary = DisplayList(messenger_vocabulary),
141        widget = MessengerWidget(
142             label = ' ',
143             )
144        ),
145    TextField('location_country',
146        index = 'FieldIndex:schema',
147        searchable = True,
148        vocabulary = 'getCountrylist',
149        widget = SelectionWidget(
150             label = 'Country',
151             label_msgid = 'label_country',
152             i18n_domain = "lemill"
153             )
154        ),       
155    TextField('location_area',
156        searchable = True,
157        index = 'FieldIndex:schema',
158        widget = StringWidget(
159             label = 'City or area',
160             label_msgid = 'label_area',
161             i18n_domain = "lemill"
162             )
163        ),
164    TextField('home_page',
165        widget = StringWidget(
166             label = 'Homepage',
167             label_msgid = 'label_home_page',
168             i18n_domain = "lemill",
169             validators = 'isURL'
170             )
171        ),
172    LinesField('language_skills',
173        default = [],
174        index = 'KeywordIndex:schema',
175        vocabulary = 'getLanguagelist',
176        widget = PicklistWidget(           
177             label = 'Languages',
178             label_msgid = 'label_language_skills',
179             description = "Choose languages you can use to create learning resources or communicate.",
180             description_msgid = 'help_language_skills',
181             i18n_domain = "lemill"
182             )
183        ),       
184    TagsField('skills',
185        index = 'KeywordIndex:schema',
186        widget = TagsWidget(           
187             label = 'Skills',
188             label_msgid = 'label_skills',
189             description = "Enter a list of your skills separated by commas.",
190             description_msgid = 'help_skills',
191             i18n_domain = "lemill"
192             )
193        ),
194    TagsField('interests',
195        index = 'KeywordIndex:schema',
196        widget = TagsWidget(
197             label = 'Interests',
198             label_msgid = 'label_interests',
199             description = "Enter a list of your interests separated by commas.",
200             description_msgid = 'help_interests',
201             i18n_domain = "lemill"
202             )
203        ),
204    ComputedField('tags',
205        index = 'KeywordIndex:schema',
206        expression = 'here.getTags()',
207        ),
208
209    LinesField('recent_activity',
210        default= [],
211        widget = LinesWidget(
212            visible = {'view':'invisible', 'edit':'invisible'},
213            ),
214        ),
215
216    IntegerField('activity_score',
217        index = 'FieldIndex:schema',
218        default = 0,
219        widget = IntegerWidget(
220                 visible = {'view':'invisible', 'edit':'invisible'},
221             ),
222        ),
223
224    TextField('biography',
225        searchable = True,
226        widget = LeTextAreaWidget(
227            label = 'Biography',
228            label_msgid = 'label_your_biography',
229            description = "Any background information about yourself that you wish to share with others.",
230            description_msgid = 'help_your_biography',
231            i18n_domain = "lemill"
232            )
233        ),
234
235    LinesField('used_content',
236        mode = 'r',
237        index = 'KeywordIndex:schema',
238        widget = LinesWidget(
239                 visible = {'view':'invisible', 'edit':'invisible'},
240             )
241        ),
242    BooleanField('hasCoverImage',
243                default=False,
244                index="FieldIndex:schema",
245                accessor='getHasCoverImage',
246                mutator='setHasCoverImage',
247                widget = BooleanWidget(
248                    visible = {'view':'invisible', 'edit':'invisible'},
249                    ),
250                ),
251    StringField('wysiwyg_editor',
252            default='kupu',
253            mode = 'r',
254            widget = StringWidget(
255                visible = {'view':'invisible', 'edit':'invisible'},
256                ),
257            ),
258
259))
260
261# This lists the fields that also exist in
262# the memberdata tool
263# TODO: this could be automatically generated from the schema and member property list
264DUPLICATED_FIELDS = ('firstname','lastname','email')
265
266schema = schema.copy()
267
268class MemberFolder(BaseFolder,CommonMixIn):
269    """Member folder"""
270
271    meta_type = "MemberFolder"
272    archetype_name = "MemberFolder" 
273    typeDescription="Member folder"
274    typeDescMsgId='description_memberfolder'
275    global_allow = 1
276
277    allowed_content_types = ('CollectionsFolder', 'StoryFolder', 'Topic')
278    default_view = ('member_view')
279    filter_content_types = True
280    security = ClassSecurityInfo()
281    schema = schema
282
283    actions = (
284        {'id':'view',
285         'name':'View',
286         'action':"string:${object_url}/member_view",
287         'permission':(VIEW,),
288        },
289        {'id':'edit',
290         'name':'Edit',
291         'action':"string:${object_url}/personalize_form",
292         'permission':(MODIFY_CONTENT,),
293        },
294    )   
295    aliases = {
296        '(Default)': '',
297        'edit': 'personalize_form',
298        'view':'member_view'
299        }
300
301    def manage_afterAdd(self, item, container):
302        BaseFolder.manage_afterAdd(self, item, container)
303        if not hasattr(item.aq_base, 'left_slots'):
304            self._setProperty('left_slots', ['here/portlet_member/macros/portlet',], 'lines')
305        else:
306            self._updateProperty('left_slots', ['here/portlet_member/macros/portlet',])
307        # Handle case when logged in user is authenticated from an
308        # external source for the first time
309        try:
310            # This is not very nicely done, should use API methods to access what I need
311            uinfo = self.acl_users.source_users.getUserInfo(self.getId())
312        except KeyError: # Doesn't exist yet - create
313            self.acl_users.source_users.addUser(self.getId(),self.getId(),'')
314            self.acl_users.portal_role_manager.assignRolesToPrincipal(('Member',),self.getId())
315        # If we've done external authentication, then the user isn't really yet
316        # properly authenticated, so we can't create
317        # the collections and resources folders.
318
319    def createResourcesTopic(self):
320        # assumes that topic 'resources' doesn't exist
321        self.invokeFactory('Topic', id='resources')
322        topic=self.resources
323        criterion = topic.addCriterion('Type', 'ATPortalTypeCriterion' )
324        criterion.setValue(CREATED_RESOURCES)
325        criterion = topic.addCriterion('review_state','ATSelectionCriterion')
326        criterion.setValue(('public','draft'))   
327        criterion = topic.addCriterion('Creator', 'ATSimpleStringCriterion')
328        criterion.setValue(self.getId())
329
330    def getMemberId(self):
331        """ return user's id """
332        return self.getId()
333
334    def getMemberFolder(self):
335        """ return the member, useful for collections, topics etc. subobjects """
336        return self
337
338    def createStoriesTopic(self):
339        # assumes that topic 'stories' doesn't exist
340        self.invokeFactory('Topic', id='stories')
341        topic=self.stories
342        criterion = topic.addCriterion('Type', 'ATPortalTypeCriterion' )
343        criterion.setValue('Story')
344        criterion = topic.addCriterion('review_state','ATSelectionCriterion')
345        criterion.setValue(('public','draft'))
346        criterion = topic.addCriterion('Creator', 'ATSimpleStringCriterion')
347        criterion.setValue(self.getId())
348
349    def getCollectionsFolder(self):
350        """..."""
351        if not hasattr(self.aq_base, 'collections'):
352            self.invokeFactory('CollectionsFolder', id='collections')
353        return self.collections
354   
355    def getCollections(self, obj_id=None):
356        """Return list of user's collections."""
357        if obj_id:
358            return self.aq_parent.getCollections(obj_id)
359        return self.getCollectionsFolder().objectValues('Collection')
360
361    def delCollection(self, obj_id):
362        obj=self.collections.get(obj_id)
363        if obj.amIOwner():       
364            self.collections._delObject(obj_id)
365            msg=u'Collection deleted'
366        else:
367            msg=u'You are not allowed to delete this collection'
368        return (self.collections, msg)     
369
370    def getStoriesFolder(self):
371        """..."""
372        if not hasattr(self.aq_base, 'stories'):
373            self.invokeFactory('StoryFolder', id='stories')
374        return self.stories
375   
376    def getStories(self):
377        """..."""
378        return self.getStoriesFolder().objectValues('Story')
379   
380    def whatamI(self):
381        # Because my dynamically created methods confuse my author
382        #print dir(self)
383        return dir(self)       
384
385    def computeTags(self):
386        return self.skills + self.interests
387
388    def getCountrylist(self):
389        """ We don't need short country codes, so we make a list where fancyname=fancyname and give that as DisplayList """
390        countries_root = [('No country specified', 'No country specified')]
391        countries_list = [(x[1],x[1]) for x in countries.items()]
392        countries_list.sort()
393        countries_root= countries_root + countries_list
394        return DisplayList(tuple(countries_root)) # not sure, but DisplayList might require tuples not lists.
395
396   
397    def getLanguagelist(self):
398        languagelist=self.availableLanguages()
399        return DisplayList(languagelist[1:]) # Cut out the first, 'no language specified'-option
400
401
402    def getPortrait(self, author=None):
403        """ Gets portrait from portal_membership """
404        mtool   = getToolByName(self, 'portal_membership')
405        if author:
406            portrait = mtool.getPersonalPortrait(author)
407        else:
408            portrait = mtool.getPersonalPortrait(self.getId())
409        return portrait       
410
411    def getPortraitURL(self,author=None):
412        """ Gets portrait from portal_membership """
413        portrait=self.getPortrait(author=author)
414        return portrait.absolute_url()
415
416    def getCoverImage(self):
417        """ This makes the method work similarly with people and objects """
418        mtool   = getToolByName(self, 'portal_membership')
419        portrait = mtool.getPersonalPortrait(self.getId())
420        return portrait
421     
422    def NiceName(self):
423        """ Nickname if exists, fullname if not """
424        if self.getNickname()!='':
425            return self.getNickname()
426        elif self.getFullname()!='':
427            return self.getFullname()
428        else:
429            return self.getId()   
430
431    def getNicename(self):
432        return self.NiceName()
433
434    def getSortable_nicename(self):
435        name = self.NiceName()
436        if name == self.getFullname():
437            name=self.getLastname()
438        return name.lower()
439
440    def prefill_title(self):
441        mdatatool = getToolByName(self, 'portal_memberdata')
442        value = mdatatool.getMemberId()
443        return value
444
445    def __getMemberProperty(self,key):
446        memberid = self.getId()
447        mtool = getToolByName(self, 'portal_membership')
448        member = mtool.getMemberById(memberid)
449        if member:
450            return member.getProperty(key)
451        else:
452            return ''
453
454    def debugProps(self):
455        """..."""
456        mtool = getToolByName(self, 'portal_memberdata')
457        raise 'FOO',str(mtool.propdict())
458       
459
460    def prefill_email(self):
461        """ When memberfolder is created first time we need to get some values right, as they will be used in templates"""
462        return self.__getMemberProperty('email')
463       
464    def prefill_firstname(self):
465        return self.__getMemberProperty('firstname')
466
467    def prefill_lastname(self):
468        return self.__getMemberProperty('lastname')
469
470    # Both the default_method and normal getter for fullname field
471    def getFullname(self):
472        first = str(self.__getMemberProperty('firstname'))
473        last = str(self.__getMemberProperty('lastname'))
474        if first=='None' or first=='':
475            first=''
476        if last=='None' or last=='':
477            last=''           
478        if first=='' and last=='':
479            return ''
480        else:
481            return " ".join((first, last))       
482
483    def setFullname(self,value):
484        names=value.split(' ')
485        self.setFirstname(names[0])
486        self.setLastname(names[1])
487
488
489
490    def flagCoverImageOn(self):
491        self.getField('hasCoverImage').set(self, True)
492
493    def flagCoverImageOff(self):
494        self.getField('hasCoverImage').set(self, False)
495
496
497    def at_post_edit_script(self):
498        putil = getToolByName(self,'plone_utils')
499        pmem = getToolByName(self,'portal_membership')
500        user = pmem.getMemberById(self.getId())
501        for field in DUPLICATED_FIELDS:
502            value = self.getField(field).get(self)
503            if value != user.getProperty(field):
504                user.setMemberProperties({field:value})
505
506    def getTags(self):
507        lst = self.getField('interests').get(self) + self.getField('skills').get(self)
508        return list(lst)
509
510    def getResources(self, n=False, REQUEST=None, filter=None):
511        """ Search for content-like created by member.
512        The funny REQUEST thingy is because the link to the resources topic
513        can't be made directly, since it might not exist. It's created here
514        if necessary."""
515        if not hasattr(self.aq_base, 'resources'):
516            if n == True:
517                return 0
518            self.createResourcesTopic()
519        if n==True:
520            #if not hasattr(self.aq_base, 'resources_n'):
521            if filter is None:
522                results=self.resources.queryCatalog()
523            else:
524                results=self.resources.queryCatalog(meta_type=filter)
525            #self.resources_n=len(results)
526            return len(results)
527        else:
528           return REQUEST.RESPONSE.redirect(self.resources.absolute_url())
529
530    def getSamples(self):
531        """ get n number of samples """
532        if not hasattr(self.aq_base, 'resources'):
533            self.createResourcesTopic()
534        q = {'review_state':'public', 'meta_type': self.getFeaturedTypes(), 'getHasCoverImage':True}
535        all = self.resources.queryCatalog(q)
536        import random
537        n = min(3, len(all))
538        return random.sample(all,n)
539
540    def note_action(self, obj_uid, portal_type, sender):
541        """ Calculate activity score and add to recent activities """       
542        acts=self.getRecent_activity()
543        uids=[x[0] for x in acts]
544        score=self.getActivity_score()
545        if score==None: score=0
546        if sender=='post_edit':
547            msg='Modify %s' % portal_type
548        elif sender=='afterAdd':
549            msg='Create %s' % portal_type           
550        else:
551            msg='?? %s' % portal_type
552       
553        if not obj_uid in uids:
554            if sender=='afterAdd':
555                if portal_type=='Piece' or portal_type=='GroupBlog':
556                    score=score+3
557                elif 'Material' in portal_type or portal_type=='Activity' or portal_type=='Tool':
558                    score=score+5
559                elif portal_type=='BlogPost':
560                    score=score+2       
561            if sender=='post_edit':
562                if 'Material' in portal_type or portal_type=='Activity' or portal_type=='Tool':
563                    score=score+5           
564            self.setActivity_score(score)
565            self.reindexObject()
566        self.addRecent_activity(obj_uid, msg)
567
568    def getRecent_activity(self):
569        """ Linesfield stores tuple of strings and we need list of tuples. So... """
570        src=list(self.getField('recent_activity').get(self))
571        return [tuple(x.split(',')) for x in src]
572       
573       
574    def addRecent_activity(self, obj_uid, act_type):
575        """ Recent activity is a list of (obj_UID, date, activity type {'modified piece', 'created' etc.})
576         that tracks member activities for all kinds of calculations """
577
578        current_date = DateTime()
579        acts= self.getRecent_activity()
580        acts=[(obj_uid,current_date,act_type)]+acts
581        # pop out old collaboration proposals from tail of the list
582        while acts[-1][1] < (current_date-31):
583            acts.pop()
584        acts=[','.join((obj_uid, str(current_date), act_type)) for (obj_uid,current_date,act_type) in acts]
585        acts_field=self.getField('recent_activity')
586        acts_field.set(self, acts)
587
588    def resize_portrait(self, portrait, member_id):
589        """ resize user portrait if it is larger than 160x120 """
590        lt = getToolByName(self, 'lemill_tool')
591        s, im = lt.resize_image(portrait)
592       
593        from OFS.Image import Image
594        pr = Image(id=member_id, file=s, title='')
595        membertool   = getToolByName(self, 'portal_memberdata')
596        membertool._setPortrait(pr, member_id)
597        return 1
598
599    def getDefaultIcon(self, meta_type='', obj=None):
600        """ general method for getting proper icon for object, used when only catalog-metadata is available """
601        # this combines folderish getDefaultIcon(for-this-type, object) and resource-specific object.getDefaultIcon()
602        # folderish behaviour is needed because members have these created resources-pages. 
603        if meta_type=='':
604            return  DEFAULT_ICONS['MemberFolder']
605        else:     
606            address=DEFAULT_ICONS[meta_type]
607            if address!='piece':
608                return address
609            else:
610                obj=obj.getObject()
611                return obj.getDefaultIcon()
612   
613    def verifyMessengers(self, array):
614        """ check if there are some instant messengers to show
615            takes an array and if there is content then returns True
616        """
617        for x in array:
618            if x: return True
619        return False
620           
621       
622registerType(MemberFolder, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.