New release (version 0.2).
[public/pyceo-broken.git] / pylib / csc / adm / members.py
1 """
2 CSC Member Management
3
4 This module contains functions for registering new members, registering
5 members for terms, searching for members, and other member-related
6 functions.
7
8 Transactions are used in each method that modifies the database. 
9 Future changes to the members database that need to be atomic
10 must also be moved into this module.
11 """
12 import re
13 from csc.adm import terms
14 from csc.backends import db
15 from csc.common import conf
16
17
18 ### Configuration ###
19
20 CONFIG_FILE = '/etc/csc/members.cf'
21
22 cfg = {}
23
24 def load_configuration():
25     """Load Members Configuration"""
26
27     string_fields = [ 'studentid_regex', 'realname_regex', 'server',
28             'database', 'user', 'password' ]
29
30     # read configuration file
31     cfg_tmp = conf.read(CONFIG_FILE)
32
33     # verify configuration
34     conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
35
36     # update the current configuration with the loaded values
37     cfg.update(cfg_tmp)
38
39
40
41 ### Exceptions ###
42
43 DBException = db.DBException
44 ConfigurationException = conf.ConfigurationException
45
46 class MemberException(Exception):
47     """Base exception class for member-related errors."""
48
49 class DuplicateStudentID(MemberException):
50     """Exception class for student ID conflicts."""
51     def __init__(self, studentid):
52         self.studentid = studentid
53     def __str__(self):
54         return "Student ID already exists in the database: %s" % self.studentid
55
56 class InvalidStudentID(MemberException):
57     """Exception class for malformed student IDs."""
58     def __init__(self, studentid):
59         self.studentid = studentid
60     def __str__(self):
61         return "Student ID is invalid: %s" % self.studentid
62
63 class InvalidTerm(MemberException):
64     """Exception class for malformed terms."""
65     def __init__(self, term):
66         self.term = term
67     def __str__(self):
68         return "Term is invalid: %s" % self.term
69
70 class InvalidRealName(MemberException):
71     """Exception class for invalid real names."""
72     def __init__(self, name):
73         self.name = name
74     def __str__(self):
75         return "Name is invalid: %s" % self.name
76
77 class NoSuchMember(MemberException):
78     """Exception class for nonexistent members."""
79     def __init__(self, memberid):
80         self.memberid = memberid
81     def __str__(self):
82         return "Member not found: %d" % self.memberid
83
84
85
86 ### Connection Management ###
87
88 # global database connection
89 connection = db.DBConnection()
90
91 def connect():
92     """Connect to PostgreSQL."""
93     
94     load_configuration()
95     connection.connect(cfg['server'], cfg['database'])
96        
97
98 def disconnect():
99     """Disconnect from PostgreSQL."""
100     
101     connection.disconnect()
102
103
104 def connected():
105     """Determine whether the connection has been established."""
106
107     return connection.connected()
108
109
110
111 ### Member Table ###
112
113 def new(realname, studentid=None, program=None, mtype='user', userid=None):
114     """
115     Registers a new CSC member. The member is added to the members table
116     and registered for the current term.
117
118     Parameters:
119         realname  - the full real name of the member
120         studentid - the student id number of the member
121         program   - the program of study of the member
122         mtype     - a string describing the type of member ('user', 'club')
123         userid    - the initial user id
124
125     Returns: the memberid of the new member
126
127     Exceptions:
128         DuplicateStudentID - if the student id already exists in the database
129         InvalidStudentID   - if the student id is malformed
130         InvalidRealName    - if the real name is malformed
131
132     Example: new("Michael Spang", program="CS") -> 3349
133     """
134
135     # blank attributes should be NULL
136     if studentid == '': studentid = None
137     if program == '': program = None
138     if userid == '': userid = None
139     if mtype == '': mtype = None
140
141     # check the student id format
142     if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)):
143         raise InvalidStudentID(studentid)
144
145     # check real name format (UNIX account real names must not contain [,:=])
146     if not re.match(cfg['realname_regex'], realname):
147         raise InvalidRealName(realname)
148
149     # check for duplicate student id
150     member = connection.select_member_by_studentid(studentid)
151     if member:
152         raise DuplicateStudentID(studentid)
153
154     # add the member
155     memberid = connection.insert_member(realname, studentid, program)
156
157     # register them for this term
158     connection.insert_term(memberid, terms.current())
159
160     # commit the transaction
161     connection.commit()
162
163     return memberid
164
165
166 def get(memberid):
167     """
168     Look up attributes of a member by memberid.
169
170     Returns: a dictionary of attributes
171
172     Example: get(3349) -> {
173                  'memberid': 3349,
174                  'name': 'Michael Spang',
175                  'program': 'Computer Science',
176                  ...
177              }
178     """
179
180     return connection.select_member_by_id(memberid)
181
182
183 def get_userid(userid):
184     """
185     Look up attributes of a member by userid.
186
187     Parameters:
188         userid - the UNIX user id
189
190     Returns: a dictionary of attributes
191
192     Example: get('mspang') -> {
193                  'memberid': 3349,
194                  'name': 'Michael Spang',
195                  'program': 'Computer Science',
196                  ...
197              }
198     """
199
200     return connection.select_member_by_userid(userid)
201
202
203 def get_studentid(studentid):
204     """
205     Look up attributes of a member by studnetid.
206
207     Parameters:
208         studentid - the student ID number
209
210     Returns: a dictionary of attributes
211     
212     Example: get(...) -> {
213                  'memberid': 3349,
214                  'name': 'Michael Spang',
215                  'program': 'Computer Science',
216                  ...
217              }
218     """
219
220     return connection.select_member_by_studentid(studentid)
221
222
223 def list_term(term):
224     """
225     Build a list of members in a term.
226
227     Parameters:
228         term - the term to match members against
229
230     Returns: a list of member dictionaries
231
232     Example: list_term('f2006'): -> [
233                  { 'memberid': 3349, ... },
234                  { 'memberid': ... }.
235                  ...
236              ]
237     """
238
239     # retrieve a list of memberids in term
240     memberlist = connection.select_members_by_term(term)
241
242     # convert the list of memberids to a list of dictionaries
243     memberlist = map(connection.select_member_by_id, memberlist)
244
245     return memberlist
246         
247
248 def list_name(name):
249     """
250     Build a list of members with matching names.
251
252     Parameters:
253         name - the name to match members against
254
255     Returns: a list of member dictionaries
256
257     Example: list_name('Spang'): -> [
258                  { 'memberid': 3349, ... },
259                  { 'memberid': ... },
260                  ...
261              ]
262     """
263
264     # retrieve a list of memberids matching name
265     memberlist = connection.select_members_by_name(name)
266
267     # convert the list of memberids to a list of dictionaries
268     memberlist = map(connection.select_member_by_id, memberlist)
269
270     return memberlist
271
272
273 def delete(memberid):
274     """
275     Erase all records of a member.
276
277     Note: real members are never removed from the database
278
279     Returns: attributes and terms of the member in a tuple
280
281     Exceptions:
282         NoSuchMember - if the member id does not exist
283
284     Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993'])
285     """
286
287     # save member data
288     member = connection.select_member_by_id(memberid)
289
290     # bail if not found
291     if not member:
292         raise NoSuchMember(memberid)
293
294     term_list = connection.select_terms(memberid)
295
296     # remove data from the db
297     connection.delete_term_all(memberid)
298     connection.delete_member(memberid)
299     connection.commit()
300
301     return (member, term_list)
302
303
304 def update(member):
305     """
306     Update CSC member attributes.
307
308     Parameters:
309         member - a dictionary with member attributes as returned by get,
310                  possibly omitting some attributes. member['memberid']
311                  must exist and be valid. None is NULL.
312
313     Exceptions:
314         NoSuchMember       - if the member id does not exist
315         InvalidStudentID   - if the student id number is malformed
316         DuplicateStudentID - if the student id number exists 
317
318     Example: update( {'memberid': 3349, userid: 'mspang'} )
319     """
320
321     if member.has_key('studentid') and member['studentid'] is not None:
322
323         studentid = member['studentid']
324         
325         # check the student id format
326         if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)):
327             raise InvalidStudentID(studentid)
328
329         # check for duplicate student id
330         dupmember = connection.select_member_by_studentid(studentid)
331         if dupmember:
332             raise DuplicateStudentID(studentid)
333
334     # not specifying memberid is a bug
335     if not member.has_key('memberid'):
336         raise Exception("no member specified in call to update")
337     memberid = member['memberid']
338
339     # see if member exists
340     if not get(memberid):
341         raise NoSuchMember(memberid)
342     
343     # do the update
344     connection.update_member(member)
345
346     # commit the transaction
347     connection.commit()
348
349
350
351 ### Term Table ###
352
353 def register(memberid, term_list):
354     """
355     Registers a member for one or more terms.
356
357     Parameters:
358         memberid  - the member id number
359         term_list - the term to register for, or a list of terms
360
361     Exceptions:
362         InvalidTerm - if a term is malformed
363
364     Example: register(3349, "w2007")
365
366     Example: register(3349, ["w2007", "s2007"])
367     """
368
369     if type(term_list) in (str, unicode):
370         term_list = [ term_list ]
371
372     for term in term_list:
373         
374         # check term syntax
375         if not re.match('^[wsf][0-9]{4}$', term):
376             raise InvalidTerm(term)
377     
378         # add term to database
379         connection.insert_term(memberid, term)
380
381     connection.commit()
382
383
384 def registered(memberid, term):
385     """
386     Determines whether a member is registered
387     for a term.
388
389     Parameters:
390         memberid - the member id number
391         term     - the term to check
392
393     Returns: whether the member is registered
394
395     Example: registered(3349, "f2006") -> True
396     """
397
398     return connection.select_term(memberid, term) is not None
399
400
401 def member_terms(memberid):
402     """
403     Retrieves a list of terms a member is
404     registered for.
405
406     Parameters:
407         memberid - the member id number
408
409     Returns: list of term strings
410
411     Example: registered(0) -> 's1993'
412     """
413
414     terms_list = connection.select_terms(memberid)
415     terms_list.sort(terms.compare)
416     return terms_list
417
418
419
420 ### Tests ###
421
422 if __name__ == '__main__':
423
424     from csc.common.test import *
425
426     # t=test m=member s=student u=updated
427     tmname = 'Test Member'
428     tmprogram = 'Metaphysics'
429     tmsid = '00000000'
430     tm2name = 'Test Member 2'
431     tm2sid = '00000001'
432     tm2uname = 'Test Member II'
433     tm2usid = '00000002'
434     tm2uprogram = 'Pseudoscience'
435     tm2uuserid = 'testmember'
436
437     tmdict = {'name': tmname, 'userid': None, 'program': tmprogram, 'type': 'user', 'studentid': tmsid }
438     tm2dict = {'name': tm2name, 'userid': None, 'program': None, 'type': 'user', 'studentid': tm2sid }
439     tm2udict = {'name': tm2uname, 'userid': tm2uuserid, 'program': tm2uprogram, 'type': 'user', 'studentid': tm2usid }
440
441     thisterm = terms.current()
442     nextterm = terms.next(thisterm)
443
444     test(connect)
445     connect()
446     success()
447
448     test(connected)
449     assert_equal(True, connected())
450     success()
451
452     dmid = get_studentid(tmsid)
453     if dmid: delete(dmid['memberid'])
454     dmid = get_studentid(tm2sid)
455     if dmid: delete(dmid['memberid'])
456     dmid = get_studentid(tm2usid)
457     if dmid: delete(dmid['memberid'])
458
459     test(new)
460     tmid = new(tmname, tmsid, tmprogram)
461     tm2id = new(tm2name, tm2sid)
462     success()
463
464     tmdict['memberid'] = tmid
465     tm2dict['memberid'] = tm2id
466     tm2udict['memberid'] = tm2id
467
468     test(registered)
469     assert_equal(True, registered(tmid, thisterm))
470     assert_equal(True, registered(tm2id, thisterm))
471     assert_equal(False, registered(tmid, nextterm))
472     success()
473
474     test(get)
475     assert_equal(tmdict, get(tmid))
476     assert_equal(tm2dict, get(tm2id))
477     success()
478
479     test(list_name)
480     assert_equal(True, tmid in [ x['memberid'] for x in list_name(tmname) ])
481     assert_equal(True, tm2id in [ x['memberid'] for x in list_name(tm2name) ])
482     success()
483
484     test(register)
485     register(tmid, terms.next(terms.current()))
486     assert_equal(True, registered(tmid, nextterm))
487     success()
488
489     test(member_terms)
490     assert_equal([thisterm, nextterm], member_terms(tmid))
491     assert_equal([thisterm], member_terms(tm2id))
492     success()
493
494     test(list_term)
495     assert_equal(True, tmid in [ x['memberid'] for x in list_term(thisterm) ])
496     assert_equal(True, tmid in [ x['memberid'] for x in list_term(nextterm) ])
497     assert_equal(True, tm2id in [ x['memberid'] for x in list_term(thisterm) ])
498     assert_equal(False, tm2id in [ x['memberid'] for x in list_term(nextterm) ])
499     success()
500
501     test(update)
502     update(tm2udict)
503     assert_equal(tm2udict, get(tm2id))
504     success()
505
506     test(get_userid)
507     assert_equal(tm2udict, get_userid(tm2uuserid))
508     success()
509
510     test(get_studentid)
511     assert_equal(tm2udict, get_studentid(tm2usid))
512     assert_equal(tmdict, get_studentid(tmsid))
513     success()
514
515     test(delete)
516     delete(tmid)
517     delete(tm2id)
518     success()
519
520     test(disconnect)
521     disconnect()
522     assert_equal(False, connected())
523     disconnect()
524     success()