| 1 | import hashlib |
|---|
| 2 | import os |
|---|
| 3 | import logging |
|---|
| 4 | from datetime import datetime |
|---|
| 5 | |
|---|
| 6 | from sqlalchemy import Table, Column, Integer, Unicode, UnicodeText, Boolean, DateTime, func, or_ |
|---|
| 7 | from sqlalchemy.orm import eagerload_all |
|---|
| 8 | |
|---|
| 9 | from babel import Locale |
|---|
| 10 | |
|---|
| 11 | import meta |
|---|
| 12 | import instance_filter as ifilter |
|---|
| 13 | import group |
|---|
| 14 | |
|---|
| 15 | log = logging.getLogger(__name__) |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | user_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 | |
|---|
| 36 | class 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 | |
|---|