Complete group and position management
This commit is contained in:
parent
97ac7fb7bd
commit
95019e376a
|
@ -12,11 +12,20 @@ attributetype ( 1.3.6.1.4.1.27934.1.1.3 NAME 'studentid'
|
||||||
EQUALITY caseIgnoreIA5Match
|
EQUALITY caseIgnoreIA5Match
|
||||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{8} SINGLE-VALUE )
|
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'
|
objectclass ( 1.3.6.1.4.1.27934.1.2.1 NAME 'member'
|
||||||
SUP top AUXILIARY
|
SUP top AUXILIARY
|
||||||
MUST ( cn $ uid )
|
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'
|
objectclass ( 1.3.6.1.4.1.27934.1.2.2 NAME 'club'
|
||||||
SUP top AUXILIARY
|
SUP top AUXILIARY
|
||||||
MUST ( cn $ uid ) )
|
MUST ( cn $ uid ) )
|
||||||
|
|
||||||
|
objectclass ( 1.3.6.1.4.1.27934.1.2.3 NAME 'group'
|
||||||
|
SUP top STRUCTURAL
|
||||||
|
MUST ( cn )
|
||||||
|
MAY ( uniqueMember ) )
|
||||||
|
|
|
@ -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
|
Future changes to the members database that need to be atomic
|
||||||
must also be moved into this module.
|
must also be moved into this module.
|
||||||
"""
|
"""
|
||||||
import re
|
import re, ldap
|
||||||
from csc.adm import terms
|
from csc.adm import terms
|
||||||
from csc.backends import ldapi
|
from csc.backends import ldapi
|
||||||
from csc.common import conf
|
from csc.common import conf
|
||||||
|
@ -220,6 +220,72 @@ def list_group(group):
|
||||||
return {}
|
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):
|
def delete(userid):
|
||||||
"""
|
"""
|
||||||
Erase all records of a member.
|
Erase all records of a member.
|
||||||
|
@ -248,6 +314,27 @@ def delete(userid):
|
||||||
return member
|
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 ###
|
### Term Table ###
|
||||||
|
|
||||||
|
|
|
@ -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)))
|
|
@ -21,7 +21,8 @@ class LdapFilter:
|
||||||
search = self.escape(self.widget.get_edit_text(self))
|
search = self.escape(self.widget.get_edit_text(self))
|
||||||
filter = '(%s=%s)' % (attr, search)
|
filter = '(%s=%s)' % (attr, search)
|
||||||
try:
|
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:
|
if len(matches) > 0:
|
||||||
(_, attrs) = matches[0]
|
(_, attrs) = matches[0]
|
||||||
for (k, v) in self.map.items():
|
for (k, v) in self.map.items():
|
||||||
|
|
|
@ -7,6 +7,8 @@ import csc.apps.urwid.newmember as newmember
|
||||||
import csc.apps.urwid.renew as renew
|
import csc.apps.urwid.renew as renew
|
||||||
import csc.apps.urwid.info as info
|
import csc.apps.urwid.info as info
|
||||||
import csc.apps.urwid.search as search
|
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.adm import accounts, members, terms
|
||||||
from csc.common.excep import InvalidArgument
|
from csc.common.excep import InvalidArgument
|
||||||
|
@ -49,12 +51,14 @@ def program_name():
|
||||||
|
|
||||||
office_data = {
|
office_data = {
|
||||||
"name" : "Office Staff",
|
"name" : "Office Staff",
|
||||||
"group" : "office"
|
"group" : "office",
|
||||||
|
"groups" : [ "office", "cdrom", "audio", "video", "www" ],
|
||||||
}
|
}
|
||||||
|
|
||||||
syscom_data = {
|
syscom_data = {
|
||||||
"name" : "Systems Committee",
|
"name" : "Systems Committee",
|
||||||
"group" : "syscom"
|
"group" : "syscom",
|
||||||
|
"groups" : [ "office", "staff", "adm", "src" ],
|
||||||
}
|
}
|
||||||
|
|
||||||
def menu_items(items):
|
def menu_items(items):
|
||||||
|
@ -67,6 +71,7 @@ def main_menu():
|
||||||
("Create Club Account", new_club, None),
|
("Create Club Account", new_club, None),
|
||||||
("Display Member", display_member, None),
|
("Display Member", display_member, None),
|
||||||
("Search", search_members, None),
|
("Search", search_members, None),
|
||||||
|
("Manage Positions", manage_positions, None),
|
||||||
("Manage Office Staff", group_members, office_data),
|
("Manage Office Staff", group_members, office_data),
|
||||||
("Manage Systems Committee", group_members, syscom_data),
|
("Manage Systems Committee", group_members, syscom_data),
|
||||||
("Exit", raise_abort, None),
|
("Exit", raise_abort, None),
|
||||||
|
@ -100,7 +105,7 @@ def new_club(*args, **kwargs):
|
||||||
newmember.ClubIntroPage,
|
newmember.ClubIntroPage,
|
||||||
newmember.ClubInfoPage,
|
newmember.ClubInfoPage,
|
||||||
(newmember.EndPage, "club"),
|
(newmember.EndPage, "club"),
|
||||||
], (60,15))
|
], (60, 15))
|
||||||
|
|
||||||
def renew_member(*args, **kwargs):
|
def renew_member(*args, **kwargs):
|
||||||
push_wizard("Renew Membership", [
|
push_wizard("Renew Membership", [
|
||||||
|
@ -137,10 +142,23 @@ def search_term(data):
|
||||||
def search_group(data):
|
def search_group(data):
|
||||||
push_wizard("By Group", [ search.GroupPage ])
|
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):
|
def group_members(data):
|
||||||
|
add_data = data.copy()
|
||||||
|
add_data['type'] = 'Add'
|
||||||
|
remove_data = data.copy()
|
||||||
|
remove_data['type'] = 'Remove'
|
||||||
menu = [
|
menu = [
|
||||||
("Add %s member" % data["name"].lower(), add_group_member, data),
|
("Add %s member" % data["name"].lower(),
|
||||||
("Remove %s member" % data["name"].lower(), remove_group_member, data),
|
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),
|
("List %s members" % data["name"].lower(), list_group_members, data),
|
||||||
("Back", raise_back, None),
|
("Back", raise_back, None),
|
||||||
]
|
]
|
||||||
|
@ -148,11 +166,11 @@ def group_members(data):
|
||||||
listbox = urwid.ListBox( menu_items( menu ) )
|
listbox = urwid.ListBox( menu_items( menu ) )
|
||||||
push_window(listbox, "Manage %s" % data["name"])
|
push_window(listbox, "Manage %s" % data["name"])
|
||||||
|
|
||||||
def add_group_member(data):
|
def change_group_member(data):
|
||||||
pass
|
push_wizard("%s %s Member" % (data["type"], data["name"]), [
|
||||||
|
(groups.ChangeMember, data),
|
||||||
def remove_group_member(data):
|
groups.EndPage,
|
||||||
pass
|
])
|
||||||
|
|
||||||
def list_group_members(data):
|
def list_group_members(data):
|
||||||
if not members.connected(): members.connect()
|
if not members.connected(): members.connect()
|
||||||
|
|
|
@ -58,7 +58,7 @@ class InfoPage(WizardPanel):
|
||||||
self.state['name'] = self.name.get_edit_text()
|
self.state['name'] = self.name.get_edit_text()
|
||||||
self.state['program'] = self.program.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 )
|
self.focus_widget( self.userid )
|
||||||
set_status("Username is too short")
|
set_status("Username is too short")
|
||||||
return True
|
return True
|
||||||
|
@ -188,5 +188,7 @@ class EndPage(WizardPanel):
|
||||||
else:
|
else:
|
||||||
self.headtext.set_text("User Added")
|
self.headtext.set_text("User Added")
|
||||||
self.midtext.set_text("Congratulations, %s has been 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']))
|
% (self.state['userid'], self.state['userid']))
|
||||||
|
|
|
@ -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()
|
|
@ -641,6 +641,23 @@ class LDAPConnection(object):
|
||||||
return gids
|
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 ###
|
### Tests ###
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue