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
7 # $Id: Persons.py,v 1.32 2007/01/11 05:37:55 mlhuang Exp $
10 from types import StringTypes
11 from datetime import datetime
14 from random import Random
18 from PLC.Faults import *
19 from PLC.Debug import log
20 from PLC.Parameter import Parameter
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),
63 foreign_fields = ['first_name', 'last_name', 'title', 'email', 'phone', 'url',
64 'bio', 'enabled', 'password', ]
65 # forget about these ones, they are read-only anyway
66 # handling them causes Cache to re-sync all over again
67 # 'last_updated', 'date_created'
69 {'field' : 'key_ids', 'class': 'Key', 'table' : 'person_key' } ,
70 {'field' : 'site_ids', 'class': 'Site', 'table' : 'person_site'},
71 # xxx this is not handled by Cache yet
72 # 'role_ids': Parameter([int], "List of role identifiers"),
75 def validate_email(self, email):
77 Validate email address. Stolen from Mailman.
80 invalid_email = PLCInvalidArgument("Invalid e-mail address")
81 email_badchars = r'[][()<>|;^,\200-\377]'
83 # Pretty minimal, cheesy check. We could do better...
84 if not email or email.count(' ') > 0:
86 if re.search(email_badchars, email) or email[0] == '-':
90 at_sign = email.find('@')
93 user = email[:at_sign]
94 rest = email[at_sign+1:]
95 domain = rest.split('.')
97 # This means local, unqualified addresses, are not allowed
103 conflicts = Persons(self.api, [email])
104 for person in conflicts:
105 if 'person_id' not in self or self['person_id'] != person['person_id']:
106 raise PLCInvalidArgument, "E-mail address already in use"
110 def validate_password(self, password):
112 Encrypt password if necessary before committing to the
118 if len(password) > len(magic) and \
119 password[0:len(magic)] == magic:
122 # Generate a somewhat unique 8 character salt string
123 salt = str(time.time()) + str(Random().random())
124 salt = md5.md5(salt).hexdigest()[:8]
125 return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
127 validate_date_created = Row.validate_timestamp
128 validate_last_updated = Row.validate_timestamp
129 validate_verification_expires = Row.validate_timestamp
131 def can_update(self, person):
133 Returns true if we can update the specified person. We can
136 1. We are the person.
138 3. We are a PI and the person is a user or tech or at
142 assert isinstance(person, Person)
144 if self['person_id'] == person['person_id']:
147 if 'admin' in self['roles']:
150 if 'pi' in self['roles']:
151 if set(self['site_ids']).intersection(person['site_ids']):
152 # Can update people with higher role IDs
153 return min(self['role_ids']) < min(person['role_ids'])
157 def can_view(self, person):
159 Returns true if we can view the specified person. We can
162 1. We are the person.
164 3. We are a PI and the person is at one of our sites.
167 assert isinstance(person, Person)
169 if self.can_update(person):
172 if 'pi' in self['roles']:
173 if set(self['site_ids']).intersection(person['site_ids']):
174 # Can view people with equal or higher role IDs
175 return min(self['role_ids']) <= min(person['role_ids'])
179 add_role = Row.add_object(Role, 'person_role')
180 remove_role = Row.remove_object(Role, 'person_role')
182 add_key = Row.add_object(Key, 'person_key')
183 remove_key = Row.remove_object(Key, 'person_key')
185 def set_primary_site(self, site, commit = True):
187 Set the primary site for an existing user.
190 assert 'person_id' in self
191 assert 'site_id' in site
193 person_id = self['person_id']
194 site_id = site['site_id']
195 self.api.db.do("UPDATE person_site SET is_primary = False" \
196 " WHERE person_id = %(person_id)d",
198 self.api.db.do("UPDATE person_site SET is_primary = True" \
199 " WHERE person_id = %(person_id)d" \
200 " AND site_id = %(site_id)d",
206 assert 'site_ids' in self
207 assert site_id in self['site_ids']
209 # Make sure that the primary site is first in the list
210 self['site_ids'].remove(site_id)
211 self['site_ids'].insert(0, site_id)
213 def delete(self, commit = True):
215 Delete existing user.
219 keys = Keys(self.api, self['key_ids'])
221 key.delete(commit = False)
223 # Clean up miscellaneous join tables
224 for table in self.join_tables:
225 self.api.db.do("DELETE FROM %s WHERE person_id = %d" % \
226 (table, self['person_id']))
229 self['deleted'] = True
232 class Persons(Table):
234 Representation of row(s) from the persons table in the
238 def __init__(self, api, person_filter = None, columns = None):
239 Table.__init__(self, api, Person, columns)
241 sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
242 ", ".join(self.columns)
244 if person_filter is not None:
245 if isinstance(person_filter, (list, tuple, set)):
246 # Separate the list into integers and strings
247 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
248 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
249 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
250 sql += " AND (%s)" % person_filter.sql(api, "OR")
251 elif isinstance(person_filter, dict):
252 person_filter = Filter(Person.fields, person_filter)
253 sql += " AND (%s)" % person_filter.sql(api, "AND")