Bug fix: CEO (main.py) still referenced an exception that changed name in 0.2
[public/pyceo-broken.git] / pylib / csc / apps / legacy / main.py
1 """
2 CEO-like Frontend
3
4 This frontend aims to be compatible in both look and function with the
5 curses UI of CEO.
6
7 Some small improvements have been made, such as not echoing passwords and
8 aborting when nothing is typed into the first input box during an operation.
9
10 This frontend is poorly documented, deprecated, and undoubtedly full of bugs.
11 """
12 import curses.ascii, re, os
13 from helpers import menu, inputbox, msgbox, reset
14 from csc.adm import accounts, members, terms
15
16 # color of the ceo border
17 BORDER_COLOR = curses.COLOR_RED
18
19
20 def action_new_member(wnd):
21     """Interactively add a new member."""
22
23     studentid, program = None, ''
24
25     # read the name
26     prompt = "      Name: "
27     realname = inputbox(wnd, prompt, 18)
28
29     # abort if no username is entered
30     if not realname or realname.lower() == 'exit':
31         return False
32
33     # read the student id
34     prompt = "Student id:"
35     while studentid is None or (re.search("[^0-9]", studentid) and not studentid.lower() == 'exit'):
36         studentid = inputbox(wnd, prompt, 18)
37
38     # abort if exit is entered
39     if studentid.lower() == 'exit':
40         return False
41
42     if studentid == '':
43         studentid = None
44
45     # read the program of study
46     prompt = "   Program:"
47     program = inputbox(wnd, prompt, 18)
48
49     # abort if exit is entered
50     if program is None or program.lower() == 'exit':
51         return False
52
53     # connect the members module to its backend if necessary
54     if not members.connected(): members.connect()
55
56     # attempt to create the member
57     try:
58         memberid = members.new(realname, studentid, program)
59
60         msgbox(wnd, "Success! Your memberid is %s.  You are now registered\n"
61                     % memberid + "for the " + terms.current() + " term.")
62
63     except members.InvalidStudentID:
64         msgbox(wnd, "Invalid student ID: %s" % studentid)
65         return False
66     except members.DuplicateStudentID:
67         msgbox(wnd, "A member with this student ID exists.")
68         return False
69     except members.InvalidRealName:
70         msgbox(wnd, 'Invalid real name: "%s"' % realname)
71         return False
72
73
74 def action_term_register(wnd):
75     """Interactively register a member for a term."""
76
77     memberid, term = '', ''
78
79     # read the member id
80     prompt = 'Enter memberid ("exit" to cancel):'
81     memberuserid = inputbox(wnd, prompt, 36)
82
83     if not memberuserid or memberuserid.lower() == 'exit':
84         return False
85
86     member = get_member_memberid_userid(wnd, memberuserid)
87     if not member: return False
88
89     memberid = member['memberid']
90     term_list = members.member_terms(memberid)
91     
92     # display user
93     display_member_details(wnd, member, term_list)
94
95     # read the term
96     prompt = "Which term to register for ([fws]20nn):"
97     while not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit':
98         term = inputbox(wnd, prompt, 41) 
99
100     # abort when exit is entered
101     if term.lower() == 'exit':
102         return False
103
104     # already registered?
105     if members.registered(memberid, term):
106         msgbox(wnd, "You are already registered for term " + term)
107         return False
108
109     try:
110
111         # attempt to register
112         members.register(memberid, term)
113         
114         # display success message [sic]
115         msgbox(wnd, "Your are now registered for term " + term)
116
117     except members.InvalidTerm:
118         msgbox(wnd, "Term is not valid: %s" % term)
119
120     return False
121
122
123 def action_term_register_multiple(wnd):
124     """Interactively register a member for multiple terms."""
125
126     memberid, base, num = '', '', None
127
128     # read the member id
129     prompt = 'Enter memberid ("exit" to cancel):'
130     memberuserid = inputbox(wnd, prompt, 36)
131
132     if not memberuserid or memberuserid.lower() == 'exit':
133         return False
134
135     member = get_member_memberid_userid(wnd, memberuserid)
136     if not member: return False
137
138     memberid = member['memberid']
139     term_list = members.member_terms(memberid)
140     
141     # display user
142     display_member_details(wnd, member, term_list)
143
144     # read the base
145     prompt = "Which term to start registering ([fws]20nn):"
146     while not re.match('^[wsf][0-9]{4}$', base) and not base == 'exit':
147         base = inputbox(wnd, prompt, 41) 
148
149     # abort when exit is entered
150     if base.lower() == 'exit':
151         return False
152
153     # read number of terms
154     prompt = 'How many terms?'
155     while not num or not re.match('^[0-9]*$', num):
156         num = inputbox(wnd, prompt, 36)
157     num = int(num)
158
159     # any terms in the range?
160     if num < 1:
161         msgbox(wnd, "No terms to register.")
162         return False
163
164     # compile a list to register
165     term_list = terms.interval(base, num)
166
167     # already registered?
168     for term in term_list:
169         if members.registered(memberid, term):
170             msgbox(wnd, "You are already registered for term " + term)
171             return False
172
173     try:
174
175         # attempt to register all terms
176         members.register(memberid, term_list)
177         
178         # display success message [sic]
179         msgbox(wnd, "Your are now registered for terms: " + ", ".join(term_list))
180
181     except members.InvalidTerm:
182         msgbox(wnd, "Invalid term entered.")
183
184     return False
185
186
187 def repair_account(wnd, memberid, userid):
188     """Attemps to repair an account."""
189
190     if not accounts.connected(): accounts.connect()
191
192     member = members.get(memberid)
193     exists, haspw = accounts.status(userid)
194
195     if not exists:
196         password = input_password(wnd)
197         accounts.create_member(userid, password, member['name'], memberid)
198         msgbox(wnd, "Account created (where the hell did it go, anyway?)\n"
199                 "If you're homedir still exists, it will not be inaccessible to you,\n"
200                 "please contact systems-committee@csclub.uwaterloo.ca to get this resolved.\n")
201
202     elif not haspw:
203         password = input_password(wnd)
204         accounts.add_password(userid, password)
205         msgbox(wnd, "Password added to account.")
206
207     else:
208         msgbox(wnd, "No problems to repair.")
209
210
211 def input_password(wnd):
212
213     # password input loop
214     password = "password"
215     check = "check"
216     while password != check:
217     
218         # read password
219         prompt = "User password:"
220         password = None
221         while not password:
222             password = inputbox(wnd, prompt, 18, False) 
223
224         # read another password
225         prompt = "Enter the password again:"
226         check = None
227         while not check:
228             check = inputbox(wnd, prompt, 27, False) 
229
230     return password
231
232
233 def action_create_account(wnd):
234     """Interactively create an account for a member."""
235     
236     memberid, userid = '', ''
237
238     # read the member id
239     prompt = 'Enter member ID (exit to cancel):'
240     memberid = inputbox(wnd, prompt, 35)
241
242     if not memberid or memberid.lower() == 'exit':
243         return False
244
245     member = get_member_memberid_userid(wnd, memberid)
246     if not member: return False
247
248     memberid = member['memberid']
249     term_list = members.member_terms(memberid)
250     
251     # display the member
252     display_member_details(wnd, member, term_list)
253     
254     # verify member
255     prompt = "Is this the correct member?"
256     answer = None
257     while answer != "yes" and answer != "y" and answer != "n" and answer != "no" and answer != "exit":
258         answer = inputbox(wnd, prompt, 28) 
259
260     # user abort
261     if answer == "exit":
262         return False
263
264     # incorrect member; abort
265     if answer == "no" or answer == "n":
266         msgbox(wnd, "I suggest searching for the member by userid or name from the main menu.")
267         return False
268
269     # member already has an account?
270     if member['userid']:
271
272         userid = member['userid']
273         msgbox(wnd, "Member " + str(memberid) + " already has an account: " + member['userid'] + "\n"
274                     "Attempting to repair it. Contact the sysadmin if there are still problems." )
275
276         repair_account(wnd, memberid, userid)
277
278         return False
279
280
281     # read user id
282     prompt = "Userid:"
283     while userid == '':
284         userid = inputbox(wnd, prompt, 18) 
285
286     # user abort
287     if userid is None or userid.lower() == 'exit':
288         return False
289
290     # read password
291     password = input_password(wnd)
292
293     # create the UNIX account
294     try:
295         if not accounts.connected(): accounts.connect()
296         accounts.create_member(userid, password, member['name'], memberid)
297     except accounts.NameConflict, e:
298         msgbox(wnd, str(e))
299         return False
300     except accounts.NoAvailableIDs, e:
301         msgbox(wnd, str(e))
302         return False
303     except accounts.InvalidArgument, e:
304         msgbox(wnd, str(e))
305         return False
306     except accounts.LDAPException, e:
307         msgbox(wnd, "Error creating LDAP entry - Contact the Systems Administrator: %s" % e)
308         return False
309     except accounts.KrbException, e:
310         msgbox(wnd, "Error creating Kerberos principal - Contact the Systems Administrator: %s" % e)
311         return False
312         
313     # now update the CEO database with the username
314     members.update( {'memberid': memberid, 'userid': userid} )
315
316     # success
317     msgbox(wnd, "Please run 'addhomedir " + userid + "'.")
318     msgbox(wnd, "Success! Your account has been added")
319
320     return False
321
322
323 def display_member_details(wnd, member, term_list):
324     """Display member attributes in a message box."""
325
326     # clone and sort term_list
327     term_list = list(term_list)
328     term_list.sort( terms.compare )
329
330     # labels for data
331     id_label, studentid_label, name_label = "ID:", "StudentID:", "Name:"
332     program_label, userid_label, terms_label = "Program:", "User ID:", "Terms:"
333
334     # format it all into a massive string
335     message =  "%8s %-20s %10s %-10s (user)\n" % (name_label, member['name'], id_label, member['memberid']) + \
336                "%8s %-20s %10s %-10s\n" % (program_label, member['program'], studentid_label, member['studentid'])
337
338     if member['userid']:
339         message += "%8s %s\n" % (userid_label, member['userid'])
340     else:
341         message += 'No user ID.\n'
342
343     message += "%s %s" % (terms_label, " ".join(term_list))
344
345     # display the string in a message box
346     msgbox(wnd, message)
347
348
349 def get_member_memberid_userid(wnd, memberuserid):
350     """Retrieve member attributes by member of user id."""
351
352     # connect the members module to its backends
353     if not members.connected(): members.connect()
354
355     # retrieve member data
356
357     if re.match('^[0-9]*$', memberuserid):
358
359         # numeric memberid, look it up
360         memberid = int(memberuserid)
361         member = members.get( memberid )
362         if not member:
363             msgbox(wnd, '%s is an invalid memberid' % memberuserid)
364
365     else:
366
367         # non-numeric memberid: try userids
368         member = members.get_userid( memberuserid )
369         if not member:
370             msgbox(wnd, "%s is an invalid account userid" % memberuserid)
371             
372     return member
373     
374
375 def action_display_member(wnd):
376     """Interactively display a member."""
377     
378     # read the member id
379     prompt = 'Memberid: '
380     memberid = inputbox(wnd, prompt, 36)
381
382     if not memberid or memberid.lower() == 'exit':
383         return False
384
385     member = get_member_memberid_userid(wnd, memberid)
386     if not member: return False
387     term_list = members.member_terms( member['memberid'] )
388
389     # display the details in a window
390     display_member_details(wnd, member, term_list)
391
392     return False
393
394
395 def page(text):
396     """Send a text buffer to an external pager for display."""
397     
398     try:
399         pager = '/usr/bin/less'
400         pipe = os.popen(pager, 'w')
401         pipe.write(text)
402         pipe.close() 
403     except IOError:
404         # broken pipe (user didn't read the whole list)
405         pass
406     
407
408 def format_members(member_list):
409     """Format a member list into a string."""
410
411     # clone and sort member_list
412     member_list = list(member_list)
413     member_list.sort( lambda x, y: x['memberid']-y['memberid'] )
414
415     buf = ''
416     
417     for member in member_list:
418         attrs = ( member['memberid'], member['name'], member['studentid'],
419                 member['type'], member['program'], member['userid'] )
420         buf += "%4d %50s %10s %10s \n%55s %10s\n\n" % attrs
421
422     return buf
423
424
425 def action_list_term(wnd):
426     """Interactively list members registered in a term."""
427
428     term = None
429
430     # read the term
431     prompt = "Which term to list members for ([fws]20nn): "
432     while term is None or (not term == '' and not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit'):
433         term = inputbox(wnd, prompt, 41) 
434
435     # abort when exit is entered
436     if not term or term.lower() == 'exit':
437         return False
438
439     # connect the members module to its backends if necessary
440     if not members.connected(): members.connect()
441     
442     # retrieve a list of members for term
443     member_list = members.list_term(term)
444
445     # format the data into a mess of text
446     buf = format_members(member_list)
447
448     # display the mass of text with a pager
449     page( buf )
450
451     return False
452
453
454 def action_list_name(wnd):
455     """Interactively search for members by name."""
456     
457     name = None
458
459     # read the name
460     prompt = "Enter the member's name: "
461     name = inputbox(wnd, prompt, 41) 
462
463     # abort when exit is entered
464     if not name or name.lower() == 'exit':
465         return False
466
467     # connect the members module to its backends if necessary
468     if not members.connected(): members.connect()
469     
470     # retrieve a list of members with similar names
471     member_list = members.list_name(name)
472
473     # format the data into a mess of text
474     buf = format_members(member_list)
475
476     # display the mass of text with a pager
477     page( buf )
478
479     return False
480
481
482 def action_list_studentid(wnd):
483     """Interactively search for members by student id."""
484
485     studentid = None
486
487     # read the studentid
488     prompt = "Enter the member's student id: "
489     studentid = inputbox(wnd, prompt, 41) 
490
491     # abort when exit is entered
492     if not studentid or studentid.lower() == 'exit':
493         return False
494
495     # connect the members module to its backends if necessary
496     if not members.connected(): members.connect()
497     
498     # retrieve a list of members for term
499     member = members.get_studentid(studentid)
500     if member != None:
501         member_list = [ members.get_studentid(studentid) ]
502     else:
503         member_list = []
504
505     # format the data into a mess of text
506     buf = format_members(member_list)
507
508     # display the mass of text with a pager
509     page( buf )
510
511     return False
512
513
514 def null_callback(wnd):
515     """Callback for unimplemented menu options."""
516     return False
517
518
519 def exit_callback(wnd):
520     """Callback for the exit option."""
521     return True
522
523
524 # the top level ceo menu
525 top_menu = [
526     ( "New member", action_new_member ),
527     ( "Register for a term", action_term_register ),
528     ( "Register for multiple terms", action_term_register_multiple ),
529     ( "Display a member", action_display_member ),
530     ( "List members registered in a term", action_list_term ),
531     ( "Search for a member by name", action_list_name ),
532     ( "Search for a member by student id", action_list_studentid ),
533     ( "Create an account", action_create_account ),
534     ( "Re Create an account", action_create_account ),
535     ( "Library functions", null_callback ),
536     ( "Exit", exit_callback ),
537 ]
538
539
540 def acquire_ceo_wnd(screen=None):
541     """Create the top level ceo window."""
542     
543     # hack to get a reference to the entire screen
544     # even when the caller doesn't (shouldn't) have one
545     if screen is None:
546         screen = globals()['screen']
547     else:
548         globals()['screen'] = screen
549
550     # if the screen changes size, a mess may be left
551     screen.erase()
552
553     # for some reason, the legacy ceo system
554     # excluded the top line from its window
555     height, width = screen.getmaxyx()
556     ceo_wnd = screen.subwin(height-1, width, 1, 0)
557
558     # draw the border around the ceo window
559     curses.init_pair(1, BORDER_COLOR, -1)
560     color_attr = curses.color_pair(1) | curses.A_BOLD
561     ceo_wnd.attron(color_attr)
562     ceo_wnd.border()
563     ceo_wnd.attroff(color_attr)
564
565     # return window and dimensions of inner area
566     return ceo_wnd, 1, 1, height-2, width-2
567
568
569 def ceo_main_curses(screen):
570     """Wrapped main for curses."""
571     
572     curses.use_default_colors()
573
574     # workaround for SSH sessions on virtual consoles (reset terminal)
575     reset()
576
577     # create ceo window
578     ceo_wnd, menu_y, menu_x, menu_height, menu_width = acquire_ceo_wnd(screen)
579
580     try:
581         # display the top level menu
582         menu(ceo_wnd, menu_y, menu_x, menu_width, top_menu, acquire_ceo_wnd)
583     finally:
584         members.disconnect()
585         accounts.disconnect()
586
587
588 def run():
589     """Main function for legacy UI."""
590
591     # workaround for xterm-color (bad terminfo? - curs_set(0) fails)
592     if "TERM" in os.environ and os.environ['TERM'] == "xterm-color":
593         os.environ['TERM'] = "xterm"
594
595     # wrap the entire program using curses.wrapper
596     # so that the terminal is restored to a sane state
597     # when the program exits
598     try:
599         curses.wrapper(ceo_main_curses)
600     except KeyboardInterrupt:
601         pass
602     except curses.error:
603         print "Is your screen too small?"
604         raise
605     except:
606         reset()
607         raise
608
609     # clean up screen before exit
610     reset()
611
612 if __name__ == '__main__':
613     run()
614