Updated LDAP backend tests.
[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     return memberlist.values()
243         
244
245 def list_name(name):
246     """
247     Build a list of members with matching names.
248
249     Parameters:
250         name - the name to match members against
251
252     Returns: a list of member dictionaries
253
254     Example: list_name('Spang'): -> [
255                  { 'memberid': 3349, ... },
256                  { 'memberid': ... },
257                  ...
258              ]
259     """
260
261     # retrieve a list of memberids matching name
262     memberlist = connection.select_members_by_name(name)
263
264     return memberlist.values()
265
266
267 def list_all():
268     """
269     Builds a list of all members.
270     
271     Returns: a list of member dictionaries
272     """
273
274     # retrieve a list of members
275     memberlist = connection.select_all_members()
276
277     return memberlist.values()
278
279
280 def delete(memberid):
281     """
282     Erase all records of a member.
283
284     Note: real members are never removed from the database
285
286     Returns: attributes and terms of the member in a tuple
287
288     Exceptions:
289         NoSuchMember - if the member id does not exist
290
291     Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993'])
292     """
293
294     # save member data
295     member = connection.select_member_by_id(memberid)
296
297     # bail if not found
298     if not member:
299         raise NoSuchMember(memberid)
300
301     term_list = connection.select_terms(memberid)
302
303     # remove data from the db
304     connection.delete_term_all(memberid)
305     connection.delete_member(memberid)
306     connection.commit()
307
308     return (member, term_list)
309
310
311 def update(member):
312     """
313     Update CSC member attributes.
314
315     Parameters:
316         member - a dictionary with member attributes as returned by get,
317                  possibly omitting some attributes. member['memberid']
318                  must exist and be valid. None is NULL.
319
320     Exceptions:
321         NoSuchMember       - if the member id does not exist
322         InvalidStudentID   - if the student id number is malformed
323         DuplicateStudentID - if the student id number exists 
324
325     Example: update( {'memberid': 3349, userid: 'mspang'} )
326     """
327
328     if member.has_key('studentid') and member['studentid'] is not None:
329
330         studentid = member['studentid']
331         
332         # check the student id format
333         if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)):
334             raise InvalidStudentID(studentid)
335
336         # check for duplicate student id
337         dupmember = connection.select_member_by_studentid(studentid)
338         if dupmember:
339             raise DuplicateStudentID(studentid)
340
341     # not specifying memberid is a bug
342     if not member.has_key('memberid'):
343         raise Exception("no member specified in call to update")
344     memberid = member['memberid']
345
346     # see if member exists
347     if not get(memberid):
348         raise NoSuchMember(memberid)
349     
350     # do the update
351     connection.update_member(member)
352
353     # commit the transaction
354     connection.commit()
355
356
357
358 ### Term Table ###
359
360 def register(memberid, term_list):
361     """
362     Registers a member for one or more terms.
363
364     Parameters:
365         memberid  - the member id number
366         term_list - the term to register for, or a list of terms
367
368     Exceptions:
369         InvalidTerm - if a term is malformed
370
371     Example: register(3349, "w2007")
372
373     Example: register(3349, ["w2007", "s2007"])
374     """
375
376     if type(term_list) in (str, unicode):
377         term_list = [ term_list ]
378
379     for term in term_list:
380         
381         # check term syntax
382         if not re.match('^[wsf][0-9]{4}$', term):
383             raise InvalidTerm(term)
384     
385         # add term to database
386         connection.insert_term(memberid, term)
387
388     connection.commit()
389
390
391 def registered(memberid, term):
392     """
393     Determines whether a member is registered
394     for a term.
395
396     Parameters:
397         memberid - the member id number
398         term     - the term to check
399
400     Returns: whether the member is registered
401
402     Example: registered(3349, "f2006") -> True
403     """
404
405     return connection.select_term(memberid, term) is not None
406
407
408 def member_terms(memberid):
409     """
410     Retrieves a list of terms a member is
411     registered for.
412
413     Parameters:
414         memberid - the member id number
415
416     Returns: list of term strings
417
418     Example: registered(0) -> 's1993'
419     """
420
421     terms_list = connection.select_terms(memberid)
422     terms_list.sort(terms.compare)
423     return terms_list
424
425
426
427 ### Tests ###
428
429 if __name__ == '__main__':
430
431     from csc.common.test import *
432
433     # t=test m=member s=student u=updated
434     tmname = 'Test Member'
435     tmprogram = 'Metaphysics'
436     tmsid = '00000000'
437     tm2name = 'Test Member 2'
438     tm2sid = '00000001'
439     tm2uname = 'Test Member II'
440     tm2usid = '00000002'
441     tm2uprogram = 'Pseudoscience'
442     tm2uuserid = 'testmember'
443
444     tmdict = {'name': tmname, 'userid': None, 'program': tmprogram, 'type': 'user', 'studentid': tmsid }
445     tm2dict = {'name': tm2name, 'userid': None, 'program': None, 'type': 'user', 'studentid': tm2sid }
446     tm2udict = {'name': tm2uname, 'userid': tm2uuserid, 'program': tm2uprogram, 'type': 'user', 'studentid': tm2usid }
447
448     thisterm = terms.current()
449     nextterm = terms.next(thisterm)
450
451     test(connect)
452     connect()
453     success()
454
455     test(connected)
456     assert_equal(True, connected())
457     success()
458
459     dmid = get_studentid(tmsid)
460     if dmid: delete(dmid['memberid'])
461     dmid = get_studentid(tm2sid)
462     if dmid: delete(dmid['memberid'])
463     dmid = get_studentid(tm2usid)
464     if dmid: delete(dmid['memberid'])
465
466     test(new)
467     tmid = new(tmname, tmsid, tmprogram)
468     tm2id = new(tm2name, tm2sid)
469     success()
470
471     tmdict['memberid'] = tmid
472     tm2dict['memberid'] = tm2id
473     tm2udict['memberid'] = tm2id
474
475     test(registered)
476     assert_equal(True, registered(tmid, thisterm))
477     assert_equal(True, registered(tm2id, thisterm))
478     assert_equal(False, registered(tmid, nextterm))
479     success()
480
481     test(get)
482     assert_equal(tmdict, get(tmid))
483     assert_equal(tm2dict, get(tm2id))
484     success()
485
486     test(list_name)
487     assert_equal(True, tmid in [ x['memberid'] for x in list_name(tmname) ])
488     assert_equal(True, tm2id in [ x['memberid'] for x in list_name(tm2name) ])
489     success()
490
491     test(list_all)
492     allmembers = list_all()
493     assert_equal(True, tmid in [ x['memberid'] for x in allmembers ])
494     assert_equal(True, tm2id in [ x['memberid'] for x in allmembers ])
495     success()
496
497     test(register)
498     register(tmid, terms.next(terms.current()))
499     assert_equal(True, registered(tmid, nextterm))
500     success()
501
502     test(member_terms)
503     assert_equal([thisterm, nextterm], member_terms(tmid))
504     assert_equal([thisterm], member_terms(tm2id))
505     success()
506
507     test(list_term)
508     assert_equal(True, tmid in [ x['memberid'] for x in list_term(thisterm) ])
509     assert_equal(True, tmid in [ x['memberid'] for x in list_term(nextterm) ])
510     assert_equal(True, tm2id in [ x['memberid'] for x in list_term(thisterm) ])
511     assert_equal(False, tm2id in [ x['memberid'] for x in list_term(nextterm) ])
512     success()
513
514     test(update)
515     update(tm2udict)
516     assert_equal(tm2udict, get(tm2id))
517     success()
518
519     test(get_userid)
520     assert_equal(tm2udict, get_userid(tm2uuserid))
521     success()
522
523     test(get_studentid)
524     assert_equal(tm2udict, get_studentid(tm2usid))
525     assert_equal(tmdict, get_studentid(tmsid))
526     success()
527
528     test(delete)
529     delete(tmid)
530     delete(tm2id)
531     success()
532
533     test(disconnect)
534     disconnect()
535     assert_equal(False, connected())
536     disconnect()
537     success()