pyceo/pylib/csc/apps/legacy/main.py

635 lines
18 KiB
Python

"""
CEO-like Frontend
This frontend aims to be compatible in both look and function with the
curses UI of CEO.
Some small improvements have been made, such as not echoing passwords and
aborting when nothing is typed into the first input box during an operation.
This frontend is poorly documented, deprecated, and undoubtedly full of bugs.
"""
import curses.ascii, re, os
from helpers import menu, inputbox, msgbox, reset
from csc.adm import accounts, members, terms
from csc.common.excep import InvalidArgument
# color of the ceo border
BORDER_COLOR = curses.COLOR_RED
def action_new_member(wnd):
"""Interactively add a new member."""
userid, studentid, program = '', None, ''
# read the name
prompt = " Name: "
realname = inputbox(wnd, prompt, 18)
# abort if no username is entered
if not realname or realname.lower() == 'exit':
return False
# read the student id
prompt = "Student id:"
while studentid is None or (re.search("[^0-9]", studentid) and not studentid.lower() == 'exit'):
studentid = inputbox(wnd, prompt, 18)
# abort if exit is entered
if studentid.lower() == 'exit':
return False
if studentid == '':
studentid = None
# read the program of study
prompt = " Program:"
program = inputbox(wnd, prompt, 18)
# abort if exit is entered
if program is None or program.lower() == 'exit':
return False
# read user id
prompt = "Userid:"
while userid == '':
userid = inputbox(wnd, prompt, 18)
# user abort
if userid is None or userid.lower() == 'exit':
return False
# connect the members module to its backend if necessary
if not members.connected(): members.connect()
# attempt to create the member
try:
memberid = members.new(userid, realname, studentid, program)
msgbox(wnd, "Success! Your memberid is %s. You are now registered\n"
% memberid + "for the " + terms.current() + " term.")
except members.InvalidStudentID:
msgbox(wnd, "Invalid student ID: %s" % studentid)
return False
except members.DuplicateStudentID:
msgbox(wnd, "A member with this student ID exists.")
return False
except members.InvalidRealName:
msgbox(wnd, 'Invalid real name: "%s"' % realname)
return False
except InvalidArgument, e:
if e.argname == 'uid' and e.explanation == 'duplicate uid':
msgbox(wnd, 'A member with this user ID exists.')
return False
else:
raise
def action_term_register(wnd):
"""Interactively register a member for a term."""
memberid, term = '', ''
# read the member id
prompt = 'Enter memberid ("exit" to cancel):'
memberuserid = inputbox(wnd, prompt, 36)
if not memberuserid or memberuserid.lower() == 'exit':
return False
member = get_member_memberid_userid(wnd, memberuserid)
if not member: return False
memberid = member['memberid']
term_list = members.member_terms(memberid)
# display user
display_member_details(wnd, member, term_list)
# read the term
prompt = "Which term to register for ([fws]20nn):"
while not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit':
term = inputbox(wnd, prompt, 41)
# abort when exit is entered
if term.lower() == 'exit':
return False
# already registered?
if members.registered(memberid, term):
msgbox(wnd, "You are already registered for term " + term)
return False
try:
# attempt to register
members.register(memberid, term)
# display success message [sic]
msgbox(wnd, "Your are now registered for term " + term)
except members.InvalidTerm:
msgbox(wnd, "Term is not valid: %s" % term)
return False
def action_term_register_multiple(wnd):
"""Interactively register a member for multiple terms."""
memberid, base, num = '', '', None
# read the member id
prompt = 'Enter memberid ("exit" to cancel):'
memberuserid = inputbox(wnd, prompt, 36)
if not memberuserid or memberuserid.lower() == 'exit':
return False
member = get_member_memberid_userid(wnd, memberuserid)
if not member: return False
memberid = member['memberid']
term_list = members.member_terms(memberid)
# display user
display_member_details(wnd, member, term_list)
# read the base
prompt = "Which term to start registering ([fws]20nn):"
while not re.match('^[wsf][0-9]{4}$', base) and not base == 'exit':
base = inputbox(wnd, prompt, 41)
# abort when exit is entered
if base.lower() == 'exit':
return False
# read number of terms
prompt = 'How many terms?'
while not num or not re.match('^[0-9]*$', num):
num = inputbox(wnd, prompt, 36)
num = int(num)
# any terms in the range?
if num < 1:
msgbox(wnd, "No terms to register.")
return False
# compile a list to register
term_list = terms.interval(base, num)
# already registered?
for term in term_list:
if members.registered(memberid, term):
msgbox(wnd, "You are already registered for term " + term)
return False
try:
# attempt to register all terms
members.register(memberid, term_list)
# display success message [sic]
msgbox(wnd, "Your are now registered for terms: " + ", ".join(term_list))
except members.InvalidTerm:
msgbox(wnd, "Invalid term entered.")
return False
def repair_account(wnd, memberid, userid):
"""Attemps to repair an account."""
if not accounts.connected(): accounts.connect()
member = members.get(memberid)
exists, haspw = accounts.status(userid)
if not exists:
password = input_password(wnd)
accounts.create_member(userid, password, member['name'], memberid)
msgbox(wnd, "Account created (where the hell did it go, anyway?)\n"
"If your homedir still exists, it will not be inaccessible to you,\n"
"please contact systems-committee@csclub.uwaterloo.ca to get this resolved.\n")
elif not haspw:
password = input_password(wnd)
accounts.add_password(userid, password)
msgbox(wnd, "Password added to account.")
else:
msgbox(wnd, "No problems to repair.")
def input_password(wnd):
# password input loop
password = "password"
check = "check"
while password != check:
# read password
prompt = "User password:"
password = None
while not password:
password = inputbox(wnd, prompt, 18, False)
# read another password
prompt = "Enter the password again:"
check = None
while not check:
check = inputbox(wnd, prompt, 27, False)
return password
def action_create_account(wnd):
"""Interactively create an account for a member."""
memberid, userid = '', ''
# read the member id
prompt = 'Enter member ID (exit to cancel):'
memberid = inputbox(wnd, prompt, 35)
if not memberid or memberid.lower() == 'exit':
return False
member = get_member_memberid_userid(wnd, memberid)
if not member: return False
memberid = member['memberid']
term_list = members.member_terms(memberid)
# display the member
display_member_details(wnd, member, term_list)
# verify member
prompt = "Is this the correct member?"
answer = None
while answer != "yes" and answer != "y" and answer != "n" and answer != "no" and answer != "exit":
answer = inputbox(wnd, prompt, 28)
# user abort
if answer == "exit":
return False
# incorrect member; abort
if answer == "no" or answer == "n":
msgbox(wnd, "I suggest searching for the member by userid or name from the main menu.")
return False
# member already has an account?
if not accounts.connected(): accounts.connect()
if member['userid'] and accounts.status(member['userid'])[0]:
userid = member['userid']
msgbox(wnd, "Member " + str(memberid) + " already has an account: " + member['userid'] + "\n"
"Attempting to repair it. Contact the sysadmin if there are still problems." )
repair_account(wnd, memberid, userid)
return False
if member['userid']:
userid = member['userid']
# read user id
prompt = "Userid:"
while userid == '':
userid = inputbox(wnd, prompt, 18)
# user abort
if userid is None or userid.lower() == 'exit':
return False
# read password
password = input_password(wnd)
# create the UNIX account
try:
if not accounts.connected(): accounts.connect()
accounts.create_member(userid, password, member['name'], memberid)
except accounts.NameConflict, e:
msgbox(wnd, str(e))
return False
except accounts.NoAvailableIDs, e:
msgbox(wnd, str(e))
return False
except accounts.InvalidArgument, e:
msgbox(wnd, str(e))
return False
except accounts.LDAPException, e:
msgbox(wnd, "Error creating LDAP entry - Contact the Systems Administrator: %s" % e)
return False
except accounts.KrbException, e:
msgbox(wnd, "Error creating Kerberos principal - Contact the Systems Administrator: %s" % e)
return False
# now update the CEO database with the username
members.update( {'memberid': memberid, 'userid': userid} )
# success
msgbox(wnd, "Please run 'addhomedir " + userid + "'.")
msgbox(wnd, "Success! Your account has been added")
return False
def display_member_details(wnd, member, term_list):
"""Display member attributes in a message box."""
# clone and sort term_list
term_list = list(term_list)
term_list.sort( terms.compare )
# labels for data
id_label, studentid_label, name_label = "ID:", "StudentID:", "Name:"
program_label, userid_label, terms_label = "Program:", "User ID:", "Terms:"
# format it all into a massive string
message = "%8s %-20s %10s %-10s (user)\n" % (name_label, member['name'], id_label, member['memberid']) + \
"%8s %-20s %10s %-10s\n" % (program_label, member['program'], studentid_label, member['studentid'])
if member['userid']:
message += "%8s %s\n" % (userid_label, member['userid'])
else:
message += 'No user ID.\n'
message += "%s %s" % (terms_label, " ".join(term_list))
# display the string in a message box
msgbox(wnd, message)
def get_member_memberid_userid(wnd, memberuserid):
"""Retrieve member attributes by member of user id."""
# connect the members module to its backends
if not members.connected(): members.connect()
# retrieve member data
if re.match('^[0-9]*$', memberuserid):
# numeric memberid, look it up
memberid = int(memberuserid)
member = members.get( memberid )
if not member:
msgbox(wnd, '%s is an invalid memberid' % memberuserid)
else:
# non-numeric memberid: try userids
member = members.get_userid( memberuserid )
if not member:
msgbox(wnd, "%s is an invalid account userid" % memberuserid)
return member
def action_display_member(wnd):
"""Interactively display a member."""
# read the member id
prompt = 'Memberid: '
memberid = inputbox(wnd, prompt, 36)
if not memberid or memberid.lower() == 'exit':
return False
member = get_member_memberid_userid(wnd, memberid)
if not member: return False
term_list = members.member_terms( member['memberid'] )
# display the details in a window
display_member_details(wnd, member, term_list)
return False
def page(text):
"""Send a text buffer to an external pager for display."""
try:
pager = '/usr/bin/less'
pipe = os.popen(pager, 'w')
pipe.write(text)
pipe.close()
except IOError:
# broken pipe (user didn't read the whole list)
pass
def format_members(member_list):
"""Format a member list into a string."""
# clone and sort member_list
member_list = list(member_list)
member_list.sort( lambda x, y: x['memberid']-y['memberid'] )
buf = ''
for member in member_list:
attrs = ( member['memberid'], member['name'], member['studentid'],
member['type'], member['program'], member['userid'] )
buf += "%4d %50s %10s %10s \n%55s %10s\n\n" % attrs
return buf
def action_list_term(wnd):
"""Interactively list members registered in a term."""
term = None
# read the term
prompt = "Which term to list members for ([fws]20nn): "
while term is None or (not term == '' and not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit'):
term = inputbox(wnd, prompt, 41)
# abort when exit is entered
if not term or term.lower() == 'exit':
return False
# connect the members module to its backends if necessary
if not members.connected(): members.connect()
# retrieve a list of members for term
member_list = members.list_term(term)
# format the data into a mess of text
buf = format_members(member_list)
# display the mass of text with a pager
page( buf )
return False
def action_list_name(wnd):
"""Interactively search for members by name."""
name = None
# read the name
prompt = "Enter the member's name: "
name = inputbox(wnd, prompt, 41)
# abort when exit is entered
if not name or name.lower() == 'exit':
return False
# connect the members module to its backends if necessary
if not members.connected(): members.connect()
# retrieve a list of members with similar names
member_list = members.list_name(name)
# format the data into a mess of text
buf = format_members(member_list)
# display the mass of text with a pager
page( buf )
return False
def action_list_studentid(wnd):
"""Interactively search for members by student id."""
studentid = None
# read the studentid
prompt = "Enter the member's student id: "
studentid = inputbox(wnd, prompt, 41)
# abort when exit is entered
if not studentid or studentid.lower() == 'exit':
return False
# connect the members module to its backends if necessary
if not members.connected(): members.connect()
# retrieve a list of members for term
member = members.get_studentid(studentid)
if member != None:
member_list = [ members.get_studentid(studentid) ]
else:
member_list = []
# format the data into a mess of text
buf = format_members(member_list)
# display the mass of text with a pager
page( buf )
return False
def null_callback(wnd):
"""Callback for unimplemented menu options."""
return False
def exit_callback(wnd):
"""Callback for the exit option."""
return True
# the top level ceo menu
top_menu = [
( "New member", action_new_member ),
( "Register for a term", action_term_register ),
( "Register for multiple terms", action_term_register_multiple ),
( "Display a member", action_display_member ),
( "List members registered in a term", action_list_term ),
( "Search for a member by name", action_list_name ),
( "Search for a member by student id", action_list_studentid ),
( "Create an account", action_create_account ),
( "Re Create an account", action_create_account ),
( "Library functions", null_callback ),
( "Exit", exit_callback ),
]
def acquire_ceo_wnd(screen=None):
"""Create the top level ceo window."""
# hack to get a reference to the entire screen
# even when the caller doesn't (shouldn't) have one
if screen is None:
screen = globals()['screen']
else:
globals()['screen'] = screen
# if the screen changes size, a mess may be left
screen.erase()
# for some reason, the legacy ceo system
# excluded the top line from its window
height, width = screen.getmaxyx()
ceo_wnd = screen.subwin(height-1, width, 1, 0)
# draw the border around the ceo window
curses.init_pair(1, BORDER_COLOR, -1)
color_attr = curses.color_pair(1) | curses.A_BOLD
ceo_wnd.attron(color_attr)
ceo_wnd.border()
ceo_wnd.attroff(color_attr)
# return window and dimensions of inner area
return ceo_wnd, 1, 1, height-2, width-2
def ceo_main_curses(screen):
"""Wrapped main for curses."""
curses.use_default_colors()
# workaround for SSH sessions on virtual consoles (reset terminal)
reset()
# create ceo window
ceo_wnd, menu_y, menu_x, menu_height, menu_width = acquire_ceo_wnd(screen)
try:
# display the top level menu
menu(ceo_wnd, menu_y, menu_x, menu_width, top_menu, acquire_ceo_wnd)
finally:
members.disconnect()
accounts.disconnect()
def run():
"""Main function for legacy UI."""
# workaround for xterm-color (bad terminfo? - curs_set(0) fails)
if "TERM" in os.environ and os.environ['TERM'] == "xterm-color":
os.environ['TERM'] = "xterm"
# wrap the entire program using curses.wrapper
# so that the terminal is restored to a sane state
# when the program exits
try:
curses.wrapper(ceo_main_curses)
except KeyboardInterrupt:
pass
except curses.error:
print "Is your screen too small?"
raise
except:
reset()
raise
# clean up screen before exit
reset()
if __name__ == '__main__':
run()