pyceo/pylib/csc/adm/members.py

525 lines
13 KiB
Python

"""
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
from csc.common import conf
### Configuration ###
CONFIG_FILE = '/etc/csc/members.cf'
cfg = {}
def load_configuration():
"""Load Members Configuration"""
string_fields = [ 'studentid_regex', 'realname_regex', 'server',
'database', 'user', 'password' ]
# 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
connection = db.DBConnection()
def connect():
"""Connect to PostgreSQL."""
load_configuration()
connection.connect(cfg['server'], cfg['database'])
def disconnect():
"""Disconnect from PostgreSQL."""
connection.disconnect()
def connected():
"""Determine whether the connection has been established."""
return connection.connected()
### Member Table ###
def new(realname, studentid=None, program=None, mtype='user', userid=None):
"""
Registers a new CSC member. The member is added to the members table
and registered for the current term.
Parameters:
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')
userid - the initial user id
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 userid == '': userid = 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 = connection.select_member_by_studentid(studentid)
if member:
raise DuplicateStudentID(studentid)
# add the member
memberid = connection.insert_member(realname, studentid, program)
# register them for this term
connection.insert_term(memberid, terms.current())
# commit the transaction
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 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 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 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 = connection.select_members_by_term(term)
# convert the list of memberids to a list of dictionaries
memberlist = map(connection.select_member_by_id, memberlist)
return memberlist
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 = connection.select_members_by_name(name)
# convert the list of memberids to a list of dictionaries
memberlist = map(connection.select_member_by_id, memberlist)
return memberlist
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 = connection.select_member_by_id(memberid)
# bail if not found
if not member:
raise NoSuchMember(memberid)
term_list = connection.select_terms(memberid)
# remove data from the db
connection.delete_term_all(memberid)
connection.delete_member(memberid)
connection.commit()
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 = 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
connection.update_member(member)
# commit the transaction
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 ]
for term in term_list:
# check term syntax
if not re.match('^[wsf][0-9]{4}$', term):
raise InvalidTerm(term)
# add term to database
connection.insert_term(memberid, term)
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 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 = 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'
tmprogram = 'Metaphysics'
tmsid = '00000000'
tm2name = 'Test Member 2'
tm2sid = '00000001'
tm2uname = 'Test Member II'
tm2usid = '00000002'
tm2uprogram = 'Pseudoscience'
tm2uuserid = 'testmember'
tmdict = {'name': tmname, 'userid': None, 'program': tmprogram, 'type': 'user', 'studentid': tmsid }
tm2dict = {'name': tm2name, 'userid': None, 'program': None, 'type': 'user', 'studentid': tm2sid }
tm2udict = {'name': tm2uname, 'userid': tm2uuserid, '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(tmname, tmsid, tmprogram)
tm2id = new(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(register)
register(tmid, terms.next(terms.current()))
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(tm2uuserid))
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()