Reorganize namespace
authorMichael Spang <mspang@csclub.uwaterloo.ca>
Thu, 13 Dec 2007 08:39:05 +0000 (03:39 -0500)
committerMichael Spang <mspang@csclub.uwaterloo.ca>
Fri, 14 Dec 2007 04:34:16 +0000 (23:34 -0500)
There were too many packages with only a couple of modules
in them. It took minimum four tab completes to find an
interesting file. This halves that.

45 files changed:
bin/ceo
bin/ceoquery
bin/csc-chfn
bin/csc-chsh
ceo/__init__.py [new file with mode: 0644]
ceo/conf.py [new file with mode: 0644]
ceo/excep.py [new file with mode: 0644]
ceo/ldapi.py [new file with mode: 0644]
ceo/members.py [new file with mode: 0644]
ceo/terms.py [new file with mode: 0644]
ceo/test.py [new file with mode: 0644]
ceo/urwid/__init__.py [new file with mode: 0644]
ceo/urwid/groups.py [new file with mode: 0644]
ceo/urwid/info.py [new file with mode: 0644]
ceo/urwid/ldapfilter.py [new file with mode: 0644]
ceo/urwid/main.py [new file with mode: 0644]
ceo/urwid/newmember.py [new file with mode: 0644]
ceo/urwid/positions.py [new file with mode: 0644]
ceo/urwid/renew.py [new file with mode: 0644]
ceo/urwid/search.py [new file with mode: 0644]
ceo/urwid/widgets.py [new file with mode: 0644]
ceo/urwid/window.py [new file with mode: 0644]
pylib/csc/__init__.py [deleted file]
pylib/csc/adm/__init__.py [deleted file]
pylib/csc/adm/members.py [deleted file]
pylib/csc/adm/terms.py [deleted file]
pylib/csc/apps/__init__.py [deleted file]
pylib/csc/apps/urwid/__init__.py [deleted file]
pylib/csc/apps/urwid/groups.py [deleted file]
pylib/csc/apps/urwid/info.py [deleted file]
pylib/csc/apps/urwid/ldapfilter.py [deleted file]
pylib/csc/apps/urwid/main.py [deleted file]
pylib/csc/apps/urwid/newmember.py [deleted file]
pylib/csc/apps/urwid/positions.py [deleted file]
pylib/csc/apps/urwid/renew.py [deleted file]
pylib/csc/apps/urwid/search.py [deleted file]
pylib/csc/apps/urwid/widgets.py [deleted file]
pylib/csc/apps/urwid/window.py [deleted file]
pylib/csc/backends/__init__.py [deleted file]
pylib/csc/backends/ldapi.py [deleted file]
pylib/csc/common/__init__.py [deleted file]
pylib/csc/common/conf.py [deleted file]
pylib/csc/common/excep.py [deleted file]
pylib/csc/common/test.py [deleted file]
setup.py

diff --git a/bin/ceo b/bin/ceo
index fd6a113..727f974 100755 (executable)
--- a/bin/ceo
+++ b/bin/ceo
@@ -1,3 +1,3 @@
 #!/usr/bin/python
-import csc.apps.urwid.main
-csc.apps.urwid.main.start()
+import ceo.urwid.main
+ceo.urwid.main.start()
index 046c25f..9e35964 100755 (executable)
@@ -3,7 +3,7 @@
 ceoquery - a script to lookup member and account information
 """
 import os, sys
-from csc.adm import members, terms
+from ceo import members, terms
 
 try:
     members.connect()
index 70fff1c..1f2e2d6 100755 (executable)
@@ -9,8 +9,8 @@ When run from an unprivileged account, authentication will be performed
 before the account information is changed.
 """
 import os, sys, pwd, getopt, PAM
-from csc.common.excep import InvalidArgument
-from csc.adm import accounts
+from ceo import accounts
+from ceo.excep import InvalidArgument
 
 progname = os.path.basename(sys.argv[0])
 
index 5d18e2a..4ec7790 100755 (executable)
@@ -9,8 +9,8 @@ When run from an unprivileged account, authentication will be performed
 before the shell is changed, and the new shell must be listed in /etc/shells.
 """
 import os, sys, pwd, getopt, PAM
-from csc.common.excep import InvalidArgument
-from csc.adm import accounts
+from ceo import accounts
+from ceo.excep import InvalidArgument
 
 progname = os.path.basename(sys.argv[0])
 
diff --git a/ceo/__init__.py b/ceo/__init__.py
new file mode 100644 (file)
index 0000000..c6f4cce
--- /dev/null
@@ -0,0 +1 @@
+"""CSC Electronic Office"""
diff --git a/ceo/conf.py b/ceo/conf.py
new file mode 100644 (file)
index 0000000..f9e6972
--- /dev/null
@@ -0,0 +1,162 @@
+"""
+Configuration Utility Module
+
+This module contains functions to load and verify very simple configuration
+files. Python supports ".ini" files, which suck, so this module is used
+instead.
+
+Example Configuration File:
+
+    include /path/to/other.cf
+
+    # these values are the same:
+    name_protected = "Michael Spang"
+    name_unprotected = Michael Spang
+    
+    # these values are not the same:
+    yes_no = " yes"
+    no_yes =  yes
+    
+    # this value is an integer
+    arbitrary_number=2
+    
+    # this value is not an integer
+    arbitrary_string="2"
+    
+    # this is a key with no value
+    csclub
+
+    # this key contains whitespace
+    white space = sure, why not
+
+    # these two lines are treated as one
+    long line = first line \\
+                second line
+
+Resultant Dictionary:
+
+    {
+      'name_protected': 'Michael Spang',
+      'name_unprotected:' 'Michael Spang',
+      'yes_no': ' yes',
+      'no_yes': 'yes',
+      'arbirary_number': 2,
+      'arbitrary_string': '2',
+      'csclub': None,
+      'white space': 'sure, why not'
+      'long line': 'first line \\n               second line' 
+      
+      ... (data from other.cf) ...
+    }
+
+"""
+from curses.ascii import isspace
+
+
+class ConfigurationException(Exception):
+    """Exception class for incomplete and incorrect configurations."""
+    
+
+def read(filename, included=None):
+    """
+    Function to read a configuration file into a dictionary.
+    
+    Parmaeters:
+        filename - the file to read
+        included - files previously read (internal)
+    
+    Exceptions:
+        IOError - when the configuration file cannot be read
+    """
+
+    if not included:
+        included = []
+    if filename in included:
+        return {}
+    included.append(filename)
+
+    conffile = open(filename)
+    
+    options = {}
+
+    while True:
+
+        line = conffile.readline()
+        if line == '':
+            break
+
+        # remove comments
+        if '#' in line:
+            line = line[:line.find('#')]
+
+        # combine lines when the newline is escaped with \
+        while len(line) > 1 and line[-2] == '\\':
+            line = line[:-2] + line[-1]
+            next = conffile.readline()
+            line += next
+            if next == '':
+                break
+
+        line = line.strip()
+
+        # process include statements
+        if line.find("include") == 0 and isspace(line[7]):
+
+            filename = line[8:].strip()
+            options.update(read(filename, included))
+            continue
+
+        # split 'key = value' into key and value and strip results
+        pair = map(str.strip, line.split('=', 1))
+        
+        # found key and value
+        if len(pair) == 2:
+            key, val = pair
+
+            # found quoted string?
+            if val and val[0] == val[-1] == '"':
+                val = val[1:-1]
+
+            # unquoted, found num?
+            elif val:
+                try:
+                    if "." in val:
+                        val = float(val)
+                    elif val[0] == '0':
+                        val = int(val, 8)
+                    else:
+                        val = int(val)
+                except ValueError:
+                    pass
+            
+            # save key and value
+            options[key] = val
+
+        # found only key, value = None
+        elif len(pair[0]) > 1:
+            key = pair[0]
+            options[key] = None
+
+    return options
+
+
+def check_string_fields(filename, field_list, cfg):
+    """Function to verify thatfields are strings."""
+
+    for field in field_list:
+        if field not in cfg or type(cfg[field]) is not str:
+            raise ConfigurationException('expected string value for option "%s" in "%s"' % (field, filename))
+
+def check_integer_fields(filename, field_list, cfg):
+    """Function to verify that fields are integers."""
+
+    for field in field_list:
+        if field not in cfg or type(cfg[field]) not in (int, long):
+            raise ConfigurationException('expected numeric value for option "%s" in "%s"' % (field, filename))
+
+def check_float_fields(filename, field_list, cfg):
+    """Function to verify that fields are integers or floats."""
+
+    for field in field_list:
+        if field not in cfg or type(cfg[field]) not in (float, long, int):
+            raise ConfigurationException('expected float value for option "%s" in "%s"' % (field, filename))
diff --git a/ceo/excep.py b/ceo/excep.py
new file mode 100644 (file)
index 0000000..6359302
--- /dev/null
@@ -0,0 +1,12 @@
+"""
+Exceptions Module
+
+This module provides some simple but generally useful exception classes.
+"""
+
+class InvalidArgument(Exception):
+    """Exception class for bad argument values."""
+    def __init__(self, argname, argval, explanation):
+        self.argname, self.argval, self.explanation = argname, argval, explanation
+    def __str__(self):
+        return 'Bad argument value "%s" for %s: %s' % (self.argval, self.argname, self.explanation)
diff --git a/ceo/ldapi.py b/ceo/ldapi.py
new file mode 100644 (file)
index 0000000..23e2ee0
--- /dev/null
@@ -0,0 +1,372 @@
+"""
+LDAP Backend Interface
+
+This module is intended to be a thin wrapper around LDAP operations.
+Methods on the connection object correspond in a straightforward way
+to LDAP queries and updates.
+
+A LDAP entry is the most important component of a CSC UNIX account.
+The entry contains the username, user id number, real name, shell,
+and other important information. All non-local UNIX accounts must
+have an LDAP entry, even if the account does not log in directly.
+
+This module makes use of python-ldap, a Python module with bindings
+to libldap, OpenLDAP's native C client library.
+"""
+import ldap.modlist
+from subprocess import Popen, PIPE
+
+
+class LDAPException(Exception):
+    """Exception class for LDAP-related errors."""
+
+
+class LDAPConnection(object):
+    """
+    Connection to the LDAP directory. All directory
+    queries and updates are made via this class.
+
+    Exceptions: (all methods)
+        LDAPException - on directory query failure
+
+    Example:
+         connection = LDAPConnection()
+         connection.connect(...)
+
+         # make queries and updates, e.g.
+         connection.user_delete('mspang')
+
+         connection.disconnect()
+    """
+
+    def __init__(self):
+        self.ldap = None
+
+
+    def connect_anon(self, uri, user_base, group_base):
+        """
+        Establish a connection to the LDAP Server.
+
+        Parameters:
+            uri        - connection string (e.g. ldap://foo.com, ldaps://bar.com)
+            user_base  - base of the users subtree
+            group_base - baes of the group subtree
+
+        Example: connect('ldaps:///', 'cn=ceo,dc=csclub,dc=uwaterloo,dc=ca',
+                     'secret', 'ou=People,dc=csclub,dc=uwaterloo,dc=ca',
+                     'ou=Group,dc=csclub,dc=uwaterloo,dc=ca')
+
+        """
+
+        # open the connection
+        self.ldap = ldap.initialize(uri)
+
+        # authenticate
+        self.ldap.simple_bind_s('', '')
+
+        self.user_base = user_base
+        self.group_base = group_base
+
+
+    def connect_sasl(self, uri, mech, realm, user_base, group_base):
+
+        # open the connection
+        self.ldap = ldap.initialize(uri)
+
+        # authenticate
+        sasl = Sasl(mech, realm)
+        self.ldap.sasl_interactive_bind_s('', sasl)
+
+        self.user_base = user_base
+        self.group_base = group_base
+
+
+    def disconnect(self):
+        """Close the connection to the LDAP server."""
+        
+        if self.ldap:
+
+            # close connection
+            try:
+                self.ldap.unbind_s()
+                self.ldap = None
+            except ldap.LDAPError, e:
+                raise LDAPException("unable to disconnect: %s" % e)
+
+
+    def connected(self):
+        """Determine whether the connection has been established."""
+
+        return self.ldap is not None
+
+
+
+    ### Helper Methods ###
+
+    def lookup(self, dn, objectClass=None):
+        """
+        Helper method to retrieve the attributes of an entry.
+
+        Parameters:
+            dn - the distinguished name of the directory entry
+
+        Returns: a dictionary of attributes of the matched dn, or
+                 None of the dn does not exist in the directory
+        """
+
+        if not self.connected(): raise LDAPException("Not connected!")
+
+        # search for the specified dn
+        try:
+            if objectClass:
+                search_filter = '(objectClass=%s)' % self.escape(objectClass)
+                matches = self.ldap.search_s(dn, ldap.SCOPE_BASE, search_filter)
+            else:
+                matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
+        except ldap.NO_SUCH_OBJECT:
+            return None
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to lookup dn %s: %s" % (dn, e))
+            
+        # this should never happen due to the nature of DNs
+        if len(matches) > 1:
+            raise LDAPException("duplicate dn in ldap: " + dn)
+
+        # dn was found, but didn't match the objectClass filter
+        elif len(matches) < 1:
+            return None
+
+        # return the attributes of the single successful match
+        match = matches[0]
+        match_dn, match_attributes = match
+        return match_attributes
+
+
+
+    ### User-related Methods ###
+
+    def user_lookup(self, uid, objectClass=None):
+        """
+        Retrieve the attributes of a user.
+
+        Parameters:
+            uid - the uid to look up
+
+        Returns: attributes of user with uid
+        """
+
+        dn = 'uid=' + uid + ',' + self.user_base
+        return self.lookup(dn, objectClass)
+
+
+    def user_search(self, search_filter, params):
+        """
+        Search for users with a filter.
+
+        Parameters:
+            search_filter - LDAP filter string to match users against
+
+        Returns: a dictionary mapping uids to attributes
+        """
+
+        if not self.connected(): raise LDAPException("Not connected!")
+
+        search_filter = search_filter % tuple(self.escape(x) for x in params)
+
+        # search for entries that match the filter
+        try:
+            matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, search_filter)
+        except ldap.LDAPError, e:
+            raise LDAPException("user search failed: %s" % e)
+
+        results = {}
+        for match in matches:
+            dn, attrs = match
+            uid = attrs['uid'][0]
+            results[uid] = attrs
+
+        return results
+
+
+    def user_modify(self, uid, attrs):
+        """
+        Update user attributes in the directory.
+
+        Parameters:
+            uid   - username of the user to modify
+            attrs - dictionary as returned by user_lookup() with changes to make.
+                    omitted attributes are DELETED.
+
+        Example: user = user_lookup('mspang')
+                 user['uidNumber'] = [ '0' ]
+                 connection.user_modify('mspang', user)
+        """
+
+        # distinguished name of the entry to modify
+        dn = 'uid=' + uid + ',' + self.user_base
+
+        # retrieve current state of user
+        old_user = self.user_lookup(uid)
+
+        try:
+
+            # build list of modifications to make
+            changes = ldap.modlist.modifyModlist(old_user, attrs)
+
+            # apply changes
+            self.ldap.modify_s(dn, changes)
+
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to modify: %s" % e)
+
+
+
+    ### Group-related Methods ###
+
+    def group_lookup(self, cn):
+        """
+        Retrieves the attributes of a group.
+
+        Parameters:
+            cn - the UNIX group name to lookup
+
+        Returns: attributes of the group's LDAP entry
+
+        Example: connection.group_lookup('office') -> {
+                     'cn': 'office',
+                     'gidNumber', '1001',
+                     ...
+                 }
+        """
+
+        dn = 'cn=' + cn + ',' + self.group_base
+        return self.lookup(dn, 'posixGroup')
+
+
+    ### Member-related Methods ###
+
+    def member_lookup(self, uid):
+        """
+        Retrieve the attributes of a member. This method will only return
+        results that have the objectClass 'member'.
+
+        Parameters:
+            uid - the username to look up
+
+        Returns: attributes of member with uid
+
+        Example: connection.member_lookup('mspang') ->
+                     { 'uid': 'mspang', 'uidNumber': 21292 ...}
+        """
+
+        if not self.connected(): raise LDAPException("Not connected!")
+
+        dn = 'uid=' + uid + ',' + self.user_base
+        return self.lookup(dn, 'member')
+
+
+    def member_search_name(self, name):
+        """
+        Retrieves a list of members with the specified name (fuzzy).
+
+        Returns: a dictionary mapping uids to attributes
+        """
+
+        search_filter = '(&(objectClass=member)(cn~=%s))'
+        return self.user_search(search_filter, [ name ] )
+
+
+    def member_search_term(self, term):
+        """
+        Retrieves a list of members who were registered in a certain term.
+
+        Returns: a dictionary mapping uids to attributes
+        """
+
+        search_filter = '(&(objectClass=member)(term=%s))'
+        return self.user_search(search_filter, [ term ])
+
+
+    def member_search_program(self, program):
+        """
+        Retrieves a list of members in a certain program (fuzzy).
+
+        Returns: a dictionary mapping uids to attributes
+        """
+
+        search_filter = '(&(objectClass=member)(program~=%s))'
+        return self.user_search(search_filter, [ program ])
+
+
+    def member_add(self, uid, cn, program=None, description=None):
+        """
+        Adds a member to the directory.
+
+        Parameters:
+            uid           - the UNIX username for the member
+            cn            - the real name of the member
+            program       - the member's program of study
+            description   - a description for the entry
+        """
+
+        dn = 'uid=' + uid + ',' + self.user_base
+        attrs = {
+            'objectClass': [ 'top', 'account', 'member' ],
+            'uid': [ uid ],
+            'cn': [ cn ],
+        }
+
+        if program:
+            attrs['program'] = [ program ]
+        if description:
+            attrs['description'] = [ description ]
+
+        try:
+            modlist = ldap.modlist.addModlist(attrs)
+            self.ldap.add_s(dn, modlist)
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to add: %s" % e)
+
+
+
+    ### Miscellaneous Methods ###
+
+    def escape(self, value):
+        """
+        Escapes special characters in a value so that it may be safely inserted
+        into an LDAP search filter.
+        """
+
+        value = str(value)
+        value = value.replace('\\', '\\5c').replace('*', '\\2a')
+        value = value.replace('(', '\\28').replace(')', '\\29')
+        value = value.replace('\x00', '\\00')
+        return value
+
+
+    def make_modlist(self, old, new):
+        keys = set(old.keys()).union(set(new))
+        mlist = []
+        for key in keys:
+            if key in old and not key in new:
+                mlist.append((ldap.MOD_DELETE, key, list(set(old[key]))))
+            elif key in new and not key in old:
+                mlist.append((ldap.MOD_ADD, key, list(set(new[key]))))
+            else:
+                to_add = list(set(new[key]) - set(old[key]))
+                if len(to_add) > 0:
+                    mlist.append((ldap.MOD_ADD, key, to_add))
+                to_del = list(set(old[key]) - set(new[key]))
+                if len(to_del) > 0:
+                    mlist.append((ldap.MOD_DELETE, key, to_del))
+        return mlist
+
+
+class Sasl:
+
+    def __init__(self, mech, realm):
+        self.mech = mech
+        self.realm = realm
+
+    def callback(self, id, challenge, prompt, defresult):
+        return ''
diff --git a/ceo/members.py b/ceo/members.py
new file mode 100644 (file)
index 0000000..9da1e61
--- /dev/null
@@ -0,0 +1,419 @@
+"""
+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, subprocess, ldap
+from ceo import conf, excep, ldapi
+from ceo.excep import InvalidArgument
+
+
+### Configuration ###
+
+CONFIG_FILE = '/etc/csc/accounts.cf'
+
+cfg = {}
+
+def configure():
+    """Load Members Configuration"""
+
+    string_fields = [ 'username_regex', 'shells_file', 'server_url',
+            'users_base', 'groups_base', 'sasl_mech', 'sasl_realm',
+            'admin_bind_keytab', 'admin_bind_userid', 'realm',
+            'admin_principal', 'admin_keytab' ]
+    numeric_fields = [ 'min_password_length' ]
+
+    # read configuration file
+    cfg_tmp = conf.read(CONFIG_FILE)
+
+    # verify configuration
+    conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
+    conf.check_integer_fields(CONFIG_FILE, numeric_fields, cfg_tmp)
+
+    # update the current configuration with the loaded values
+    cfg.update(cfg_tmp)
+
+
+
+### Exceptions ###
+
+ConfigurationException = conf.ConfigurationException
+LDAPException = ldapi.LDAPException
+
+class MemberException(Exception):
+    """Base exception class for member-related errors."""
+
+class InvalidTerm(MemberException):
+    """Exception class for malformed terms."""
+    def __init__(self, term):
+        self.term = term
+    def __str__(self):
+        return "Term is invalid: %s" % self.term
+
+class NoSuchMember(MemberException):
+    """Exception class for nonexistent members."""
+    def __init__(self, memberid):
+        self.memberid = memberid
+    def __str__(self):
+        return "Member not found: %d" % self.memberid
+
+class ChildFailed(MemberException):
+    def __init__(self, program, status, output):
+        self.program, self.status, self.output = program, status, output
+    def __str__(self):
+        msg = '%s failed with status %d' % (self.program, self.status)
+        if self.output:
+            msg += ': %s' % self.output
+        return msg
+
+
+### Connection Management ###
+
+# global directory connection
+ldap_connection = ldapi.LDAPConnection()
+
+def connect():
+    """Connect to LDAP."""
+
+    configure()
+
+    ldap_connection.connect_sasl(cfg['server_url'], cfg['sasl_mech'],
+        cfg['sasl_realm'], cfg['users_base'], cfg['groups_base'])
+
+def disconnect():
+    """Disconnect from LDAP."""
+
+    ldap_connection.disconnect()
+
+
+def connected():
+    """Determine whether the connection has been established."""
+
+    return ldap_connection.connected()
+
+
+
+### Members ###
+
+def create_member(username, password, name, program):
+    """
+    Creates a UNIX user account with options tailored to CSC members.
+
+    Parameters:
+        username - the desired UNIX username
+        password - the desired UNIX password
+        name     - the member's real name
+        program  - the member's program of study
+
+    Exceptions:
+        InvalidArgument - on bad account attributes provided
+
+    Returns: the uid number of the new account
+
+    See: create()
+    """
+
+    # check username format
+    if not username or not re.match(cfg['username_regex'], username):
+        raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
+
+    # check password length
+    if not password or len(password) < cfg['min_password_length']:
+        raise InvalidArgument("password", "<hidden>", "too short (minimum %d characters)" % cfg['min_password_length'])
+
+    args = [ "/usr/bin/addmember", "--stdin", username, name, program ]
+    addmember = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    out, err = addmember.communicate(password)
+    status = addmember.wait()
+
+    if status:
+        raise ChildFailed("addmember", status, out+err)
+
+
+def get(userid):
+    """
+    Look up attributes of a member by userid.
+
+    Returns: a dictionary of attributes
+
+    Example: get('mspang') -> {
+                 'cn': [ 'Michael Spang' ],
+                 'program': [ 'Computer Science' ],
+                 ...
+             }
+    """
+
+    return ldap_connection.user_lookup(userid)
+
+
+def list_term(term):
+    """
+    Build a list of members in a term.
+
+    Parameters:
+        term - the term to match members against
+
+    Returns: a list of members
+
+    Example: list_term('f2006'): -> {
+                 'mspang': { 'cn': 'Michael Spang', ... },
+                 'ctdalek': { 'cn': 'Calum T. Dalek', ... },
+                 ...
+             }
+    """
+
+    return ldap_connection.member_search_term(term)
+
+
+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'): -> {
+                 'mspang': { 'cn': 'Michael Spang', ... },
+                 ...
+             ]
+    """
+
+    return ldap_connection.member_search_name(name)
+
+
+def list_group(group):
+    """
+    Build a list of members in a group.
+
+    Parameters:
+        group - the group to match members against
+
+    Returns: a list of member dictionaries
+
+    Example: list_name('syscom'): -> {
+                 'mspang': { 'cn': 'Michael Spang', ... },
+                 ...
+             ]
+    """
+
+    members = group_members(group)
+    ret = {}
+    if members:
+        for member in members:
+            info = get(member)
+            if info:
+                ret[member] = info
+    return ret
+
+
+def list_positions():
+    """
+    Build a list of positions
+
+    Returns: a list of positions and who holds them
+
+    Example: list_positions(): -> {
+                 'president': { 'mspang': { 'cn': 'Michael Spang', ... } } ],
+                 ...
+             ]
+    """
+
+    ceo_ldap = ldap_connection.ldap
+    user_base = ldap_connection.user_base
+
+    members = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE, '(position=*)')
+    positions = {}
+    for (_, member) in members:
+        for position in member['position']:
+            if not position in positions:
+                positions[position] = {}
+            positions[position][member['uid'][0]] = member
+    return positions
+
+def set_position(position, members):
+    """
+    Sets a position
+
+    Parameters:
+        position - the position to set
+        members - an array of members that hold the position
+
+    Example: set_position('president', ['dtbartle'])
+    """
+
+    ceo_ldap = ldap_connection.ldap
+    user_base = ldap_connection.user_base
+    escape = ldap_connection.escape
+
+    res = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE,
+        '(&(objectClass=member)(position=%s))' % escape(position))
+    old = set([ member['uid'][0] for (_, member) in res ])
+    new = set(members)
+    mods = {
+        'del': set(old) - set(new),
+        'add': set(new) - set(old),
+    }
+    if len(mods['del']) == 0 and len(mods['add']) == 0:
+        return
+
+    for action in ['del', 'add']:
+        for userid in mods[action]:
+            dn = 'uid=%s,%s' % (escape(userid), user_base)
+            entry1 = {'position' : [position]}
+            entry2 = {} #{'position' : []}
+            entry = ()
+            if action == 'del':
+                entry = (entry1, entry2)
+            elif action == 'add':
+                entry = (entry2, entry1)
+            mlist = ldap_connection.make_modlist(entry[0], entry[1])
+            ceo_ldap.modify_s(dn, mlist)
+
+
+def change_group_member(action, group, userid):
+
+    ceo_ldap = ldap_connection.ldap
+    user_base = ldap_connection.user_base
+    group_base = ldap_connection.group_base
+    escape = ldap_connection.escape
+
+    user_dn = 'uid=%s,%s' % (escape(userid), user_base)
+    group_dn = 'cn=%s,%s' % (escape(group), group_base)
+    entry1 = {'uniqueMember' : []}
+    entry2 = {'uniqueMember' : [user_dn]}
+    entry = []
+    if action == 'add' or action == 'insert':
+        entry = (entry1, entry2)
+    elif action == 'remove' or action == 'delete':
+        entry = (entry2, entry1)
+    else:
+        raise InvalidArgument("action", action, "invalid action")
+    mlist = ldap_connection.make_modlist(entry[0], entry[1])
+    ceo_ldap.modify_s(group_dn, mlist)
+
+
+
+### Clubs ###
+
+def create_club(username, name):
+    """
+    Creates a UNIX user account with options tailored to CSC-hosted clubs.
+    
+    Parameters:
+        username - the desired UNIX username
+        name     - the club name
+
+    Exceptions:
+        InvalidArgument - on bad account attributes provided
+
+    Returns: the uid number of the new account
+
+    See: create()
+    """
+
+    # check username format
+    if not username or not re.match(cfg['username_regex'], username):
+        raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
+    
+    args = [ "/usr/bin/addclub", username, name ]
+    addclub = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    out, err = addclub.communicate()
+    status = addclub.wait()
+
+    if status:
+        raise ChildFailed("addclub", status, out+err)
+
+
+
+### Terms ###
+
+def register(userid, term_list):
+    """
+    Registers a member for one or more terms.
+
+    Parameters:
+        userid  - the member's username
+        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"])
+    """
+
+    ceo_ldap = ldap_connection.ldap
+    user_base = ldap_connection.user_base
+    escape = ldap_connection.escape
+    user_dn = 'uid=%s,%s' % (escape(userid), user_base)
+
+    if type(term_list) in (str, unicode):
+        term_list = [ term_list ]
+
+    ldap_member = ldap_connection.member_lookup(userid)
+    if ldap_member and 'term' not in ldap_member:
+        ldap_member['term'] = []
+
+    if not ldap_member:
+        raise NoSuchMember(userid)
+
+    new_member = ldap_member.copy()
+    new_member['term'] = new_member['term'][:]
+
+    for term in term_list:
+
+        # check term syntax
+        if not re.match('^[wsf][0-9]{4}$', term):
+            raise InvalidTerm(term)
+
+        # add the term to the entry
+        if not term in ldap_member['term']:
+            new_member['term'].append(term)
+
+    mlist = ldap_connection.make_modlist(ldap_member, new_member)
+    ceo_ldap.modify_s(user_dn, mlist)
+
+
+def registered(userid, term):
+    """
+    Determines whether a member is registered
+    for a term.
+
+    Parameters:
+        userid   - the member's username
+        term     - the term to check
+
+    Returns: whether the member is registered
+
+    Example: registered("mspang", "f2006") -> True
+    """
+
+    member = ldap_connection.member_lookup(userid)
+    return 'term' in member and term in member['term']
+
+
+def group_members(group):
+
+    """
+    Returns a list of group members
+    """
+
+    group = ldap_connection.group_lookup(group)
+    if group:
+        if 'uniqueMember' in group:
+            r = re.compile('^uid=([^,]*)')
+            return map(lambda x: r.match(x).group(1), group['uniqueMember'])
+        elif 'memberUid' in group:
+            return group['memberUid']
+        else:
+            return []
+    else:
+        return []
diff --git a/ceo/terms.py b/ceo/terms.py
new file mode 100644 (file)
index 0000000..63b9bb5
--- /dev/null
@@ -0,0 +1,254 @@
+"""
+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 validate(term):
+    """
+    Determines whether a term is well-formed.
+
+    Parameters:
+        term - the term string
+
+    Returns: whether the term is valid (boolean)
+
+    Example: validate("f2006") -> True
+    """
+
+    regex = '^[wsf][0-9]{4}$'
+    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 validate(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__':
+
+    from ceo.test import test, assert_equal, success
+
+    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()
diff --git a/ceo/test.py b/ceo/test.py
new file mode 100644 (file)
index 0000000..a60b2ed
--- /dev/null
@@ -0,0 +1,42 @@
+"""
+Common Test Routines
+
+This module contains helpful functions called by each module's test suite.
+"""
+from types import FunctionType, MethodType, ClassType, TypeType
+
+
+class TestException(Exception):
+    """Exception class for test failures."""
+
+
+def test(subject):
+    """Print a test message."""
+    if type(subject) in (MethodType, FunctionType, ClassType, TypeType):
+        print "testing %s()..." % subject.__name__,
+    else:
+        print "testing %s..." % subject,
+
+
+def success():
+    """Print a success message."""
+    print "pass."
+
+
+def assert_equal(expected, actual):
+    if expected != actual:
+        message = "Expected (%s)\nWas      (%s)" % (repr(expected), repr(actual))
+        fail(message)
+
+
+def fail(message):
+    print "failed!"
+    raise TestException("Test failed:\n%s" % message)
+
+
+def negative(call, args, excep, message):
+    try:
+        call(*args)
+        fail(message)
+    except excep:
+        pass
diff --git a/ceo/urwid/__init__.py b/ceo/urwid/__init__.py
new file mode 100644 (file)
index 0000000..c868e93
--- /dev/null
@@ -0,0 +1 @@
+"""Urwid User Interface"""
diff --git a/ceo/urwid/groups.py b/ceo/urwid/groups.py
new file mode 100644 (file)
index 0000000..e8fa66a
--- /dev/null
@@ -0,0 +1,129 @@
+import urwid
+from ceo import members
+from ceo.excep import InvalidArgument
+from ceo.urwid import search
+from ceo.urwid.widgets import *
+from ceo.urwid.window import *
+
+def menu_items(items):
+    return [ urwid.AttrWrap( ButtonText( cb, data, txt ), 'menu', 'selected') for (txt, cb, data) in items ]
+
+def change_group_member(data):
+    push_wizard("%s %s Member" % (data["action"], data["name"]), [
+        (ChangeMember, data),
+        EndPage,
+    ])
+
+def list_group_members(data):
+    mlist = members.list_group( data["group"] ).values()
+    search.member_list( mlist )
+
+def group_members(data):
+    add_data = data.copy()
+    add_data['action'] = 'Add'
+    remove_data = data.copy()
+    remove_data['action'] = 'Remove'
+    menu = [
+        ("Add %s member" % data["name"].lower(),
+            change_group_member, add_data),
+        ("Remove %s member" % data["name"].lower(),
+            change_group_member, remove_data),
+        ("List %s members" % data["name"].lower(), list_group_members, data),
+        ("Back", raise_back, None),
+    ]
+
+    listbox = urwid.ListBox( menu_items( menu ) )
+    push_window(listbox, "Manage %s" % data["name"])
+
+class IntroPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Managing Club or Group" ),
+            urwid.Divider(),
+            urwid.Text( "Adding a member to a club will also grant them "
+                        "access to the club's files and allow them to "
+                        "become_club."
+                        "\n\n"
+                        "Do not manage office and syscom related groups using "
+                        "this interface. Instead use the \"Manage Office "
+                        "Staff\" and \"Manage Systems Committee\" entries "
+                        "from the main menu." )
+        ]
+    def focusable(self):
+        return False
+
+class InfoPage(WizardPanel):
+    def init_widgets(self):
+        self.group = WordEdit("Club or Group: ")
+        self.widgets = [
+            urwid.Text( "Club or Group Information"),
+            urwid.Divider(),
+            self.group,
+        ]
+    def check(self):
+        group = self.group.get_edit_text()
+        # TODO - check that group is valid
+        group_name = group # TODO
+        data = {
+            "name" : group,
+            "group" : group_name,
+            "groups" : [group],
+        }
+        group_members(data)
+
+class ChangeMember(WizardPanel):
+    def __init__(self, state, data):
+        state['data'] = data
+        WizardPanel.__init__(self, state)
+    def init_widgets(self):
+        self.userid = WordEdit("Username: ")
+
+        data = self.state['data']
+        self.widgets = [
+            urwid.Text( "%s %s Member" % (data['action'], data['name']) ),
+            urwid.Divider(),
+            self.userid,
+        ]
+    def check(self):
+        self.state['userid'] = self.userid.get_edit_text()
+        if self.state['userid']:
+            self.state['member'] = members.get(self.userid.get_edit_text())
+        if not self.state['member']:
+            set_status("Member not found")
+            self.focus_widget(self.userid)
+            return True
+        clear_status()
+
+class EndPage(WizardPanel):
+    def init_widgets(self):
+        self.headtext = urwid.Text("")
+        self.midtext = urwid.Text("")
+        self.widgets = [
+            self.headtext,
+            urwid.Divider(),
+            self.midtext,
+        ]
+    def focusable(self):
+        return False
+    def check(self):
+        pop_window()
+    def activate(self):
+        data = self.state['data']
+        action = data['action'].lower()
+        failed = []
+        for group in data['groups']:
+            try:
+                members.change_group_member(action, group, self.state['userid'])
+            except:
+                failed.append(group)
+        if len(failed) == 0:
+            self.headtext.set_text("%s succeeded" % data['action'])
+            self.midtext.set_text("Congratulations, the group modification "
+                "has succeeded.")
+        else:
+            self.headtext.set_text("%s partially succeeded" % data['action'])
+            self.midtext.set_text("Failed to %s member to %s for the "
+                "following groups: %s. This may indicate an attempt to add a "
+                "duplicate group member or to delete a non-present group "
+                "member." % (data['action'].lower(), data['name'],
+                ', '.join(failed)))
diff --git a/ceo/urwid/info.py b/ceo/urwid/info.py
new file mode 100644 (file)
index 0000000..3dab4ac
--- /dev/null
@@ -0,0 +1,37 @@
+import urwid
+from ceo import members
+from ceo.excep import InvalidArgument
+from ceo.urwid.widgets import *
+from ceo.urwid.window import *
+
+class InfoPage(WizardPanel):
+    def init_widgets(self):
+        self.userid = urwid.Text("")
+        self.name = urwid.Text("")
+        self.terms = urwid.Text("")
+        self.program = urwid.Text("")
+
+        self.widgets = [
+            urwid.Text( "Member Details" ),
+            urwid.Divider(),
+            self.name,
+            self.userid,
+            self.program,
+            urwid.Divider(),
+            self.terms,
+        ]
+    def focusable(self):
+        return False
+    def activate(self):
+        member  = self.state.get('member', {})
+        name    = member.get('cn', [''])[0]
+        userid  = self.state['userid']
+        program = member.get('program', [''])[0]
+        terms   = member.get('term', [])
+
+        self.name.set_text("Name: %s" % name)
+        self.userid.set_text("User: %s" % userid)
+        self.program.set_text("Program: %s" % program)
+        self.terms.set_text("Terms: %s" % ", ".join(terms))
+    def check(self):
+        pop_window()
diff --git a/ceo/urwid/ldapfilter.py b/ceo/urwid/ldapfilter.py
new file mode 100644 (file)
index 0000000..6aee341
--- /dev/null
@@ -0,0 +1,40 @@
+import ldap
+
+class LdapFilter:
+    def __init__(self, widget):
+        self.widget = widget
+
+    def set_ldap_filter(self, ldap_uri, ldap_base, ldap_attr, ldap_map):
+        try:
+            self.ldap = ldap.initialize(ldap_uri)
+            self.ldap.simple_bind_s("", "")
+        except ldap.LDAPError:
+            return
+        self.base = ldap_base
+        self.attr = ldap_attr
+        self.map = ldap_map
+
+    def keypress(self, size, key):
+        if self.ldap != None:
+            if key == 'enter' or key == 'down' or key == 'up':
+                attr = self.escape(self.attr)
+                search = self.escape(self.widget.get_edit_text(self))
+                filter = '(%s=%s)' % (attr, search)
+                try:
+                    matches = self.ldap.search_s(self.base,
+                        ldap.SCOPE_SUBTREE, filter)
+                    if len(matches) > 0:
+                        (_, attrs) = matches[0]
+                        for (k, v) in self.map.items():
+                            if attrs.has_key(k) and len(attrs[k]) > 0:
+                                v.set_edit_text(attrs[k][0])
+                except ldap.LDAPError:
+                    pass
+        return self.widget.keypress(self, size, key)
+
+    def escape(self, value):
+        value = str(value)
+        value = value.replace('\\', '\\5c').replace('*', '\\2a')
+        value = value.replace('(', '\\28').replace(')', '\\29')
+        value = value.replace('\x00', '\\00')
+        return value
diff --git a/ceo/urwid/main.py b/ceo/urwid/main.py
new file mode 100644 (file)
index 0000000..a5da33c
--- /dev/null
@@ -0,0 +1,155 @@
+import random, ldap, urwid.curses_display
+from ceo import members, terms
+from ceo.excep import InvalidArgument
+from ceo.urwid.widgets import *
+from ceo.urwid.window import *
+from ceo.urwid import newmember, renew, info, search, positions, groups
+
+ui = urwid.curses_display.Screen()
+
+ui.register_palette([
+    # name, foreground, background, mono
+    ('banner', 'light gray', 'default', None),
+    ('menu', 'light gray', 'default', 'bold'),
+    ('selected', 'black', 'light gray', 'bold'),
+])
+
+
+def program_name():
+    cwords = [ "CSC" ] * 20 + [ "Club" ] * 10 + [ "Campus" ] * 5 + \
+        [ "Communist", "Canadian", "Celestial", "Cryptographic", "Calum's",
+          "Canonical", "Capitalist", "Catastrophic", "Ceremonial", "Chaotic", "Civic",
+          "City", "County", "Caffeinated" ]
+    ewords = [ "Embellished", "Ergonomic", "Electric", "Eccentric", "European", "Economic",
+        "Evil", "Egotistical", "Elliptic", "Emasculating", "Embalming",
+        "Embryonic", "Emigrant", "Emissary's", "Emoting", "Employment", "Emulated",
+        "Enabling", "Enamoring", "Encapsulated", "Enchanted", "Encoded", "Encrypted",
+        "Encumbered", "Endemic", "Enhanced", "Enigmatic", "Enlightened", "Enormous",
+        "Enrollment", "Enshrouded", "Ephemeral", "Epidemic", "Episodic", "Epsilon",
+        "Equitable", "Equestrian", "Equilateral", "Erroneous", "Erratic",
+        "Espresso", "Essential", "Estate", "Esteemed", "Eternal", "Ethical", "Eucalyptus",
+        "Euphemistic", "Evangelist", "Evasive", "Everyday", "Evidence", "Eviction", "Evildoer's",
+        "Evolution", "Exacerbation", "Exalted", "Examiner's", "Excise", "Exciting", "Exclusion",
+        "Exec", "Executioner's", "Exile", "Existential", "Expedient", "Expert", "Expletive",
+        "Exploiter's", "Explosive", "Exponential", "Exposing", "Extortion", "Extraction",
+        "Extraneous", "Extravaganza", "Extreme", "Extraterrestrial", "Extremist", "Eerie" ]
+    owords = [ "Office" ] * 50 + [ "Outhouse", "Outpost" ]
+
+    cword = random.choice(cwords)
+    eword = random.choice(ewords)
+    oword = random.choice(owords)
+
+    return "%s %s %s" % (cword, eword, oword)
+
+office_data = {
+    "name" : "Office Staff",
+    "group" : "office",
+    "groups" : [ "office", "cdrom", "audio", "video", "www" ],
+}
+
+syscom_data = {
+    "name" : "Systems Committee",
+    "group" : "syscom",
+    "groups" : [ "office", "staff", "adm", "src" ],
+}
+
+def menu_items(items):
+    return [ urwid.AttrWrap( ButtonText( cb, data, txt ), 'menu', 'selected') for (txt, cb, data) in items ]
+
+def main_menu():
+    menu = [
+        ("New Member", new_member, None),
+        ("Renew Membership", renew_member, None),
+        ("Create Club Account", new_club, None),
+        ("Display Member", display_member, None),
+        ("Search", search_members, None),
+        ("Manage Club or Group Members", manage_group, None),
+        ("Manage Positions", manage_positions, None),
+        ("Manage Office Staff", groups.group_members, office_data),
+        ("Manage Systems Committee", groups.group_members, syscom_data),
+        ("Exit", raise_abort, None),
+    ]
+
+    listbox = urwid.ListBox( menu_items( menu ) )
+    return listbox
+
+def new_member(*args, **kwargs):
+    push_wizard("New Member", [
+        newmember.IntroPage,
+        newmember.InfoPage,
+        newmember.SignPage,
+        newmember.PassPage,
+        newmember.EndPage,
+    ])
+
+def new_club(*args, **kwargs):
+    push_wizard("New Club Account", [
+        newmember.ClubIntroPage,
+        newmember.ClubInfoPage,
+        (newmember.EndPage, "club"),
+    ], (60, 15))
+
+def manage_group(*args, **kwargs):
+    push_wizard("Manage Club or Group Members", [
+        groups.IntroPage,
+        groups.InfoPage,
+    ], (60, 15))
+
+def renew_member(*args, **kwargs):
+    push_wizard("Renew Membership", [
+        renew.IntroPage,
+        renew.UserPage,
+        renew.TermPage,
+        renew.PayPage,
+        renew.EndPage,
+    ])
+
+def display_member(data):
+    push_wizard("Display Member", [
+        renew.UserPage,
+        info.InfoPage,
+    ], (60, 15))
+
+def search_members(data):
+    menu = [
+        ("Members by term", search_term, None),
+        ("Members by name", search_name, None),
+        ("Members by group", search_group, None),
+        ("Back", raise_back, None),
+    ]
+
+    listbox = urwid.ListBox( menu_items( menu ) )
+    push_window(listbox, "Search")
+
+def search_name(data):
+    push_wizard("By Name", [ search.NamePage ])
+
+def search_term(data):
+    push_wizard("By Term", [ search.TermPage ])
+
+def search_group(data):
+    push_wizard("By Group", [ search.GroupPage ])
+
+def manage_positions(data):
+    push_wizard("Manage Positions", [
+        positions.IntroPage,
+        positions.InfoPage,
+        positions.EndPage,
+    ], (50, 15))
+
+def run():
+    members.connect()
+
+    push_window( main_menu(), program_name() )
+    event_loop( ui )
+
+def start():
+    try:
+        ui.run_wrapper( run )
+    except ldap.LOCAL_ERROR, e:
+        print e[0]['info']
+        print "Hint: You may need to run 'kinit'"
+
+if __name__ == '__main__':
+    import os
+    start()
diff --git a/ceo/urwid/newmember.py b/ceo/urwid/newmember.py
new file mode 100644 (file)
index 0000000..1a56608
--- /dev/null
@@ -0,0 +1,178 @@
+import urwid
+from ceo import members, terms
+from ceo.excep import InvalidArgument
+from ceo.urwid.widgets import *
+from ceo.urwid.window import *
+
+class IntroPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Joining the Computer Science Club" ),
+            urwid.Divider(),
+            urwid.Text( "CSC membership is $2.00 for one term. Please ensure "
+                        "the fee is deposited into the safe before continuing." ),
+        ]
+    def focusable(self):
+        return False
+
+class ClubIntroPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Club Services" ),
+            urwid.Divider(),
+            urwid.Text( "We provide other UW clubs accounts for email and "
+                        "web hosting, free of charge. Like members, clubs "
+                        "get web hosting at %s. We can also arrange for "
+                        "uwaterloo.ca subdomains; please instruct the club "
+                        "representative to contact the systems committee "
+                        "for more information."
+                        "\n\nNote: This is not complete. Authorizing members "
+                        "to access the club account still requires manual "
+                        "intervention."
+                        % "http://csclub.uwaterloo.ca/~clubid/"
+            )
+        ]
+    def focusable(self):
+        return False
+
+class InfoPage(WizardPanel):
+    def init_widgets(self):
+        self.userid = LdapFilterWordEdit("UWuserid: ")
+        self.name = SingleEdit("Full name: ")
+        self.program = SingleEdit("Program of Study: ")
+        self.userid.set_ldap_filter(
+            "ldap://uwldap.uwaterloo.ca/", "dc=uwaterloo,dc=ca",
+            "uid", {'cn':self.name, 'ou':self.program}
+        )
+        self.widgets = [
+            urwid.Text( "Member Information - Please Check ID" ),
+            urwid.Divider(),
+            self.userid,
+            self.name,
+            self.program,
+        ]
+    def check(self):
+        self.state['userid'] = self.userid.get_edit_text()
+        self.state['name'] = self.name.get_edit_text()
+        self.state['program'] = self.program.get_edit_text()
+
+        if len( self.state['userid'] ) < 3:
+            self.focus_widget( self.userid )
+            set_status("Username is too short")
+            return True
+        elif len( self.state['name'] ) < 4:
+            self.focus_widget( self.name )
+            set_status("Name is too short")
+            return True
+        clear_status()
+
+class ClubInfoPage(WizardPanel):
+    def init_widgets(self):
+        self.userid = WordEdit("Username: ")
+        self.name = SingleEdit("Club Name: ")
+        self.widgets = [
+            urwid.Text( "Club Information" ),
+            urwid.Divider(),
+            self.userid,
+            self.name,
+        ]
+    def check(self):
+        self.state['userid'] = self.userid.get_edit_text()
+        self.state['name'] = self.name.get_edit_text()
+
+        if len( self.state['userid'] ) < 3:
+            self.focus_widget( self.userid )
+            set_status("Username is too short")
+            return True
+        elif len( self.state['name'] ) < 4:
+            self.focus_widget( self.name )
+            set_status("Name is too short")
+            return True
+        clear_status()
+
+class SignPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Machine Usage Policy" ),
+            urwid.Divider(),
+            urwid.Text( "Ensure the new member has signed the "
+                        "Machine Usage Policy. Accounts of users who have not "
+                        "signed will be suspended if discovered." ),
+        ]
+    def focusable(self):
+        return False
+
+class PassPage(WizardPanel):
+    def init_widgets(self):
+        self.password = PassEdit("Password: ")
+        self.pwcheck = PassEdit("Re-enter: ")
+        self.widgets = [
+            urwid.Text( "Member Password" ),
+            urwid.Divider(),
+            self.password,
+            self.pwcheck,
+        ]
+    def focus_widget(self, widget):
+        self.box.set_focus( self.widgets.index( widget ) )
+    def clear_password(self):
+        self.focus_widget( self.password )
+        self.password.set_edit_text("")
+        self.pwcheck.set_edit_text("")
+    def check(self):
+        self.state['password'] = self.password.get_edit_text()
+        pwcheck = self.pwcheck.get_edit_text()
+
+        if self.state['password'] != pwcheck:
+            self.clear_password()
+            set_status("Passwords do not match")
+            return True
+        elif len(self.state['password']) < 5:
+            self.clear_password()
+            set_status("Password is too short")
+            return True
+        clear_status()
+
+class EndPage(WizardPanel):
+    def __init__(self, state, utype='member'):
+        self.utype = utype
+        WizardPanel.__init__(self, state)
+    def init_widgets(self):
+        self.headtext = urwid.Text("")
+        self.midtext = urwid.Text("")
+        self.widgets = [
+            self.headtext,
+            urwid.Divider(),
+            self.midtext,
+        ]
+    def focusable(self):
+        return False
+    def check(self):
+        pop_window()
+    def activate(self):
+        problem = None
+        try:
+            if self.utype == 'member':
+                members.create_member( self.state['userid'], self.state['password'], self.state['name'], self.state['program'] )
+                members.register( self.state['userid'], terms.current() )
+            elif self.utype == 'club':
+                members.create_club( self.state['userid'], self.state['name'] )
+            else:
+                raise Exception("Internal Error")
+        except members.InvalidArgument, e:
+            problem = str(e)
+        except members.LDAPException, e:
+            problem = str(e)
+        except members.ChildFailed, e:
+            problem = str(e)
+
+        if problem:
+            self.headtext.set_text("Failures Occured Adding User")
+            self.midtext.set_text("The error was:\n%s\nThe account may be partially added "
+                "and you may or may not be able to log in. Please contact systems committee." % problem)
+            return
+        else:
+            self.headtext.set_text("User Added")
+            self.midtext.set_text("Congratulations, %s has been added "
+                "successfully. You should also rebuild the website in "
+                "order to update the memberlist."
+                % self.state['userid'])
diff --git a/ceo/urwid/positions.py b/ceo/urwid/positions.py
new file mode 100644 (file)
index 0000000..33d89e2
--- /dev/null
@@ -0,0 +1,86 @@
+import urwid
+from ceo import members
+from ceo.excep import InvalidArgument
+from ceo.urwid.widgets import *
+from ceo.urwid.window import *
+
+position_data = [
+    ('president',       'President'),
+    ('vice-president',  'Vice-president'),
+    ('treasurer',       'Treasurer'),
+    ('secretary',       'Secretary'),
+    ('sysadmin',        'System Administrator'),
+    ('librarian',       'Librarian'),
+    ('imapd',           'Imapd'),
+    ('webmaster',       'Web Master'),
+]
+
+class IntroPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Managing Positions" ),
+            urwid.Divider(),
+            urwid.Text( "Enter a username for each position. If a position is "
+                        "held by multiple people, enter a comma-separated "
+                        "list of usernames. If a position is held by nobody "
+                        "leave the username blank." ),
+        ]
+    def focusable(self):
+        return False
+
+class InfoPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Positions" ),
+            urwid.Divider(),
+        ]
+        positions = members.list_positions()
+        self.position_widgets = {}
+        for (position, text) in position_data:
+            widget = WordEdit("%s: " % text)
+            if position in positions:
+                widget.set_edit_text(','.join(positions[position].keys()))
+            else:
+                widget.set_edit_text('')
+            self.position_widgets[position] = widget
+            self.widgets.append(widget)
+
+    def parse(self, entry):
+        if len(entry) == 0:
+            return []
+        return entry.split(',')
+
+    def check(self):
+        self.state['positions'] = {}
+        for (position, widget) in self.position_widgets.iteritems():
+            self.state['positions'][position] = \
+                self.parse(widget.get_edit_text())
+            for p in self.state['positions'][position]:
+                if members.get(p) == None:
+                    self.focus_widget(widget)
+                    set_status( "Invalid username: '%s'" % p )
+                    return True
+        clear_status()
+
+class EndPage(WizardPanel):
+    def init_widgets(self):
+        old = members.list_positions()
+        self.headtext = urwid.Text("")
+        self.midtext = urwid.Text("")
+
+        self.widgets = [
+            self.headtext,
+            urwid.Divider(),
+            self.midtext,
+        ]
+    def focusable(self):
+        return False
+    def activate(self):
+        for (position, info) in self.state['positions'].iteritems():
+            members.set_position(position, info)
+        self.headtext.set_text("Positions Updated")
+        self.midtext.set_text("Congratulations, positions have been updated. "
+            "You should rebuild the website in order to update the Positions "
+            "page.")
+    def check(self):
+        pop_window()
diff --git a/ceo/urwid/renew.py b/ceo/urwid/renew.py
new file mode 100644 (file)
index 0000000..cc66607
--- /dev/null
@@ -0,0 +1,115 @@
+import urwid
+from ceo import members, terms
+from ceo.urwid.widgets import *
+from ceo.urwid.window import *
+
+class IntroPage(WizardPanel):
+    def init_widgets(self):
+        self.widgets = [
+            urwid.Text( "Renewing Membership" ),
+            urwid.Divider(),
+            urwid.Text( "CSC membership is $2.00 per term. You may pre-register "
+                        "for future terms if desired." )
+        ]
+    def focusable(self):
+        return False
+
+class UserPage(WizardPanel):
+    def init_widgets(self):
+        self.userid = WordEdit("Username: ")
+
+        self.widgets = [
+            urwid.Text( "Member Information" ),
+            urwid.Divider(),
+            self.userid,
+        ]
+    def check(self):
+        self.state['userid'] = self.userid.get_edit_text()
+        self.state['member'] = None
+        if self.state['userid']:
+            self.state['member'] = members.get(self.userid.get_edit_text())
+        if not self.state['member']:
+            set_status("Member not found")
+            self.focus_widget(self.userid)
+            return True
+
+class TermPage(WizardPanel):
+    def init_widgets(self):
+        self.start = SingleEdit("Start: ")
+        self.count = SingleIntEdit("Count: ")
+
+        self.widgets = [
+            urwid.Text( "Terms to Register" ),
+            urwid.Divider(),
+            self.start,
+            self.count,
+        ]
+    def activate(self):
+        if not self.start.get_edit_text():
+            old_terms = []
+            if 'term' in self.state['member']:
+                old_terms = self.state['member']['term']
+            self.start.set_edit_text( terms.next_unregistered( old_terms ) )
+            self.count.set_edit_text( "1" )
+    def check(self):
+        try:
+            self.state['terms'] = terms.interval( self.start.get_edit_text(), self.count.value() )
+        except Exception, e:
+            self.focus_widget( self.start )
+            set_status( "Invalid start term" )
+            return True
+        for term in self.state['terms']:
+            if members.registered( self.state['userid'], term):
+                self.focus_widget( self.start )
+                set_status( "Already registered for " + term )
+                return True
+        if len(self.state['terms']) == 0:
+            self.focus_widget(self.count)
+            set_status( "Registering for zero terms?" )
+            return True
+
+class PayPage(WizardPanel):
+    def init_widgets(self):
+        self.midtext = urwid.Text("")
+
+        self.widgets = [
+            urwid.Text("Membership Fee"),
+            urwid.Divider(),
+            self.midtext,
+        ]
+    def focusable(self):
+        return False
+    def activate(self):
+        regterms = self.state['terms']
+        plural = "term"
+        if len(self.state['terms']) > 1:
+            plural = "terms"
+        self.midtext.set_text("You are registering for %d %s, and owe the "
+                       "Computer Science Club $%d.00 in membership fees. "
+                       "Please deposit the money in the safe before "
+                       "continuing. " % ( len(regterms), plural, len(regterms * 2)))
+
+class EndPage(WizardPanel):
+    def init_widgets(self):
+        self.headtext = urwid.Text("")
+        self.midtext = urwid.Text("")
+
+        self.widgets = [
+            self.headtext,
+            urwid.Divider(),
+            self.midtext,
+        ]
+    def focusable(self):
+        return False
+    def activate(self):
+        try:
+            members.register( self.state['userid'], self.state['terms'] )
+            self.headtext.set_text("Registration Succeeded")
+            self.midtext.set_text("The member has been registered for the following "
+                             "terms: " + ", ".join(self.state['terms']) + ".")
+        except Exception, e:
+            self.headtext.set_text("Failed to Register")
+            self.midtext.set_text("You may refund any fees paid or retry."
+                             "The error was: '%s'" % e)
+    def check(self):
+        pop_window()
diff --git a/ceo/urwid/search.py b/ceo/urwid/search.py
new file mode 100644 (file)
index 0000000..fa66b52
--- /dev/null
@@ -0,0 +1,84 @@
+import urwid
+from ceo import members, terms
+from ceo.excep import InvalidArgument
+from ceo.urwid.widgets import *
+from ceo.urwid.window import *
+
+class TermPage(WizardPanel):
+    def init_widgets(self):
+        self.term = SingleEdit("Term: ")
+
+        self.widgets = [
+            urwid.Text( "Terms Members" ),
+            urwid.Divider(),
+            self.term,
+        ]
+    def check(self):
+        try:
+            self.state['term'] = self.term.get_edit_text()
+            terms.parse( self.state['term'] )
+        except:
+            self.focus_widget( self.term )
+            set_status( "Invalid term" )
+            return True
+        mlist = members.list_term( self.state['term'] ).values()
+        pop_window()
+        member_list( mlist )
+
+class NamePage(WizardPanel):
+    def init_widgets(self):
+        self.name = SingleEdit("Name: ")
+
+        self.widgets = [
+            urwid.Text( "Members by Name" ),
+            urwid.Divider(),
+            self.name,
+        ]
+    def check(self):
+        self.state['name'] = self.name.get_edit_text()
+        if not self.state['name']:
+            self.focus_widget( self.name )
+            set_status( "Invalid name" )
+            return True
+        mlist = members.list_name( self.state['name'] ).values()
+        pop_window()
+        member_list( mlist )
+
+class GroupPage(WizardPanel):
+    def init_widgets(self):
+        self.group = SingleEdit("Group: ")
+
+        self.widgets = [
+            urwid.Text( "Members by Group" ),
+            urwid.Divider(),
+            self.group,
+        ]
+    def check(self):
+        self.state['group'] = self.group.get_edit_text()
+        if not self.state['group']:
+            self.focus_widget( self.group )
+            set_status( "Invalid group" )
+            return True
+        mlist = members.list_group( self.state['group'] ).values()
+        pop_window()
+        member_list( mlist )
+
+def member_list(mlist):
+    mlist = list(mlist)
+    mlist.sort( lambda x, y: cmp(x['uid'], y['uid']) )
+    buf = ''
+    for member in mlist:
+        if 'uid' in member:
+            uid = member['uid'][0]
+        else:
+            uid = None
+        if 'program' in member:
+            program = member['program'][0]
+        else:
+            program = None
+        attrs = ( uid, member['cn'][0], program )
+        buf += "%10s %30s\n%41s\n\n" % attrs
+    set_status("Press escape to return to the menu")
+    push_window(urwid.ListBox([urwid.Text(buf)]))
+
+
diff --git a/ceo/urwid/widgets.py b/ceo/urwid/widgets.py
new file mode 100644 (file)
index 0000000..ca52416
--- /dev/null
@@ -0,0 +1,113 @@
+import urwid
+from ceo.urwid.ldapfilter import *
+from ceo.urwid.window import raise_back, push_window
+
+def push_wizard(name, pages, dimensions=(50, 10)):
+    state = {}
+    wiz = Wizard()
+    for page in pages:
+        if type(page) != tuple:
+            page = (page, )
+        wiz.add_panel( page[0](state, *page[1:]) )
+    push_window( urwid.Filler( urwid.Padding(
+        urwid.LineBox(wiz), 'center', dimensions[0]),
+        'middle', dimensions[1] ), name )
+
+class ButtonText(urwid.Text):
+    def __init__(self, callback, data, *args, **kwargs):
+        self.callback = callback
+        self.data = data
+        urwid.Text.__init__(self, *args, **kwargs)
+    def selectable(self):
+        return True
+    def keypress(self, size, key):
+        if key == 'enter' and self.callback:
+            self.callback(self.data)
+        else:
+            return key
+
+class SingleEdit(urwid.Edit):
+    def keypress(self, size, key):
+        if key == 'enter':
+            return urwid.Edit.keypress(self, size, 'down')
+        else:
+            return urwid.Edit.keypress(self, size, key)
+
+class SingleIntEdit(urwid.IntEdit):
+    def keypress(self, size, key):
+        if key == 'enter':
+            return urwid.Edit.keypress(self, size, 'down')
+        else:
+            return urwid.Edit.keypress(self, size, key)
+
+class WordEdit(SingleEdit):
+    def valid_char(self, ch):
+        return urwid.Edit.valid_char(self, ch) and ch != ' '
+
+class LdapFilterWordEdit(LdapFilter, WordEdit):
+    def __init__(self, *args):
+        LdapFilter.__init__(self, WordEdit)
+        WordEdit.__init__(self, *args)
+
+class PassEdit(SingleEdit):
+    def get_text(self):
+        text = urwid.Edit.get_text(self)
+        return (self.caption + " " * len(self.get_edit_text()), text[1])
+
+class Wizard(urwid.WidgetWrap):
+    def __init__(self):
+        self.selected = None
+        self.panels = []
+
+        self.panelwrap = urwid.WidgetWrap( urwid.SolidFill() )
+        self.back = urwid.Button("Back", self.back)
+        self.next = urwid.Button("Next", self.next)
+        self.buttons = urwid.Columns( [ self.back, self.next ], dividechars=3, focus_column=1 )
+        pad = urwid.Padding( self.buttons, ('fixed right', 2), 19 )
+        self.pile = urwid.Pile( [self.panelwrap, ('flow', pad)], 0 )
+        urwid.WidgetWrap.__init__(self, self.pile)
+
+    def add_panel(self, panel):
+        self.panels.append( panel )
+        if len(self.panels) == 1:
+            self.select(0)
+
+    def select(self, panelno, set_focus=True):
+        if 0 <= panelno < len(self.panels):
+            self.selected = panelno
+            self.panelwrap.set_w( self.panels[panelno] )
+            self.panels[panelno].activate()
+
+            if set_focus:
+                if self.panels[panelno].focusable():
+                    self.pile.set_focus( 0 )
+                else:
+                    self.pile.set_focus( 1 )
+
+    def next(self, *args, **kwargs):
+        if self.panels[self.selected].check():
+            self.select( self.selected )
+            return
+        self.select(self.selected + 1)
+
+    def back(self, *args, **kwargs):
+        if self.selected == 0:
+            raise_back()
+        self.select(self.selected - 1, False)
+
+class WizardPanel(urwid.WidgetWrap):
+    def __init__(self, state):
+        self.state = state
+        self.init_widgets()
+        self.box = urwid.ListBox( urwid.SimpleListWalker( self.widgets ) )
+        urwid.WidgetWrap.__init__( self, self.box )
+    def init_widgets(self):
+        self.widgets = []
+    def focus_widget(self, widget):
+        self.box.set_focus( self.widgets.index( widget ) )
+    def focusable(self):
+        return True
+    def check(self):
+        return
+    def activate(self):
+        return
diff --git a/ceo/urwid/window.py b/ceo/urwid/window.py
new file mode 100644 (file)
index 0000000..836b11f
--- /dev/null
@@ -0,0 +1,66 @@
+import urwid
+
+window_stack = []
+window_names = []
+
+header = urwid.Text( "" )
+footer = urwid.Text( "" )
+top = urwid.Frame( urwid.SolidFill(), header, footer )
+
+def push_window( frame, name=None ):
+    window_stack.append( frame )
+    window_names.append( name )
+    update_top()
+
+def pop_window():
+    if len(window_stack) == 1:
+       return False
+    window_stack.pop()
+    window_names.pop()
+    update_top()
+    clear_status()
+    return True
+
+def update_top():
+    names = [ n for n in window_names if n ]
+    header.set_text(" - ".join( names ) + "\n")
+    top.set_body( window_stack[-1] )
+
+def set_status(message):
+    footer.set_text(message)
+
+def clear_status():
+    footer.set_text("")
+
+class Abort(Exception):
+    pass
+
+class Back(Exception):
+    pass
+
+def raise_abort(*args, **kwargs):
+    raise Abort()
+
+def raise_back(*args, **kwarg):
+    raise Back()
+
+def event_loop(ui):
+    while True:
+        try:
+           cols, rows = ui.get_cols_rows()
+           canvas = top.render( (cols, rows), focus=True )
+           ui.draw_screen( (cols, rows), canvas )
+
+           keys = ui.get_input()
+           for k in keys:
+              if k == "esc":
+                 if not pop_window():
+                     break
+              elif k == "window resize":
+                 (cols, rows) = ui.get_cols_rows()
+              else:
+                 top.keypress( (cols, rows), k )
+        except Back:
+            pop_window()
+        except (Abort, KeyboardInterrupt):
+            return
diff --git a/pylib/csc/__init__.py b/pylib/csc/__init__.py
deleted file mode 100644 (file)
index dfdefbd..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-"""
-Computer Science Club Python Modules
-
-The csc module is a container for all CSC-specific Python modules.
-"""
diff --git a/pylib/csc/adm/__init__.py b/pylib/csc/adm/__init__.py
deleted file mode 100644 (file)
index 2c37b1b..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-"""
-CSC Administrative Modules
-
-This module provides member and account management modules.
-
-    members  - member registration management functions
-    terms    - helper routines for manipulating terms
-"""
diff --git a/pylib/csc/adm/members.py b/pylib/csc/adm/members.py
deleted file mode 100644 (file)
index bdf5bac..0000000
+++ /dev/null
@@ -1,420 +0,0 @@
-"""
-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, subprocess, ldap
-from csc.common import conf
-from csc.common.excep import InvalidArgument
-from csc.backends import ldapi
-
-
-### Configuration ###
-
-CONFIG_FILE = '/etc/csc/accounts.cf'
-
-cfg = {}
-
-def configure():
-    """Load Members Configuration"""
-
-    string_fields = [ 'username_regex', 'shells_file', 'server_url',
-            'users_base', 'groups_base', 'sasl_mech', 'sasl_realm',
-            'admin_bind_keytab', 'admin_bind_userid', 'realm',
-            'admin_principal', 'admin_keytab' ]
-    numeric_fields = [ 'min_password_length' ]
-
-    # read configuration file
-    cfg_tmp = conf.read(CONFIG_FILE)
-
-    # verify configuration
-    conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
-    conf.check_integer_fields(CONFIG_FILE, numeric_fields, cfg_tmp)
-
-    # update the current configuration with the loaded values
-    cfg.update(cfg_tmp)
-
-
-
-### Exceptions ###
-
-ConfigurationException = conf.ConfigurationException
-LDAPException = ldapi.LDAPException
-
-class MemberException(Exception):
-    """Base exception class for member-related errors."""
-
-class InvalidTerm(MemberException):
-    """Exception class for malformed terms."""
-    def __init__(self, term):
-        self.term = term
-    def __str__(self):
-        return "Term is invalid: %s" % self.term
-
-class NoSuchMember(MemberException):
-    """Exception class for nonexistent members."""
-    def __init__(self, memberid):
-        self.memberid = memberid
-    def __str__(self):
-        return "Member not found: %d" % self.memberid
-
-class ChildFailed(MemberException):
-    def __init__(self, program, status, output):
-        self.program, self.status, self.output = program, status, output
-    def __str__(self):
-        msg = '%s failed with status %d' % (self.program, self.status)
-        if self.output:
-            msg += ': %s' % self.output
-        return msg
-
-
-### Connection Management ###
-
-# global directory connection
-ldap_connection = ldapi.LDAPConnection()
-
-def connect():
-    """Connect to LDAP."""
-
-    configure()
-
-    ldap_connection.connect_sasl(cfg['server_url'], cfg['sasl_mech'],
-        cfg['sasl_realm'], cfg['users_base'], cfg['groups_base'])
-
-def disconnect():
-    """Disconnect from LDAP."""
-
-    ldap_connection.disconnect()
-
-
-def connected():
-    """Determine whether the connection has been established."""
-
-    return ldap_connection.connected()
-
-
-
-### Members ###
-
-def create_member(username, password, name, program):
-    """
-    Creates a UNIX user account with options tailored to CSC members.
-
-    Parameters:
-        username - the desired UNIX username
-        password - the desired UNIX password
-        name     - the member's real name
-        program  - the member's program of study
-
-    Exceptions:
-        InvalidArgument - on bad account attributes provided
-
-    Returns: the uid number of the new account
-
-    See: create()
-    """
-
-    # check username format
-    if not username or not re.match(cfg['username_regex'], username):
-        raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
-
-    # check password length
-    if not password or len(password) < cfg['min_password_length']:
-        raise InvalidArgument("password", "<hidden>", "too short (minimum %d characters)" % cfg['min_password_length'])
-
-    args = [ "/usr/bin/addmember", "--stdin", username, name, program ]
-    addmember = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-    out, err = addmember.communicate(password)
-    status = addmember.wait()
-
-    if status:
-        raise ChildFailed("addmember", status, out+err)
-
-
-def get(userid):
-    """
-    Look up attributes of a member by userid.
-
-    Returns: a dictionary of attributes
-
-    Example: get('mspang') -> {
-                 'cn': [ 'Michael Spang' ],
-                 'program': [ 'Computer Science' ],
-                 ...
-             }
-    """
-
-    return ldap_connection.user_lookup(userid)
-
-
-def list_term(term):
-    """
-    Build a list of members in a term.
-
-    Parameters:
-        term - the term to match members against
-
-    Returns: a list of members
-
-    Example: list_term('f2006'): -> {
-                 'mspang': { 'cn': 'Michael Spang', ... },
-                 'ctdalek': { 'cn': 'Calum T. Dalek', ... },
-                 ...
-             }
-    """
-
-    return ldap_connection.member_search_term(term)
-
-
-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'): -> {
-                 'mspang': { 'cn': 'Michael Spang', ... },
-                 ...
-             ]
-    """
-
-    return ldap_connection.member_search_name(name)
-
-
-def list_group(group):
-    """
-    Build a list of members in a group.
-
-    Parameters:
-        group - the group to match members against
-
-    Returns: a list of member dictionaries
-
-    Example: list_name('syscom'): -> {
-                 'mspang': { 'cn': 'Michael Spang', ... },
-                 ...
-             ]
-    """
-
-    members = group_members(group)
-    ret = {}
-    if members:
-        for member in members:
-            info = get(member)
-            if info:
-                ret[member] = info
-    return ret
-
-
-def list_positions():
-    """
-    Build a list of positions
-
-    Returns: a list of positions and who holds them
-
-    Example: list_positions(): -> {
-                 'president': { 'mspang': { 'cn': 'Michael Spang', ... } } ],
-                 ...
-             ]
-    """
-
-    ceo_ldap = ldap_connection.ldap
-    user_base = ldap_connection.user_base
-
-    members = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE, '(position=*)')
-    positions = {}
-    for (_, member) in members:
-        for position in member['position']:
-            if not position in positions:
-                positions[position] = {}
-            positions[position][member['uid'][0]] = member
-    return positions
-
-def set_position(position, members):
-    """
-    Sets a position
-
-    Parameters:
-        position - the position to set
-        members - an array of members that hold the position
-
-    Example: set_position('president', ['dtbartle'])
-    """
-
-    ceo_ldap = ldap_connection.ldap
-    user_base = ldap_connection.user_base
-    escape = ldap_connection.escape
-
-    res = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE,
-        '(&(objectClass=member)(position=%s))' % escape(position))
-    old = set([ member['uid'][0] for (_, member) in res ])
-    new = set(members)
-    mods = {
-        'del': set(old) - set(new),
-        'add': set(new) - set(old),
-    }
-    if len(mods['del']) == 0 and len(mods['add']) == 0:
-        return
-
-    for action in ['del', 'add']:
-        for userid in mods[action]:
-            dn = 'uid=%s,%s' % (escape(userid), user_base)
-            entry1 = {'position' : [position]}
-            entry2 = {} #{'position' : []}
-            entry = ()
-            if action == 'del':
-                entry = (entry1, entry2)
-            elif action == 'add':
-                entry = (entry2, entry1)
-            mlist = ldap_connection.make_modlist(entry[0], entry[1])
-            ceo_ldap.modify_s(dn, mlist)
-
-
-def change_group_member(action, group, userid):
-
-    ceo_ldap = ldap_connection.ldap
-    user_base = ldap_connection.user_base
-    group_base = ldap_connection.group_base
-    escape = ldap_connection.escape
-
-    user_dn = 'uid=%s,%s' % (escape(userid), user_base)
-    group_dn = 'cn=%s,%s' % (escape(group), group_base)
-    entry1 = {'uniqueMember' : []}
-    entry2 = {'uniqueMember' : [user_dn]}
-    entry = []
-    if action == 'add' or action == 'insert':
-        entry = (entry1, entry2)
-    elif action == 'remove' or action == 'delete':
-        entry = (entry2, entry1)
-    else:
-        raise InvalidArgument("action", action, "invalid action")
-    mlist = ldap_connection.make_modlist(entry[0], entry[1])
-    ceo_ldap.modify_s(group_dn, mlist)
-
-
-
-### Clubs ###
-
-def create_club(username, name):
-    """
-    Creates a UNIX user account with options tailored to CSC-hosted clubs.
-    
-    Parameters:
-        username - the desired UNIX username
-        name     - the club name
-
-    Exceptions:
-        InvalidArgument - on bad account attributes provided
-
-    Returns: the uid number of the new account
-
-    See: create()
-    """
-
-    # check username format
-    if not username or not re.match(cfg['username_regex'], username):
-        raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
-    
-    args = [ "/usr/bin/addclub", username, name ]
-    addclub = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-    out, err = addclub.communicate()
-    status = addclub.wait()
-
-    if status:
-        raise ChildFailed("addclub", status, out+err)
-
-
-
-### Terms ###
-
-def register(userid, term_list):
-    """
-    Registers a member for one or more terms.
-
-    Parameters:
-        userid  - the member's username
-        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"])
-    """
-
-    ceo_ldap = ldap_connection.ldap
-    user_base = ldap_connection.user_base
-    escape = ldap_connection.escape
-    user_dn = 'uid=%s,%s' % (escape(userid), user_base)
-
-    if type(term_list) in (str, unicode):
-        term_list = [ term_list ]
-
-    ldap_member = ldap_connection.member_lookup(userid)
-    if ldap_member and 'term' not in ldap_member:
-        ldap_member['term'] = []
-
-    if not ldap_member:
-        raise NoSuchMember(userid)
-
-    new_member = ldap_member.copy()
-    new_member['term'] = new_member['term'][:]
-
-    for term in term_list:
-
-        # check term syntax
-        if not re.match('^[wsf][0-9]{4}$', term):
-            raise InvalidTerm(term)
-
-        # add the term to the entry
-        if not term in ldap_member['term']:
-            new_member['term'].append(term)
-
-    mlist = ldap_connection.make_modlist(ldap_member, new_member)
-    ceo_ldap.modify_s(user_dn, mlist)
-
-
-def registered(userid, term):
-    """
-    Determines whether a member is registered
-    for a term.
-
-    Parameters:
-        userid   - the member's username
-        term     - the term to check
-
-    Returns: whether the member is registered
-
-    Example: registered("mspang", "f2006") -> True
-    """
-
-    member = ldap_connection.member_lookup(userid)
-    return 'term' in member and term in member['term']
-
-
-def group_members(group):
-
-    """
-    Returns a list of group members
-    """
-
-    group = ldap_connection.group_lookup(group)
-    if group:
-        if 'uniqueMember' in group:
-            r = re.compile('^uid=([^,]*)')
-            return map(lambda x: r.match(x).group(1), group['uniqueMember'])
-        elif 'memberUid' in group:
-            return group['memberUid']
-        else:
-            return []
-    else:
-        return []
diff --git a/pylib/csc/adm/terms.py b/pylib/csc/adm/terms.py
deleted file mode 100644 (file)
index e70d190..0000000
+++ /dev/null
@@ -1,254 +0,0 @@
-"""
-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 validate(term):
-    """
-    Determines whether a term is well-formed.
-
-    Parameters:
-        term - the term string
-
-    Returns: whether the term is valid (boolean)
-
-    Example: validate("f2006") -> True
-    """
-
-    regex = '^[wsf][0-9]{4}$'
-    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 validate(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__':
-
-    from csc.common.test import *
-
-    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()
diff --git a/pylib/csc/apps/__init__.py b/pylib/csc/apps/__init__.py
deleted file mode 100644 (file)
index 162075b..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-"""
-Application-style User Interfaces
-
-This module contains large frontends with many functions
-and fancy graphical user interfaces.
-
-    legacy - aims to reproduce the curses UI of the previous CEO
-"""
diff --git a/pylib/csc/apps/urwid/__init__.py b/pylib/csc/apps/urwid/__init__.py
deleted file mode 100644 (file)
index 67bb77a..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-"""
-Urwid User Interface
-"""
diff --git a/pylib/csc/apps/urwid/groups.py b/pylib/csc/apps/urwid/groups.py
deleted file mode 100644 (file)
index 5bc19b3..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-import urwid, pwd, grp
-from csc.apps.urwid.widgets import *
-from csc.apps.urwid.window import *
-import csc.apps.urwid.search as search
-
-from csc.adm import members
-from csc.common.excep import InvalidArgument
-
-def menu_items(items):
-    return [ urwid.AttrWrap( ButtonText( cb, data, txt ), 'menu', 'selected') for (txt, cb, data) in items ]
-
-def change_group_member(data):
-    push_wizard("%s %s Member" % (data["action"], data["name"]), [
-        (ChangeMember, data),
-        EndPage,
-    ])
-
-def list_group_members(data):
-    mlist = members.list_group( data["group"] ).values()
-    search.member_list( mlist )
-
-def group_members(data):
-    add_data = data.copy()
-    add_data['action'] = 'Add'
-    remove_data = data.copy()
-    remove_data['action'] = 'Remove'
-    menu = [
-        ("Add %s member" % data["name"].lower(),
-            change_group_member, add_data),
-        ("Remove %s member" % data["name"].lower(),
-            change_group_member, remove_data),
-        ("List %s members" % data["name"].lower(), list_group_members, data),
-        ("Back", raise_back, None),
-    ]
-
-    listbox = urwid.ListBox( menu_items( menu ) )
-    push_window(listbox, "Manage %s" % data["name"])
-
-class IntroPage(WizardPanel):
-    def init_widgets(self):
-        self.widgets = [
-            urwid.Text( "Managing Club or Group" ),
-            urwid.Divider(),
-            urwid.Text( "Adding a member to a club will also grant them "
-                        "access to the club's files and allow them to "
-                        "become_club."
-                        "\n\n"
-                        "Do not manage office and syscom related groups using "
-                        "this interface. Instead use the \"Manage Office "
-                        "Staff\" and \"Manage Systems Committee\" entries "
-                        "from the main menu." )
-        ]
-    def focusable(self):
-        return False
-
-class InfoPage(WizardPanel):
-    def init_widgets(self):
-        self.group = WordEdit("Club or Group: ")
-        self.widgets = [
-            urwid.Text( "Club or Group Information"),
-            urwid.Divider(),
-            self.group,
-        ]
-    def check(self):
-        group = self.group.get_edit_text()
-        # TODO - check that group is valid
-        group_name = group # TODO
-        data = {
-            "name" : group,
-            "group" : group_name,
-            "groups" : [group],
-        }
-        group_members(data)
-
-class ChangeMember(WizardPanel):
-    def __init__(self, state, data):
-        state['data'] = data
-        WizardPanel.__init__(self, state)
-    def init_widgets(self):
-        self.userid = WordEdit("Username: ")
-
-        data = self.state['data']
-        self.widgets = [
-            urwid.Text( "%s %s Member" % (data['action'], data['name']) ),
-            urwid.Divider(),
-            self.userid,
-        ]
-    def check(self):
-        self.state['userid'] = self.userid.get_edit_text()
-        if self.state['userid']:
-            self.state['member'] = members.get(self.userid.get_edit_text())
-        if not self.state['member']:
-            set_status("Member not found")
-            self.focus_widget(self.userid)
-            return True
-        clear_status()
-
-class EndPage(WizardPanel):
-    def init_widgets(self):
-        self.headtext = urwid.Text("")
-        self.midtext = urwid.Text("")
-        self.widgets = [
-            self.headtext,
-            urwid.Divider(),
-            self.midtext,
-        ]
-    def focusable(self):
-        return False
-    def check(self):
-        pop_window()
-    def activate(self):
-        data = self.state['data']
-        action = data['action'].lower()
-        failed = []
-        for group in data['groups']:
-            try:
-                members.change_group_member(action, group, self.state['userid'])
-            except:
-                failed.append(group)
-        if len(failed) == 0:
-            self.headtext.set_text("%s succeeded" % data['action'])
-            self.midtext.set_text("Congratulations, the group modification "
-                "has succeeded.")
-        else:
-            self.headtext.set_text("%s partially succeeded" % data['action'])
-            self.midtext.set_text("Failed to %s member to %s for the "
-                "following groups: %s. This may indicate an attempt to add a "
-                "duplicate group member or to delete a non-present group "
-                "member." % (data['action'].lower(), data['name'],
-                ', '.join(failed)))
diff --git a/pylib/csc/apps/urwid/info.py b/pylib/csc/apps/urwid/info.py
deleted file mode 100644 (file)
index be15683..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-import urwid
-
-from csc.apps.urwid.widgets import *
-from csc.apps.urwid.window import *
-
-from csc.adm import members
-from csc.common.excep import InvalidArgument
-
-class InfoPage(WizardPanel):
-    def init_widgets(self):
-        self.userid = urwid.Text("")
-        self.name = urwid.Text("")
-        self.terms = urwid.Text("")
-        self.program = urwid.Text("")
-
-        self.widgets = [
-            urwid.Text( "Member Details" ),
-            urwid.Divider(),
-            self.name,
-            self.userid,
-            self.program,
-            urwid.Divider(),
-            self.terms,
-        ]
-    def focusable(self):
-        return False
-    def activate(self):
-        member  = self.state.get('member', {})
-        name    = member.get('cn', [''])[0]
-        userid  = self.state['userid']
-        program = member.get('program', [''])[0]
-        terms   = member.get('term', [])
-
-        self.name.set_text("Name: %s" % name)
-        self.userid.set_text("User: %s" % userid)
-        self.program.set_text("Program: %s" % program)
-        self.terms.set_text("Terms: %s" % ", ".join(terms))
-    def check(self):
-        pop_window()
diff --git a/pylib/csc/apps/urwid/ldapfilter.py b/pylib/csc/apps/urwid/ldapfilter.py
deleted file mode 100644 (file)
index 6aee341..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-import ldap
-
-class LdapFilter:
-    def __init__(self, widget):
-        self.widget = widget
-
-    def set_ldap_filter(self, ldap_uri, ldap_base, ldap_attr, ldap_map):
-        try:
-            self.ldap = ldap.initialize(ldap_uri)
-            self.ldap.simple_bind_s("", "")
-        except ldap.LDAPError:
-            return
-        self.base = ldap_base
-        self.attr = ldap_attr
-        self.map = ldap_map
-
-    def keypress(self, size, key):
-        if self.ldap != None:
-            if key == 'enter' or key == 'down' or key == 'up':
-                attr = self.escape(self.attr)
-                search = self.escape(self.widget.get_edit_text(self))
-                filter = '(%s=%s)' % (attr, search)
-                try:
-                    matches = self.ldap.search_s(self.base,
-                        ldap.SCOPE_SUBTREE, filter)
-                    if len(matches) > 0:
-                        (_, attrs) = matches[0]
-                        for (k, v) in self.map.items():
-                            if attrs.has_key(k) and len(attrs[k]) > 0:
-                                v.set_edit_text(attrs[k][0])
-                except ldap.LDAPError:
-                    pass
-        return self.widget.keypress(self, size, key)
-
-    def escape(self, value):
-        value = str(value)
-        value = value.replace('\\', '\\5c').replace('*', '\\2a')
-        value = value.replace('(', '\\28').replace(')', '\\29')
-        value = value.replace('\x00', '\\00')
-        return value
diff --git a/pylib/csc/apps/urwid/main.py b/pylib/csc/apps/urwid/main.py
deleted file mode 100644 (file)
index 625cc0f..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-import random, ldap, urwid.curses_display
-
-from csc.apps.urwid.widgets import *
-from csc.apps.urwid.window import *
-import csc.apps.urwid.newmember as newmember
-import csc.apps.urwid.renew as renew
-import csc.apps.urwid.info as info
-import csc.apps.urwid.search as search
-import csc.apps.urwid.positions as positions
-import csc.apps.urwid.groups as groups
-
-from csc.adm import members, terms
-from csc.common.excep import InvalidArgument
-
-ui = urwid.curses_display.Screen()
-
-ui.register_palette([
-    # name, foreground, background, mono
-    ('banner', 'light gray', 'default', None),
-    ('menu', 'light gray', 'default', 'bold'),
-    ('selected', 'black', 'light gray', 'bold'),
-])
-
-
-def program_name():
-    cwords = [ "CSC" ] * 20 + [ "Club" ] * 10 + [ "Campus" ] * 5 + \
-        [ "Communist", "Canadian", "Celestial", "Cryptographic", "Calum's",
-          "Canonical", "Capitalist", "Catastrophic", "Ceremonial", "Chaotic", "Civic",
-          "City", "County", "Caffeinated" ]
-    ewords = [ "Embellished", "Ergonomic", "Electric", "Eccentric", "European", "Economic",
-        "Evil", "Egotistical", "Elliptic", "Emasculating", "Embalming",
-        "Embryonic", "Emigrant", "Emissary's", "Emoting", "Employment", "Emulated",
-        "Enabling", "Enamoring", "Encapsulated", "Enchanted", "Encoded", "Encrypted",
-        "Encumbered", "Endemic", "Enhanced", "Enigmatic", "Enlightened", "Enormous",
-        "Enrollment", "Enshrouded", "Ephemeral", "Epidemic", "Episodic", "Epsilon",
-        "Equitable", "Equestrian", "Equilateral", "Erroneous", "Erratic",
-        "Espresso", "Essential", "Estate", "Esteemed", "Eternal", "Ethical", "Eucalyptus",
-        "Euphemistic", "Evangelist", "Evasive", "Everyday", "Evidence", "Eviction", "Evildoer's",
-        "Evolution", "Exacerbation", "Exalted", "Examiner's", "Excise", "Exciting", "Exclusion",
-        "Exec", "Executioner's", "Exile", "Existential", "Expedient", "Expert", "Expletive",
-        "Exploiter's", "Explosive", "Exponential", "Exposing", "Extortion", "Extraction",
-        "Extraneous", "Extravaganza", "Extreme", "Extraterrestrial", "Extremist", "Eerie" ]
-    owords = [ "Office" ] * 50 + [ "Outhouse", "Outpost" ]
-
-    cword = random.choice(cwords)
-    eword = random.choice(ewords)
-    oword = random.choice(owords)
-
-    return "%s %s %s" % (cword, eword, oword)
-
-office_data = {
-    "name" : "Office Staff",
-    "group" : "office",
-    "groups" : [ "office", "cdrom", "audio", "video", "www" ],
-}
-
-syscom_data = {
-    "name" : "Systems Committee",
-    "group" : "syscom",
-    "groups" : [ "office", "staff", "adm", "src" ],
-}
-
-def menu_items(items):
-    return [ urwid.AttrWrap( ButtonText( cb, data, txt ), 'menu', 'selected') for (txt, cb, data) in items ]
-
-def main_menu():
-    menu = [
-        ("New Member", new_member, None),
-        ("Renew Membership", renew_member, None),
-        ("Create Club Account", new_club, None),
-        ("Display Member", display_member, None),
-        ("Search", search_members, None),
-        ("Manage Club or Group Members", manage_group, None),
-        ("Manage Positions", manage_positions, None),
-        ("Manage Office Staff", groups.group_members, office_data),
-        ("Manage Systems Committee", groups.group_members, syscom_data),
-        ("Exit", raise_abort, None),
-    ]
-
-    listbox = urwid.ListBox( menu_items( menu ) )
-    return listbox
-
-def new_member(*args, **kwargs):
-    push_wizard("New Member", [
-        newmember.IntroPage,
-        newmember.InfoPage,
-        newmember.SignPage,
-        newmember.PassPage,
-        newmember.EndPage,
-    ])
-
-def new_club(*args, **kwargs):
-    push_wizard("New Club Account", [
-        newmember.ClubIntroPage,
-        newmember.ClubInfoPage,
-        (newmember.EndPage, "club"),
-    ], (60, 15))
-
-def manage_group(*args, **kwargs):
-    push_wizard("Manage Club or Group Members", [
-        groups.IntroPage,
-        groups.InfoPage,
-    ], (60, 15))
-
-def renew_member(*args, **kwargs):
-    push_wizard("Renew Membership", [
-        renew.IntroPage,
-        renew.UserPage,
-        renew.TermPage,
-        renew.PayPage,
-        renew.EndPage,
-    ])
-
-def display_member(data):
-    push_wizard("Display Member", [
-        renew.UserPage,
-        info.InfoPage,
-    ], (60, 15))
-
-def search_members(data):
-    menu = [
-        ("Members by term", search_term, None),
-        ("Members by name", search_name, None),
-        ("Members by group", search_group, None),
-        ("Back", raise_back, None),
-    ]
-
-    listbox = urwid.ListBox( menu_items( menu ) )
-    push_window(listbox, "Search")
-
-def search_name(data):
-    push_wizard("By Name", [ search.NamePage ])
-
-def search_term(data):
-    push_wizard("By Term", [ search.TermPage ])
-
-def search_group(data):
-    push_wizard("By Group", [ search.GroupPage ])
-
-def manage_positions(data):
-    push_wizard("Manage Positions", [
-        positions.IntroPage,
-        positions.InfoPage,
-        positions.EndPage,
-    ], (50, 15))
-
-def run():
-    members.connect()
-
-    push_window( main_menu(), program_name() )
-    event_loop( ui )
-
-def start():
-    try:
-        ui.run_wrapper( run )
-    except ldap.LOCAL_ERROR, e:
-        print e[0]['info']
-        print "Hint: You may need to run 'kinit'"
-
-if __name__ == '__main__':
-    import os
-    start()
diff --git a/pylib/csc/apps/urwid/newmember.py b/pylib/csc/apps/urwid/newmember.py
deleted file mode 100644 (file)
index 37f4c8f..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-import urwid
-from csc.apps.urwid.widgets import *
-from csc.apps.urwid.window import *
-
-from csc.adm import members, terms
-from csc.common.excep import InvalidArgument
-
-class IntroPage(WizardPanel):
-    def init_widgets(self):
-        self.widgets = [
-            urwid.Text( "Joining the Computer Science Club" ),
-            urwid.Divider(),
-            urwid.Text( "CSC membership is $2.00 for one term. Please ensure "
-                        "the fee is deposited into the safe before continuing." ),
-        ]
-    def focusable(self):
-        return False
-
-class ClubIntroPage(WizardPanel):
-    def init_widgets(self):
-        self.widgets = [
-            urwid.Text( "Club Services" ),
-            urwid.Divider(),
-            urwid.Text( "We provide other UW clubs accounts for email and "
-                        "web hosting, free of charge. Like members, clubs "
-                        "get web hosting at %s. We can also arrange for "
-                        "uwaterloo.ca subdomains; please instruct the club "
-                        "representative to contact the systems committee "
-                        "for more information."
-                        "\n\nNote: This is not complete. Authorizing members "
-                        "to access the club account still requires manual "
-                        "intervention."
-                        % "http://csclub.uwaterloo.ca/~clubid/"
-            )
-        ]
-    def focusable(self):
-        return False
-
-class InfoPage(WizardPanel):
-    def init_widgets(self):
-        self.userid = LdapFilterWordEdit("UWuserid: ")
-        self.name = SingleEdit("Full name: ")
-        self.program = SingleEdit("Program of Study: ")
-        self.userid.set_ldap_filter(
-            "ldap://uwldap.uwaterloo.ca/", "dc=uwaterloo,dc=ca",
-            "uid", {'cn':self.name, 'ou':self.program}
-        )
-        self.widgets = [
-            urwid.Text( "Member Information - Please Check ID" ),
-            urwid.Divider(),
-            self.userid,
-            self.name,
-            self.program,
-        ]
-    def check(self):
-        self.state['userid'] = self.userid.get_edit_text()
-        self.state['name'] = self.name.get_edit_text()
-        self.state['program'] = self.program.get_edit_text()
-
-        if len( self.state['userid'] ) < 3:
-            self.focus_widget( self.userid )
-            set_status("Username is too short")
-            return True
-        elif len( self.state['name'] ) < 4:
-            self.focus_widget( self.name )
-            set_status("Name is too short")
-            return True
-        clear_status()
-
-class ClubInfoPage(WizardPanel):
-    def init_widgets(self):
-        self.userid = WordEdit("Username: ")
-        self.name = SingleEdit("Club Name: ")
-        self.widgets = [
-            urwid.Text( "Club Information" ),
-            urwid.Divider(),
-            self.userid,
-            self.name,
-        ]
-    def check(self):
-        self.state['userid'] = self.userid.get_edit_text()
-        self.state['name'] = self.name.get_edit_text()
-
-        if len( self.state['userid'] ) < 3:
-            self.focus_widget( self.userid )
-            set_status("Username is too short")
-            return True
-        elif len( self.state['name'] ) < 4:
-            self.focus_widget( self.name )
-            set_status("Name is too short")
-            return True
-        clear_status()
-
-class SignPage(WizardPanel):
-    def init_widgets(self):
-        self.widgets = [
-            urwid.Text( "Machine Usage Policy" ),
-            urwid.Divider(),
-            urwid.Text( "Ensure the new member has signed the "
-                        "Machine Usage Policy. Accounts of users who have not "
-                        "signed will be suspended if discovered." ),
-        ]
-    def focusable(self):
-        return False
-
-class PassPage(WizardPanel):
-    def init_widgets(self):
-        self.password = PassEdit("Password: ")
-        self.pwcheck = PassEdit("Re-enter: ")
-        self.widgets = [
-            urwid.Text( "Member Password" ),
-            urwid.Divider(),
-            self.password,
-            self.pwcheck,
-        ]
-    def focus_widget(self, widget):
-        self.box.set_focus( self.widgets.index( widget ) )
-    def clear_password(self):
-        self.focus_widget( self.password )
-        self.password.set_edit_text("")
-        self.pwcheck.set_edit_text("")
-    def check(self):
-        self.state['password'] = self.password.get_edit_text()
-        pwcheck = self.pwcheck.get_edit_text()
-
-        if self.state['password'] != pwcheck:
-            self.clear_password()
-            set_status("Passwords do not match")
-            return True
-        elif len(self.state['password']) < 5:
-            self.clear_password()
-            set_status("Password is too short")
-            return True
-        clear_status()
-
-class EndPage(WizardPanel):
-    def __init__(self, state, utype='member'):
-        self.utype = utype
-        WizardPanel.__init__(self, state)
-    def init_widgets(self):
-        self.headtext = urwid.Text("")
-        self.midtext = urwid.Text("")
-        self.widgets = [
-            self.headtext,
-            urwid.Divider(),
-            self.midtext,
-        ]
-    def focusable(self):
-        return False
-    def check(self):
-        pop_window()
-    def activate(self):
-        problem = None
-        try:
-            if self.utype == 'member':
-                members.create_member( self.state['userid'], self.state['password'], self.state['name'], self.state['program'] )
-                members.register( self.state['userid'], terms.current() )
-            elif self.utype == 'club':
-                members.create_club( self.state['userid'], self.state['name'] )
-            else:
-                raise Exception("Internal Error")
-        except members.InvalidArgument, e:
-            problem = str(e)
-        except members.LDAPException, e:
-            problem = str(e)
-        except members.ChildFailed, e:
-            problem = str(e)
-
-        if problem:
-            self.headtext.set_text("Failures Occured Adding User")
-            self.midtext.set_text("The error was:\n%s\nThe account may be partially added "
-                "and you may or may not be able to log in. Please contact systems committee." % problem)
-            return
-        else:
-            self.headtext.set_text("User Added")
-            self.midtext.set_text("Congratulations, %s has been added "
-                "successfully. You should also rebuild the website in "
-                "order to update the memberlist."
-                % self.state['userid'])
diff --git a/pylib/csc/apps/urwid/positions.py b/pylib/csc/apps/urwid/positions.py
deleted file mode 100644 (file)
index dded23b..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-import urwid
-from csc.apps.urwid.widgets import *
-from csc.apps.urwid.window import *
-
-from csc.adm import members
-from csc.common.excep import InvalidArgument
-
-position_data = [
-    ('president',       'President'),
-    ('vice-president',  'Vice-president'),
-    ('treasurer',       'Treasurer'),
-    ('secretary',       'Secretary'),
-    ('sysadmin',        'System Administrator'),
-    ('librarian',       'Librarian'),
-    ('imapd',           'Imapd'),
-    ('webmaster',       'Web Master'),
-]
-
-class IntroPage(WizardPanel):
-    def init_widgets(self):
-        self.widgets = [
-            urwid.Text( "Managing Positions" ),
-            urwid.Divider(),
-            urwid.Text( "Enter a username for each position. If a position is "
-                        "held by multiple people, enter a comma-separated "
-                        "list of usernames. If a position is held by nobody "
-                        "leave the username blank." ),
-        ]
-    def focusable(self):
-        return False
-
-class InfoPage(WizardPanel):
-    def init_widgets(self):
-        self.widgets = [
-            urwid.Text( "Positions" ),
-            urwid.Divider(),
-        ]
-        positions = members.list_positions()
-        self.position_widgets = {}
-        for (position, text) in position_data:
-            widget = WordEdit("%s: " % text)
-            if position in positions:
-                widget.set_edit_text(','.join(positions[position].keys()))
-            else:
-                widget.set_edit_text('')
-            self.position_widgets[position] = widget
-            self.widgets.append(widget)
-
-    def parse(self, entry):
-        if len(entry) == 0:
-            return []
-        return entry.split(',')
-
-    def check(self):
-        self.state['positions'] = {}
-        for (position, widget) in self.position_widgets.iteritems():
-            self.state['positions'][position] = \
-                self.parse(widget.get_edit_text())
-            for p in self.state['positions'][position]:
-                if members.get(p) == None:
-                    self.focus_widget(widget)
-                    set_status( "Invalid username: '%s'" % p )
-                    return True
-        clear_status()
-
-class EndPage(WizardPanel):
-    def init_widgets(self):
-        old = members.list_positions()
-        self.headtext = urwid.Text("")
-        self.midtext = urwid.Text("")
-
-        self.widgets = [
-            self.headtext,
-            urwid.Divider(),
-            self.midtext,
-        ]
-    def focusable(self):
-        return False
-    def activate(self):
-        for (position, info) in self.state['positions'].iteritems():
-            members.set_position(position, info)
-        self.headtext.set_text("Positions Updated")
-        self.midtext.set_text("Congratulations, positions have been updated. "
-            "You should rebuild the website in order to update the Positions "
-            "page.")
-    def check(self):
-        pop_window()
diff --git a/pylib/csc/apps/urwid/renew.py b/pylib/csc/apps/urwid/renew.py
deleted file mode 100644 (file)
index 9217004..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-import urwid
-
-from csc.apps.urwid.widgets import *
-from csc.apps.urwid.window import *
-
-from csc.adm import members, terms
-
-class IntroPage(WizardPanel):
-    def init_widgets(self):
-        self.widgets = [
-            urwid.Text( "Renewing Membership" ),
-            urwid.Divider(),
-            urwid.Text( "CSC membership is $2.00 per term. You may pre-register "
-                        "for future terms if desired." )
-        ]
-    def focusable(self):
-        return False
-
-class UserPage(WizardPanel):
-    def init_widgets(self):
-        self.userid = WordEdit("Username: ")
-
-        self.widgets = [
-            urwid.Text( "Member Information" ),
-            urwid.Divider(),
-            self.userid,
-        ]
-    def check(self):
-        self.state['userid'] = self.userid.get_edit_text()
-        self.state['member'] = None
-        if self.state['userid']:
-            self.state['member'] = members.get(self.userid.get_edit_text())
-        if not self.state['member']:
-            set_status("Member not found")
-            self.focus_widget(self.userid)
-            return True
-
-class TermPage(WizardPanel):
-    def init_widgets(self):
-        self.start = SingleEdit("Start: ")
-        self.count = SingleIntEdit("Count: ")
-
-        self.widgets = [
-            urwid.Text( "Terms to Register" ),
-            urwid.Divider(),
-            self.start,
-            self.count,
-        ]
-    def activate(self):
-        if not self.start.get_edit_text():
-            old_terms = []
-            if 'term' in self.state['member']:
-                old_terms = self.state['member']['term']
-            self.start.set_edit_text( terms.next_unregistered( old_terms ) )
-            self.count.set_edit_text( "1" )
-    def check(self):
-        try:
-            self.state['terms'] = terms.interval( self.start.get_edit_text(), self.count.value() )
-        except Exception, e:
-            self.focus_widget( self.start )
-            set_status( "Invalid start term" )
-            return True
-        for term in self.state['terms']:
-            if members.registered( self.state['userid'], term):
-                self.focus_widget( self.start )
-                set_status( "Already registered for " + term )
-                return True
-        if len(self.state['terms']) == 0:
-            self.focus_widget(self.count)
-            set_status( "Registering for zero terms?" )
-            return True
-
-class PayPage(WizardPanel):
-    def init_widgets(self):
-        self.midtext = urwid.Text("")
-
-        self.widgets = [
-            urwid.Text("Membership Fee"),
-            urwid.Divider(),
-            self.midtext,
-        ]
-    def focusable(self):
-        return False
-    def activate(self):
-        regterms = self.state['terms']
-        plural = "term"
-        if len(self.state['terms']) > 1:
-            plural = "terms"
-        self.midtext.set_text("You are registering for %d %s, and owe the "
-                       "Computer Science Club $%d.00 in membership fees. "
-                       "Please deposit the money in the safe before "
-                       "continuing. " % ( len(regterms), plural, len(regterms * 2)))
-
-class EndPage(WizardPanel):
-    def init_widgets(self):
-        self.headtext = urwid.Text("")
-        self.midtext = urwid.Text("")
-
-        self.widgets = [
-            self.headtext,
-            urwid.Divider(),
-            self.midtext,
-        ]
-    def focusable(self):
-        return False
-    def activate(self):
-        try:
-            members.register( self.state['userid'], self.state['terms'] )
-            self.headtext.set_text("Registration Succeeded")
-            self.midtext.set_text("The member has been registered for the following "
-                             "terms: " + ", ".join(self.state['terms']) + ".")
-        except Exception, e:
-            self.headtext.set_text("Failed to Register")
-            self.midtext.set_text("You may refund any fees paid or retry."
-                             "The error was: '%s'" % e)
-    def check(self):
-        pop_window()
diff --git a/pylib/csc/apps/urwid/search.py b/pylib/csc/apps/urwid/search.py
deleted file mode 100644 (file)
index d4b23c6..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-import urwid
-
-from csc.apps.urwid.widgets import *
-from csc.apps.urwid.window import *
-
-from csc.adm import members, terms
-from csc.common.excep import InvalidArgument
-
-class TermPage(WizardPanel):
-    def init_widgets(self):
-        self.term = SingleEdit("Term: ")
-
-        self.widgets = [
-            urwid.Text( "Terms Members" ),
-            urwid.Divider(),
-            self.term,
-        ]
-    def check(self):
-        try:
-            self.state['term'] = self.term.get_edit_text()
-            terms.parse( self.state['term'] )
-        except:
-            self.focus_widget( self.term )
-            set_status( "Invalid term" )
-            return True
-        mlist = members.list_term( self.state['term'] ).values()
-        pop_window()
-        member_list( mlist )
-
-class NamePage(WizardPanel):
-    def init_widgets(self):
-        self.name = SingleEdit("Name: ")
-
-        self.widgets = [
-            urwid.Text( "Members by Name" ),
-            urwid.Divider(),
-            self.name,
-        ]
-    def check(self):
-        self.state['name'] = self.name.get_edit_text()
-        if not self.state['name']:
-            self.focus_widget( self.name )
-            set_status( "Invalid name" )
-            return True
-        mlist = members.list_name( self.state['name'] ).values()
-        pop_window()
-        member_list( mlist )
-
-class GroupPage(WizardPanel):
-    def init_widgets(self):
-        self.group = SingleEdit("Group: ")
-
-        self.widgets = [
-            urwid.Text( "Members by Group" ),
-            urwid.Divider(),
-            self.group,
-        ]
-    def check(self):
-        self.state['group'] = self.group.get_edit_text()
-        if not self.state['group']:
-            self.focus_widget( self.group )
-            set_status( "Invalid group" )
-            return True
-        mlist = members.list_group( self.state['group'] ).values()
-        pop_window()
-        member_list( mlist )
-
-def member_list(mlist):
-    mlist = list(mlist)
-    mlist.sort( lambda x, y: cmp(x['uid'], y['uid']) )
-    buf = ''
-    for member in mlist:
-        if 'uid' in member:
-            uid = member['uid'][0]
-        else:
-            uid = None
-        if 'program' in member:
-            program = member['program'][0]
-        else:
-            program = None
-        attrs = ( uid, member['cn'][0], program )
-        buf += "%10s %30s\n%41s\n\n" % attrs
-    set_status("Press escape to return to the menu")
-    push_window(urwid.ListBox([urwid.Text(buf)]))
-
-
diff --git a/pylib/csc/apps/urwid/widgets.py b/pylib/csc/apps/urwid/widgets.py
deleted file mode 100644 (file)
index 8912726..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-import urwid
-from csc.apps.urwid.ldapfilter import *
-from csc.apps.urwid.window import raise_back, push_window
-
-def push_wizard(name, pages, dimensions=(50, 10)):
-    state = {}
-    wiz = Wizard()
-    for page in pages:
-        if type(page) != tuple:
-            page = (page, )
-        wiz.add_panel( page[0](state, *page[1:]) )
-    push_window( urwid.Filler( urwid.Padding(
-        urwid.LineBox(wiz), 'center', dimensions[0]),
-        'middle', dimensions[1] ), name )
-
-class ButtonText(urwid.Text):
-    def __init__(self, callback, data, *args, **kwargs):
-        self.callback = callback
-        self.data = data
-        urwid.Text.__init__(self, *args, **kwargs)
-    def selectable(self):
-        return True
-    def keypress(self, size, key):
-        if key == 'enter' and self.callback:
-            self.callback(self.data)
-        else:
-            return key
-
-class SingleEdit(urwid.Edit):
-    def keypress(self, size, key):
-        if key == 'enter':
-            return urwid.Edit.keypress(self, size, 'down')
-        else:
-            return urwid.Edit.keypress(self, size, key)
-
-class SingleIntEdit(urwid.IntEdit):
-    def keypress(self, size, key):
-        if key == 'enter':
-            return urwid.Edit.keypress(self, size, 'down')
-        else:
-            return urwid.Edit.keypress(self, size, key)
-
-class WordEdit(SingleEdit):
-    def valid_char(self, ch):
-        return urwid.Edit.valid_char(self, ch) and ch != ' '
-
-class LdapFilterWordEdit(LdapFilter, WordEdit):
-    def __init__(self, *args):
-        LdapFilter.__init__(self, WordEdit)
-        WordEdit.__init__(self, *args)
-
-class PassEdit(SingleEdit):
-    def get_text(self):
-        text = urwid.Edit.get_text(self)
-        return (self.caption + " " * len(self.get_edit_text()), text[1])
-
-class Wizard(urwid.WidgetWrap):
-    def __init__(self):
-        self.selected = None
-        self.panels = []
-
-        self.panelwrap = urwid.WidgetWrap( urwid.SolidFill() )
-        self.back = urwid.Button("Back", self.back)
-        self.next = urwid.Button("Next", self.next)
-        self.buttons = urwid.Columns( [ self.back, self.next ], dividechars=3, focus_column=1 )
-        pad = urwid.Padding( self.buttons, ('fixed right', 2), 19 )
-        self.pile = urwid.Pile( [self.panelwrap, ('flow', pad)], 0 )
-        urwid.WidgetWrap.__init__(self, self.pile)
-
-    def add_panel(self, panel):
-        self.panels.append( panel )
-        if len(self.panels) == 1:
-            self.select(0)
-
-    def select(self, panelno, set_focus=True):
-        if 0 <= panelno < len(self.panels):
-            self.selected = panelno
-            self.panelwrap.set_w( self.panels[panelno] )
-            self.panels[panelno].activate()
-
-            if set_focus:
-                if self.panels[panelno].focusable():
-                    self.pile.set_focus( 0 )
-                else:
-                    self.pile.set_focus( 1 )
-
-    def next(self, *args, **kwargs):
-        if self.panels[self.selected].check():
-            self.select( self.selected )
-            return
-        self.select(self.selected + 1)
-
-    def back(self, *args, **kwargs):
-        if self.selected == 0:
-            raise_back()
-        self.select(self.selected - 1, False)
-
-class WizardPanel(urwid.WidgetWrap):
-    def __init__(self, state):
-        self.state = state
-        self.init_widgets()
-        self.box = urwid.ListBox( urwid.SimpleListWalker( self.widgets ) )
-        urwid.WidgetWrap.__init__( self, self.box )
-    def init_widgets(self):
-        self.widgets = []
-    def focus_widget(self, widget):
-        self.box.set_focus( self.widgets.index( widget ) )
-    def focusable(self):
-        return True
-    def check(self):
-        return
-    def activate(self):
-        return
diff --git a/pylib/csc/apps/urwid/window.py b/pylib/csc/apps/urwid/window.py
deleted file mode 100644 (file)
index 836b11f..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-import urwid
-
-window_stack = []
-window_names = []
-
-header = urwid.Text( "" )
-footer = urwid.Text( "" )
-top = urwid.Frame( urwid.SolidFill(), header, footer )
-
-def push_window( frame, name=None ):
-    window_stack.append( frame )
-    window_names.append( name )
-    update_top()
-
-def pop_window():
-    if len(window_stack) == 1:
-       return False
-    window_stack.pop()
-    window_names.pop()
-    update_top()
-    clear_status()
-    return True
-
-def update_top():
-    names = [ n for n in window_names if n ]
-    header.set_text(" - ".join( names ) + "\n")
-    top.set_body( window_stack[-1] )
-
-def set_status(message):
-    footer.set_text(message)
-
-def clear_status():
-    footer.set_text("")
-
-class Abort(Exception):
-    pass
-
-class Back(Exception):
-    pass
-
-def raise_abort(*args, **kwargs):
-    raise Abort()
-
-def raise_back(*args, **kwarg):
-    raise Back()
-
-def event_loop(ui):
-    while True:
-        try:
-           cols, rows = ui.get_cols_rows()
-           canvas = top.render( (cols, rows), focus=True )
-           ui.draw_screen( (cols, rows), canvas )
-
-           keys = ui.get_input()
-           for k in keys:
-              if k == "esc":
-                 if not pop_window():
-                     break
-              elif k == "window resize":
-                 (cols, rows) = ui.get_cols_rows()
-              else:
-                 top.keypress( (cols, rows), k )
-        except Back:
-            pop_window()
-        except (Abort, KeyboardInterrupt):
-            return
diff --git a/pylib/csc/backends/__init__.py b/pylib/csc/backends/__init__.py
deleted file mode 100644 (file)
index f747f24..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Backend Modules
-
-This module contains backend interfaces and related modules.
-
-    ldapi - LDAP interface for UNIX account attribute administration
-"""
diff --git a/pylib/csc/backends/ldapi.py b/pylib/csc/backends/ldapi.py
deleted file mode 100644 (file)
index 23e2ee0..0000000
+++ /dev/null
@@ -1,372 +0,0 @@
-"""
-LDAP Backend Interface
-
-This module is intended to be a thin wrapper around LDAP operations.
-Methods on the connection object correspond in a straightforward way
-to LDAP queries and updates.
-
-A LDAP entry is the most important component of a CSC UNIX account.
-The entry contains the username, user id number, real name, shell,
-and other important information. All non-local UNIX accounts must
-have an LDAP entry, even if the account does not log in directly.
-
-This module makes use of python-ldap, a Python module with bindings
-to libldap, OpenLDAP's native C client library.
-"""
-import ldap.modlist
-from subprocess import Popen, PIPE
-
-
-class LDAPException(Exception):
-    """Exception class for LDAP-related errors."""
-
-
-class LDAPConnection(object):
-    """
-    Connection to the LDAP directory. All directory
-    queries and updates are made via this class.
-
-    Exceptions: (all methods)
-        LDAPException - on directory query failure
-
-    Example:
-         connection = LDAPConnection()
-         connection.connect(...)
-
-         # make queries and updates, e.g.
-         connection.user_delete('mspang')
-
-         connection.disconnect()
-    """
-
-    def __init__(self):
-        self.ldap = None
-
-
-    def connect_anon(self, uri, user_base, group_base):
-        """
-        Establish a connection to the LDAP Server.
-
-        Parameters:
-            uri        - connection string (e.g. ldap://foo.com, ldaps://bar.com)
-            user_base  - base of the users subtree
-            group_base - baes of the group subtree
-
-        Example: connect('ldaps:///', 'cn=ceo,dc=csclub,dc=uwaterloo,dc=ca',
-                     'secret', 'ou=People,dc=csclub,dc=uwaterloo,dc=ca',
-                     'ou=Group,dc=csclub,dc=uwaterloo,dc=ca')
-
-        """
-
-        # open the connection
-        self.ldap = ldap.initialize(uri)
-
-        # authenticate
-        self.ldap.simple_bind_s('', '')
-
-        self.user_base = user_base
-        self.group_base = group_base
-
-
-    def connect_sasl(self, uri, mech, realm, user_base, group_base):
-
-        # open the connection
-        self.ldap = ldap.initialize(uri)
-
-        # authenticate
-        sasl = Sasl(mech, realm)
-        self.ldap.sasl_interactive_bind_s('', sasl)
-
-        self.user_base = user_base
-        self.group_base = group_base
-
-
-    def disconnect(self):
-        """Close the connection to the LDAP server."""
-        
-        if self.ldap:
-
-            # close connection
-            try:
-                self.ldap.unbind_s()
-                self.ldap = None
-            except ldap.LDAPError, e:
-                raise LDAPException("unable to disconnect: %s" % e)
-
-
-    def connected(self):
-        """Determine whether the connection has been established."""
-
-        return self.ldap is not None
-
-
-
-    ### Helper Methods ###
-
-    def lookup(self, dn, objectClass=None):
-        """
-        Helper method to retrieve the attributes of an entry.
-
-        Parameters:
-            dn - the distinguished name of the directory entry
-
-        Returns: a dictionary of attributes of the matched dn, or
-                 None of the dn does not exist in the directory
-        """
-
-        if not self.connected(): raise LDAPException("Not connected!")
-
-        # search for the specified dn
-        try:
-            if objectClass:
-                search_filter = '(objectClass=%s)' % self.escape(objectClass)
-                matches = self.ldap.search_s(dn, ldap.SCOPE_BASE, search_filter)
-            else:
-                matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
-        except ldap.NO_SUCH_OBJECT:
-            return None
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to lookup dn %s: %s" % (dn, e))
-            
-        # this should never happen due to the nature of DNs
-        if len(matches) > 1:
-            raise LDAPException("duplicate dn in ldap: " + dn)
-
-        # dn was found, but didn't match the objectClass filter
-        elif len(matches) < 1:
-            return None
-
-        # return the attributes of the single successful match
-        match = matches[0]
-        match_dn, match_attributes = match
-        return match_attributes
-
-
-
-    ### User-related Methods ###
-
-    def user_lookup(self, uid, objectClass=None):
-        """
-        Retrieve the attributes of a user.
-
-        Parameters:
-            uid - the uid to look up
-
-        Returns: attributes of user with uid
-        """
-
-        dn = 'uid=' + uid + ',' + self.user_base
-        return self.lookup(dn, objectClass)
-
-
-    def user_search(self, search_filter, params):
-        """
-        Search for users with a filter.
-
-        Parameters:
-            search_filter - LDAP filter string to match users against
-
-        Returns: a dictionary mapping uids to attributes
-        """
-
-        if not self.connected(): raise LDAPException("Not connected!")
-
-        search_filter = search_filter % tuple(self.escape(x) for x in params)
-
-        # search for entries that match the filter
-        try:
-            matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, search_filter)
-        except ldap.LDAPError, e:
-            raise LDAPException("user search failed: %s" % e)
-
-        results = {}
-        for match in matches:
-            dn, attrs = match
-            uid = attrs['uid'][0]
-            results[uid] = attrs
-
-        return results
-
-
-    def user_modify(self, uid, attrs):
-        """
-        Update user attributes in the directory.
-
-        Parameters:
-            uid   - username of the user to modify
-            attrs - dictionary as returned by user_lookup() with changes to make.
-                    omitted attributes are DELETED.
-
-        Example: user = user_lookup('mspang')
-                 user['uidNumber'] = [ '0' ]
-                 connection.user_modify('mspang', user)
-        """
-
-        # distinguished name of the entry to modify
-        dn = 'uid=' + uid + ',' + self.user_base
-
-        # retrieve current state of user
-        old_user = self.user_lookup(uid)
-
-        try:
-
-            # build list of modifications to make
-            changes = ldap.modlist.modifyModlist(old_user, attrs)
-
-            # apply changes
-            self.ldap.modify_s(dn, changes)
-
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to modify: %s" % e)
-
-
-
-    ### Group-related Methods ###
-
-    def group_lookup(self, cn):
-        """
-        Retrieves the attributes of a group.
-
-        Parameters:
-            cn - the UNIX group name to lookup
-
-        Returns: attributes of the group's LDAP entry
-
-        Example: connection.group_lookup('office') -> {
-                     'cn': 'office',
-                     'gidNumber', '1001',
-                     ...
-                 }
-        """
-
-        dn = 'cn=' + cn + ',' + self.group_base
-        return self.lookup(dn, 'posixGroup')
-
-
-    ### Member-related Methods ###
-
-    def member_lookup(self, uid):
-        """
-        Retrieve the attributes of a member. This method will only return
-        results that have the objectClass 'member'.
-
-        Parameters:
-            uid - the username to look up
-
-        Returns: attributes of member with uid
-
-        Example: connection.member_lookup('mspang') ->
-                     { 'uid': 'mspang', 'uidNumber': 21292 ...}
-        """
-
-        if not self.connected(): raise LDAPException("Not connected!")
-
-        dn = 'uid=' + uid + ',' + self.user_base
-        return self.lookup(dn, 'member')
-
-
-    def member_search_name(self, name):
-        """
-        Retrieves a list of members with the specified name (fuzzy).
-
-        Returns: a dictionary mapping uids to attributes
-        """
-
-        search_filter = '(&(objectClass=member)(cn~=%s))'
-        return self.user_search(search_filter, [ name ] )
-
-
-    def member_search_term(self, term):
-        """
-        Retrieves a list of members who were registered in a certain term.
-
-        Returns: a dictionary mapping uids to attributes
-        """
-
-        search_filter = '(&(objectClass=member)(term=%s))'
-        return self.user_search(search_filter, [ term ])
-
-
-    def member_search_program(self, program):
-        """
-        Retrieves a list of members in a certain program (fuzzy).
-
-        Returns: a dictionary mapping uids to attributes
-        """
-
-        search_filter = '(&(objectClass=member)(program~=%s))'
-        return self.user_search(search_filter, [ program ])
-
-
-    def member_add(self, uid, cn, program=None, description=None):
-        """
-        Adds a member to the directory.
-
-        Parameters:
-            uid           - the UNIX username for the member
-            cn            - the real name of the member
-            program       - the member's program of study
-            description   - a description for the entry
-        """
-
-        dn = 'uid=' + uid + ',' + self.user_base
-        attrs = {
-            'objectClass': [ 'top', 'account', 'member' ],
-            'uid': [ uid ],
-            'cn': [ cn ],
-        }
-
-        if program:
-            attrs['program'] = [ program ]
-        if description:
-            attrs['description'] = [ description ]
-
-        try:
-            modlist = ldap.modlist.addModlist(attrs)
-            self.ldap.add_s(dn, modlist)
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to add: %s" % e)
-
-
-
-    ### Miscellaneous Methods ###
-
-    def escape(self, value):
-        """
-        Escapes special characters in a value so that it may be safely inserted
-        into an LDAP search filter.
-        """
-
-        value = str(value)
-        value = value.replace('\\', '\\5c').replace('*', '\\2a')
-        value = value.replace('(', '\\28').replace(')', '\\29')
-        value = value.replace('\x00', '\\00')
-        return value
-
-
-    def make_modlist(self, old, new):
-        keys = set(old.keys()).union(set(new))
-        mlist = []
-        for key in keys:
-            if key in old and not key in new:
-                mlist.append((ldap.MOD_DELETE, key, list(set(old[key]))))
-            elif key in new and not key in old:
-                mlist.append((ldap.MOD_ADD, key, list(set(new[key]))))
-            else:
-                to_add = list(set(new[key]) - set(old[key]))
-                if len(to_add) > 0:
-                    mlist.append((ldap.MOD_ADD, key, to_add))
-                to_del = list(set(old[key]) - set(new[key]))
-                if len(to_del) > 0:
-                    mlist.append((ldap.MOD_DELETE, key, to_del))
-        return mlist
-
-
-class Sasl:
-
-    def __init__(self, mech, realm):
-        self.mech = mech
-        self.realm = realm
-
-    def callback(self, id, challenge, prompt, defresult):
-        return ''
diff --git a/pylib/csc/common/__init__.py b/pylib/csc/common/__init__.py
deleted file mode 100644 (file)
index 7876ba0..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Generally Useful Common Modules
-
-    conf  - simple configuration file reader
-    excep - generally useful exceptions
-    test  - test suite utility routines
-"""
diff --git a/pylib/csc/common/conf.py b/pylib/csc/common/conf.py
deleted file mode 100644 (file)
index f9e6972..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-"""
-Configuration Utility Module
-
-This module contains functions to load and verify very simple configuration
-files. Python supports ".ini" files, which suck, so this module is used
-instead.
-
-Example Configuration File:
-
-    include /path/to/other.cf
-
-    # these values are the same:
-    name_protected = "Michael Spang"
-    name_unprotected = Michael Spang
-    
-    # these values are not the same:
-    yes_no = " yes"
-    no_yes =  yes
-    
-    # this value is an integer
-    arbitrary_number=2
-    
-    # this value is not an integer
-    arbitrary_string="2"
-    
-    # this is a key with no value
-    csclub
-
-    # this key contains whitespace
-    white space = sure, why not
-
-    # these two lines are treated as one
-    long line = first line \\
-                second line
-
-Resultant Dictionary:
-
-    {
-      'name_protected': 'Michael Spang',
-      'name_unprotected:' 'Michael Spang',
-      'yes_no': ' yes',
-      'no_yes': 'yes',
-      'arbirary_number': 2,
-      'arbitrary_string': '2',
-      'csclub': None,
-      'white space': 'sure, why not'
-      'long line': 'first line \\n               second line' 
-      
-      ... (data from other.cf) ...
-    }
-
-"""
-from curses.ascii import isspace
-
-
-class ConfigurationException(Exception):
-    """Exception class for incomplete and incorrect configurations."""
-    
-
-def read(filename, included=None):
-    """
-    Function to read a configuration file into a dictionary.
-    
-    Parmaeters:
-        filename - the file to read
-        included - files previously read (internal)
-    
-    Exceptions:
-        IOError - when the configuration file cannot be read
-    """
-
-    if not included:
-        included = []
-    if filename in included:
-        return {}
-    included.append(filename)
-
-    conffile = open(filename)
-    
-    options = {}
-
-    while True:
-
-        line = conffile.readline()
-        if line == '':
-            break
-
-        # remove comments
-        if '#' in line:
-            line = line[:line.find('#')]
-
-        # combine lines when the newline is escaped with \
-        while len(line) > 1 and line[-2] == '\\':
-            line = line[:-2] + line[-1]
-            next = conffile.readline()
-            line += next
-            if next == '':
-                break
-
-        line = line.strip()
-
-        # process include statements
-        if line.find("include") == 0 and isspace(line[7]):
-
-            filename = line[8:].strip()
-            options.update(read(filename, included))
-            continue
-
-        # split 'key = value' into key and value and strip results
-        pair = map(str.strip, line.split('=', 1))
-        
-        # found key and value
-        if len(pair) == 2:
-            key, val = pair
-
-            # found quoted string?
-            if val and val[0] == val[-1] == '"':
-                val = val[1:-1]
-
-            # unquoted, found num?
-            elif val:
-                try:
-                    if "." in val:
-                        val = float(val)
-                    elif val[0] == '0':
-                        val = int(val, 8)
-                    else:
-                        val = int(val)
-                except ValueError:
-                    pass
-            
-            # save key and value
-            options[key] = val
-
-        # found only key, value = None
-        elif len(pair[0]) > 1:
-            key = pair[0]
-            options[key] = None
-
-    return options
-
-
-def check_string_fields(filename, field_list, cfg):
-    """Function to verify thatfields are strings."""
-
-    for field in field_list:
-        if field not in cfg or type(cfg[field]) is not str:
-            raise ConfigurationException('expected string value for option "%s" in "%s"' % (field, filename))
-
-def check_integer_fields(filename, field_list, cfg):
-    """Function to verify that fields are integers."""
-
-    for field in field_list:
-        if field not in cfg or type(cfg[field]) not in (int, long):
-            raise ConfigurationException('expected numeric value for option "%s" in "%s"' % (field, filename))
-
-def check_float_fields(filename, field_list, cfg):
-    """Function to verify that fields are integers or floats."""
-
-    for field in field_list:
-        if field not in cfg or type(cfg[field]) not in (float, long, int):
-            raise ConfigurationException('expected float value for option "%s" in "%s"' % (field, filename))
diff --git a/pylib/csc/common/excep.py b/pylib/csc/common/excep.py
deleted file mode 100644 (file)
index 6359302..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-Exceptions Module
-
-This module provides some simple but generally useful exception classes.
-"""
-
-class InvalidArgument(Exception):
-    """Exception class for bad argument values."""
-    def __init__(self, argname, argval, explanation):
-        self.argname, self.argval, self.explanation = argname, argval, explanation
-    def __str__(self):
-        return 'Bad argument value "%s" for %s: %s' % (self.argval, self.argname, self.explanation)
diff --git a/pylib/csc/common/test.py b/pylib/csc/common/test.py
deleted file mode 100644 (file)
index a60b2ed..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-"""
-Common Test Routines
-
-This module contains helpful functions called by each module's test suite.
-"""
-from types import FunctionType, MethodType, ClassType, TypeType
-
-
-class TestException(Exception):
-    """Exception class for test failures."""
-
-
-def test(subject):
-    """Print a test message."""
-    if type(subject) in (MethodType, FunctionType, ClassType, TypeType):
-        print "testing %s()..." % subject.__name__,
-    else:
-        print "testing %s..." % subject,
-
-
-def success():
-    """Print a success message."""
-    print "pass."
-
-
-def assert_equal(expected, actual):
-    if expected != actual:
-        message = "Expected (%s)\nWas      (%s)" % (repr(expected), repr(actual))
-        fail(message)
-
-
-def fail(message):
-    print "failed!"
-    raise TestException("Test failed:\n%s" % message)
-
-
-def negative(call, args, excep, message):
-    try:
-        call(*args)
-        fail(message)
-    except excep:
-        pass
index e4729fd..ede1215 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -5,8 +5,7 @@ from distutils.core import setup
 setup(
     name='ceo',
     description='CSC Electronic Office',
-    packages=[ 'csc', 'csc.common', 'csc.adm', 'csc.backends', 'csc.apps', 'csc.apps.urwid' ],
-    package_dir = {'': 'pylib'},
+    packages=[ 'ceo', 'ceo.urwid' ],
     scripts=['bin/ceo', 'bin/ceoquery', 'bin/csc-chfn', 'bin/csc-chsh'],
 )