New release (version 0.2).

Updates in this version:

  * Tests added to most Python modules.
  * Split configuration files.
  * Added maintainer scripts to manage permissions during install and purge.
  * Added functions for use by tools planned for next release (chfn, etc).

ceo:

  * Added support for account "repair", which will recreate LDAP entries
    and Kerberos principals if necessary.
  * The recreate account menu option is now active.

Miscellaneous:

  * Replaced instances of "== None" and "!= None" with "is None" and
    "is not None", respectively (thanks to: Nick Guenther).
  * Renamed terms.valid() to terms.validate() (thanks to: Nick Guenther).
This commit is contained in:
Michael Spang 2007-01-27 19:23:18 -05:00
parent cb59e85c2e
commit 58bf72726a
36 changed files with 2365 additions and 709 deletions

16
bin/ceo
View File

@ -1,22 +1,20 @@
#!/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_MESSAGE', 'LC_MONETARY',
'LC_NUMERIC', 'LC_TIME', 'UID', 'GID',
'SSH_CONNECTION', 'SSH_AUTH_SOCK',
'SSH_CLIENT']
safe_environment = ['LOGNAME', 'USERNAME', 'USER', 'HOME', 'TERM', 'LANG'
'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MESSAGE', 'LC_MONETARY',
'LC_NUMERIC', 'LC_TIME', 'UID', 'GID', 'SSH_CONNECTION', 'SSH_AUTH_SOCK',
'SSH_CLIENT']
for key in os.environ.keys():
if not key in safe_environment:
if key not in safe_environment:
del os.environ[key]
os.environ['PATH'] = '/bin:/usr/bin'
for dir in sys.path[:]:
if not dir.find('/usr') == 0 or dir.find('/usr/local') == 0:
if not dir.find('/usr') == 0:
while dir in sys.path:
sys.path.remove(dir)

15
debian/changelog vendored
View File

@ -1,3 +1,18 @@
csc (0.2) unstable; urgency=low
* Tests added to most Python modules.
* Split configuration files.
* Added maintainer scripts to manage permissions during install and purge.
* Added functions for use by tools planned for next release (chfn, etc).
* Added support for account "repair", which will recreate LDAP entries
and principals if necessary.
* The recreate account menu option in CEO is now active.
* Replaced instances of "== None" and "!= None" with "is None" and
"is not None", respectively (thanks to: Nick Guenther).
* Renamed terms.valid() to terms.validate() (thanks to: Nick Guenther).
-- Michael Spang <mspang@uwaterloo.ca> Fri, 26 Jan 2007 20:10:14 -0500
csc (0.1) unstable; urgency=low
* Initial Release.

4
debian/control vendored
View File

@ -3,11 +3,11 @@ Section: admin
Priority: optional
Maintainer: Michael Spang <mspang@uwaterloo.ca>
Build-Depends: debhelper (>= 4.0.0)
Standards-Version: 3.6.1
Standards-Version: 3.7.2
Package: csc
Architecture: any
Depends: python, python2.4, python2.4-ldap, python2.4-pygresql, krb5-user, less
Depends: python, python2.4, python2.4-ldap, python2.4-pygresql, krb5-user, less, ${shlibs:Depends}
Description: Computer Science Club Administrative Utilities
This package contains the CSC Electronic Office
and other Computer Science Club administrative

4
debian/copyright vendored
View File

@ -1,7 +1,7 @@
This package was debianized by mspang <mspang@uwaterloo.ca> on
This package was debianized by Michael Spang <mspang@uwaterloo.ca> on
Thu, 28 Dec 2006 04:07:03 -0500.
Copyright (c) 2006, 2007 Michael Spang
Copyright (c) 2006-2007, Michael Spang
All rights reserved.
Redistribution and use in source and binary forms, with or without

58
debian/postinst vendored Normal file
View File

@ -0,0 +1,58 @@
#!/bin/bash -e
case "$1" in
configure|upgrade)
if getent passwd ceo > /dev/null; then
CEO=ceo
SUID=4750
else
CEO=root
SUID=755
fi
if getent group office > /dev/null; then
OFFICE=office
else
OFFICE=root
fi
if ! dpkg-statoverride --list /usr/bin/ceo > /dev/null; then
dpkg-statoverride --add --update $CEO $OFFICE $SUID /usr/bin/ceo
fi
if [ -f /etc/csc/ldap.cf ] && ! dpkg-statoverride --list /etc/csc/ldap.cf > /dev/null; then
dpkg-statoverride --add --update $CEO staff 640 /etc/csc/ldap.cf
fi
if [ ! -e /etc/csc/ceo.keytab ] && [ -x /usr/sbin/kadmin.local ]; then
if dpkg-statoverride --list /etc/csc/ceo.keytab > /dev/null; then
dpkg-statoverride --remove /etc/csc/ceo.keytab || true
fi
echo 'warning: re-creating ceo.keytab'
echo 'ktadd -k /etc/csc/ceo.keytab ceo/admin' | /usr/sbin/kadmin.local || true
if [ -e /etc/csc/ceo.keytab ]; then
echo -e "\nSuccess!"
else
echo -e "\nFailed!"
fi
fi
if [ -f /etc/csc/ceo.keytab ] && ! dpkg-statoverride --list /etc/csc/ceo.keytab > /dev/null; then
dpkg-statoverride --add --update $CEO staff 640 /etc/csc/ceo.keytab
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \"$1\"" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

33
debian/postrm vendored Normal file
View File

@ -0,0 +1,33 @@
#!/bin/bash -e
case "$1" in
purge)
if dpkg-statoverride --list /usr/bin/ceo > /dev/null; then
dpkg-statoverride --remove /usr/bin/ceo || true
fi
if dpkg-statoverride --list /etc/csc/ldap.cf > /dev/null; then
dpkg-statoverride --remove /etc/csc/ldap.cf || true
fi
if dpkg-statoverride --list /etc/csc/ceo.keytab > /dev/null; then
dpkg-statoverride --remove /etc/csc/ceo.keytab || true
fi
rmdir --ignore-fail-on-non-empty /etc/csc
;;
remove|failed-upgrade|upgrade)
;;
*)
echo "postrm called with invalid argument \"$1\"" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

30
debian/rules vendored
View File

@ -2,13 +2,11 @@
PYTHON := python2.4
configure:
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"' -o build/ceo misc/setuid-prog.c
touch build-stamp
clean:
@ -17,27 +15,22 @@ clean:
dh_clean
rm -f build-stamp
rm -rf build/
find pylib/ -name '*.pyc' -print0 | xargs -0 rm -f
find pylib/ -name "*.pyc" -print0 | xargs -0 rm -f
install: build
dh_testdir
dh_testroot
dh_clean -k
# configuration files will contain sensitive information
chmod 600 etc/*
dh_installdirs etc/csc usr/lib/$(PYTHON)/site-packages usr/share/csc \
usr/lib/csc usr/bin
dh_install -X.svn -X.pyc pylib/csc usr/lib/$(PYTHON)/site-packages/
dh_install -X.svn -X.pyc etc/* etc/csc/
dh_install -X.svn -X.pyc sql/* usr/share/csc/
dh_install pylib/* usr/lib/$(PYTHON)/site-packages/
dh_install etc/* etc/csc/
dh_install sql/* usr/share/csc/
dh_install -X.svn -X.pyc bin/ceo usr/lib/csc/
dh_install -X.svn -X.pyc build/ceo usr/bin/
dh_install bin/ceo usr/lib/csc/
dh_install build/ceo usr/bin/
binary-indep: build install
binary-arch: build install
dh_testdir
dh_testroot
dh_installchangelogs
@ -60,7 +53,8 @@ binary-indep: build install
dh_md5sums
dh_builddeb
binary: binary-indep binary-arch
.PHONY: build clean binary-indep binary-arch binary install configure
binary-indep:
binary-arch: build install
binary: binary-indep binary-arch
.PHONY: build clean binary-indep binary-arch binary install

View File

@ -3,6 +3,6 @@ Bugs and Caveats
================
CEO:
- curses does not draw borders/lines correctly in a screen session
- windows don't always clear properly
- the menu is not redrawn between windows and therefore a gap may grow there
- curses does not draw borders/lines correctly in a screen session. screen apparently ignores
some font-changing characters. workaround should be possible (other progs work).
- the menu is not redrawn between windows and therefore a gap tends to grow there

8
docs/TODO Normal file
View File

@ -0,0 +1,8 @@
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

@ -1,35 +1,44 @@
# $Id: accounts.cf 45 2007-01-02 01:39:10Z mspang $
# CSC Accounts Configuration
# /etc/csc/accounts.cf: CSC Accounts Configuration
### Account Options ###
include /etc/csc/ldap.cf
include /etc/csc/kerberos.cf
minimum_id = 20000
maximum_id = 40000
### Member Account Options ###
shell = "/bin/bash"
home = "/users"
gid = 100
member_min_id = 20000
member_max_id = 39999
member_shell = "/bin/bash"
member_home = "/users"
member_desc = "CSC Member Account"
member_group = "users"
### Club Account Options ###
### LDAP Configuration ###
club_min_id = 15000
club_max_id = 19999
club_shell = "/bin/bash"
club_home = "/users"
club_desc = "CSC Club Account"
club_group = "users"
server_url = "ldap:///"
### Administrative Account Options
users_base = "ou=People,dc=csclub,dc=uwaterloo,dc=ca"
groups_base = "ou=Group,dc=csclub,dc=uwaterloo,dc=ca"
admin_min_id = 10000
admin_max_id = 14999
admin_shell = "/bin/bash"
admin_home = "/users"
admin_desc = "CSC Administrative Account"
admin_group = "users"
bind_dn = "cn=ceo,dc=csclub,dc=uwaterloo,dc=ca"
bind_password = "secret"
### Kerberos Configuration ###
realm = "CSCLUB.UWATERLOO.CA"
principal = "ceo/admin@CSCLUB.UWATERLOO.CA"
keytab = "/etc/csc/ceo.keytab"
### Account Group Options ###
group_min_id = 10000
group_max_id = 14999
group_desc = "CSC Group"
### Validation Tuning ###
username_regex = "^[a-z][-a-z0-9]*$"
realname_regex = "^[^,:=]*$"
groupname_regex = "^[a-z][-a-z0-9]*$"
min_password_length = 4
shells_file = "/etc/shells"

5
etc/kerberos.cf Normal file
View File

@ -0,0 +1,5 @@
# /etc/csc/kerberos.cf: CSC Kerberos Administration Configuration
realm = "CSCLUB.UWATERLOO.CA"
admin_principal = "ceo/admin@CSCLUB.UWATERLOO.CA"
admin_keytab = "/etc/csc/ceo.keytab"

9
etc/ldap.cf Normal file
View File

@ -0,0 +1,9 @@
# /etc/csc/ldap.cf: CSC LDAP Configuration
server_url = "ldaps:///"
users_base = "ou=People,dc=csclub,dc=uwaterloo,dc=ca"
groups_base = "ou=Group,dc=csclub,dc=uwaterloo,dc=ca"
admin_bind_dn = "cn=ceo,dc=csclub,dc=uwaterloo,dc=ca"
admin_bind_pw = "secret"

View File

@ -1,13 +1,6 @@
# $Id: members.cf 45 2007-01-02 01:39:10Z mspang $
# CSC Members Configuration
# /etc/csc/members.cf: CSC Members Configuration
### Database Configuration ###
server = "localhost"
database = "ceo"
user = "ceo"
password = "secret"
include /etc/csc/pgsql.cf
### Validation Tuning ###

11
etc/pgsql.cf Normal file
View File

@ -0,0 +1,11 @@
# /etc/csc/pgsql.cf: PostgreSQL database configuration
### Database Configuration ###
# server = "localhost"
server = ""
database = "ceo"
# not used
user = "ceo"
password = "secret"

View File

@ -1,19 +1,5 @@
# $Id: __init__.py 24 2006-12-18 20:23:12Z mspang $
"""
PyCSC - CSC Administrative Utilities
Member Management:
ceo - legacy ceo interface
Account Management:
ceo - legacy ceo interface
Modules:
admin - administrative code (member and account management)
backend - backend interface code
ui - user interface code
Computer Science Club Python Modules
The csc module is a container for all CSC-specific Python modules.
"""

View File

@ -0,0 +1,9 @@
"""
CSC Administrative Modules
This module provides member and account management modules.
members - member registration management functions
accounts - account administration functions
terms - helper routines for manipulating terms
"""

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
# $Id: members.py 44 2006-12-31 07:09:27Z mspang $
"""
CSC Member Management
@ -10,44 +9,29 @@ Transactions are used in each method that modifies the database.
Future changes to the members database that need to be atomic
must also be moved into this module.
"""
import re
from csc.adm import terms
from csc.backends import db
from csc.common.conf import read_config
from csc.common import conf
### Configuration
### Configuration ###
CONFIG_FILE = '/etc/csc/members.cf'
cfg = {}
def load_configuration():
"""Load Members Configuration"""
# configuration already loaded?
if len(cfg) > 0:
return
string_fields = [ 'studentid_regex', 'realname_regex', 'server',
'database', 'user', 'password' ]
# read in the file
cfg_tmp = read_config(CONFIG_FILE)
# read configuration file
cfg_tmp = conf.read(CONFIG_FILE)
if not cfg_tmp:
raise MemberException("unable to read configuration file: %s"
% CONFIG_FILE)
# check that essential fields are completed
mandatory_fields = [ 'server', 'database', 'user', 'password' ]
for field in mandatory_fields:
if not field in cfg_tmp:
raise MemberException("missing configuratino option: %s" % field)
if not cfg_tmp[field]:
raise MemberException("null configuration option: %s" %field)
# verify configuration
conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
# update the current configuration with the loaded values
cfg.update(cfg_tmp)
@ -56,24 +40,46 @@ def load_configuration():
### Exceptions ###
DBException = db.DBException
ConfigurationException = conf.ConfigurationException
class MemberException(Exception):
"""Exception class for member-related errors."""
"""Base exception class for member-related errors."""
class DuplicateStudentID(MemberException):
"""Exception class for student ID conflicts."""
pass
def __init__(self, studentid):
self.studentid = studentid
def __str__(self):
return "Student ID already exists in the database: %s" % self.studentid
class InvalidStudentID(MemberException):
"""Exception class for malformed student IDs."""
pass
def __init__(self, studentid):
self.studentid = studentid
def __str__(self):
return "Student ID is invalid: %s" % self.studentid
class InvalidTerm(MemberException):
"""Exception class for malformed terms."""
pass
def __init__(self, term):
self.term = term
def __str__(self):
return "Term is invalid: %s" % self.term
class InvalidRealName(MemberException):
"""Exception class for invalid real names."""
def __init__(self, name):
self.name = name
def __str__(self):
return "Name is invalid: %s" % self.name
class NoSuchMember(MemberException):
"""Exception class for nonexistent members."""
pass
def __init__(self, memberid):
self.memberid = memberid
def __str__(self):
return "Member not found: %d" % self.memberid
@ -82,12 +88,10 @@ class NoSuchMember(MemberException):
# global database connection
connection = db.DBConnection()
def connect():
"""Connect to PostgreSQL."""
load_configuration()
connection.connect(cfg['server'], cfg['database'])
@ -103,24 +107,27 @@ def connected():
return connection.connected()
### Member Table ###
def new(realname, studentid=None, program=None):
def new(realname, studentid=None, program=None, mtype='user', userid=None):
"""
Registers a new CSC member. The member is added
to the members table and registered for the current
term.
Registers a new CSC member. The member is added to the members table
and registered for the current term.
Parameters:
realname - the full real name of the member
studentid - the student id number of the member
program - the program of study of the member
mtype - a string describing the type of member ('user', 'club')
userid - the initial user id
Returns: the memberid of the new member
Exceptions:
DuplicateStudentID - if the student id already exists in the database
InvalidStudentID - if the student id is malformed
InvalidRealName - if the real name is malformed
Example: new("Michael Spang", program="CS") -> 3349
"""
@ -128,16 +135,21 @@ def new(realname, studentid=None, program=None):
# blank attributes should be NULL
if studentid == '': studentid = None
if program == '': program = None
if userid == '': userid = None
if mtype == '': mtype = None
# check the student id format
regex = '^[0-9]{8}$'
if studentid != None and not re.match(regex, str(studentid)):
raise InvalidStudentID("student id is invalid: %s" % studentid)
if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)):
raise InvalidStudentID(studentid)
# check real name format (UNIX account real names must not contain [,:=])
if not re.match(cfg['realname_regex'], realname):
raise InvalidRealName(realname)
# check for duplicate student id
member = connection.select_member_by_studentid(studentid)
if member:
raise DuplicateStudentID("student id exists in database: %s" % studentid)
raise DuplicateStudentID(studentid)
# add the member
memberid = connection.insert_member(realname, studentid, program)
@ -155,9 +167,6 @@ def get(memberid):
"""
Look up attributes of a member by memberid.
Parameters:
memberid - the member id number
Returns: a dictionary of attributes
Example: get(3349) -> {
@ -188,7 +197,7 @@ def get_userid(userid):
}
"""
return connection.select_member_by_account(userid)
return connection.select_member_by_userid(userid)
def get_studentid(studentid):
@ -265,20 +274,23 @@ def delete(memberid):
"""
Erase all records of a member.
Note: real members are never removed
from the database
Note: real members are never removed from the database
Parameters:
memberid - the member id number
Returns: attributes and terms of the member in a tuple
Returns: attributes and terms of the
member in a tuple
Exceptions:
NoSuchMember - if the member id does not exist
Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993'])
"""
# save member data
member = connection.select_member_by_id(memberid)
# bail if not found
if not member:
raise NoSuchMember(memberid)
term_list = connection.select_terms(memberid)
# remove data from the db
@ -291,13 +303,12 @@ def delete(memberid):
def update(member):
"""
Update CSC member attributes. None is NULL.
Update CSC member attributes.
Parameters:
member - a dictionary with member attributes as
returned by get, possibly omitting some
attributes. member['memberid'] must exist
and be valid.
member - a dictionary with member attributes as returned by get,
possibly omitting some attributes. member['memberid']
must exist and be valid. None is NULL.
Exceptions:
NoSuchMember - if the member id does not exist
@ -307,20 +318,18 @@ def update(member):
Example: update( {'memberid': 3349, userid: 'mspang'} )
"""
if member.has_key('studentid') and member['studentid'] != None:
if member.has_key('studentid') and member['studentid'] is not None:
studentid = member['studentid']
# check the student id format
regex = '^[0-9]{8}$'
if studentid != None and not re.match(regex, str(studentid)):
raise InvalidStudentID("student id is invalid: %s" % studentid)
if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)):
raise InvalidStudentID(studentid)
# check for duplicate student id
member = connection.select_member_by_studentid(studentid)
if member:
raise DuplicateStudentID("student id exists in database: %s" %
studentid)
dupmember = connection.select_member_by_studentid(studentid)
if dupmember:
raise DuplicateStudentID(studentid)
# not specifying memberid is a bug
if not member.has_key('memberid'):
@ -328,10 +337,8 @@ def update(member):
memberid = member['memberid']
# see if member exists
old_member = connection.select_member_by_id(memberid)
if not old_member:
raise NoSuchMember("memberid does not exist in database: %d" %
memberid)
if not get(memberid):
raise NoSuchMember(memberid)
# do the update
connection.update_member(member)
@ -359,14 +366,14 @@ def register(memberid, term_list):
Example: register(3349, ["w2007", "s2007"])
"""
if not type(term_list) in (list, tuple):
if type(term_list) in (str, unicode):
term_list = [ term_list ]
for term in term_list:
# check term syntax
if not re.match('^[wsf][0-9]{4}$', term):
raise InvalidTerm("term is invalid: %s" % term)
raise InvalidTerm(term)
# add term to database
connection.insert_term(memberid, term)
@ -388,10 +395,10 @@ def registered(memberid, term):
Example: registered(3349, "f2006") -> True
"""
return connection.select_term(memberid, term) != None
return connection.select_term(memberid, term) is not None
def terms_list(memberid):
def member_terms(memberid):
"""
Retrieves a list of terms a member is
registered for.
@ -404,7 +411,9 @@ def terms_list(memberid):
Example: registered(0) -> 's1993'
"""
return connection.select_terms(memberid)
terms_list = connection.select_terms(memberid)
terms_list.sort(terms.compare)
return terms_list
@ -412,15 +421,104 @@ def terms_list(memberid):
if __name__ == '__main__':
connect()
sid = new("Test User", "99999999", "CS")
from csc.common.test import *
assert registered(id, terms.current())
print get(sid)
register(sid, terms.next(terms.current()))
assert registered(sid, terms.next(terms.current()))
print terms_list(sid)
print get(sid)
print delete(sid)
# t=test m=member s=student u=updated
tmname = 'Test Member'
tmprogram = 'Metaphysics'
tmsid = '00000000'
tm2name = 'Test Member 2'
tm2sid = '00000001'
tm2uname = 'Test Member II'
tm2usid = '00000002'
tm2uprogram = 'Pseudoscience'
tm2uuserid = 'testmember'
tmdict = {'name': tmname, 'userid': None, 'program': tmprogram, 'type': 'user', 'studentid': tmsid }
tm2dict = {'name': tm2name, 'userid': None, 'program': None, 'type': 'user', 'studentid': tm2sid }
tm2udict = {'name': tm2uname, 'userid': tm2uuserid, 'program': tm2uprogram, 'type': 'user', 'studentid': tm2usid }
thisterm = terms.current()
nextterm = terms.next(thisterm)
test(connect)
connect()
success()
test(connected)
assert_equal(True, connected())
success()
dmid = get_studentid(tmsid)
if dmid: delete(dmid['memberid'])
dmid = get_studentid(tm2sid)
if dmid: delete(dmid['memberid'])
dmid = get_studentid(tm2usid)
if dmid: delete(dmid['memberid'])
test(new)
tmid = new(tmname, tmsid, tmprogram)
tm2id = new(tm2name, tm2sid)
success()
tmdict['memberid'] = tmid
tm2dict['memberid'] = tm2id
tm2udict['memberid'] = tm2id
test(registered)
assert_equal(True, registered(tmid, thisterm))
assert_equal(True, registered(tm2id, thisterm))
assert_equal(False, registered(tmid, nextterm))
success()
test(get)
assert_equal(tmdict, get(tmid))
assert_equal(tm2dict, get(tm2id))
success()
test(list_name)
assert_equal(True, tmid in [ x['memberid'] for x in list_name(tmname) ])
assert_equal(True, tm2id in [ x['memberid'] for x in list_name(tm2name) ])
success()
test(register)
register(tmid, terms.next(terms.current()))
assert_equal(True, registered(tmid, nextterm))
success()
test(member_terms)
assert_equal([thisterm, nextterm], member_terms(tmid))
assert_equal([thisterm], member_terms(tm2id))
success()
test(list_term)
assert_equal(True, tmid in [ x['memberid'] for x in list_term(thisterm) ])
assert_equal(True, tmid in [ x['memberid'] for x in list_term(nextterm) ])
assert_equal(True, tm2id in [ x['memberid'] for x in list_term(thisterm) ])
assert_equal(False, tm2id in [ x['memberid'] for x in list_term(nextterm) ])
success()
test(update)
update(tm2udict)
assert_equal(tm2udict, get(tm2id))
success()
test(get_userid)
assert_equal(tm2udict, get_userid(tm2uuserid))
success()
test(get_studentid)
assert_equal(tm2udict, get_studentid(tm2usid))
assert_equal(tmdict, get_studentid(tmsid))
success()
test(delete)
delete(tmid)
delete(tm2id)
success()
test(disconnect)
disconnect()
assert_equal(False, connected())
disconnect()
success()

View File

@ -1,11 +1,9 @@
# $Id: terms.py 44 2006-12-31 07:09:27Z mspang $
"""
Terms Routines
This module contains functions for manipulating
terms, such as determining the current term,
finding the next or previous term, converting
dates to terms, and more.
This module contains functions for manipulating terms, such as determining
the current term, finding the next or previous term, converting dates to
terms, and more.
"""
import time, datetime, re
@ -16,27 +14,27 @@ EPOCH = 1970
SEASONS = [ 'w', 's', 'f' ]
def valid(term):
def validate(term):
"""
Determines whether a term is well-formed:
Determines whether a term is well-formed.
Parameters:
term - the term string
Returns: whether the term is valid (boolean)
Example: valid("f2006") -> True
Example: validate("f2006") -> True
"""
regex = '^[wsf][0-9]{4}$'
return re.match(regex, term) != None
return re.match(regex, term) is not None
def parse(term):
"""Helper function to convert a term string to the number of terms
since the epoch. Such numbers are intended for internal use only."""
if not valid(term):
if not validate(term):
raise Exception("malformed term: %s" % term)
year = int( term[1:] )
@ -176,8 +174,8 @@ def from_timestamp(timestamp):
This function notes that:
WINTER = JANUARY to APRIL
SPRING = MAY TO AUGUST
FALL = SEPTEMBER TO DECEMBER
SPRING = MAY to AUGUST
FALL = SEPTEMBER to DECEMBER
Parameters:
timestamp - number of seconds since the epoch
@ -235,18 +233,22 @@ def next_unregistered(registered):
if __name__ == '__main__':
assert parse('f2006') == 110
assert generate(110) == 'f2006'
assert next('f2006') == 'w2007'
assert previous('f2006') == 's2006'
assert delta('f2006', 'w2007') == 1
assert add('f2006', delta('f2006', 'w2010')) == 'w2010'
assert interval('f2006', 3) == ['f2006', 'w2007', 's2007']
assert from_timestamp(1166135779) == 'f2006'
assert parse( current() ) >= 110
assert next_unregistered( [current()] ) == next( current() )
assert next_unregistered( [] ) == current()
assert next_unregistered( [previous(current())] ) == current()
assert next_unregistered( [add(current(), -2)] ) == current()
from csc.common.test import *
print "All tests passed." "\n"
test(parse); assert_equal(110, parse('f2006')); success()
test(generate); assert_equal('f2006', generate(110)); success()
test(next); assert_equal('w2007', next('f2006')); success()
test(previous); assert_equal('s2006', previous('f2006')); success()
test(delta); assert_equal(1, delta('f2006', 'w2007')); success()
test(compare); assert_equal(-1, compare('f2006', 'w2007')); success()
test(add); assert_equal('w2010', add('f2006', delta('f2006', 'w2010'))); success()
test(interval); assert_equal(['f2006', 'w2007', 's2007'], interval('f2006', 3)); success()
test(from_timestamp); assert_equal('f2006', from_timestamp(1166135779)); success()
test(current); assert_equal(True, parse( current() ) >= 110 ); success()
test(next_unregistered)
assert_equal( next(current()), next_unregistered([ current() ]))
assert_equal( current(), next_unregistered([]))
assert_equal( current(), next_unregistered([ previous(current()) ]))
assert_equal( current(), next_unregistered([ add(current(), -2) ]))
success()

View File

@ -1,9 +1,8 @@
# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $
"""
User Interfaces
Application-style User Interfaces
This module contains frontends and related modules.
CEO's primary frontends are:
This module contains large frontends with many functions
and fancy graphical user interfaces.
legacy - aims to reproduce the curses UI of the previous CEO
"""

View File

@ -1,10 +1,8 @@
# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $
"""
Legacy User Interface
This module contains the legacy CEO user interface and related modules.
Important modules are:
main.py - all of the main UI logic
helpers.py - user interface library routines
main - all of the main UI logic
helpers - user interface library routines
"""

View File

@ -1,4 +1,3 @@
# $Id: helpers.py 35 2006-12-28 05:14:05Z mspang $
"""
Helpers for legacy User Interface
@ -7,7 +6,7 @@ the look and behavior of the previous CEO. Included is code for various
curses-based UI widgets that were provided by Perl 5's Curses and
Curses::Widgets libraries.
Though attempts have been made to keep the UI bug-compatible with
Though attempts have been made to keep the UI [bug-]compatible with
the previous system, some compromises have been made. For example,
the input and textboxes draw 'OK' and 'Cancel' buttons where the old
CEO had them, but they are fake. That is, the buttons in the old
@ -52,14 +51,14 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# turn on cursor
try:
curses.curs_set(1)
except:
except curses.error:
pass
# set keypad mode to allow UP, DOWN, etc
wnd.keypad(1)
# the input string
input = ""
inputbuf = ""
# offset of cursor in input
# i.e. the next operation is applied at input[inputoff]
@ -78,7 +77,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
if echo:
# discard characters before displayoff,
# as the window may be scrolled to the right
substring = input[displayoff:]
substring = inputbuf[displayoff:]
# pad the string with zeroes to overwrite stale characters
substring = substring + " " * (width - len(substring))
@ -96,7 +95,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# enter returns input
if key == KEY_RETURN:
return input
return inputbuf
# escape aborts input
elif key == KEY_ESCAPE:
@ -104,7 +103,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# EOT (C-d) aborts if there is no input
elif key == KEY_EOT:
if len(input) == 0:
if len(inputbuf) == 0:
return None
# backspace removes the previous character
@ -112,7 +111,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
if inputoff > 0:
# remove the character immediately before the input offset
input = input[0:inputoff-1] + input[inputoff:]
inputbuf = inputbuf[0:inputoff-1] + inputbuf[inputoff:]
inputoff -= 1
# move either the cursor or entire line of text left
@ -124,7 +123,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
if inputoff < len(input):
# remove the character at the input offset
input = input[0:inputoff] + input[inputoff+1:]
inputbuf = inputbuf[0:inputoff] + inputbuf[inputoff+1:]
# left moves the cursor one character left
elif key == curses.KEY_LEFT:
@ -139,7 +138,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# right moves the cursor one character right
elif key == curses.KEY_RIGHT:
if inputoff < len(input):
if inputoff < len(inputbuf):
# move the cursor to the right
inputoff += 1
@ -155,8 +154,8 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# end moves the cursor past the last character
elif key == curses.KEY_END:
inputoff = len(input)
displayoff = len(input) - width + 1
inputoff = len(inputbuf)
displayoff = len(inputbuf) - width + 1
# insert toggles insert/overwrite mode
elif key == curses.KEY_IC:
@ -164,15 +163,15 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# other (printable) characters are added to the input string
elif curses.ascii.isprint(key):
if len(input) < maxlen or maxlen == 0:
if len(inputbuf) < maxlen or maxlen == 0:
# insert mode: insert before current offset
if insert:
input = input[0:inputoff] + chr(key) + input[inputoff:]
inputbuf = inputbuf[0:inputoff] + chr(key) + inputbuf[inputoff:]
# overwrite mode: replace current offset
else:
input = input[0:inputoff] + chr(key) + input[inputoff+1:]
inputbuf = inputbuf[0:inputoff] + chr(key) + inputbuf[inputoff+1:]
# increment the input offset
inputoff += 1
@ -218,13 +217,13 @@ def inputbox(wnd, prompt, field_width, echo=True):
# read an input string within the field region of text_wnd
inputy, inputx, inputwidth = 1, 1, textwidth - 2
input = read_input(text_wnd, inputy, inputx, inputwidth, 0, echo)
inputbuf = read_input(text_wnd, inputy, inputx, inputwidth, 0, echo)
# erase the window
child_wnd.erase()
child_wnd.refresh()
return input
return inputbuf
def line_wrap(line, width):
@ -323,7 +322,7 @@ def msgbox(wnd, msg, title="Message"):
curses.curs_set(0)
outer_wnd.keypad(1)
while True:
key = outer_wnd.getch(0,0)
key = outer_wnd.getch(0, 0)
if key == KEY_RETURN or key == KEY_ESCAPE:
break
@ -379,18 +378,18 @@ def menu(wnd, offy, offx, width, options, _acquire_wnd=None):
wnd.refresh()
# read one keypress
input = wnd.getch()
keypress = wnd.getch()
# UP moves to the previous option
if input == curses.KEY_UP and selected > 0:
if keypress == curses.KEY_UP and selected > 0:
selected = (selected - 1)
# DOWN moves to the next option
elif input == curses.KEY_DOWN and selected < len(options) - 1:
elif keypress == curses.KEY_DOWN and selected < len(options) - 1:
selected = (selected + 1)
# RETURN runs the callback for the selected option
elif input == KEY_RETURN:
elif keypress == KEY_RETURN:
text, callback = options[selected]
# highlight the selected option

View File

@ -1,4 +1,3 @@
# $Id: main.py 44 2006-12-31 07:09:27Z mspang $
"""
CEO-like Frontend
@ -21,7 +20,7 @@ BORDER_COLOR = curses.COLOR_RED
def action_new_member(wnd):
"""Interactively add a new member."""
username, studentid, program = '', None, ''
studentid, program = None, ''
# read the name
prompt = " Name: "
@ -33,7 +32,7 @@ def action_new_member(wnd):
# read the student id
prompt = "Student id:"
while studentid == None or (re.search("[^0-9]", studentid) and not studentid.lower() == 'exit'):
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
@ -48,7 +47,7 @@ def action_new_member(wnd):
program = inputbox(wnd, prompt, 18)
# abort if exit is entered
if program == None or program.lower() == 'exit':
if program is None or program.lower() == 'exit':
return False
# connect the members module to its backend if necessary
@ -59,14 +58,17 @@ def action_new_member(wnd):
memberid = members.new(realname, studentid, program)
msgbox(wnd, "Success! Your memberid is %s. You are now registered\n"
% memberid + "for the " + terms.current() + " term.");
% memberid + "for the " + terms.current() + " term.")
except members.InvalidStudentID:
msgbox(wnd, "Invalid student ID.")
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
def action_term_register(wnd):
@ -85,7 +87,7 @@ def action_term_register(wnd):
if not member: return False
memberid = member['memberid']
term_list = members.terms_list(memberid)
term_list = members.member_terms(memberid)
# display user
display_member_details(wnd, member, term_list)
@ -134,7 +136,7 @@ def action_term_register_multiple(wnd):
if not member: return False
memberid = member['memberid']
term_list = members.terms_list(memberid)
term_list = members.member_terms(memberid)
# display user
display_member_details(wnd, member, term_list)
@ -177,11 +179,57 @@ def action_term_register_multiple(wnd):
msgbox(wnd, "Your are now registered for terms: " + ", ".join(term_list))
except members.InvalidTerm:
msgbox(wnd, "Term is not valid: %s" % term)
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 you're 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."""
@ -198,7 +246,7 @@ def action_create_account(wnd):
if not member: return False
memberid = member['memberid']
term_list = members.terms_list(memberid)
term_list = members.member_terms(memberid)
# display the member
display_member_details(wnd, member, term_list)
@ -218,67 +266,59 @@ def action_create_account(wnd):
msgbox(wnd, "I suggest searching for the member by userid or name from the main menu.")
return False
# member already has an account?
if member['userid']:
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
# read user id
prompt = "Userid:"
while userid == '':
userid = inputbox(wnd, prompt, 18)
# user abort
if userid == None or userid.lower() == 'exit':
if userid is None or userid.lower() == 'exit':
return False
# member already has an account?
#if member['userid'] != None:
# msgbox(wnd, "Member " + str(memberid) + " already has an account: " + member['userid'] + "\n"
# "Contact the sysadmin if there are still problems." )
# return False
# 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)
# read password
password = input_password(wnd)
# create the UNIX account
result = accounts.create_account(userid, password, member['name'], memberid)
if result == accounts.LDAP_EXISTS:
msgbox(wnd, "Error: Could not do stuff , Already exists.")
try:
if not accounts.connected(): accounts.connect()
accounts.create_member(userid, password, member['name'], memberid)
except accounts.AccountExists, e:
msgbox