Rip out studentid support
[mspang/pyceo.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 = [ '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 InvalidTerm(MemberException):
51     """Exception class for malformed terms."""
52     def __init__(self, term):
53         self.term = term
54     def __str__(self):
55         return "Term is invalid: %s" % self.term
56
57 class InvalidRealName(MemberException):
58     """Exception class for invalid real names."""
59     def __init__(self, name):
60         self.name = name
61     def __str__(self):
62         return "Name is invalid: %s" % self.name
63
64 class NoSuchMember(MemberException):
65     """Exception class for nonexistent members."""
66     def __init__(self, memberid):
67         self.memberid = memberid
68     def __str__(self):
69         return "Member not found: %d" % self.memberid
70
71
72
73 ### Connection Management ###
74
75 # global directory connection
76 ldap_connection = ldapi.LDAPConnection()
77
78 def connect():
79     """Connect to PostgreSQL."""
80
81     load_configuration()
82     ldap_connection.connect(cfg['server_url'], cfg['admin_bind_dn'], cfg['admin_bind_pw'], cfg['users_base'], cfg['groups_base'])
83
84
85 def disconnect():
86     """Disconnect from PostgreSQL."""
87
88     ldap_connection.disconnect()
89
90
91 def connected():
92     """Determine whether the connection has been established."""
93
94     return ldap_connection.connected()
95
96
97
98 ### Member Table ###
99
100 def new(uid, realname, program=None):
101     """
102     Registers a new CSC member. The member is added to the members table
103     and registered for the current term.
104
105     Parameters:
106         uid       - the initial user id
107         realname  - the full real name of the member
108         program   - the program of study of the member
109
110     Returns: the username of the new member
111
112     Exceptions:
113         InvalidRealName    - if the real name is malformed
114
115     Example: new("Michael Spang", program="CS") -> "mspang"
116     """
117
118     # blank attributes should be NULL
119     if program == '': program = None
120     if uid == '': uid = None
121
122
123     # check real name format (UNIX account real names must not contain [,:=])
124     if not re.match(cfg['realname_regex'], realname):
125         raise InvalidRealName(realname)
126
127     # check for duplicate userid
128     member = ldap_connection.user_lookup(uid)
129     if member:
130         raise InvalidArgument("uid", uid, "duplicate uid")
131
132     # add the member to the directory
133     ldap_connection.member_add(uid, realname, program)
134
135     # register them for this term in the directory
136     member = ldap_connection.member_lookup(uid)
137     member['term'] = [ terms.current() ]
138     ldap_connection.user_modify(uid, member)
139
140     return uid
141
142
143 def get(userid):
144     """
145     Look up attributes of a member by userid.
146
147     Returns: a dictionary of attributes
148
149     Example: get('mspang') -> {
150                  'cn': [ 'Michael Spang' ],
151                  'program': [ 'Computer Science' ],
152                  ...
153              }
154     """
155
156     return ldap_connection.user_lookup(userid)
157
158
159 def list_term(term):
160     """
161     Build a list of members in a term.
162
163     Parameters:
164         term - the term to match members against
165
166     Returns: a list of members
167
168     Example: list_term('f2006'): -> {
169                  'mspang': { 'cn': 'Michael Spang', ... },
170                  'ctdalek': { 'cn': 'Calum T. Dalek', ... },
171                  ...
172              }
173     """
174
175     return ldap_connection.member_search_term(term)
176
177
178 def list_name(name):
179     """
180     Build a list of members with matching names.
181
182     Parameters:
183         name - the name to match members against
184
185     Returns: a list of member dictionaries
186
187     Example: list_name('Spang'): -> {
188                  'mspang': { 'cn': 'Michael Spang', ... },
189                  ...
190              ]
191     """
192
193     return ldap_connection.member_search_name(name)
194
195
196 def delete(userid):
197     """
198     Erase all records of a member.
199
200     Note: real members are never removed from the database
201
202     Returns: ldap entry of the member
203
204     Exceptions:
205         NoSuchMember - if the user id does not exist
206
207     Example: delete('ctdalek') -> { 'cn': [ 'Calum T. Dalek' ], 'term': ['s1993'], ... }
208     """
209
210     # save member data
211     member = ldap_connection.user_lookup(userid)
212
213     # bail if not found
214     if not member:
215         raise NoSuchMember(userid)
216
217     # remove data from the directory
218     uid = member['uid'][0]
219     ldap_connection.user_delete(uid)
220
221     return member
222
223
224
225 ### Term Table ###
226
227 def register(userid, term_list):
228     """
229     Registers a member for one or more terms.
230
231     Parameters:
232         userid  - the member's username
233         term_list - the term to register for, or a list of terms
234
235     Exceptions:
236         InvalidTerm - if a term is malformed
237
238     Example: register(3349, "w2007")
239
240     Example: register(3349, ["w2007", "s2007"])
241     """
242
243     if type(term_list) in (str, unicode):
244         term_list = [ term_list ]
245
246     ldap_member = ldap_connection.member_lookup(userid)
247     if ldap_member and 'term' not in ldap_member:
248         ldap_member['term'] = []
249
250     if not ldap_member:
251         raise NoSuchMember(userid)
252
253     for term in term_list:
254
255         # check term syntax
256         if not re.match('^[wsf][0-9]{4}$', term):
257             raise InvalidTerm(term)
258
259         # add the term to the directory
260         ldap_member['term'].append(term)
261
262     ldap_connection.user_modify(userid, ldap_member)
263
264
265 def registered(userid, term):
266     """
267     Determines whether a member is registered
268     for a term.
269
270     Parameters:
271         userid   - the member's username
272         term     - the term to check
273
274     Returns: whether the member is registered
275
276     Example: registered("mspang", "f2006") -> True
277     """
278
279     member = ldap_connection.member_lookup(userid)
280     return 'term' in member and term in member['term']
281
282
283 def member_terms(userid):
284     """
285     Retrieves a list of terms a member is
286     registered for.
287
288     Parameters:
289         userid - the member's username
290
291     Returns: list of term strings
292
293     Example: registered('ctdalek') -> 's1993'
294     """
295
296     member = ldap_connection.member_lookup(userid)
297     if not 'term' in member:
298         return []
299     else:
300         return member['term']
301
302
303
304 ### Tests ###
305
306 if __name__ == '__main__':
307
308     from csc.common.test import *
309
310     # t=test m=member u=updated
311     tmname = 'Test Member'
312     tmuid = 'testmember'
313     tmprogram = 'Metaphysics'
314     tm2name = 'Test Member 2'
315     tm2uid = 'testmember2'
316     tm2uname = 'Test Member II'
317     tm2uprogram = 'Pseudoscience'
318
319     tmdict = {'cn': [tmname], 'uid': [tmuid], 'program': [tmprogram] }
320     tm2dict = {'cn': [tm2name], 'uid': [tm2uid] }
321     tm2udict = {'cn': [tm2uname], 'uid': [tm2uid], 'program': [tm2uprogram] }
322
323     thisterm = terms.current()
324     nextterm = terms.next(thisterm)
325
326     test(connect)
327     connect()
328     success()
329
330     test(connected)
331     assert_equal(True, connected())
332     success()
333
334     test(new)
335     tmid = new(tmuid, tmname, tmprogram)
336     tm2id = new(tm2uid, tm2name)
337     success()
338
339     test(registered)
340     assert_equal(True, registered(tmid, thisterm))
341     assert_equal(True, registered(tm2id, thisterm))
342     assert_equal(False, registered(tmid, nextterm))
343     success()
344
345     test(get)
346     tmp = get(tmid)
347     del tmp['objectClass']
348     del tmp['term']
349     assert_equal(tmdict, tmp)
350     tmp = get(tm2id)
351     del tmp['objectClass']
352     del tmp['term']
353     assert_equal(tm2dict, tmp)
354     success()
355
356     test(list_name)
357     assert_equal(True, tmid in list_name(tmname).keys())
358     assert_equal(True, tm2id in list_name(tm2name).keys())
359     success()
360
361     test(register)
362     register(tmid, nextterm)
363     assert_equal(True, registered(tmid, nextterm))
364     success()
365
366     test(member_terms)
367     assert_equal([thisterm, nextterm], member_terms(tmid))
368     assert_equal([thisterm], member_terms(tm2id))
369     success()
370
371     test(list_term)
372     assert_equal(True, tmid in list_term(thisterm).keys())
373     assert_equal(True, tmid in list_term(nextterm).keys())
374     assert_equal(True, tm2id in list_term(thisterm).keys())
375     assert_equal(False, tm2id in list_term(nextterm).keys())
376     success()
377
378     test(get)
379     tmp = get(tm2id)
380     del tmp['objectClass']
381     del tmp['term']
382     assert_equal(tm2dict, tmp)
383     success()
384
385     test(delete)
386     delete(tmid)
387     delete(tm2id)
388     success()
389
390     test(disconnect)
391     disconnect()
392     assert_equal(False, connected())
393     disconnect()
394     success()