remove simplejson dependency
[plcapi.git] / PLC / Persons.py
index 139deef..8276c06 100644 (file)
@@ -4,12 +4,12 @@
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2006 The Trustees of Princeton University
 #
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2006 The Trustees of Princeton University
 #
-# $Id: Persons.py,v 1.38 2007/08/22 19:54:07 tmack Exp $
-#
 
 from types import StringTypes
 
 from types import StringTypes
-from datetime import datetime
-import md5
+try:
+    from hashlib import md5
+except ImportError:
+    from md5 import md5
 import time
 from random import Random
 import re
 import time
 from random import Random
 import re
@@ -17,7 +17,7 @@ import crypt
 
 from PLC.Faults import *
 from PLC.Debug import log
 
 from PLC.Faults import *
 from PLC.Debug import log
-from PLC.Parameter import Parameter
+from PLC.Parameter import Parameter, Mixed
 from PLC.Filter import Filter
 from PLC.Table import Row, Table
 from PLC.Roles import Role, Roles
 from PLC.Filter import Filter
 from PLC.Table import Row, Table
 from PLC.Roles import Role, Roles
@@ -46,8 +46,8 @@ class Person(Row):
         'enabled': Parameter(bool, "Has been enabled"),
         'password': Parameter(str, "Account password in crypt() form", max = 254),
         'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
         'enabled': Parameter(bool, "Has been enabled"),
         'password': Parameter(str, "Account password in crypt() form", max = 254),
         'verification_key': Parameter(str, "Reset password key", max = 254, nullok = True),
-       'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
-       'last_updated': Parameter(int, "Date and time of last update", ro = True),
+        'verification_expires': Parameter(int, "Date and time when verification_key expires", nullok = True),
+        'last_updated': Parameter(int, "Date and time of last update", ro = True),
         'date_created': Parameter(int, "Date and time when account was created", ro = True),
         'role_ids': Parameter([int], "List of role identifiers"),
         'roles': Parameter([str], "List of roles"),
         'date_created': Parameter(int, "Date and time when account was created", ro = True),
         'role_ids': Parameter([int], "List of role identifiers"),
         'roles': Parameter([str], "List of roles"),
@@ -56,59 +56,46 @@ class Person(Row):
         'slice_ids': Parameter([int], "List of slice identifiers"),
         'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
         'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
         'slice_ids': Parameter([int], "List of slice identifiers"),
         'peer_id': Parameter(int, "Peer to which this user belongs", nullok = True),
         'peer_person_id': Parameter(int, "Foreign user identifier at peer", nullok = True),
+        'person_tag_ids' : Parameter ([int], "List of tags attached to this person"),
         }
         }
-
-    # for Cache
-    class_key = 'email'
-    foreign_fields = ['first_name', 'last_name', 'title', 'email', 'phone', 'url',
-                     'bio', 'enabled', 'password', ]
-    # forget about these ones, they are read-only anyway
-    # handling them causes Cache to re-sync all over again 
-    # 'last_updated', 'date_created'
-    foreign_xrefs = [
-        {'field' : 'key_ids',  'class': 'Key',  'table' : 'person_key' } ,
-        {'field' : 'site_ids', 'class': 'Site', 'table' : 'person_site'},
-#       xxx this is not handled by Cache yet
-#        'role_ids': Parameter([int], "List of role identifiers"),
-]
+    related_fields = {
+        'roles': [Mixed(Parameter(int, "Role identifier"),
+                        Parameter(str, "Role name"))],
+        'sites': [Mixed(Parameter(int, "Site identifier"),
+                        Parameter(str, "Site name"))],
+        'keys': [Mixed(Parameter(int, "Key identifier"),
+                       Filter(Key.fields))],
+        'slices': [Mixed(Parameter(int, "Slice identifier"),
+                         Parameter(str, "Slice name"))]
+        }
+    view_tags_name = "view_person_tags"
+    # tags are used by the Add/Get/Update methods to expose tags
+    # this is initialized here and updated by the accessors factory
+    tags = { }
 
     def validate_email(self, email):
         """
         Validate email address. Stolen from Mailman.
         """
 
     def validate_email(self, email):
         """
         Validate email address. Stolen from Mailman.
         """
-
-        invalid_email = PLCInvalidArgument("Invalid e-mail address")
-        email_badchars = r'[][()<>|;^,\200-\377]'
-
-        # Pretty minimal, cheesy check.  We could do better...
-        if not email or email.count(' ') > 0:
-            raise invalid_email
-        if re.search(email_badchars, email) or email[0] == '-':
-            raise invalid_email
-
         email = email.lower()
         email = email.lower()
-        at_sign = email.find('@')
-        if at_sign < 1:
-            raise invalid_email
-        user = email[:at_sign]
-        rest = email[at_sign+1:]
-        domain = rest.split('.')
+        invalid_email = PLCInvalidArgument("Invalid e-mail address %s"%email)
 
 
-        # This means local, unqualified addresses, are not allowed
-        if not domain:
+        if not email:
             raise invalid_email
             raise invalid_email
-        if len(domain) < 2:
+
+        email_re = re.compile('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z')
+        if not email_re.match(email):
             raise invalid_email
 
             raise invalid_email
 
-               # check only against users on the same peer  
-       if 'peer_id' in self:
+        # check only against users on the same peer
+        if 'peer_id' in self:
             namespace_peer_id = self['peer_id']
         else:
             namespace_peer_id = None
             namespace_peer_id = self['peer_id']
         else:
             namespace_peer_id = None
-         
-       conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id}) 
-       
-       for person in conflicts:
+
+        conflicts = Persons(self.api, {'email':email,'peer_id':namespace_peer_id})
+
+        for person in conflicts:
             if 'person_id' not in self or self['person_id'] != person['person_id']:
                 raise PLCInvalidArgument, "E-mail address already in use"
 
             if 'person_id' not in self or self['person_id'] != person['person_id']:
                 raise PLCInvalidArgument, "E-mail address already in use"
 
@@ -128,7 +115,7 @@ class Person(Row):
         else:
             # Generate a somewhat unique 8 character salt string
             salt = str(time.time()) + str(Random().random())
         else:
             # Generate a somewhat unique 8 character salt string
             salt = str(time.time()) + str(Random().random())
-            salt = md5.md5(salt).hexdigest()[:8] 
+            salt = md5(salt).hexdigest()[:8]
             return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
 
     validate_date_created = Row.validate_timestamp
             return crypt.crypt(password.encode(self.api.encoding), magic + salt + "$")
 
     validate_date_created = Row.validate_timestamp
@@ -156,8 +143,8 @@ class Person(Row):
 
         if 'pi' in self['roles']:
             if set(self['site_ids']).intersection(person['site_ids']):
 
         if 'pi' in self['roles']:
             if set(self['site_ids']).intersection(person['site_ids']):
-                # Can update people with higher role IDs
-                return min(self['role_ids']) < min(person['role_ids'])
+                # non-admin users cannot update a person who is neither a PI or ADMIN
+                return (not set(['pi','admin']).intersection(person['roles']))
 
         return False
 
 
         return False
 
@@ -168,7 +155,7 @@ class Person(Row):
 
         1. We are the person.
         2. We are an admin.
 
         1. We are the person.
         2. We are an admin.
-        3. We are a PI and the person is at one of our sites.
+        3. We are a PI or Tech and the person is at one of our sites.
         """
 
         assert isinstance(person, Person)
         """
 
         assert isinstance(person, Person)
@@ -176,10 +163,10 @@ class Person(Row):
         if self.can_update(person):
             return True
 
         if self.can_update(person):
             return True
 
-        if 'pi' in self['roles']:
+        # pis and techs can see all people on their site
+        if set(['pi','tech']).intersection(self['roles']):
             if set(self['site_ids']).intersection(person['site_ids']):
             if set(self['site_ids']).intersection(person['site_ids']):
-                # Can view people with equal or higher role IDs
-                return min(self['role_ids']) <= min(person['role_ids'])
+                return True
 
         return False
 
 
         return False
 
@@ -221,14 +208,140 @@ class Person(Row):
         """
         Update last_updated field with current time
         """
         """
         Update last_updated field with current time
         """
-       
-       assert 'person_id' in self
-       assert self.table_name
-       
-       self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
+
+        assert 'person_id' in self
+        assert self.table_name
+
+        self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
                        " where person_id = %d" % (self['person_id']) )
         self.sync(commit)
 
                        " where person_id = %d" % (self['person_id']) )
         self.sync(commit)
 
+    def associate_roles(self, auth, field, value):
+        """
+        Adds roles found in value list to this person (using AddRoleToPerson).
+        Deletes roles not found in value list from this person (using DeleteRoleFromPerson).
+        """
+
+        assert 'role_ids' in self
+        assert 'person_id' in self
+        assert isinstance(value, list)
+
+        (role_ids, role_names) = self.separate_types(value)[0:2]
+
+        # Translate roles into role_ids
+        if role_names:
+            roles = Roles(self.api, role_names).dict('role_id')
+            role_ids += roles.keys()
+
+        # Add new ids, remove stale ids
+        if self['role_ids'] != role_ids:
+            from PLC.Methods.AddRoleToPerson import AddRoleToPerson
+            from PLC.Methods.DeleteRoleFromPerson import DeleteRoleFromPerson
+            new_roles = set(role_ids).difference(self['role_ids'])
+            stale_roles = set(self['role_ids']).difference(role_ids)
+
+            for new_role in new_roles:
+                AddRoleToPerson.__call__(AddRoleToPerson(self.api), auth, new_role, self['person_id'])
+            for stale_role in stale_roles:
+                DeleteRoleFromPerson.__call__(DeleteRoleFromPerson(self.api), auth, stale_role, self['person_id'])
+
+
+    def associate_sites(self, auth, field, value):
+        """
+        Adds person to sites found in value list (using AddPersonToSite).
+        Deletes person from site not found in value list (using DeletePersonFromSite).
+        """
+
+        from PLC.Sites import Sites
+
+        assert 'site_ids' in self
+        assert 'person_id' in self
+        assert isinstance(value, list)
+
+        (site_ids, site_names) = self.separate_types(value)[0:2]
+
+        # Translate roles into role_ids
+        if site_names:
+            sites = Sites(self.api, site_names, ['site_id']).dict('site_id')
+            site_ids += sites.keys()
+
+        # Add new ids, remove stale ids
+        if self['site_ids'] != site_ids:
+            from PLC.Methods.AddPersonToSite import AddPersonToSite
+            from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
+            new_sites = set(site_ids).difference(self['site_ids'])
+            stale_sites = set(self['site_ids']).difference(site_ids)
+
+            for new_site in new_sites:
+                AddPersonToSite.__call__(AddPersonToSite(self.api), auth, self['person_id'], new_site)
+            for stale_site in stale_sites:
+                DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, self['person_id'], stale_site)
+
+
+    def associate_keys(self, auth, field, value):
+        """
+        Deletes key_ids not found in value list (using DeleteKey).
+        Adds key if key_fields w/o key_id is found (using AddPersonKey).
+        Updates key if key_fields w/ key_id is found (using UpdateKey).
+        """
+        assert 'key_ids' in self
+        assert 'person_id' in self
+        assert isinstance(value, list)
+
+        (key_ids, blank, keys) = self.separate_types(value)
+
+        if self['key_ids'] != key_ids:
+            from PLC.Methods.DeleteKey import DeleteKey
+            stale_keys = set(self['key_ids']).difference(key_ids)
+
+            for stale_key in stale_keys:
+                DeleteKey.__call__(DeleteKey(self.api), auth, stale_key)
+
+        if keys:
+            from PLC.Methods.AddPersonKey import AddPersonKey
+            from PLC.Methods.UpdateKey import UpdateKey
+            updated_keys = filter(lambda key: 'key_id' in key, keys)
+            added_keys = filter(lambda key: 'key_id' not in key, keys)
+
+            for key in added_keys:
+                AddPersonKey.__call__(AddPersonKey(self.api), auth, self['person_id'], key)
+            for key in updated_keys:
+                key_id = key.pop('key_id')
+                UpdateKey.__call__(UpdateKey(self.api), auth, key_id, key)
+
+
+    def associate_slices(self, auth, field, value):
+        """
+        Adds person to slices found in value list (using AddPersonToSlice).
+        Deletes person from slices found in value list (using DeletePersonFromSlice).
+        """
+
+        from PLC.Slices import Slices
+
+        assert 'slice_ids' in self
+        assert 'person_id' in self
+        assert isinstance(value, list)
+
+        (slice_ids, slice_names) = self.separate_types(value)[0:2]
+
+        # Translate roles into role_ids
+        if slice_names:
+            slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
+            slice_ids += slices.keys()
+
+        # Add new ids, remove stale ids
+        if self['slice_ids'] != slice_ids:
+            from PLC.Methods.AddPersonToSlice import AddPersonToSlice
+            from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
+            new_slices = set(slice_ids).difference(self['slice_ids'])
+            stale_slices = set(self['slice_ids']).difference(slice_ids)
+
+            for new_slice in new_slices:
+                AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, self['person_id'], new_slice)
+            for stale_slice in stale_slices:
+                DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, self['person_id'], stale_slice)
+
+
     def delete(self, commit = True):
         """
         Delete existing user.
     def delete(self, commit = True):
         """
         Delete existing user.
@@ -246,7 +359,14 @@ class Person(Row):
 
         # Mark as deleted
         self['deleted'] = True
 
         # Mark as deleted
         self['deleted'] = True
-        self.sync(commit)
+
+        # delete will fail if timestamp fields aren't validated, so lets remove them
+        for field in ['verification_expires', 'date_created', 'last_updated']:
+            if field in self:
+                self.pop(field)
+
+        # don't validate, so duplicates can be consistently removed
+        self.sync(commit, validate=False)
 
 class Persons(Table):
     """
 
 class Persons(Table):
     """
@@ -256,108 +376,33 @@ class Persons(Table):
 
     def __init__(self, api, person_filter = None, columns = None):
         Table.__init__(self, api, Person, columns)
 
     def __init__(self, api, person_filter = None, columns = None):
         Table.__init__(self, api, Person, columns)
-        #sql = "SELECT %s FROM view_persons WHERE deleted IS False" % \
-        #      ", ".join(self.columns)
-       foreign_fields = {'role_ids': ('role_id', 'person_role'),
-                         'roles': ('name', 'roles'),
-                          'site_ids': ('site_id', 'person_site'),
-                          'key_ids': ('key_id', 'person_key'),
-                          'slice_ids': ('slice_id', 'slice_person')
-                          }
-       foreign_keys = {}
-       db_fields = filter(lambda field: field not in foreign_fields.keys(), Person.fields.keys())
-       all_fields = db_fields + [value[0] for value in foreign_fields.values()]
-       fields = []
-       _select = "SELECT "
-       _from = " FROM persons "
-       _join = " LEFT JOIN peer_person USING (person_id) "  
-       _where = " WHERE deleted IS False "
-
-       if not columns:
-           # include all columns       
-           fields = all_fields
-           tables = [value[1] for value in foreign_fields.values()]
-           tables.sort()
-           for key in foreign_fields.keys():
-               foreign_keys[foreign_fields[key][0]] = key  
-           for table in tables:
-               if table in ['roles']:
-                   _join += " LEFT JOIN roles USING(role_id) "
-               else:   
-                   _join += " LEFT JOIN %s USING (person_id) " % (table)
-       else: 
-           tables = set()
-           columns = filter(lambda column: column in db_fields+foreign_fields.keys(), columns)
-           columns.sort()
-           for column in columns: 
-               if column in foreign_fields.keys():
-                   (field, table) = foreign_fields[column]
-                   foreign_keys[field] = column
-                   fields += [field]
-                   tables.add(table)
-                   if column in ['roles']:
-                       _join += " LEFT JOIN roles USING(role_id) "
-                   else:
-                       _join += " LEFT JOIN %s USING (person_id)" % \
-                               (foreign_fields[column][1])
-               
-               else:
-                   fields += [column]  
-       
-       # postgres will return timestamps as datetime objects. 
-       # XMLPRC cannot marshal datetime so convert to int
-       timestamps = ['date_created', 'last_updated', 'verification_expires']
-       for field in fields:
-           if field in timestamps:
-               fields[fields.index(field)] = \
-                "CAST(date_part('epoch', %s) AS bigint) AS %s" % (field, field)
-
-       _select += ", ".join(fields)
-       sql = _select + _from + _join + _where
-
-       # deal with filter                      
+
+        view = "view_persons"
+        for tagname in self.tag_columns:
+            view= "%s left join %s using (%s)"%(view,Person.tagvalue_view_name(tagname),
+                                                Person.primary_key)
+
+        sql = "SELECT %s FROM %s WHERE deleted IS False" % \
+            (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
+
         if person_filter is not None:
             if isinstance(person_filter, (list, tuple, set)):
                 # Separate the list into integers and strings
                 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
                 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
                 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
         if person_filter is not None:
             if isinstance(person_filter, (list, tuple, set)):
                 # Separate the list into integers and strings
                 ints = filter(lambda x: isinstance(x, (int, long)), person_filter)
                 strs = filter(lambda x: isinstance(x, StringTypes), person_filter)
                 person_filter = Filter(Person.fields, {'person_id': ints, 'email': strs})
-                sql += " AND (%s)" % person_filter.sql(api, "OR")
+                sql += " AND (%s) %s" % person_filter.sql(api, "OR")
             elif isinstance(person_filter, dict):
             elif isinstance(person_filter, dict):
-                person_filter = Filter(Person.fields, person_filter)
-                sql += " AND (%s)" % person_filter.sql(api, "AND")
-           elif isinstance (person_filter, StringTypes):
-                person_filter = Filter(Person.fields, {'email':[person_filter]})
-                sql += " AND (%s)" % person_filter.sql(api, "AND")
-            elif isinstance (person_filter, int):
-                person_filter = Filter(Person.fields, {'person_id':[person_filter]})
-                sql += " AND (%s)" % person_filter.sql(api, "AND")
-           else:
-                raise PLCInvalidArgument, "Wrong person filter %r"%person_filter           
-
-       # aggregate data
-       all_persons = {}
-       for row in self.api.db.selectall(sql):
-           person_id = row['person_id']
-
-           if all_persons.has_key(person_id):
-               for (key, key_list) in foreign_keys.items():
-                   data = row.pop(key)
-                   row[key_list] = [data]
-                   if data and data not in all_persons[person_id][key_list]:
-                       all_persons[person_id][key_list].append(data)
+                allowed_fields=dict(Person.fields.items()+Person.tags.items())
+                person_filter = Filter(allowed_fields, person_filter)
+                sql += " AND (%s) %s" % person_filter.sql(api, "AND")
+            elif isinstance (person_filter, StringTypes):
+                person_filter = Filter(Person.fields, {'email':person_filter})
+                sql += " AND (%s) %s" % person_filter.sql(api, "AND")
+            elif isinstance (person_filter, (int, long)):
+                person_filter = Filter(Person.fields, {'person_id':person_filter})
+                sql += " AND (%s) %s" % person_filter.sql(api, "AND")
             else:
             else:
-               for key in foreign_keys.keys():
-                    value = row.pop(key)
-                   if value:   
-                       row[foreign_keys[key]] = [value]
-                   else:
-                       row[foreign_keys[key]] = []
-               if row: 
-                   all_persons[person_id] = row
-               
-       # populate self
-       for row in all_persons.values():
-           obj = self.classobj(self.api, row)
-            self.append(obj)
+                raise PLCInvalidArgument, "Wrong person filter %r"%person_filter
 
 
+        self.selectall(sql)