Remove remnants of SQL interface

This commit is contained in:
Michael Spang 2007-10-26 00:05:05 -04:00
parent f1702130d7
commit 31c05d869f
10 changed files with 3 additions and 783 deletions

2
debian/control vendored
View File

@ -7,7 +7,7 @@ Standards-Version: 3.7.2
Package: ceo
Architecture: any
Depends: python2.4, python-ldap, python-pygresql, python-urwid, krb5-user, less, python-pam, ${shlibs:Depends}
Depends: python2.4, python-ldap, python-urwid, krb5-user, less, python-pam, ${shlibs:Depends}
Recommends: quota
Description: Computer Science Club Administrative Utilities
This package contains the CSC Electronic Office

1
debian/rules vendored
View File

@ -30,7 +30,6 @@ install: build
usr/lib/csc usr/bin etc/ldap/schema
dh_install pylib/* usr/lib/$(PYTHON)/site-packages/
dh_install etc/* etc/csc/
dh_install sql/* usr/share/csc/
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/

View File

@ -1,6 +1,5 @@
# /etc/csc/members.cf: CSC Members Configuration
include /etc/csc/pgsql.cf
include /etc/csc/ldap.cf
### Validation Tuning ###

View File

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

View File

@ -76,14 +76,14 @@ class NoSuchMember(MemberException):
ldap_connection = ldapi.LDAPConnection()
def connect():
"""Connect to PostgreSQL."""
"""Connect to LDAP."""
load_configuration()
ldap_connection.connect(cfg['server_url'], cfg['admin_bind_dn'], cfg['admin_bind_pw'], cfg['users_base'], cfg['groups_base'])
def disconnect():
"""Disconnect from PostgreSQL."""
"""Disconnect from LDAP."""
ldap_connection.disconnect()

View File

@ -1,671 +0,0 @@
"""
Database Backend Interface
This module is intended to be a thin wrapper around CEO database operations.
Methods on the connection class correspond in a straightforward way to SQL
queries. These methods may restructure and clean up query output but may make
no other assumptions about its content or purpose.
This module makes use of the PyGreSQL Python bindings to libpq,
PostgreSQL's native C client library.
"""
import pgdb
class DBException(Exception):
"""Exception class for database-related errors."""
pass
class DBConnection(object):
"""
A connection to CEO's backend database. All database queries
and updates are made via this class.
Exceptions: (all methods)
DBException - on database query failure
Note: Updates will never take place until commit() is called.
Note: In the event that any method of this class raises a
DBException and that exception is caught, rollback()
must be called before further queries will be successful.
Example:
connection = DBConnection()
connection.connect("localhost", "ceo")
# make queries and updates, i.e.
connection.insert_member("Calum T. Dalek")
connection.commit()
connection.disconnect()
"""
def __init__(self):
self.cnx = None
self.cursor = None
def connect(self, hostname=None, database=None, username=None, password=None):
"""
Establishes the connection to CEO's PostgreSQL database.
Parameters:
hostname - hostname:port to connect to
database - name of database
username - user to authenticate as
password - password of username
"""
if self.cnx: raise DBException("unable to connect: already connected")
try:
self.cnx = pgdb.connect(host=hostname, database=database,
user=username, password=password)
self.cursor = self.cnx.cursor()
except pgdb.Error, e:
raise DBException("unable to connect: %s" % e)
def disconnect(self):
"""Closes the connection to CEO's PostgreSQL database."""
if self.cursor:
self.cursor.close()
self.cursor = None
if self.cnx:
self.cnx.close()
self.cnx = None
def connected(self):
"""Determine whether the connection has been established."""
return self.cnx is not None
def commit(self):
"""Commits the current transaction and starts a new one."""
self.cnx.commit()
def rollback(self):
"""Aborts the current transaction."""
self.cnx.rollback()
### Member-related methods ###
def select_members(self, sql, params=None):
"""
Retrieves a list CSC members selected by given SQL statement.
This is a helper function that should generally not be called directly.
Parameters:
sql - the SELECT sql statement
params - parameters for the SQL statement
The sql statement must select the six columns
(memberid, name, studentid, program, type, userid)
from the members table in that order.
Returns: a memberid-keyed dictionary whose values are
column-keyed dictionaries with member attributes
"""
# retrieve a list of all members
try:
self.cursor.execute(sql, params)
members_list = self.cursor.fetchall()
except pgdb.Error, e:
raise DBException("SELECT statement failed: %s" % e)
# build a dictionary of dictionaries from the result (a list of lists)
members_dict = {}
for member in members_list:
members_dict[member[0]] = {
'memberid': member[0],
'name': member[1],
'studentid': member[2],
'program': member[3],
'type': member[4],
'userid': member[5],
}
return members_dict
def select_single_member(self, sql, params=None):
"""
Retrieves a single member by memberid.
This is a helper function that should generally not be called directly.
See: self.select_members()
Returns: a column-keyed dictionary with member attributes, or
None if no member matching member exists
"""
# retrieve the member
results = self.select_members(sql, params)
# too many members returned
if len(results) > 1:
raise DBException("multiple members selected: sql='%s' params=%s" % (sql, repr(params)))
# no such member
elif len(results) < 1:
return None
# return the single match
memberid = results.keys()[0]
return results[memberid]
def select_all_members(self):
"""
Retrieves a list of all CSC members (past and present).
See: self.select_members()
Example: connection.select_all_members() -> {
0: { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
...
}
"""
sql = "SELECT memberid, name, studentid, program, type, userid FROM members"
return self.select_members(sql)
def select_members_by_name(self, name_re):
"""
Retrieves a list of all CSC members whose name matches name_re.
See: self.select_members()
Example: connection.select_members_by_name('Michael') -> {
3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
...
}
"""
sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE name ~* %s"
params = [ str(name_re) ]
return self.select_members(sql, params)
def select_members_by_term(self, term):
"""
Retrieves a list of all CSC members who were members in the specified term.
See: self.select_members()
Example: connection.select_members_by_term('f2006') -> {
3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
...
}
"""
sql = "SELECT members.memberid, name, studentid, program, type, userid FROM members JOIN terms ON members.memberid=terms.memberid WHERE term=%s"
params = [ str(term) ]
return self.select_members(sql, params)
def select_member_by_id(self, memberid):
"""
Retrieves a single member by memberid.
See: self.select_single_member()
Example: connection.select_member_by_id(0) ->
{ 'memberid': 0, 'name': 'Calum T. Dalek' ...}
"""
sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE memberid=%d"
params = [ int(memberid) ]
return self.select_single_member(sql, params)
def select_member_by_userid(self, username):
"""
Retrieves a single member by UNIX account username.
See: self.select_single_member()
Example: connection.select_member_by_userid('ctdalek') ->
{ 'memberid': 0, 'name': 'Calum T. Dalek' ...}
"""
sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE userid=%s"
params = [ username ]
return self.select_single_member(sql, params)
def select_member_by_studentid(self, studentid):
"""
Retrieves a single member by student id number.
See: self.select_single_member()
Example: connection.select_member_by_studentid('nnnnnnnn') ->
{ 'memberid': 3349, 'name': 'Michael Spang' ...}
"""
sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE studentid=%s"
params = [ studentid ]
return self.select_single_member(sql, params)
def insert_member(self, name, studentid=None, program=None, mtype='user', userid=None):
"""
Creates a member with the specified attributes.
Parameters:
name - full name of member
studentid - student id number
program - program of study
mtype - member type
userid - account id
Example: connection.insert_member('Michael Spang', '99999999', 'Math/CS') -> 3349
Returns: a memberid of created user
"""
try:
# retrieve the next memberid
sql = "SELECT nextval('memberid_seq')"
self.cursor.execute(sql)
result = self.cursor.fetchone()
memberid = result[0]
# insert the member
sql = "INSERT INTO members (memberid, name, studentid, program, type, userid) VALUES (%d, %s, %s, %s, %s, %s)"
params = [ memberid, name, studentid, program, mtype, userid ]
self.cursor.execute(sql, params)
return memberid
except pgdb.Error, e:
raise DBException("failed to create member: %s" % e)
def delete_member(self, memberid):
"""
Deletes a member. Note that a member cannot
be deleted until it has been unregistered from
all terms.
Parameters:
memberid - the member id number to delete
Example: connection.delete_member(3349)
"""
sql = "DELETE FROM members WHERE memberid=%d"
params = [ memberid ]
try:
self.cursor.execute(sql, params)
except pgdb.Error, e:
raise DBException("DELETE statement failed: %s" %e)
def update_member(self, member):
"""
Modifies member attributes.
Parameters:
member - a column-keyed dictionary with the new state of the member.
member['memberid'] must be present. ommitted columns
will not be changed. None is NULL.
Returns: the full new state of the member as a column-keyed dictionary
Example: connection.update_member({
'memberid': 3349,
'name': 'Michael C. Spang',
'program': 'CS!'
}) -> {
'memberid': 3349,
'name': 'Michael C. Spang',
'program': CS!',
'studentid': '99999999' # unchanged
}
Equivalent Example:
member = connection.select_member_by_id(3349)
member['name'] = 'Michael C. Spang'
member['program'] = 'CS!'
connection.update_member(member) -> { see above }
"""
try:
# memberid to update
memberid = member['memberid']
# retrieve current state of member
member_state = self.select_member_by_id(memberid)
# build a list of changes to make
changes = []
for column in member.keys():
if member[column] != member_state[column]:
# column's value must be updated
changes.append( (column, member[column]) )
member_state[column] = member[column]
# no changes?
if len(changes) < 1:
return member_state
# make the necessary changes in an update statement
changes = zip(*changes)
sql = "UPDATE members SET " + ", ".join(["%s=%%s"] * len(changes[0])) % changes[0] + " WHERE memberid=%d"
params = changes[1] + ( memberid, )
self.cursor.execute(sql, params)
return member_state
except pgdb.Error, e:
raise DBException("member update failed: %s" % e)
### Term-related methods ###
def select_term(self, memberid, term):
"""
Determines whether a member is registered for a term.
Parameters:
memberid - the member id number
term - the term to check
Returns: a matching term, or None
Example: connection.select_term(3349, 'f2006') -> 'f2006'
"""
sql = "SELECT term FROM terms WHERE memberid=%d AND term=%s"
params = [ memberid, term ]
# retrieve matches
try:
self.cursor.execute(sql, params)
result = self.cursor.fetchall()
except pgdb.Error, e:
raise DBException("SELECT statement failed: %s" % e)
if len(result) > 1:
raise DBException("multiple rows in terms with memberid=%d term=%s" % (memberid, term))
elif len(result) == 0:
return None
else:
return result[0][0]
def select_terms(self, memberid):
"""
Retrieves a list of terms a member is registered for.
Parameters:
memberid - the member id number
Returns: a sorted list of terms
Example: connection.select_terms(3349) -> ['f2006']
"""
sql = "SELECT term FROM terms WHERE memberid=%d"
params = [ memberid ]
# retrieve the list of terms
try:
self.cursor.execute(sql, params)
result = self.cursor.fetchall()
except pgdb.Error, e:
raise DBException("SELECT statement failed: %s" % e)
result = [ row[0] for row in result ]
return result
def insert_term(self, memberid, term):
"""
Registers a member for a term.
Parameters:
memberid - the member id number to register
term - string representation of the term
Example: connection.insert_term(3349, 'f2006')
"""
sql = "INSERT INTO terms (memberid, term) VALUES (%d, %s)"
params = [ memberid, term ]
try:
self.cursor.execute(sql, params)
except pgdb.Error, e:
raise DBException("INSERT statement failed: %s" % e)
def delete_term(self, memberid, term):
"""
Unregisters a member for a term.
Parameters:
memberid - the member id number to register
term - string representation of the term
Example: connection.delete_term(3349, 'f2006')
"""
sql = "DELETE FROM terms WHERE memberid=%d and term=%s"
params = [ memberid, term ]
try:
self.cursor.execute(sql, params)
except pgdb.Error, e:
raise DBException("DELETE statement failed: %s" % e)
def delete_term_all(self, memberid):
"""
Unregisters a member for all registered terms.
Parameters:
memberid - the member id number to unregister
Example: connection.delete_term_all(3349)
"""
sql = "DELETE FROM terms WHERE memberid=%d"
params = [ memberid ]
# retrieve a list of terms
try:
self.cursor.execute(sql, params)
except pgdb.Error, e:
raise DBException("DELETE statement failed: %s" % e)
### Miscellaneous methods ###
def trim_memberid_sequence(self):
"""
Sets the value of the member id sequence to the id of the newest
member. For use after testing to prevent large intervals of unused
memberids from developing.
Note: this does nothing unless the most recently added member(s) have been deleted
"""
self.cursor.execute("SELECT setval('memberid_seq', (SELECT max(memberid) FROM members))")
### Tests ###
if __name__ == '__main__':
from csc.common.test import *
conffile = "/etc/csc/pgsql.cf"
cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ])
hostnm = cfg['server'][1:-1]
dbase = cfg['database'][1:-1]
# t=test m=member s=student d=default e=expected u=updated
tmname = 'Test Member'
tmuname = 'Member Test'
tmsid = '00000004'
tmusid = '00000008'
tmprogram = 'Undecidable'
tmuprogram = 'Nondetermined'
tmtype = 'Untyped'
tmutype = 'Poly'
tmuserid = 'tmem'
tmuuserid = 'identifier'
tm2name = 'Test Member 2'
tm2sid = '00000005'
tm2program = 'Undeclared'
tm3name = 'T. M. 3'
dtype = 'user'
tmterm = 'w0000'
tm3term = 'f1112'
tm3term2 = 's1010'
emdict = { 'name': tmname, 'program': tmprogram, 'studentid': tmsid, 'type': tmtype, 'userid': tmuserid }
emudict = { 'name': tmuname, 'program': tmuprogram, 'studentid': tmusid, 'type': tmutype, 'userid': tmuuserid }
em2dict = { 'name': tm2name, 'program': tm2program, 'studentid': tm2sid, 'type': dtype, 'userid': None }
em3dict = { 'name': tm3name, 'program': None, 'studentid': None, 'type': dtype, 'userid': None }
test(DBConnection)
connection = DBConnection()
success()
test(connection.connect)
connection.connect(hostnm, dbase)
success()
test(connection.connected)
assert_equal(True, connection.connected())
success()
test(connection.insert_member)
tmid = connection.insert_member(tmname, tmsid, tmprogram, tmtype, tmuserid)
tm2id = connection.insert_member(tm2name, tm2sid, tm2program)
tm3id = connection.insert_member(tm3name)
assert_equal(True, int(tmid) >= 0)
assert_equal(True, int(tmid) >= 0)
success()
emdict['memberid'] = tmid
emudict['memberid'] = tmid
em2dict['memberid'] = tm2id
em3dict['memberid'] = tm3id
test(connection.select_member_by_id)
m1 = connection.select_member_by_id(tmid)
m2 = connection.select_member_by_id(tm2id)
m3 = connection.select_member_by_id(tm3id)
assert_equal(emdict, m1)
assert_equal(em2dict, m2)
assert_equal(em3dict, m3)
success()
test(connection.select_all_members)
members = connection.select_all_members()
assert_equal(True, tmid in members)
assert_equal(True, tm2id in members)
assert_equal(True, tm3id in members)
assert_equal(emdict, members[tmid])
success()
test(connection.select_members_by_name)
members = connection.select_members_by_name(tmname)
assert_equal(True, tmid in members)
assert_equal(False, tm3id in members)
assert_equal(emdict, members[tmid])
success()
test(connection.select_member_by_userid)
assert_equal(emdict, connection.select_member_by_userid(tmuserid))
success()
test(connection.insert_term)
connection.insert_term(tmid, tmterm)
connection.insert_term(tm3id, tm3term)
connection.insert_term(tm3id, tm3term2)
success()
test(connection.select_members_by_term)
members = connection.select_members_by_term(tmterm)
assert_equal(True, tmid in members)
assert_equal(False, tm2id in members)
assert_equal(False, tm3id in members)
success()
test(connection.select_term)
assert_equal(tmterm, connection.select_term(tmid, tmterm))
assert_equal(None, connection.select_term(tm2id, tmterm))
assert_equal(tm3term, connection.select_term(tm3id, tm3term))
assert_equal(tm3term2, connection.select_term(tm3id, tm3term2))
success()
test(connection.select_terms)
trms = connection.select_terms(tmid)
trms2 = connection.select_terms(tm2id)
assert_equal([tmterm], trms)
assert_equal([], trms2)
success()
test(connection.delete_term)
assert_equal(tm3term, connection.select_term(tm3id, tm3term))
connection.delete_term(tm3id, tm3term)
assert_equal(None, connection.select_term(tm3id, tm3term))
success()
test(connection.update_member)
connection.update_member({'memberid': tmid, 'name': tmuname})
connection.update_member({'memberid': tmid, 'program': tmuprogram, 'studentid': tmusid })
connection.update_member({'memberid': tmid, 'userid': tmuuserid, 'type': tmutype })
assert_equal(emudict, connection.select_member_by_id(tmid))
connection.update_member(emdict)
assert_equal(emdict, connection.select_member_by_id(tmid))
success()
test(connection.delete_term_all)
connection.delete_term_all(tm2id)
connection.delete_term_all(tm3id)
assert_equal([], connection.select_terms(tm2id))
assert_equal([], connection.select_terms(tm3id))
success()
test(connection.delete_member)
connection.delete_member(tm3id)
assert_equal(None, connection.select_member_by_id(tm3id))
negative(connection.delete_member, (tmid,), DBException, "delete of term-registered member")
success()
test(connection.rollback)
connection.rollback()
assert_equal(None, connection.select_member_by_id(tm2id))
success()
test(connection.commit)
connection.commit()
success()
test(connection.trim_memberid_sequence)
connection.trim_memberid_sequence()
success()
test(connection.disconnect)
connection.disconnect()
assert_equal(False, connection.connected())
connection.disconnect()
success()

View File

@ -1,13 +0,0 @@
#!/bin/sh
# Initializes a database for CEO.
# initialize the database
createdb $1
createlang plpythonu $1
# initialize the tables
psql $1 < structure.sql
# initialize check triggers
psql $1 < verify_studentid.sql
psql $1 < verify_term.sql

View File

@ -1,24 +0,0 @@
-- Table structure for CEO's SQL database.
-- Usage:
-- su postgres
-- createdb ceo
-- psql ceo < structure.sql
CREATE SEQUENCE memberid_seq;
CREATE TABLE members (
memberid integer PRIMARY KEY DEFAULT nextval('memberid_seq') NOT NULL,
name character varying(50) NOT NULL,
studentid character varying(10) UNIQUE,
program character varying(50),
"type" character varying(10),
userid character varying(32) UNIQUE
);
CREATE TABLE terms (
memberid integer NOT NULL,
term character(5) NOT NULL,
UNIQUE(memberid, term)
);

View File

@ -1,30 +0,0 @@
-- PL/Python trigger to verify student ids for validity
-- Dedicated to office staff who can't type student ids.
-- To (re)install:
-- su postgres
-- psql ceo < verify_studentid.sql
-- To uninstall:
-- su postgres
-- echo 'DROP FUNCTION verify_studentid() CASCADE' | psql ceo
DROP FUNCTION verify_studentid() CASCADE;
CREATE FUNCTION verify_studentid() RETURNS trigger AS '
import re
# update this line if the student id format changes
STUDENTID_REGEX = "^[0-9]{8}$"
studentid = TD["new"]["studentid"]
if studentid and not re.match(STUDENTID_REGEX, studentid):
plpy.error("student id is invalid (%s)" % studentid)
' LANGUAGE plpythonu;
CREATE TRIGGER verify_studentid_insert BEFORE INSERT on members
FOR ROW EXECUTE PROCEDURE verify_studentid();
CREATE TRIGGER verify_studentid_update BEFORE UPDATE ON members
FOR ROW EXECUTE PROCEDURE verify_studentid();

View File

@ -1,29 +0,0 @@
-- PL/Python trigger to verify terms for validity
-- To (re)install:
-- su postgres
-- psql ceo < verify_term.sql
-- To uninstall:
-- su postgres
-- echo 'DROP FUNCTION verify_term() CASCADE' | psql ceo
DROP FUNCTION verify_term() CASCADE;
CREATE FUNCTION verify_term() RETURNS trigger AS '
import re
# update this line if the term format changes
TERM_REGEX = "^[wsf][0-9]{4}$"
term = TD["new"]["term"]
if term and not re.match(TERM_REGEX, term):
plpy.error("term is invalid (%s)" % term)
' LANGUAGE plpythonu;
CREATE TRIGGER verify_term_insert BEFORE INSERT on terms
FOR ROW EXECUTE PROCEDURE verify_term();
CREATE TRIGGER verify_term_update BEFORE UPDATE ON terms
FOR ROW EXECUTE PROCEDURE verify_term();