summaryrefslogtreecommitdiffstats
path: root/ipa-server/ipa-gui
diff options
context:
space:
mode:
Diffstat (limited to 'ipa-server/ipa-gui')
-rw-r--r--ipa-server/ipa-gui/README.multivalue27
-rw-r--r--ipa-server/ipa-gui/ipagui/controllers.py4
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/Makefile.am3
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/delegate.py2
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/group.py9
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/ipapolicy.py59
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/user.py11
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/userhelper.py2
-rw-r--r--ipa-server/ipa-gui/ipagui/proxyprovider.py39
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/style.css19
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am2
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py45
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/group.py84
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py168
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/policy.py32
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/user.py190
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/Makefile.am5
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupeditform.kid25
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/grouplist.kid12
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupshow.kid23
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid15
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid176
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid120
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/loginfailed.kid43
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/master.kid12
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/policyindex.kid31
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/policylayout.kid17
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/useredit.kid4
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usereditform.kid229
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userlayout.kid4
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userlist.kid8
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usernew.kid4
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usernewform.kid281
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usershow.kid112
34 files changed, 1567 insertions, 250 deletions
diff --git a/ipa-server/ipa-gui/README.multivalue b/ipa-server/ipa-gui/README.multivalue
new file mode 100644
index 000000000..ba315181d
--- /dev/null
+++ b/ipa-server/ipa-gui/README.multivalue
@@ -0,0 +1,27 @@
+The way multi-valued fields work is this:
+ - A new widget is added to the form. I name it as the attribute + s.
+ For example, I use cns for the cn attribute.
+ - If you need a new validator use a ForEach() so that each value is
+ checked.
+ - This attribute is populated from the incoming attribute from the
+ user or group record. The widget can support multiple fields at once
+ but I'm using it for just one field. In fact, I don't know if it
+ will work with more the way I'm using it.
+ - In the GUI an operator can add/remove values to each multi-valued field.
+ - Naming is very important in the widget. TurboGears automatically
+ re-assembles the data into a list of dict entries if you name things
+ properly. For example, the cns (multiple CN entries) looks like:
+ cns-0.cn=Rob+Crittenden&cns-1.cn=Robert+Crittenden&cns-2.cn=rcrit
+ - This gets converted to:
+ [{'cn': u'Rob Crittenden'}, {'cn': u'Robert Crittenden'}, {'cn': u'rcrit'}]
+ - I take this list of dicts and pull out each value and append it to a new
+ list that represents the original multi-valued field
+ - Then the list/dict version is removed (in this case, kw['cns']).
+
+When adding a new field you have to update:
+
+1. The form to add the new ExpandingForm() field and perhaps a validator
+2. The edit template to add the boilerplate to display the field
+3. The show template to be able to display all the fields separately
+4. The new template if you want to be able to enter these on new entries
+5. The subcontroller so you can do the input and output conversions
diff --git a/ipa-server/ipa-gui/ipagui/controllers.py b/ipa-server/ipa-gui/ipagui/controllers.py
index 5d0bfee03..d1ee22e01 100644
--- a/ipa-server/ipa-gui/ipagui/controllers.py
+++ b/ipa-server/ipa-gui/ipagui/controllers.py
@@ -17,6 +17,8 @@ import ipa.ipaclient
from subcontrollers.user import UserController
from subcontrollers.group import GroupController
from subcontrollers.delegation import DelegationController
+from subcontrollers.policy import PolicyController
+from subcontrollers.ipapolicy import IPAPolicyController
ipa.config.init_config()
@@ -27,6 +29,8 @@ class Root(controllers.RootController):
user = UserController()
group = GroupController()
delegate = DelegationController()
+ policy = PolicyController()
+ ipapolicy = IPAPolicyController()
@expose(template="ipagui.templates.welcome")
@identity.require(identity.not_anonymous())
diff --git a/ipa-server/ipa-gui/ipagui/forms/Makefile.am b/ipa-server/ipa-gui/ipagui/forms/Makefile.am
index 5f07e4cb0..c075b9f47 100644
--- a/ipa-server/ipa-gui/ipagui/forms/Makefile.am
+++ b/ipa-server/ipa-gui/ipagui/forms/Makefile.am
@@ -4,8 +4,9 @@ appdir = $(IPA_DATA_DIR)/ipagui/forms
app_PYTHON = \
__init__.py \
group.py \
+ ipapolicy.py \
user.py \
- delegate.py \
+ delegate.py \
$(NULL)
EXTRA_DIST = \
diff --git a/ipa-server/ipa-gui/ipagui/forms/delegate.py b/ipa-server/ipa-gui/ipagui/forms/delegate.py
index 89011f4a2..419df4fc7 100644
--- a/ipa-server/ipa-gui/ipagui/forms/delegate.py
+++ b/ipa-server/ipa-gui/ipagui/forms/delegate.py
@@ -44,7 +44,7 @@ aci_checkbox_attrs = [(field.name, field.label) for field in aci_attrs]
aci_name_to_label = dict(aci_checkbox_attrs)
class DelegateFields():
- name = widgets.TextField(name="name", label="Name")
+ name = widgets.TextField(name="name", label="Delegation Name")
source_group_dn = widgets.HiddenField(name="source_group_dn")
dest_group_dn = widgets.HiddenField(name="dest_group_dn")
diff --git a/ipa-server/ipa-gui/ipagui/forms/group.py b/ipa-server/ipa-gui/ipagui/forms/group.py
index 380c904a4..b67156641 100644
--- a/ipa-server/ipa-gui/ipagui/forms/group.py
+++ b/ipa-server/ipa-gui/ipagui/forms/group.py
@@ -1,14 +1,18 @@
import turbogears
from turbogears import validators, widgets
+from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm
class GroupFields():
cn = widgets.TextField(name="cn", label="Name")
gidnumber = widgets.TextField(name="gidnumber", label="GID")
description = widgets.TextField(name="description", label="Description")
- cn_hidden = widgets.HiddenField(name="cn")
editprotected_hidden = widgets.HiddenField(name="editprotected")
+ nsAccountLock = widgets.SingleSelectField(name="nsAccountLock",
+ label="Group Status",
+ options = [("", "active"), ("true", "inactive")])
+
group_orig = widgets.HiddenField(name="group_orig")
member_data = widgets.HiddenField(name="member_data")
dn_to_info_json = widgets.HiddenField(name="dn_to_info_json")
@@ -37,6 +41,7 @@ class GroupNewForm(widgets.Form):
class GroupEditValidator(validators.Schema):
+ cn = validators.String(not_empty=True)
gidnumber = validators.Int(not_empty=False)
description = validators.String(not_empty=False)
@@ -48,7 +53,7 @@ class GroupEditForm(widgets.Form):
params = ['members', 'group_fields']
hidden_fields = [
- GroupFields.cn_hidden, GroupFields.editprotected_hidden,
+ GroupFields.editprotected_hidden,
GroupFields.group_orig, GroupFields.member_data,
GroupFields.dn_to_info_json
]
diff --git a/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py b/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py
new file mode 100644
index 000000000..ec0e8c6f8
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py
@@ -0,0 +1,59 @@
+import turbogears
+from turbogears import validators, widgets
+
+class IPAPolicyFields():
+ # From cn=ipaConfig
+ ipausersearchfields = widgets.TextField(name="ipausersearchfields", label="User Search Fields")
+ ipagroupsearchfields = widgets.TextField(name="ipagroupsearchfields", label="Group Search Fields")
+ ipasearchtimelimit = widgets.TextField(name="ipasearchtimelimit", label="Search Time Limit (sec.)", attrs=dict(size=6,maxlength=6))
+ ipasearchrecordslimit = widgets.TextField(name="ipasearchrecordslimit", label="Search Records Limit", attrs=dict(size=6,maxlength=6))
+ ipahomesrootdir = widgets.TextField(name="ipahomesrootdir", label="Root for Home Directories")
+ ipadefaultloginshell = widgets.TextField(name="ipadefaultloginshell", label="Default shell")
+ ipadefaultprimarygroup = widgets.TextField(name="ipadefaultprimarygroup", label="Default Users group")
+ ipamaxusernamelength = widgets.TextField(name="ipamaxusernamelength", label="Max. Username Length", attrs=dict(size=3,maxlength=3))
+ ipapwdexpadvnotify = widgets.TextField(name="ipapwdexpadvnotify", label="Password Expiration Notification (days)", attrs=dict(size=3,maxlength=3))
+
+ ipapolicy_orig = widgets.HiddenField(name="ipapolicy_orig")
+
+ # From cn=accounts
+ krbmaxpwdlife = widgets.TextField(name="krbmaxpwdlife", label="Max. Password Lifetime", attrs=dict(size=3,maxlength=3))
+ krbminpwdlife = widgets.TextField(name="krbminpwdlife", label="Min. Password Lifetime", attrs=dict(size=3,maxlength=3))
+ krbpwdmindiffchars = widgets.TextField(name="krbpwdmindiffchars", label="Min. number of character classes", attrs=dict(size=3,maxlength=3))
+ krbpwdminlength = widgets.TextField(name="krbpwdminlength", label="Min. Length of password", attrs=dict(size=3,maxlength=3))
+ krbpwdhistorylength = widgets.TextField(name="krbpwdhistorylength", label="Password History size", attrs=dict(size=3,maxlength=3))
+
+ password_orig = widgets.HiddenField(name="password_orig")
+
+class IPAPolicyValidator(validators.Schema):
+ ipausersearchfields = validators.String(not_empty=True)
+ ipagroupsearchfields = validators.String(not_empty=True)
+ ipasearchtimelimit = validators.Number(not_empty=True)
+ ipasearchrecordslimit = validators.Number(not_empty=True)
+ ipamaxusernamelength = validators.Number(not_empty=True)
+ ipapwdexpadvnotify = validators.Number(not_empty=True)
+ ipahomesrootdir = validators.String(not_empty=True)
+ ipadefaultloginshell = validators.String(not_empty=True)
+ ipadefaultprimarygroup = validators.String(not_empty=True)
+ krbmaxpwdlife = validators.Number(not_empty=True)
+ krbminpwdlife = validators.Number(not_empty=True)
+ krbpwdmindiffchars = validators.Number(not_empty=True)
+ krbpwdminlength = validators.Number(not_empty=True)
+ krbpwdhistorylength = validators.Number(not_empty=True)
+
+class IPAPolicyForm(widgets.Form):
+ params = ['ipapolicy_fields']
+
+ hidden_fields = [
+ IPAPolicyFields.ipapolicy_orig, IPAPolicyFields.password_orig
+ ]
+
+ validator = IPAPolicyValidator()
+
+ def __init__(self, *args, **kw):
+ super(IPAPolicyForm,self).__init__(*args, **kw)
+ (self.template_c, self.template) = widgets.meta.load_kid_template(
+ "ipagui.templates.ipapolicyeditform")
+ self.ipapolicy_fields = IPAPolicyFields
+
+ def update_params(self, params):
+ super(IPAPolicyForm,self).update_params(params)
diff --git a/ipa-server/ipa-gui/ipagui/forms/user.py b/ipa-server/ipa-gui/ipagui/forms/user.py
index 1a35b4e07..7d3d37193 100644
--- a/ipa-server/ipa-gui/ipagui/forms/user.py
+++ b/ipa-server/ipa-gui/ipagui/forms/user.py
@@ -1,10 +1,12 @@
import turbogears
from turbogears import validators, widgets
+from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm
class UserFields():
givenname = widgets.TextField(name="givenname", label="Given Name")
sn = widgets.TextField(name="sn", label="Family Name")
cn = widgets.TextField(name="cn", label="Common Names")
+ cns = ExpandingForm(name="cns", label="Common Names", fields=[cn])
title = widgets.TextField(name="title", label="Title")
displayname = widgets.TextField(name="displayname", label="Display Name")
initials = widgets.TextField(name="initials", label="Initials")
@@ -21,11 +23,16 @@ class UserFields():
mail = widgets.TextField(name="mail", label="E-mail Address")
telephonenumber = widgets.TextField(name="telephonenumber", label="Work Number")
+ telephonenumbers = ExpandingForm(name="telephonenumbers", label="Work Numbers", fields=[telephonenumber])
facsimiletelephonenumber = widgets.TextField(name="facsimiletelephonenumber",
label="Fax Number")
+ facsimiletelephonenumbers = ExpandingForm(name="facsimiletelephonenumbers", label="Fax Numbers", fields=[facsimiletelephonenumber])
mobile = widgets.TextField(name="mobile", label="Cell Number")
+ mobiles = ExpandingForm(name="mobiles", label="Cell Numbers", fields=[mobile])
pager = widgets.TextField(name="pager", label="Pager Number")
+ pagers = ExpandingForm(name="pagers", label="Pager Numbers", fields=[pager])
homephone = widgets.TextField(name="homephone", label="Home Number")
+ homephones = ExpandingForm(name="homephones", label="Home Numbers", fields=[homephone])
street = widgets.TextField(name="street", label="Street Address")
l = widgets.TextField(name="l", label="City")
@@ -67,7 +74,8 @@ class UserNewValidator(validators.Schema):
userpassword_confirm = validators.String(not_empty=False)
givenname = validators.String(not_empty=True)
sn = validators.String(not_empty=True)
- mail = validators.Email(not_empty=True)
+ cn = validators.ForEach(validators.String(not_empty=True))
+ mail = validators.Email(not_empty=False)
chained_validators = [
validators.FieldsMatch('userpassword', 'userpassword_confirm')
@@ -102,6 +110,7 @@ class UserEditValidator(validators.Schema):
userpassword_confirm = validators.String(not_empty=False)
givenname = validators.String(not_empty=True)
sn = validators.String(not_empty=True)
+ cn = validators.ForEach(validators.String(not_empty=True))
mail = validators.Email(not_empty=True)
uidnumber = validators.Int(not_empty=False)
gidnumber = validators.Int(not_empty=False)
diff --git a/ipa-server/ipa-gui/ipagui/helpers/userhelper.py b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py
index e1ade3a2c..52d37c9e4 100644
--- a/ipa-server/ipa-gui/ipagui/helpers/userhelper.py
+++ b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py
@@ -13,7 +13,7 @@ def password_expires_in(datestr):
if not expdate:
return sys.maxint
- delta = expdate - datetime.datetime.now()
+ delta = expdate - datetime.datetime.now(ipautil.GeneralizedTimeZone())
return delta.days
def password_is_expired(days):
diff --git a/ipa-server/ipa-gui/ipagui/proxyprovider.py b/ipa-server/ipa-gui/ipagui/proxyprovider.py
index e8ef69830..bd9cf87a8 100644
--- a/ipa-server/ipa-gui/ipagui/proxyprovider.py
+++ b/ipa-server/ipa-gui/ipagui/proxyprovider.py
@@ -2,6 +2,11 @@ from turbogears.identity.soprovider import *
from turbogears.identity.visitor import *
import logging
import os
+import ipa.ipaclient
+from ipaserver import funcs
+import ipa.config
+import ipa.group
+import ipa.user
log = logging.getLogger("turbogears.identity")
@@ -15,7 +20,25 @@ class IPA_User(object):
(principal, realm) = user_name.split('@')
self.display_name = principal
self.permissions = None
- self.groups = None
+ transport = funcs.IPAServer()
+ client = ipa.ipaclient.IPAClient(transport)
+ client.set_krbccache(os.environ["KRB5CCNAME"])
+ try:
+ user = client.get_user_by_principal(user_name, ['dn'])
+ self.groups = []
+ groups = client.get_groups_by_member(user.dn, ['dn', 'cn'])
+ if isinstance(groups, str):
+ groups = [groups]
+ for ginfo in groups:
+ # cn may be multi-valued, add them all just in case
+ cn = ginfo.getValue('cn')
+ if isinstance(cn, str):
+ cn = [cn]
+ for c in cn:
+ self.groups.append(c)
+ except:
+ raise
+
return
class ProxyIdentity(object):
@@ -57,7 +80,7 @@ class ProxyIdentity(object):
def _get_groups(self):
try:
- return self._groups
+ return self._user.groups
except AttributeError:
# Groups haven't been computed yet
return None
@@ -87,10 +110,14 @@ class ProxyIdentityProvider(SqlObjectIdentityProvider):
pass
def validate_identity(self, user_name, password, visit_key):
- user = IPA_User(user_name)
- log.debug( "validate_identity %s" % user_name)
-
- return ProxyIdentity(visit_key, user)
+ try:
+ user = IPA_User(user_name)
+ log.debug( "validate_identity %s" % user_name)
+ return ProxyIdentity(visit_key, user)
+ except:
+ # Something went wrong in fetching the user. Set to
+ # anonymous which will deny access.
+ return ProxyIdentity( None )
def validate_password(self, user, user_name, password):
'''Validation has already occurred in the proxy'''
diff --git a/ipa-server/ipa-gui/ipagui/static/css/style.css b/ipa-server/ipa-gui/ipagui/static/css/style.css
index 28f501352..1a7cbb1fb 100644
--- a/ipa-server/ipa-gui/ipagui/static/css/style.css
+++ b/ipa-server/ipa-gui/ipagui/static/css/style.css
@@ -383,3 +383,22 @@ ul.checkboxlist li input {
#inactive {
background-color: silver;
}
+
+/*
+ * * TableKit css
+ *
+ */
+
+.sortcol {
+ cursor: pointer;
+ padding-right: 20px !important;
+ background-repeat: no-repeat !important;
+ background-position: right center !important;
+ text-decoration: underline;
+}
+.sortasc {
+ background-image: url(/static/images/up.gif) !important;
+}
+.sortdesc {
+ background-image: url(/static/images/down.gif) !important;
+}
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am b/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am
index d409bac7d..2f596f2ef 100644
--- a/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am
@@ -5,6 +5,8 @@ app_PYTHON = \
__init__.py \
group.py \
ipacontroller.py \
+ ipapolicy.py \
+ policy.py \
user.py \
delegation.py \
$(NULL)
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py b/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py
index 1515b04c1..142d34430 100644
--- a/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py
@@ -19,6 +19,7 @@ import ipagui.forms.delegate
import ipa.aci
import ldap.dn
+import operator
log = logging.getLogger(__name__)
@@ -34,7 +35,7 @@ class DelegationController(IPAController):
raise turbogears.redirect("/delegate/list")
@expose("ipagui.templates.delegatenew")
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_group("admins"))
def new(self):
"""Display delegate page"""
client = self.get_ipaclient()
@@ -45,7 +46,7 @@ class DelegationController(IPAController):
return dict(form=delegate_form, delegate=delegate)
@expose()
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_group("admins"))
def create(self, **kw):
"""Creates a new delegation"""
self.restrict_post()
@@ -63,11 +64,34 @@ class DelegationController(IPAController):
tg_template='ipagui.templates.delegatenew')
try:
+ aci_entry = client.get_aci_entry(aci_fields)
+
new_aci = ipa.aci.ACI()
new_aci.name = kw.get('name')
new_aci.source_group = kw.get('source_group_dn')
new_aci.dest_group = kw.get('dest_group_dn')
new_aci.attrs = kw.get('attrs')
+ if (new_aci.attrs, str):
+ new_aci.attrs = [new_aci.attrs]
+
+ # Look for an existing ACI of the same name
+ aci_str_list = aci_entry.getValues('aci')
+ if aci_str_list is None:
+ aci_str_list = []
+ if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)):
+ aci_str_list = [aci_str_list]
+
+ for aci_str in aci_str_list:
+ try:
+ old_aci = ipa.aci.ACI(aci_str)
+ if old_aci.name == new_aci.name:
+ turbogears.flash("Delgate add failed: a delegation of that name already exists")
+ return dict(form=delegate_form, delegate=kw,
+ tg_template='ipagui.templates.delegatenew')
+ except SyntaxError:
+ # ignore aci_str's that ACI can't parse
+ pass
+
# not pulling down existing aci attributes
aci_entry = client.get_aci_entry(['dn'])
@@ -75,7 +99,7 @@ class DelegationController(IPAController):
client.update_entry(aci_entry)
except ipaerror.IPAError, e:
- turbogears.flash("Delgate add failed: " + str(e))
+ turbogears.flash("Delgate add failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(form=delegate_form, delegate=kw,
tg_template='ipagui.templates.delegatenew')
@@ -83,7 +107,7 @@ class DelegationController(IPAController):
raise turbogears.redirect('/delegate/list')
@expose("ipagui.templates.delegateedit")
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_group("admins"))
def edit(self, acistr, tg_errors=None):
"""Display delegate page"""
if tg_errors:
@@ -105,12 +129,12 @@ class DelegationController(IPAController):
return dict(form=delegate_form, delegate=delegate)
except (SyntaxError, ipaerror.IPAError), e:
- turbogears.flash("Delegation edit failed: " + str(e))
+ turbogears.flash("Delegation edit failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
raise turbogears.redirect('/delegate/list')
@expose()
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_group("admins"))
def update(self, **kw):
"""Display delegate page"""
self.restrict_post()
@@ -162,7 +186,7 @@ class DelegationController(IPAController):
turbogears.flash("delegate updated")
raise turbogears.redirect('/delegate/list')
except (SyntaxError, ipaerror.IPAError), e:
- turbogears.flash("Delegation update failed: " + str(e))
+ turbogears.flash("Delegation update failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(form=delegate_form, delegate=kw,
tg_template='ipagui.templates.delegateedit')
@@ -175,7 +199,7 @@ class DelegationController(IPAController):
try:
aci_entry = client.get_aci_entry(aci_fields)
except ipaerror.IPAError, e:
- turbogears.flash("Delegation list failed: " + str(e))
+ turbogears.flash("Delegation list failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
raise turbogears.redirect('/')
aci_str_list = aci_entry.getValues('aci')
@@ -194,6 +218,7 @@ class DelegationController(IPAController):
pass
group_dn_to_cn = ipa.aci.extract_group_cns(aci_list, client)
+ aci_list = sorted(aci_list, key=operator.itemgetter(0))
# The list page needs to display field labels, not raw
# LDAP attributes
for aci in aci_list:
@@ -205,7 +230,7 @@ class DelegationController(IPAController):
fields=ipagui.forms.delegate.DelegateFields())
@expose()
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_group("admins"))
def delete(self, acistr):
"""Display delegate page"""
self.restrict_post()
@@ -237,7 +262,7 @@ class DelegationController(IPAController):
turbogears.flash("delegate deleted")
raise turbogears.redirect('/delegate/list')
except (SyntaxError, ipaerror.IPAError), e:
- turbogears.flash("Delegation deletion failed: " + str(e))
+ turbogears.flash("Delegation deletion failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
raise turbogears.redirect('/delegate/list')
@expose("ipagui.templates.delegategroupsearch")
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/group.py b/ipa-server/ipa-gui/ipagui/subcontrollers/group.py
index f0574a21c..dbcc77b9a 100644
--- a/ipa-server/ipa-gui/ipagui/subcontrollers/group.py
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/group.py
@@ -22,7 +22,7 @@ log = logging.getLogger(__name__)
group_new_form = ipagui.forms.group.GroupNewForm()
group_edit_form = ipagui.forms.group.GroupEditForm()
-group_fields = ['*']
+group_fields = ['*', 'nsAccountLock']
class GroupController(IPAController):
@@ -37,7 +37,7 @@ class GroupController(IPAController):
raise turbogears.redirect("/group/list")
@expose("ipagui.templates.groupnew")
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_group("admins"))
def new(self, tg_errors=None):
"""Displays the new group form"""
if tg_errors:
@@ -49,7 +49,7 @@ class GroupController(IPAController):
return dict(form=group_new_form, group={})
@expose()
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_group("admins"))
def create(self, **kw):
"""Creates a new group"""
self.restrict_post()
@@ -75,13 +75,16 @@ class GroupController(IPAController):
new_group.setValue('description', kw.get('description'))
rv = client.add_group(new_group)
+
+ if kw.get('nsAccountLock'):
+ client.mark_group_inactive(kw.get('cn'))
except ipaerror.exception_for(ipaerror.LDAP_DUPLICATE):
turbogears.flash("Group with name '%s' already exists" %
kw.get('cn'))
return dict(form=group_new_form, group=kw,
tg_template='ipagui.templates.groupnew')
except ipaerror.IPAError, e:
- turbogears.flash("Group add failed: " + str(e) + "<br/>" + str(e.detail))
+ turbogears.flash("Group add failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(form=group_new_form, group=kw,
tg_template='ipagui.templates.groupnew')
@@ -90,7 +93,11 @@ class GroupController(IPAController):
# on any error, we redirect to the _edit_ group page.
# this code does data setup, similar to groupedit()
#
- group = client.get_entry_by_cn(kw['cn'], group_fields)
+ if isinstance(kw['cn'], list):
+ cn0 = kw['cn'][0]
+ else:
+ cn0 = kw['cn']
+ group = client.get_entry_by_cn(cn0, group_fields)
group_dict = group.toDict()
member_dicts = []
@@ -166,7 +173,7 @@ class GroupController(IPAController):
@expose("ipagui.templates.groupedit")
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_group("admins"))
def edit(self, cn, tg_errors=None):
"""Displays the edit group form"""
if tg_errors:
@@ -204,20 +211,31 @@ class GroupController(IPAController):
raise turbogears.redirect('/group/show', uid=cn)
@expose()
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_group("admins"))
def update(self, **kw):
"""Updates an existing group"""
self.restrict_post()
client = self.get_ipaclient()
if kw.get('submit') == 'Cancel Edit':
+ orig_group_dict = loads(b64decode(kw.get('group_orig')))
+ # if cancelling need to use the original group because the one
+ # in kw may not exist yet.
+ cn = orig_group_dict.get('cn')
+ if (isinstance(cn,str)):
+ cn = [cn]
turbogears.flash("Edit group cancelled")
- raise turbogears.redirect('/group/show', cn=kw.get('cn'))
+ raise turbogears.redirect('/group/show', cn=cn[0])
+
+ if kw.get('editprotected') == '':
+ # if editprotected set these don't get sent in kw
+ orig_group_dict = loads(b64decode(kw.get('group_orig')))
+ kw['cn'] = orig_group_dict['cn']
+ kw['gidnumber'] = orig_group_dict['gidnumber']
# Decode the member data, in case we need to round trip
member_dicts = loads(b64decode(kw.get('member_data')))
-
tg_errors, kw = self.groupupdatevalidate(**kw)
if tg_errors:
turbogears.flash("There were validation errors.<br/>" +
@@ -242,6 +260,20 @@ class GroupController(IPAController):
if new_group.gidnumber != new_gid:
group_modified = True
new_group.setValue('gidnumber', new_gid)
+ else:
+ new_group.setValue('gidnumber', orig_group_dict.get('gidnumber'))
+ new_group.setValue('cn', orig_group_dict.get('cn'))
+ if new_group.cn != kw.get('cn'):
+ group_modified = True
+ new_group.setValue('cn', kw['cn'])
+
+ if group_modified:
+ rv = client.update_group(new_group)
+ #
+ # If the group update succeeds, but below operations fail, we
+ if new_group.cn != kw.get('cn'):
+ group_modified = True
+ new_group.setValue('cn', kw['cn'])
if group_modified:
rv = client.update_group(new_group)
@@ -252,10 +284,21 @@ class GroupController(IPAController):
#
kw['group_orig'] = b64encode(dumps(new_group.toDict()))
except ipaerror.IPAError, e:
- turbogears.flash("Group update failed: " + str(e))
+ turbogears.flash("Group update failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(form=group_edit_form, group=kw, members=member_dicts,
tg_template='ipagui.templates.groupedit')
+ if kw.get('nsAccountLock') == '':
+ kw['nsAccountLock'] = "false"
+
+ modify_no_update = False
+ if kw.get('nsAccountLock') == "false" and new_group.getValues('nsaccountlock') == "true":
+ client.mark_group_active(kw.get('cn'))
+ modify_no_update = True
+ elif kw.get('nsAccountLock') == "true" and new_group.nsaccountlock != "true":
+ client.mark_group_inactive(kw.get('cn'))
+ modify_no_update = True
+
#
# Add members
#
@@ -268,8 +311,9 @@ class GroupController(IPAController):
failed_adds = client.add_members_to_group(
utf8_encode_values(dnadds), new_group.dn)
kw['dnadd'] = failed_adds
+ group_modified = True
except ipaerror.IPAError, e:
- turbogears.flash("Group update failed: " + str(e))
+ turbogears.flash("Group update failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(form=group_edit_form, group=kw, members=member_dicts,
tg_template='ipagui.templates.groupedit')
@@ -285,8 +329,9 @@ class GroupController(IPAController):
failed_dels = client.remove_members_from_group(
utf8_encode_values(dndels), new_group.dn)
kw['dndel'] = failed_dels
+ group_modified = True
except ipaerror.IPAError, e:
- turbogears.flash("Group update failed: " + str(e))
+ turbogears.flash("Group update failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(form=group_edit_form, group=kw, members=member_dicts,
tg_template='ipagui.templates.groupedit')
@@ -308,8 +353,15 @@ class GroupController(IPAController):
return dict(form=group_edit_form, group=kw, members=member_dicts,
tg_template='ipagui.templates.groupedit')
- turbogears.flash("%s updated!" % kw['cn'])
- raise turbogears.redirect('/group/show', cn=kw['cn'])
+ if isinstance(kw['cn'], list):
+ cn0 = kw['cn'][0]
+ else:
+ cn0 = kw['cn']
+ if group_modified == True or modify_no_update == True:
+ turbogears.flash("%s updated!" % cn0)
+ else:
+ turbogears.flash("No modifications requested.")
+ raise turbogears.redirect('/group/show', cn=cn0)
@expose("ipagui.templates.grouplist")
@@ -330,7 +382,7 @@ class GroupController(IPAController):
turbogears.flash("These results are truncated.<br />" +
"Please refine your search and try again.")
except ipaerror.IPAError, e:
- turbogears.flash("Find groups failed: " + str(e))
+ turbogears.flash("Find groups failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
raise turbogears.redirect("/group/list")
return dict(groups=groups, criteria=criteria,
@@ -374,7 +426,7 @@ class GroupController(IPAController):
turbogears.flash("group deleted")
raise turbogears.redirect('/group/list')
except (SyntaxError, ipaerror.IPAError), e:
- turbogears.flash("Group deletion failed: " + str(e) + "<br/>" + str(e.detail))
+ turbogears.flash("Group deletion failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
raise turbogears.redirect('/group/list')
@validate(form=group_new_form)
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py b/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py
new file mode 100644
index 000000000..781ca35d4
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py
@@ -0,0 +1,168 @@
+import os
+from pickle import dumps, loads
+from base64 import b64encode, b64decode
+import copy
+import logging
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+from ipacontroller import IPAController
+from ipa.entity import utf8_encode_values
+from ipa import ipaerror
+import ipa.entity
+import ipagui.forms.ipapolicy
+
+import ldap.dn
+
+log = logging.getLogger(__name__)
+
+ipapolicy_edit_form = ipagui.forms.ipapolicy.IPAPolicyForm()
+
+class IPAPolicyController(IPAController):
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def index(self):
+ raise turbogears.redirect("/ipapolicy/show")
+
+ @expose("ipagui.templates.ipapolicyshow")
+ @identity.require(identity.in_group("admins"))
+ def show(self, tg_errors=None):
+ """Displays the one policy page"""
+ client = self.get_ipaclient()
+ config = client.get_ipa_config()
+ ipapolicy = config.toDict()
+
+ ppolicy = client.get_password_policy()
+ password = ppolicy.toDict()
+
+ return dict(ipapolicy=ipapolicy,password=password,fields=ipagui.forms.ipapolicy.IPAPolicyFields())
+
+ @expose("ipagui.templates.ipapolicyedit")
+ @identity.require(identity.in_group("admins"))
+ def edit(self, tg_errors=None):
+ """Displays the edit IPA policy form"""
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+
+ try:
+ client = self.get_ipaclient()
+ config = client.get_ipa_config()
+ ipapolicy_dict = config.toDict()
+
+ ppolicy = client.get_password_policy()
+ password_dict = ppolicy.toDict()
+
+ # store a copy of the original policy for the update later
+ ipapolicy_data = b64encode(dumps(ipapolicy_dict))
+ ipapolicy_dict['ipapolicy_orig'] = ipapolicy_data
+
+ # store a copy of the original policy for the update later
+ password_data = b64encode(dumps(password_dict))
+ password_dict['password_orig'] = password_data
+
+ # Combine the 2 dicts to make the form easier
+ ipapolicy_dict.update(password_dict)
+
+ return dict(form=ipapolicy_edit_form, ipapolicy=ipapolicy_dict)
+ except ipaerror.IPAError, e:
+ turbogears.flash("IPA Policy edit failed: " + str(e) + "<br/>" + str(e.detail))
+ raise turbogears.redirect('/ipapolicy/show')
+
+
+ @expose()
+ @identity.require(identity.in_group("admins"))
+ def update(self, **kw):
+ """Display delegate page"""
+ self.restrict_post()
+ client = self.get_ipaclient()
+
+ if kw.get('submit', '').startswith('Cancel'):
+ turbogears.flash("Edit policy cancelled")
+ raise turbogears.redirect('/ipapolicy/show')
+
+ tg_errors, kw = self.ipapolicyupdatevalidate(**kw)
+ if tg_errors:
+ turbogears.flash("There were validation errors.<br/>" +
+ "Please see the messages below for details.")
+ return dict(form=ipapolicy_edit_form, ipapolicy=kw,
+ tg_template='ipagui.templates.ipapolicyedit')
+
+ policy_modified = False
+ password_modified = False
+
+ try:
+ orig_ipapolicy_dict = loads(b64decode(kw.get('ipapolicy_orig')))
+ orig_password_dict = loads(b64decode(kw.get('password_orig')))
+
+ new_ipapolicy = ipa.entity.Entity(orig_ipapolicy_dict)
+ new_password = ipa.entity.Entity(orig_password_dict)
+
+ if str(new_ipapolicy.ipasearchtimelimit) != str(kw.get('ipasearchtimelimit')):
+ policy_modified = True
+ new_ipapolicy.setValue('ipasearchtimelimit', kw.get('ipasearchtimelimit'))
+ if str(new_ipapolicy.ipasearchrecordslimit) != str(kw.get('ipasearchrecordslimit')):
+ policy_modified = True
+ new_ipapolicy.setValue('ipasearchrecordslimit', kw.get('ipasearchrecordslimit'))
+ if new_ipapolicy.ipausersearchfields != kw.get('ipausersearchfields'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipausersearchfields', kw.get('ipausersearchfields'))
+ if new_ipapolicy.ipagroupsearchfields != kw.get('ipagroupsearchfields'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipagroupsearchfields', kw.get('ipagroupsearchfields'))
+ if str(new_ipapolicy.ipapwdexpadvnotify) != str(kw.get('ipapwdexpadvnotify')):
+ policy_modified = True
+ new_ipapolicy.setValue('ipapwdexpadvnotify', kw.get('ipapwdexpadvnotify'))
+ if str(new_ipapolicy.ipamaxusernamelength) != str(kw.get('ipamaxusernamelength')):
+ policy_modified = True
+ new_ipapolicy.setValue('ipamaxusernamelength', kw.get('ipamaxusernamelength'))
+ if new_ipapolicy.ipahomesrootdir != kw.get('ipahomesrootdir'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipahomesrootdir', kw.get('ipahomesrootdir'))
+ if new_ipapolicy.ipadefaultloginshell != kw.get('ipadefaultloginshell'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipadefaultloginshell', kw.get('ipadefaultloginshell'))
+ if new_ipapolicy.ipadefaultprimarygroup != kw.get('ipadefaultprimarygroup'):
+ policy_modified = True
+ new_ipapolicy.setValue('ipadefaultprimarygroup', kw.get('ipadefaultprimarygroup'))
+
+ if policy_modified:
+ rv = client.update_ipa_config(new_ipapolicy)
+
+ # Now check the password policy for updates
+ if str(new_password.krbmaxpwdlife) != str(kw.get('krbmaxpwdlife')):
+ password_modified = True
+ new_password.setValue('krbmaxpwdlife', str(kw.get('krbmaxpwdlife')))
+ if str(new_password.krbminpwdlife) != str(kw.get('krbminpwdlife')):
+ password_modified = True
+ new_password.setValue('krbminpwdlife', str(kw.get('krbminpwdlife')))
+ if str(new_password.krbpwdhistorylength) != str(kw.get('krbpwdhistorylength')):
+ password_modified = True
+ new_password.setValue('krbpwdhistorylength', str(kw.get('krbpwdhistorylength')))
+ if str(new_password.krbpwdmindiffchars) != str(kw.get('krbpwdmindiffchars')):
+ password_modified = True
+ new_password.setValue('krbpwdmindiffchars', str(kw.get('krbpwdmindiffchars')))
+ if str(new_password.krbpwdminlength) != str(kw.get('krbpwdminlength')):
+ password_modified = True
+ new_password.setValue('krbpwdminlength', str(kw.get('krbpwdminlength')))
+ if password_modified:
+ rv = client.update_password_policy(new_password)
+
+ turbogears.flash("IPA Policy updated")
+ raise turbogears.redirect('/ipapolicy/show')
+ except ipaerror.IPAError, e:
+ turbogears.flash("Policy update failed: " + str(e) + e.detail[0]['desc'])
+ return dict(form=ipapolicy_edit_form, ipapolicy=kw,
+ tg_template='ipagui.templates.ipapolicyedit')
+
+ @validate(form=ipapolicy_edit_form)
+ @identity.require(identity.not_anonymous())
+ def ipapolicyupdatevalidate(self, tg_errors=None, **kw):
+ return tg_errors, kw
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py b/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py
new file mode 100644
index 000000000..1f2e45876
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py
@@ -0,0 +1,32 @@
+import os
+from pickle import dumps, loads
+from base64 import b64encode, b64decode
+import copy
+import logging
+
+import cherrypy
+import turbogears
+from turbogears import controllers, expose, flash
+from turbogears import validators, validate
+from turbogears import widgets, paginate
+from turbogears import error_handler
+from turbogears import identity
+
+from ipacontroller import IPAController
+from ipa.entity import utf8_encode_values
+from ipa import ipaerror
+
+import ldap.dn
+
+log = logging.getLogger(__name__)
+
+class PolicyController(IPAController):
+
+ @expose("ipagui.templates.policyindex")
+ @identity.require(identity.in_group("admins"))
+ def index(self, tg_errors=None):
+ """Displays the one policy page"""
+
+ # TODO: return a dict of the items and URLs to display on
+ # Manage Policy
+ return dict()
diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/user.py b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py
index d328052b1..39343b595 100644
--- a/ipa-server/ipa-gui/ipagui/subcontrollers/user.py
+++ b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py
@@ -34,26 +34,48 @@ class UserController(IPAController):
def __init__(self, *args, **kw):
super(UserController,self).__init__(*args, **kw)
- self.load_custom_fields()
+# self.load_custom_fields()
def load_custom_fields(self):
- # client = self.get_ipaclient()
- # schema = client.get_user_custom_schema()
- schema = [
- { 'label': 'See Also',
- 'field': 'seeAlso',
- 'required': 'true', } ,
- { 'label': 'O O O',
- 'field': 'o',
- 'required': 'false', } ,
- ]
+
+ client = self.get_ipaclient()
+ schema = client.get_custom_fields()
+
+ # FIXME: Don't load from LDAP every single time it is called
+
+ # FIXME: Is removing the attributes on the fly thread-safe? Do we
+ # need to lock here?
for s in schema:
required=False
- if (s['required'] == "true"):
+ if (s['required'].lower() == "true"):
required=True
field = widgets.TextField(name=s['field'],label=s['label'])
validator = validators.String(not_empty=required)
+ # Don't allow dupes on the new form
+ try:
+ for i in range(len(user_new_form.custom_fields)):
+ if user_new_form.custom_fields[i].name == s['field']:
+ user_new_form.custom_fields.pop(i)
+ except:
+ pass
+
+ # Don't allow dupes on the edit form
+ try:
+ for i in range(len(user_edit_form.custom_fields)):
+ if user_edit_form.custom_fields[i].name == s['field']:
+ user_edit_form.custom_fields.pop(i)
+ except:
+ pass
+
+ # Don't allow dupes in the list of user fields
+ try:
+ for i in range(len(ipagui.forms.user.UserFields.custom_fields)):
+ if ipagui.forms.user.UserFields.custom_fields[i].name == s['field']:
+ ipagui.forms.user.UserFields.custom_fields.pop(i)
+ except:
+ pass
+
ipagui.forms.user.UserFields.custom_fields.append(field)
user_new_form.custom_fields.append(field)
user_edit_form.custom_fields.append(field)
@@ -61,15 +83,45 @@ class UserController(IPAController):
user_new_form.validator.add_field(s['field'], validator)
user_edit_form.validator.add_field(s['field'], validator)
+ def setup_mv_fields(self, field, fieldname):
+ """Given a field (must be a list) and field name, convert that
+ field into a list of dictionaries of the form:
+ [ { fieldname : v1}, { fieldname : v2 }, .. ]
+
+ This is how we pre-fill values for multi-valued fields.
+ """
+ mvlist = []
+ if field is not None:
+ for v in field:
+ mvlist.append({ fieldname : v } )
+ else:
+ # We need to return an empty value so something can be
+ # displayed on the edit page. Otherwise only an Add link
+ # will show, not an empty field.
+ mvlist.append({ fieldname : '' } )
+ return mvlist
+
+ def fix_incoming_fields(self, fields, fieldname, multifieldname):
+ """This is called by the update() function. It takes the incoming
+ list of dictionaries and converts it into back into the original
+ field, then removes the multiple field.
+ """
+ fields[fieldname] = []
+ for i in range(len(fields[multifieldname])):
+ fields[fieldname].append(fields[multifieldname][i][fieldname])
+ del(fields[multifieldname])
+
+ return fields
@expose()
def index(self):
raise turbogears.redirect("/user/list")
@expose("ipagui.templates.usernew")
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_any_group("admins","editors"))
def new(self, tg_errors=None):
"""Displays the new user form"""
+ self.load_custom_fields()
if tg_errors:
turbogears.flash("There were validation errors.<br/>" +
"Please see the messages below for details.")
@@ -77,7 +129,7 @@ class UserController(IPAController):
return dict(form=user_new_form, user={})
@expose()
- @identity.require(identity.not_anonymous())
+ @identity.require(identity.in_any_group("admins","editors"))
def create(self, **kw):
"""Creates a new user"""
self.restrict_post()
@@ -88,6 +140,15 @@ class UserController(IPAController):
raise turbogears.redirect('/user/list')
tg_errors, kw = self.usercreatevalidate(**kw)
+
+ # Fix incoming multi-valued fields we created for the form
+ kw = self.fix_incoming_fields(kw, 'cn', 'cns')
+ kw = self.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers')
+ kw = self.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers')
+ kw = self.fix_incoming_fields(kw, 'mobile', 'mobiles')
+ kw = self.fix_incoming_fields(kw, 'pager', 'pagers')
+ kw = self.fix_incoming_fields(kw, 'homephone', 'homephones')
+
if tg_errors:
turbogears.flash("There were validation errors.<br/>" +
"Please see the messages below for details.")
@@ -136,21 +197,21 @@ class UserController(IPAController):
new_user.setValue('carlicense', kw.get('carlicense'))
new_user.setValue('labeleduri', kw.get('labeleduri'))
- if kw.get('nsAccountLock'):
- new_user.setValue('nsAccountLock', 'true')
-
for custom_field in user_new_form.custom_fields:
new_user.setValue(custom_field.name,
kw.get(custom_field.name, ''))
rv = client.add_user(new_user)
+
+ if kw.get('nsAccountLock'):
+ client.mark_user_inactive(kw.get('uid'))
except ipaerror.exception_for(ipaerror.LDAP_DUPLICATE):
- turbogears.flash("Person with login '%s' already exists" %
+ turbogears.flash("User with login '%s' already exists" %
kw.get('uid'))
return dict(form=user_new_form, user=kw,
tg_template='ipagui.templates.usernew')
except ipaerror.IPAError, e:
- turbogears.flash("User add failed: " + str(e))
+ turbogears.flash("User add failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(form=user_new_form, user=kw,
tg_template='ipagui.templates.usernew')
@@ -181,7 +242,7 @@ class UserController(IPAController):
try:
client.modifyPassword(user_dict['krbprincipalname'], "", kw.get('userpassword'))
except ipaerror.IPAError, e:
- message = "Person successfully created.<br />"
+ message = "User successfully created.<br />"
message += "There was an error setting the password.<br />"
turbogears.flash(message)
return dict(form=user_edit_form, user=user_dict,
@@ -204,7 +265,7 @@ class UserController(IPAController):
failed_adds = dnadds
if len(failed_adds) > 0:
- message = "Person successfully created.<br />"
+ message = "User successfully created.<br />"
message += "There was an error adding groups.<br />"
message += "Failures have been preserved in the add/remove lists."
turbogears.flash(message)
@@ -243,6 +304,7 @@ class UserController(IPAController):
@identity.require(identity.not_anonymous())
def edit(self, uid=None, principal=None, tg_errors=None):
"""Displays the edit user form"""
+ self.load_custom_fields()
if tg_errors:
turbogears.flash("There were validation errors.<br/>" +
"Please see the messages below for details.")
@@ -259,6 +321,32 @@ class UserController(IPAController):
turbogears.flash("User edit failed: No uid or principal provided")
raise turbogears.redirect('/')
user_dict = user.toDict()
+
+ # Load potential multi-valued fields
+ if isinstance(user_dict['cn'], str):
+ user_dict['cn'] = [user_dict['cn']]
+ user_dict['cns'] = self.setup_mv_fields(user_dict['cn'], 'cn')
+
+ if isinstance(user_dict.get('telephonenumber',''), str):
+ user_dict['telephonenumber'] = [user_dict.get('telephonenumber'),'']
+ user_dict['telephonenumbers'] = self.setup_mv_fields(user_dict.get('telephonenumber'), 'telephonenumber')
+
+ if isinstance(user_dict.get('facsimiletelephonenumber',''), str):
+ user_dict['facsimiletelephonenumber'] = [user_dict.get('facsimiletelephonenumber'),'']
+ user_dict['facsimiletelephonenumbers'] = self.setup_mv_fields(user_dict.get('facsimiletelephonenumber'), 'facsimiletelephonenumber')
+
+ if isinstance(user_dict.get('mobile',''), str):
+ user_dict['mobile'] = [user_dict.get('mobile'),'']
+ user_dict['mobiles'] = self.setup_mv_fields(user_dict.get('mobile'), 'mobile')
+
+ if isinstance(user_dict.get('pager',''), str):
+ user_dict['pager'] = [user_dict.get('pager'),'']
+ user_dict['pagers'] = self.setup_mv_fields(user_dict.get('pager'), 'pager')
+
+ if isinstance(user_dict.get('homephone',''), str):
+ user_dict['homephone'] = [user_dict.get('homephone'),'']
+ user_dict['homephones'] = self.setup_mv_fields(user_dict.get('homephone'), 'homephone')
+
# Edit shouldn't fill in the password field.
if user_dict.has_key('userpassword'):
del(user_dict['userpassword'])
@@ -300,7 +388,7 @@ class UserController(IPAController):
except ipaerror.IPAError, e:
if uid is None:
uid = principal
- turbogears.flash("User edit failed: " + str(e))
+ turbogears.flash("User edit failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
raise turbogears.redirect('/user/show', uid=uid)
@expose()
@@ -314,6 +402,23 @@ class UserController(IPAController):
turbogears.flash("Edit user cancelled")
raise turbogears.redirect('/user/show', uid=kw.get('uid'))
+ # Fix incoming multi-valued fields we created for the form
+ kw = self.fix_incoming_fields(kw, 'cn', 'cns')
+ kw = self.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers')
+ kw = self.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers')
+ kw = self.fix_incoming_fields(kw, 'mobile', 'mobiles')
+ kw = self.fix_incoming_fields(kw, 'pager', 'pagers')
+ kw = self.fix_incoming_fields(kw, 'homephone', 'homephones')
+
+ # admins and editors can update anybody. A user can only update
+ # themselves. We need this check because it is very easy to guess
+ # the edit URI.
+ if ((not 'admins' in turbogears.identity.current.groups and
+ not 'editors' in turbogears.identity.current.groups) and
+ (kw.get('uid') != turbogears.identity.current.display_name)):
+ turbogears.flash("You do not have permission to update this user.")
+ raise turbogears.redirect('/user/show', uid=kw.get('uid'))
+
# Decode the group data, in case we need to round trip
user_groups_dicts = loads(b64decode(kw.get('user_groups_data')))
@@ -334,6 +439,14 @@ class UserController(IPAController):
try:
orig_user_dict = loads(b64decode(kw.get('user_orig')))
+ # remove multi-valued fields we created for the form
+ del(orig_user_dict['cns'])
+ del(orig_user_dict['telephonenumbers'])
+ del(orig_user_dict['facsimiletelephonenumbers'])
+ del(orig_user_dict['mobiles'])
+ del(orig_user_dict['pagers'])
+ del(orig_user_dict['homephones'])
+
new_user = ipa.user.User(orig_user_dict)
new_user.setValue('title', kw.get('title'))
new_user.setValue('givenname', kw.get('givenname'))
@@ -369,12 +482,6 @@ class UserController(IPAController):
new_user.setValue('carlicense', kw.get('carlicense'))
new_user.setValue('labeleduri', kw.get('labeleduri'))
-
- if kw.get('nsAccountLock'):
- new_user.setValue('nsAccountLock', 'true')
- else:
- new_user.setValue('nsAccountLock', None)
-
if kw.get('editprotected') == 'true':
if kw.get('userpassword'):
password_change = True
@@ -400,7 +507,7 @@ class UserController(IPAController):
# too much work to figure out unless someone really screams
pass
except ipaerror.IPAError, e:
- turbogears.flash("User update failed: " + str(e))
+ turbogears.flash("User update failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(form=user_edit_form, user=kw,
user_groups=user_groups_dicts,
tg_template='ipagui.templates.useredit')
@@ -412,7 +519,7 @@ class UserController(IPAController):
if password_change:
rv = client.modifyPassword(kw['krbprincipalname'], "", kw.get('userpassword'))
except ipaerror.IPAError, e:
- turbogears.flash("User password change failed: " + str(e))
+ turbogears.flash("User password change failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(form=user_edit_form, user=kw,
user_groups=user_groups_dicts,
tg_template='ipagui.templates.useredit')
@@ -459,6 +566,20 @@ class UserController(IPAController):
user_groups=user_groups_dicts,
tg_template='ipagui.templates.useredit')
+ if kw.get('nsAccountLock') == '':
+ kw['nsAccountLock'] = "false"
+
+ try:
+ if kw.get('nsAccountLock') == "false" and new_user.getValues('nsaccountlock') == "true":
+ client.mark_user_active(kw.get('uid'))
+ elif kw.get('nsAccountLock') == "true" and new_user.nsaccountlock != "true":
+ client.mark_user_inactive(kw.get('uid'))
+ except ipaerror.IPAError, e:
+ turbogears.flash("User status change failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
+ return dict(form=user_edit_form, user=kw,
+ user_groups=user_groups_dicts,
+ tg_template='ipagui.templates.useredit')
+
turbogears.flash("%s updated!" % kw['uid'])
raise turbogears.redirect('/user/show', uid=kw['uid'])
@@ -481,7 +602,7 @@ class UserController(IPAController):
turbogears.flash("These results are truncated.<br />" +
"Please refine your search and try again.")
except ipaerror.IPAError, e:
- turbogears.flash("User list failed: " + str(e))
+ turbogears.flash("User list failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
raise turbogears.redirect("/user/list")
return dict(users=users, uid=uid, fields=ipagui.forms.user.UserFields())
@@ -492,6 +613,7 @@ class UserController(IPAController):
def show(self, uid):
"""Retrieve a single user for display"""
client = self.get_ipaclient()
+ self.load_custom_fields()
try:
user = client.get_user_by_uid(uid, user_fields)
@@ -523,7 +645,7 @@ class UserController(IPAController):
user_groups=user_groups, user_reports=user_reports,
user_manager=user_manager, user_secretary=user_secretary)
except ipaerror.IPAError, e:
- turbogears.flash("User show failed: " + str(e))
+ turbogears.flash("User show failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
raise turbogears.redirect("/")
@expose()
@@ -539,7 +661,7 @@ class UserController(IPAController):
turbogears.flash("user deleted")
raise turbogears.redirect('/user/list')
except (SyntaxError, ipaerror.IPAError), e:
- turbogears.flash("User deletion failed: " + str(e))
+ turbogears.flash("User deletion failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
raise turbogears.redirect('/user/list')
@validate(form=user_new_form)
@@ -661,7 +783,7 @@ class UserController(IPAController):
users_counter = users[0]
users = users[1:]
except ipaerror.IPAError, e:
- turbogears.flash("search failed: " + str(e))
+ turbogears.flash("search failed: " + str(e) + "<br/>" + e.detail[0]['desc'])
return dict(users=users, criteria=criteria,
which_select=kw.get('which_select'),
diff --git a/ipa-server/ipa-gui/ipagui/templates/Makefile.am b/ipa-server/ipa-gui/ipagui/templates/Makefile.am
index 18db5fffc..6626ad8c2 100644
--- a/ipa-server/ipa-gui/ipagui/templates/Makefile.am
+++ b/ipa-server/ipa-gui/ipagui/templates/Makefile.am
@@ -20,8 +20,13 @@ app_DATA = \
groupnewform.kid \
groupnew.kid \
groupshow.kid \
+ ipapolicyeditform.kid \
+ ipapolicyedit.kid \
+ ipapolicyshow.kid \
loginfailed.kid \
master.kid \
+ policyindex.kid \
+ policylayout.kid \
usereditform.kid \
useredit.kid \
userlayout.kid \
diff --git a/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid b/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid
index cab585fcc..6a5c5adb8 100644
--- a/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid
@@ -25,17 +25,22 @@ from ipagui.helpers import ipahelper
<script type="text/javascript" charset="utf-8"
src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script>
<?python searchurl = tg.url('/group/edit_search') ?>
<script type="text/javascript">
function toggleProtectedFields(checkbox) {
var gidnumberField = $('form_gidnumber');
+ var cnField = $('form_cn');
if (checkbox.checked) {
gidnumberField.disabled = false;
+ cnField.disabled = false;
$('form_editprotected').value = 'true';
} else {
gidnumberField.disabled = true;
+ cnField.disabled = true;
$('form_editprotected').value = '';
}
}
@@ -70,11 +75,9 @@ from ipagui.helpers import ipahelper
py:content="group_fields.cn.label" />:
</th>
<td>
- <!-- <span py:replace="group_fields.cn.display(value_for(group_fields.cn))" />
+ <span py:replace="group_fields.cn.display(value_for(group_fields.cn))" />
<span py:if="tg.errors.get('cn')" class="fielderror"
- py:content="tg.errors.get('cn')" /> -->
- ${value_for(group_fields.cn)}
-
+ py:content="tg.errors.get('cn')" />
</td>
</tr>
@@ -88,6 +91,9 @@ from ipagui.helpers import ipahelper
<span py:if="tg.errors.get('description')" class="fielderror"
py:content="tg.errors.get('description')" />
+ <script type="text/javascript">
+ document.getElementById('form_cn').disabled = true;
+ </script>
</td>
</tr>
@@ -106,6 +112,16 @@ from ipagui.helpers import ipahelper
</script>
</td>
</tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${group_fields.nsAccountLock.field_id}" py:content="group_fields.nsAccountLock.label" />:
+ </th>
+ <td>
+ <span py:replace="group_fields.nsAccountLock.display(value_for(group_fields.nsAccountLock))" />
+ <span py:if="tg.errors.get('nsAccountLock')" class="fielderror"
+ py:content="tg.errors.get('nsAccountLock')" />
+ </td>
+ </tr>
</table>
<div>
@@ -160,6 +176,7 @@ from ipagui.helpers import ipahelper
div_counter = div_counter + 1
?>
</div>
+ &nbsp; <!-- a space here to prevent an empty div -->
</div>
</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/grouplist.kid b/ipa-server/ipa-gui/ipagui/templates/grouplist.kid
index 9f9bc4840..9489b3744 100644
--- a/ipa-server/ipa-gui/ipagui/templates/grouplist.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/grouplist.kid
@@ -20,7 +20,7 @@
</div>
<div py:if='(groups != None) and (len(groups) > 0)'>
<h2>${len(groups)} results returned:</h2>
- <table id="resultstable" class="details sortable resizable">
+ <table id="resultstable" class="details sortable resizable" cellspacing="0">
<thead>
<tr>
<th>
@@ -32,7 +32,15 @@
</tr>
</thead>
<tbody>
- <tr py:for="group in groups">
+ <tr py:for="group in groups" py:if="group.nsAccountLock != 'true'">
+ <td>
+ <a href="${tg.url('/group/show',cn=group.cn)}">${group.cn}</a>
+ </td>
+ <td>
+ ${group.description}
+ </td>
+ </tr>
+ <tr id="inactive" py:for="group in groups" py:if="group.nsAccountLock == 'true'">
<td>
<a href="${tg.url('/group/show',cn=group.cn)}">${group.cn}</a>
</td>
diff --git a/ipa-server/ipa-gui/ipagui/templates/groupshow.kid b/ipa-server/ipa-gui/ipagui/templates/groupshow.kid
index 7a66acdbe..8713742d5 100644
--- a/ipa-server/ipa-gui/ipagui/templates/groupshow.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/groupshow.kid
@@ -7,12 +7,17 @@
</head>
<body>
<?python
-edit_url = tg.url('/group/edit', cn=group.get('cn'))
+cn = group.get('cn')
+if isinstance(cn, list):
+ cn = cn[0]
+edit_url = tg.url('/group/edit', cn=cn)
+from ipagui.helpers import userhelper
?>
<div id="details">
<h1>View Group</h1>
- <input class="submitbutton" type="button"
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups"
+ class="submitbutton" type="button"
onclick="document.location.href='${edit_url}'"
value="Edit Group" />
@@ -38,6 +43,12 @@ edit_url = tg.url('/group/edit', cn=group.get('cn'))
</th>
<td>${group.get("gidnumber")}</td>
</tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.nsAccountLock.label" />:
+ </th>
+ <td>${userhelper.account_status_display(group.get("nsAccountLock"))}</td>
+ </tr>
</table>
<h2 class="formsection">Group Members</h2>
@@ -51,7 +62,10 @@ edit_url = tg.url('/group/edit', cn=group.get('cn'))
member_type = "user"
view_url = tg.url('/user/show', uid=member_uid)
else:
- member_cn = "%s" % member.get('cn')
+ mem = member.get('cn')
+ if isinstance(mem, list):
+ mem = mem[0]
+ member_cn = "%s" % mem
member_desc = "[group]"
member_type = "group"
view_url = tg.url('/group/show', cn=member_cn)
@@ -70,7 +84,8 @@ edit_url = tg.url('/group/edit', cn=group.get('cn'))
<br/>
<hr />
- <input class="submitbutton" type="button"
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups"
+ class="submitbutton" type="button"
onclick="document.location.href='${edit_url}'"
value="Edit Group" />
</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid
new file mode 100644
index 000000000..5987cc40a
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid
@@ -0,0 +1,15 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'policylayout.kid'">
+<head>
+ <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+ <title>Edit IPA Policy</title>
+</head>
+<body>
+ <div>
+ <h1>Edit IPA Policy</h1>
+
+ ${form.display(action=tg.url('/ipapolicy/update'), value=ipapolicy)}
+</div>
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid
new file mode 100644
index 000000000..106657636
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid
@@ -0,0 +1,176 @@
+<div xmlns:py="http://purl.org/kid/ns#"
+ class="simpleroster">
+
+ <form action="${action}" name="${name}" method="${method}" class="tableform"
+ onsubmit="preSubmit()" >
+
+ <input type="submit" class="submitbutton" name="submit"
+ value="Update Policy"/>
+ <input type="submit" class="submitbutton" name="submit"
+ value="Cancel Edit" />
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
+
+ <div py:for="field in hidden_fields"
+ py:replace="field.display(value_for(field), **params_for(field))"
+ />
+
+ <h2 class="formsection">Search</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipasearchtimelimit.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipasearchtimelimit.display(value_for(ipapolicy_fields.ipasearchtimelimit))" />
+ <span py:if="tg.errors.get('ipasearchtimelimit')" class="fielderror"
+ py:content="tg.errors.get('ipasearchtimelimit')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipasearchrecordslimit.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipasearchrecordslimit.display(value_for(ipapolicy_fields.ipasearchrecordslimit))" />
+ <span py:if="tg.errors.get('ipasearchrecordslimit')" class="fielderror"
+ py:content="tg.errors.get('ipasearchrecordslimit')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipausersearchfields.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipausersearchfields.display(value_for(ipapolicy_fields.ipausersearchfields))" />
+ <span py:if="tg.errors.get('ipausersearchfields')" class="fielderror"
+ py:content="tg.errors.get('ipausersearchfields')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipagroupsearchfields.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipagroupsearchfields.display(value_for(ipapolicy_fields.ipagroupsearchfields))" />
+ <span py:if="tg.errors.get('ipagroupsearchfields')" class="fielderror"
+ py:content="tg.errors.get('ipagroupsearchfields')" />
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Password Policy</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipapwdexpadvnotify.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipapwdexpadvnotify.display(value_for(ipapolicy_fields.ipapwdexpadvnotify))" />
+ <span py:if="tg.errors.get('ipapwdexpadvnotify')" class="fielderror"
+ py:content="tg.errors.get('ipapwdexpadvnotify')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbminpwdlife.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbminpwdlife.display(value_for(ipapolicy_fields.krbminpwdlife))" />
+ <span py:if="tg.errors.get('krbminpwdlife')" class="fielderror"
+ py:content="tg.errors.get('krbminpwdlife')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbmaxpwdlife.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbmaxpwdlife.display(value_for(ipapolicy_fields.krbmaxpwdlife))" />
+ <span py:if="tg.errors.get('krbmaxpwdlife')" class="fielderror"
+ py:content="tg.errors.get('krbmaxpwdlife')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbpwdmindiffchars.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbpwdmindiffchars.display(value_for(ipapolicy_fields.krbpwdmindiffchars))" />
+ <span py:if="tg.errors.get('krbpwdmindiffchars')" class="fielderror"
+ py:content="tg.errors.get('krbpwdmindiffchars')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbpwdminlength.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbpwdminlength.display(value_for(ipapolicy_fields.krbpwdminlength))" />
+ <span py:if="tg.errors.get('krbpwdminlength')" class="fielderror"
+ py:content="tg.errors.get('krbpwdminlength')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.krbpwdhistorylength.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.krbpwdhistorylength.display(value_for(ipapolicy_fields.krbpwdhistorylength))" />
+ <span py:if="tg.errors.get('krbpwdhistorylength')" class="fielderror"
+ py:content="tg.errors.get('krbpwdhistorylength')" />
+ </td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">User Settings</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipamaxusernamelength.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipamaxusernamelength.display(value_for(ipapolicy_fields.ipamaxusernamelength))" />
+ <span py:if="tg.errors.get('ipamaxusernamelength')" class="fielderror"
+ py:content="tg.errors.get('ipamaxusernamelength')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipahomesrootdir.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipahomesrootdir.display(value_for(ipapolicy_fields.ipahomesrootdir))" />
+ <span py:if="tg.errors.get('ipahomesrootdir')" class="fielderror"
+ py:content="tg.errors.get('ipahomesrootdir')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipadefaultloginshell.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipadefaultloginshell.display(value_for(ipapolicy_fields.ipadefaultloginshell))" />
+ <span py:if="tg.errors.get('ipadefaultloginshell')" class="fielderror"
+ py:content="tg.errors.get('ipadefaultloginshell')" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="ipapolicy_fields.ipadefaultprimarygroup.label" />:
+ </th>
+ <td>
+ <span py:replace="ipapolicy_fields.ipadefaultprimarygroup.display(value_for(ipapolicy_fields.ipadefaultprimarygroup))" />
+ <span py:if="tg.errors.get('ipadefaultprimarygroup')" class="fielderror"
+ py:content="tg.errors.get('ipadefaultprimarygroup')" />
+ </td>
+ </tr>
+ </table>
+ </form>
+
+</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid
new file mode 100644
index 000000000..089fb494e
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid
@@ -0,0 +1,120 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'policylayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Manage IPA Policy</title>
+</head>
+<body>
+
+<?python
+from ipagui.helpers import ipahelper
+edit_url = tg.url('/ipapolicy/edit')
+?>
+
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
+
+ <h1>Manage IPA Policy</h1>
+
+ <h2 class="formsection">Search</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipasearchtimelimit.label" />:
+ </th>
+ <td>${ipapolicy.get("ipasearchtimelimit")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipasearchrecordslimit.label" />:
+ </th>
+ <td>${ipapolicy.get("ipasearchrecordslimit")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipausersearchfields.label" />:
+ </th>
+ <td>${ipapolicy.get("ipausersearchfields")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipagroupsearchfields.label" />:
+ </th>
+ <td>${ipapolicy.get("ipagroupsearchfields")}</td>
+ </tr>
+ </table>
+
+ <h2 class="formsection">Password Policy</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipapwdexpadvnotify.label" />:
+ </th>
+ <td>${ipapolicy.get("ipapwdexpadvnotify")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbminpwdlife.label" />:
+ </th>
+ <td>${password.get("krbminpwdlife")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbmaxpwdlife.label" />:
+ </th>
+ <td>${password.get("krbmaxpwdlife")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbpwdmindiffchars.label" />:
+ </th>
+ <td>${password.get("krbpwdmindiffchars")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbpwdminlength.label" />:
+ </th>
+ <td>${password.get("krbpwdminlength")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.krbpwdhistorylength.label" />:
+ </th>
+ <td>${password.get("krbpwdhistorylength")}</td>
+ </tr>
+ </table>
+ <h2 class="formsection">User Settings</h2>
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0">
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipamaxusernamelength.label" />:
+ </th>
+ <td>${ipapolicy.get("ipamaxusernamelength")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipahomesrootdir.label" />:
+ </th>
+ <td>${ipapolicy.get("ipahomesrootdir")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipadefaultloginshell.label" />:
+ </th>
+ <td>${ipapolicy.get("ipadefaultloginshell")}</td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" py:content="fields.ipadefaultprimarygroup.label" />:
+ </th>
+ <td>${ipapolicy.get("ipadefaultprimarygroup")}</td>
+ </tr>
+ </table>
+<hr />
+ <input class="submitbutton" type="button"
+ onclick="document.location.href='${edit_url}'"
+ value="Edit Policy" />
+
+
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid b/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid
index 84896be5c..b31db82a7 100644
--- a/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid
@@ -1,35 +1,24 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:py="http://purl.org/kid/ns#">
-
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
<head>
- <meta content="text/html; charset=UTF-8"
- http-equiv="content-type" py:replace="''"/>
- <title>Login Failure</title>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Permission Denied</title>
</head>
<body>
- <div id="header">
- <div id="logo">
- <a href="${tg.url('/')}"><img
- src="${tg.url('/static/images/logo.png')}"
- border="0" alt="homepage"
- /></a>
- </div>
- <div id="headerinfo">
- <div id="login">
- <div py:if="tg.config('identity.on') and not defined('logging_in')" id="page
-Login">
- <span py:if="tg.identity.anonymous">
- Kerberos login failed.
- </span>
- <span py:if="not tg.identity.anonymous">
- Logged in as: ${tg.identity.user.display_name}
- </span>
+ <div id="main_content">
+ <div id="details">
+ <div id="alertbox" py:if="value_of('tg_flash', None)">
+ <p py:content="XML(tg_flash)"></p></div>
+ <h1>Permission Denied</h1>
+ <div class="instructions">
+ <p>
+ You do not have permission to access this page.
+ </p>
</div>
+ </div>
</div>
- </div>
- </div>
</body>
+
</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/master.kid b/ipa-server/ipa-gui/ipagui/templates/master.kid
index fd527a278..12e54fa1d 100644
--- a/ipa-server/ipa-gui/ipagui/templates/master.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/master.kid
@@ -70,18 +70,20 @@
<div id="sidebar">
<h2>Tasks</h2>
<ul>
- <li><a href="${tg.url('/user/new')}">Add Person</a></li>
- <li><a href="${tg.url('/user/list')}">Find People</a></li>
+ <li py:if="'admins' in tg.identity.groups"><a href="${tg.url('/user/new')}">Add User</a></li>
+ <li><a href="${tg.url('/user/list')}">Find Users</a></li>
</ul>
<ul>
- <li><a href="${tg.url('/group/new')}">Add Group</a></li>
+ <li py:if="'admins' in tg.identity.groups"><a href="${tg.url('/group/new')}">Add Group</a></li>
<li><a href="${tg.url('/group/list')}">Find Groups</a></li>
</ul>
+ <ul py:if="'admins' in tg.identity.groups">
+ <li><a href="${tg.url('/policy/index')}">Manage Policy</a></li>
+ </ul>
<ul>
- <li><a href="${tg.url('/')}">Manage Policy</a></li>
<li><a href="${tg.url('/user/edit/', principal=tg.identity.user.display_name)}">Self Service</a></li>
</ul>
- <ul>
+ <ul py:if="'admins' in tg.identity.groups">
<li><a href="${tg.url('/delegate/list')}">Delegations</a></li>
</ul>
</div>
diff --git a/ipa-server/ipa-gui/ipagui/templates/policyindex.kid b/ipa-server/ipa-gui/ipagui/templates/policyindex.kid
new file mode 100644
index 000000000..88fa4bcc2
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/policyindex.kid
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'policylayout.kid'">
+<head>
+<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
+<title>Manage Policy</title>
+</head>
+<body>
+
+<?python
+from ipagui.helpers import ipahelper
+?>
+
+ <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
+
+ <h1>Manage Policy</h1>
+
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ <a href="${tg.url('/ipapolicy/show')}"
+ >IPA Policy</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+
+</body>
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/policylayout.kid b/ipa-server/ipa-gui/ipagui/templates/policylayout.kid
new file mode 100644
index 000000000..171326539
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/templates/policylayout.kid
@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"
+ py:extends="'master.kid'">
+<head>
+</head>
+
+<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()">
+ <div id="main_content">
+<div id="details">
+ <div id="alertbox" py:if="value_of('tg_flash', None)"><p py:content="XML(tg_flash)"></p></div>
+
+ <div py:replace="[item.text]+item[:]"></div>
+ </div>
+</div>
+</body>
+
+</html>
diff --git a/ipa-server/ipa-gui/ipagui/templates/useredit.kid b/ipa-server/ipa-gui/ipagui/templates/useredit.kid
index 3f9482a3d..f5cb1b02e 100644
--- a/ipa-server/ipa-gui/ipagui/templates/useredit.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/useredit.kid
@@ -3,7 +3,7 @@
py:extends="'userlayout.kid'">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
-<title>Edit Person</title>
+<title>Edit User</title>
</head>
<body>
@@ -14,7 +14,7 @@
<span class="small">edit protected fields</span>
</input>
</div>
- <h1>Edit Person</h1>
+ <h1>Edit User</h1>
</div>
<?python
diff --git a/ipa-server/ipa-gui/ipagui/templates/usereditform.kid b/ipa-server/ipa-gui/ipagui/templates/usereditform.kid
index f6da48870..88b778d8c 100644
--- a/ipa-server/ipa-gui/ipagui/templates/usereditform.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/usereditform.kid
@@ -10,11 +10,11 @@
onsubmit="preSubmit()">
<input type="submit" class="submitbutton" name="submit"
- value="Update Person"/>
+ value="Update User"/>
<input type="submit" class="submitbutton" name="submit"
value="Cancel Edit" />
<input type="button" class="submitbutton"
- value="Delete Person"
+ value="Delete User"
onclick="return confirmDelete();"
/>
@@ -26,6 +26,8 @@ from ipagui.helpers import ipahelper
src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
<script type="text/javascript" charset="utf-8"
src="${tg.url('/static/javascript/dynamicselect.js')}"></script>
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script>
<?python
searchurl = tg.url('/user/edit_search')
@@ -141,14 +143,35 @@ from ipagui.helpers import ipahelper
<tr>
<th>
- <label class="fieldlabel" for="${user_fields.cn.field_id}"
- py:content="user_fields.cn.label" />:
- </th>
- <td>
- <span py:replace="user_fields.cn.display(value_for(user_fields.cn))" />
- <span py:if="tg.errors.get('cn')" class="fielderror"
- py:content="tg.errors.get('cn')" />
-
+ <label class="fieldlabel" for="${user_fields.cns.field_id}"
+ py:content="user_fields.cns.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.cns.field_id}">
+ <tbody>
+ <?python repetition = 0
+ cn_index = 0
+ cn_error = tg.errors.get('cn')
+ ?>
+ <tr py:for="cn in value_for(user_fields.cn)"
+ id="${user_fields.cns.field_id}_${repetition}"
+ class="${user_fields.cns.field_class}">
+
+ <td py:for="field in user_fields.cns.fields">
+ <span><input class="textfield" type="text" id="${user_fields.cns.field_id}_${repetition}_cn" name="cns-${repetition}.cn" value="${cn}"/></span>
+ <span py:if="cn_error and cn_error[cn_index]" class="fielderror"
+ py:content="tg.errors.get('cn')" />
+ </td>
+ <?python cn_index = cn_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.cns.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.cns.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.cns.field_id}');">Add Common Name</a>
</td>
</tr>
@@ -364,61 +387,170 @@ from ipagui.helpers import ipahelper
<tr>
<th>
- <label class="fieldlabel" for="${user_fields.telephonenumber.field_id}"
- py:content="user_fields.telephonenumber.label" />:
- </th>
- <td>
- <span py:replace="user_fields.telephonenumber.display(value_for(user_fields.telephonenumber))" />
- <span py:if="tg.errors.get('telephonenumber')" class="fielderror"
- py:content="tg.errors.get('telephonenumber')" />
+ <label class="fieldlabel" for="${user_fields.telephonenumbers.field_id}"
+ py:content="user_fields.telephonenumbers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.telephonenumbers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ tele_index = 0
+ tele_error = tg.errors.get('telephonenumber')
+ ?>
+ <tr py:for="tele in value_for(user_fields.telephonenumber)"
+ id="${user_fields.telephonenumbers.field_id}_${repetition}"
+ class="${user_fields.telephonenumbers.field_class}">
+
+ <td py:for="field in user_fields.telephonenumbers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.telephonenumbers.field_id}_${repetition}_telephonenumber" name="telephonenumbers-${repetition}.telephonenumber" value="${tele}"/></span>
+ <span py:if="tele_error and tele_error[tele_index]" class="fielderror"
+ py:content="tg.errors.get('telephonenumber')" />
+ </td>
+ <?python tele_index = tele_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.telephonenumbers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.telephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.telephonenumbers.field_id}');">Add Work Number</a>
</td>
</tr>
-
<tr>
<th>
- <label class="fieldlabel" for="${user_fields.facsimiletelephonenumber.field_id}"
- py:content="user_fields.facsimiletelephonenumber.label" />:
- </th>
- <td>
- <span py:replace="user_fields.facsimiletelephonenumber.display(value_for(user_fields.facsimiletelephonenumber))" />
- <span py:if="tg.errors.get('facsimiletelephonenumber')" class="fielderror"
- py:content="tg.errors.get('facsimiletelephonenumber')" />
+ <label class="fieldlabel" for="${user_fields.facsimiletelephonenumbers.field_id}"
+ py:content="user_fields.facsimiletelephonenumbers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.facsimiletelephonenumbers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ fax_index = 0
+ fax_error = tg.errors.get('facsimiletelephonenumber')
+ ?>
+ <tr py:for="fax in value_for(user_fields.facsimiletelephonenumber)"
+ id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}"
+ class="${user_fields.facsimiletelephonenumbers.field_class}">
+
+ <td py:for="field in user_fields.facsimiletelephonenumbers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}_facsimiletelephonenumber" name="facsimiletelephonenumbers-${repetition}.facsimiletelephonenumber" value="${fax}"/></span>
+ <span py:if="fax_error and fax_error[fax_index]" class="fielderror"
+ py:content="tg.errors.get('facsimiletelephonenumber')" />
+ </td>
+ <?python fax_index = fax_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.facsimiletelephonenumbers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.facsimiletelephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.facsimiletelephonenumbers.field_id}');">Add Fax Number</a>
</td>
</tr>
<tr>
<th>
- <label class="fieldlabel" for="${user_fields.mobile.field_id}"
- py:content="user_fields.mobile.label" />:
- </th>
- <td>
- <span py:replace="user_fields.mobile.display(value_for(user_fields.mobile))" />
- <span py:if="tg.errors.get('mobile')" class="fielderror"
- py:content="tg.errors.get('mobile')" />
+ <label class="fieldlabel" for="${user_fields.mobiles.field_id}"
+ py:content="user_fields.mobiles.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.mobiles.field_id}">
+ <tbody>
+ <?python repetition = 0
+ mobile_index = 0
+ mobile_error = tg.errors.get('mobile')
+ ?>
+ <tr py:for="mobile in value_for(user_fields.mobile)"
+ id="${user_fields.mobiles.field_id}_${repetition}"
+ class="${user_fields.mobiles.field_class}">
+
+ <td py:for="field in user_fields.mobiles.fields">
+ <span><input class="textfield" type="text" id="${user_fields.mobiles.field_id}_${repetition}_mobile" name="mobiles-${repetition}.mobile" value="${mobile}"/></span>
+ <span py:if="mobile_error and mobile_error[mobile_index]" class="fielderror"
+ py:content="tg.errors.get('mobile')" />
+ </td>
+ <?python mobile_index = mobile_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.mobiles.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.mobiles.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.mobiles.field_id}');">Add Cell Number</a>
</td>
</tr>
<tr>
<th>
- <label class="fieldlabel" for="${user_fields.pager.field_id}"
- py:content="user_fields.pager.label" />:
- </th>
- <td>
- <span py:replace="user_fields.pager.display(value_for(user_fields.pager))" />
- <span py:if="tg.errors.get('pager')" class="fielderror"
- py:content="tg.errors.get('pager')" />
+ <label class="fieldlabel" for="${user_fields.pagers.field_id}"
+ py:content="user_fields.pagers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.pagers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ pager_index = 0
+ pager_error = tg.errors.get('pager')
+ ?>
+ <tr py:for="pager in value_for(user_fields.pager)"
+ id="${user_fields.pagers.field_id}_${repetition}"
+ class="${user_fields.pagers.field_class}">
+
+ <td py:for="field in user_fields.pagers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.pagers.field_id}_${repetition}_pager" name="pagers-${repetition}.pager" value="${pager}"/></span>
+ <span py:if="pager_error and pager_error[pager_index]" class="fielderror"
+ py:content="tg.errors.get('pager')" />
+ </td>
+ <?python pager_index = pager_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.pagers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.pagers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.pagers.field_id}');">Add Pager Number</a>
</td>
</tr>
<tr>
<th>
- <label class="fieldlabel" for="${user_fields.homephone.field_id}"
- py:content="user_fields.homephone.label" />:
- </th>
- <td>
- <span py:replace="user_fields.homephone.display(value_for(user_fields.homephone))" />
- <span py:if="tg.errors.get('homephone')" class="fielderror"
- py:content="tg.errors.get('homephone')" />
+ <label class="fieldlabel" for="${user_fields.homephones.field_id}"
+ py:content="user_fields.homephones.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.homephones.field_id}">
+ <tbody>
+ <?python repetition = 0
+ homephone_index = 0
+ homephone_error = tg.errors.get('homephone')
+ ?>
+ <tr py:for="homephone in value_for(user_fields.homephone)"
+ id="${user_fields.homephones.field_id}_${repetition}"
+ class="${user_fields.homephones.field_class}">
+
+ <td py:for="field in user_fields.homephones.fields">
+ <span><input class="textfield" type="text" id="${user_fields.homephones.field_id}_${repetition}_homephone" name="homephones-${repetition}.homephone" value="${homephone}"/></span>
+ <span py:if="homephone_error and homephone_error[homephone_index]" class="fielderror"
+ py:content="tg.errors.get('homephone')" />
+ </td>
+ <?python homephone_index = homephone_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.homephones.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.homephones.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.homephones.field_id}');">Add Home Phone</a>
</td>
</tr>
</table>
@@ -685,6 +817,7 @@ from ipagui.helpers import ipahelper
div_counter = div_counter + 1
?>
</div>
+ &nbsp; <!-- a space here to prevent an empty div -->
</div>
</div>
@@ -714,11 +847,11 @@ from ipagui.helpers import ipahelper
<hr/>
<input type="submit" class="submitbutton" name="submit"
- value="Update Person"/>
+ value="Update User"/>
<input type="submit" class="submitbutton" name="submit"
value="Cancel Edit" />
<input type="button" class="submitbutton"
- value="Delete Person"
+ value="Delete User"
onclick="return confirmDelete();"
/>
diff --git a/ipa-server/ipa-gui/ipagui/templates/userlayout.kid b/ipa-server/ipa-gui/ipagui/templates/userlayout.kid
index bbeb81399..c4e8104e6 100644
--- a/ipa-server/ipa-gui/ipagui/templates/userlayout.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/userlayout.kid
@@ -15,8 +15,8 @@
<!-- <div id="sidebar">
<h2>Tools</h2>
- <a href="${tg.url('/user/new')}">Add Person</a><br/>
- <a href="${tg.url('/user/list')}">Find People</a><br/>
+ <a href="${tg.url('/user/new')}">Add User</a><br/>
+ <a href="${tg.url('/user/list')}">Find Users</a><br/>
</div> -->
</div>
</body>
diff --git a/ipa-server/ipa-gui/ipagui/templates/userlist.kid b/ipa-server/ipa-gui/ipagui/templates/userlist.kid
index fdeeb3169..9ca3753d0 100644
--- a/ipa-server/ipa-gui/ipagui/templates/userlist.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/userlist.kid
@@ -3,15 +3,15 @@
py:extends="'userlayout.kid'">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
-<title>Find People</title>
+<title>Find Users</title>
</head>
<body>
- <h1>Find People</h1>
+ <h1>Find Users</h1>
<script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script>
<div id="search">
<form action="${tg.url('/user/list')}" method="get">
<input id="uid" type="text" name="uid" value="${uid}" />
- <input class="searchbutton" type="submit" value="Find People"/>
+ <input class="searchbutton" type="submit" value="Find Users"/>
</form>
<script type="text/javascript">
document.getElementById("uid").focus();
@@ -23,7 +23,7 @@
<thead>
<tr>
<th>
- Person
+ User
</th>
<th>
Phone
diff --git a/ipa-server/ipa-gui/ipagui/templates/usernew.kid b/ipa-server/ipa-gui/ipagui/templates/usernew.kid
index 16f0e66b9..b740bca22 100644
--- a/ipa-server/ipa-gui/ipagui/templates/usernew.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/usernew.kid
@@ -3,10 +3,10 @@
py:extends="'userlayout.kid'">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
- <title>Add Person</title>
+ <title>Add User</title>
</head>
<body>
- <h1>Add Person</h1>
+ <h1>Add User</h1>
${form.display(action=tg.url("/user/create"), value=user)}
</body>
diff --git a/ipa-server/ipa-gui/ipagui/templates/usernewform.kid b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid
index eeaa87fa4..97be52732 100644
--- a/ipa-server/ipa-gui/ipagui/templates/usernewform.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid
@@ -2,8 +2,8 @@
class="simpleroster">
<form action="${action}" name="${name}" method="${method}" class="tableform"
onsubmit="preSubmit()">
-
-<input type="submit" class="submitbutton" name="submit" value="Add Person"/>
+
+<input type="submit" class="submitbutton" name="submit" value="Add User"/>
<?python
from ipagui.helpers import ipahelper
@@ -13,6 +13,8 @@ from ipagui.helpers import ipahelper
src="${tg.url('/static/javascript/dynamicedit.js')}"></script>
<script type="text/javascript" charset="utf-8"
src="${tg.url('/static/javascript/dynamicselect.js')}"></script>
+ <script type="text/javascript" charset="utf-8"
+ src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script>
<?python
searchurl = tg.url('/user/edit_search')
@@ -51,7 +53,7 @@ from ipagui.helpers import ipahelper
</script>
<div py:for="field in hidden_fields"
- py:replace="field.display(value_for(field), **params_for(field))"
+ py:replace="field.display(value_for(field), **params_for(field))"
/>
<h2 class="formsection">Identity Details</h2>
@@ -107,7 +109,7 @@ from ipagui.helpers import ipahelper
var uid = $('form_uid');
var mail = $('form_mail');
- var cn = $('form_cn');
+ var cn = $('form_cns_0_cn');
var displayname = $('form_displayname');
var initials = $('form_initials');
@@ -164,14 +166,38 @@ from ipagui.helpers import ipahelper
<tr>
<th>
- <label class="fieldlabel" for="${user_fields.cn.field_id}"
- py:content="user_fields.cn.label" />:
+ <label class="fieldlabel" for="${user_fields.cns.field_id}"
+ py:content="user_fields.cns.label" />:
</th>
- <td>
- <span py:replace="user_fields.cn.display(value_for(user_fields.cn))" />
- <span py:if="tg.errors.get('cn')" class="fielderror"
- py:content="tg.errors.get('cn')" />
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.cns.field_id}">
+ <tbody>
+ <?python repetition = 0
+ cn_index = 0
+ cn_error = tg.errors.get('cn')
+ values = value_for(user_fields.cn)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="cn in values"
+ id="${user_fields.cns.field_id}_${repetition}"
+ class="${user_fields.cns.field_class}">
+ <td py:for="field in user_fields.cns.fields">
+ <span><input class="textfield" type="text" id="${user_fields.cns.field_id}_${repetition}_cn" name="cns-${repetition}.cn" value="${cn}"/></span>
+ <span py:if="cn_error and cn_error[cn_index]" class="fielderror"
+ py:content="tg.errors.get('cn')" />
+ </td>
+ <?python cn_index = cn_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.cns.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.cns.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.cns.field_id}');">Add Common Name</a>
</td>
</tr>
@@ -337,63 +363,188 @@ from ipagui.helpers import ipahelper
<tr>
<th>
- <label class="fieldlabel" for="${user_fields.telephonenumber.field_id}"
- py:content="user_fields.telephonenumber.label" />:
- </th>
- <td>
- <span py:replace="user_fields.telephonenumber.display(value_for(user_fields.telephonenumber))" />
- <span py:if="tg.errors.get('telephonenumber')" class="fielderror"
- py:content="tg.errors.get('telephonenumber')" />
- </td>
- </tr>
-
- <tr>
- <th>
- <label class="fieldlabel" for="${user_fields.facsimiletelephonenumber.field_id}"
- py:content="user_fields.facsimiletelephonenumber.label" />:
- </th>
- <td>
- <span py:replace="user_fields.facsimiletelephonenumber.display(value_for(user_fields.facsimiletelephonenumber))" />
- <span py:if="tg.errors.get('facsimiletelephonenumber')" class="fielderror"
- py:content="tg.errors.get('facsimiletelephonenumber')" />
- </td>
- </tr>
-
- <tr>
- <th>
- <label class="fieldlabel" for="${user_fields.mobile.field_id}"
- py:content="user_fields.mobile.label" />:
- </th>
- <td>
- <span py:replace="user_fields.mobile.display(value_for(user_fields.mobile))" />
- <span py:if="tg.errors.get('mobile')" class="fielderror"
- py:content="tg.errors.get('mobile')" />
- </td>
- </tr>
-
- <tr>
- <th>
- <label class="fieldlabel" for="${user_fields.pager.field_id}"
- py:content="user_fields.pager.label" />:
- </th>
- <td>
- <span py:replace="user_fields.pager.display(value_for(user_fields.pager))" />
- <span py:if="tg.errors.get('pager')" class="fielderror"
- py:content="tg.errors.get('pager')" />
+ <label class="fieldlabel" for="${user_fields.telephonenumbers.field_id}"
+ py:content="user_fields.telephonenumbers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.telephonenumbers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ tele_index = 0
+ tele_error = tg.errors.get('telephonenumber')
+ values = value_for(user_fields.telephonenumber)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="tele in values"
+ id="${user_fields.telephonenumbers.field_id}_${repetition}"
+ class="${user_fields.telephonenumbers.field_class}">
+
+ <td py:if="user_fields.telephonenumbers.fields is not None" py:for="field in user_fields.telephonenumbers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.telephonenumbers.field_id}_${repetition}_telephonenumber" name="telephonenumbers-${repetition}.telephonenumber" value="${tele}"/></span>
+ <span py:if="tele_error and tele_error[tele_index]" class="fielderror"
+ py:content="tg.errors.get('telephonenumber')" />
+ </td>
+ <?python tele_index = tele_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.telephonenumbers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.telephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.telephonenumbers.field_id}');">Add Work Number</a>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.facsimiletelephonenumbers.field_id}"
+ py:content="user_fields.facsimiletelephonenumbers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.facsimiletelephonenumbers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ fax_index = 0
+ fax_error = tg.errors.get('facsimiletelephonenumber')
+ values = value_for(user_fields.facsimiletelephonenumber)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="fax in values"
+ id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}"
+ class="${user_fields.facsimiletelephonenumbers.field_class}">
+
+ <td py:for="field in user_fields.facsimiletelephonenumbers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}_facsimiletelephonenumber" name="facsimiletelephonenumbers-${repetition}.facsimiletelephonenumber" value="${fax}"/></span>
+ <span py:if="fax_error and fax_error[fax_index]" class="fielderror"
+ py:content="tg.errors.get('facsimiletelephonenumber')" />
+ </td>
+ <?python fax_index = fax_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.facsimiletelephonenumbers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.facsimiletelephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.facsimiletelephonenumbers.field_id}');">Add Fax Number</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.mobiles.field_id}"
+ py:content="user_fields.mobiles.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.mobiles.field_id}">
+ <tbody>
+ <?python repetition = 0
+ mobile_index = 0
+ mobile_error = tg.errors.get('mobile')
+ values = value_for(user_fields.mobile)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="mobile in values"
+ id="${user_fields.mobiles.field_id}_${repetition}"
+ class="${user_fields.mobiles.field_class}">
+
+ <td py:for="field in user_fields.mobiles.fields">
+ <span><input class="textfield" type="text" id="${user_fields.mobiles.field_id}_${repetition}_mobile" name="mobiles-${repetition}.mobile" value="${mobile}"/></span>
+ <span py:if="mobile_error and mobile_error[mobile_index]" class="fielderror"
+ py:content="tg.errors.get('mobile')" />
+ </td>
+ <?python mobile_index = mobile_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.mobiles.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.mobiles.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.mobiles.field_id}');">Add Cell Number</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.pagers.field_id}"
+ py:content="user_fields.pagers.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.pagers.field_id}">
+ <tbody>
+ <?python repetition = 0
+ pager_index = 0
+ pager_error = tg.errors.get('pager')
+ values = value_for(user_fields.pager)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="pager in values"
+ id="${user_fields.pagers.field_id}_${repetition}"
+ class="${user_fields.pagers.field_class}">
+
+ <td py:for="field in user_fields.pagers.fields">
+ <span><input class="textfield" type="text" id="${user_fields.pagers.field_id}_${repetition}_pager" name="pagers-${repetition}.pager" value="${pager}"/></span>
+ <span py:if="pager_error and pager_error[pager_index]" class="fielderror"
+ py:content="tg.errors.get('pager')" />
+ </td>
+ <?python pager_index = pager_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.pagers.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.pagers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.pagers.field_id}');">Add Pager Number</a>
+ </td>
+ </tr>
+
+ <tr>
+ <th>
+ <label class="fieldlabel" for="${user_fields.homephones.field_id}"
+ py:content="user_fields.homephones.label" />:
+ </th>
+ <td colspan="3">
+ <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.homephones.field_id}">
+ <tbody>
+ <?python repetition = 0
+ homephone_index = 0
+ homephone_error = tg.errors.get('homephone')
+ values = value_for(user_fields.homephone)
+ if values is None:
+ values=['']
+ ?>
+ <tr py:for="homephone in values"
+ id="${user_fields.homephones.field_id}_${repetition}"
+ class="${user_fields.homephones.field_class}">
+
+ <td py:for="field in user_fields.homephones.fields">
+ <span><input class="textfield" type="text" id="${user_fields.homephones.field_id}_${repetition}_homephone" name="homephones-${repetition}.homephone" value="${homephone}"/></span>
+ <span py:if="homephone_error and homephone_error[homephone_index]" class="fielderror"
+ py:content="tg.errors.get('homephone')" />
+ </td>
+ <?python homephone_index = homephone_index + 1 ?>
+ <td>
+ <a
+ href="javascript:ExpandingForm.removeItem('${user_fields.homephones.field_id}_${repetition}')">Remove</a>
+ </td>
+ <?python repetition = repetition + 1?>
+ </tr>
+ </tbody>
+ </table>
+ <a id="${user_fields.homephones.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.homephones.field_id}');">Add Home Phone</a>
</td>
</tr>
- <tr>
- <th>
- <label class="fieldlabel" for="${user_fields.homephone.field_id}"
- py:content="user_fields.homephone.label" />:
- </th>
- <td>
- <span py:replace="user_fields.homephone.display(value_for(user_fields.homephone))" />
- <span py:if="tg.errors.get('homephone')" class="fielderror"
- py:content="tg.errors.get('homephone')" />
- </td>
- </tr>
</table>
<h2 class="formsection">Mailing Address</h2>
@@ -635,7 +786,7 @@ from ipagui.helpers import ipahelper
</div>
<hr />
-<input type="submit" class="submitbutton" name="submit" value="Add Person"/>
+<input type="submit" class="submitbutton" name="submit" value="Add User"/>
</form>
diff --git a/ipa-server/ipa-gui/ipagui/templates/usershow.kid b/ipa-server/ipa-gui/ipagui/templates/usershow.kid
index cc56340d9..8cc356b89 100644
--- a/ipa-server/ipa-gui/ipagui/templates/usershow.kid
+++ b/ipa-server/ipa-gui/ipagui/templates/usershow.kid
@@ -3,17 +3,18 @@
py:extends="'userlayout.kid'">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/>
- <title>View Person</title>
+ <title>View User</title>
</head>
<body>
<?python
edit_url = tg.url('/user/edit', uid=user.get('uid'))
?>
- <h1>View Person</h1>
+ <h1>View User</h1>
- <input class="submitbutton" type="button"
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups"
+ class="submitbutton" type="button"
onclick="document.location.href='${edit_url}'"
- value="Edit Person" />
+ value="Edit User" />
<?python
from ipagui.helpers import userhelper
@@ -57,7 +58,21 @@ else:
<th>
<label class="fieldlabel" py:content="fields.cn.label" />:
</th>
- <td>${user.get("cn")}</td>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("cn")
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
</tr>
<tr>
<th>
@@ -132,31 +147,101 @@ else:
<th>
<label class="fieldlabel" py:content="fields.telephonenumber.label" />:
</th>
- <td>${user.get("telephonenumber")}</td>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("telephonenumber", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
</tr>
<tr>
<th>
<label class="fieldlabel" py:content="fields.facsimiletelephonenumber.label" />:
</th>
- <td>${user.get("facsimiletelephonenumber")}</td>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("facsimiletelephonenumber", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
</tr>
<tr>
<th>
<label class="fieldlabel" py:content="fields.mobile.label" />:
</th>
- <td>${user.get("mobile")}</td>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("mobile", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
</tr>
<tr>
<th>
<label class="fieldlabel" py:content="fields.pager.label" />:
</th>
- <td>${user.get("pager")}</td>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("pager", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
</tr>
<tr>
<th>
<label class="fieldlabel" py:content="fields.homephone.label" />:
</th>
- <td>${user.get("homephone")}</td>
+ <td>
+ <table cellpadding="2" cellspacing="0" border="0">
+ <tbody>
+ <?python
+ index = 0
+ values = user.get("homephone", '')
+ if isinstance(values, str):
+ values = [values]
+ ?>
+ <tr py:for="index in range(len(values))">
+ <td>${values[index]}</td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
</tr>
</table>
@@ -260,7 +345,7 @@ else:
</table>
<div py:if='len(fields.custom_fields) &gt; 0'>
- <div class="formsection" >Custom Fields</div>
+ <h2 class="formsection">Custom Fields</h2>
<table class="formtable" cellpadding="2" cellspacing="0" border="0">
<tr py:for='custom_field in fields.custom_fields'>
<th>
@@ -289,8 +374,9 @@ else:
<br/>
<hr />
- <input class="submitbutton" type="button"
+ <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups"
+ class="submitbutton" type="button"
onclick="document.location.href='${edit_url}'"
- value="Edit Person" />
+ value="Edit User" />
</body>
</html>