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.Debug import log
20 from PLC.Parameter import Parameter, Mixed
21 from PLC.Filter import Filter
22 from PLC.Table import Row, Table
23 from PLC.Roles import Role, Roles
24 from PLC.Keys import Key, Keys
25 from PLC.Messages import Message, Messages
29 Representation of a row in the persons table. To use, optionally
30 instantiate with a dict of values. Update as you would a
31 dict. Commit to the database with sync().
34 table_name = 'persons'
35 primary_key = 'person_id'
36 join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
38 'person_id': Parameter(int, "User identifier"),
39 'first_name': Parameter(str, "Given name", max = 128),
40 'last_name': Parameter(str, "Surname", max = 128),
41 'title': Parameter(str, "Title", max = 128, nullok = True),
42 'email': Parameter(str, "Primary e-mail address", max = 254),
43 'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
44 'url': Parameter(str, "Home page", max = 254, nullok = True),
45 'bio': Parameter(str, "Biography", max = 254, nullok = True),
46 'enabled': Parameter(bool, "Has been enabled"),
47 'password': Parameter(str, "Account password in crypt() form", max = 254),
48 'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
49 'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
50 'last_updated': Parameter(int, "Date and time of last update", ro = True),
51 'date_created': Parameter(int, "Date and time when account was created", ro = True),
52 'role_ids': Parameter([int], "List of role identifiers"),
53 'roles': Parameter([str], "List of roles"),
54 'site_ids': Parameter([int], "List of site identifiers"),
55 'key_ids': Parameter([int], "List of key identifiers"),
56 'slice_ids': Parameter([int], "List of slice identifiers"),
57 'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
58 'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
59 'person_tag_ids' : Parameter ([int], "List of tags attached to this person"),
62 'roles': [Mixed(Parameter(int, "Role identifier"),
63 Parameter(str, "Role name"))],
64 'sites': [Mixed(Parameter(int, "Site identifier"),
65 Parameter(str, "Site name"))],
66 'keys': [Mixed(Parameter(int, "Key identifier"),
68 'slices': [Mixed(Parameter(int, "Slice identifier"),
69 Parameter(str, "Slice name"))]
71 view_tags_name = "view_person_tags"
72 # tags are used by the Add/Get/Update methods to expose tags
73 # this is initialized here and updated by the accessors factory
76 def validate_email(self, email):
78 Validate email address. Stolen from Mailman.
81 invalid_email = PLCInvalidArgument("Invalid e-mail address")
86 email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
87 if not email_re.match(email):
90 # check only against users on the same peer
92 namespace_peer_id = self['peer_id']
94 namespace_peer_id = None
96 conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
98 for person in conflicts:
99 if 'person_id' not in self or self['person_id'] != person['person_id']:
100 raise PLCInvalidArgument, "E-mail address already in use"
104 def validate_password(self, password):
106 Encrypt password if necessary before committing to the
112 if len(password) > len(magic) and \
113 password[0:len(magic)] == magic:
116 # Generate a somewhat unique 8 character salt string
117 salt = str(time.time()) + str(Random().random())
118 salt = md5(salt).hexdigest()[:8]
119 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
121 validate_date_created = Row.validate_timestamp
122 validate_last_updated = Row.validate_timestamp
123 validate_verification_expires = Row.validate_timestamp
125 def can_update(self, person):
127 Returns true if we can update the specified person. We can
130 1. We are the person.
132 3. We are a PI and the person is a user or tech or at
136 assert isinstance(person, Person)
138 if self['person_id'] == person['person_id']:
141 if 'admin' in self['roles']:
144 if 'pi' in self['roles']:
145 if set(self['site_ids']).intersection(person['site_ids']):
146 # Can update person is neither a PI or ADMIN
147 return (not (('pi' in person['roles']) or ('admin' in person['roles'])))
151 def can_view(self, person):
153 Returns true if we can view the specified person. We can
156 1. We are the person.
158 3. We are a PI and the person is at one of our sites.
161 assert isinstance(person, Person)
163 if self.can_update(person):
166 if 'pi' in self['roles']:
167 if set(self['site_ids']).intersection(person['site_ids']):
168 # Can view people with equal or higher role IDs
169 return 'admin' not in person['roles']
173 add_role = Row.add_object(Role, 'person_role')
174 remove_role = Row.remove_object(Role, 'person_role')
176 add_key = Row.add_object(Key, 'person_key')
177 remove_key = Row.remove_object(Key, 'person_key')
179 def set_primary_site(self, site, commit = True):
181 Set the primary site for an existing user.
184 assert 'person_id' in self
185 assert 'site_id' in site
187 person_id = self['person_id']
188 site_id = site['site_id']
189 self.api.db.do("UPDATE person_site SET is_primary = False" \
190 " WHERE person_id = %(person_id)d",
192 self.api.db.do("UPDATE person_site SET is_primary = True" \
193 " WHERE person_id = %(person_id)d" \
194 " AND site_id = %(site_id)d",
200 assert 'site_ids' in self
201 assert site_id in self['site_ids']
203 # Make sure that the primary site is first in the list
204 self['site_ids'].remove(site_id)
205 self['site_ids'].insert(0, site_id)
207 def update_last_updated(self, commit = True):
209 Update last_updated field with current time
212 assert 'person_id' in self
213 assert self.table_name
215 self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
216 " where person_id = %d" % (self['person_id']) )
219 def associate_roles(self, auth, field, value):
221 Adds roles found in value list to this person (using AddRoleToPerson).
222 Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
225 assert 'role_ids' in self
226 assert 'person_id' in self
227 assert isinstance(value, list)
229 (role_ids, role_names) = self.separate_types(value)[0:2]
231 # Translate roles into role_ids
233 roles = Roles(self.api, role_names).dict('role_id')
234 role_ids += roles.keys()
236 # Add new ids, remove stale ids
237 if self['role_ids'] != role_ids:
238 from PLC.Methods.AddRoleToPerson import AddRoleToPerson
239 from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
240 new_roles = set(role_ids).difference(self['role_ids'])
241 stale_roles = set(self['role_ids']).difference(role_ids)
243 for new_role in new_roles:
244 AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
245 for stale_role in stale_roles:
246 DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
249 def associate_sites(self, auth, field, value):
251 Adds person to sites found in value list (using AddPersonToSite).
252 Deletes person from site not found in value list (using DeletePersonFromSite).
255 from PLC.Sites import Sites
257 assert 'site_ids' in self
258 assert 'person_id' in self
259 assert isinstance(value, list)
261 (site_ids, site_names) = self.separate_types(value)[0:2]
263 # Translate roles into role_ids
265 sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
266 site_ids += sites.keys()
268 # Add new ids, remove stale ids
269 if self['site_ids'] != site_ids:
270 from PLC.Methods.AddPersonToSite import AddPersonToSite
271 from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
272 new_sites = set(site_ids).difference(self['site_ids'])
273 stale_sites = set(self['site_ids']).difference(site_ids)
275 for new_site in new_sites:
276 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
277 for stale_site in stale_sites:
278 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
281 def associate_keys(self, auth, field, value):
283 Deletes key_ids not found in value list (using DeleteKey).
284 Adds key if key_fields w/o key_id is found (using AddPersonKey).
285 Updates key if key_fields w/ key_id is found (using UpdateKey).
287 assert 'key_ids' in self
288 assert 'person_id' in self
289 assert isinstance(value, list)
291 (key_ids, blank, keys) = self.separate_types(value)
293 if self['key_ids'] != key_ids:
294 from PLC.Methods.DeleteKey import DeleteKey
295 stale_keys = set(self['key_ids']).difference(key_ids)
297 for stale_key in stale_keys:
298 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
301 from PLC.Methods.AddPersonKey import AddPersonKey
302 from PLC.Methods.UpdateKey import UpdateKey
303 updated_keys = filter(lambda key: 'key_id' in key, keys)
304 added_keys = filter(lambda key: 'key_id' not in key, keys)
306 for key in added_keys:
307 AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
308 for key in updated_keys:
309 key_id = key.pop('key_id')
310 UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
313 def associate_slices(self, auth, field, value):
315 Adds person to slices found in value list (using AddPersonToSlice).
316 Deletes person from slices found in value list (using DeletePersonFromSlice).
319 from PLC.Slices import Slices
321 assert 'slice_ids' in self
322 assert 'person_id' in self
323 assert isinstance(value, list)
325 (slice_ids, slice_names) = self.separate_types(value)[0:2]
327 # Translate roles into role_ids
329 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
330 slice_ids += slices.keys()
332 # Add new ids, remove stale ids
333 if self['slice_ids'] != slice_ids:
334 from PLC.Methods.AddPersonToSlice import AddPersonToSlice
335 from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
336 new_slices = set(slice_ids).difference(self['slice_ids'])
337 stale_slices = set(self['slice_ids']).difference(slice_ids)
339 for new_slice in new_slices:
340 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
341 for stale_slice in stale_slices:
342 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
345 def delete(self, commit = True):
347 Delete existing user.
351 keys = Keys(self.api, self['key_ids'])
353 key.delete(commit = False)
355 # Clean up miscellaneous join tables
356 for table in self.join_tables:
357 self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
358 (table, self['person_id']))
361 self['deleted'] = True
362 # don't validate, so duplicates can be consistently removed
363 self.sync(commit, validate=False)
365 class Persons(Table):
367 Representation of row(s) from the persons table in the
371 def __init__(self, api, person_filter = None, columns = None):
372 Table.__init__(self, api, Person, columns)
374 view = "view_persons"
375 for tagname in self.tag_columns:
376 view= "%s left join %s using (%s)"%(view,Person.tagvalue_view_name(tagname),
379 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
380 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
382 if person_filter is not None:
383 if isinstance(person_filter, (list, tuple, set)):
384 # Separate the list into integers and strings
385 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
386 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
387 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
388 sql += " AND (%s) %s" % person_filter.sql(api, "OR")
389 elif isinstance(person_filter, dict):
390 person_filter = Filter(Person.fields, person_filter)
391 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
392 elif isinstance (person_filter, StringTypes):
393 person_filter = Filter(Person.fields, {'email':person_filter})
394 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
395 elif isinstance (person_filter, (int, long)):
396 person_filter = Filter(Person.fields, {'person_id':person_filter})
397 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
399 raise PLCInvalidArgument, "Wrong person filter %r"%person_filter