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