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")
87 email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
88 if not email_re.match(email):
91 # check only against users on the same peer
93 namespace_peer_id = self['peer_id']
95 namespace_peer_id = None
97 conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
99 for person in conflicts:
100 if 'person_id' not in self or self['person_id'] != person['person_id']:
101 raise PLCInvalidArgument, "E-mail address already in use"
105 def validate_password(self, password):
107 Encrypt password if necessary before committing to the
113 if len(password) > len(magic) and \
114 password[0:len(magic)] == magic:
117 # Generate a somewhat unique 8 character salt string
118 salt = str(time.time()) + str(Random().random())
119 salt = md5.md5(salt).hexdigest()[:8]
120 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
122 validate_date_created = Row.validate_timestamp
123 validate_last_updated = Row.validate_timestamp
124 validate_verification_expires = Row.validate_timestamp
126 def can_update(self, person):
128 Returns true if we can update the specified person. We can
131 1. We are the person.
133 3. We are a PI and the person is a user or tech or at
137 assert isinstance(person, Person)
139 if self['person_id'] == person['person_id']:
142 if 'admin' in self['roles']:
145 if 'pi' in self['roles']:
146 if set(self['site_ids']).intersection(person['site_ids']):
147 # Can update person is neither a PI or ADMIN
148 return (not (('pi' in person['roles']) or ('admin' in person['roles'])))
152 def can_view(self, person):
154 Returns true if we can view the specified person. We can
157 1. We are the person.
159 3. We are a PI and the person is at one of our sites.
162 assert isinstance(person, Person)
164 if self.can_update(person):
167 if 'pi' in self['roles']:
168 if set(self['site_ids']).intersection(person['site_ids']):
169 # Can view people with equal or higher role IDs
170 return 'admin' not in person['roles']
174 add_role = Row.add_object(Role, 'person_role')
175 remove_role = Row.remove_object(Role, 'person_role')
177 add_key = Row.add_object(Key, 'person_key')
178 remove_key = Row.remove_object(Key, 'person_key')
180 def set_primary_site(self, site, commit = True):
182 Set the primary site for an existing user.
185 assert 'person_id' in self
186 assert 'site_id' in site
188 person_id = self['person_id']
189 site_id = site['site_id']
190 self.api.db.do("UPDATE person_site SET is_primary = False" \
191 " WHERE person_id = %(person_id)d",
193 self.api.db.do("UPDATE person_site SET is_primary = True" \
194 " WHERE person_id = %(person_id)d" \
195 " AND site_id = %(site_id)d",
201 assert 'site_ids' in self
202 assert site_id in self['site_ids']
204 # Make sure that the primary site is first in the list
205 self['site_ids'].remove(site_id)
206 self['site_ids'].insert(0, site_id)
208 def update_last_updated(self, commit = True):
210 Update last_updated field with current time
213 assert 'person_id' in self
214 assert self.table_name
216 self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
217 " where person_id = %d" % (self['person_id']) )
220 def associate_roles(self, auth, field, value):
222 Adds roles found in value list to this person (using AddRoleToPerson).
223 Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
226 assert 'role_ids' in self
227 assert 'person_id' in self
228 assert isinstance(value, list)
230 (role_ids, role_names) = self.separate_types(value)[0:2]
232 # Translate roles into role_ids
234 roles = Roles(self.api, role_names).dict('role_id')
235 role_ids += roles.keys()
237 # Add new ids, remove stale ids
238 if self['role_ids'] != role_ids:
239 from PLC.Methods.AddRoleToPerson import AddRoleToPerson
240 from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
241 new_roles = set(role_ids).difference(self['role_ids'])
242 stale_roles = set(self['role_ids']).difference(role_ids)
244 for new_role in new_roles:
245 AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
246 for stale_role in stale_roles:
247 DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
250 def associate_sites(self, auth, field, value):
252 Adds person to sites found in value list (using AddPersonToSite).
253 Deletes person from site not found in value list (using DeletePersonFromSite).
256 from PLC.Sites import Sites
258 assert 'site_ids' in self
259 assert 'person_id' in self
260 assert isinstance(value, list)
262 (site_ids, site_names) = self.separate_types(value)[0:2]
264 # Translate roles into role_ids
266 sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
267 site_ids += sites.keys()
269 # Add new ids, remove stale ids
270 if self['site_ids'] != site_ids:
271 from PLC.Methods.AddPersonToSite import AddPersonToSite
272 from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
273 new_sites = set(site_ids).difference(self['site_ids'])
274 stale_sites = set(self['site_ids']).difference(site_ids)
276 for new_site in new_sites:
277 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
278 for stale_site in stale_sites:
279 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
282 def associate_keys(self, auth, field, value):
284 Deletes key_ids not found in value list (using DeleteKey).
285 Adds key if key_fields w/o key_id is found (using AddPersonKey).
286 Updates key if key_fields w/ key_id is found (using UpdateKey).
288 assert 'key_ids' in self
289 assert 'person_id' in self
290 assert isinstance(value, list)
292 (key_ids, blank, keys) = self.separate_types(value)
294 if self['key_ids'] != key_ids:
295 from PLC.Methods.DeleteKey import DeleteKey
296 stale_keys = set(self['key_ids']).difference(key_ids)
298 for stale_key in stale_keys:
299 DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
302 from PLC.Methods.AddPersonKey import AddPersonKey
303 from PLC.Methods.UpdateKey import UpdateKey
304 updated_keys = filter(lambda key: 'key_id' in key, keys)
305 added_keys = filter(lambda key: 'key_id' not in key, keys)
307 for key in added_keys:
308 AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
309 for key in updated_keys:
310 key_id = key.pop('key_id')
311 UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
314 def associate_slices(self, auth, field, value):
316 Adds person to slices found in value list (using AddPersonToSlice).
317 Deletes person from slices found in value list (using DeletePersonFromSlice).
320 from PLC.Slices import Slices
322 assert 'slice_ids' in self
323 assert 'person_id' in self
324 assert isinstance(value, list)
326 (slice_ids, slice_names) = self.separate_types(value)[0:2]
328 # Translate roles into role_ids
330 slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
331 slice_ids += slices.keys()
333 # Add new ids, remove stale ids
334 if self['slice_ids'] != slice_ids:
335 from PLC.Methods.AddPersonToSlice import AddPersonToSlice
336 from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
337 new_slices = set(slice_ids).difference(self['slice_ids'])
338 stale_slices = set(self['slice_ids']).difference(slice_ids)
340 for new_slice in new_slices:
341 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
342 for stale_slice in stale_slices:
343 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
346 def delete(self, commit = True):
348 Delete existing user.
352 keys = Keys(self.api, self['key_ids'])
354 key.delete(commit = False)
356 # Clean up miscellaneous join tables
357 for table in self.join_tables:
358 self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
359 (table, self['person_id']))
362 self['deleted'] = True
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):
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