Add experimental urwid-based GUI

This commit is contained in:
Michael Spang 2007-09-23 22:32:56 -04:00
parent 299c25d610
commit 588f90b082
12 changed files with 679 additions and 6 deletions

View File

@ -15,7 +15,7 @@ os.environ['LESSSECURE'] = '1'
os.environ['PATH'] = '/usr/sbin:/usr/bin:/sbin:/bin'
for pathent in sys.path[:]:
if not pathent.find('/usr') == 0:
if not pathent.find('/usr') == 0 and not pathent.find('/var') == 0:
sys.path.remove(pathent)
euid = os.geteuid()
@ -27,5 +27,5 @@ except OSError, e:
print str(e)
sys.exit(1)
import csc.apps.legacy.main
csc.apps.legacy.main.run()
import csc.apps.urwid.main
csc.apps.urwid.main.start()

31
bin/ceo-old Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/python2.4 --
"""CEO SUID Python Wrapper Script"""
import os, sys
safe_environment = ['LOGNAME', 'USERNAME', 'USER', 'HOME', 'TERM', 'LANG'
'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY',
'LC_NUMERIC', 'LC_TIME', 'UID', 'GID', 'SSH_CONNECTION', 'SSH_AUTH_SOCK',
'SSH_CLIENT']
for key in os.environ.keys():
if key not in safe_environment:
del os.environ[key]
os.environ['LESSSECURE'] = '1'
os.environ['PATH'] = '/usr/sbin:/usr/bin:/sbin:/bin'
for pathent in sys.path[:]:
if not pathent.find('/usr') == 0:
sys.path.remove(pathent)
euid = os.geteuid()
egid = os.getegid()
try:
os.setreuid(euid, euid)
os.setregid(egid, egid)
except OSError, e:
print str(e)
sys.exit(1)
import csc.apps.legacy.main
csc.apps.legacy.main.run()

5
debian/rules vendored
View File

@ -7,6 +7,7 @@ build: build-stamp
build-stamp:
mkdir build
$(CC) -DFULL_PATH='"/usr/lib/csc/ceo"' -o build/ceo misc/setuid-prog.c
$(CC) -DFULL_PATH='"/usr/lib/csc/ceo-old"' -o build/ceo-old misc/setuid-prog.c
$(CC) -DFULL_PATH='"/usr/lib/csc/addhomedir"' -o build/addhomedir misc/setuid-prog.c
$(CC) -DFULL_PATH='"/usr/lib/csc/ceoquery"' -o build/ceoquery misc/setuid-prog.c
$(CC) -DFULL_PATH='"/usr/lib/csc/csc-chfn"' -o build/csc-chfn misc/setuid-prog.c
@ -31,8 +32,8 @@ install: build
dh_install etc/* etc/csc/
dh_install sql/* usr/share/csc/
dh_install bin/ceo bin/addhomedir bin/ceoquery bin/csc-chsh bin/csc-chfn usr/lib/csc/
dh_install build/ceo build/addhomedir build/ceoquery build/csc-chsh build/csc-chfn usr/bin/
dh_install bin/ceo bin/ceo-old bin/addhomedir bin/ceoquery bin/csc-chsh bin/csc-chfn usr/lib/csc/
dh_install build/ceo build/ceo-old build/addhomedir build/ceoquery build/csc-chsh build/csc-chfn usr/bin/
dh_install misc/csc.schema etc/ldap/schema/
binary-arch: build install

View File

@ -2,7 +2,6 @@ TODO:
* Python bindings for libkadm5
* Python bindings for quota?
* New UI: urwid-based?
* Logging via syslog
* Try to recover and roll-back on error during account creation
* Write manpages

View File

@ -0,0 +1,3 @@
"""
Urwid User Interface
"""

View File

@ -0,0 +1,39 @@
import urwid
from csc.apps.urwid.widgets import *
from csc.apps.urwid.window import *
from csc.adm import accounts, members
from csc.common.excep import InvalidArgument
class InfoPage(WizardPanel):
def init_widgets(self):
self.userid = urwid.Text("")
self.name = urwid.Text("")
self.terms = urwid.Text("")
self.program = urwid.Text("")
self.widgets = [
urwid.Text( "Member Details" ),
urwid.Divider(),
self.name,
self.userid,
self.program,
urwid.Divider(),
self.terms,
]
def focusable(self):
return False
def activate(self):
member = self.state.get('member', {})
name = member.get('cn', [''])[0]
userid = self.state['userid']
program = member.get('program', [''])[0]
terms = member.get('term', [])
self.name.set_text("Name: %s" % name)
self.userid.set_text("User: %s" % userid)
self.program.set_text("Program: %s" % program)
self.terms.set_text("Terms: %s" % ", ".join(terms))
def check(self):
pop_window()

View File

@ -0,0 +1,122 @@
import random, time
import urwid, urwid.curses_display
from csc.apps.urwid.widgets import *
from csc.apps.urwid.window import *
import csc.apps.urwid.newmember as newmember
import csc.apps.urwid.renew as renew
import csc.apps.urwid.info as info
import csc.apps.urwid.search as search
from csc.adm import accounts, members, terms
from csc.common.excep import InvalidArgument
ui = urwid.curses_display.Screen()
ui.register_palette([
# name, foreground, background, mono
('banner', 'light gray', 'default', None),
('menu', 'light gray', 'default', 'bold'),
('selected', 'black', 'light gray', 'bold'),
])
def program_name():
cwords = [ "CSC" ] * 20 + [ "Club" ] * 10 + [ "Campus" ] * 5 + \
[ "Communist", "Canadian", "Celestial", "Cryptographic", "Calum's",
"Canonical", "Capitalist", "Catastrophic", "Ceremonial", "Chaotic", "Civic",
"City", "County", "Caffeinated" ]
ewords = [ "Embellished", "Ergonomic", "Electric", "Eccentric", "European", "Economic",
"Evil", "Egotistical", "Elliptic", "Emasculating", "Embalming",
"Embryonic", "Emigrant", "Emissary's", "Emoting", "Employment", "Emulated",
"Enabling", "Enamoring", "Encapsulated", "Enchanted", "Encoded", "Encrypted",
"Encumbered", "Endemic", "Enhanced", "Enigmatic", "Enlightened", "Enormous",
"Enrollment", "Enshrouded", "Ephermal", "Epidemic", "Episodic", "Epsilon",
"Equitable", "Equestrian", "Equilateral", "Erroneous", "Erratic",
"Espresso", "Essential", "Estate", "Esteemed", "Eternal", "Ethical", "Eucalyptus",
"Euphemistic", "Envangelist", "Evasive", "Everyday", "Evidence", "Eviction", "Evildoer's",
"Evolution", "Exacerbation", "Exalted", "Examiner's", "Excise", "Exciting", "Exclusion",
"Exec", "Executioner's", "Exile", "Existential", "Expedient", "Expert", "Expletive",
"Exploiter's", "Explosive", "Exponential", "Exposing", "Extortion", "Extraction",
"Extraneous", "Extravaganza", "Extreme", "Extraterrestrial", "Extremist", "Eerie" ]
owords = [ "Office" ] * 50 + [ "Outhouse", "Outpost" ]
cword = random.choice(cwords)
eword = random.choice(ewords)
oword = random.choice(owords)
return "%s %s %s" % (cword, eword, oword)
def menu_items(items):
return [ urwid.AttrWrap( ButtonText( cb, txt ), 'menu', 'selected') for (txt, cb) in items ]
def main_menu():
menu = [
("New Member", new_member),
("Renew Membership", renew_member),
("Display Member", display_member),
("Search", search_members),
("Exit", raise_abort),
]
listbox = urwid.ListBox( menu_items( menu ) )
return listbox
def push_wizard(name, pages, dimensions=(50, 10)):
state = {}
wiz = Wizard()
for page in pages:
wiz.add_panel( page(state) )
push_window( urwid.Filler( urwid.Padding(
urwid.LineBox(wiz), 'center', dimensions[0]),
'middle', dimensions[1] ), name )
def new_member(*args, **kwargs):
push_wizard("New Member", [
newmember.IntroPage,
newmember.InfoPage,
newmember.SignPage,
newmember.PassPage,
newmember.EndPage,
])
def renew_member(*args, **kwargs):
push_wizard("Renew Membership", [
renew.IntroPage,
renew.UserPage,
renew.TermPage,
renew.PayPage,
renew.EndPage,
])
def display_member(a):
push_wizard("Display Member", [
renew.UserPage,
info.InfoPage,
], (60, 15))
def search_members(a):
menu = [
("Members by term", search_term),
("Members by name", search_name),
("Back", raise_back),
]
listbox = urwid.ListBox( menu_items( menu ) )
push_window(listbox, "Search")
def search_name(a):
push_wizard("By Name", [ search.NamePage ])
def search_term(a):
push_wizard("By Term", [ search.TermPage ])
def run():
push_window( main_menu(), program_name() )
event_loop( ui )
def start():
ui.run_wrapper( run )
if __name__ == '__main__':
start()

View File

@ -0,0 +1,134 @@
import urwid
from csc.apps.urwid.widgets import *
from csc.apps.urwid.window import *
from csc.adm import accounts, members
from csc.common.excep import InvalidArgument
class IntroPage(WizardPanel):
def init_widgets(self):
self.widgets = [
urwid.Text( "Joining the Computer Science Club" ),
urwid.Divider(),
urwid.Text( "CSC membership is $2.00 for one term. Please ensure "
"the fee is deposited into the safe before continuing." ),
]
def focusable(self):
return False
class InfoPage(WizardPanel):
def init_widgets(self):
self.userid = WordEdit("UWdir ID: ")
self.name = SingleEdit("Full name: ")
self.program = SingleEdit("Program of Study: ")
self.widgets = [
urwid.Text( "Member Information - Please Check ID" ),
urwid.Divider(),
self.userid,
self.name,
self.program,
]
def check(self):
self.state['userid'] = self.userid.get_edit_text()
self.state['name'] = self.name.get_edit_text()
self.state['program'] = self.program.get_edit_text()
if len( self.state['userid'] ) < 4:
self.focus_widget( self.userid )
set_status("Username is too short")
return True
elif len( self.state['name'] ) < 4:
self.focus_widget( self.name )
set_status("Name is too short")
return True
clear_status()
class SignPage(WizardPanel):
def init_widgets(self):
self.widgets = [
urwid.Text( "Machine Usage Policy" ),
urwid.Divider(),
urwid.Text( "Ensure the new member has signed the "
"Machine Usage Policy. Accounts of users who have not "
"signed will be suspended if discovered." ),
]
def focusable(self):
return False
class PassPage(WizardPanel):
def init_widgets(self):
self.password = PassEdit("Password: ")
self.pwcheck = PassEdit("Re-enter: ")
self.widgets = [
urwid.Text( "Member Password" ),
urwid.Divider(),
self.password,
self.pwcheck,
]
def focus_widget(self, widget):
self.box.set_focus( self.widgets.index( widget ) )
def clear_password(self):
self.focus_widget( self.password )
self.password.set_edit_text("")
self.pwcheck.set_edit_text("")
def check(self):
self.state['password'] = self.password.get_edit_text()
pwcheck = self.pwcheck.get_edit_text()
if self.state['password'] != pwcheck:
self.clear_password()
set_status("Passwords do not match")
return True
elif len(self.state['password']) < 5:
self.clear_password()
set_status("Password is too short")
return True
clear_status()
class EndPage(WizardPanel):
def init_widgets(self):
self.headtext = urwid.Text("")
self.midtext = urwid.Text("")
self.widgets = [
self.headtext,
urwid.Divider(),
self.midtext,
]
def focusable(self):
return False
def check(self):
pop_window()
def activate(self):
try:
if not members.connected(): members.connect()
members.new( self.state['userid'], self.state['name'], self.state['program'] )
problem = None
except members.InvalidRealName:
problem = "Invalid real name"
except InvalidArgument, e:
if e.argname == 'uid' and e.explanation == 'duplicate uid':
problem = 'Duplicate userid'
else:
raise
if not problem:
try:
if not accounts.connected(): accounts.connect()
accounts.create_member( self.state['userid'], self.state['password'], self.state['name'] )
except accounts.NameConflict, e:
problem = str(e)
except accounts.NoAvailableIDs, e:
problem = str(e)
except accounts.InvalidArgument, e:
problem = str(e)
except accounts.LDAPException, e:
problem = str(e)
except accounts.KrbException, e:
problem = str(e)
if problem:
self.headtext.set_text("Failed to add member")
self.midtext.set_text("The error was: '%s'" % problem)
else:
self.headtext.set_text("Member Added")
self.midtext.set_text("Congratulations, %s has been added "
"successfully. Please run 'addhomedir %s'."
% (self.state['userid'], self.state['userid']))

View File

@ -0,0 +1,118 @@
import urwid
from csc.apps.urwid.widgets import *
from csc.apps.urwid.window import *
from csc.adm import members, terms
class IntroPage(WizardPanel):
def init_widgets(self):
self.widgets = [
urwid.Text( "Renewing Membership" ),
urwid.Divider(),
urwid.Text( "CSC membership is $2.00 per term. You may pre-register "
"for future terms if desired." )
]
def focusable(self):
return False
class UserPage(WizardPanel):
def init_widgets(self):
self.userid = WordEdit("Username: ")
self.widgets = [
urwid.Text( "Member Information" ),
urwid.Divider(),
self.userid,
]
def check(self):
self.state['userid'] = self.userid.get_edit_text()
self.state['member'] = None
if self.state['userid']:
if not members.connected(): members.connect()
self.state['member'] = members.get(self.userid.get_edit_text())
if not self.state['member']:
set_status("Member not found")
self.focus_widget(self.userid)
return True
class TermPage(WizardPanel):
def init_widgets(self):
self.start = SingleEdit("Start: ")
self.count = SingleIntEdit("Count: ")
self.widgets = [
urwid.Text( "Terms to Register" ),
urwid.Divider(),
self.start,
self.count,
]
def activate(self):
if not self.start.get_edit_text():
old_terms = []
if 'term' in self.state['member']:
old_terms = self.state['member']['term']
self.start.set_edit_text( terms.next_unregistered( old_terms ) )
self.count.set_edit_text( "1" )
def check(self):
try:
self.state['terms'] = terms.interval( self.start.get_edit_text(), self.count.value() )
except e:
self.focus_widget( self.start )
set_status( "Invalid start term" )
return True
for term in self.state['terms']:
if members.registered( self.state['userid'], term):
self.focus_widget( self.start )
set_status( "Already registered for " + term )
return True
if len(self.state['terms']) == 0:
self.focus_widget(self.count)
set_status( "Registering for zero terms?" )
return True
class PayPage(WizardPanel):
def init_widgets(self):
self.midtext = urwid.Text("")
self.widgets = [
urwid.Text("Membership Fee"),
urwid.Divider(),
self.midtext,
]
def focusable(self):
return False
def activate(self):
regterms = self.state['terms']
plural = "term"
if len(self.state['terms']) > 1:
plural = "terms"
self.midtext.set_text("You are registering for %d %s, and owe the "
"Computer Science Club $%d.00 in membership fees. "
"Please deposit the money in the safe before "
"continuing. " % ( len(regterms), plural, len(regterms * 2)))
class EndPage(WizardPanel):
def init_widgets(self):
self.headtext = urwid.Text("")
self.midtext = urwid.Text("")
self.widgets = [
self.headtext,
urwid.Divider(),
self.midtext,
]
def focusable(self):
return False
def activate(self):
try:
members.register( self.state['userid'], self.state['terms'] )
self.headtext.set_text("Registration Succeeded")
self.midtext.set_text("The member has been registered for the following "
"terms: " + ", ".join(self.state['terms']) + ".")
except Exception, e:
self.headtext.set_text("Failed to Register")
self.midtext.set_text("You may refund any fees paid or retry."
"The error was: '%s'" % e)
def check(self):
pop_window()

View File

@ -0,0 +1,69 @@
import urwid
from csc.apps.urwid.widgets import *
from csc.apps.urwid.window import *
from csc.adm import accounts, members, terms
from csc.common.excep import InvalidArgument
class TermPage(WizardPanel):
def init_widgets(self):
self.term = SingleEdit("Term: ")
self.widgets = [
urwid.Text( "Terms Members" ),
urwid.Divider(),
self.term,
]
def check(self):
if not members.connected(): members.connect()
try:
self.state['term'] = self.term.get_edit_text()
terms.parse( self.state['term'] )
except:
self.focus_widget( self.term )
set_status( "Invalid term" )
return True
mlist = members.list_term( self.state['term'] ).values()
pop_window()
member_list( mlist )
class NamePage(WizardPanel):
def init_widgets(self):
self.name = SingleEdit("Name: ")
self.widgets = [
urwid.Text( "Members by Name" ),
urwid.Divider(),
self.name,
]
def check(self):
if not members.connected(): members.connect()
self.state['name'] = self.name.get_edit_text()
if not self.state['name']:
self.focus_widget( self.term )
set_status( "Invalid name" )
return True
mlist = members.list_name( self.state['name'] ).values()
pop_window()
member_list( mlist )
def member_list(mlist):
mlist = list(mlist)
mlist.sort( lambda x, y: cmp(x['uid'], y['uid']) )
buf = ''
for member in mlist:
if 'uid' in member:
uid = member['uid'][0]
else:
uid = None
if 'program' in member:
program = member['program'][0]
else:
program = None
attrs = ( uid, member['cn'][0], program )
buf += "%10s %30s\n%41s\n\n" % attrs
set_status("Press escape to return to the menu")
push_window(urwid.ListBox([urwid.Text(buf)]))

View File

@ -0,0 +1,92 @@
import urwid
class ButtonText(urwid.Text):
def __init__(self, callback, *args, **kwargs):
self.callback = callback
urwid.Text.__init__(self, *args, **kwargs)
def selectable(self):
return True
def keypress(self, size, key):
if key == 'enter':
self.callback(self.get_text())
else:
return key
class SingleEdit(urwid.Edit):
def keypress(self, size, key):
if key == 'enter':
return urwid.Edit.keypress(self, size, 'down')
else:
return urwid.Edit.keypress(self, size, key)
class SingleIntEdit(urwid.IntEdit):
def keypress(self, size, key):
if key == 'enter':
return urwid.Edit.keypress(self, size, 'down')
else:
return urwid.Edit.keypress(self, size, key)
class WordEdit(SingleEdit):
def valid_char(self, ch):
return urwid.Edit.valid_char(self, ch) and ch != ' '
class PassEdit(SingleEdit):
def get_text(self):
text = urwid.Edit.get_text(self)
return (self.caption + " " * len(self.get_edit_text()), text[1])
class Wizard(urwid.WidgetWrap):
def __init__(self):
self.selected = None
self.panels = []
self.panelwrap = urwid.WidgetWrap( urwid.SolidFill() )
self.back = urwid.Button("Back", self.back)
self.next = urwid.Button("Next", self.next)
self.buttons = urwid.Columns( [ self.back, self.next ], dividechars=3, focus_column=1 )
pad = urwid.Padding( self.buttons, ('fixed right', 2), 19 )
self.pile = urwid.Pile( [self.panelwrap, ('flow', pad)], 0 )
urwid.WidgetWrap.__init__(self, self.pile)
def add_panel(self, panel):
self.panels.append( panel )
if len(self.panels) == 1:
self.select(0)
def select(self, panelno, set_focus=True):
if 0 <= panelno < len(self.panels):
self.selected = panelno
self.panelwrap.set_w( self.panels[panelno] )
self.panels[panelno].activate()
if set_focus:
if self.panels[panelno].focusable():
self.pile.set_focus( 0 )
else:
self.pile.set_focus( 1 )
def next(self, *args, **kwargs):
if self.panels[self.selected].check():
self.select( self.selected )
return
self.select(self.selected + 1)
def back(self, *args, **kwargs):
self.select(self.selected - 1, False)
class WizardPanel(urwid.WidgetWrap):
def __init__(self, state):
self.state = state
self.init_widgets()
self.box = urwid.ListBox( urwid.SimpleListWalker( self.widgets ) )
urwid.WidgetWrap.__init__( self, self.box )
def init_widgets(self):
self.widgets = []
def focus_widget(self, widget):
self.box.set_focus( self.widgets.index( widget ) )
def focusable(self):
return True
def check(self):
return
def activate(self):
return

View File

@ -0,0 +1,65 @@
import urwid
window_stack = []
window_names = []
header = urwid.Text( "" )
footer = urwid.Text( "" )
top = urwid.Frame( urwid.SolidFill(), header, footer )
def push_window( frame, name=None ):
window_stack.append( frame )
window_names.append( name )
update_top()
def pop_window():
if len(window_stack) == 1:
return False
window_stack.pop()
window_names.pop()
update_top()
return True
def update_top():
names = [ n for n in window_names if n ]
header.set_text(" - ".join( names ) + "\n")
top.set_body( window_stack[-1] )
def set_status(message):
footer.set_text(message)
def clear_status():
footer.set_text("")
class Abort(Exception):
pass
class Back(Exception):
pass
def raise_abort(*args, **kwargs):
raise Abort()
def raise_back(*args, **kwarg):
raise Back()
def event_loop(ui):
while True:
try:
cols, rows = ui.get_cols_rows()
canvas = top.render( (cols, rows), focus=True )
ui.draw_screen( (cols, rows), canvas )
keys = ui.get_input()
for k in keys:
if k == "esc":
if not pop_window():
break
elif k == "window resize":
(cols, rows) = ui.get_cols_rows()
else:
top.keypress( (cols, rows), k )
except Back:
pop_window()
except (Abort, KeyboardInterrupt):
return