source: trunk/MemberFolder.py @ 1937

Revision 1937, 24.3 KB checked in by gabor, 12 years ago (diff)

fixed #1487 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 SharedMetadata import coverImage, score, latest_edit_schema
30from permissions import MODIFY_CONTENT
31from Products.PloneLanguageTool.availablelanguages import countries
32from FieldsWidgets import HTMLLinkWidget
33try:
34    from Products.PloneLanguageTool.availablelanguages import languages_english
35except ImportError:
36    from Products.PloneLanguageTool.availablelanguages import languages
37    languages_english = dict([(key, val['english']) for (key, val) in languages.iteritems()])
38    del languages
39
40from Products.LeMill import LeMillMessageFactory as _
41from FieldsWidgets import TagsField, TagsWidget, MessengerWidget, MobileWidget, LeTextAreaWidget, TwoColumnMultiSelectionWidget
42from Resources import CommonMixIn, CoverImageMixIn
43
44from DateTime import DateTime
45import datetime
46import random
47
48### MemberFolders should be created automatically. If users get their account from other authentication scheme, their folders can be created with methods at LargeCommunityFolder (my_page -method is run, if top-right-corner link to memberpage fails)
49
50messenger_vocabulary = [(x,x) for x in INSTANT_MESSENGERS]
51
52# 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.
53
54schema = BaseFolderSchema +coverImage + score + latest_edit_schema + Schema((
55    StringField('title',
56        mode ='r',
57        # default_method is called only once, when first needed; after that, getXxx is used
58        default_method = 'prefill_title',
59        widget=StringWidget(
60            label_msgid='label_title',
61            visible={'edit' : 'invisible'},           
62        ),
63    ),
64    TextField('firstname',
65        default_method = 'prefill_firstname',
66        searchable = True,
67        required = True,
68        widget = StringWidget(
69             label = 'First name',
70             label_msgid = 'label_firstname',
71             i18n_domain = "lemill"
72             )
73        ),
74    TextField('lastname',
75        index = 'FieldIndex:schema',
76        searchable = True,
77        required = True,
78        default_method = 'prefill_lastname',
79        widget = StringWidget(
80             label = 'Last name',
81             label_msgid = 'label_lastname',
82             i18n_domain = "lemill"
83             )
84        ),
85    TextField('full_name',
86        index = 'FieldIndex:schema',
87        default_method = 'prefill_full_name',
88        widget = StringWidget(
89             visible = {'view':'invisible', 'edit':'invisible'},
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_full_name',
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        required = True,
106        index = 'FieldIndex:schema',
107        default_method = 'prefill_email',
108        validators = 'isEmail',
109        widget = StringWidget(
110             label = 'Email address',
111             label_msgid = 'label_email',
112             i18n_domain = "lemill"
113             )
114        ),
115
116    TextField('mobile',
117        widget = MobileWidget(
118            label = 'Phone',
119            label_msgid = 'label_phone',
120            description = 'Check the box if you approve community members sending text messages.',
121            description_msgid = 'help_mobile',
122            i18n_domain ='lemill',
123            )
124    ),         
125
126    TextField('messenger1',
127        vocabulary = DisplayList(messenger_vocabulary),
128        widget = MessengerWidget(
129             label = 'Instant messengers',
130             label_msgid = 'label_messenger',
131             i18n_domain = "lemill"
132             )
133        ),
134    TextField('messenger2',
135        vocabulary = DisplayList(messenger_vocabulary),
136        widget = MessengerWidget(
137             label = ' ',
138             )
139        ),
140    TextField('messenger3',
141        vocabulary = DisplayList(messenger_vocabulary),
142        widget = MessengerWidget(
143             label = ' ',
144             )
145        ),
146    TextField('location_country',
147        index = 'FieldIndex:schema',
148        searchable = True,
149        vocabulary = 'getCountrylist',
150        widget = SelectionWidget(
151             label = 'Country',
152             label_msgid = 'label_country',
153             i18n_domain = "lemill"
154             )
155        ),       
156    TextField('location_area',
157        searchable = True,
158        index = 'FieldIndex:schema',
159        widget = StringWidget(
160             label = 'City or area',
161             label_msgid = 'label_area',
162             i18n_domain = "lemill"
163             )
164        ),
165    TextField('home_page',
166        widget = HTMLLinkWidget(
167             label = 'Homepage',
168             label_msgid = 'label_home_page',
169             i18n_domain = "lemill",
170             validators = 'isURL'
171             )
172        ),
173    LinesField('language_skills',
174        default = [],
175        index = 'KeywordIndex:schema',
176        vocabulary = 'getLanguagelist',
177        widget = PicklistWidget(           
178             label = 'Languages',
179             label_msgid = 'label_language_skills',
180             description = "Choose languages you can use to create learning resources or communicate.",
181             description_msgid = 'help_language_skills',
182             i18n_domain = "lemill"
183             )
184        ),       
185    TagsField('skills',
186        index = 'KeywordIndex:schema',
187        widget = TagsWidget(           
188             label = 'Skills',
189             label_msgid = 'label_skills',
190             description = "Enter a list of your skills separated by commas.",
191             description_msgid = 'help_skills',
192             i18n_domain = "lemill"
193             )
194        ),
195    TagsField('interests',
196        index = 'KeywordIndex:schema',
197        widget = TagsWidget(
198             label = 'Interests',
199             label_msgid = 'label_interests',
200             description = "Enter a list of your interests separated by commas.",
201             description_msgid = 'help_interests',
202             i18n_domain = "lemill"
203             )
204        ),
205    LinesField('subject_area',
206        vocabulary = DisplayList([(x,x) for x in SUBJECT_AREAS]),
207        index = "KeywordIndex:schema",
208        multivalued=True,
209        searchable=True,
210        widget=TwoColumnMultiSelectionWidget(
211            format="checkbox",
212            label="Subject area",
213            label_msgid="label_member_subject_area",
214            description="Choose subject areas that you are teaching.",
215            description_msgid="description_member_subject_area",
216            i18n_domain="lemill"
217            )
218        ),
219    ComputedField('tags',
220        index = 'KeywordIndex:schema',
221        expression = 'here.getTags()',
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    StringField('wysiwyg_editor',
243            default='kupu',
244            mode = 'r',
245            widget = StringWidget(
246                visible = {'view':'invisible', 'edit':'invisible'},
247                ),
248            ),
249    ReferenceField('listOfContacts',
250        required = False,
251        searchable = False,
252        relationship = 'is contact of',
253        accessor = 'getListOfContacts',
254        mutator = 'addListOfContacts',
255        multiValued = True,
256        widget = ReferenceWidget(
257            visible = {'view':'invisible', 'edit':'invisible'},
258            ),
259        ),
260
261))
262
263# This lists the fields that also exist in
264# the memberdata tool
265# TODO: this could be automatically generated from the schema and member property list
266DUPLICATED_FIELDS = ('firstname','lastname','email')
267
268schema = schema.copy()
269schema['coverImage'].original_size=(140,120)
270
271class MemberFolder(BaseFolder,CommonMixIn,CoverImageMixIn):
272    """Member folder"""
273
274    meta_type = "MemberFolder"
275    archetype_name = "MemberFolder" 
276    typeDescription="Member folder"
277    typeDescMsgId='description_memberfolder'
278    global_allow = 1
279
280    allowed_content_types = ('CollectionsFolder', 'Topic')
281    default_view = ('member_view')
282    filter_content_types = True
283    security = ClassSecurityInfo()
284    schema = schema
285
286    actions = (
287        {'id':'view',
288         'name':'View',
289         'action':"string:${object_url}/member_view",
290         'permission':(VIEW,),
291        },
292        {'id':'edit',
293         'name':'Edit',
294         'action':"string:${object_url}/personalize_form",
295         'permission':(MODIFY_CONTENT,),
296        },
297    )   
298    aliases = {
299        '(Default)': '',
300        'edit': 'personalize_form',
301        'view':'member_view'
302        }
303
304    def getRSSDescription(self):
305        """ English Description accessor for RSS """
306        return "Folder of " + self.getTitle()       
307
308    def manage_afterAdd(self, item, container):
309        BaseFolder.manage_afterAdd(self, item, container)
310        if not hasattr(item.aq_base, 'left_slots'):
311            self._setProperty('left_slots', ['here/portlet_member/macros/portlet',], 'lines')
312        else:
313            self._updateProperty('left_slots', ['here/portlet_member/macros/portlet',])
314        # Handle case when logged in user is authenticated from an
315        # external source for the first time
316        try:
317            # This is not very nicely done, should use API methods to access what I need
318            uinfo = self.acl_users.source_users.getUserInfo(self.Creator())
319        except KeyError: # Doesn't exist yet - create
320            self.acl_users.source_users.addUser(self.Creator(),self.Creator(),'')
321            self.acl_users.portal_role_manager.assignRolesToPrincipal(('Member',),self.Creator())
322        # If we've done external authentication, then the user isn't really yet
323        # properly authenticated, so we can't create
324        # the collections and resources folders.
325
326
327    def setFullname(self, value):
328        """ had to rename fullname-field, but its set method is still widely used """ 
329        self.setFull_name(value)
330
331    def getMemberId(self):
332        """ return user's id """
333        return self.Creator()
334
335    def getMemberFolder(self):
336        """ return the member, useful for collections, topics etc. subobjects """
337        return self
338
339    def getLatestEditDate(self):
340        """Returns creation date, we don't want to notify about every edit"""
341        return DateTime(self.CreationDate())
342
343    def getCollectionsFolder(self):
344        """..."""
345        if not hasattr(self.aq_base, 'collections'):
346            self.invokeFactory('CollectionsFolder', id='collections')
347        return self.collections
348   
349    def getCollections(self, obj_id=None):
350        """Return list of user's collections."""
351        if obj_id:
352            return self.aq_parent.getCollections(obj_id)
353        return self.getCollectionsFolder().objectValues('Collection')
354
355    security.declareProtected(MODIFY_CONTENT,'delCollection')
356    def delCollection(self, obj_id):
357        obj=self.collections.get(obj_id)
358        if obj.amIOwner():       
359            self.collections._delObject(obj_id)
360            msg=_(u'Collection deleted')
361        else:
362            msg=_(u'You are not allowed to delete this collection')
363        return (self.collections, msg)     
364   
365    def getCountrylist(self):
366        """ We don't need short country codes, so we make a list where fancyname=fancyname and give that as DisplayList """
367        countries_root = [('No country specified', 'No country specified')]
368        countries_list = [(x[1],x[1]) for x in countries.items()]
369        countries_list.sort()
370        countries_root= countries_root + countries_list
371        return DisplayList(tuple(countries_root)) # not sure, but DisplayList might require tuples not lists.
372
373   
374    def getLanguagelist(self):
375        languagelist=self.availableLanguages()
376        return DisplayList(languagelist[1:]) # Cut out the first, 'no language specified'-option
377
378
379    def getPortrait(self, author=None):
380        """ Gets portrait"""
381        if not author:
382            return self.getCoverImage()
383        else:
384            return getattr(self.community,author).getCoverImage()
385
386    def getPortraitURL(self,author=None):
387        """ Gets portrait URL"""
388        return self.getCoverImageURL()
389
390    def NiceName(self):
391        """ First try full_name, then userid """
392        name=self.getFull_name()
393        if name:
394            return name
395        else:
396            return self.Creator()   
397
398    def getNicename(self):
399        return self.NiceName()
400
401    def getSortable_nicename(self):
402        lastname= self.getLastname()
403        if lastname:
404            return lastname.lower()
405        else:
406            return self.Creator().lower()
407
408    def prefill_title(self):
409        mdatatool = getToolByName(self, 'portal_memberdata')
410        value = mdatatool.getMemberId()
411        return value
412
413    def __getMemberProperty(self,key):
414        memberid = self.Creator()
415        mtool = getToolByName(self, 'portal_membership')
416        member = mtool.getMemberById(memberid)
417        if member:
418            return member.getProperty(key)
419        else:
420            return ''
421
422    def prefill_email(self):
423        # When memberfolder is created first time we need to get some values right, as they will be used in templates
424        return self.__getMemberProperty('email')
425       
426    def prefill_firstname(self):
427        return self.__getMemberProperty('firstname')
428
429    def prefill_lastname(self):
430        return self.__getMemberProperty('lastname')
431
432    def prefill_full_name(self):
433        first=str(self.__getMemberProperty('firstname'))
434        last=str(self.__getMemberProperty('lastname'))
435        if first and last:
436            return ' '.join((first,last))
437        else:
438            return '' 
439
440    def getFull_name(self):
441        full= self.getField('full_name').get(self)
442        if full:           
443            return full
444        else:
445            first=self.getField('firstname').get(self)
446            last=self.getField('lastname').get(self)
447            if first and last:
448                self.getField('full_name').set(self, ' '.join((first, last)))
449                return ' '.join((first, last))
450            else:
451                return ''
452
453    def setLastname(self, value):
454        self.getField('lastname').set(self, value)
455        first=self.getField('firstname').get(self)
456        if value:
457            self.getField('full_name').set(self, ' '.join((first, value)))
458       
459    def setFirstname(self, value):
460        self.getField('firstname').set(self, value)
461        last=self.getField('lastname').get(self)
462        if value:
463            self.getField('full_name').set(self, ' '.join((value, last)))
464
465    security.declarePrivate('at_post_edit_script')
466    def at_post_edit_script(self):
467        putil = getToolByName(self,'plone_utils')
468        pmem = getToolByName(self,'portal_membership')
469        user = pmem.getMemberById(self.Creator())
470        for field in DUPLICATED_FIELDS:
471            value = self.getField(field).get(self)
472            if value != user.getProperty(field):
473                user.setMemberProperties({field:value})
474        self.recalculateScore()
475        self.reindexObject()
476
477    def getTags(self):
478        lst = self.getField('interests').get(self) + self.getField('skills').get(self)
479        return list(lst)
480
481    def getResources(self, creator, n=False, filter=None, as_dict=False):
482        """ Get resources from subject of this memberfolder by using general portfolio-topic """
483        public_results = []
484        if filter is None:
485            results=self.portfolio.queryCatalog(Creator=creator)
486        else:
487            results=self.portfolio.queryCatalog(meta_type=filter, Creator=creator)
488        if as_dict:
489            # Make a dictionary with following keys: 'Content','Activities','Tools','Collections'
490            dict={'Content':[],'Activities':[],'Tools':[],'Collections':[],'Pieces':[]}
491            for res in results:
492                if res.review_state=='Deleted':
493                    raise 'Deleted stuff found in portfolio'
494                if res.meta_type in MATERIAL_TYPES:
495                    dict['Content'].append(res)
496                elif res.meta_type=='Activity':
497                    dict['Activities'].append(res)
498                elif res.meta_type=='Tool':
499                    dict['Tools'].append(res)
500                elif res.meta_type=='Collection':
501                    dict['Collections'].append(res)
502                elif res.meta_type=='Piece' or res.meta_type=='LeMillReference':
503                    dict['Pieces'].append(res)
504            if n:
505                for key in dict.keys():
506                    dict[key]=len(dict[key])
507            return dict                   
508        if n:
509            return len(results)
510        return results
511
512
513    def getGroups(self, objects=False):
514        """ Gets groups where member belongs"""
515        pc=getToolByName(self, 'portal_catalog')
516        objlist=pc({'getGroupMembers':self.getMemberId(), 'portal_type':'GroupBlog'})
517        if objects:
518            return [o.getObject() for o in objlist]
519        else:
520            return objlist
521
522
523    def getSamples(self):
524        """ get n number of samples """
525        my_id=self.getMemberId()
526        all_contents = self.portfolio.queryCatalog({'review_state':'public', 'meta_type': self.getFeaturedTypes(), 'getHasCoverImage':True, 'Creator': my_id})
527 
528        if len(all_contents) >= 3:
529            return random.sample(all_contents,3)
530        else:
531            all_pieces = self.portfolio.queryCatalog({'review_state':'public', 'meta_type': ('Piece',), 'getHasCoverImage':True, 'Creator': my_id})
532            return list(all_contents) + random.sample(all_pieces,min(len(all_pieces), 3-len(all_contents)))
533       
534
535    def getDefaultIcon(self, meta_type='', obj=None):
536        """ general method for getting proper icon for object, used when only catalog-metadata is available """
537        # this combines folderish getDefaultIcon(for-this-type, object) and resource-specific object.getDefaultIcon()
538        # folderish behaviour is needed because members have these created resources-pages. 
539        if meta_type=='':
540            return  DEFAULT_ICONS['MemberFolder']
541        else:     
542            address=DEFAULT_ICONS[meta_type]
543            if address!='piece':
544                return address
545            else:
546                obj=obj.getObject()
547                return obj.getDefaultIcon()
548   
549    def verifyMessengers(self, array):
550        """ check if there are some instant messengers to show
551            takes an array and if there is content then returns True
552        """
553        for x in array:
554            if x: return True
555        return False
556
557
558    def sendInvitationMail(self, message, email):
559        putils=getToolByName(self,'plone_utils')
560        mhost=putils.getMailHost()
561        utool=getToolByName(self,'portal_url')
562        msg="""
563        From: %s <%s>
564        To: %s
565        Subject: Invitation to group in %s
566
567        %s
568
569        """ % (utool.email_from_name, utool.email_from_address, email, utool.Title(), message)
570        message_from = "%s <%s>" % (utool.email_from_name, utool.email_from_address)
571        message_subject = "Invitation to group in %s" % (utool.Title())
572        try:
573            mhost.send(message, mto=email, mfrom=message_from, subject=message_subject)
574        except:
575            print 'Something is wrong with outgoing email. Sent message was:'
576            print msg 
577
578    security.declareProtected(MODIFY_CONTENT,'addToContacts')
579    def addToContacts(self,uid=None):
580        """ Add person to contacts """
581        field=self.getField('listOfContacts')
582        uidlist=field.getRaw(self)
583        if uid not in uidlist:
584            uidlist.append(uid)
585            field.set(self, uidlist)
586       
587    security.declareProtected(MODIFY_CONTENT,'removeFromContacts')
588    def removeFromContacts(self,uid=None):
589        """ Remove person from contacts """
590        field=self.getField('listOfContacts')
591        uidlist=field.getRaw(self)       
592        if uid in uidlist:
593            uidlist.remove(uid)
594            field.set(self, uidlist)
595
596    def showRemoveContactLink(self):
597        """ Determines if we show the remove link for user this is still done in reverse """
598        mtool = getToolByName(self, 'portal_membership')
599        home = mtool.getHomeFolder()       
600        field = home.getField('listOfContacts')
601        return self.UID() in field.getRaw(home)
602
603    def giveSortedListOfContacts(self):
604        """ Sorts list of contacts by sortable nicename """
605        contacts = self.getListOfContacts()
606        contacts = [(x.getSortable_nicename(),x) for x in contacts]
607        contacts.sort()
608        return [obj for (snn,obj) in contacts]
609
610    def amIOwner(self):
611        """ check owner of member folder """
612        roles = self.portal_membership.getAuthenticatedMember().getRolesInContext(self)
613        return 'Owner' in roles
614
615    def getRelatedContacts(self):
616        """ Returns all related contacts """
617        obj_uid = self.UID()
618        res = []
619        q = { 'targetUID': obj_uid }
620        qres = self.reference_catalog(q)
621        for q in qres:
622            v = self.reference_catalog.lookupObject(q.UID)
623            source = v.getSourceObject()
624            if source.meta_type == 'MemberFolder':
625                res.append(source)
626        return res
627
628    def recalculateScore(self):
629        """ Recalculates score for MemberFolder according to specifications """
630        score = 0
631        member = self.getMemberId()
632        all_resources = self.getResources(member, as_dict=True)
633        # Get pieces and filter out References
634        pieces = all_resources['Pieces']
635        pieces = [x for x in pieces if x.meta_type=='Piece']
636        # Get Material types, Methods and Tools
637        main_resources = all_resources['Content'] + all_resources['Activities'] + all_resources['Tools']
638        # Get collections and see which ones are complete
639        collections = self.getCollections()
640        stories = [c for c in collections if c.getGoodStory()]
641        # Get all posts by member
642        posts = self.queryCatalog({'review_state':'public', 'meta_type':('BlogPost',), 'Creator':member})
643        # Get people that have this member as contact
644        contacts = self.getRelatedContacts()
645
646        score = score + len(pieces) # 1 point for each piece
647        score = score + len(main_resources) * 10 # 10 points for each learning resource, method and tool
648        score = score + len(stories) * 10 # 10 points for each storie
649        score = score + len(posts) # 1 point for each post
650        score = score + len(contacts) # 1 point for each contact
651
652        # Make sure that score is at least 1
653        if score<1:
654            score = 1
655        self.setScore(score)
656
657       
658registerType(MemberFolder, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.