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
8 from types import StringTypes
10 from hashlib import md5
14 from random import Random
18 from PLC.Faults import *
19 from PLC.Parameter import Parameter, Mixed
20 from PLC.Filter import Filter
21 from PLC.Table import Row, Table
22 from PLC.Roles import Role, Roles
23 from PLC.Keys import Key, Keys
24 from PLC.Messages import Message, Messages
28 Representation of a row in the persons table. To use, optionally
29 instantiate with a dict of values. Update as you would a
30 dict. Commit to the database with sync().
33 table_name = 'persons'
34 primary_key = 'person_id'
35 join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
37 'person_id': Parameter(int, "User identifier"),
38 'first_name': Parameter(str, "Given name", max = 128),
39 'last_name': Parameter(str, "Surname", max = 128),
40 'title': Parameter(str, "Title", max = 128, nullok = True),
41 'email': Parameter(str, "Primary e-mail address", max = 254),
42 'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
43 'url': Parameter(str, "Home page", max = 254, nullok = True),
44 'bio': Parameter(str, "Biography", max = 254, nullok = True),
45 'enabled': Parameter(bool, "Has been enabled"),
46 'password': Parameter(str, "Account password in crypt() form", max = 254),
47 'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
48 'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
49 'last_updated': Parameter(int, "Date and time of last update", ro = True),
50 'date_created': Parameter(int, "Date and time when account was created", ro = True),
51 'role_ids': Parameter([int], "List of role identifiers"),
52 'roles': Parameter([str], "List of roles"),
53 'site_ids': Parameter([int], "List of site identifiers"),
54 'key_ids': Parameter([int], "List of key identifiers"),
55 'slice_ids': Parameter([int], "List of slice identifiers"),
56 'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
57 'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
58 'person_tag_ids' : Parameter ([int], "List of tags attached to this person"),
61 'roles': [Mixed(Parameter(int, "Role identifier"),
62 Parameter(str, "Role name"))],
63 'sites': [Mixed(Parameter(int, "Site identifier"),
64 Parameter(str, "Site name"))],
65 'keys': [Mixed(Parameter(int, "Key identifier"),
67 'slices': [Mixed(Parameter(int, "Slice identifier"),
68 Parameter(str, "Slice name"))]
70 view_tags_name = "view_person_tags"
71 # tags are used by the Add/Get/Update methods to expose tags
72 # this is initialized here and updated by the accessors factory
75 def validate_email(self, email):
77 Validate email address. Stolen from Mailman.
80 invalid_email = PLCInvalidArgument("Invalid e-mail address %s"%email)
85 email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
86 if not email_re.match(email):
89 # check only against users on the same peer
91 namespace_peer_id = self['peer_id']
93 namespace_peer_id = None
95 conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
97 for person in conflicts:
98 if 'person_id' not in self or self['person_id'] != person['person_id']:
99 raise PLCInvalidArgument, "E-mail address already in use"
103 def validate_password(self, password):
105 Encrypt password if necessary before committing to the
111 if len(password) > len(magic) and \
112 password[0:len(magic)] == magic:
115 # Generate a somewhat unique 8 character salt string
116 salt = str(time.time()) + str(Random().random())
117 salt = md5(salt).hexdigest()[:8]
118 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
120 validate_date_created = Row.validate_timestamp
121 validate_last_updated = Row.validate_timestamp
122 validate_verification_expires = Row.validate_timestamp
124 def can_update(self, person):
126 Returns true if we can update the specified person. We can
129 1. We are the person.
131 3. We are a PI and the person is a user or tech or at
135 assert isinstance(person, Person)
137 if self['person_id'] == person['person_id']:
140 if 'admin' in self['roles']:
143 if 'pi' in self['roles']:
144 if set(self['site_ids']).intersection(person['site_ids']):
145 # non-admin users cannot update a person who is neither a PI or ADMIN
146 return (not set(['pi','admin']).intersection(person['roles']))
150 def can_view(self, person):
152 Returns true if we can view the specified person. We can
155 1. We are the person.
157 3. We are a PI or Tech and the person is at one of our sites.
160 assert isinstance(person, Person)
162 if self.can_update(person):
165 # pis and techs can see all people on their site
166 if set(['pi','tech']).intersection(self['roles']):
167 if set(self['site_ids']).intersection(person['site_ids']):
172 add_role = Row.add_object(Role, 'person_role')
173 remove_role = Row.remove_object(Role, 'person_role')
175 add_key = Row.add_object(Key, 'person_key')
176 remove_key = Row.remove_object(Key, 'person_key')
178 def set_primary_site(self, site, commit = True):
180 Set the primary site for an existing user.
183 assert 'person_id' in self
184 assert 'site_id' in site
186 person_id = self['person_id']
187 site_id = site['site_id']
188 self.api.db.do("UPDATE person_site SET is_primary = False" \
189 " WHERE person_id = %(person_id)d",
191 self.api.db.do("UPDATE person_site SET is_primary = True" \
192 " WHERE person_id = %(person_id)d" \
193 " AND site_id = %(site_id)d",
199 assert 'site_ids' in self
200 assert site_id in self['site_ids']
202 # Make sure that the primary site is first in the list
203 self['site_ids'].remove(site_id)
204 self['site_ids'].insert(0, site_id)
206 def update_last_updated(self, commit = True):
208 Update last_updated field with current time
211 assert 'person_id' in self
212 assert self.table_name
214 self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
215 " where person_id = %d" % (self['person_id']) )
218 def associate_roles(self, auth, field, value):
220 Adds roles found in value list to this person (using AddRoleToPerson).
221 Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
224 assert 'role_ids' in self
225 assert 'person_id' in self
226 assert isinstance(value, list)
228 (role_ids, role_names) = self.separate_types(value)[0:2]
230 # Translate roles into role_ids
232 roles = Roles(self.api, role_names).dict('role_id')
233 role_ids += roles.keys()
235 # Add new ids, remove stale ids
236 if self['role_ids'] != role_ids:
237 from PLC.Methods.AddRoleToPerson import AddRoleToPerson
238 from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
239 new_roles = set(role_ids).difference(self['role_ids'])
240 stale_roles = set(self['role_ids']).difference(role_ids)
242 for new_role in new_roles:
243 AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
244 for stale_role in stale_roles:
245 DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
248 def associate_sites(self, auth, field, value):
250 Adds person to sites found in value list (using AddPersonToSite).
251 Deletes person from site not found in value list (using DeletePersonFromSite).
254 from PLC.Sites import Sites
256 assert 'site_ids' in self
257 assert 'person_id' in self
258 assert isinstance(value, list)
260 (site_ids, site_names) = self.separate_types(value)[0:2]
262 # Translate roles into role_ids
264 sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
265 site_ids += sites.keys()
267 # Add new ids, remove stale ids
268 if self['site_ids'] != site_ids:
269 from PLC.Methods.AddPersonToSite import AddPersonToSite
270 from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
271 new_sites = set(site_ids).difference(self['site_ids'])
272 stale_sites = set(self['site_ids']).difference(site_ids)
274 for new_site in new_sites:
275 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
276 for stale_site in stale_sites:
277 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
280 def associate_keys(self, auth, field, value):
282 Deletes key_ids not found in value list (using DeleteKey).
283 Adds key if key_fields w/o key_id is found (using AddPersonKey).
284 Updates key if key_fields w/ key_id is found (using UpdateKey).
286 assert 'key_ids' in self
287 assert 'person_id' in self
288 assert isinstance(value, list)
290 (key_ids, blank, keys) = self.separate_types(value)
292 if self['key_ids'] != key_ids:
293 from PLC.Methods.DeleteKey import DeleteKey
294 stale_keys = set(self['key_ids']).difference(key_ids)
296 for stale_key in stale_keys:
297 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
300 from PLC.Methods.AddPersonKey import AddPersonKey
301 from PLC.Methods.UpdateKey import UpdateKey
302 updated_keys = filter(lambda key: 'key_id' in key, keys)
303 added_keys = filter(lambda key: 'key_id' not in key, keys)
305 for key in added_keys:
306 AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
307 for key in updated_keys:
308 key_id = key.pop('key_id')
309 UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
312 def associate_slices(self, auth, field, value):
314 Adds person to slices found in value list (using AddPersonToSlice).
315 Deletes person from slices found in value list (using DeletePersonFromSlice).
318 from PLC.Slices import Slices
320 assert 'slice_ids' in self
321 assert 'person_id' in self
322 assert isinstance(value, list)
324 (slice_ids, slice_names) = self.separate_types(value)[0:2]
326 # Translate roles into role_ids
328 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
329 slice_ids += slices.keys()
331 # Add new ids, remove stale ids
332 if self['slice_ids'] != slice_ids:
333 from PLC.Methods.AddPersonToSlice import AddPersonToSlice
334 from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
335 new_slices = set(slice_ids).difference(self['slice_ids'])
336 stale_slices = set(self['slice_ids']).difference(slice_ids)
338 for new_slice in new_slices:
339 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
340 for stale_slice in stale_slices:
341 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
344 def delete(self, commit = True):
346 Delete existing user.
350 keys = Keys(self.api, self['key_ids'])
352 key.delete(commit = False)
354 # Clean up miscellaneous join tables
355 for table in self.join_tables:
356 self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
357 (table, self['person_id']))
360 self['deleted'] = True
362 # delete will fail if timestamp fields aren't validated, so lets remove them
363 for field in ['verification_expires', 'date_created', 'last_updated']:
367 # don't validate, so duplicates can be consistently removed
368 self.sync(commit, validate=False)
370 class Persons(Table):
372 Representation of row(s) from the persons table in the
376 def __init__(self, api, person_filter = None, columns = None):
377 Table.__init__(self, api, Person, columns)
379 view = "view_persons"
380 for tagname in self.tag_columns:
381 view= "%s left join %s using (%s)"%(view,Person.tagvalue_view_name(tagname),
384 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
385 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
387 if person_filter is not None:
388 if isinstance(person_filter, (list, tuple, set)):
389 # Separate the list into integers and strings
390 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
391 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
392 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
393 sql += " AND (%s) %s" % person_filter.sql(api, "OR")
394 elif isinstance(person_filter, dict):
395 allowed_fields=dict(Person.fields.items()+Person.tags.items())
396 person_filter = Filter(allowed_fields, person_filter)
397 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
398 elif isinstance (person_filter, StringTypes):
399 person_filter = Filter(Person.fields, {'email':person_filter})
400 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
401 elif isinstance (person_filter, (int, long)):
402 person_filter = Filter(Person.fields, {'person_id':person_filter})
403 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
405 raise PLCInvalidArgument, "Wrong person filter %r"%person_filter