source: trunk/MemberFolder.py @ 1933

Revision 1933, 24.3 KB checked in by pjotr, 12 years ago (diff)

References #1482, these fields should be searchable now and it wil get work on lemill.net as soon as updated. I'll close the ticket as soon as confirmed.

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        searchable = True,
66        required = True,
67        widget = StringWidget(
68             label = 'First name',
69             label_msgid = 'label_firstname',
70             i18n_domain = "lemill"
71             )
72        ),
73    TextField('lastname',
74        index = 'FieldIndex:schema',
75        searchable = True,
76        required = True,
77        default_method = 'prefill_lastname',
78        widget = StringWidget(
79             label = 'Last name',
80             label_msgid = 'label_lastname',
81             i18n_domain = "lemill"
82             )
83        ),
84    TextField('full_name',
85        index = 'FieldIndex:schema',
86        default_method = 'prefill_full_name',
87        widget = StringWidget(
88             visible = {'view':'invisible', 'edit':'invisible'},
89             )
90        ),
91    ComputedField('nicename', # so that displayed name can be pulled from catalog metadata
92        index= 'FieldIndex:schema',
93        expression = 'here.getNicename()',
94        default_method = 'prefill_full_name',
95        ),
96    ComputedField('sortable_nicename', # so that displayed names can be ordered alphabetically
97        index= 'FieldIndex:schema',
98        expression = 'here.getSortable_nicename',
99        default_method = 'prefill_lastname',
100        ),
101       
102    TextField('email',
103        searchable = True,
104        required = 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    LinesField('subject_area',
205        vocabulary = DisplayList([(x,x) for x in SUBJECT_AREAS]),
206        index = "KeywordIndex:schema",
207        multivalued=True,
208        searchable=True,
209        widget=TwoColumnMultiSelectionWidget(
210            format="checkbox",
211            label="Subject area",
212            label_msgid="label_member_subject_area",
213            description="Choose subject areas that you are teaching.",
214            description_msgid="description_member_subject_area",
215            i18n_domain="lemill"
216            )
217        ),
218    ComputedField('tags',
219        index = 'KeywordIndex:schema',
220        expression = 'here.getTags()',
221        ),
222
223    TextField('biography',
224        searchable = True,
225        widget = LeTextAreaWidget(
226            label = 'Biography',
227            label_msgid = 'label_your_biography',
228            description = "Any background information about yourself that you wish to share with others.",
229            description_msgid = 'help_your_biography',
230            i18n_domain = "lemill"
231            )
232        ),
233
234    LinesField('used_content',
235        mode = 'r',
236        index = 'KeywordIndex:schema',
237        widget = LinesWidget(
238                 visible = {'view':'invisible', 'edit':'invisible'},
239             )
240        ),
241    StringField('wysiwyg_editor',
242            default='kupu',
243            mode = 'r',
244            widget = StringWidget(
245                visible = {'view':'invisible', 'edit':'invisible'},
246                ),
247            ),
248    ReferenceField('listOfContacts',
249        required = False,
250        searchable = False,
251        relationship = 'is contact of',
252        accessor = 'getListOfContacts',
253        mutator = 'addListOfContacts',
254        multiValued = True,
255        widget = ReferenceWidget(
256            visible = {'view':'invisible', 'edit':'invisible'},
257            ),
258        ),
259
260))
261
262# This lists the fields that also exist in
263# the memberdata tool
264# TODO: this could be automatically generated from the schema and member property list
265DUPLICATED_FIELDS = ('firstname','lastname','email')
266
267schema = schema.copy()
268schema['coverImage'].original_size=(140,120)
269
270class MemberFolder(BaseFolder,CommonMixIn,CoverImageMixIn):
271    """Member folder"""
272
273    meta_type = "MemberFolder"
274    archetype_name = "MemberFolder" 
275    typeDescription="Member folder"
276    typeDescMsgId='description_memberfolder'
277    global_allow = 1
278
279    allowed_content_types = ('CollectionsFolder', 'Topic')
280    default_view = ('member_view')
281    filter_content_types = True
282    security = ClassSecurityInfo()
283    schema = schema
284
285    actions = (
286        {'id':'view',
287         'name':'View',
288         'action':"string:${object_url}/member_view",
289         'permission':(VIEW,),
290        },
291        {'id':'edit',
292         'name':'Edit',
293         'action':"string:${object_url}/personalize_form",
294         'permission':(MODIFY_CONTENT,),
295        },
296    )   
297    aliases = {
298        '(Default)': '',
299        'edit': 'personalize_form',
300        'view':'member_view'
301        }
302
303    def getRSSDescription(self):
304        """ English Description accessor for RSS """
305        return "Folder of " + self.getTitle()       
306
307    def manage_afterAdd(self, item, container):
308        BaseFolder.manage_afterAdd(self, item, container)
309        if not hasattr(item.aq_base, 'left_slots'):
310            self._setProperty('left_slots', ['here/portlet_member/macros/portlet',], 'lines')
311        else:
312            self._updateProperty('left_slots', ['here/portlet_member/macros/portlet',])
313        # Handle case when logged in user is authenticated from an
314        # external source for the first time
315        try:
316            # This is not very nicely done, should use API methods to access what I need
317            uinfo = self.acl_users.source_users.getUserInfo(self.Creator())
318        except KeyError: # Doesn't exist yet - create
319            self.acl_users.source_users.addUser(self.Creator(),self.Creator(),'')
320            self.acl_users.portal_role_manager.assignRolesToPrincipal(('Member',),self.Creator())
321        # If we've done external authentication, then the user isn't really yet
322        # properly authenticated, so we can't create
323        # the collections and resources folders.
324
325
326    def setFullname(self, value):
327        """ had to rename fullname-field, but its set method is still widely used """ 
328        self.setFull_name(value)
329
330    def getMemberId(self):
331        """ return user's id """
332        return self.Creator()
333
334    def getMemberFolder(self):
335        """ return the member, useful for collections, topics etc. subobjects """
336        return self
337
338    def getLatestEditDate(self):
339        """Returns creation date, we don't want to notify about every edit"""
340        return DateTime(self.CreationDate())
341
342    def getCollectionsFolder(self):
343        """..."""
344        if not hasattr(self.aq_base, 'collections'):
345            self.invokeFactory('CollectionsFolder', id='collections')
346        return self.collections
347   
348    def getCollections(self, obj_id=None):
349        """Return list of user's collections."""
350        if obj_id:
351            return self.aq_parent.getCollections(obj_id)
352        return self.getCollectionsFolder().objectValues('Collection')
353
354    security.declareProtected(MODIFY_CONTENT,'delCollection')
355    def delCollection(self, obj_id):
356        obj=self.collections.get(obj_id)
357        if obj.amIOwner():       
358            self.collections._delObject(obj_id)
359            msg=_(u'Collection deleted')
360        else:
361            msg=_(u'You are not allowed to delete this collection')
362        return (self.collections, msg)     
363   
364    def getCountrylist(self):
365        """ We don't need short country codes, so we make a list where fancyname=fancyname and give that as DisplayList """
366        countries_root = [('No country specified', 'No country specified')]
367        countries_list = [(x[1],x[1]) for x in countries.items()]
368        countries_list.sort()
369        countries_root= countries_root + countries_list
370        return DisplayList(tuple(countries_root)) # not sure, but DisplayList might require tuples not lists.
371
372   
373    def getLanguagelist(self):
374        languagelist=self.availableLanguages()
375        return DisplayList(languagelist[1:]) # Cut out the first, 'no language specified'-option
376
377
378    def getPortrait(self, author=None):
379        """ Gets portrait"""
380        if not author:
381            return self.getCoverImage()
382        else:
383            return getattr(self.community,author).getCoverImage()
384
385    def getPortraitURL(self,author=None):
386        """ Gets portrait URL"""
387        return self.getCoverImageURL()
388
389    def NiceName(self):
390        """ First try full_name, then userid """
391        name=self.getFull_name()
392        if name:
393            return name
394        else:
395            return self.Creator()   
396
397    def getNicename(self):
398        return self.NiceName()
399
400    def getSortable_nicename(self):
401        lastname= self.getLastname()
402        if lastname:
403            return lastname.lower()
404        else:
405            return self.Creator().lower()
406
407    def prefill_title(self):
408        mdatatool = getToolByName(self, 'portal_memberdata')
409        value = mdatatool.getMemberId()
410        return value
411
412    def __getMemberProperty(self,key):
413        memberid = self.Creator()
414        mtool = getToolByName(self, 'portal_membership')
415        member = mtool.getMemberById(memberid)
416        if member:
417            return member.getProperty(key)
418        else:
419            return ''
420
421    def prefill_email(self):
422        # When memberfolder is created first time we need to get some values right, as they will be used in templates
423        return self.__getMemberProperty('email')
424       
425    def prefill_firstname(self):
426        return self.__getMemberProperty('firstname')
427
428    def prefill_lastname(self):
429        return self.__getMemberProperty('lastname')
430
431    def prefill_full_name(self):
432        first=str(self.__getMemberProperty('firstname'))
433        last=str(self.__getMemberProperty('lastname'))
434        if first and last:
435            return ' '.join((first,last))
436        else:
437            return '' 
438
439    def getFull_name(self):
440        full= self.getField('full_name').get(self)
441        if full:           
442            return full
443        else:
444            first=self.getField('firstname').get(self)
445            last=self.getField('lastname').get(self)
446            if first and last:
447                self.getField('full_name').set(self, ' '.join((first, last)))
448                return ' '.join((first, last))
449            else:
450                return ''
451
452    def setLastname(self, value):
453        self.getField('lastname').set(self, value)
454        first=self.getField('firstname').get(self)
455        if value:
456            self.getField('full_name').set(self, ' '.join((first, value)))
457       
458    def setFirstname(self, value):
459        self.getField('firstname').set(self, value)
460        last=self.getField('lastname').get(self)
461        if value:
462            self.getField('full_name').set(self, ' '.join((value, last)))
463
464    security.declarePrivate('at_post_edit_script')
465    def at_post_edit_script(self):
466        putil = getToolByName(self,'plone_utils')
467        pmem = getToolByName(self,'portal_membership')
468        user = pmem.getMemberById(self.Creator())
469        for field in DUPLICATED_FIELDS:
470            value = self.getField(field).get(self)
471            if value != user.getProperty(field):
472                user.setMemberProperties({field:value})
473        self.recalculateScore()
474        self.reindexObject()
475
476    def getTags(self):
477        lst = self.getField('interests').get(self) + self.getField('skills').get(self)
478        return list(lst)
479
480    def getResources(self, creator, n=False, filter=None, as_dict=False):
481        """ Get resources from subject of this memberfolder by using general portfolio-topic """
482        public_results = []
483        if filter is None:
484            results=self.portfolio.queryCatalog(Creator=creator)
485        else:
486            results=self.portfolio.queryCatalog(meta_type=filter, Creator=creator)
487        if as_dict:
488            # Make a dictionary with following keys: 'Content','Activities','Tools','Collections'
489            dict={'Content':[],'Activities':[],'Tools':[],'Collections':[],'Pieces':[]}
490            for res in results:
491                if res.review_state=='Deleted':
492                    raise 'Deleted stuff found in portfolio'
493                if res.meta_type in MATERIAL_TYPES:
494                    dict['Content'].append(res)
495                elif res.meta_type=='Activity':
496                    dict['Activities'].append(res)
497                elif res.meta_type=='Tool':
498                    dict['Tools'].append(res)
499                elif res.meta_type=='Collection':
500                    dict['Collections'].append(res)
501                elif res.meta_type=='Piece' or res.meta_type=='LeMillReference':
502                    dict['Pieces'].append(res)
503            if n:
504                for key in dict.keys():
505                    dict[key]=len(dict[key])
506            return dict                   
507        if n:
508            return len(results)
509        return results
510
511
512    def getGroups(self, objects=False):
513        """ Gets groups where member belongs"""
514        pc=getToolByName(self, 'portal_catalog')
515        objlist=pc({'getGroupMembers':self.getMemberId(), 'portal_type':'GroupBlog'})
516        if objects:
517            return [o.getObject() for o in objlist]
518        else:
519            return objlist
520
521
522    def getSamples(self):
523        """ get n number of samples """
524        my_id=self.getMemberId()
525        all_contents = self.portfolio.queryCatalog({'review_state':'public', 'meta_type': self.getFeaturedTypes(), 'getHasCoverImage':True, 'Creator': my_id})
526 
527        if len(all_contents) >= 3:
528            return random.sample(all_contents,3)
529        else:
530            all_pieces = self.portfolio.queryCatalog({'review_state':'public', 'meta_type': ('Piece',), 'getHasCoverImage':True, 'Creator': my_id})
531            return list(all_contents) + random.sample(all_pieces,min(len(all_pieces), 3-len(all_contents)))
532       
533
534    def getDefaultIcon(self, meta_type='', obj=None):
535        """ general method for getting proper icon for object, used when only catalog-metadata is available """
536        # this combines folderish getDefaultIcon(for-this-type, object) and resource-specific object.getDefaultIcon()
537        # folderish behaviour is needed because members have these created resources-pages. 
538        if meta_type=='':
539            return  DEFAULT_ICONS['MemberFolder']
540        else:     
541            address=DEFAULT_ICONS[meta_type]
542            if address!='piece':
543                return address
544            else:
545                obj=obj.getObject()
546                return obj.getDefaultIcon()
547   
548    def verifyMessengers(self, array):
549        """ check if there are some instant messengers to show
550            takes an array and if there is content then returns True
551        """
552        for x in array:
553            if x: return True
554        return False
555
556
557    def sendInvitationMail(self, message, email):
558        putils=getToolByName(self,'plone_utils')
559        mhost=putils.getMailHost()
560        utool=getToolByName(self,'portal_url')
561        msg="""
562        From: %s <%s>
563        To: %s
564        Subject: Invitation to group in %s
565
566        %s
567
568        """ % (utool.email_from_name, utool.email_from_address, email, utool.Title(), message)
569        message_from = "%s <%s>" % (utool.email_from_name, utool.email_from_address)
570        message_subject = "Invitation to group in %s" % (utool.Title())
571        try:
572            mhost.send(message, mto=email, mfrom=message_from, subject=message_subject)
573        except:
574            print 'Something is wrong with outgoing email. Sent message was:'
575            print msg 
576
577    security.declareProtected(MODIFY_CONTENT,'addToContacts')
578    def addToContacts(self,uid=None):
579        """ Add person to contacts """
580        field=self.getField('listOfContacts')
581        uidlist=field.getRaw(self)
582        if uid not in uidlist:
583            uidlist.append(uid)
584            field.set(self, uidlist)
585       
586    security.declareProtected(MODIFY_CONTENT,'removeFromContacts')
587    def removeFromContacts(self,uid=None):
588        """ Remove person from contacts """
589        field=self.getField('listOfContacts')
590        uidlist=field.getRaw(self)       
591        if uid in uidlist:
592            uidlist.remove(uid)
593            field.set(self, uidlist)
594
595    def showRemoveContactLink(self):
596        """ Determines if we show the remove link for user this is still done in reverse """
597        mtool = getToolByName(self, 'portal_membership')
598        home = mtool.getHomeFolder()       
599        field = home.getField('listOfContacts')
600        return self.UID() in field.getRaw(home)
601
602    def giveSortedListOfContacts(self):
603        """ Sorts list of contacts by sortable nicename """
604        contacts = self.getListOfContacts()
605        contacts = [(x.getSortable_nicename(),x) for x in contacts]
606        contacts.sort()
607        return [obj for (snn,obj) in contacts]
608
609    def amIOwner(self):
610        """ check owner of member folder """
611        roles = self.portal_membership.getAuthenticatedMember().getRolesInContext(self)
612        return 'Owner' in roles
613
614    def getRelatedContacts(self):
615        """ Returns all related contacts """
616        obj_uid = self.UID()
617        res = []
618        q = { 'targetUID': obj_uid }
619        qres = self.reference_catalog(q)
620        for q in qres:
621            v = self.reference_catalog.lookupObject(q.UID)
622            source = v.getSourceObject()
623            if source.meta_type == 'MemberFolder':
624                res.append(source)
625        return res
626
627    def recalculateScore(self):
628        """ Recalculates score for MemberFolder according to specifications """
629        score = 0
630        member = self.getMemberId()
631        all_resources = self.getResources(member, as_dict=True)
632        # Get pieces and filter out References
633        pieces = all_resources['Pieces']
634        pieces = [x for x in pieces if x.meta_type=='Piece']
635        # Get Material types, Methods and Tools
636        main_resources = all_resources['Content'] + all_resources['Activities'] + all_resources['Tools']
637        # Get collections and see which ones are complete
638        collections = self.getCollections()
639        stories = [c for c in collections if c.getGoodStory()]
640        # Get all posts by member
641        posts = self.queryCatalog({'review_state':'public', 'meta_type':('BlogPost',), 'Creator':member})
642        # Get people that have this member as contact
643        contacts = self.getRelatedContacts()
644
645        score = score + len(pieces) # 1 point for each piece
646        score = score + len(main_resources) * 10 # 10 points for each learning resource, method and tool
647        score = score + len(stories) * 10 # 10 points for each storie
648        score = score + len(posts) # 1 point for each post
649        score = score + len(contacts) # 1 point for each contact
650
651        # Make sure that score is at least 1
652        if score<1:
653            score = 1
654        self.setScore(score)
655
656       
657registerType(MemberFolder, PROJECTNAME)
Note: See TracBrowser for help on using the repository browser.