Added a per-user stash.
[zzz-spline-users.git] / splinext / users / model / __init__.py
1 # encoding: utf8
2 import colorsys
3 import json
4 from math import sin, pi
5 import random
6
7 from sqlalchemy import Column, ForeignKey, or_
8 from sqlalchemy.ext.associationproxy import association_proxy
9 from sqlalchemy.orm import relation
10 from sqlalchemy.orm.session import Session
11 from sqlalchemy.types import Integer, PickleType, Unicode
12
13 from spline.model.meta import TableBase
14
15 class AnonymousUser(object):
16 """Fake user object, for when the user isn't actually logged in.
17
18 Tests as false and tries to respond to method calls the expected way.
19 """
20 stash = {}
21
22 def __nonzero__(self):
23 return False
24 def __bool__(self):
25 return False
26
27 def can(self, action):
28 """Anonymous users aren't ever allowed to do anything."""
29 return False
30
31
32 class User(TableBase):
33 __tablename__ = 'users'
34 id = Column(Integer, primary_key=True)
35 name = Column(Unicode(length=20), nullable=False)
36 unique_identifier = Column(Unicode(length=32), nullable=False)
37 stash = Column(PickleType(pickler=json), nullable=False, default=dict())
38
39 def __init__(self, *args, **kwargs):
40 # Generate a unique hash if one isn't provided (which it shouldn't be)
41 if 'unique_identifier' not in kwargs:
42 ident = u''.join(random.choice(u'0123456789abcdef')
43 for _ in range(32))
44 kwargs['unique_identifier'] = ident
45
46 super(User, self).__init__(*args, **kwargs)
47
48 _root_user_id = None
49 _default_permissions = None
50 def can(self, action):
51 """Returns True iff this user has permission to do `action`.
52
53 If `_root_user_id` is this user's id, all permissions are allowed. The
54 property is usually set by the spline-users after_setup hook.
55 """
56
57 if self.id == self._root_user_id:
58 return True
59
60 if action in self.permissions:
61 return True
62
63 # Permissions assigned to NULL apply to all roles
64 if self._default_permissions is None:
65 session = Session.object_session(self)
66 self._default_permissions = [
67 row.permission
68 for row in session.query(RolePermission)
69 .filter_by(role_id=None)
70 ]
71
72 return (action in self._default_permissions)
73
74 @property
75 def unique_colors(self):
76 """Returns a list of (width, '#rrggbb') tuples that semi-uniquely
77 identify this user.
78 """
79 width_blob, colors_blob = self.unique_identifier[0:8], \
80 self.unique_identifier[8:32]
81
82 widths = []
83 for i in range(4):
84 width_hex = width_blob[i*2:i*2+2]
85 widths.append(int(width_hex, 16))
86 total_width = sum(widths)
87
88 ret = []
89 last_hue = None
90 for i in range(4):
91 raw_hue = int(colors_blob[i*6:i*6+2], 16) / 256.0
92 if last_hue:
93 # Make adjacent hues relatively close together, to avoid green
94 # + purple sorts of clashes.
95 # Minimum distance is 0.1; maximum is 0.35. Leaves half the
96 # spectrum available for any given color.
97 # Change 0.0–0.1 to -0.35–-0.1, 0.1–0.35
98 hue_offset = raw_hue * 0.5 - 0.25
99 if raw_hue < 0:
100 raw_hue -= 0.1
101 else:
102 raw_hue += 0.1
103
104 h = last_hue + raw_hue
105 else:
106 h = raw_hue
107 last_hue = h
108
109 l = int(colors_blob[i*6+2:i*6+4], 16) / 256.0
110 s = int(colors_blob[i*6+4:i*6+6], 16) / 256.0
111
112 # Secondary colors are extremely biased against when picking
113 # randomly from the hue spectrum.
114 # To alleviate this, try to bias hue towards secondary colors.
115 # This adjustment is based purely on experimentation; sin() works
116 # well because hue is periodic, * 6 means each period is 1/3 the
117 # hue spectrum, and the final / 24 is eyeballed
118 h += sin(h * pi * 6) / 24
119
120 # Cap lightness to 0.4 to 0.95, so it's not too close to white or
121 # black
122 l = l * 0.6 + 0.3
123
124 # Cap saturation to 0.5 to 1.0, so the color isn't too gray
125 s = s * 0.6 + 0.3
126
127 r, g, b = colorsys.hls_to_rgb(h, l, s)
128 color = "#{0:02x}{1:02x}{2:02x}".format(
129 int(r * 256),
130 int(g * 256),
131 int(b * 256),
132 )
133
134 ret.append((1.0 * widths[i] / total_width, color))
135
136 return ret
137
138 class OpenID(TableBase):
139 __tablename__ = 'openid'
140 openid = Column(Unicode(length=255), primary_key=True)
141 user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
142
143
144 # Permissions stuff
145 class Role(TableBase):
146 __tablename__ = 'roles'
147 id = Column(Integer, primary_key=True, nullable=False)
148 name = Column(Unicode(64), nullable=False)
149 icon = Column(Unicode(64), nullable=False)
150
151 class UserRole(TableBase):
152 __tablename__ = 'user_roles'
153 user_id = Column(Integer, ForeignKey('users.id'), primary_key=True, nullable=False, autoincrement=False)
154 role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True, nullable=False, autoincrement=False)
155
156 class RolePermission(TableBase):
157 __tablename__ = 'role_permissions'
158 id = Column(Integer, nullable=False, primary_key=True)
159 role_id = Column(Integer, ForeignKey('roles.id'), nullable=True)
160 permission = Column(Unicode(64), nullable=False)
161
162
163 ### Relations
164 OpenID.user = relation(User, lazy=False, backref='openids')
165
166 Role.role_permissions = relation(RolePermission, backref='role')
167
168 User.roles = relation(Role, secondary=UserRole.__table__, backref='users')
169 User.role_permissions = relation(RolePermission,
170 primaryjoin=User.id==UserRole.user_id,
171 secondary=UserRole.__table__,
172 secondaryjoin=UserRole.role_id==RolePermission.role_id,
173 foreign_keys=[UserRole.user_id, RolePermission.role_id],
174 )
175 User.permissions = association_proxy('role_permissions', 'permission')
176
177 UserRole.user = relation(User)
178 UserRole.role = relation(Role)