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