diff --git a/misc/csc.schema b/misc/csc.schema index 21f9d83..13c24e2 100644 --- a/misc/csc.schema +++ b/misc/csc.schema @@ -12,11 +12,20 @@ attributetype ( 1.3.6.1.4.1.27934.1.1.3 NAME 'studentid' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{8} SINGLE-VALUE ) +attributetype ( 1.3.6.1.4.1.27934.1.1.4 NAME 'position' + EQUALITY caseIgnoreIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} ) + objectclass ( 1.3.6.1.4.1.27934.1.2.1 NAME 'member' SUP top AUXILIARY MUST ( cn $ uid ) - MAY ( studentid $ program $ term $ description ) ) + MAY ( studentid $ program $ term $ description $ position ) ) objectclass ( 1.3.6.1.4.1.27934.1.2.2 NAME 'club' SUP top AUXILIARY MUST ( cn $ uid ) ) + +objectclass ( 1.3.6.1.4.1.27934.1.2.3 NAME 'group' + SUP top STRUCTURAL + MUST ( cn ) + MAY ( uniqueMember ) ) diff --git a/pylib/csc/adm/members.py b/pylib/csc/adm/members.py index da3a958..4da25f4 100644 --- a/pylib/csc/adm/members.py +++ b/pylib/csc/adm/members.py @@ -9,7 +9,7 @@ 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 +import re, ldap from csc.adm import terms from csc.backends import ldapi from csc.common import conf @@ -220,6 +220,72 @@ def list_group(group): return {} +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 + escape = ldap_connection.escape + + if not ldap_connection.connected(): ldap_connection.connect() + + 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 type in ['del', 'add']: + for userid in mods[type]: + dn = 'uid=%s,%s' % (escape(userid), user_base) + entry1 = {'position' : [position]} + entry2 = {} #{'position' : []} + entry = () + if type == 'del': + entry = (entry1, entry2) + elif type == 'add': + entry = (entry2, entry1) + mlist = ldap_connection.make_modlist(entry[0], entry[1]) + ceo_ldap.modify_s(dn, mlist) + def delete(userid): """ Erase all records of a member. @@ -248,6 +314,27 @@ def delete(userid): return member +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) + ### Term Table ### diff --git a/pylib/csc/apps/urwid/groups.py b/pylib/csc/apps/urwid/groups.py new file mode 100644 index 0000000..80ee9df --- /dev/null +++ b/pylib/csc/apps/urwid/groups.py @@ -0,0 +1,64 @@ +import urwid +from csc.apps.urwid.widgets import * +from csc.apps.urwid.window import * + +from csc.adm import accounts, members +from csc.common.excep import InvalidArgument + +class 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['type'], data['name']) ), + urwid.Divider(), + self.userid, + ] + def check(self): + self.state['userid'] = self.userid.get_edit_text() + if self.state['userid']: + if not members.connected(): members.connect() + self.state['member'] = members.get(self.userid.get_edit_text()) + if not self.state['member']: + set_status("Member not found") + self.focus_widget(self.userid) + return True + 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'] + type = data['type'].lower() + failed = [] + for group in data['groups']: + try: + members.change_group_member(type, group, self.state['userid']) + except: + failed.append(group) + if len(failed) == 0: + self.headtext.set_text("%s succeeded" % data['type']) + self.midtext.set_text("Congratulations, the group modification " + "has succeeded.") + else: + self.headtext.set_text("%s partially succeeded" % data['type']) + 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['type'].lower(), data['name'], + ', '.join(failed))) diff --git a/pylib/csc/apps/urwid/ldapfilter.py b/pylib/csc/apps/urwid/ldapfilter.py index 46d425e..6aee341 100644 --- a/pylib/csc/apps/urwid/ldapfilter.py +++ b/pylib/csc/apps/urwid/ldapfilter.py @@ -21,7 +21,8 @@ class LdapFilter: 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) + 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(): diff --git a/pylib/csc/apps/urwid/main.py b/pylib/csc/apps/urwid/main.py index ee9bf7c..8ea09aa 100644 --- a/pylib/csc/apps/urwid/main.py +++ b/pylib/csc/apps/urwid/main.py @@ -7,6 +7,8 @@ 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 accounts, members, terms from csc.common.excep import InvalidArgument @@ -49,12 +51,14 @@ def program_name(): office_data = { "name" : "Office Staff", - "group" : "office" + "group" : "office", + "groups" : [ "office", "cdrom", "audio", "video", "www" ], } syscom_data = { "name" : "Systems Committee", - "group" : "syscom" + "group" : "syscom", + "groups" : [ "office", "staff", "adm", "src" ], } def menu_items(items): @@ -67,6 +71,7 @@ def main_menu(): ("Create Club Account", new_club, None), ("Display Member", display_member, None), ("Search", search_members, None), + ("Manage Positions", manage_positions, None), ("Manage Office Staff", group_members, office_data), ("Manage Systems Committee", group_members, syscom_data), ("Exit", raise_abort, None), @@ -100,7 +105,7 @@ def new_club(*args, **kwargs): newmember.ClubIntroPage, newmember.ClubInfoPage, (newmember.EndPage, "club"), - ], (60,15)) + ], (60, 15)) def renew_member(*args, **kwargs): push_wizard("Renew Membership", [ @@ -137,10 +142,23 @@ def search_term(data): 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 group_members(data): + add_data = data.copy() + add_data['type'] = 'Add' + remove_data = data.copy() + remove_data['type'] = 'Remove' menu = [ - ("Add %s member" % data["name"].lower(), add_group_member, data), - ("Remove %s member" % data["name"].lower(), remove_group_member, data), + ("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), ] @@ -148,11 +166,11 @@ def group_members(data): listbox = urwid.ListBox( menu_items( menu ) ) push_window(listbox, "Manage %s" % data["name"]) -def add_group_member(data): - pass - -def remove_group_member(data): - pass +def change_group_member(data): + push_wizard("%s %s Member" % (data["type"], data["name"]), [ + (groups.ChangeMember, data), + groups.EndPage, + ]) def list_group_members(data): if not members.connected(): members.connect() diff --git a/pylib/csc/apps/urwid/newmember.py b/pylib/csc/apps/urwid/newmember.py index 48f3210..c0f3ad1 100644 --- a/pylib/csc/apps/urwid/newmember.py +++ b/pylib/csc/apps/urwid/newmember.py @@ -58,7 +58,7 @@ class InfoPage(WizardPanel): self.state['name'] = self.name.get_edit_text() self.state['program'] = self.program.get_edit_text() - if len( self.state['userid'] ) < 4: + if len( self.state['userid'] ) < 3: self.focus_widget( self.userid ) set_status("Username is too short") return True @@ -188,5 +188,7 @@ class EndPage(WizardPanel): else: self.headtext.set_text("User Added") self.midtext.set_text("Congratulations, %s has been added " - "successfully. Please run 'addhomedir %s'." + "successfully. Please run 'addhomedir %s'. " + "You should also rebuild the website in order to update the " + "memberlist." % (self.state['userid'], self.state['userid'])) diff --git a/pylib/csc/apps/urwid/positions.py b/pylib/csc/apps/urwid/positions.py new file mode 100644 index 0000000..489be5a --- /dev/null +++ b/pylib/csc/apps/urwid/positions.py @@ -0,0 +1,83 @@ +import urwid +from csc.apps.urwid.widgets import * +from csc.apps.urwid.window import * + +from csc.adm import accounts, members +from csc.common.excep import InvalidArgument + +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): + if not members.connected(): members.connect() + 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()) + 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/backends/ldapi.py b/pylib/csc/backends/ldapi.py index 002cfbe..3da2314 100644 --- a/pylib/csc/backends/ldapi.py +++ b/pylib/csc/backends/ldapi.py @@ -641,6 +641,23 @@ class LDAPConnection(object): return gids + 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 + ### Tests ###