""" CSC Member Management This module contains functions for registering new members, registering members for terms, searching for members, and other member-related functions. Transactions are used in each method that modifies the database. Future changes to the members database that need to be atomic must also be moved into this module. """ import re from csc.adm import terms from csc.backends import db, ldapi from csc.common import conf from csc.common.excep import InvalidArgument ### Configuration ### CONFIG_FILE = '/etc/csc/members.cf' cfg = {} def load_configuration(): """Load Members Configuration""" string_fields = [ 'studentid_regex', 'realname_regex', 'server', 'database', 'user', 'password', 'server_url', 'users_base', 'groups_base', 'admin_bind_dn', 'admin_bind_pw' ] # read configuration file cfg_tmp = conf.read(CONFIG_FILE) # verify configuration conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp) # update the current configuration with the loaded values cfg.update(cfg_tmp) ### Exceptions ### DBException = db.DBException ConfigurationException = conf.ConfigurationException class MemberException(Exception): """Base exception class for member-related errors.""" class DuplicateStudentID(MemberException): """Exception class for student ID conflicts.""" def __init__(self, studentid): self.studentid = studentid def __str__(self): return "Student ID already exists in the database: %s" % self.studentid class InvalidStudentID(MemberException): """Exception class for malformed student IDs.""" def __init__(self, studentid): self.studentid = studentid def __str__(self): return "Student ID is invalid: %s" % self.studentid class InvalidTerm(MemberException): """Exception class for malformed terms.""" def __init__(self, term): self.term = term def __str__(self): return "Term is invalid: %s" % self.term class InvalidRealName(MemberException): """Exception class for invalid real names.""" def __init__(self, name): self.name = name def __str__(self): return "Name is invalid: %s" % self.name class NoSuchMember(MemberException): """Exception class for nonexistent members.""" def __init__(self, memberid): self.memberid = memberid def __str__(self): return "Member not found: %d" % self.memberid ### Connection Management ### # global database connection db_connection = db.DBConnection() # global directory connection ldap_connection = ldapi.LDAPConnection() def connect(): """Connect to PostgreSQL.""" load_configuration() db_connection.connect(cfg['server'], cfg['database']) ldap_connection.connect(cfg['server_url'], cfg['admin_bind_dn'], cfg['admin_bind_pw'], cfg['users_base'], cfg['groups_base']) def disconnect(): """Disconnect from PostgreSQL.""" db_connection.disconnect() ldap_connection.disconnect() def connected(): """Determine whether the db_connection has been established.""" return db_connection.connected() and ldap_connection.connected() ### Member Table ### def new(uid, realname, studentid=None, program=None, mtype='user'): """ Registers a new CSC member. The member is added to the members table and registered for the current term. Parameters: uid - the initial user id realname - the full real name of the member studentid - the student id number of the member program - the program of study of the member mtype - a string describing the type of member ('user', 'club') Returns: the memberid of the new member Exceptions: DuplicateStudentID - if the student id already exists in the database InvalidStudentID - if the student id is malformed InvalidRealName - if the real name is malformed Example: new("Michael Spang", program="CS") -> 3349 """ # blank attributes should be NULL if studentid == '': studentid = None if program == '': program = None if uid == '': uid = None if mtype == '': mtype = None # check the student id format if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)): raise InvalidStudentID(studentid) # check real name format (UNIX account real names must not contain [,:=]) if not re.match(cfg['realname_regex'], realname): raise InvalidRealName(realname) # check for duplicate student id member = db_connection.select_member_by_studentid(studentid) or \ ldap_connection.member_search_studentid(studentid) if member: raise DuplicateStudentID(studentid) # check for duplicate userid member = db_connection.select_member_by_userid(uid) or \ ldap_connection.user_lookup(uid) if member: raise InvalidArgument("uid", uid, "duplicate uid") # add the member to the database memberid = db_connection.insert_member(realname, studentid, program, userid=uid) # add the member to the directory ldap_connection.member_add(uid, realname, studentid, program) # register them for this term in the database db_connection.insert_term(memberid, terms.current()) # register them for this term in the directory member = ldap_connection.member_lookup(uid) member['term'] = [ terms.current() ] ldap_connection.user_modify(uid, member) # commit the database transaction db_connection.commit() return memberid def get(memberid): """ Look up attributes of a member by memberid. Returns: a dictionary of attributes Example: get(3349) -> { 'memberid': 3349, 'name': 'Michael Spang', 'program': 'Computer Science', ... } """ return db_connection.select_member_by_id(memberid) def get_userid(userid): """ Look up attributes of a member by userid. Parameters: userid - the UNIX user id Returns: a dictionary of attributes Example: get('mspang') -> { 'memberid': 3349, 'name': 'Michael Spang', 'program': 'Computer Science', ... } """ return db_connection.select_member_by_userid(userid) def get_studentid(studentid): """ Look up attributes of a member by studnetid. Parameters: studentid - the student ID number Returns: a dictionary of attributes Example: get(...) -> { 'memberid': 3349, 'name': 'Michael Spang', 'program': 'Computer Science', ... } """ return db_connection.select_member_by_studentid(studentid) def list_term(term): """ Build a list of members in a term. Parameters: term - the term to match members against Returns: a list of member dictionaries Example: list_term('f2006'): -> [ { 'memberid': 3349, ... }, { 'memberid': ... }. ... ] """ # retrieve a list of memberids in term memberlist = db_connection.select_members_by_term(term) return memberlist.values() def list_name(name): """ Build a list of members with matching names. Parameters: name - the name to match members against Returns: a list of member dictionaries Example: list_name('Spang'): -> [ { 'memberid': 3349, ... }, { 'memberid': ... }, ... ] """ # retrieve a list of memberids matching name memberlist = db_connection.select_members_by_name(name) return memberlist.values() def list_all(): """ Builds a list of all members. Returns: a list of member dictionaries """ # retrieve a list of members memberlist = db_connection.select_all_members() return memberlist.values() def delete(memberid): """ Erase all records of a member. Note: real members are never removed from the database Returns: attributes and terms of the member in a tuple Exceptions: NoSuchMember - if the member id does not exist Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993']) """ # save member data member = db_connection.select_member_by_id(memberid) # bail if not found if not member: raise NoSuchMember(memberid) term_list = db_connection.select_terms(memberid) # remove data from the db db_connection.delete_term_all(memberid) db_connection.delete_member(memberid) db_connection.commit() # remove data from the directory if member and member['userid']: uid = member['userid'] ldap_connection.user_delete(uid) return (member, term_list) def update(member): """ Update CSC member attributes. Parameters: member - a dictionary with member attributes as returned by get, possibly omitting some attributes. member['memberid'] must exist and be valid. None is NULL. Exceptions: NoSuchMember - if the member id does not exist InvalidStudentID - if the student id number is malformed DuplicateStudentID - if the student id number exists Example: update( {'memberid': 3349, userid: 'mspang'} ) """ if member.has_key('studentid') and member['studentid'] is not None: studentid = member['studentid'] # check the student id format if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)): raise InvalidStudentID(studentid) # check for duplicate student id dupmember = db_connection.select_member_by_studentid(studentid) if dupmember: raise DuplicateStudentID(studentid) # not specifying memberid is a bug if not member.has_key('memberid'): raise Exception("no member specified in call to update") memberid = member['memberid'] # see if member exists if not get(memberid): raise NoSuchMember(memberid) # do the update db_connection.update_member(member) # commit the transaction db_connection.commit() ### Term Table ### def register(memberid, term_list): """ Registers a member for one or more terms. Parameters: memberid - the member id number term_list - the term to register for, or a list of terms Exceptions: InvalidTerm - if a term is malformed Example: register(3349, "w2007") Example: register(3349, ["w2007", "s2007"]) """ if type(term_list) in (str, unicode): term_list = [ term_list ] ldap_member = None db_member = get(memberid) if db_member['userid']: uid = db_member['userid'] ldap_member = ldap_connection.member_lookup(uid) if ldap_member and 'term' not in ldap_member: ldap_member['term'] = [] for term in term_list: # check term syntax if not re.match('^[wsf][0-9]{4}$', term): raise InvalidTerm(term) # add term to database db_connection.insert_term(memberid, term) # add the term to the directory if ldap_member: ldap_member['term'].append(term) if ldap_member: ldap_connection.user_modify(uid, ldap_member) db_connection.commit() def registered(memberid, term): """ Determines whether a member is registered for a term. Parameters: memberid - the member id number term - the term to check Returns: whether the member is registered Example: registered(3349, "f2006") -> True """ return db_connection.select_term(memberid, term) is not None def member_terms(memberid): """ Retrieves a list of terms a member is registered for. Parameters: memberid - the member id number Returns: list of term strings Example: registered(0) -> 's1993' """ terms_list = db_connection.select_terms(memberid) terms_list.sort(terms.compare) return terms_list ### Tests ### if __name__ == '__main__': from csc.common.test import * # t=test m=member s=student u=updated tmname = 'Test Member' tmuid = 'testmember' tmprogram = 'Metaphysics' tmsid = '00000000' tm2name = 'Test Member 2' tm2uid = 'testmember2' tm2sid = '00000001' tm2uname = 'Test Member II' tm2usid = '00000002' tm2uprogram = 'Pseudoscience' tmdict = {'name': tmname, 'userid': tmuid, 'program': tmprogram, 'type': 'user', 'studentid': tmsid } tm2dict = {'name': tm2name, 'userid': tm2uid, 'program': None, 'type': 'user', 'studentid': tm2sid } tm2udict = {'name': tm2uname, 'userid': tm2uid, 'program': tm2uprogram, 'type': 'user', 'studentid': tm2usid } thisterm = terms.current() nextterm = terms.next(thisterm) test(connect) connect() success() test(connected) assert_equal(True, connected()) success() dmid = get_studentid(tmsid) if dmid: delete(dmid['memberid']) dmid = get_studentid(tm2sid) if dmid: delete(dmid['memberid']) dmid = get_studentid(tm2usid) if dmid: delete(dmid['memberid']) test(new) tmid = new(tmuid, tmname, tmsid, tmprogram) tm2id = new(tm2uid, tm2name, tm2sid) success() tmdict['memberid'] = tmid tm2dict['memberid'] = tm2id tm2udict['memberid'] = tm2id test(registered) assert_equal(True, registered(tmid, thisterm)) assert_equal(True, registered(tm2id, thisterm)) assert_equal(False, registered(tmid, nextterm)) success() test(get) assert_equal(tmdict, get(tmid)) assert_equal(tm2dict, get(tm2id)) success() test(list_name) assert_equal(True, tmid in [ x['memberid'] for x in list_name(tmname) ]) assert_equal(True, tm2id in [ x['memberid'] for x in list_name(tm2name) ]) success() test(list_all) allmembers = list_all() assert_equal(True, tmid in [ x['memberid'] for x in allmembers ]) assert_equal(True, tm2id in [ x['memberid'] for x in allmembers ]) success() test(register) register(tmid, nextterm) assert_equal(True, registered(tmid, nextterm)) success() test(member_terms) assert_equal([thisterm, nextterm], member_terms(tmid)) assert_equal([thisterm], member_terms(tm2id)) success() test(list_term) assert_equal(True, tmid in [ x['memberid'] for x in list_term(thisterm) ]) assert_equal(True, tmid in [ x['memberid'] for x in list_term(nextterm) ]) assert_equal(True, tm2id in [ x['memberid'] for x in list_term(thisterm) ]) assert_equal(False, tm2id in [ x['memberid'] for x in list_term(nextterm) ]) success() test(update) update(tm2udict) assert_equal(tm2udict, get(tm2id)) success() test(get_userid) assert_equal(tm2udict, get_userid(tm2uid)) success() test(get_studentid) assert_equal(tm2udict, get_studentid(tm2usid)) assert_equal(tmdict, get_studentid(tmsid)) success() test(delete) delete(tmid) delete(tm2id) success() test(disconnect) disconnect() assert_equal(False, connected()) disconnect() success()