Initial import (version 0.1).

This commit is contained in:
Michael Spang 2007-01-27 18:41:51 -05:00
commit dfc747f9c6
29 changed files with 4244 additions and 0 deletions

24
bin/ceo Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/python2.4 --
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']
for key in os.environ.keys():
if not key 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:
while dir in sys.path:
sys.path.remove(dir)
import csc.apps.legacy.main
csc.apps.legacy.main.run()

6
debian/changelog vendored Normal file
View File

@ -0,0 +1,6 @@
csc (0.1) unstable; urgency=low
* Initial Release.
-- Michael Spang <mspang@uwaterloo.ca> Thu, 28 Dec 2006 04:07:03 -0500

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
4

14
debian/control vendored Normal file
View File

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

31
debian/copyright vendored Normal file
View File

@ -0,0 +1,31 @@
This package was debianized by mspang <mspang@uwaterloo.ca> on
Thu, 28 Dec 2006 04:07:03 -0500.
Copyright (c) 2006, 2007 Michael Spang
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the University of Waterloo Computer Science Club
nor the names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

66
debian/rules vendored Executable file
View File

@ -0,0 +1,66 @@
#!/usr/bin/make -f
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
touch build-stamp
clean:
dh_testdir
dh_testroot
dh_clean
rm -f build-stamp
rm -rf build/
find py/ -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/csc/*
dh_installdirs etc usr/lib/$(PYTHON)/site-packages usr/share/csc \
usr/lib/csc usr/bin
dh_install -X.svn -X.pyc py/csc usr/lib/$(PYTHON)/site-packages/
dh_install -X.svn -X.pyc etc/csc etc/
dh_install -X.svn -X.pyc 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/
binary-indep: build install
dh_testdir
dh_testroot
dh_installchangelogs
dh_installdocs docs/*
dh_installexamples
dh_install
# dh_installlogrotate
# dh_installcron
dh_installman
dh_link
dh_strip
dh_compress
dh_fixperms
# dh_perl
# dh_python
# dh_makeshlibs
dh_installdeb
dh_shlibdeps
dh_gencontrol
dh_md5sums
dh_builddeb
binary: binary-indep binary-arch
.PHONY: build clean binary-indep binary-arch binary install configure
binary-arch: build install

8
docs/BUGS Normal file
View File

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

35
etc/csc/accounts.cf Normal file
View File

@ -0,0 +1,35 @@
# $Id: accounts.cf 45 2007-01-02 01:39:10Z mspang $
# CSC Accounts Configuration
### Account Options ###
minimum_id = 20000
maximum_id = 40000
shell = "/bin/bash"
home = "/users"
gid = 100
### LDAP Configuration ###
server_url = "ldap:///"
users_base = "ou=People,dc=csclub,dc=uwaterloo,dc=ca"
groups_base = "ou=Group,dc=csclub,dc=uwaterloo,dc=ca"
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"
### Validation Tuning ###
username_regex = "^[a-z][-a-z0-9]*$"
realname_regex = "^[^,:=]*$"

15
etc/csc/members.cf Normal file
View File

@ -0,0 +1,15 @@
# $Id: members.cf 45 2007-01-02 01:39:10Z mspang $
# CSC Members Configuration
### Database Configuration ###
server = "localhost"
database = "ceo"
user = "ceo"
password = "secret"
### Validation Tuning ###
studentid_regex = "^[0-9]{8}$"
realname_regex = "^[^,:=]*$"

176
misc/setuid-prog.c Normal file
View File

@ -0,0 +1,176 @@
/*
Template for a setuid program that calls a script.
The script should be in an unwritable directory and should itself
be unwritable. In fact all parent directories up to the root
should be unwritable. The script must not be setuid, that's what
this program is for.
This is a template program. You need to fill in the name of the
script that must be executed. This is done by changing the
definition of FULL_PATH below.
There are also some rules that should be adhered to when writing
the script itself.
The first and most important rule is to never, ever trust that the
user of the program will behave properly. Program defensively.
Check your arguments for reasonableness. If the user is allowed to
create files, check the names of the files. If the program depends
on argv[0] for the action it should perform, check it.
Assuming the script is a Bourne shell script, the first line of the
script should be
#!/bin/sh -
The - is important, don't omit it. If you're using esh, the first
line should be
#!/usr/local/bin/esh -f
and for ksh, the first line should be
#!/usr/local/bin/ksh -p
The script should then set the variable IFS to the string
consisting of <space>, <tab>, and <newline>. After this (*not*
before!), the PATH variable should be set to a reasonable value and
exported. Do not expect the PATH to have a reasonable value, so do
not trust the old value of PATH. You should then set the umask of
the program by calling
umask 077 # or 022 if you want the files to be readable
If you plan to change directories, you should either unset CDPATH
or set it to a good value. Setting CDPATH to just ``.'' (dot) is a
good idea.
If, for some reason, you want to use csh, the first line should be
#!/bin/csh -fb
You should then set the path variable to something reasonable,
without trusting the inherited path. Here too, you should set the
umask using the command
umask 077 # or 022 if you want the files to be readable
*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
/* CONFIGURATION SECTION */
#ifndef FULL_PATH /* so that this can be specified from the Makefile */
/* Uncomment the following line:
#define FULL_PATH "/full/path/of/script"
* Then comment out the #error line. */
#error "You must define FULL_PATH somewhere"
#endif
#ifndef UMASK
#define UMASK 077
#endif
/* END OF CONFIGURATION SECTION */
#if defined(__STDC__) && defined(__sgi)
#define environ _environ
#endif
/* don't change def_IFS */
char def_IFS[] = "IFS= \t\n";
/* you may want to change def_PATH, but you should really change it in */
/* your script */
#ifdef __sgi
char def_PATH[] = "PATH=/usr/bsd:/usr/bin:/bin:/usr/local/bin:/usr/sbin";
#else
char def_PATH[] = "PATH=/usr/ucb:/usr/bin:/bin:/usr/local/bin";
#endif
/* don't change def_CDPATH */
char def_CDPATH[] = "CDPATH=.";
/* don't change def_ENV */
char def_ENV[] = "ENV=:";
/*
This function changes all environment variables that start with LD_
into variables that start with XD_. This is important since we
don't want the script that is executed to use any funny shared
libraries.
The other changes to the environment are, strictly speaking, not
needed here. They can safely be done in the script. They are done
here because we don't trust the script writer (just like the script
writer shouldn't trust the user of the script).
If IFS is set in the environment, set it to space,tab,newline.
If CDPATH is set in the environment, set it to ``.''.
Set PATH to a reasonable default.
*/
void
clean_environ(void)
{
char **p;
extern char **environ;
for (p = environ; *p; p++) {
if (strncmp(*p, "LD_", 3) == 0)
**p = 'X';
else if (strncmp(*p, "_RLD", 4) == 0)
**p = 'X';
else if (strncmp(*p, "PYTHON", 6) == 0)
**p = 'X';
else if (strncmp(*p, "IFS=", 4) == 0)
*p = def_IFS;
else if (strncmp(*p, "CDPATH=", 7) == 0)
*p = def_CDPATH;
else if (strncmp(*p, "ENV=", 4) == 0)
*p = def_ENV;
}
putenv(def_PATH);
}
int
main(int argc, char **argv)
{
struct stat statb;
gid_t egid = getegid();
uid_t euid = geteuid();
/*
Sanity check #1.
This check should be made compile-time, but that's not possible.
If you're sure that you specified a full path name for FULL_PATH,
you can omit this check.
*/
if (FULL_PATH[0] != '/') {
fprintf(stderr, "%s: %s is not a full path name\n", argv[0],
FULL_PATH);
fprintf(stderr, "You can only use this wrapper if you\n");
fprintf(stderr, "compile it with an absolute path.\n");
exit(1);
}
/*
Sanity check #2.
Check that the owner of the script is equal to either the
effective uid or the super user.
*/
if (stat(FULL_PATH, &statb) < 0) {
perror("stat");
exit(1);
}
if (statb.st_uid != 0 && statb.st_uid != euid) {
fprintf(stderr, "%s: %s has the wrong owner\n", argv[0],
FULL_PATH);
fprintf(stderr, "The script should be owned by root,\n");
fprintf(stderr, "and shouldn't be writeable by anyone.\n");
exit(1);
}
if (setregid(egid, egid) < 0)
perror("setregid");
if (setreuid(euid, euid) < 0)
perror("setreuid");
clean_environ();
umask(UMASK);
while (**argv == '-') /* don't let argv[0] start with '-' */
(*argv)++;
execv(FULL_PATH, argv);
fprintf(stderr, "%s: could not execute the script\n", argv[0]);
exit(1);
}

19
py/csc/__init__.py Normal file
View File

@ -0,0 +1,19 @@
# $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
"""

0
py/csc/admin/__init__.py Normal file
View File

232
py/csc/admin/accounts.py Normal file
View File

@ -0,0 +1,232 @@
# $Id: accounts.py 44 2006-12-31 07:09:27Z mspang $
# UNIX Accounts Module
import re
from csc.backend import ldapi, krb
from csc.lib import read_config
CONFIG_FILE = '/etc/csc/accounts.cf'
cfg = {}
# error constants
SUCCESS = 0
LDAP_EXISTS = 1
LDAP_NO_IDS = 2
LDAP_NO_USER = 3
KRB_EXISTS = 5
KRB_NO_USER = 6
BAD_USERNAME = 8
BAD_REALNAME = 9
# error messages
errors = [ "Success", "LDAP: entry exists",
"LDAP: no user ids available", "LDAP: no such entry",
"KRB: principal exists", "KRB: no such principal",
"Invalid username", "Invalid real name"]
class AccountException(Exception):
"""Exception class for account-related errors."""
def load_configuration():
"""Load Accounts Configuration."""
# configuration already loaded?
if len(cfg) > 0:
return
# read in the file
cfg_tmp = read_config(CONFIG_FILE)
if not cfg_tmp:
raise AccountException("unable to read configuration file: %s" % CONFIG_FILE)
# check that essential fields are completed
mandatory_fields = [ 'minimum_id', 'maximum_id', 'shell', 'home',
'gid', 'server_url', 'users_base', 'groups_base', 'bind_dn',
'bind_password', 'realm', 'principal', 'keytab', 'username_regex',
'realname_regex'
]
for field in mandatory_fields:
if not field in cfg_tmp:
raise AccountException("missing configuration option: %s" % field)
if not cfg_tmp[field]:
raise AccountException("null configuration option: %s" % field)
# check that numeric fields are ints
numeric_fields = [ 'minimum_id', 'maximum_id', 'gid' ]
for field in numeric_fields:
if not type(cfg_tmp[field]) in (int, long):
raise AccountException("non-numeric value for configuration option: %s" % field)
# update the current configuration with the loaded values
cfg.update(cfg_tmp)
def create_account(username, password, realname='', gecos_other=''):
"""
Creates a UNIX account for a member. This involves
first creating a directory entry, then creating
a Kerberos principal.
Parameters:
username - UNIX username for the member
realname - real name of the member
password - password for the account
Exceptions:
LDAPException - on LDAP failure
KrbException - on Kerberos failure
Returns:
SUCCESS - on success
BAD_REALNAME - on badly formed real name
BAD_USERNAME - on badly formed user name
LDAP_EXISTS - when the user exists in LDAP
LDAP_NO_IDS - when no user ids are free
KRB_EXISTS - when the user exists in Kerberos
"""
# Load Configuration
load_configuration()
### Connect to the Backends ###
ldap_connection = ldapi.LDAPConnection()
krb_connection = krb.KrbConnection()
try:
# connect to the LDAP server
ldap_connection.connect(cfg['server_url'], cfg['bind_dn'], cfg['bind_password'], cfg['users_base'], cfg['groups_base'])
# connect to the Kerberos master server
krb_connection.connect(cfg['principal'], cfg['keytab'])
### Sanity-checks ###
# check the username and realame for validity
if not re.match(cfg['username_regex'], username):
return BAD_USERNAME
if not re.match(cfg['realname_regex'], realname):
return BAD_REALNAME
# see if user exists in LDAP
if ldap_connection.user_lookup(username):
return LDAP_EXISTS
# determine the first available userid
userid = ldap_connection.first_id(cfg['minimum_id'], cfg['maximum_id'])
if not userid: return LDAP_NO_IDS
# build principal name from username
principal = username + '@' + cfg['realm']
# see if user exists in Kerberos
if krb_connection.get_principal(principal):
return KRB_EXISTS
### User creation ###
# process gecos_other (used to store memberid)
if gecos_other:
gecos_other = ',' + str(gecos_other)
# account information defaults
shell = cfg['shell']
home = cfg['home'] + '/' + username
gecos = realname + ',,,' + gecos_other
gid = cfg['gid']
# create the LDAP entry
ldap_connection.user_add(username, realname, shell, userid, gid, home, gecos)
# create the Kerberos principal
krb_connection.add_principal(principal, password)
finally:
ldap_connection.disconnect()
krb_connection.disconnect()
return SUCCESS
def delete_account(username):
"""
Deletes the UNIX account of a member.
Parameters:
username - UNIX username for the member
Exceptions:
LDAPException - on LDAP failure
KrbException - on Kerberos failure
Returns:
SUCCESS - on success
LDAP_NO_USER - when the user does not exist in LDAP
KRB_NO_USER - when the user does not exist in Kerberos
"""
# Load Configuration
load_configuration()
### Connect to the Backends ###
ldap_connection = ldapi.LDAPConnection()
krb_connection = krb.KrbConnection()
try:
# connect to the LDAP server
ldap_connection.connect(cfg['server_url'], cfg['bind_dn'], cfg['bind_password'], cfg['users_base'], cfg['groups_base'])
# connect to the Kerberos master server
krb_connection.connect(cfg['principal'], cfg['keytab'])
### Sanity-checks ###
# ensure user exists in LDAP
if not ldap_connection.user_lookup(username):
return LDAP_NO_USER
# build principal name from username
principal = username + '@' + cfg['realm']
# see if user exists in Kerberos
if not krb_connection.get_principal(principal):
return KRB_NO_USER
### User deletion ###
# delete the LDAP entry
ldap_connection.user_delete(username)
# delete the Kerberos principal
krb_connection.delete_principal(principal)
finally:
ldap_connection.disconnect()
krb_connection.disconnect()
return SUCCESS
### Tests ###
if __name__ == '__main__':
# A word of notice: this test creates a _working_ account (and then deletes it).
# If deletion fails it must be cleaned up manually.
# a bit of salt so the test account is reasonably tough to crack
import random
pw = str(random.randint(100000000000000000, 999999999999999999))
print "running create_account('testuser', ..., 'Test User', ...)", "->", errors[create_account('testuser', pw, 'Test User')]
print "running delete_account('testuser')", "->", errors[delete_account('testuser')]

426
py/csc/admin/members.py Normal file
View File

@ -0,0 +1,426 @@
# $Id: members.py 44 2006-12-31 07:09:27Z mspang $
"""
CSC Member Management
This module contains functions for registering new members, registering
members for terms, searching for members, and other member-related
functions.
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.admin import terms
from csc.backend import db
from csc.lib import read_config
### Configuration
CONFIG_FILE = '/etc/csc/members.cf'
cfg = {}
def load_configuration():
"""Load Members Configuration"""
# configuration already loaded?
if len(cfg) > 0:
return
# read in the file
cfg_tmp = read_config(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)
# update the current configuration with the loaded values
cfg.update(cfg_tmp)
### Exceptions ###
class MemberException(Exception):
"""Exception class for member-related errors."""
class DuplicateStudentID(MemberException):
"""Exception class for student ID conflicts."""
pass
class InvalidStudentID(MemberException):
"""Exception class for malformed student IDs."""
pass
class InvalidTerm(MemberException):
"""Exception class for malformed terms."""
pass
class NoSuchMember(MemberException):
"""Exception class for nonexistent members."""
pass
### Connection Management ###
# global database connection
connection = db.DBConnection()
def connect():
"""Connect to PostgreSQL."""
load_configuration()
connection.connect(cfg['server'], cfg['database'])
def disconnect():
"""Disconnect from PostgreSQL."""
connection.disconnect()
def connected():
"""Determine whether the connection has been established."""
return connection.connected()
### Member Table ###
def new(realname, studentid=None, program=None):
"""
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
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
Example: new("Michael Spang", program="CS") -> 3349
"""
# blank attributes should be NULL
if studentid == '': studentid = None
if program == '': program = 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)
# check for duplicate student id
member = connection.select_member_by_studentid(studentid)
if member:
raise DuplicateStudentID("student id exists in database: %s" % studentid)
# add the member
memberid = connection.insert_member(realname, studentid, program)
# register them for this term
connection.insert_term(memberid, terms.current())
# commit the transaction
connection.commit()
return memberid
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) -> {
'memberid': 3349,
'name': 'Michael Spang',
'program': 'Computer Science',
...
}
"""
return connection.select_member_by_id(memberid)
def get_userid(userid):
"""
Look up attributes of a member by userid.
Parameters:
userid - the UNIX user id
Returns: a dictionary of attributes
Example: get('mspang') -> {
'memberid': 3349,
'name': 'Michael Spang',
'program': 'Computer Science',
...
}
"""
return connection.select_member_by_account(userid)
def get_studentid(studentid):
"""
Look up attributes of a member by studnetid.
Parameters:
studentid - the student ID number
Returns: a dictionary of attributes
Example: get(...) -> {
'memberid': 3349,
'name': 'Michael Spang',
'program': 'Computer Science',
...
}
"""
return connection.select_member_by_studentid(studentid)
def list_term(term):
"""
Build a list of members in a term.
Parameters:
term - the term to match members against
Returns: a list of member dictionaries
Example: list_term('f2006'): -> [
{ 'memberid': 3349, ... },
{ 'memberid': ... }.
...
]
"""
# retrieve a list of memberids in term
memberlist = connection.select_members_by_term(term)
# convert the list of memberids to a list of dictionaries
memberlist = map(connection.select_member_by_id, memberlist)
return memberlist
def list_name(name):
"""
Build a list of members with matching names.
Parameters:
name - the name to match members against
Returns: a list of member dictionaries
Example: list_name('Spang'): -> [
{ 'memberid': 3349, ... },
{ 'memberid': ... },
...
]
"""
# retrieve a list of memberids matching name
memberlist = connection.select_members_by_name(name)
# convert the list of memberids to a list of dictionaries
memberlist = map(connection.select_member_by_id, memberlist)
return memberlist
def delete(memberid):
"""
Erase all records of a member.
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
Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993'])
"""
# save member data
member = connection.select_member_by_id(memberid)
term_list = connection.select_terms(memberid)
# remove data from the db
connection.delete_term_all(memberid)
connection.delete_member(memberid)
connection.commit()
return (member, term_list)
def update(member):
"""
Update CSC member attributes. None is NULL.
Parameters:
member - a dictionary with member attributes as
returned by get, possibly omitting some
attributes. member['memberid'] must exist
and be valid.
Exceptions:
NoSuchMember - if the member id does not exist
InvalidStudentID - if the student id number is malformed
DuplicateStudentID - if the student id number exists
Example: update( {'memberid': 3349, userid: 'mspang'} )
"""
if member.has_key('studentid') and member['studentid'] != 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)
# check for duplicate student id
member = connection.select_member_by_studentid(studentid)
if member:
raise DuplicateStudentID("student id exists in database: %s" %
studentid)
# not specifying memberid is a bug
if not member.has_key('memberid'):
raise Exception("no member specified in call to update")
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)
# do the update
connection.update_member(member)
# commit the transaction
connection.commit()
### Term Table ###
def register(memberid, term_list):
"""
Registers a member for one or more terms.
Parameters:
memberid - the member id number
term_list - the term to register for, or a list of terms
Exceptions:
InvalidTerm - if a term is malformed
Example: register(3349, "w2007")
Example: register(3349, ["w2007", "s2007"])
"""
if not type(term_list) in (list, tuple):
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)
# add term to database
connection.insert_term(memberid, term)
connection.commit()
def registered(memberid, term):
"""
Determines whether a member is registered
for a term.
Parameters:
memberid - the member id number
term - the term to check
Returns: whether the member is registered
Example: registered(3349, "f2006") -> True
"""
return connection.select_term(memberid, term) != None
def terms_list(memberid):
"""
Retrieves a list of terms a member is
registered for.
Parameters:
memberid - the member id number
Returns: list of term strings
Example: registered(0) -> 's1993'
"""
return connection.select_terms(memberid)
### Tests ###
if __name__ == '__main__':
connect()
sid = new("Test User", "99999999", "CS")
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)

252
py/csc/admin/terms.py Normal file
View File

@ -0,0 +1,252 @@
# $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.
"""
import time, datetime, re
# year to count terms from
EPOCH = 1970
# seasons list
SEASONS = [ 'w', 's', 'f' ]
def valid(term):
"""
Determines whether a term is well-formed:
Parameters:
term - the term string
Returns: whether the term is valid (boolean)
Example: valid("f2006") -> True
"""
regex = '^[wsf][0-9]{4}$'
return re.match(regex, term) != 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):
raise Exception("malformed term: %s" % term)
year = int( term[1:] )
season = SEASONS.index( term[0] )
return (year - EPOCH) * len(SEASONS) + season
def generate(term):
"""Helper function to convert a year and season to a term string."""
year = int(term / len(SEASONS)) + EPOCH
season = term % len(SEASONS)
return "%s%04d" % ( SEASONS[season], year )
def next(term):
"""
Returns the next term. (convenience function)
Parameters:
term - the term string
Retuns: the term string of the following term
Example: next("f2006") -> "w2007"
"""
return add(term, 1)
def previous(term):
"""
Returns the previous term. (convenience function)
Parameters:
term - the term string
Returns: the term string of the preceding term
Example: previous("f2006") -> "s2006"
"""
return add(term, -1)
def add(term, offset):
"""
Calculates a term relative to some base term.
Parameters:
term - the base term
offset - the number of terms since term (may be negative)
Returns: the term that comes offset terms after term
"""
return generate(parse(term) + offset)
def delta(initial, final):
"""
Calculates the distance between two terms.
It should be true that add(a, delta(a, b)) == b.
Parameters:
initial - the base term
final - the term at some offset from the base term
Returns: the offset of final relative to initial
"""
return parse(final) - parse(initial)
def compare(first, second):
"""
Compares two terms. This function is suitable
for use with list.sort().
Parameters:
first - base term for comparison
second - term to compare to
Returns: > 0 (if first > second)
= 0 (if first == second)
< 0 (if first < second)
"""
return delta(second, first)
def interval(base, count):
"""
Returns a list of adjacent terms.
Parameters:
base - the first term in the interval
count - the number of terms to include
Returns: a list of count terms starting with initial
Example: interval('f2006', 3) -> [ 'f2006', 'w2007', 's2007' ]
"""
terms = []
for num in xrange(count):
terms.append( add(base, num) )
return terms
def tstamp(timestamp):
"""Helper to convert seconds since the epoch
to terms since the epoch."""
# let python determine the month and year
date = datetime.date.fromtimestamp(timestamp)
# determine season
if date.month <= 4:
season = SEASONS.index('w')
elif date.month <= 8:
season = SEASONS.index('s')
else:
season = SEASONS.index('f')
return (date.year - EPOCH) * len(SEASONS) + season
def from_timestamp(timestamp):
"""
Converts a number of seconds since
the epoch to a number of terms since
the epoch.
This function notes that:
WINTER = JANUARY to APRIL
SPRING = MAY TO AUGUST
FALL = SEPTEMBER TO DECEMBER
Parameters:
timestamp - number of seconds since the epoch
Returns: the number of terms since the epoch
Example: from_timestamp(1166135779) -> 'f2006'
"""
return generate( tstamp(timestamp) )
def curr():
"""Helper to determine the current term."""
return tstamp( time.time() )
def current():
"""
Determines the current term.
Returns: current term
Example: current() -> 'f2006'
"""
return generate( curr() )
def next_unregistered(registered):
"""
Find the first future or current unregistered term.
Intended as the 'default' for registrations.
Parameters:
registered - a list of terms a member is registered for
Returns: the next unregistered term
"""
# get current term number
now = curr()
# never registered -> current term is next
if len( registered) < 1:
return generate( now )
# return the first unregistered, or the current term (whichever is greater)
return generate(max([max(map(parse, registered))+1, now]))
### Tests ###
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()
print "All tests passed." "\n"

9
py/csc/apps/__init__.py Normal file
View File

@ -0,0 +1,9 @@
# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $
"""
User Interfaces
This module contains frontends and related modules.
CEO's primary frontends are:
legacy - aims to reproduce the curses UI of the previous CEO
"""

View File

@ -0,0 +1,10 @@
# $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
"""

View File

@ -0,0 +1,412 @@
# $Id: helpers.py 35 2006-12-28 05:14:05Z mspang $
"""
Helpers for legacy User Interface
This module contains numerous functions that are designed to immitate
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
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
CEO were selectable but non-operational, but in the new CEO they are
not even selectable.
"""
import curses.ascii
# key constants not defined in CURSES
KEY_RETURN = ord('\n')
KEY_ESCAPE = 27
KEY_EOT = 4
def center(parent_dim, child_dim):
"""Helper for centering a length in a larget length."""
return (parent_dim-child_dim)/2
def read_input(wnd, offy, offx, width, maxlen, echo=True):
"""
Read user input within a confined region of a window.
Basic line-editing is supported:
LEFT, RIGHT, HOME, and END move around.
BACKSPACE and DEL remove characters.
INSERT switches between insert and overwrite mode.
ESC and C-d abort input.
RETURN completes input.
Parameters:
wnd - parent window for region
offy - the vertical offset for the beginning of the input region
offx - the horizontal offset for the beginning of the input region
width - the width of the region
maxlen - greatest number of characters to read (0 for no limit)
echo - boolean: whether to display typed characters
Returns: the string, or None when the user aborts.
"""
# turn on cursor
try:
curses.curs_set(1)
except:
pass
# set keypad mode to allow UP, DOWN, etc
wnd.keypad(1)
# the input string
input = ""
# offset of cursor in input
# i.e. the next operation is applied at input[inputoff]
inputoff = 0
# display offset (for scrolling)
# i.e. the first character in the region is input[displayoff]
displayoff = 0
# insert mode (True) or overwrite mode (False)
insert = True
while True:
# echo mode, display the string
if echo:
# discard characters before displayoff,
# as the window may be scrolled to the right
substring = input[displayoff:]
# pad the string with zeroes to overwrite stale characters
substring = substring + " " * (width - len(substring))
# display the substring
wnd.addnstr(offy, offx, substring, width)
# await input
key = wnd.getch(offy, offx + inputoff - displayoff)
# not echo mode, don't display the string
else:
# await input at arbitrary location
key = wnd.getch(offy, offx)
# enter returns input
if key == KEY_RETURN:
return input
# escape aborts input
elif key == KEY_ESCAPE:
return None
# EOT (C-d) aborts if there is no input
elif key == KEY_EOT:
if len(input) == 0:
return None
# backspace removes the previous character
elif key == curses.KEY_BACKSPACE:
if inputoff > 0:
# remove the character immediately before the input offset
input = input[0:inputoff-1] + input[inputoff:]
inputoff -= 1
# move either the cursor or entire line of text left
if displayoff > 0:
displayoff -= 1
# delete removes the current character
elif key == curses.KEY_DC:
if inputoff < len(input):
# remove the character at the input offset
input = input[0:inputoff] + input[inputoff+1:]
# left moves the cursor one character left
elif key == curses.KEY_LEFT:
if inputoff > 0:
# move the cursor to the left
inputoff -= 1
# scroll left if necessary
if inputoff < displayoff:
displayoff -= 1
# right moves the cursor one character right
elif key == curses.KEY_RIGHT:
if inputoff < len(input):
# move the cursor to the right
inputoff += 1
# scroll right if necessary
if displayoff - inputoff == width:
displayoff += 1
# home moves the cursor to the first character
elif key == curses.KEY_HOME:
inputoff = 0
displayoff = 0
# end moves the cursor past the last character
elif key == curses.KEY_END:
inputoff = len(input)
displayoff = len(input) - width + 1
# insert toggles insert/overwrite mode
elif key == curses.KEY_IC:
insert = not insert
# other (printable) characters are added to the input string
elif curses.ascii.isprint(key):
if len(input) < maxlen or maxlen == 0:
# insert mode: insert before current offset
if insert:
input = input[0:inputoff] + chr(key) + input[inputoff:]
# overwrite mode: replace current offset
else:
input = input[0:inputoff] + chr(key) + input[inputoff+1:]
# increment the input offset
inputoff += 1
# scroll right if necessary
if inputoff - displayoff == width:
displayoff += 1
def inputbox(wnd, prompt, field_width, echo=True):
"""Display a window for user input."""