Moved files into their new locations prior to commit of 0.2.
[public/pyceo-broken.git] / pylib / csc / adm / members.py
1 # $Id: members.py 44 2006-12-31 07:09:27Z mspang $
2 """
3 CSC Member Management
4
5 This module contains functions for registering new members, registering
6 members for terms, searching for members, and other member-related
7 functions.
8
9 Transactions are used in each method that modifies the database. 
10 Future changes to the members database that need to be atomic
11 must also be moved into this module.
12 """
13
14 import re
15 from csc.adm import terms
16 from csc.backends import db
17 from csc.common.conf import read_config
18
19
20
21
22 ### Configuration
23
24 CONFIG_FILE = '/etc/csc/members.cf'
25
26 cfg = {}
27
28
29 def load_configuration():
30     """Load Members Configuration"""
31
32     # configuration already loaded?
33     if len(cfg) > 0:
34         return
35
36     # read in the file
37     cfg_tmp = read_config(CONFIG_FILE)
38
39     if not cfg_tmp:
40         raise MemberException("unable to read configuration file: %s"
41                 % CONFIG_FILE)
42
43     # check that essential fields are completed
44     mandatory_fields = [ 'server', 'database', 'user', 'password' ]
45
46     for field in mandatory_fields:
47         if not field in cfg_tmp:
48             raise MemberException("missing configuratino option: %s" % field)
49         if not cfg_tmp[field]:
50             raise MemberException("null configuration option: %s" %field)
51
52     # update the current configuration with the loaded values
53     cfg.update(cfg_tmp)
54
55
56
57 ### Exceptions ###
58
59 class MemberException(Exception):
60     """Exception class for member-related errors."""
61
62 class DuplicateStudentID(MemberException):
63     """Exception class for student ID conflicts."""
64     pass
65
66 class InvalidStudentID(MemberException):
67     """Exception class for malformed student IDs."""
68     pass
69
70 class InvalidTerm(MemberException):
71     """Exception class for malformed terms."""
72     pass
73
74 class NoSuchMember(MemberException):
75     """Exception class for nonexistent members."""
76     pass
77
78
79
80 ### Connection Management ###
81
82 # global database connection
83 connection = db.DBConnection()
84
85
86 def connect():
87     """Connect to PostgreSQL."""
88     
89     load_configuration()
90     
91     connection.connect(cfg['server'], cfg['database'])
92        
93
94 def disconnect():
95     """Disconnect from PostgreSQL."""
96     
97     connection.disconnect()
98
99
100 def connected():
101     """Determine whether the connection has been established."""
102
103     return connection.connected()
104
105
106 ### Member Table ###
107
108 def new(realname, studentid=None, program=None):
109     """
110     Registers a new CSC member. The member is added
111     to the members table and registered for the current
112     term.
113
114     Parameters:
115         realname  - the full real name of the member
116         studentid - the student id number of the member
117         program   - the program of study of the member
118
119     Returns: the memberid of the new member
120
121     Exceptions:
122         DuplicateStudentID - if the student id already exists in the database
123         InvalidStudentID   - if the student id is malformed
124
125     Example: new("Michael Spang", program="CS") -> 3349
126     """
127
128     # blank attributes should be NULL
129     if studentid == '': studentid = None
130     if program == '': program = None
131
132     # check the student id format
133     regex = '^[0-9]{8}$'
134     if studentid != None and not re.match(regex, str(studentid)):
135         raise InvalidStudentID("student id is invalid: %s" % studentid)
136
137     # check for duplicate student id
138     member = connection.select_member_by_studentid(studentid)
139     if member:
140         raise DuplicateStudentID("student id exists in database: %s" % studentid)
141
142     # add the member
143     memberid = connection.insert_member(realname, studentid, program)
144
145     # register them for this term
146     connection.insert_term(memberid, terms.current())
147
148     # commit the transaction
149     connection.commit()
150
151     return memberid
152
153
154 def get(memberid):
155     """
156     Look up attributes of a member by memberid.
157
158     Parameters:
159         memberid - the member id number
160
161     Returns: a dictionary of attributes
162
163     Example: get(3349) -> {
164                  'memberid': 3349,
165                  'name': 'Michael Spang',
166                  'program': 'Computer Science',
167                  ...
168              }
169     """
170
171     return connection.select_member_by_id(memberid)
172
173
174 def get_userid(userid):
175     """
176     Look up attributes of a member by userid.
177
178     Parameters:
179         userid - the UNIX user id
180
181     Returns: a dictionary of attributes
182
183     Example: get('mspang') -> {
184                  'memberid': 3349,
185                  'name': 'Michael Spang',
186                  'program': 'Computer Science',
187                  ...
188              }
189     """
190
191     return connection.select_member_by_account(userid)
192
193
194 def get_studentid(studentid):
195     """
196     Look up attributes of a member by studnetid.
197
198     Parameters:
199         studentid - the student ID number
200
201     Returns: a dictionary of attributes
202     
203     Example: get(...) -> {
204                  'memberid': 3349,
205                  'name': 'Michael Spang',
206                  'program': 'Computer Science',
207                  ...
208              }
209     """
210
211     return connection.select_member_by_studentid(studentid)
212
213
214 def list_term(term):
215     """
216     Build a list of members in a term.
217
218     Parameters:
219         term - the term to match members against
220
221     Returns: a list of member dictionaries
222
223     Example: list_term('f2006'): -> [
224                  { 'memberid': 3349, ... },
225                  { 'memberid': ... }.
226                  ...
227              ]
228     """
229
230     # retrieve a list of memberids in term
231     memberlist = connection.select_members_by_term(term)
232
233     # convert the list of memberids to a list of dictionaries
234     memberlist = map(connection.select_member_by_id, memberlist)
235
236     return memberlist
237         
238
239 def list_name(name):
240     """
241     Build a list of members with matching names.
242
243     Parameters:
244         name - the name to match members against
245
246     Returns: a list of member dictionaries
247
248     Example: list_name('Spang'): -> [
249                  { 'memberid': 3349, ... },
250                  { 'memberid': ... },
251                  ...
252              ]
253     """
254
255     # retrieve a list of memberids matching name
256     memberlist = connection.select_members_by_name(name)
257
258     # convert the list of memberids to a list of dictionaries
259     memberlist = map(connection.select_member_by_id, memberlist)
260
261     return memberlist
262
263
264 def delete(memberid):
265     """
266     Erase all records of a member.
267
268     Note: real members are never removed
269           from the database
270
271     Parameters:
272         memberid - the member id number
273
274     Returns: attributes and terms of the
275              member in a tuple
276
277     Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993'])
278     """
279
280     # save member data
281     member = connection.select_member_by_id(memberid)
282     term_list = connection.select_terms(memberid)
283
284     # remove data from the db
285     connection.delete_term_all(memberid)
286     connection.delete_member(memberid)
287     connection.commit()
288
289     return (member, term_list)
290
291
292 def update(member):
293     """
294     Update CSC member attributes. None is NULL.
295
296     Parameters:
297         member - a dictionary with member attributes as
298                  returned by get, possibly omitting some
299                  attributes. member['memberid'] must exist
300                  and be valid.
301
302     Exceptions:
303         NoSuchMember       - if the member id does not exist
304         InvalidStudentID   - if the student id number is malformed
305         DuplicateStudentID - if the student id number exists 
306
307     Example: update( {'memberid': 3349, userid: 'mspang'} )
308     """
309
310     if member.has_key('studentid') and member['studentid'] != None:
311
312         studentid = member['studentid']
313         
314         # check the student id format
315         regex = '^[0-9]{8}$'
316         if studentid != None and not re.match(regex, str(studentid)):
317             raise InvalidStudentID("student id is invalid: %s" % studentid)
318
319         # check for duplicate student id
320         member = connection.select_member_by_studentid(studentid)
321         if member:
322             raise DuplicateStudentID("student id exists in database: %s" %
323                     studentid)
324
325     # not specifying memberid is a bug
326     if not member.has_key('memberid'):
327         raise Exception("no member specified in call to update")
328     memberid = member['memberid']
329
330     # see if member exists
331     old_member = connection.select_member_by_id(memberid)
332     if not old_member:
333         raise NoSuchMember("memberid does not exist in database: %d" %
334                 memberid)
335     
336     # do the update
337     connection.update_member(member)
338
339     # commit the transaction
340     connection.commit()
341
342
343
344 ### Term Table ###
345
346 def register(memberid, term_list):
347     """
348     Registers a member for one or more terms.
349
350     Parameters:
351         memberid  - the member id number
352         term_list - the term to register for, or a list of terms
353
354     Exceptions:
355         InvalidTerm - if a term is malformed
356
357     Example: register(3349, "w2007")
358
359     Example: register(3349, ["w2007", "s2007"])
360     """
361
362     if not type(term_list) in (list, tuple):
363         term_list = [ term_list ]
364
365     for term in term_list:
366         
367         # check term syntax
368         if not re.match('^[wsf][0-9]{4}$', term):
369             raise InvalidTerm("term is invalid: %s" % term)
370     
371         # add term to database
372         connection.insert_term(memberid, term)
373
374     connection.commit()
375
376
377 def registered(memberid, term):
378     """
379     Determines whether a member is registered
380     for a term.
381
382     Parameters:
383         memberid - the member id number
384         term     - the term to check
385
386     Returns: whether the member is registered
387
388     Example: registered(3349, "f2006") -> True
389     """
390
391     return connection.select_term(memberid, term) != None
392
393
394 def terms_list(memberid):
395     """
396     Retrieves a list of terms a member is
397     registered for.
398
399     Parameters:
400         memberid - the member id number
401
402     Returns: list of term strings
403
404     Example: registered(0) -> 's1993'
405     """
406
407     return connection.select_terms(memberid)
408
409
410
411 ### Tests ###
412
413 if __name__ == '__main__':
414
415     connect()
416     
417     
418     sid = new("Test User", "99999999", "CS")
419
420     assert registered(id, terms.current())
421     print get(sid)
422     register(sid, terms.next(terms.current()))
423     assert registered(sid, terms.next(terms.current()))
424     print terms_list(sid)
425     print get(sid)
426     print delete(sid)