Increase widths of UI windows.
[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 ldapi
15 from csc.common import conf
16 from csc.common.excep import InvalidArgument
17
18
19 ### Configuration ###
20
21 CONFIG_FILE = '/etc/csc/members.cf'
22
23 cfg = {}
24
25 def load_configuration():
26     """Load Members Configuration"""
27
28     string_fields = [ 'studentid_regex', 'realname_regex', 'server',
29             'database', 'user', 'password', 'server_url', 'users_base',
30             'groups_base', 'admin_bind_dn', 'admin_bind_pw' ]
31
32     # read configuration file
33     cfg_tmp = conf.read(CONFIG_FILE)
34
35     # verify configuration
36     conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
37
38     # update the current configuration with the loaded values
39     cfg.update(cfg_tmp)
40
41
42
43 ### Exceptions ###
44
45 ConfigurationException = conf.ConfigurationException
46
47 class MemberException(Exception):
48     """Base exception class for member-related errors."""
49
50 class DuplicateStudentID(MemberException):
51     """Exception class for student ID conflicts."""
52     def __init__(self, studentid):
53         self.studentid = studentid
54     def __str__(self):
55         return "Student ID already exists in the database: %s" % self.studentid
56
57 class InvalidStudentID(MemberException):
58     """Exception class for malformed student IDs."""
59     def __init__(self, studentid):
60         self.studentid = studentid
61     def __str__(self):
62         return "Student ID is invalid: %s" % self.studentid
63
64 class InvalidTerm(MemberException):
65     """Exception class for malformed terms."""
66     def __init__(self, term):
67         self.term = term
68     def __str__(self):
69         return "Term is invalid: %s" % self.term
70
71 class InvalidRealName(MemberException):
72     """Exception class for invalid real names."""
73     def __init__(self, name):
74         self.name = name
75     def __str__(self):
76         return "Name is invalid: %s" % self.name
77
78 class NoSuchMember(MemberException):
79     """Exception class for nonexistent members."""
80     def __init__(self, memberid):
81         self.memberid = memberid
82     def __str__(self):
83         return "Member not found: %d" % self.memberid
84
85
86
87 ### Connection Management ###
88
89 # global directory connection
90 ldap_connection = ldapi.LDAPConnection()
91
92 def connect():
93     """Connect to PostgreSQL."""
94
95     load_configuration()
96     ldap_connection.connect(cfg['server_url'], cfg['admin_bind_dn'], cfg['admin_bind_pw'], cfg['users_base'], cfg['groups_base'])
97
98
99 def disconnect():
100     """Disconnect from PostgreSQL."""
101
102     ldap_connection.disconnect()
103
104
105 def connected():
106     """Determine whether the connection has been established."""
107
108     return ldap_connection.connected()
109
110
111
112 ### Member Table ###
113
114 def new(uid, realname, studentid=None, program=None):
115     """
116     Registers a new CSC member. The member is added to the members table
117     and registered for the current term.
118
119     Parameters:
120         uid       - the initial user id
121         realname  - the full real name of the member
122         studentid - the student id number of the member
123         program   - the program of study of the member
124
125     Returns: the username 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") -> "mspang"
133     """
134
135     # blank attributes should be NULL
136     if studentid == '': studentid = None
137     if program == '': program = None
138     if uid == '': uid = None
139
140     # check the student id format
141     if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)):
142         raise InvalidStudentID(studentid)
143
144     # check real name format (UNIX account real names must not contain [,:=])
145     if not re.match(cfg['realname_regex'], realname):
146         raise InvalidRealName(realname)
147
148     # check for duplicate student id
149     member = ldap_connection.member_search_studentid(studentid)
150     if member:
151         raise DuplicateStudentID(studentid)
152
153     # check for duplicate userid
154     member = ldap_connection.user_lookup(uid)
155     if member:
156         raise InvalidArgument("uid", uid, "duplicate uid")
157
158     # add the member to the directory
159     ldap_connection.member_add(uid, realname, studentid, program)
160
161     # register them for this term in the directory
162     member = ldap_connection.member_lookup(uid)
163     member['term'] = [ terms.current() ]
164     ldap_connection.user_modify(uid, member)
165
166     return uid
167
168
169 def get(userid):
170     """
171     Look up attributes of a member by userid.
172
173     Returns: a dictionary of attributes
174
175     Example: get('mspang') -> {
176                  'cn': [ 'Michael Spang' ],
177                  'program': [ 'Computer Science' ],
178                  ...
179              }
180     """
181
182     return ldap_connection.user_lookup(userid)
183
184
185 def get_studentid(studentid):
186     """
187     Look up attributes of a member by studentid.
188
189     Parameters:
190         studentid - the student ID number
191
192     Returns: a dict of members
193     
194     Example: get(...) -> {
195                 'mspang': {
196                     'name': [ 'Michael Spang' ],
197                     'program': [ 'Computer Science' ],
198                  }
199                  ...
200              }
201     """
202
203     return ldap_connection.member_search_studentid(studentid)
204
205
206 def list_term(term):
207     """
208     Build a list of members in a term.
209
210     Parameters:
211         term - the term to match members against
212
213     Returns: a list of members
214
215     Example: list_term('f2006'): -> {
216                  'mspang': { 'cn': 'Michael Spang', ... },
217                  'ctdalek': { 'cn': 'Calum T. Dalek', ... },
218                  ...
219              }
220     """
221
222     return ldap_connection.member_search_term(term)
223
224
225 def list_name(name):
226     """
227     Build a list of members with matching names.
228
229     Parameters:
230         name - the name to match members against
231
232     Returns: a list of member dictionaries
233
234     Example: list_name('Spang'): -> {
235                  'mspang': { 'cn': 'Michael Spang', ... },
236                  ...
237              ]
238     """
239
240     return ldap_connection.member_search_name(name)
241
242
243 def delete(userid):
244     """
245     Erase all records of a member.
246
247     Note: real members are never removed from the database
248
249     Returns: ldap entry of the member
250
251     Exceptions:
252         NoSuchMember - if the user id does not exist
253
254     Example: delete('ctdalek') -> { 'cn': [ 'Calum T. Dalek' ], 'term': ['s1993'], ... }
255     """
256
257     # save member data
258     member = ldap_connection.user_lookup(userid)
259
260     # bail if not found
261     if not member:
262         raise NoSuchMember(userid)
263
264     # remove data from the directory
265     uid = member['uid'][0]
266     ldap_connection.user_delete(uid)
267
268     return member
269
270
271
272 ### Term Table ###
273
274 def register(userid, term_list):
275     """
276     Registers a member for one or more terms.
277
278     Parameters:
279         userid  - the member's username
280         term_list - the term to register for, or a list of terms
281
282     Exceptions:
283         InvalidTerm - if a term is malformed
284
285     Example: register(3349, "w2007")
286
287     Example: register(3349, ["w2007", "s2007"])
288     """
289
290     if type(term_list) in (str, unicode):
291         term_list = [ term_list ]
292
293     ldap_member = ldap_connection.member_lookup(userid)
294     if ldap_member and 'term' not in ldap_member:
295         ldap_member['term'] = []
296
297     if not ldap_member:
298         raise NoSuchMember(userid)
299
300     for term in term_list:
301
302         # check term syntax
303         if not re.match('^[wsf][0-9]{4}$', term):
304             raise InvalidTerm(term)
305
306         # add the term to the directory
307         ldap_member['term'].append(term)
308
309     ldap_connection.user_modify(userid, ldap_member)
310
311
312 def registered(userid, term):
313     """
314     Determines whether a member is registered
315     for a term.
316
317     Parameters:
318         userid   - the member's username
319         term     - the term to check
320
321     Returns: whether the member is registered
322
323     Example: registered("mspang", "f2006") -> True
324     """
325
326     member = ldap_connection.member_lookup(userid)
327     return 'term' in member and term in member['term']
328
329
330 def member_terms(userid):
331     """
332     Retrieves a list of terms a member is
333     registered for.
334
335     Parameters:
336         userid - the member's username
337
338     Returns: list of term strings
339
340     Example: registered('ctdalek') -> 's1993'
341     """
342
343     member = ldap_connection.member_lookup(userid)
344     if not 'term' in member:
345         return []
346     else:
347         return member['term']
348
349
350
351 ### Tests ###
352
353 if __name__ == '__main__':
354
355     from csc.common.test import *
356
357     # t=test m=member s=student u=updated
358     tmname = 'Test Member'
359     tmuid = 'testmember'
360     tmprogram = 'Metaphysics'
361     tmsid = '00000000'
362     tm2name = 'Test Member 2'
363     tm2uid = 'testmember2'
364     tm2sid = '00000001'
365     tm2uname = 'Test Member II'
366     tm2usid = '00000002'
367     tm2uprogram = 'Pseudoscience'
368
369     tmdict = {'cn': [tmname], 'uid': [tmuid], 'program': [tmprogram], 'studentid': [tmsid] }
370     tm2dict = {'cn': [tm2name], 'uid': [tm2uid], 'studentid': [tm2sid] }
371     tm2udict = {'cn': [tm2uname], 'uid': [tm2uid], 'program': [tm2uprogram], 'studentid': [tm2usid] }
372
373     thisterm = terms.current()
374     nextterm = terms.next(thisterm)
375
376     test(connect)
377     connect()
378     success()
379
380     test(connected)
381     assert_equal(True, connected())
382     success()
383
384     dmid = get_studentid(tmsid)
385     if tmuid in dmid: delete(dmid[tmuid]['uid'][0])
386     dmid = get_studentid(tm2sid)
387     if tm2uid in dmid: delete(dmid[tm2uid]['uid'][0])
388     dmid = get_studentid(tm2usid)
389     if tm2uid in dmid: delete(dmid[tm2uid]['uid'][0])
390
391     test(new)
392     tmid = new(tmuid, tmname, tmsid, tmprogram)
393     tm2id = new(tm2uid, tm2name, tm2sid)
394     success()
395
396     test(registered)
397     assert_equal(True, registered(tmid, thisterm))
398     assert_equal(True, registered(tm2id, thisterm))
399     assert_equal(False, registered(tmid, nextterm))
400     success()
401
402     test(get)
403     tmp = get(tmid)
404     del tmp['objectClass']
405     del tmp['term']
406     assert_equal(tmdict, tmp)
407     tmp = get(tm2id)
408     del tmp['objectClass']
409     del tmp['term']
410     assert_equal(tm2dict, tmp)
411     success()
412
413     test(list_name)
414     assert_equal(True, tmid in list_name(tmname).keys())
415     assert_equal(True, tm2id in list_name(tm2name).keys())
416     success()
417
418     test(register)
419     register(tmid, nextterm)
420     assert_equal(True, registered(tmid, nextterm))
421     success()
422
423     test(member_terms)
424     assert_equal([thisterm, nextterm], member_terms(tmid))
425     assert_equal([thisterm], member_terms(tm2id))
426     success()
427
428     test(list_term)
429     assert_equal(True, tmid in list_term(thisterm).keys())
430     assert_equal(True, tmid in list_term(nextterm).keys())
431     assert_equal(True, tm2id in list_term(thisterm).keys())
432     assert_equal(False, tm2id in list_term(nextterm).keys())
433     success()
434
435     test(get)
436     tmp = get(tm2id)
437     del tmp['objectClass']
438     del tmp['term']
439     assert_equal(tm2dict, tmp)
440     success()
441
442     test(get_studentid)
443     tmp = get_studentid(tm2sid)[tm2uid]
444     del tmp['objectClass']
445     del tmp['term']
446     assert_equal(tm2dict, tmp)
447     tmp = get_studentid(tmsid)[tmuid]
448     del tmp['objectClass']
449     del tmp['term']
450     assert_equal(tmdict, tmp)
451     success()
452
453     test(delete)
454     delete(tmid)
455     delete(tm2id)
456     success()
457
458     test(disconnect)
459     disconnect()
460     assert_equal(False, connected())
461     disconnect()
462     success()