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
15 from random import Random
19 from PLC.Faults import *
20 from PLC.Debug import log
21 from PLC.Parameter import Parameter, Mixed
22 from PLC.Filter import Filter
23 from PLC.Table import Row, Table
24 from PLC.Roles import Role, Roles
25 from PLC.Keys import Key, Keys
26 from PLC.Messages import Message, Messages
30 Representation of a row in the persons table. To use, optionally
31 instantiate with a dict of values. Update as you would a
32 dict. Commit to the database with sync().
35 table_name = 'persons'
36 primary_key = 'person_id'
37 join_tables = ['person_key', 'person_role', 'person_site', 'slice_person', 'person_session', 'peer_person']
39 'person_id': Parameter(int, "User identifier"),
40 'first_name': Parameter(str, "Given name", max = 128),
41 'last_name': Parameter(str, "Surname", max = 128),
42 'title': Parameter(str, "Title", max = 128, nullok = True),
43 'email': Parameter(str, "Primary e-mail address", max = 254),
44 'phone': Parameter(str, "Telephone number", max = 64, nullok = True),
45 'url': Parameter(str, "Home page", max = 254, nullok = True),
46 'bio': Parameter(str, "Biography", max = 254, nullok = True),
47 'enabled': Parameter(bool, "Has been enabled"),
48 'password': Parameter(str, "Account password in crypt() form", max = 254),
49 'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
50 'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
51 'last_updated': Parameter(int, "Date and time of last update", ro = True),
52 'date_created': Parameter(int, "Date and time when account was created", ro = True),
53 'role_ids': Parameter([int], "List of role identifiers"),
54 'roles': Parameter([str], "List of roles"),
55 'site_ids': Parameter([int], "List of site identifiers"),
56 'key_ids': Parameter([int], "List of key identifiers"),
57 'slice_ids': Parameter([int], "List of slice identifiers"),
58 'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
59 'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
60 'person_tag_ids' : Parameter ([int], "List of tags attached to this person"),
63 'roles': [Mixed(Parameter(int, "Role identifier"),
64 Parameter(str, "Role name"))],
65 'sites': [Mixed(Parameter(int, "Site identifier"),
66 Parameter(str, "Site name"))],
67 'keys': [Mixed(Parameter(int, "Key identifier"),
69 'slices': [Mixed(Parameter(int, "Slice identifier"),
70 Parameter(str, "Slice name"))]
72 view_tags_name = "view_person_tags"
73 # tags are used by the Add/Get/Update methods to expose tags
74 # this is initialized here and updated by the accessors factory
77 def validate_email(self, email):
79 Validate email address. Stolen from Mailman.
82 invalid_email = PLCInvalidArgument("Invalid e-mail address")
83 email_badchars = r'[][()<>|;^,\200-\377]'
85 # Pretty minimal, cheesy check. We could do better...
86 if not email or email.count(' ') > 0:
88 if re.search(email_badchars, email) or email[0] == '-':
92 at_sign = email.find('@')
95 user = email[:at_sign]
96 rest = email[at_sign+1:]
97 domain = rest.split('.')
99 # This means local, unqualified addresses, are not allowed
105 # check only against users on the same peer
106 if 'peer_id' in self:
107 namespace_peer_id = self['peer_id']
109 namespace_peer_id = None
111 conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
113 for person in conflicts:
114 if 'person_id' not in self or self['person_id'] != person['person_id']:
115 raise PLCInvalidArgument, "E-mail address already in use"
119 def validate_password(self, password):
121 Encrypt password if necessary before committing to the
127 if len(password) > len(magic) and \
128 password[0:len(magic)] == magic:
131 # Generate a somewhat unique 8 character salt string
132 salt = str(time.time()) + str(Random().random())
133 salt = md5.md5(salt).hexdigest()[:8]
134 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
136 validate_date_created = Row.validate_timestamp
137 validate_last_updated = Row.validate_timestamp
138 validate_verification_expires = Row.validate_timestamp
140 def can_update(self, person):
142 Returns true if we can update the specified person. We can
145 1. We are the person.
147 3. We are a PI and the person is a user or tech or at
151 assert isinstance(person, Person)
153 if self['person_id'] == person['person_id']:
156 if 'admin' in self['roles']:
159 if 'pi' in self['roles']:
160 if set(self['site_ids']).intersection(person['site_ids']):
161 # Can update person is neither a PI or ADMIN
162 return (not (('pi' in person['roles']) or ('admin' in person['roles'])))
166 def can_view(self, person):
168 Returns true if we can view the specified person. We can
171 1. We are the person.
173 3. We are a PI and the person is at one of our sites.
176 assert isinstance(person, Person)
178 if self.can_update(person):
181 if 'pi' in self['roles']:
182 if set(self['site_ids']).intersection(person['site_ids']):
183 # Can view people with equal or higher role IDs
184 return 'admin' not in person['roles']
188 add_role = Row.add_object(Role, 'person_role')
189 remove_role = Row.remove_object(Role, 'person_role')
191 add_key = Row.add_object(Key, 'person_key')
192 remove_key = Row.remove_object(Key, 'person_key')
194 def set_primary_site(self, site, commit = True):
196 Set the primary site for an existing user.
199 assert 'person_id' in self
200 assert 'site_id' in site
202 person_id = self['person_id']
203 site_id = site['site_id']
204 self.api.db.do("UPDATE person_site SET is_primary = False" \
205 " WHERE person_id = %(person_id)d",
207 self.api.db.do("UPDATE person_site SET is_primary = True" \
208 " WHERE person_id = %(person_id)d" \
209 " AND site_id = %(site_id)d",
215 assert 'site_ids' in self
216 assert site_id in self['site_ids']
218 # Make sure that the primary site is first in the list
219 self['site_ids'].remove(site_id)
220 self['site_ids'].insert(0, site_id)
222 def update_last_updated(self, commit = True):
224 Update last_updated field with current time
227 assert 'person_id' in self
228 assert self.table_name
230 self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
231 " where person_id = %d" % (self['person_id']) )
234 def associate_roles(self, auth, field, value):
236 Adds roles found in value list to this person (using AddRoleToPerson).
237 Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
240 assert 'role_ids' in self
241 assert 'person_id' in self
242 assert isinstance(value, list)
244 (role_ids, role_names) = self.separate_types(value)[0:2]
246 # Translate roles into role_ids
248 roles = Roles(self.api, role_names).dict('role_id')
249 role_ids += roles.keys()
251 # Add new ids, remove stale ids
252 if self['role_ids'] != role_ids:
253 from PLC.Methods.AddRoleToPerson import AddRoleToPerson
254 from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
255 new_roles = set(role_ids).difference(self['role_ids'])
256 stale_roles = set(self['role_ids']).difference(role_ids)
258 for new_role in new_roles:
259 AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
260 for stale_role in stale_roles:
261 DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
264 def associate_sites(self, auth, field, value):
266 Adds person to sites found in value list (using AddPersonToSite).
267 Deletes person from site not found in value list (using DeletePersonFromSite).
270 from PLC.Sites import Sites
272 assert 'site_ids' in self
273 assert 'person_id' in self
274 assert isinstance(value, list)
276 (site_ids, site_names) = self.separate_types(value)[0:2]
278 # Translate roles into role_ids
280 sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
281 site_ids += sites.keys()
283 # Add new ids, remove stale ids
284 if self['site_ids'] != site_ids:
285 from PLC.Methods.AddPersonToSite import AddPersonToSite
286 from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
287 new_sites = set(site_ids).difference(self['site_ids'])
288 stale_sites = set(self['site_ids']).difference(site_ids)
290 for new_site in new_sites:
291 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
292 for stale_site in stale_sites:
293 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
296 def associate_keys(self, auth, field, value):
298 Deletes key_ids not found in value list (using DeleteKey).
299 Adds key if key_fields w/o key_id is found (using AddPersonKey).
300 Updates key if key_fields w/ key_id is found (using UpdateKey).
302 assert 'key_ids' in self
303 assert 'person_id' in self
304 assert isinstance(value, list)
306 (key_ids, blank, keys) = self.separate_types(value)
308 if self['key_ids'] != key_ids:
309 from PLC.Methods.DeleteKey import DeleteKey
310 stale_keys = set(self['key_ids']).difference(key_ids)
312 for stale_key in stale_keys:
313 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
316 from PLC.Methods.AddPersonKey import AddPersonKey
317 from PLC.Methods.UpdateKey import UpdateKey
318 updated_keys = filter(lambda key: 'key_id' in key, keys)
319 added_keys = filter(lambda key: 'key_id' not in key, keys)
321 for key in added_keys:
322 AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
323 for key in updated_keys:
324 key_id = key.pop('key_id')
325 UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
328 def associate_slices(self, auth, field, value):
330 Adds person to slices found in value list (using AddPersonToSlice).
331 Deletes person from slices found in value list (using DeletePersonFromSlice).
334 from PLC.Slices import Slices
336 assert 'slice_ids' in self
337 assert 'person_id' in self
338 assert isinstance(value, list)
340 (slice_ids, slice_names) = self.separate_types(value)[0:2]
342 # Translate roles into role_ids
344 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
345 slice_ids += slices.keys()
347 # Add new ids, remove stale ids
348 if self['slice_ids'] != slice_ids:
349 from PLC.Methods.AddPersonToSlice import AddPersonToSlice
350 from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
351 new_slices = set(slice_ids).difference(self['slice_ids'])
352 stale_slices = set(self['slice_ids']).difference(slice_ids)
354 for new_slice in new_slices:
355 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
356 for stale_slice in stale_slices:
357 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
360 def delete(self, commit = True):
362 Delete existing user.
366 keys = Keys(self.api, self['key_ids'])
368 key.delete(commit = False)
370 # Clean up miscellaneous join tables
371 for table in self.join_tables:
372 self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
373 (table, self['person_id']))
376 self['deleted'] = True
379 class Persons(Table):
381 Representation of row(s) from the persons table in the
385 def __init__(self, api, person_filter = None, columns = None):
386 Table.__init__(self, api, Person, columns)
388 view = "view_persons"
389 for tagname in self.tag_columns:
390 view= "%s left join %s using (%s)"%(view,Person.tagvalue_view_name(tagname),
393 sql = "SELECT %s FROM %s WHERE deleted IS False" % \
394 (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
396 if person_filter is not None:
397 if isinstance(person_filter, (list, tuple, set)):
398 # Separate the list into integers and strings
399 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
400 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
401 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
402 sql += " AND (%s) %s" % person_filter.sql(api, "OR")
403 elif isinstance(person_filter, dict):
404 person_filter = Filter(Person.fields, person_filter)
405 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
406 elif isinstance (person_filter, StringTypes):
407 person_filter = Filter(Person.fields, {'email':[person_filter]})
408 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
409 elif isinstance (person_filter, int):
410 person_filter = Filter(Person.fields, {'person_id':[person_filter]})
411 sql += " AND (%s) %s" % person_filter.sql(api, "AND")
413 raise PLCInvalidArgument, "Wrong person filter %r"%person_filter