source: adhocracy/model/user.py @ 890:01626215f2ac

Revision 890:01626215f2ac, 13.2 KB checked in by fl, 3 years ago (diff)

[svn r1236] performance tuning

Line 
1import hashlib
2import os
3import logging
4from datetime import datetime
5
6from sqlalchemy import Table, Column, Integer, Unicode, UnicodeText, Boolean, DateTime, func, or_
7from sqlalchemy.orm import eagerload_all
8
9from babel import Locale
10
11import meta
12import instance_filter as ifilter
13import group
14
15log = logging.getLogger(__name__)
16
17
18user_table = Table('user', meta.data,
19    Column('id', Integer, primary_key=True),
20    Column('user_name', Unicode(255), nullable=False, unique=True, index=True),
21    Column('display_name', Unicode(255), nullable=True, index=True),
22    Column('bio', UnicodeText(), nullable=True),
23    Column('email', Unicode(255), nullable=True, unique=False),
24    Column('email_priority', Integer, default=3),
25    Column('activation_code', Unicode(255), nullable=True, unique=False),
26    Column('reset_code', Unicode(255), nullable=True, unique=False),
27    Column('password', Unicode(80), nullable=False),
28    Column('locale', Unicode(7), nullable=True),
29    Column('create_time', DateTime, default=datetime.utcnow),
30    Column('access_time', DateTime, default=datetime.utcnow, onupdate=datetime.utcnow),
31    Column('delete_time', DateTime),
32    Column('no_help', Boolean, default=False, nullable=True),
33    Column('page_size', Integer, default=10, nullable=True)
34    )
35
36class User(object):
37   
38    def __init__(self, user_name, email, password, locale, display_name=None, bio=None):
39        self.user_name = user_name
40        self.email = email
41        self.password = password
42        self.locale = locale
43        self.display_name = display_name
44        self.bio = bio
45       
46   
47    @property   
48    def name(self):
49        return self.display_name.strip() \
50            if self.display_name and len(self.display_name.strip()) > 0 \
51            else self.user_name
52   
53   
54    def _get_locale(self):
55        if not self._locale:
56            return None
57        return Locale.parse(self._locale)
58   
59    def _set_locale(self, locale):
60        self._locale = unicode(locale)
61       
62    locale = property(_get_locale, _set_locale)
63       
64    def _get_email(self):
65        return self._email
66   
67    def _set_email(self, email):
68        import adhocracy.lib.util as util
69        if not self._email == email:
70            self.activation_code = util.random_token()
71        self._email = email
72       
73    email = property(_get_email, _set_email)
74   
75   
76    @property
77    def email_hash(self):
78        return hashlib.sha1(self.email).hexdigest()
79   
80    @property
81    def groups(self):
82        groups = []
83        for membership in self.memberships:
84            if membership.is_expired():
85                continue
86            if not membership.instance or \
87                membership.instance == ifilter.get_instance():
88                groups.append(membership.group)
89        return groups
90   
91   
92    def _has_permission(self, permission_name):
93        for group in self.groups:
94            for perm in group.permissions:
95                if perm.permission_name == permission_name:
96                    return True
97        return False
98   
99   
100    def instance_membership(self, instance):
101        if not instance:
102            return None
103        for membership in self.memberships:
104            if (not membership.is_expired()) and \
105                membership.instance_id == instance.id:
106                return membership
107        return None
108   
109   
110    def is_member(self, instance):
111        return self.instance_membership(instance) is not None
112   
113   
114    @property
115    def instances(self):
116        instances = []
117        for membership in self.memberships:
118            if (not membership.is_expired()) and \
119                (membership.instance is not None):
120                instances.append(membership.instance)
121        return list(set(instances))
122   
123   
124    @property
125    def twitter(self):
126        for twitter in self.twitters:
127            if not twitter.is_deleted():
128                return twitter
129        return None
130   
131   
132    @property
133    def num_watches(self):
134        from watch import Watch
135        q = meta.Session.query(Watch)
136        q = q.filter(Watch.user==self)
137        q = q.filter(or_(Watch.delete_time==None,
138                         Watch.delete_time>=datetime.utcnow()))
139        return q.count()
140   
141   
142    def _set_password(self, password):
143        """Hash password on the fly."""
144        if isinstance(password, unicode):
145            password_8bit = password.encode('ascii', 'ignore')
146        else:
147            password_8bit = password
148
149        salt = hashlib.sha1(os.urandom(60))
150        hash = hashlib.sha1(password_8bit + salt.hexdigest())
151        hashed_password = salt.hexdigest() + hash.hexdigest()
152       
153        if not isinstance(hashed_password, unicode):
154            hashed_password = hashed_password.decode('utf-8')
155        self._password = hashed_password
156   
157    def _get_password(self):
158        """Return the password hashed"""
159        return self._password
160   
161    def validate_password(self, password):
162        """
163        Check the password against existing credentials.
164       
165        :param password: the password that was provided by the user to
166            try and authenticate. This is the clear text version that we will
167            need to match against the hashed one in the database.
168        :type password: unicode object.
169        :return: Whether the password is valid.
170        :rtype: bool
171        """
172        if isinstance(password, unicode):
173            password_8bit = password.encode('ascii', 'ignore')
174        else:
175            password_8bit = password
176        hashed_pass = hashlib.sha1(password_8bit + self.password[:40])
177        return self.password[40:] == hashed_pass.hexdigest()
178   
179    password = property(_get_password, _set_password)
180   
181    def current_agencies(self, instance_filter=True):
182        ds = filter(lambda d: not d.is_revoked(), self.agencies)
183        if ifilter.has_instance() and instance_filter:
184            ds = filter(lambda d: d.scope.instance == ifilter.get_instance(), ds)
185        return ds
186   
187    def current_delegated(self, instance_filter=True):
188        ds = filter(lambda d: not d.is_revoked(), self.delegated)
189        if ifilter.has_instance() and instance_filter:
190            ds = filter(lambda d: d.scope.instance == ifilter.get_instance(), ds)
191        return ds
192   
193    @classmethod
194    def complete(cls, prefix, limit=5, instance_filter=True):
195        q = meta.Session.query(User)
196        prefix = prefix.lower()
197        q = q.filter(or_(func.lower(User.user_name).like(prefix + u"%"),
198                         func.lower(User.display_name).like(prefix + u"%")))
199        q = q.limit(limit)
200        completions = q.all()
201        if ifilter.has_instance() and instance_filter:
202            inst = ifilter.get_instance()
203            completions = filter(lambda u: u.is_member(inst), completions)
204        return completions
205   
206   
207   
208    @classmethod
209    #@meta.session_cached
210    def find(cls, user_name, instance_filter=True, include_deleted=False):
211        from membership import Membership
212        try:
213            q = meta.Session.query(User)
214            try:
215                q = q.filter(User.id==int(user_name))
216            except ValueError:
217                q = q.filter(User.user_name==unicode(user_name))
218            if not include_deleted:
219                q = q.filter(or_(User.delete_time==None,
220                                 User.delete_time>datetime.utcnow()))
221            if ifilter.has_instance() and instance_filter:
222                q = q.join(Membership)
223                q = q.filter(or_(Membership.expire_time==None,
224                                 Membership.expire_time>datetime.utcnow()))
225                q = q.filter(Membership.instance==ifilter.get_instance())
226            return q.limit(1).first()
227        except Exception, e:
228            log.warn("find(%s): %s" % (user_name, e))
229            return None
230   
231   
232    @classmethod
233    def find_by_email(cls, email):
234        try:
235            q = meta.Session.query(User)
236            q = q.filter(User.email==unicode(email).lower())
237            return q.limit(1).first()
238        except Exception, e:
239            log.warn("find_by_email(%s): %s" % (email, e))
240            return None
241   
242   
243    @classmethod
244    def find_all(cls, unames, instance_filter=True, include_deleted=False):
245        from membership import Membership
246        q = meta.Session.query(User)
247        q = q.filter(User.user_name.in_(unames))
248        if not include_deleted:
249            q = q.filter(or_(User.delete_time==None,
250                             User.delete_time>datetime.utcnow()))
251        if ifilter.has_instance() and instance_filter:
252            q = q.join(Membership)
253            q = q.filter(or_(Membership.expire_time==None,
254                             Membership.expire_time>datetime.utcnow()))
255            q = q.filter(Membership.instance==ifilter.get_instance())
256        #log.debug("QueryAll: %s" % q)
257        #log.debug("LEN: %s" % len(q.all()))
258        return q.all()
259   
260   
261    def _index_id(self):
262        return self.user_name
263   
264   
265    @classmethod
266    def all(cls, instance=None, include_deleted=False):
267        from membership import Membership
268        q = meta.Session.query(User)
269        if not include_deleted:
270            q = q.filter(or_(User.delete_time==None,
271                             User.delete_time>datetime.utcnow()))
272        if instance:
273            q = q.options(eagerload_all('memberships'))
274            q = q.join(Membership)
275            q = q.filter(or_(Membership.expire_time==None,
276                             Membership.expire_time>datetime.utcnow()))
277            q = q.filter(Membership.instance==instance)
278        return q.all()
279   
280   
281    def is_deleted(self, at_time=None):
282        if at_time is None:
283            at_time = datetime.utcnow()
284        return (self.delete_time is not None) and \
285               self.delete_time<=at_time
286   
287   
288    def revoke_delegations(self, instance=None):
289        from delegation import Delegation
290        q = meta.Session.query(Delegation)
291        q = q.filter(or_(Delegation.agent==self,
292                         Delegation.principal==self))
293        q = q.filter(or_(Delegation.revoke_time == None,
294                         Delegation.revoke_time > datetime.utcnow()))
295        for delegation in q:
296            if instance is None or delegation.scope.instance == instance:
297                delegation.revoke()
298       
299               
300    def is_email_activated(self):
301        return self.email is not None and self.activation_code is None
302   
303   
304    def delegation_node(self, scope):
305        from adhocracy.lib.democracy import DelegationNode
306        return DelegationNode(self, scope)
307   
308   
309    def number_of_votes_in_scope(self, scope):
310        """
311        May be a bit too much as multiple delegations are counted for each user
312        they are delegated to. (This is the safety net delegation)
313        """
314        if not self._has_permission('vote.cast'):
315            return 0
316        return self.delegation_node(scope).number_of_delegations() + 1
317       
318   
319    def position_on_poll(self, poll):
320        from adhocracy.lib.democracy.decision import Decision
321        return Decision(self, poll).result
322       
323       
324    def any_position_on_proposal(self, proposal):
325        # this is fuzzy since it includes two types of opinions
326        from adhocracy.lib.democracy.decision import Decision
327        if proposal.adopt_poll:
328            dec = Decision(self, proposal.adopt_poll)
329            if dec.is_decided():
330                return dec.result
331        if proposal.rate_poll:
332            return Decision(self, proposal.rate_poll).result
333   
334   
335    @classmethod
336    def create(cls, user_name, email, password=None, locale=None,
337               openid_identity=None, global_admin=False):
338        from group import Group
339        from membership import Membership
340        from openid import OpenID
341       
342        import adhocracy.lib.util as util
343        if password is None:
344            password = util.random_token()
345       
346        import adhocracy.i18n as i18n
347        if locale is None:
348            locale = i18n.get_default_locale()
349       
350        user = User(user_name, email, password, locale)
351        meta.Session.add(user)
352        default_group = Group.by_code(Group.CODE_DEFAULT)
353        default_membership = Membership(user, None, default_group)
354        meta.Session.add(default_membership)
355       
356        if global_admin:
357            admin_group = Group.by_code(Group.CODE_ADMIN)
358            admin_membership = Membership(user, None, admin_group)
359            meta.Session.add(admin_membership)
360       
361        if openid_identity is not None:
362            openid = OpenID(unicode(openid_identity), user)
363            meta.Session.add(openid)
364       
365        meta.Session.flush()
366        return user
367   
368   
369    def to_dict(self):
370        from adhocracy.lib import helpers as h
371        d = dict(id=self.id,
372                 user_name=self.user_name,
373                 locale=self._locale,
374                 url=h.entity_url(self),
375                 create_time=self.create_time,
376                 mbox=self.email_hash)
377        if self.display_name:
378            d['display_name'] = self.display_name
379        if self.bio:
380            d['bio'] = self.bio
381        #d['memberships'] = map(lambda m: m.instance.key,
382        #                       self.memberships)
383        return d
384   
385   
386    def __repr__(self):
387        return u"<User(%s,%s)>" % (self.id, self.user_name)
388   
389
Note: See TracBrowser for help on using the repository browser.