source: trunk/MemberFolder.py @ 1930

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