2 # Functions for interacting with the persons table in the database
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
11 from types import StringTypes
12 from datetime import datetime
14 from hashlib import md5
18 from random import Random
22 from PLC.Faults import *
23 from PLC.Debug import log
24 from PLC.Parameter import Parameter, Mixed
25 from PLC.Filter import Filter
26 from PLC.Table import Row, Table
27 from PLC.Roles import Role, Roles
28 from PLC.Keys import Key, Keys
29 from PLC.Messages import Message, Messages
33 Representation of a row in the persons table. To use, optionally
34 instantiate with a dict of values. Update as you would a
35 dict. Commit to the database with sync().
38 table_name = 'persons'
39 primary_key = 'person_id'
40 join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
42 'person_id': Parameter(int, "User identifier"),
43 'first_name': Parameter(str, "Given name", max = 128),
44 'last_name': Parameter(str, "Surname", max = 128),
45 'title': Parameter(str, "Title", max = 128, nullok = True),
46 'email': Parameter(str, "Primary e-mail address", max = 254),
47 'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
48 'url': Parameter(str, "Home page", max = 254, nullok = True),
49 'bio': Parameter(str, "Biography", max = 254, nullok = True),
50 'enabled': Parameter(bool, "Has been enabled"),
51 'password': Parameter(str, "Account password in crypt() form", max = 254),
52 'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
53 'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
54 'last_updated': Parameter(int, "Date and time of last update", ro = True),
55 'date_created': Parameter(int, "Date and time when account was created", ro = True),
56 'role_ids': Parameter([int], "List of role identifiers"),
57 'roles': Parameter([str], "List of roles"),
58 'site_ids': Parameter([int], "List of site identifiers"),
59 'key_ids': Parameter([int], "List of key identifiers"),
60 'slice_ids': Parameter([int], "List of slice identifiers"),
61 'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
62 'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
63 'person_tag_ids' : Parameter ([int], "List of tags attached to this person"),
66 'roles': [Mixed(Parameter(int, "Role identifier"),
67 Parameter(str, "Role name"))],
68 'sites': [Mixed(Parameter(int, "Site identifier"),
69 Parameter(str, "Site name"))],
70 'keys': [Mixed(Parameter(int, "Key identifier"),
72 'slices': [Mixed(Parameter(int, "Slice identifier"),
73 Parameter(str, "Slice name"))]
75 view_tags_name = "view_person_tags"
76 # tags are used by the Add/Get/Update methods to expose tags
77 # this is initialized here and updated by the accessors factory
80 def validate_email(self, email):
82 Validate email address. Stolen from Mailman.
85 invalid_email = PLCInvalidArgument("Invalid e-mail address")
90 email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
91 if not email_re.match(email):
94 # check only against users on the same peer
96 namespace_peer_id = self['peer_id']
98 namespace_peer_id = None
100 conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
102 for person in conflicts:
103 if 'person_id' not in self or self['person_id'] != person['person_id']:
104 raise PLCInvalidArgument, "E-mail address already in use"
108 def validate_password(self, password):
110 Encrypt password if necessary before committing to the
116 if len(password) > len(magic) and \
117 password[0:len(magic)] == magic:
120 # Generate a somewhat unique 8 character salt string
121 salt = str(time.time()) + str(Random().random())
122 salt = md5(salt).hexdigest()[:8]
123 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
125 validate_date_created = Row.validate_timestamp
126 validate_last_updated = Row.validate_timestamp
127 validate_verification_expires = Row.validate_timestamp
129 def can_update(self, person):
131 Returns true if we can update the specified person. We can
134 1. We are the person.
136 3. We are a PI and the person is a user or tech or at
140 assert isinstance(person, Person)
142 if self['person_id'] == person['person_id']:
145 if 'admin' in self['roles']:
148 if 'pi' in self['roles']:
149 if set(self['site_ids']).intersection(person['site_ids']):
150 # Can update person is neither a PI or ADMIN
151 return (not (('pi' in person['roles']) or ('admin' in person['roles'])))
155 def can_view(self, person):
157 Returns true if we can view the specified person. We can
160 1. We are the person.
162 3. We are a PI and the person is at one of our sites.
165 assert isinstance(person, Person)
167 if self.can_update(person):
170 if 'pi' in self['roles']:
171 if set(self['site_ids']).intersection(person['site_ids']):
172 # Can view people with equal or higher role IDs
173 return 'admin' not in person['roles']
177 add_role = Row.add_object(Role, 'person_role')
178 remove_role = Row.remove_object(Role, 'person_role')
180 add_key = Row.add_object(Key, 'person_key')
181 remove_key = Row.remove_object(Key, 'person_key')
183 def set_primary_site(self, site, commit = True):
185 Set the primary site for an existing user.
188 assert 'person_id' in self
189 assert 'site_id' in site
191 person_id = self['person_id']
192 site_id = site['site_id']
193 self.api.db.do("UPDATE person_site SET is_primary = False" \
194 " WHERE person_id = %(person_id)d",
196 self.api.db.do("UPDATE person_site SET is_primary = True" \
197 " WHERE person_id = %(person_id)d" \
198 " AND site_id = %(site_id)d",
204 assert 'site_ids' in self
205 assert site_id in self['site_ids']
207 # Make sure that the primary site is first in the list
208 self['site_ids'].remove(site_id)
209 self['site_ids'].insert(0, site_id)
211 def update_last_updated(self, commit = True):
213 Update last_updated field with current time
216 assert 'person_id' in self
217 assert self.table_name
219 self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
220 " where person_id = %d" % (self['person_id']) )
223 def associate_roles(self, auth, field, value):
225 Adds roles found in value list to this person (using AddRoleToPerson).
226 Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
229 assert 'role_ids' in self
230 assert 'person_id' in self
231 assert isinstance(value, list)
233 (role_ids, role_names) = self.separate_types(value)[0:2]
235 # Translate roles into role_ids
237 roles = Roles(self.api, role_names).dict('role_id')
238 role_ids += roles.keys()
240 # Add new ids, remove stale ids
241 if self['role_ids'] != role_ids:
242 from PLC.Methods.AddRoleToPerson import AddRoleToPerson
243 from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
244 new_roles = set(role_ids).difference(self['role_ids'])
245 stale_roles = set(self['role_ids']).difference(role_ids)
247 for new_role in new_roles:
248 AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
249 for stale_role in stale_roles:
250 DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
253 def associate_sites(self, auth, field, value):
255 Adds person to sites found in value list (using AddPersonToSite).
256 Deletes person from site not found in value list (using DeletePersonFromSite).
259 from PLC.Sites import Sites
261 assert 'site_ids' in self
262 assert 'person_id' in self
263 assert isinstance(value, list)
265 (site_ids, site_names) = self.separate_types(value)[0:2]
267 # Translate roles into role_ids
269 sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
270 site_ids += sites.keys()
272 # Add new ids, remove stale ids
273 if self['site_ids'] != site_ids:
274 from PLC.Methods.AddPersonToSite import AddPersonToSite
275 from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
276 new_sites = set(site_ids).difference(self['site_ids'])
277 stale_sites = set(self['site_ids']).difference(site_ids)
279 for new_site in new_sites:
280 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
281 for stale_site in stale_sites:
282 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
285 def associate_keys(self, auth, field, value):
287 Deletes key_ids not found in value list (using DeleteKey).
288 Adds key if key_fields w/o key_id is found (using AddPersonKey).
289 Updates key if key_fields w/ key_id is found (using UpdateKey).
291 assert 'key_ids' in self
292 assert 'person_id' in self
293 assert isinstance(value, list)
295 (key_ids, blank, keys) = self.separate_types(value)
297 if self['key_ids'] != key_ids:
298 from PLC.Methods.DeleteKey import DeleteKey
299 stale_keys = set(self['key_ids']).difference(key_ids)
301 for stale_key in stale_keys:
302 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
305 from PLC.Methods.AddPersonKey import AddPersonKey
306 from PLC.Methods.UpdateKey import UpdateKey
307 updated_keys = filter(lambda key: 'key_id' in key, keys)
308 added_keys = filter(lambda key: 'key_id' not in key, keys)
310 for key in added_keys:
311 AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
312 for key in updated_keys:
313 key_id = key.pop('key_id')
314 UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
317 def associate_slices(self, auth, field, value):
319 Adds person to slices found in value list (using AddPersonToSlice).
320 Deletes person from slices found in value list (using DeletePersonFromSlice).
323 from PLC.Slices import Slices
325 assert 'slice_ids' in self
326 assert 'person_id' in self
327 assert isinstance(value, list)
329 (slice_ids, slice_names) = self.separate_types(value)[0:2]
331 # Translate roles into role_ids
333 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
334 slice_ids += slices.keys()
336 # Add new ids, remove stale ids
337 if self['slice_ids'] != slice_ids:
338 from PLC.Methods.AddPersonToSlice import AddPersonToSlice
339 from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
340 new_slices = set(slice_ids).difference(self['slice_ids'])
341 stale_slices = set(self['slice_ids']).difference(slice_ids)
343 for new_slice in new_slices:
344 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
345 for stale_slice in stale_slices:
346 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
349 def delete(self, commit = True):
351 Delete existing user.
355 keys = Keys(self.api, self['key_ids'])
357 key.delete(commit = False)
359 # Clean up miscellaneous join tables
360 for table in self.join_tables:
361 self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
362 (table, self['person_id']))
365 self['deleted'] = True
368 class Persons(Table):
370 Representation of row(s) from the persons table in the
374 def __init__(self, api, person_filter = None, columns = None):
375 Table.__init__(self, api, Person, columns)
377 view = "view_persons"
378 for tagname in self.tag_columns:
379 view= "%s left join %s using (%s)"%(view,Person.tagvalue_view_name(tagname),
382 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
383 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
385 if person_filter is not None:
386 if isinstance(person_filter, (list, tuple, set)):
387 # Separate the list into integers and strings
388 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
389 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
390 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
391 sql += " AND (%s) %s" % person_filter.sql(api, "OR")
392 elif isinstance(person_filter, dict):
393 person_filter = Filter(Person.fields, person_filter)
394 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
395 elif isinstance (person_filter, StringTypes):
396 person_filter = Filter(Person.fields, {'email':[person_filter]})
397 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
398 elif isinstance (person_filter, int):
399 person_filter = Filter(Person.fields, {'person_id':[person_filter]})
400 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
402 raise PLCInvalidArgument, "Wrong person filter %r"%person_filter