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
13 from hashlib import md5
17 from random import Random
21 from PLC.Faults import *
22 from PLC.Debug import log
23 from PLC.Parameter import Parameter, Mixed
24 from PLC.Filter import Filter
25 from PLC.Table import Row, Table
26 from PLC.Roles import Role, Roles
27 from PLC.Keys import Key, Keys
28 from PLC.Messages import Message, Messages
32 Representation of a row in the persons table. To use, optionally
33 instantiate with a dict of values. Update as you would a
34 dict. Commit to the database with sync().
37 table_name = 'persons'
38 primary_key = 'person_id'
39 join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
41 'person_id': Parameter(int, "User identifier"),
42 'first_name': Parameter(str, "Given name", max = 128),
43 'last_name': Parameter(str, "Surname", max = 128),
44 'title': Parameter(str, "Title", max = 128, nullok = True),
45 'email': Parameter(str, "Primary e-mail address", max = 254),
46 'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
47 'url': Parameter(str, "Home page", max = 254, nullok = True),
48 'bio': Parameter(str, "Biography", max = 254, nullok = True),
49 'enabled': Parameter(bool, "Has been enabled"),
50 'password': Parameter(str, "Account password in crypt() form", max = 254),
51 'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
52 'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
53 'last_updated': Parameter(int, "Date and time of last update", ro = True),
54 'date_created': Parameter(int, "Date and time when account was created", ro = True),
55 'role_ids': Parameter([int], "List of role identifiers"),
56 'roles': Parameter([str], "List of roles"),
57 'site_ids': Parameter([int], "List of site identifiers"),
58 'key_ids': Parameter([int], "List of key identifiers"),
59 'slice_ids': Parameter([int], "List of slice identifiers"),
60 'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
61 'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
62 'person_tag_ids' : Parameter ([int], "List of tags attached to this person"),
65 'roles': [Mixed(Parameter(int, "Role identifier"),
66 Parameter(str, "Role name"))],
67 'sites': [Mixed(Parameter(int, "Site identifier"),
68 Parameter(str, "Site name"))],
69 'keys': [Mixed(Parameter(int, "Key identifier"),
71 'slices': [Mixed(Parameter(int, "Slice identifier"),
72 Parameter(str, "Slice name"))]
74 view_tags_name = "view_person_tags"
75 # tags are used by the Add/Get/Update methods to expose tags
76 # this is initialized here and updated by the accessors factory
79 def validate_email(self, email):
81 Validate email address. Stolen from Mailman.
84 invalid_email = PLCInvalidArgument("Invalid e-mail address")
89 email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
90 if not email_re.match(email):
93 # check only against users on the same peer
95 namespace_peer_id = self['peer_id']
97 namespace_peer_id = None
99 conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
101 for person in conflicts:
102 if 'person_id' not in self or self['person_id'] != person['person_id']:
103 raise PLCInvalidArgument, "E-mail address already in use"
107 def validate_password(self, password):
109 Encrypt password if necessary before committing to the
115 if len(password) > len(magic) and \
116 password[0:len(magic)] == magic:
119 # Generate a somewhat unique 8 character salt string
120 salt = str(time.time()) + str(Random().random())
121 salt = md5(salt).hexdigest()[:8]
122 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
124 validate_date_created = Row.validate_timestamp
125 validate_last_updated = Row.validate_timestamp
126 validate_verification_expires = Row.validate_timestamp
128 def can_update(self, person):
130 Returns true if we can update the specified person. We can
133 1. We are the person.
135 3. We are a PI and the person is a user or tech or at
139 assert isinstance(person, Person)
141 if self['person_id'] == person['person_id']:
144 if 'admin' in self['roles']:
147 if 'pi' in self['roles']:
148 if set(self['site_ids']).intersection(person['site_ids']):
149 # Can update person is neither a PI or ADMIN
150 return (not (('pi' in person['roles']) or ('admin' in person['roles'])))
154 def can_view(self, person):
156 Returns true if we can view the specified person. We can
159 1. We are the person.
161 3. We are a PI and the person is at one of our sites.
164 assert isinstance(person, Person)
166 if self.can_update(person):
169 if 'pi' in self['roles']:
170 if set(self['site_ids']).intersection(person['site_ids']):
171 # Can view people with equal or higher role IDs
172 return 'admin' not in person['roles']
176 add_role = Row.add_object(Role, 'person_role')
177 remove_role = Row.remove_object(Role, 'person_role')
179 add_key = Row.add_object(Key, 'person_key')
180 remove_key = Row.remove_object(Key, 'person_key')
182 def set_primary_site(self, site, commit = True):
184 Set the primary site for an existing user.
187 assert 'person_id' in self
188 assert 'site_id' in site
190 person_id = self['person_id']
191 site_id = site['site_id']
192 self.api.db.do("UPDATE person_site SET is_primary = False" \
193 " WHERE person_id = %(person_id)d",
195 self.api.db.do("UPDATE person_site SET is_primary = True" \
196 " WHERE person_id = %(person_id)d" \
197 " AND site_id = %(site_id)d",
203 assert 'site_ids' in self
204 assert site_id in self['site_ids']
206 # Make sure that the primary site is first in the list
207 self['site_ids'].remove(site_id)
208 self['site_ids'].insert(0, site_id)
210 def update_last_updated(self, commit = True):
212 Update last_updated field with current time
215 assert 'person_id' in self
216 assert self.table_name
218 self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
219 " where person_id = %d" % (self['person_id']) )
222 def associate_roles(self, auth, field, value):
224 Adds roles found in value list to this person (using AddRoleToPerson).
225 Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
228 assert 'role_ids' in self
229 assert 'person_id' in self
230 assert isinstance(value, list)
232 (role_ids, role_names) = self.separate_types(value)[0:2]
234 # Translate roles into role_ids
236 roles = Roles(self.api, role_names).dict('role_id')
237 role_ids += roles.keys()
239 # Add new ids, remove stale ids
240 if self['role_ids'] != role_ids:
241 from PLC.Methods.AddRoleToPerson import AddRoleToPerson
242 from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
243 new_roles = set(role_ids).difference(self['role_ids'])
244 stale_roles = set(self['role_ids']).difference(role_ids)
246 for new_role in new_roles:
247 AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
248 for stale_role in stale_roles:
249 DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
252 def associate_sites(self, auth, field, value):
254 Adds person to sites found in value list (using AddPersonToSite).
255 Deletes person from site not found in value list (using DeletePersonFromSite).
258 from PLC.Sites import Sites
260 assert 'site_ids' in self
261 assert 'person_id' in self
262 assert isinstance(value, list)
264 (site_ids, site_names) = self.separate_types(value)[0:2]
266 # Translate roles into role_ids
268 sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
269 site_ids += sites.keys()
271 # Add new ids, remove stale ids
272 if self['site_ids'] != site_ids:
273 from PLC.Methods.AddPersonToSite import AddPersonToSite
274 from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
275 new_sites = set(site_ids).difference(self['site_ids'])
276 stale_sites = set(self['site_ids']).difference(site_ids)
278 for new_site in new_sites:
279 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
280 for stale_site in stale_sites:
281 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
284 def associate_keys(self, auth, field, value):
286 Deletes key_ids not found in value list (using DeleteKey).
287 Adds key if key_fields w/o key_id is found (using AddPersonKey).
288 Updates key if key_fields w/ key_id is found (using UpdateKey).
290 assert 'key_ids' in self
291 assert 'person_id' in self
292 assert isinstance(value, list)
294 (key_ids, blank, keys) = self.separate_types(value)
296 if self['key_ids'] != key_ids:
297 from PLC.Methods.DeleteKey import DeleteKey
298 stale_keys = set(self['key_ids']).difference(key_ids)
300 for stale_key in stale_keys:
301 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
304 from PLC.Methods.AddPersonKey import AddPersonKey
305 from PLC.Methods.UpdateKey import UpdateKey
306 updated_keys = filter(lambda key: 'key_id' in key, keys)
307 added_keys = filter(lambda key: 'key_id' not in key, keys)
309 for key in added_keys:
310 AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
311 for key in updated_keys:
312 key_id = key.pop('key_id')
313 UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
316 def associate_slices(self, auth, field, value):
318 Adds person to slices found in value list (using AddPersonToSlice).
319 Deletes person from slices found in value list (using DeletePersonFromSlice).
322 from PLC.Slices import Slices
324 assert 'slice_ids' in self
325 assert 'person_id' in self
326 assert isinstance(value, list)
328 (slice_ids, slice_names) = self.separate_types(value)[0:2]
330 # Translate roles into role_ids
332 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
333 slice_ids += slices.keys()
335 # Add new ids, remove stale ids
336 if self['slice_ids'] != slice_ids:
337 from PLC.Methods.AddPersonToSlice import AddPersonToSlice
338 from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
339 new_slices = set(slice_ids).difference(self['slice_ids'])
340 stale_slices = set(self['slice_ids']).difference(slice_ids)
342 for new_slice in new_slices:
343 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
344 for stale_slice in stale_slices:
345 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
348 def delete(self, commit = True):
350 Delete existing user.
354 keys = Keys(self.api, self['key_ids'])
356 key.delete(commit = False)
358 # Clean up miscellaneous join tables
359 for table in self.join_tables:
360 self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
361 (table, self['person_id']))
364 self['deleted'] = True
365 # don't validate, so duplicates can be consistently removed
366 self.sync(commit, validate=False)
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, long)):
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