diff options
Diffstat (limited to 'ipa-server/ipa-gui/ipagui')
121 files changed, 15248 insertions, 0 deletions
diff --git a/ipa-server/ipa-gui/ipagui/Makefile.am b/ipa-server/ipa-gui/ipagui/Makefile.am new file mode 100644 index 00000000..83636323 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/Makefile.am @@ -0,0 +1,30 @@ +NULL = + +SUBDIRS = \ + config \ + forms \ + helpers \ + static \ + subcontrollers \ + templates \ + tests \ + $(NULL) + +appdir = $(IPA_DATA_DIR)/ipagui +app_PYTHON = \ + __init__.py \ + controllers.py \ + json.py \ + model.py \ + proxyprovider.py \ + proxyvisit.py \ + release.py \ + $(NULL) + +EXTRA_DIST = \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/__init__.py b/ipa-server/ipa-gui/ipagui/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/__init__.py diff --git a/ipa-server/ipa-gui/ipagui/config/Makefile.am b/ipa-server/ipa-gui/ipagui/config/Makefile.am new file mode 100644 index 00000000..db96758f --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/config/Makefile.am @@ -0,0 +1,20 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/config +app_PYTHON = \ + __init__.py \ + $(NULL) + +app_DATA = \ + app.cfg \ + log.cfg \ + $(NULL) + +EXTRA_DIST = \ + $(app_DATA) \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/config/__init__.py b/ipa-server/ipa-gui/ipagui/config/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/config/__init__.py diff --git a/ipa-server/ipa-gui/ipagui/config/app.cfg b/ipa-server/ipa-gui/ipagui/config/app.cfg new file mode 100644 index 00000000..01bb5ad2 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/config/app.cfg @@ -0,0 +1,104 @@ +[global] +# The settings in this file should not vary depending on the deployment +# environment. dev.cfg and prod.cfg are the locations for +# the different deployment settings. Settings in this file will +# be overridden by settings in those other files. + +# The commented out values below are the defaults + +# VIEW + +# which view (template engine) to use if one is not specified in the +# template name +# tg.defaultview = "kid" + +# The following kid settings determine the settings used by the kid serializer. + +# One of (html|html-strict|xhtml|xhtml-strict|xml|json) +# kid.outputformat="html" + +# kid.encoding="utf-8" + +# The sitetemplate is used for overall styling of a site that +# includes multiple TurboGears applications +# tg.sitetemplate="<packagename.templates.templatename>" + +# Allow every exposed function to be called as json, +# tg.allow_json = False + +# List of Widgets to include on every page. +# for exemple ['turbogears.mochikit'] +# tg.include_widgets = [] + +# Set to True if the scheduler should be started +# tg.scheduler = False + +# Set session or cookie +session_filter.on = True + +# VISIT TRACKING +# Each visit to your application will be assigned a unique visit ID tracked via +# a cookie sent to the visitor's browser. +# -------------- + +# Enable Visit tracking +visit.on=True + +# Number of minutes a visit may be idle before it expires. +# visit.timeout=20 + +# The name of the cookie to transmit to the visitor's browser. +# visit.cookie.name="tg-visit" + +# Domain name to specify when setting the cookie (must begin with . according to +# RFC 2109). The default (None) should work for most cases and will default to +# the machine to which the request was made. NOTE: localhost is NEVER a valid +# value and will NOT WORK. +# visit.cookie.domain=None + +# Specific path for the cookie +# visit.cookie.path="/" + +# The name of the VisitManager plugin to use for visitor tracking. +visit.manager="proxyvisit" + +# IDENTITY +# General configuration of the TurboGears Identity management module +# -------- + +# Switch to turn on or off the Identity management module +identity.on=True + +# [REQUIRED] URL to which CherryPy will internally redirect when an access +# control check fails. If Identity management is turned on, a value for this +# option must be specified. +identity.failure_url="/loginfailed" + +identity.provider='proxyprovider' + +# The names of the fields on the login form containing the visitor's user ID +# and password. In addition, the submit button is specified simply so its +# existence may be stripped out prior to passing the form data to the target +# controller. +# identity.form.user_name="user_name" +# identity.form.password="password" +# identity.form.submit="login" + +# What sources should the identity provider consider when determining the +# identity associated with a request? Comma separated list of identity sources. +# Valid sources: form, visit, http_auth +# identity.source="form,http_auth,visit" +identity.source="visit" + +# compress the data sends to the web browser +# [/] +# gzip_filter.on = True +# gzip_filter.mime_types = ["application/x-javascript", "text/javascript", "text/html", "text/css", "text/plain"] + +[/static] +static_filter.on = True +static_filter.dir = "%(top_level_dir)s/static" + +[/favicon.ico] +static_filter.on = True +static_filter.file = "%(top_level_dir)s/static/images/favicon.ico" diff --git a/ipa-server/ipa-gui/ipagui/config/log.cfg b/ipa-server/ipa-gui/ipagui/config/log.cfg new file mode 100644 index 00000000..483069cd --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/config/log.cfg @@ -0,0 +1,32 @@ +# LOGGING +# Logging is often deployment specific, but some handlers and +# formatters can be defined here. + +[logging] +[[formatters]] +[[[message_only]]] +format='*(message)s' + +[[[full_content]]] +format='*(asctime)s *(name)s *(levelname)s *(message)s' + +[[[datestamped]]] +format='*(asctime)s *(message)s' + +[[handlers]] +[[[debug_out]]] +class='StreamHandler' +level='DEBUG' +args='(sys.stdout,)' +formatter='full_content' + +[[[access_out]]] +class='StreamHandler' +level='INFO' +args='(sys.stdout,)' +formatter='datestamped' + +[[[error_out]]] +class='StreamHandler' +level='ERROR' +args='(sys.stdout,)' diff --git a/ipa-server/ipa-gui/ipagui/controllers.py b/ipa-server/ipa-gui/ipagui/controllers.py new file mode 100644 index 00000000..024a804d --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/controllers.py @@ -0,0 +1,135 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import logging +import StringIO +import traceback + +import cherrypy +import turbogears +from turbogears import controllers, expose, flash +from turbogears import config +from turbogears import validators, validate +from turbogears import widgets, paginate +from turbogears import error_handler +from turbogears import identity + +import ipa.config +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 +from subcontrollers.principal import PrincipalController + +ipa.config.init_config() + +log = logging.getLogger(__name__) + +class Root(controllers.RootController): + + user = UserController() + group = GroupController() + delegate = DelegationController() + policy = PolicyController() + ipapolicy = IPAPolicyController() + principal = PrincipalController() + + @expose(template="ipagui.templates.welcome") + @identity.require(identity.not_anonymous()) + def index(self): + return dict() + + @expose() + @identity.require(identity.not_anonymous()) + def topsearch(self, **kw): + if kw.get('searchtype') == "Users": + return Root.user.list(uid=kw.get('searchvalue')) + else: + return Root.group.list(criteria=kw.get('searchvalue')) + + @expose("ipagui.templates.loginfailed") + def loginfailed(self, **kw): + return dict() + + + _error_codes = { + None: u'General Error', + 400: u'400 - Bad Request', + 401: u'401 - Unauthorized', + 403: u'403 - Forbidden', + 404: u'404 - Not Found', + 500: u'500 - Internal Server Error', + 501: u'501 - Not Implemented', + 502: u'502 - Bad Gateway', + } + + def handle_error(self, status, message): + """This method is derived from the sample error catcher on + http://docs.turbogears.org/1.0/ErrorReporting.""" + try: + error_msg = self._error_codes.get(status, self._error_codes[None]) + url = "%s %s" % (cherrypy.request.method, cherrypy.request.path) + if (status == 500): + log.exception("%s error (%s) for request '%s'", status, + error_msg, url) + else: + log.error("%s error (%s) for request '%s'", status, + error_msg, url) + + if config.get('server.environment') == 'production': + details = '' + else: + buf = StringIO.StringIO() + traceback.print_exc(file=buf) + details = buf.getvalue() + buf.close() + + data = dict( + status = status, + message = message, + error_msg = error_msg, + url = url, + details = details, + ) + + if status == 404: + page_template = 'ipagui.templates.not_found' + else: + page_template = 'ipagui.templates.unhandled_exception' + + body = controllers._process_output( + data, + page_template, + 'html', + 'text/html', + None + ) + cherrypy.response.headers['Content-Length'] = len(body) + cherrypy.response.body = body + + # don't catch SystemExit + except StandardError, exc: + log.exception('Error handler failed: %s', exc) + + # To hook in error handler for production only: + # if config.get('server.environment') == 'production': + # _cp_on_http_error = handle_error + + _cp_on_http_error = handle_error diff --git a/ipa-server/ipa-gui/ipagui/forms/Makefile.am b/ipa-server/ipa-gui/ipagui/forms/Makefile.am new file mode 100644 index 00000000..a7f3c762 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/Makefile.am @@ -0,0 +1,19 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/forms +app_PYTHON = \ + __init__.py \ + group.py \ + ipapolicy.py \ + user.py \ + delegate.py \ + principal.py \ + $(NULL) + +EXTRA_DIST = \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/forms/__init__.py b/ipa-server/ipa-gui/ipagui/forms/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/__init__.py diff --git a/ipa-server/ipa-gui/ipagui/forms/delegate.py b/ipa-server/ipa-gui/ipagui/forms/delegate.py new file mode 100644 index 00000000..e7ba03f9 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/delegate.py @@ -0,0 +1,110 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import turbogears +from turbogears import validators, widgets +from ipagui.helpers import ipahelper + +from ipagui.forms.user import UserFields + +# TODO - get from config or somewhere +aci_attrs = [ + UserFields.givenname, + UserFields.sn, + UserFields.cn, + UserFields.title, + UserFields.displayname, + UserFields.initials, + UserFields.uid, + UserFields.krbprincipalkey, + UserFields.uidnumber, + UserFields.gidnumber, + UserFields.homedirectory, + UserFields.loginshell, + UserFields.gecos, + UserFields.mail, + UserFields.telephonenumber, + UserFields.facsimiletelephonenumber, + UserFields.mobile, + UserFields.pager, + UserFields.homephone, + UserFields.street, + UserFields.l, + UserFields.st, + UserFields.postalcode, + UserFields.ou, + UserFields.businesscategory, + UserFields.description, + UserFields.employeetype, + UserFields.manager, + UserFields.roomnumber, + UserFields.secretary, + UserFields.carlicense, + UserFields.labeleduri, +] + +aci_checkbox_attrs = [(field.name, field.label) for field in aci_attrs] + +aci_name_to_label = dict(aci_checkbox_attrs) + +class DelegateFields(object): + 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") + + source_group_cn = widgets.HiddenField(name="source_group_cn", + label="People in Group") + dest_group_cn = widgets.HiddenField(name="dest_group_cn", + label="For People in Group") + + orig_acistr = widgets.HiddenField(name="orig_acistr") + + attrs = widgets.CheckBoxList(name="attrs", label="Can Modify", + options=aci_checkbox_attrs, validator=validators.NotEmpty) + +class DelegateValidator(validators.Schema): + name = validators.String(not_empty=True) + source_group_dn = validators.String(not_empty=True, + messages = { 'empty': _("Please choose a group"), }) + dest_group_dn = validators.String(not_empty=True, + messages = { 'empty': _("Please choose a group"), }) + # There is no attrs validator here because then it shows as one + # huge block of color in the form. The validation is done in + # the subcontroller. + +class DelegateForm(widgets.Form): + params = ['delegate_fields', 'attr_list'] + + hidden_fields = [ + DelegateFields.source_group_dn, + DelegateFields.dest_group_dn, + DelegateFields.source_group_cn, + DelegateFields.dest_group_cn, + DelegateFields.orig_acistr, + ] + + validator = DelegateValidator() + + def __init__(self, *args, **kw): + super(DelegateForm,self).__init__(*args, **kw) + (self.template_c, self.template) = ipahelper.load_template( + "ipagui.templates.delegateform") + self.delegate_fields = DelegateFields + + def update_params(self, params): + super(DelegateForm,self).update_params(params) diff --git a/ipa-server/ipa-gui/ipagui/forms/group.py b/ipa-server/ipa-gui/ipagui/forms/group.py new file mode 100644 index 00000000..564e577a --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/group.py @@ -0,0 +1,89 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import turbogears +from turbogears import validators, widgets +from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm +from ipagui.helpers import ipahelper,validators + +class GroupFields(object): + cn = widgets.TextField(name="cn", label="Name") + gidnumber = widgets.TextField(name="gidnumber", label="GID") + description = widgets.TextField(name="description", label="Description") + + 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") + +class GroupNewValidator(validators.Schema): + filter_extra_fields = True + allow_extra_fields = True + cn = validators.GoodName(not_empty=True) + description = validators.String(not_empty=False) + + +class GroupNewForm(widgets.Form): + params = ['group_fields'] + + hidden_fields = [ + GroupFields.dn_to_info_json + ] + + validator = GroupNewValidator() + + def __init__(self, *args, **kw): + super(GroupNewForm,self).__init__(*args, **kw) + (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.groupnewform") + self.group_fields = GroupFields + + def update_params(self, params): + super(GroupNewForm,self).update_params(params) + + +class GroupEditValidator(validators.Schema): + filter_extra_fields = True + allow_extra_fields = True + cn = validators.GoodName(not_empty=False) + gidnumber = validators.Int(not_empty=False) + description = validators.String(not_empty=False) + + pre_validators = [ + validators.RequireIfPresent(required='cn', present='editprotected'), + validators.RequireIfPresent(required='gidnumber', present='editprotected'), + ] + +class GroupEditForm(widgets.Form): + params = ['members', 'group_fields'] + + hidden_fields = [ + GroupFields.editprotected_hidden, + GroupFields.group_orig, GroupFields.member_data, + GroupFields.dn_to_info_json + ] + + validator = GroupEditValidator() + + def __init__(self, *args, **kw): + super(GroupEditForm,self).__init__(*args, **kw) + (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.groupeditform") + self.group_fields = GroupFields 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 00000000..7ad9fe08 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py @@ -0,0 +1,87 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import turbogears +from turbogears import validators, widgets +from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm +from ipagui.helpers import ipahelper + +class IPAPolicyFields(object): + # From cn=ipaConfig + ipausersearchfields = widgets.TextField(name="ipausersearchfields", label="User Search Fields", attrs=dict(size=50)) + 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 User 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)) + ipauserobjectclasses = widgets.TextField(name="ipauserobjectclasses", label="Default User Object Classes", attrs=dict(size=50)) + userobjectclasses = ExpandingForm(name="userobjectclasses", label="Default User Object Classes", fields=[ipauserobjectclasses]) + ipagroupobjectclasses = widgets.TextField(name="ipagroupobjectclasses", label="Default Group Object Classes", attrs=dict(size=50)) + groupobjectclasses = ExpandingForm(name="groupobjectclasses", label="Default Group Object Classes", fields=[ipagroupobjectclasses]) + ipadefaultemaildomain = widgets.TextField(name="ipadefaultemaildomain", label="Default E-mail Domain", attrs=dict(size=20)) + + ipapolicy_orig = widgets.HiddenField(name="ipapolicy_orig") + + # From cn=accounts + krbmaxpwdlife = widgets.TextField(name="krbmaxpwdlife", label="Max. Password Lifetime (days)", attrs=dict(size=3,maxlength=3)) + krbminpwdlife = widgets.TextField(name="krbminpwdlife", label="Min. Password Lifetime (hours)", 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) + ipauserobjectclasses = validators.ForEach(validators.String(not_empty=True)) + ipagroupobjectclasses = validators.ForEach(validators.String(not_empty=True)) + ipadefaultemaildomain = 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) = ipahelper.load_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/principal.py b/ipa-server/ipa-gui/ipagui/forms/principal.py new file mode 100644 index 00000000..8ff5eed0 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/principal.py @@ -0,0 +1,55 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import turbogears +from turbogears import validators, widgets +from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm +from ipagui.helpers import ipahelper + +class PrincipalFields(object): + hostname = widgets.TextField(name="hostname", label="Host Name") + service = widgets.SingleSelectField(name="service", + label="Service Type", + options = [ + ("cifs", "cifs"), + ("dns", "dns"), + ("host", "host"), + ("HTTP", "HTTP"), + ("ldap", "ldap"), + ("nfs", "nfs"), + ("other", "other") + ], + attrs=dict(onchange="toggleOther(this.id)")) + other = widgets.TextField(name="other", label="Other Service", attrs=dict(size=10)) + +class PrincipalNewValidator(validators.Schema): + hostname = validators.String(not_empty=True) + service = validators.String(not_empty=True) + other = validators.String(not_empty=False) + +class PrincipalNewForm(widgets.Form): + params = ['principal_fields'] + + validator = PrincipalNewValidator() + + def __init__(self, *args, **kw): + super(PrincipalNewForm,self).__init__(*args, **kw) + (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.principalnewform") + self.principal_fields = PrincipalFields + + def update_params(self, params): + super(PrincipalNewForm,self).update_params(params) diff --git a/ipa-server/ipa-gui/ipagui/forms/user.py b/ipa-server/ipa-gui/ipagui/forms/user.py new file mode 100644 index 00000000..62fc0dfd --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/user.py @@ -0,0 +1,207 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import turbogears +from turbogears import validators, widgets +from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm +from ipagui.helpers.validators import * +from ipagui.helpers import ipahelper + +class UserFields(object): + givenname = widgets.TextField(name="givenname", label="First Name") + sn = widgets.TextField(name="sn", label="Last Name") + cn = widgets.TextField(name="cn", label="Full Name") + cns = ExpandingForm(name="cns", label="Full Name", fields=[cn]) + title = widgets.TextField(name="title", label="Job Title") + displayname = widgets.TextField(name="displayname", label="Display Name") + initials = widgets.TextField(name="initials", label="Initials") + + uid = widgets.TextField(name="uid", label="Login", attrs=dict(onchange="warnRDN(this.id)")) + krbprincipalkey = widgets.PasswordField(name="krbprincipalkey", label="Password") + krbprincipalkey_confirm = widgets.PasswordField(name="krbprincipalkey_confirm", + label="Confirm Password") + uidnumber = widgets.TextField(name="uidnumber", label="UID") + gidnumber = widgets.TextField(name="gidnumber", label="GID") + homedirectory = widgets.TextField(name="homedirectory", label="Home Directory") + loginshell = widgets.TextField(name="loginshell", label="Login Shell") + gecos = widgets.TextField(name="gecos", label="GECOS") + + 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") + st = widgets.TextField(name="st", label="State") + postalcode = widgets.TextField(name="postalcode", label="ZIP") + + ou = widgets.TextField(name="ou", label="Org Unit") + businesscategory = widgets.TextField(name="businesscategory", label="Tags") + description = widgets.TextField(name="description", label="Description") + employeetype = widgets.TextField(name="employeetype", label="Employee Type") + manager = widgets.HiddenField(name="manager", label="Manager") + manager_cn = widgets.HiddenField(name="manager_cn", label="Manager") + roomnumber = widgets.TextField(name="roomnumber", label="Room Number") + secretary = widgets.HiddenField(name="secretary", label="Secretary") + secretary_cn = widgets.HiddenField(name="secretary_cn", label="Manager") + + carlicense = widgets.TextField(name="carlicense", label="Car License") + labeleduri = widgets.TextField(name="labeleduri", label="Home Page") + + nsAccountLock = widgets.SingleSelectField(name="nsAccountLock", + label="Account Status", + options = [("", "active"), ("true", "inactive")]) + + uid_hidden = widgets.HiddenField(name="uid_hidden") + krbPasswordExpiration_hidden = widgets.HiddenField(name="krbPasswordExpiration") + editprotected_hidden = widgets.HiddenField(name="editprotected") + + user_orig = widgets.HiddenField(name="user_orig") + user_groups_data = widgets.HiddenField(name="user_groups_data") + dn_to_info_json = widgets.HiddenField(name="dn_to_info_json") + + custom_fields = [] + +class UserNewValidator(validators.Schema): + uid = GoodName(not_empty=True) + krbprincipalkey = validators.String(not_empty=False) + krbprincipalkey_confirm = validators.String(not_empty=False) + givenname = validators.String(not_empty=True) + sn = validators.String(not_empty=True) + cn = UniqueList(not_empty=True) + mail = validators.Email(not_empty=False) + telephonenumber = UniqueList(not_empty=False) + facsimiletelephonenumber = UniqueList(not_empty=False) + mobile = UniqueList(not_empty=False) + pager = UniqueList(not_empty=False) + homephone = UniqueList(not_empty=False) + + chained_validators = [ + validators.FieldsMatch('krbprincipalkey', 'krbprincipalkey_confirm') + ] + + +class UserNewForm(widgets.Form): + params = ['user_fields', 'custom_fields'] + + hidden_fields = [ + UserFields.dn_to_info_json, + UserFields.manager, + UserFields.manager_cn, + UserFields.secretary, + UserFields.secretary_cn, + ] + + custom_fields = [] + + validator = UserNewValidator() + + def __init__(self, *args, **kw): + super(UserNewForm,self).__init__(*args, **kw) + (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.usernewform") + + self.user_fields = UserFields + + def update_params(self, params): + super(UserNewForm,self).update_params(params) + +class UserEditValidator(validators.Schema): + uid = GoodName(not_empty=False) + krbprincipalkey = validators.String(not_empty=False) + krbprincipalkey_confirm = validators.String(not_empty=False) + givenname = validators.String(not_empty=True) + sn = validators.String(not_empty=True) + cn = UniqueList(not_empty=True) + mail = validators.Email(not_empty=False) + uidnumber = validators.Int(not_empty=False) + gidnumber = validators.Int(not_empty=False) + telephonenumber = UniqueList(not_empty=False) + facsimiletelephonenumber = UniqueList(not_empty=False) + mobile = UniqueList(not_empty=False) + pager = UniqueList(not_empty=False) + homephone = UniqueList(not_empty=False) + + pre_validators = [ + validators.RequireIfPresent(required='uid', present='editprotected'), + validators.RequireIfPresent(required='uidnumber', present='editprotected'), + validators.RequireIfPresent(required='gidnumber', present='editprotected'), + ] + + chained_validators = [ + validators.FieldsMatch('krbprincipalkey', 'krbprincipalkey_confirm') + ] + +class UserEditForm(widgets.Form): + params = ['user_fields', 'custom_fields'] + + hidden_fields = [ + UserFields.uid_hidden, UserFields.user_orig, + UserFields.krbPasswordExpiration_hidden, + UserFields.editprotected_hidden, + UserFields.user_groups_data, + UserFields.dn_to_info_json, + UserFields.manager, + UserFields.manager_cn, + UserFields.secretary, + UserFields.secretary_cn, + ] + + custom_fields = [] + + validator = UserEditValidator() + + def __init__(self, *args, **kw): + super(UserEditForm,self).__init__(*args, **kw) + (self.template_c, self.template) = ipahelper.load_template("ipagui.templates.usereditform") + + self.user_fields = UserFields + + +# TODO - add dynamic field retrieval: +# myfields=[] +# schema = ipa.rpcclient.get_add_schema () +# +# # FIXME: What if schema is None or an error is thrown? +# +# for s in schema: +# required=False +# +# if (s['type'] == "text"): +# field = widgets.TextField(name=s['name'],label=s['label']) +# elif (s['type'] == "password"): +# field = widgets.PasswordField(name=s['name'],label=s['label']) +# +# if (s['required'] == "true"): +# required=True +# +# if (s['validator'] == "text"): +# field.validator=validators.PlainText(not_empty=required) +# elif (s['validator'] == "email"): +# field.validator=validators.Email(not_empty=required) +# elif (s['validator'] == "string"): +# field.validator=validators.String(not_empty=required) +# +# myfields.append(field) diff --git a/ipa-server/ipa-gui/ipagui/helpers/Makefile.am b/ipa-server/ipa-gui/ipagui/helpers/Makefile.am new file mode 100644 index 00000000..46185b09 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/helpers/Makefile.am @@ -0,0 +1,17 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/helpers +app_PYTHON = \ + __init__.py \ + ipahelper.py \ + userhelper.py \ + validators.py \ + $(NULL) + +EXTRA_DIST = \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/helpers/__init__.py b/ipa-server/ipa-gui/ipagui/helpers/__init__.py new file mode 100644 index 00000000..143f486c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/helpers/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py b/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py new file mode 100644 index 00000000..9b340483 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py @@ -0,0 +1,88 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import re +import logging +import turbogears +import kid +from turbokid import kidsupport +from pkg_resources import resource_filename + +def javascript_string_escape(input): + """Escapes the ' " and \ characters in a string so + it can be embedded inside a dynamically generated string.""" + + return re.sub(r'[\'\"\\]', + lambda match: "\\%s" % match.group(), + input) + +def setup_mv_fields(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: + for v in field: + if v: + mvlist.append({ fieldname : v } ) + if len(mvlist) == 0: + # 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(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] = [] + try: + for i in range(len(fields[multifieldname])): + if fields[multifieldname][i][fieldname] is not None and len(fields[multifieldname][i][fieldname]) > 0: + fields[fieldname].append(fields[multifieldname][i][fieldname]) + del(fields[multifieldname]) + except Exception, e: + logging.warn("fix_incoming_fields error: " + str(e)) + + return fields + +def load_template(classname, encoding=None): + """ + Loads the given template. This only handles .kid files. + Returns a tuple (compiled_tmpl, None) to emulate + turbogears.meta.load_kid_template() which ends up not properly handling + encoding. + """ + if not encoding: + encoding = turbogears.config.get('kid.encoding', kidsupport.KidSupport.assume_encoding) + divider = classname.rfind(".") + package, basename = classname[:divider], classname[divider+1:] + file_path = resource_filename(package, basename + ".kid") + + tclass = kid.load_template( + file_path, + name = classname, + ).Template + tclass.serializer = kid.HTMLSerializer(encoding=encoding) + tclass.assume_encoding=encoding + + return (tclass, None) diff --git a/ipa-server/ipa-gui/ipagui/helpers/userhelper.py b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py new file mode 100644 index 00000000..d80c4d3a --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py @@ -0,0 +1,46 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import sys +import datetime + +from ipa import ipautil + +def password_expires_in(datestr): + """Returns the number of days that password expires in. Returns a negative number + if the password is already expired.""" + if (datestr == None) or (datestr == ""): + return sys.maxint + + expdate = ipautil.parse_generalized_time(datestr) + if not expdate: + return sys.maxint + + delta = expdate - datetime.datetime.now(ipautil.GeneralizedTimeZone()) + return delta.days + +def password_is_expired(days): + return days < 0 + +def password_expires_soon(days): + return (not password_is_expired(days)) and (days < 7) + +def account_status_display(status): + if status == "true": + return "inactive" + else: + return "active" diff --git a/ipa-server/ipa-gui/ipagui/helpers/validators.py b/ipa-server/ipa-gui/ipagui/helpers/validators.py new file mode 100644 index 00000000..8ed73b87 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/helpers/validators.py @@ -0,0 +1,92 @@ +# Copyright (C) 2007-2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +from formencode.validators import * +from formencode.compound import * +from formencode.api import Invalid, NoDefault +from formencode.schema import Schema +from formencode import ForEach + +def _(s): return s # dummy + +class UniqueList(FancyValidator): + """ + Given a list, ensure that all of the values in it are unique. + + >>> x = UniqueList() + >>> x.validate_python(['1','1'],'') + Traceback (most recent call last): + ... + formencode.api.Invalid: Duplicate values are not allowed + >>> x.validate_python(['1','2'],'') + >>> + """ + + not_empty = None + + messages = { + 'notunique': _('Duplicate values are not allowed'), + 'empty': _('Empty values not allowed'), + } + + def __initargs__(self, new_attrs): + if self.not_empty is None: + self.not_empty = True + + def validate_python(self, value, state): + if not isinstance(value, list): + return # just punt for now + + if self.not_empty: + for v in value: + if v is None or len(v) == 0: + raise Invalid(self.message('empty', state), + value, state) + + orig = len(value) + check = len(set(value)) + + if orig > check: + raise Invalid(self.message('notunique', state), + value, state) + +class GoodName(Regex): + """ + Test that the field contains only letters, numbers, underscore, + dash, hyphen and $. + + Examples:: + + >>> GoodName.to_python('_this9_') + '_this9_' + >>> GoodName.from_python(' this ') + ' this ' + >>> GoodName(accept_python=False).from_python(' this ') + Traceback (most recent call last): + ... + Invalid: Enter only letters, numbers, _ (underscore), - (dash) or $') + >>> GoodName(strip=True).to_python(' this ') + 'this' + >>> GoodName(strip=True).from_python(' this ') + 'this' + """ + + regex = r"^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,30}[a-zA-Z0-9_.$-]?$" + + messages = { + 'invalid': _('Enter only letters, numbers, _ (underscore), - (dash) or $'), + } diff --git a/ipa-server/ipa-gui/ipagui/json.py b/ipa-server/ipa-gui/ipagui/json.py new file mode 100644 index 00000000..6d912178 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/json.py @@ -0,0 +1,27 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# A JSON-based API(view) for your app. +# Most rules would look like: +# @jsonify.when("isinstance(obj, YourClass)") +# def jsonify_yourclass(obj): +# return [obj.val1, obj.val2] +# @jsonify can convert your objects to following types: +# lists, dicts, numbers and strings + +from turbojson.jsonify import jsonify + diff --git a/ipa-server/ipa-gui/ipagui/model.py b/ipa-server/ipa-gui/ipagui/model.py new file mode 100644 index 00000000..49820c39 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/model.py @@ -0,0 +1,26 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +from turbogears.database import PackageHub +from sqlobject import * + +hub = PackageHub('ipagui') +__connection__ = hub + +# class YourDataClass(SQLObject): +# pass + diff --git a/ipa-server/ipa-gui/ipagui/proxyprovider.py b/ipa-server/ipa-gui/ipagui/proxyprovider.py new file mode 100644 index 00000000..90257d39 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/proxyprovider.py @@ -0,0 +1,176 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +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 +import ldap +import krbV + +log = logging.getLogger("turbogears.identity") + +class IPA_User(object): + ''' + Shell of a User definition. We don't really need much here. + ''' + + def __init__(self, user_name): + self.user_name = user_name + (principal, realm) = user_name.split('@') + self.permissions = None + transport = funcs.IPAServer() + client = ipa.ipaclient.IPAClient(transport) + client.set_krbccache(os.environ["KRB5CCNAME"]) + try: + # Use memberof so we can see recursive group memberships as well. + user = client.get_user_by_principal(user_name, ['dn', 'uid', 'memberof']) + self.display_name = user.getValue('uid') + self.groups = [] + memberof = user.getValues('memberof') + if memberof is None: + # the user isn't in any groups + return + if isinstance(memberof, str): + memberof = [memberof] + for mo in memberof: + rdn_list = ldap.explode_dn(mo, 0) + first_rdn = rdn_list[0] + (type,value) = first_rdn.split('=') + if type == "cn": + self.groups.append(value) + except: + raise + + return + +class ProxyIdentity(object): + def __init__(self, visit_key, user=None): + self._user= user + self.visit_key= visit_key + + def _get_user(self): + try: + return self._user + except AttributeError: + # User hasn't already been set + return None + user= property(_get_user) + + def _get_user_name(self): + if not self._user: + return None + return self._user.user_name + user_name= property(_get_user_name) + + def _get_display_name(self): + if not self._user: + return None + return self._user.display_name + display_name= property(_get_display_name) + + def _get_anonymous(self): + return not self._user + anonymous= property(_get_anonymous) + + def _get_permissions(self): + try: + return self._permissions + except AttributeError: + # Permissions haven't been computed yet + return None + permissions= property(_get_permissions) + + def _get_groups(self): + try: + return self._user.groups + except AttributeError: + # Groups haven't been computed yet + return [] + groups= property(_get_groups) + + def logout(self): + ''' + Remove the link between this identity and the visit. + ''' + # Clear the current identity + anon= ProxyObjectIdentity(None,None) + #XXX if user is None anonymous will be true, no need to set attr. + #anon.anonymous= True + identity.set_current_identity( anon ) + +class ProxyIdentityProvider(SqlObjectIdentityProvider): + ''' + IdentityProvider that uses REMOTE_USER from Apache + ''' + def __init__(self): + super(ProxyIdentityProvider, self).__init__() + get = turbogears.config.get + # We can get any config variables here + log.info( "Proxy Identity starting" ) + + def create_provider_model(self): + pass + + def validate_identity(self, user_name, password, visit_key): + try: + user = IPA_User(user_name) + log.debug( "validate_identity %s" % user_name) + return ProxyIdentity(visit_key, user) + except Exception, e: + # 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''' + return True + + def load_identity(self, visit_key): + try: + os.environ["KRB5CCNAME"] = cherrypy.request.headers['X-FORWARDED-KEYTAB'] + ccache = krbV.CCache(cherrypy.request.headers['X-FORWARDED-KEYTAB']) + user_name = ccache.principal().name +# user_name = "test@FREEIPA.ORG" +# os.environ["KRB5CCNAME"] = "FILE:/tmp/krb5cc_500" + except KeyError: + return None + except AttributeError: + return None + except krbV.Krb5Error: + return None + + set_login_attempted( True ) + return self.validate_identity( user_name, None, visit_key ) + + def anonymous_identity( self ): + ''' + This shouldn't ever happen in IPA but including it to include the + entire identity API. + ''' + return ProxyIdentity( None ) + + def authenticated_identity(self, user): + ''' + Constructs Identity object for user that has no associated visit_key. + ''' + return ProxyIdentity(None, user) diff --git a/ipa-server/ipa-gui/ipagui/proxyvisit.py b/ipa-server/ipa-gui/ipagui/proxyvisit.py new file mode 100644 index 00000000..91b20d27 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/proxyvisit.py @@ -0,0 +1,42 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +from turbogears.visit.api import BaseVisitManager, Visit +from turbogears import config + +import logging + +log = logging.getLogger("turbogears.visit.proxyvisit") + +class ProxyVisitManager(BaseVisitManager): + """Virtually empty class just so can avoid saving this stuff in a + database.""" + def __init__(self, timeout): + super(ProxyVisitManager,self).__init__(timeout) + return + + def create_model(self): + return + + def new_visit_with_key(self, visit_key): + return Visit(visit_key, True) + + def visit_for_key(self, visit_key): + return Visit(visit_key, False) + + def update_queued_visits(self, queue): + return None diff --git a/ipa-server/ipa-gui/ipagui/release.py b/ipa-server/ipa-gui/ipagui/release.py new file mode 100644 index 00000000..f5bc211c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/release.py @@ -0,0 +1,16 @@ +# Release information about ipa-gui + +version = "1.0" + +# NOTE: We aren't really using this because we aren't shipping the UI as +# a separate .egg but it might look something like this: + +# description = "The Identity, Policy and Audit system" +# long_description = "IPA is an integrated solution to provide centrally managed Identity (machine, user, virtual machines, groups, authentication credentials), Policy (configuration settings, access control information) and Audit (events, logs, analysis thereof)." +# author = "Your Name Here" +# email = "YourEmail@YourDomain" +# copyright = "2007 Red Hat, Inc." + +# url = "http://www.freeipa.org/" +# download_url = "http://www.freeipa.org/page/Downloads" +# license = "GPLv2" diff --git a/ipa-server/ipa-gui/ipagui/static/Makefile.am b/ipa-server/ipa-gui/ipagui/static/Makefile.am new file mode 100644 index 00000000..d4ca4557 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/Makefile.am @@ -0,0 +1,12 @@ +NULL = + +SUBDIRS = \ + css \ + images \ + javascript \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/static/css/Makefile.am b/ipa-server/ipa-gui/ipagui/static/css/Makefile.am new file mode 100644 index 00000000..eb5502ab --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/css/Makefile.am @@ -0,0 +1,17 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/static/css +app_DATA = \ + style_freeipa.css \ + style_platform.css \ + style_platform-objects.css \ + $(NULL) + +EXTRA_DIST = \ + $(app_DATA) \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/static/css/style_freeipa.css b/ipa-server/ipa-gui/ipagui/static/css/style_freeipa.css new file mode 100644 index 00000000..f56971bd --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/css/style_freeipa.css @@ -0,0 +1,62 @@ +/* freeipa-specific styles */ + +#login { + float: right; + padding-top: 15px; + padding-right: 10px; +} + +#details { + border-top: 1px solid #bbdc5f; +} + +#details h1 { + background-repeat: no-repeat; + margin-bottom: 18px; +} + + +#alertbox { + background-color: #6995d5; +} + +#footer { + padding-top: 0px; + border-top: none; + text-align: center; + margin-left: auto; + margin-right: auto; + width: 30%; + padding: 20px 20px; +} + +/*** TableKit CSS - see http://www.millstream.com.au/view/code/tablekit/ **/ + +.sortcol { + cursor: pointer; + padding-left: 10px !important; + background-repeat: no-repeat !important; + background-position: left center !important; + text-decoration: underline; +} +.sortasc { + background-image: url(/ipa/ui/static/images/up.gif) !important; +} +.sortdesc { + background-image: url(/ipa/ui/static/images/down.gif) !important; +} + +.warning_message { + font-size: 120%; + font-weight: bolder; +} + +.fielderror { + color: red !important; + font-weight: bold; +} + +.requiredfield { + background-color: #eebbbb !important; +} + diff --git a/ipa-server/ipa-gui/ipagui/static/css/style_platform-objects.css b/ipa-server/ipa-gui/ipagui/static/css/style_platform-objects.css new file mode 100644 index 00000000..51d57089 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/css/style_platform-objects.css @@ -0,0 +1,19 @@ +/* object h1 styles */ + +#details h1.overview { background-image: url('/ipa/ui/static/images/objects/object-overview.png'); } + +#details h1.accesscontrol { background-image: url('/ipa/ui/static/images/objects/object-accesscontrol.png'); } + +#details h1.user { background-image: url('/ipa/ui/static/images/objects/object-user.png'); } +#details h1.usergroup { background-image: url('/ipa/ui/static/images/objects/object-usergroup.png'); } + +#details h1.content-overview { background-image: url('/ipa/ui/static/images/objects/object-content.png'); } +#details h1.channel { background-image: url('/ipa/ui/static/images/objects/object-channel.png'); } +#details h1.channel-new { background-image: url('/ipa/ui/static/images/objects/object-channel.png'); } +#details h1.channels { background-image: url('/ipa/ui/static/images/objects/object-channels.png'); } +#details h1.media { background-image: url('/ipa/ui/static/images/objects/object-media.png'); } + +#details h1.system { background-image: url('/ipa/ui/static/images/objects/object-system.png'); } +#details h1.virtualsystem { background-image: url('/ipa/ui/static/images/objects/object-virtualsystem.png'); } + +#details h1.policy { background-image: url('/ipa/ui/static/images/objects/object-policy.png'); } diff --git a/ipa-server/ipa-gui/ipagui/static/css/style_platform.css b/ipa-server/ipa-gui/ipagui/static/css/style_platform.css new file mode 100644 index 00000000..03ac52d3 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/css/style_platform.css @@ -0,0 +1,517 @@ +/** BASIC PAGE STYLES */ + +* +{ + margin: 0; + padding: 0; + font-size: small; +} + +html, body { + height: 100%; +} + +body { + background-image: url('/ipa/ui/static/images/template/background.png'); + background-repeat: repeat-x; + background-color: #f9f9f9; + margin: 0px; + padding: 0px; + padding-top: 16px; + min-width: 750px; +} + +body, h1, h2, h3, h4, h5, p, ul, li, div, span, td { + font-family: "Luxi Sans", "Gill Sans", "Verdana", "Helvetica", sans-serif; + font-size: small; + color: #444; +} + +td, th { + text-align: left; +} + +#head { + margin: 0px; + padding: 0px 1.5ex; +} + +#head h1 a { + display: block; + text-indent: -9999px; + height: 60px; + width: 350px; + overflow: hidden; + float: left; + margin-top: -10px; + + background: url('/ipa/ui/static/images/branding/logo.png') no-repeat; +} + +#content { + width: 100%; + min-height: 100%; + + background-color: #f9f9f9; + background-image: url('/ipa/ui/static/images/template/background-content.png'); + background-repeat: repeat-x; +} + +#main_content table { + clear: left; +} + +#main_content { + height: auto; + margin-bottom: 4ex; +} + +#footer { + font-size: x-small; + color: #ccc; + clear: both; + text-align: center; + padding-top: 4ex; + border-top: 1px solid #efefef; + width: 100%; +} + +/* freeipa only? */ +div#search { + padding-top: 16px; + padding-bottom: 24px; +} + +#searchbar { + float: right; + margin-top: 18px; +} + +/** MAIN NAVBAR SECTION **/ + +#navbar { + width: 100%; + height: 70px; + margin: 0px; + clear: both; + + background-image: url('/ipa/ui/static/images/template/background-navbar.png'); + background-repeat: repeat-x; + +} + +#navbar ul { + margin: 0px; + padding: 0px; + padding-left: 10px; + list-style: none; +} + +#navbar li { + float: left; + margin: 0px; + padding: 0px; + + font-size: small; +} + +#navbar a { + display: block; + margin: 22px 15px; +} + +#navbar .active { + background-image: url('/ipa/ui/static/images/template/background-navbar-active.png'); + height: 70px; + width: 116px; + + text-align: center; +} + +#navbar-secondary li { + font-size: medium; +} + +#navbar-secondary .active a:link, +#navbar-secondary .active a:visited, +#navbar-secondary .active a:active, +#navbar-secondary .active a, +#navbar-secondary li +{ + color: #555 !important; + text-decoration: none; + font-weight: bold; +} + +/** SIDEBAR SECTION **/ + +#sidebar { + width: 250px; + text-align: left; + + padding: 18px 12px; + margin-right: 24px; + float: right; + height: 100%; + + border: 1px solid #aaa; + background-color: #ccc; + background-image: url('/ipa/ui/static/images/template/background-sidebar.png'); + background-repeat: repeat-y; +} + +#sidebar h1, h2, h3 { + padding: 0px; + margin: 0px; +} + +#sidebar h2 { font-size: medium; } +#sidebar h3 { font-size: small; } + +#sidebar ul { + padding: 0px; + margin: 0px; + list-style: none; + padding-bottom: 10px; +} + +#sidebar ul, #sidebar li { + margin-bottom: 6px; + font-size: small; +} + +#sidebar hr { + border-top: 1px solid #aaa; + border-bottom: 1px solid #ddd; + color: #ddd; + margin-top: 20px !important; + margin-bottom: 20px !important; +} + +.context-tools { + float: right; + margin-top: -1.2em; + font-size: small; +} + +.context-tools a:link, .context-tools a:active, .context-tools a:visited { + text-decoration: none; +} + +/** DETAILS SECTION **/ + +#details { + height: 100%; + margin: 0px 24px; + margin-right: 298px; + + padding: 18px 18px; + padding-bottom: 12%; + border-top: 1px solid #aaa; + background-color: white; + text-align: left; + color: #444; +} + +#details p { + margin-top: 1ex; + margin-bottom: 1ex; +} + +#details h3 { + font-size: medium; + text-transform: uppercase; + margin-bottom: 1ex; + margin-top: 1.5ex; +} + +#details h4 { + font-size: medium; + color: #8aa445; +} + + +#details p, +#details td, +#details li { + font-size: small; + color: #555; +} + +#details h1 { + color: #7d7d5b; + font-size: x-large; + margin-bottom: 18px; + height: 40px; + padding-left: 48px; + padding-top: 6px; + vertical-align: middle; + background-repeat: no-repeat; +} + + +#details h2, #details table caption { + color: #999; + font-size: large; + font-weight: normal; + + border-bottom: 1px solid #999; + margin-bottom: 10px; + + text-align: left; + width: 100%; +} + +#details h2 img { + margin-right: 1.4ex; +} + +table.details { + margin-bottom: 18px; + width: 100%; +} + +#details h3, table.formtable th { + font-size: small; + color: black; +} + +#details table.details th { + font-size: small; + width: 150px; + padding: 4px 0px; + padding-right: 8px; + border-bottom: 1px dotted #ddd; +} + +#details table.details th.even { background-color: white; } +#details table.details th.odd { background-color: #eee; } + +#details table.details td { + padding-left: 8px; + padding-bottom: 3px; + border-bottom: 1px dotted #ddd; +} + +#details hr { + margin-top: 48px; + margin-bottom: 12px; + height: 1px; + border-color: #bbb; + border-width: 0pt 0pt 1px; + padding: 0.5em; + border-style: none none dashed; +} + +.details-block { + border-top: 1px solid #eeeeee; +} + +#details ul.context-nav { + float: left; + width: 100%; + padding: 0; + margin: 0; + list-style-type: none; + border-bottom: 6px solid #eee; + margin-bottom: 2ex; +} + +#details ul.context-nav li a { + float: left; + text-decoration: none; + background-color: #d6d6d6; + padding: 1ex 2ex; + text-align: center; + margin-right: 3px; + + -moz-border-radius-topleft: 12px; + -moz-border-radius-topright: 12px; +} + +#details ul.context-nav li#active a { + color: #444; + background-color: #eee; + font-weight: bold; +} + +/** FORMS SECTION **/ + +input.text { + border: 1px solid #8e8e8e; + background-color: #e5f1f4; + color: #444444; +} + +input.submitbutton { + float: right; +} + +form.tableform table th { + padding-right: 2ex; + text-align: right; +} + +h2.formsection { + color: #999; + font-size: large; + font-weight: normal; + + border-bottom: 1px solid #999; + margin-bottom: 10px; + margin-top: 12px; + + text-align: left; + width: 100%; +} + +table.formtable { + width: 100%; +} + +/**** freeipa only below? ****/ +ul.checkboxlist li { + list-style: none; + margin: 8px 0px; +} + +ul.checkboxlist li input { + background-color: yellow; + height: 1.1em; + width: 1.2em; + border: 1px solid red; +} + +table.formtable th, table.formtable td { + vertical-align: top; + padding-bottom: 10px; +} + +table.formtable th { + width: 28%; +} + +input.submitbutton, input.searchbutton, #source_searcharea input.searchbutton { + border: 1px outset #aaa; + padding: 2px 1px; + margin-bottom: 2px; +} + +table.formtable td input[type="text"], input#criteria { + border: 1px inset #dcdcdc; + font-size: medium; + padding: 2px 1px; +} + +table.formtable td select { + border: 1px inset #dcdcdc; + font-size: small; + padding: 2px 1px; +} + +#inactive { + background-color: silver; +} + +/** ALERTS / MESSAGING SECTION **/ + +#alertbox { + width: 100%; + padding: 10px 0px; + margin-top: 12px; + margin-bottom: 18px; + vertical-align: middle; + + -moz-border-radius: 6px; + background-color: #7d7d5b; + color: white; +} + +#alertbox h2 { + width: auto; + padding: 0px 16px; + float: left; + font-size: medium; + text-transform: uppercase; + color: white; + font-weight: bold; + border: none; +} + +#alertbox p { + padding: 0px 16px; + text-align: center; + color: white; + width: auto; +} + +/**** freeipa only alerts/messaging below ****/ + +p.empty-message { + font-size: large; + font-style: italic; + color: #888 !important; +} + +div.instructions { + padding: 2px 6px; + margin-top: 16px; + border-top: 1px solid #c0d5f1; + border-bottom: 1px solid #c0d5f1; + background-color: #eef4fd; +} + + +/** OVERVIEW PAGE STYLES **/ + +.summary { + width: 40%; + float: left; + clear: none; + padding-top: 2ex; +} + +.tasks, .search { + padding: 3ex; + padding-top: 2ex; + width: 40%; + float: right; + background-image: url('/ipa/ui/static/images/template/background-search.png'); + background-repeat: repeat-y; + background-color: white; +} + +.summary ul, .tasks ul, ul { margin-top: 1ex; padding-top: 1ex; list-style: square; margin-left: 2ex; } +.summary ul + ul, .tasks ul + ul { border-top: 1px solid #eee; } +.search ul { list-style: none; margin-left: 2ex; } + +.additional-link { + font-size: x-small; +} + +#perspectives h3 a { + font-size: x-small; + text-transform: none; + margin-left: 1ex; + font-weight: normal; +} + +/** TURBOGEARS GRID-TABLE-SPECIFIC STYLES **/ + +.grid td, .grid th {padding:3px;border:none;} +.grid .action_cell {text-align:right;} +.grid THEAD tr th {text-align:left;background-color:#f0f0f0;color:#333;} +.grid .heading img {float:right;margin-left:2px;margin-right:3px;} +.grid .heading a {text-decoration:none;color:#333;} +.grid td a {text-decoration:none;color:#333;} +.grid tr.odd td {background-color:#edf3fe;} +.grid tr.even td {background-color:#fff;} +.grid .pointer {cursor:pointer;} +.grid .column_chooser_link {position:relative;background-color:#e3e3e3;} +.grid .column_chooser_link ul {position:absolute;display:none;top:0px;right:-20px;} +.grid .column_chooser_list a {width:200px;display:block;padding:3px;background-color:#e3e3e3;} +.grid .column_chooser_list a:hover {background-color:#cdcdcd;} +.grid .column_chooser_list {padding:0;margin:0;list-style:none;background-color:#e3e3e3;} + + + diff --git a/ipa-server/ipa-gui/ipagui/static/images/Makefile.am b/ipa-server/ipa-gui/ipagui/static/images/Makefile.am new file mode 100644 index 00000000..bb0f3aab --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/Makefile.am @@ -0,0 +1,29 @@ +NULL = + +SUBDIRS = \ + branding \ + objects \ + template \ + $(NULL) + +appdir = $(IPA_DATA_DIR)/ipagui/static/images +app_DATA = \ + down.gif \ + favicon.ico \ + header_inner.png \ + info.png \ + logo.png \ + ok.png \ + tg_under_the_hood.png \ + under_the_hood_blue.png \ + up.gif + $(NULL) + +EXTRA_DIST = \ + $(app_DATA) \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/static/images/branding/Makefile.am b/ipa-server/ipa-gui/ipagui/static/images/branding/Makefile.am new file mode 100644 index 00000000..cb0a8142 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/branding/Makefile.am @@ -0,0 +1,15 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/static/images/branding +app_DATA = \ + logo.png \ + $(NULL) + +EXTRA_DIST = \ + $(app_DATA) \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/static/images/branding/logo.png b/ipa-server/ipa-gui/ipagui/static/images/branding/logo.png Binary files differnew file mode 100644 index 00000000..ebabed79 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/branding/logo.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/down.gif b/ipa-server/ipa-gui/ipagui/static/images/down.gif Binary files differnew file mode 100644 index 00000000..c527b4e6 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/down.gif diff --git a/ipa-server/ipa-gui/ipagui/static/images/favicon.ico b/ipa-server/ipa-gui/ipagui/static/images/favicon.ico Binary files differnew file mode 100644 index 00000000..bafbff92 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/favicon.ico diff --git a/ipa-server/ipa-gui/ipagui/static/images/header_inner.png b/ipa-server/ipa-gui/ipagui/static/images/header_inner.png Binary files differnew file mode 100644 index 00000000..2b2d87d5 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/header_inner.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/info.png b/ipa-server/ipa-gui/ipagui/static/images/info.png Binary files differnew file mode 100644 index 00000000..329c523f --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/info.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/logo.png b/ipa-server/ipa-gui/ipagui/static/images/logo.png Binary files differnew file mode 100644 index 00000000..ebabed79 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/logo.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/Makefile.am b/ipa-server/ipa-gui/ipagui/static/images/objects/Makefile.am new file mode 100644 index 00000000..02e89883 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/Makefile.am @@ -0,0 +1,25 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/static/images/objects +app_DATA = \ + object-accesscontrol.png \ + object-channel.png \ + object-channels.png \ + object-content.png \ + object-media.png \ + object-overview.png \ + object-system.png \ + object-usergroup.png \ + object-user.png \ + object-virtualsystem.png \ + object-policy.png \ + $(NULL) + +EXTRA_DIST = \ + $(app_DATA) \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-accesscontrol.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-accesscontrol.png Binary files differnew file mode 100644 index 00000000..bddec41b --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-accesscontrol.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-channel.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channel.png Binary files differnew file mode 100644 index 00000000..7fd37c4e --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channel.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-channels.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channels.png Binary files differnew file mode 100644 index 00000000..7fd37c4e --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channels.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-content.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-content.png Binary files differnew file mode 100644 index 00000000..608a19e5 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-content.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-media.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-media.png Binary files differnew file mode 100644 index 00000000..0220fc05 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-media.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-overview.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-overview.png Binary files differnew file mode 100644 index 00000000..a320b9c8 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-overview.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-policy.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-policy.png Binary files differnew file mode 100644 index 00000000..eb55f453 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-policy.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-system.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-system.png Binary files differnew file mode 100644 index 00000000..82b09a5d --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-system.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-user.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-user.png Binary files differnew file mode 100644 index 00000000..bf294efd --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-user.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-usergroup.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-usergroup.png Binary files differnew file mode 100644 index 00000000..7338ad2e --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-usergroup.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/objects/object-virtualsystem.png b/ipa-server/ipa-gui/ipagui/static/images/objects/object-virtualsystem.png Binary files differnew file mode 100644 index 00000000..8f2befca --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-virtualsystem.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/ok.png b/ipa-server/ipa-gui/ipagui/static/images/ok.png Binary files differnew file mode 100644 index 00000000..fee6751c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/ok.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/Makefile.am b/ipa-server/ipa-gui/ipagui/static/images/template/Makefile.am new file mode 100644 index 00000000..a446ce63 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/template/Makefile.am @@ -0,0 +1,21 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/static/images/template +app_DATA = \ + background-content.png \ + background-navbar-active_fullsize.png \ + background-navbar-active.png \ + background-navbar_fullsize.png \ + background-navbar.png \ + background.png \ + background-sidebar.png \ + $(NULL) + +EXTRA_DIST = \ + $(app_DATA) \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-content.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-content.png Binary files differnew file mode 100644 index 00000000..082f10ae --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-content.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active.png Binary files differnew file mode 100644 index 00000000..1b088501 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active_fullsize.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active_fullsize.png Binary files differnew file mode 100644 index 00000000..756a1e61 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active_fullsize.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar.png Binary files differnew file mode 100644 index 00000000..2c6a2de4 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar_fullsize.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar_fullsize.png Binary files differnew file mode 100644 index 00000000..72a71063 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar_fullsize.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background-sidebar.png b/ipa-server/ipa-gui/ipagui/static/images/template/background-sidebar.png Binary files differnew file mode 100644 index 00000000..4eaadbbc --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-sidebar.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background.png b/ipa-server/ipa-gui/ipagui/static/images/template/background.png Binary files differnew file mode 100644 index 00000000..96ead97f --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/template/background.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/tg_under_the_hood.png b/ipa-server/ipa-gui/ipagui/static/images/tg_under_the_hood.png Binary files differnew file mode 100644 index 00000000..bc9c79cc --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/tg_under_the_hood.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/under_the_hood_blue.png b/ipa-server/ipa-gui/ipagui/static/images/under_the_hood_blue.png Binary files differnew file mode 100644 index 00000000..90e84b72 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/under_the_hood_blue.png diff --git a/ipa-server/ipa-gui/ipagui/static/images/up.gif b/ipa-server/ipa-gui/ipagui/static/images/up.gif Binary files differnew file mode 100644 index 00000000..c5ef548a --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/images/up.gif diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/Makefile.am b/ipa-server/ipa-gui/ipagui/static/javascript/Makefile.am new file mode 100644 index 00000000..a2ca2289 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/javascript/Makefile.am @@ -0,0 +1,21 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/static/javascript +app_DATA = \ + dynamicedit.js \ + dynamicselect.js \ + effects.js \ + ipautil.js \ + prototype.js \ + scriptaculous.js \ + tablekit.js \ + $(NULL) + +EXTRA_DIST = \ + $(app_DATA) \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/dynamicedit.js b/ipa-server/ipa-gui/ipagui/static/javascript/dynamicedit.js new file mode 100644 index 00000000..b670c457 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/javascript/dynamicedit.js @@ -0,0 +1,217 @@ +/** + * Copyright (C) 2007 Red Hat + * see file 'COPYING' for use and warranty information + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; version 2 only + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/** + * dynamicedit.js + * + * Shared code, data, and functions for the dynamic add/remove lists on the + * edit group/user pages. + * + * These functions have specific expectations of the page they are used on: + * + * - If you want to preserve the dn_to_info_hash on round trip: + * - The form must have a 'form_dn_to_info_json' hidden field. + * - The form must have onsubmit="preSubmit()" set in its tag. + * - Restoring the contents of add/remove lists on round trip unfortunately + * can't be shared because it is a mixture of python and javascript. See + * the bottom part editgroup.kid for example code on this. + * + * - The page must have a div: 'newmembers' + * that new members are dynamically added to. + * + * - The page must have a div: 'delmembers' + * that removed members are dynamically added to. + * + * - Hidden fields called 'dnadd' and 'dndel' will be dynamically created, + * holding the values of the 'dn' passed to addmember() and removemember() + * + * Other Notes: + * + * - Many of the fields refer to 'dn'. There is no intrinsic reason this has + * to be a dn (it can hold any "unique id" for the objects to add/remove) + * + * - Similarly, the word 'member' is used because the code was originally + * written for editgroup. A 'member' is just a 'thing' to add/remove. + * On the useredit pages, for example, a 'member' is actually a group. + */ + +// Stored as the values in the dn_to_info_hash +MemberDisplayInfo = Class.create(); +MemberDisplayInfo.prototype = { + initialize: function(name, descr, type) { + this.name = name; + this.descr = descr; + this.type = type; + } +}; + + +// this is used for round-trip recontruction of the names. +// the hidden fields only contain dns. +var dn_to_info_hash = new Hash(); + +// used to filter search results. +// records dns already in the group +var member_hash = new Hash(); + +// used to prevent double adding +// records dns to be added +var added_hash = new Hash(); + +// Tracks the div ids that each member belongs to. +// Since dn's will contain illegal characters for div ids, this is used +// to map them to the correct div +var dn_to_member_div_id = new Hash(); + + + +/* + * Renders the information about the member into the passed in + * element. This is used by addmember and removemember to + * consistently create the dom for the member information + * (name, descr) and add icons/font changes correct for each type. + */ +function renderMemberInfo(newdiv, info) { + if (info.type == "user") { + bold = document.createElement('b'); + bold.appendChild(document.createTextNode( + info.name + " " + info.descr + " ")); + newdiv.appendChild(bold); + } else if (info.type == "iuser") { + newdiv.appendChild(document.createTextNode( + info.name + " " + info.descr + " ")); + } else if (info.type == "group") { + ital = document.createElement('i'); + bold = document.createElement('b'); + ital.appendChild(bold); + bold.appendChild(document.createTextNode( + info.name + " " + + info.descr + " ")); + newdiv.appendChild(ital); + } else if (info.type == "igroup") { + ital = document.createElement('i'); + ital.appendChild(document.createTextNode( + info.name + " " + + info.descr + " ")); + newdiv.appendChild(ital); + } +} + +/* + * Callback used for afterFinish in scriptaculous effect + */ +function removeElement(effect) { + Element.remove(effect.element); +} + +function addmember(dn, info) { + dn_to_info_hash[dn] = info; + + if ((added_hash[dn] == 1) || (member_hash[dn] == 1)) { + return null; + } + added_hash[dn] = 1; + + var newdiv = document.createElement('div'); + renderMemberInfo(newdiv, info); + + var undolink = document.createElement('a'); + undolink.setAttribute('href', ''); + undolink.setAttribute('onclick', + 'new Effect.Fade(Element.up(this), {afterFinish: removeElement, duration: 0.75});' + + 'added_hash.remove("' + jsStringEscape(dn) + '");' + + 'return false;'); + undolink.appendChild(document.createTextNode("undo")); + newdiv.appendChild(undolink); + + var dnInfo = document.createElement('input'); + dnInfo.setAttribute('type', 'hidden'); + dnInfo.setAttribute('name', 'dnadd'); + dnInfo.setAttribute('value', dn); + newdiv.appendChild(dnInfo); + + newdiv.style.display = 'none'; + $('newmembers').appendChild(newdiv); + + return newdiv; +} + +function addmemberHandler(element, dn, info) { + var newdiv = addmember(dn, info); + if (newdiv != null) { + new Effect.Fade(Element.up(element), {duration: 0.75}); + new Effect.Appear(newdiv, {duration: 0.75}); + /* Element.up(element).remove(); */ + } +} + +function removemember(dn, info) { + dn_to_info_hash[dn] = info; + + var newdiv = document.createElement('div'); + renderMemberInfo(newdiv, info); + + orig_div_id = dn_to_member_div_id[dn]; + var undolink = document.createElement('a'); + undolink.setAttribute('href', ''); + undolink.setAttribute('onclick', + 'new Effect.Fade(Element.up(this), {afterFinish: removeElement, duration: 0.75});' + + "new Effect.Appear($('" + orig_div_id + "'), {duration: 0.75});" + + 'return false;'); + undolink.appendChild(document.createTextNode("undo")); + newdiv.appendChild(undolink); + + var dnInfo = document.createElement('input'); + dnInfo.setAttribute('type', 'hidden'); + dnInfo.setAttribute('name', 'dndel'); + dnInfo.setAttribute('value', dn); + newdiv.appendChild(dnInfo); + + newdiv.style.display = 'none'; + $('delmembers').appendChild(newdiv); + + return newdiv; +} + +function removememberHandler(element, dn, info) { + var newdiv = removemember(dn, info); + new Effect.Fade(Element.up(element), {duration: 0.75}); + new Effect.Appear(newdiv, {duration: 0.75}); + /* Element.up(element).remove(); */ +} + +function preSubmit() { + var json = dn_to_info_hash.toJSON(); + $('form_dn_to_info_json').value = json; + return true; +} + +function enterDoSearch(e) { + var keyPressed; + if (window.event) { + keyPressed = window.event.keyCode; + } else { + keyPressed = e.which; + } + + if (keyPressed == 13) { + return doSearch(); + } else { + return true; + } +} diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/dynamicselect.js b/ipa-server/ipa-gui/ipagui/static/javascript/dynamicselect.js new file mode 100644 index 00000000..17fdf8fe --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/javascript/dynamicselect.js @@ -0,0 +1,70 @@ +/* Copyright (C) 2007 Red Hat + * see file 'COPYING' for use and warranty information + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; version 2 only + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/** + * dynamicselect.js + * + * Shared code, data, and functions for the dynamic select lists on the + * edit user pages. + * + */ + +function enterDoSelectSearch(e, which_select) { + var keyPressed; + if (window.event) { + keyPressed = window.event.keyCode; + } else { + keyPressed = e.which; + } + + if (keyPressed == 13) { + return doSelectSearch(which_select); + } else { + return true; + } +} + +function startSelect(which_select) { + new Effect.Appear($(which_select + '_searcharea'), {duration: 0.25}); + new Effect.Fade($(which_select + '_links'), {duration: 0.25}); + return false; +} + +function doSelect(which_select, select_dn, select_cn) { + select_dn_field = $('form_' + which_select); + select_cn_field = $('form_' + which_select + '_cn'); + select_cn_span = $(which_select + '_select_cn'); + + select_dn_field.value = select_dn; + select_cn_field.value = select_cn; + select_cn_span.update(select_cn); + + new Effect.Fade($(which_select + '_searcharea'), {duration: 0.25}); + new Effect.Appear($(which_select + '_links'), {duration: 0.25}); +} + +function clearSelect(which_select) { + select_dn_field = $('form_' + which_select); + select_cn_field = $('form_' + which_select + '_cn'); + select_cn_span = $(which_select + '_select_cn'); + + select_dn_field.value = ''; + select_cn_field.value = ''; + select_cn_span.update(''); + + return false; +} diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/effects.js b/ipa-server/ipa-gui/ipagui/static/javascript/effects.js new file mode 100644 index 00000000..70d07526 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/javascript/effects.js @@ -0,0 +1,1094 @@ +// script.aculo.us effects.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007 + +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if(this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if(this.slice(0,1) == '#') { + if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if(this.length==7) color = this.toLowerCase(); + } + } + return(color.length==7 ? color : (arguments[0] || this)); +} + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +} + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +} + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; +} + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +} + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +Array.prototype.call = function() { + var args = arguments; + this.each(function(f){ f.apply(this, args) }); +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + tagifyText: function(element) { + if(typeof Builder == 'undefined') + throw("Effect.tagifyText requires including script.aculo.us' builder.js library"); + + var tagifyStyle = 'position:relative'; + if(Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || {}); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || {}); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; + return (pos > 1 ? 1 : pos); + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; + }, + pulse: function(pos, pulses) { + pulses = pulses || 5; + return ( + Math.round((pos % (1/pulses)) * pulses) == 0 ? + ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : + 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) + ); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } +}; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(); +Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = (typeof effect.options.queue == 'string') ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if(this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + for(var i=0, len=this.effects.length;i<len;i++) + this.effects[i] && this.effects[i].loop(timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if(typeof queueName != 'string') return queueName; + + if(!this.instances[queueName]) + this.instances[queueName] = new Effect.ScopedQueue(); + + return this.instances[queueName]; + } +} +Effect.Queue = Effect.Queues.get('global'); + +Effect.DefaultOptions = { + transition: Effect.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 100, // 100= assume 66fps max. + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' +} + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + start: function(options) { + function codeForEvent(options,eventName){ + return ( + (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') + + (options[eventName] ? 'this.options.'+eventName+'(this);' : '') + ); + } + if(options.transition === false) options.transition = Effect.Transitions.linear; + this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {}); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn+(this.options.duration*1000); + this.fromToDelta = this.options.to-this.options.from; + this.totalTime = this.finishOn-this.startOn; + this.totalFrames = this.options.fps*this.options.duration; + + eval('this.render = function(pos){ '+ + 'if(this.state=="idle"){this.state="running";'+ + codeForEvent(options,'beforeSetup')+ + (this.setup ? 'this.setup();':'')+ + codeForEvent(options,'afterSetup')+ + '};if(this.state=="running"){'+ + 'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+ + 'this.position=pos;'+ + codeForEvent(options,'beforeUpdate')+ + (this.update ? 'this.update(pos);':'')+ + codeForEvent(options,'afterUpdate')+ + '}}'); + + this.event('beforeStart'); + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).add(this); + }, + loop: function(timePos) { + if(timePos >= this.startOn) { + if(timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if(this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / this.totalTime, + frame = Math.round(pos * this.totalFrames); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + cancel: function() { + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if(this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + var data = $H(); + for(property in this) + if(typeof this[property] != 'function') data[property] = this[property]; + return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>'; + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if(effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Event = Class.create(); +Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), { + initialize: function() { + var options = Object.extend({ + duration: 0 + }, arguments[0] || {}); + this.start(options); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if(Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(); +Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if(this.options.mode == 'absolute') { + // absolute movement, so we need to calc deltaX and deltaY + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: Math.round(this.options.x * position + this.originalLeft) + 'px', + top: Math.round(this.options.y * position + this.originalTop) + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || {})); +}; + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || {}); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = {}; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { + if(fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if(this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if(/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if(!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = {}; + if(this.options.scaleX) d.width = Math.round(width) + 'px'; + if(this.options.scaleY) d.height = Math.round(height) + 'px'; + if(this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if(this.elementPositioning == 'absolute') { + if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if(this.options.scaleY) d.top = -topd + 'px'; + if(this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if(this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = {}; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } + if(!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if(!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + if(this.options.offset) offsets[1] += this.options.offset; + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if(effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element) + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || {}) + ); +} + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || {})); +} + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }) + } + }, arguments[1] || {})); +} + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || {})); +} + +Effect.Shake = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element).cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom}); + effect.element.down().undoPositioned(); + } + }, arguments[1] || {}) + ); +} + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } + }, options) + ) + } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } + }, options) + ); +} + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || {}; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +} + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + element.makeClipping(); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || {})); +}; + +Effect.Morph = Class.create(); +Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: {} + }, arguments[1] || {}); + if (typeof options.style == 'string') { + if(options.style.indexOf(':') == -1) { + var cssText = '', selector = '.' + options.style; + $A(document.styleSheets).reverse().each(function(styleSheet) { + if (styleSheet.cssRules) cssRules = styleSheet.cssRules; + else if (styleSheet.rules) cssRules = styleSheet.rules; + $A(cssRules).reverse().each(function(rule) { + if (selector == rule.selectorText) { + cssText = rule.style.cssText; + throw $break; + } + }); + if (cssText) throw $break; + }); + this.style = cssText.parseStyle(); + options.afterFinishInternal = function(effect){ + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + if(transform.style != 'opacity') + effect.element.style[transform.style] = ''; + }); + } + } else this.style = options.style.parseStyle(); + } else this.style = $H(options.style) + this.start(options); + }, + setup: function(){ + function parseColor(color){ + if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ) + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if(value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if(property == 'opacity') { + value = parseFloat(value); + if(Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if(Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ) + }); + }, + update: function(position) { + var style = {}, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + transform.originalValue + Math.round( + ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit; + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create(); +Object.extend(Effect.Transform.prototype, { + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || {}; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + var data = $H(track).values().first(); + this.tracks.push($H({ + ids: $H(track).keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var elements = [$(track.ids) || $$(track.ids)].flatten(); + return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.prototype.parseStyle = function(){ + var element = document.createElement('div'); + element.innerHTML = '<div style="' + this + '"></div>'; + var style = element.childNodes[0].style, styleRules = $H(); + + Element.CSS_PROPERTIES.each(function(property){ + if(style[property]) styleRules[property] = style[property]; + }); + if(Prototype.Browser.IE && this.indexOf('opacity') > -1) { + styleRules.opacity = this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]; + } + return styleRules; +}; + +Element.morph = function(element, style) { + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {})); + return element; +}; + +['getInlineOpacity','forceRerendering','setContentZoom', + 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each( + function(f) { Element.Methods[f] = Element[f]; } +); + +Element.Methods.visualEffect = function(element, effect, options) { + s = effect.dasherize().camelize(); + effect_class = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[effect_class](element, options); + return $(element); +}; + +Element.addMethods();
\ No newline at end of file diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/ipautil.js b/ipa-server/ipa-gui/ipagui/static/javascript/ipautil.js new file mode 100644 index 00000000..de747c5c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/javascript/ipautil.js @@ -0,0 +1,24 @@ +/* Copyright (C) 2007 Red Hat + * see file 'COPYING' for use and warranty information + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; version 2 only + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* + * Escapes the ' " and \ characters in a string, so + * it can be embedded inside a dynamically generated string. + */ +function jsStringEscape(input) { + return input.gsub(/(['"\\])/, function(match){ return "\\" + match[0];} ); +} diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/prototype.js b/ipa-server/ipa-gui/ipagui/static/javascript/prototype.js new file mode 100644 index 00000000..a3f21ac7 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/javascript/prototype.js @@ -0,0 +1,3277 @@ +/* Prototype JavaScript framework, version 1.5.1.1 + * (c) 2005-2007 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.5.1.1', + + Browser: { + IE: !!(window.attachEvent && !window.opera), + Opera: !!window.opera, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1 + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + (document.createElement('div').__proto__ !== + document.createElement('form').__proto__) + }, + + ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (var property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.extend(Object, { + inspect: function(object) { + try { + if (object === undefined) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + toJSON: function(object) { + var type = typeof object; + switch(type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (object.ownerDocument === document) return; + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (value !== undefined) + results.push(property.toJSON() + ': ' + value); + } + return '{' + results.join(', ') + '}'; + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({}, object); + } +}); + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + }, + + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + }, + + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; + } +}); + +Date.prototype.toJSON = function() { + return '"' + this.getFullYear() + '-' + + (this.getMonth() + 1).toPaddedString(2) + '-' + + this.getDate().toPaddedString(2) + 'T' + + this.getHours().toPaddedString(2) + ':' + + this.getMinutes().toPaddedString(2) + ':' + + this.getSeconds().toPaddedString(2) + '"'; +}; + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(this); + } finally { + this.currentlyExecuting = false; + } + } + } +} +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return this; + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : this; + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; + }, + + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return {}; + + return match[1].split(separator || '&').inject({}, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (hash[key].constructor != Array) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + }, + + toArray: function() { + return this.split(''); + }, + + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + times: function(count) { + var result = ''; + for (var i = 0; i < count; i++) result += this; + return result; + }, + + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + }, + unescapeHTML: function() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (typeof replacement == 'function') return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +} + +String.prototype.parseQuery = String.prototype.toQueryParams; + +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +with (String.prototype.escapeHTML) div.appendChild(text); + +var Template = Class.create(); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; +Template.prototype = { + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + return this.template.gsub(this.pattern, function(match) { + var before = match[1]; + if (before == '\\') return match[2]; + return before + String.interpret(object[match[3]]); + }); + } +} + +var $break = {}, $continue = new Error('"throw $continue" is deprecated, use "return" instead'); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + iterator(value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator) { + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.map(iterator); + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = false; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push((iterator || Prototype.K)(value, index)); + }); + return results; + }, + + detect: function(iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = fillWith === undefined ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.map(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#<Enumerable:' + this.toArray().inspect() + '>'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0, length = iterable.length; i < length; i++) + results.push(iterable[i]); + return results; + } +} + +if (Prototype.Browser.WebKit) { + $A = Array.from = function(iterable) { + if (!iterable) return []; + if (!(typeof iterable == 'function' && iterable == '[object NodeList]') && + iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0, length = iterable.length; i < length; i++) + results.push(iterable[i]); + return results; + } + } +} + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) + Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value && value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0, length = this.length; i < length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (value !== undefined) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } +}); + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string) { + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { + var array = []; + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (arguments[i].constructor == Array) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + } +} +var Hash = function(object) { + if (object instanceof Hash) this.merge(object); + else Object.extend(this, object || {}); +}; + +Object.extend(Hash, { + toQueryString: function(obj) { + var parts = []; + parts.add = arguments.callee.addPair; + + this.prototype._each.call(obj, function(pair) { + if (!pair.key) return; + var value = pair.value; + + if (value && typeof value == 'object') { + if (value.constructor == Array) value.each(function(value) { + parts.add(pair.key, value); + }); + return; + } + parts.add(pair.key, value); + }); + + return parts.join('&'); + }, + + toJSON: function(object) { + var results = []; + this.prototype._each.call(object, function(pair) { + var value = Object.toJSON(pair.value); + if (value !== undefined) results.push(pair.key.toJSON() + ': ' + value); + }); + return '{' + results.join(', ') + '}'; + } +}); + +Hash.toQueryString.addPair = function(key, value, prefix) { + key = encodeURIComponent(key); + if (value === undefined) this.push(key); + else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value))); +} + +Object.extend(Hash.prototype, Enumerable); +Object.extend(Hash.prototype, { + _each: function(iterator) { + for (var key in this) { + var value = this[key]; + if (value && value == Hash.prototype[key]) continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject(this, function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + remove: function() { + var result; + for(var i = 0, length = arguments.length; i < length; i++) { + var value = this[arguments[i]]; + if (value !== undefined){ + if (result === undefined) result = value; + else { + if (result.constructor != Array) result = [result]; + result.push(value) + } + } + delete this[arguments[i]]; + } + return result; + }, + + toQueryString: function() { + return Hash.toQueryString(this); + }, + + inspect: function() { + return '#<Hash:{' + this.map(function(pair) { + return pair.map(Object.inspect).join(': '); + }).join(', ') + '}>'; + }, + + toJSON: function() { + return Hash.toJSON(this); + } +}); + +function $H(object) { + if (object instanceof Hash) return object; + return new Hash(object); +}; + +// Safari iterates over shadowed properties +if (function() { + var i = 0, Test = function(value) { this.key = value }; + Test.prototype.key = 'foo'; + for (var property in new Test('bar')) i++; + return i > 1; +}()) Hash.prototype._each = function(iterator) { + var cache = []; + for (var key in this) { + var value = this[key]; + if ((value && value == Hash.prototype[key]) || cache.include(key)) continue; + cache.push(key); + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } +}; +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '' + } + Object.extend(this.options, options || {}); + + this.options.method = this.options.method.toLowerCase(); + if (typeof this.options.parameters == 'string') + this.options.parameters = this.options.parameters.toQueryParams(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + _complete: false, + + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Hash.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + if (this.options.onCreate) this.options.onCreate(this.transport); + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) + setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (typeof extras.push == 'function') + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + return !this.transport.status + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + this.transport.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + var contentType = this.getHeader('Content-type'); + if (contentType && contentType.strip(). + match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i)) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + state, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) { return null } + }, + + evalJSON: function() { + try { + var json = this.getHeader('X-JSON'); + return json ? json.evalJSON() : null; + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, param) { + this.updateContent(); + onComplete(transport, param); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.container[this.success() ? 'success' : 'failure']; + var response = this.transport.responseText; + + if (!this.options.evalScripts) response = response.stripScripts(); + + if (receiver = $(receiver)) { + if (this.options.insertion) + new this.options.insertion(receiver, response); + else + receiver.update(response); + } + + if (this.success()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (typeof element == 'string') + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(query.snapshotItem(i)); + return results; + }; + + document.getElementsByClassName = function(className, parentElement) { + var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]"; + return document._getElementsByXPath(q, parentElement); + } + +} else document.getElementsByClassName = function(className, parentElement) { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + var elements = [], child, pattern = new RegExp("(^|\\s)" + className + "(\\s|$)"); + for (var i = 0, length = children.length; i < length; i++) { + child = children[i]; + var elementClassName = child.className; + if (elementClassName.length == 0) continue; + if (elementClassName == className || elementClassName.match(pattern)) + elements.push(Element.extend(child)); + } + return elements; +}; + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) var Element = {}; + +Element.extend = function(element) { + var F = Prototype.BrowserFeatures; + if (!element || !element.tagName || element.nodeType == 3 || + element._extended || F.SpecificElementExtensions || element == window) + return element; + + var methods = {}, tagName = element.tagName, cache = Element.extend.cache, + T = Element.Methods.ByTag; + + // extend methods for all tags (Safari doesn't need this) + if (!F.ElementExtensions) { + Object.extend(methods, Element.Methods), + Object.extend(methods, Element.Methods.Simulated); + } + + // extend methods for specific tags + if (T[tagName]) Object.extend(methods, T[tagName]); + + for (var property in methods) { + var value = methods[property]; + if (typeof value == 'function' && !(property in element)) + element[property] = cache.findOrStore(value); + } + + element._extended = Prototype.emptyFunction; + return element; +}; + +Element.extend.cache = { + findOrStore: function(value) { + return this[value] = this[value] || function() { + return value.apply(null, [this].concat($A(arguments))); + } + } +}; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + $(element).style.display = 'none'; + return element; + }, + + show: function(element) { + $(element).style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, html) { + html = typeof html == 'undefined' ? '' : html.toString(); + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + replace: function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + if (element.outerHTML) { + element.outerHTML = html.stripScripts(); + } else { + var range = element.ownerDocument.createRange(); + range.selectNodeContents(element); + element.parentNode.replaceChild( + range.createContextualFragment(html.stripScripts()), element); + } + setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $A($(element).getElementsByTagName('*')).each(Element.extend); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (typeof selector == 'string') + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return expression ? Selector.findElement(ancestors, expression, index) : + ancestors[index || 0]; + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + var descendants = element.descendants(); + return expression ? Selector.findElement(descendants, expression, index) : + descendants[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return expression ? Selector.findElement(previousSiblings, expression, index) : + previousSiblings[index || 0]; + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return expression ? Selector.findElement(nextSiblings, expression, index) : + nextSiblings[index || 0]; + }, + + getElementsBySelector: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + getElementsByClassName: function(element, className) { + return document.getElementsByClassName(className, element); + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + if (!element.attributes) return null; + var t = Element._attributeTranslations; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + var attribute = element.attributes[name]; + return attribute ? attribute.nodeValue : null; + } + return element.getAttribute(name); + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + if (elementClassName.length == 0) return false; + if (elementClassName == className || + elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + return true; + return false; + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element).add(className); + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element).remove(className); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className); + return element; + }, + + observe: function() { + Event.observe.apply(Event, arguments); + return $A(arguments).first(); + }, + + stopObserving: function() { + Event.stopObserving.apply(Event, arguments); + return $A(arguments).first(); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = Position.cumulativeOffset(element); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles, camelized) { + element = $(element); + var elementStyle = element.style; + + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]) + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') : + (camelized ? property : property.camelize())] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = $(element).getStyle('display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = element.style.overflow || 'auto'; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + } +}; + +Object.extend(Element.Methods, { + childOf: Element.Methods.descendantOf, + childElements: Element.Methods.immediateDescendants +}); + +if (Prototype.Browser.Opera) { + Element.Methods._getStyle = Element.Methods.getStyle; + Element.Methods.getStyle = function(element, style) { + switch(style) { + case 'left': + case 'top': + case 'right': + case 'bottom': + if (Element._getStyle(element, 'position') == 'static') return null; + default: return Element._getStyle(element, style); + } + }; +} +else if (Prototype.Browser.IE) { + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset'+style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + element = $(element); + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + style.filter = filter.replace(/alpha\([^\)]*\)/gi,''); + return element; + } else if (value < 0.00001) value = 0; + style.filter = filter.replace(/alpha\([^\)]*\)/gi, '') + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + // IE is missing .innerHTML support for TABLE-related elements + Element.Methods.update = function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + var tagName = element.tagName.toUpperCase(); + if (['THEAD','TBODY','TR','TD'].include(tagName)) { + var div = document.createElement('div'); + switch (tagName) { + case 'THEAD': + case 'TBODY': + div.innerHTML = '<table><tbody>' + html.stripScripts() + '</tbody></table>'; + depth = 2; + break; + case 'TR': + div.innerHTML = '<table><tbody><tr>' + html.stripScripts() + '</tr></tbody></table>'; + depth = 3; + break; + case 'TD': + div.innerHTML = '<table><tbody><tr><td>' + html.stripScripts() + '</td></tr></tbody></table>'; + depth = 4; + } + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + depth.times(function() { div = div.firstChild }); + $A(div.childNodes).each(function(node) { element.appendChild(node) }); + } else { + element.innerHTML = html.stripScripts(); + } + setTimeout(function() { html.evalScripts() }, 10); + return element; + } +} +else if (Prototype.Browser.Gecko) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +Element._attributeTranslations = { + names: { + colspan: "colSpan", + rowspan: "rowSpan", + valign: "vAlign", + datetime: "dateTime", + accesskey: "accessKey", + tabindex: "tabIndex", + enctype: "encType", + maxlength: "maxLength", + readonly: "readOnly", + longdesc: "longDesc" + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + var node = element.getAttributeNode('title'); + return node.specified ? node.nodeValue : null; + } + } +}; + +(function() { + Object.extend(this, { + href: this._getAttr, + src: this._getAttr, + type: this._getAttr, + disabled: this._flag, + checked: this._flag, + readonly: this._flag, + multiple: this._flag + }); +}).call(Element._attributeTranslations.values); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + var t = Element._attributeTranslations, node; + attribute = t.names[attribute] || attribute; + node = $(element).getAttributeNode(attribute); + return node && node.specified; + } +}; + +Element.Methods.ByTag = {}; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div').__proto__) { + window.HTMLElement = {}; + window.HTMLElement.prototype = document.createElement('div').__proto__; + Prototype.BrowserFeatures.ElementExtensions = true; +} + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || {}); + else { + if (tagName.constructor == Array) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = {}; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + var cache = Element.extend.cache; + for (var property in methods) { + var value = methods[property]; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = cache.findOrStore(value); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = {}; + window[klass].prototype = document.createElement(tagName).__proto__; + return window[klass]; + } + + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (typeof klass == "undefined") continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; +}; + +var Toggle = { display: Element.toggle }; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + var tagName = this.element.tagName.toUpperCase(); + if (['TBODY', 'TR'].include(tagName)) { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>'; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set($A(this).concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set($A(this).without(classNameToRemove).join(' ')); + }, + + toString: function() { + return $A(this).join(' '); + } +}; + +Object.extend(Element.ClassNames.prototype, Enumerable); +/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create(); + +Selector.prototype = { + initialize: function(expression) { + this.expression = expression.strip(); + this.compileMatcher(); + }, + + compileMatcher: function() { + // Selectors with namespaced attributes can't use the XPath version + if (Prototype.BrowserFeatures.XPath && !(/\[[\w-]*?:/).test(this.expression)) + return this.compileXPathMatcher(); + + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; return; + } + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(typeof c[i] == 'function' ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(typeof x[i] == 'function' ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + if (this.xpath) return document._getElementsByXPath(this.xpath, root); + return this.matcher(root); + }, + + match: function(element) { + return this.findElements(document).include(element); + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#<Selector:" + this.expression.inspect() + ">"; + } +}; + +Object.extend(Selector, { + _cache: {}, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: "[@#{1}]", + attr: function(m) { + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (typeof h === 'function') return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]", + 'checked': "[@checked]", + 'disabled': "[@disabled]", + 'enabled': "[not(@disabled)]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, m, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = typeof x[i] == 'function' ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|\s|(?=:))/, + attrPresence: /^\[([\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\]]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._counted = true; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._counted) { + n._counted = true; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, children = [], child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + tagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() == tagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!nodes && root == document) return targetNode ? [targetNode] : []; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr) { + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._counted) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._counted) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled) results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv.startsWith(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } + }, + + matchElements: function(elements, expression) { + var matches = new Selector(expression).findElements(), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._counted) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (typeof expression == 'number') { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + var exprs = expressions.join(','), expressions = []; + exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, getHash) { + var data = elements.inject({}, function(result, element) { + if (!element.disabled && element.name) { + var key = element.name, value = $(element).getValue(); + if (value != null) { + if (key in result) { + if (result[key].constructor != Array) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return getHash ? data : Hash.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, getHash) { + return Form.serializeElements(Form.getElements(form), getHash); + }, + + getElements: function(form) { + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + return $(form).getElements().find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || {}); + + var params = options.parameters; + options.parameters = form.serialize(true); + + if (params) { + if (typeof params == 'string') params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(form.readAttribute('action'), options); + } +} + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +} + +Form.Element.Methods = { + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = {}; + pair[element.name] = value; + return Hash.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) {} + return element; + }, + + disable: function(element) { + element = $(element); + element.blur(); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +} + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + default: + return Form.Element.Serializers.textarea(element); + } + }, + + inputSelector: function(element) { + return element.checked ? element.value : null; + }, + + textarea: function(element) { + return element.value; + }, + + select: function(element) { + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +} + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + var changed = ('string' == typeof this.lastValue && 'string' == typeof value + ? this.lastValue != value : String(this.lastValue) != String(value)); + if (changed) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback.bind(this)); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + + element: function(event) { + return $(event.target || event.srcElement); + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0, length = Event.observers.length; i < length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (Prototype.Browser.WebKit || element.attachEvent)) + name = 'keydown'; + + Event._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (Prototype.Browser.WebKit || element.attachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + try { + element.detachEvent('on' + name, observer); + } catch (e) {} + } + } +}); + +/* prevent memory leaks in IE */ +if (Prototype.Browser.IE) + Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if(element.tagName=='BODY') break; + var p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!window.opera || element.tagName=='BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (Prototype.Browser.WebKit) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} + +Element.addMethods();
\ No newline at end of file diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/scriptaculous.js b/ipa-server/ipa-gui/ipagui/static/javascript/scriptaculous.js new file mode 100644 index 00000000..7c472a60 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/javascript/scriptaculous.js @@ -0,0 +1,58 @@ +// script.aculo.us scriptaculous.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007 + +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +var Scriptaculous = { + Version: '1.7.1_beta3', + require: function(libraryName) { + // inserting via DOM fails in Safari 2.0, so brute force approach + document.write('<script type="text/javascript" src="'+libraryName+'"></script>'); + }, + REQUIRED_PROTOTYPE: '1.5.1', + load: function() { + function convertVersionString(versionString){ + var r = versionString.split('.'); + return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]); + } + + if((typeof Prototype=='undefined') || + (typeof Element == 'undefined') || + (typeof Element.Methods=='undefined') || + (convertVersionString(Prototype.Version) < + convertVersionString(Scriptaculous.REQUIRED_PROTOTYPE))) + throw("script.aculo.us requires the Prototype JavaScript framework >= " + + Scriptaculous.REQUIRED_PROTOTYPE); + + $A(document.getElementsByTagName("script")).findAll( function(s) { + return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/)) + }).each( function(s) { + var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,''); + var includes = s.src.match(/\?.*load=([a-z,]*)/); + (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider,sound').split(',').each( + function(include) { Scriptaculous.require(path+include+'.js') }); + }); + } +} + +Scriptaculous.load();
\ No newline at end of file diff --git a/ipa-server/ipa-gui/ipagui/static/javascript/tablekit.js b/ipa-server/ipa-gui/ipagui/static/javascript/tablekit.js new file mode 100644 index 00000000..54036948 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/static/javascript/tablekit.js @@ -0,0 +1,848 @@ +/* +* +* Copyright (c) 2007 Andrew Tetlaw & Millstream Web Software +* http://www.millstream.com.au/view/code/tablekit/ +* Version: 1.2.1 2007-03-11 +* +* Permission is hereby granted, free of charge, to any person +* obtaining a copy of this software and associated documentation +* files (the "Software"), to deal in the Software without +* restriction, including without limitation the rights to use, copy, +* modify, merge, publish, distribute, sublicense, and/or sell copies +* of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be +* included in all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* * +*/ + +// Use the TableKit class constructure if you'd prefer to init your tables as JS objects +var TableKit = Class.create(); + +TableKit.prototype = { + initialize : function(elm, options) { + var table = $(elm); + if(table.tagName !== "TABLE") { + return; + } + TableKit.register(table,Object.extend(TableKit.options,options || {})); + this.id = table.id; + var op = TableKit.option('sortable resizable editable', this.id); + if(op.sortable) { + TableKit.Sortable.init(table); + } + if(op.resizable) { + TableKit.Resizable.init(table); + } + if(op.editable) { + TableKit.Editable.init(table); + } + }, + sort : function(column, order) { + TableKit.Sortable.sort(this.id, column, order); + }, + resizeColumn : function(column, w) { + TableKit.Resizable.resize(this.id, column, w); + }, + editCell : function(row, column) { + TableKit.Editable.editCell(this.id, row, column); + } +}; + +Object.extend(TableKit, { + getBodyRows : function(table) { + table = $(table); + var id = table.id; + if(!TableKit.rows[id]) { + TableKit.rows[id] = (table.tHead && table.tHead.rows.length > 0) ? $A(table.tBodies[0].rows) : $A(table.rows).without(table.rows[0]); + } + return TableKit.rows[id]; + }, + getHeaderCells : function(table, cell) { + if(!table) { table = $(cell).up('table'); } + var id = table.id; + if(!TableKit.heads[id]) { + TableKit.heads[id] = $A((table.tHead && table.tHead.rows.length > 0) ? table.tHead.rows[table.tHead.rows.length-1].cells : table.rows[0].cells); + } + return TableKit.heads[id]; + }, + getCellIndex : function(cell) { + return $A(cell.parentNode.cells).indexOf(cell); + }, + getRowIndex : function(row) { + return $A(row.parentNode.rows).indexOf(row); + }, + getCellText : function(cell, refresh) { + if(!cell) { return ""; } + TableKit.registerCell(cell); + var data = TableKit.cells[cell.id]; + if(refresh || data.refresh || !data.textContent) { + data.textContent = cell.textContent ? cell.textContent : cell.innerText; + data.refresh = false; + } + return data.textContent; + }, + register : function(table, options) { + if(!table.id) { + TableKit._tblcount += 1; + table.id = "tablekit-table-" + TableKit._tblcount; + } + var id = table.id; + TableKit.tables[id] = TableKit.tables[id] ? Object.extend(TableKit.tables[id], options || {}) : Object.extend({sortable:false,resizable:false,editable:false}, options || {}); + }, + registerCell : function(cell) { + if(!cell.id) { + TableKit._cellcount += 1; + cell.id = "tablekit-cell-" + TableKit._cellcount; + } + if(!TableKit.cells[cell.id]) { + TableKit.cells[cell.id] = {textContent : '', htmlContent : '', active : false}; + } + }, + isSortable : function(table) { + return TableKit.tables[table.id] ? TableKit.tables[table.id].sortable : false; + }, + isResizable : function(table) { + return TableKit.tables[table.id] ? TableKit.tables[table.id].resizable : false; + }, + isEditable : function(table) { + return TableKit.tables[table.id] ? TableKit.tables[table.id].editable : false; + }, + setup : function(o) { + Object.extend(TableKit.options, o || {} ); + }, + option : function(s, id, o1, o2) { + o1 = o1 || TableKit.options; + o2 = o2 || (id ? (TableKit.tables[id] ? TableKit.tables[id] : {}) : {}); + var key = id + s; + if(!TableKit._opcache[key]){ + TableKit._opcache[key] = $A($w(s)).inject([],function(a,v){ + a.push(a[v] = o2[v] || o1[v]); + return a; + }); + } + return TableKit._opcache[key]; + }, + e : function(event) { + return event || window.event; + }, + tables : {}, + _opcache : {}, + cells : {}, + rows : {}, + heads : {}, + options : { + autoLoad : true, + stripe : true, + sortable : true, + resizable : true, + editable : true, + rowEvenClass : 'roweven', + rowOddClass : 'rowodd', + sortableSelector : ['table.sortable'], + columnClass : 'sortcol', + descendingClass : 'sortdesc', + ascendingClass : 'sortasc', + noSortClass : 'nosort', + sortFirstAscendingClass : 'sortfirstasc', + sortFirstDecendingClass : 'sortfirstdesc', + resizableSelector : ['table.resizable'], + minWidth : 10, + showHandle : true, + resizeOnHandleClass : 'resize-handle-active', + editableSelector : ['table.editable'], + formClassName : 'editable-cell-form', + noEditClass : 'noedit', + editAjaxURI : '/', + editAjaxOptions : {} + }, + _tblcount : 0, + _cellcount : 0, + load : function() { + if(TableKit.options.autoLoad) { + if(TableKit.options.sortable) { + $A(TableKit.options.sortableSelector).each(function(s){ + $$(s).each(function(t) { + TableKit.Sortable.init(t); + }); + }); + } + if(TableKit.options.resizable) { + $A(TableKit.options.resizableSelector).each(function(s){ + $$(s).each(function(t) { + TableKit.Resizable.init(t); + }); + }); + } + if(TableKit.options.editable) { + $A(TableKit.options.editableSelector).each(function(s){ + $$(s).each(function(t) { + TableKit.Editable.init(t); + }); + }); + } + } + } +}); + +TableKit.Rows = { + stripe : function(table) { + var rows = TableKit.getBodyRows(table); + rows.each(function(r,i) { + TableKit.Rows.addStripeClass(table,r,i); + }); + }, + addStripeClass : function(t,r,i) { + t = t || r.up('table'); + var op = TableKit.option('rowEvenClass rowOddClass', t.id); + var css = ((i+1)%2 === 0 ? op[0] : op[1]); + // using prototype's assClassName/RemoveClassName was not efficient for large tables, hence: + var cn = r.className.split(/\s+/); + var newCn = []; + for(var x = 0, l = cn.length; x < l; x += 1) { + if(cn[x] !== op[0] && cn[x] !== op[1]) { newCn.push(cn[x]); } + } + newCn.push(css); + r.className = newCn.join(" "); + } +}; + +TableKit.Sortable = { + init : function(elm, options){ + var table = $(elm); + if(table.tagName !== "TABLE") { + return; + } + TableKit.register(table,Object.extend(options || {},{sortable:true})); + var sortFirst; + var cells = TableKit.getHeaderCells(table); + var op = TableKit.option('noSortClass columnClass sortFirstAscendingClass sortFirstDecendingClass', table.id); + cells.each(function(c){ + c = $(c); + if(!c.hasClassName(op.noSortClass)) { + Event.observe(c, 'mousedown', TableKit.Sortable._sort); + c.addClassName(op.columnClass); + if(c.hasClassName(op.sortFirstAscendingClass) || c.hasClassName(op.sortFirstDecendingClass)) { + sortFirst = c; + } + } + }); + + if(sortFirst) { + if(sortFirst.hasClassName(op.sortFirstAscendingClass)) { + TableKit.Sortable.sort(table, sortFirst, 1); + } else { + TableKit.Sortable.sort(table, sortFirst, -1); + } + } else { // just add row stripe classes + TableKit.Rows.stripe(table); + } + }, + reload : function(table) { + table = $(table); + var cells = TableKit.getHeaderCells(table); + var op = TableKit.option('noSortClass columnClass', table.id); + cells.each(function(c){ + c = $(c); + if(!c.hasClassName(op.noSortClass)) { + Event.stopObserving(c, 'mousedown', TableKit.Sortable._sort); + c.removeClassName(op.columnClass); + } + }); + TableKit.Sortable.init(table); + }, + _sort : function(e) { + if(TableKit.Resizable._onHandle) {return;} + e = TableKit.e(e); + Event.stop(e); + var cell = Event.element(e); + while(!(cell.tagName && cell.tagName.match(/td|th/gi))) { + cell = cell.parentNode; + } + TableKit.Sortable.sort(null, cell); + }, + sort : function(table, index, order) { + var cell; + if(typeof index === 'number') { + if(!table || (table.tagName && table.tagName !== "TABLE")) { + return; + } + table = $(table); + index = Math.min(table.rows[0].cells.length, index); + index = Math.max(1, index); + index -= 1; + cell = (table.tHead && table.tHead.rows.length > 0) ? $(table.tHead.rows[table.tHead.rows.length-1].cells[index]) : $(table.rows[0].cells[index]); + } else { + cell = $(index); + table = table ? $(table) : cell.up('table'); + index = TableKit.getCellIndex(cell); + } + var op = TableKit.option('noSortClass descendingClass ascendingClass', table.id); + + if(cell.hasClassName(op.noSortClass)) {return;} + + // order = order ? order : (cell.hasClassName(op.descendingClass) ? 1 : -1); + // kmccarth - change default sort order to ascending + order = order ? order : (cell.hasClassName(op.ascendingClass) ? -1 : 1); + var rows = TableKit.getBodyRows(table); + + if(cell.hasClassName(op.ascendingClass) || cell.hasClassName(op.descendingClass)) { + rows.reverse(); // if it was already sorted we just need to reverse it. + } else { + var datatype = TableKit.Sortable.getDataType(cell,index,table); + var tkst = TableKit.Sortable.types; + rows.sort(function(a,b) { + return order * tkst[datatype].compare(TableKit.getCellText(a.cells[index]),TableKit.getCellText(b.cells[index])); + }); + } + var tb = table.tBodies[0]; + var tkr = TableKit.Rows; + rows.each(function(r,i) { + tb.appendChild(r); + tkr.addStripeClass(table,r,i); + }); + var hcells = TableKit.getHeaderCells(null, cell); + $A(hcells).each(function(c,i){ + c = $(c); + c.removeClassName(op.ascendingClass); + c.removeClassName(op.descendingClass); + if(index === i) { + if(order === 1) { + c.removeClassName(op.descendingClass); + c.addClassName(op.ascendingClass); + } else { + c.removeClassName(op.ascendingClass); + c.addClassName(op.descendingClass); + } + } + }); + }, + types : {}, + detectors : [], + addSortType : function() { + $A(arguments).each(function(o){ + TableKit.Sortable.types[o.name] = o; + }); + }, + getDataType : function(cell,index,table) { + cell = $(cell); + index = (index || index === 0) ? index : TableKit.getCellIndex(cell); + + var colcache = TableKit.Sortable._coltypecache; + var cache = colcache[table.id] ? colcache[table.id] : (colcache[table.id] = {}); + + if(!cache[index]) { + var t = ''; + // first look for a data type id on the heading row cell + if(cell.id && TableKit.Sortable.types[cell.id]) { + t = cell.id; + } + t = cell.classNames().detect(function(n){ // then look for a data type classname on the heading row cell + return (TableKit.Sortable.types[n]) ? true : false; + }); + if(!t) { + var rows = TableKit.getBodyRows(table); + cell = rows[0].cells[index]; // grab same index cell from body row to try and match data type + t = TableKit.Sortable.detectors.detect( + function(d){ + return TableKit.Sortable.types[d].detect(TableKit.getCellText(cell)); + }); + } + cache[index] = t; + } + return cache[index]; + }, + _coltypecache : {} +}; + +TableKit.Sortable.detectors = $A($w('date-iso date date-eu date-au time currency datasize number casesensitivetext text')); // setting it here because Safari complained when I did it above... + +TableKit.Sortable.Type = Class.create(); +TableKit.Sortable.Type.prototype = { + initialize : function(name, options){ + this.name = name; + options = Object.extend({ + normal : function(v){ + return v; + }, + pattern : /.*/ + }, options || {}); + this.normal = options.normal; + this.pattern = options.pattern; + if(options.compare) { + this.compare = options.compare; + } + if(options.detect) { + this.detect = options.detect; + } + }, + compare : function(a,b){ + return TableKit.Sortable.Type.compare(this.normal(a), this.normal(b)); + }, + detect : function(v){ + return this.pattern.test(v); + } +}; + +TableKit.Sortable.Type.compare = function(a,b) { + return a < b ? -1 : a === b ? 0 : 1; +}; + +TableKit.Sortable.addSortType( + new TableKit.Sortable.Type('number', { + pattern : /^[-+]?[\d]*\.?[\d]+(?:[eE][-+]?[\d]+)?/, + normal : function(v) { + // This will grab the first thing that looks like a number from a string, so you can use it to order a column of various srings containing numbers. + v = parseFloat(v.replace(/^.*?([-+]?[\d]*\.?[\d]+(?:[eE][-+]?[\d]+)?).*$/,"$1")); + return isNaN(v) ? 0 : v; + }}), + new TableKit.Sortable.Type('text',{ + normal : function(v) { + return v ? v.toLowerCase() : ''; + }}), + new TableKit.Sortable.Type('casesensitivetext',{pattern : /^[A-Z]+$/}), + new TableKit.Sortable.Type('datasize',{ + pattern : /^[-+]?[\d]*\.?[\d]+(?:[eE][-+]?[\d]+)?\s?[k|m|g|t]b$/i, + normal : function(v) { + var r = v.match(/^([-+]?[\d]*\.?[\d]+([eE][-+]?[\d]+)?)\s?([k|m|g|t]?b)?/i); + var b = r[1] ? Number(r[1]).valueOf() : 0; + var m = r[3] ? r[3].substr(0,1).toLowerCase() : ''; + var result = b; + switch(m) { + case 'k': + result = b * 1024; + break; + case 'm': + result = b * 1024 * 1024; + break; + case 'g': + result = b * 1024 * 1024 * 1024; + break; + case 't': + result = b * 1024 * 1024 * 1024 * 1024; + break; + } + return result; + }}), + new TableKit.Sortable.Type('date-au',{ + pattern : /^\d{2}\/\d{2}\/\d{4}\s?(?:\d{1,2}\:\d{2}(?:\:\d{2})?\s?[a|p]?m?)?/i, + normal : function(v) { + if(!this.pattern.test(v)) {return 0;} + var r = v.match(/^(\d{2})\/(\d{2})\/(\d{4})\s?(?:(\d{1,2})\:(\d{2})(?:\:(\d{2}))?\s?([a|p]?m?))?/i); + var yr_num = r[3]; + var mo_num = parseInt(r[2],10)-1; + var day_num = r[1]; + var hr_num = r[4] ? r[4] : 0; + if(r[7] && r[7].toLowerCase().indexOf('p') !== -1) { + hr_num = parseInt(r[4],10) + 12; + } + var min_num = r[5] ? r[5] : 0; + var sec_num = r[6] ? r[6] : 0; + return new Date(yr_num, mo_num, day_num, hr_num, min_num, sec_num, 0).valueOf(); + }}), + new TableKit.Sortable.Type('date-us',{ + pattern : /^\d{2}\/\d{2}\/\d{4}\s?(?:\d{1,2}\:\d{2}(?:\:\d{2})?\s?[a|p]?m?)?/i, + normal : function(v) { + if(!this.pattern.test(v)) {return 0;} + var r = v.match(/^(\d{2})\/(\d{2})\/(\d{4})\s?(?:(\d{1,2})\:(\d{2})(?:\:(\d{2}))?\s?([a|p]?m?))?/i); + var yr_num = r[3]; + var mo_num = parseInt(r[1],10)-1; + var day_num = r[2]; + var hr_num = r[4] ? r[4] : 0; + if(r[7] && r[7].toLowerCase().indexOf('p') !== -1) { + hr_num = parseInt(r[4],10) + 12; + } + var min_num = r[5] ? r[5] : 0; + var sec_num = r[6] ? r[6] : 0; + return new Date(yr_num, mo_num, day_num, hr_num, min_num, sec_num, 0).valueOf(); + }}), + new TableKit.Sortable.Type('date-eu',{ + pattern : /^\d{2}-\d{2}-\d{4}/i, + normal : function(v) { + if(!this.pattern.test(v)) {return 0;} + var r = v.match(/^(\d{2})-(\d{2})-(\d{4})/); + var yr_num = r[3]; + var mo_num = parseInt(r[2],10)-1; + var day_num = r[1]; + return new Date(yr_num, mo_num, day_num).valueOf(); + }}), + new TableKit.Sortable.Type('date-iso',{ + pattern : /[\d]{4}-[\d]{2}-[\d]{2}(?:T[\d]{2}\:[\d]{2}(?:\:[\d]{2}(?:\.[\d]+)?)?(Z|([-+][\d]{2}:[\d]{2})?)?)?/, // 2005-03-26T19:51:34Z + normal : function(v) { + if(!this.pattern.test(v)) {return 0;} + var d = v.match(/([\d]{4})(-([\d]{2})(-([\d]{2})(T([\d]{2}):([\d]{2})(:([\d]{2})(\.([\d]+))?)?(Z|(([-+])([\d]{2}):([\d]{2})))?)?)?)?/); + var offset = 0; + var date = new Date(d[1], 0, 1); + if (d[3]) { date.setMonth(d[3] - 1) ;} + if (d[5]) { date.setDate(d[5]); } + if (d[7]) { date.setHours(d[7]); } + if (d[8]) { date.setMinutes(d[8]); } + if (d[10]) { date.setSeconds(d[10]); } + if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); } + if (d[14]) { + offset = (Number(d[16]) * 60) + Number(d[17]); + offset *= ((d[15] === '-') ? 1 : -1); + } + offset -= date.getTimezoneOffset(); + if(offset !== 0) { + var time = (Number(date) + (offset * 60 * 1000)); + date.setTime(Number(time)); + } + return date.valueOf(); + }}), + new TableKit.Sortable.Type('date',{ + pattern: /^(?:sun|mon|tue|wed|thu|fri|sat)\,\s\d{1,2}\s(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s\d{4}(?:\s\d{2}\:\d{2}(?:\:\d{2})?(?:\sGMT(?:[+-]\d{4})?)?)?/i, //Mon, 18 Dec 1995 17:28:35 GMT + compare : function(a,b) { // must be standard javascript date format + if(a && b) { + return TableKit.Sortable.Type.compare(new Date(a),new Date(b)); + } else { + return TableKit.Sortable.Type.compare(a ? 1 : 0, b ? 1 : 0); + } + }}), + new TableKit.Sortable.Type('time',{ + pattern : /^\d{1,2}\:\d{2}(?:\:\d{2})?(?:\s[a|p]m)?$/i, + compare : function(a,b) { + var d = new Date(); + var ds = d.getMonth() + "/" + d.getDate() + "/" + d.getFullYear() + " "; + return TableKit.Sortable.Type.compare(new Date(ds + a),new Date(ds + b)); + }}), + new TableKit.Sortable.Type('currency',{ + pattern : /^[$£¥€¤]/, // dollar,pound,yen,euro,generic currency symbol + normal : function(v) { + return v ? parseFloat(v.replace(/[^-\d\.]/g,'')) : 0; + }}) +); + +TableKit.Resizable = { + init : function(elm, options){ + var table = $(elm); + if(table.tagName !== "TABLE") {return;} + TableKit.register(table,Object.extend(options || {},{resizable:true})); + var cells = TableKit.getHeaderCells(table); + cells.each(function(c){ + c = $(c); + Event.observe(c, 'mouseover', TableKit.Resizable.initDetect); + Event.observe(c, 'mouseout', TableKit.Resizable.killDetect); + }); + }, + resize : function(table, index, w) { + var cell; + if(typeof index === 'number') { + if(!table || (table.tagName && table.tagName !== "TABLE")) {return;} + table = $(table); + index = Math.min(table.rows[0].cells.length, index); + index = Math.max(1, index); + index -= 1; + cell = (table.tHead && table.tHead.rows.length > 0) ? $(table.tHead.rows[table.tHead.rows.length-1].cells[index]) : $(table.rows[0].cells[index]); + } else { + cell = $(index); + table = table ? $(table) : cell.up('table'); + index = TableKit.getCellIndex(cell); + } + var pad = parseInt(cell.getStyle('paddingLeft'),10) + parseInt(cell.getStyle('paddingRight'),10); + w = Math.max(w-pad, TableKit.option('minWidth', table.id)[0]); + + cell.setStyle({'width' : w + 'px'}); + }, + initDetect : function(e) { + e = TableKit.e(e); + var cell = Event.element(e); + Event.observe(cell, 'mousemove', TableKit.Resizable.detectHandle); + Event.observe(cell, 'mousedown', TableKit.Resizable.startResize); + }, + detectHandle : function(e) { + e = TableKit.e(e); + var cell = Event.element(e); + if(TableKit.Resizable.pointerPos(cell,Event.pointerX(e),Event.pointerY(e))){ + cell.addClassName(TableKit.option('resizeOnHandleClass', cell.up('table').id)[0]); + TableKit.Resizable._onHandle = true; + } else { + cell.removeClassName(TableKit.option('resizeOnHandleClass', cell.up('table').id)[0]); + TableKit.Resizable._onHandle = false; + } + }, + killDetect : function(e) { + e = TableKit.e(e); + TableKit.Resizable._onHandle = false; + var cell = Event.element(e); + Event.stopObserving(cell, 'mousemove', TableKit.Resizable.detectHandle); + Event.stopObserving(cell, 'mousedown', TableKit.Resizable.startResize); + cell.removeClassName(TableKit.option('resizeOnHandleClass', cell.up('table').id)[0]); + }, + startResize : function(e) { + e = TableKit.e(e); + if(!TableKit.Resizable._onHandle) {return;} + var cell = Event.element(e); + Event.stopObserving(cell, 'mousemove', TableKit.Resizable.detectHandle); + Event.stopObserving(cell, 'mousedown', TableKit.Resizable.startResize); + Event.stopObserving(cell, 'mouseout', TableKit.Resizable.killDetect); + TableKit.Resizable._cell = cell; + var table = cell.up('table'); + TableKit.Resizable._tbl = table; + if(TableKit.option('showHandle', table.id)[0]) { + TableKit.Resizable._handle = $(document.createElement('div')).addClassName('resize-handle').setStyle({ + 'top' : Position.cumulativeOffset(cell)[1] + 'px', + 'left' : Event.pointerX(e) + 'px', + 'height' : table.getDimensions().height + 'px' + }); + document.body.appendChild(TableKit.Resizable._handle); + } + Event.observe(document, 'mousemove', TableKit.Resizable.drag); + Event.observe(document, 'mouseup', TableKit.Resizable.endResize); + Event.stop(e); + }, + endResize : function(e) { + e = TableKit.e(e); + var cell = TableKit.Resizable._cell; + TableKit.Resizable.resize(null, cell, (Event.pointerX(e) - Position.cumulativeOffset(cell)[0])); + Event.stopObserving(document, 'mousemove', TableKit.Resizable.drag); + Event.stopObserving(document, 'mouseup', TableKit.Resizable.endResize); + if(TableKit.option('showHandle', TableKit.Resizable._tbl.id)[0]) { + $$('div.resize-handle').each(function(elm){ + document.body.removeChild(elm); + }); + } + Event.observe(cell, 'mouseout', TableKit.Resizable.killDetect); + TableKit.Resizable._tbl = TableKit.Resizable._handle = TableKit.Resizable._cell = null; + Event.stop(e); + }, + drag : function(e) { + e = TableKit.e(e); + if(TableKit.Resizable._handle === null) { + try { + TableKit.Resizable.resize(TableKit.Resizable._tbl, TableKit.Resizable._cell, (Event.pointerX(e) - Position.cumulativeOffset(TableKit.Resizable._cell)[0])); + } catch(e) {} + } else { + TableKit.Resizable._handle.setStyle({'left' : Event.pointerX(e) + 'px'}); + } + return false; + }, + pointerPos : function(element, x, y) { + var offset = Position.cumulativeOffset(element); + return (y >= offset[1] && + y < offset[1] + element.offsetHeight && + x >= offset[0] + element.offsetWidth - 5 && + x < offset[0] + element.offsetWidth); + }, + _onHandle : false, + _cell : null, + _tbl : null, + _handle : null +}; + + +TableKit.Editable = { + init : function(elm, options){ + var table = $(elm); + if(table.tagName !== "TABLE") {return;} + TableKit.register(table,Object.extend(options || {},{editable:true})); + Event.observe(table.tBodies[0], 'click', TableKit.Editable._editCell); + }, + _editCell : function(e) { + e = TableKit.e(e); + var cell = Event.findElement(e,'td'); + TableKit.Editable.editCell(null, cell); + }, + editCell : function(table, index, cindex) { + var cell, row; + if(typeof index === 'number') { + if(!table || (table.tagName && table.tagName !== "TABLE")) {return;} + table = $(table); + index = Math.min(table.tBodies[0].rows.length, index); + index = Math.max(1, index); + index -= 1; + cindex = Math.min(table.rows[0].cells.length, cindex); + cindex = Math.max(1, cindex); + cindex -= 1; + row = $(table.tBodies[0].rows[index]); + cell = $(row.cells[cindex]); + } else { + cell = $(index); + table = (table && table.tagName && table.tagName !== "TABLE") ? $(table) : cell.up('table'); + row = cell.up('tr'); + } + var op = TableKit.option('noEditClass', table.id); + if(cell.hasClassName(op.noEditClass)) {return;} + + var head = $(TableKit.getHeaderCells(table, cell)[TableKit.getCellIndex(cell)]); + if(head.hasClassName(op.noEditClass)) {return;} + + TableKit.registerCell(cell); + var data = TableKit.cells[cell.id]; + if(data.active) {return;} + data.htmlContent = cell.innerHTML; + var ftype = TableKit.Editable.types['text-input']; + if(head.id && TableKit.Editable.types[head.id]) { + ftype = TableKit.Editable.types[head.id]; + } else { + var n = head.classNames().detect(function(n){ + return (TableKit.Editable.types[n]) ? true : false; + }); + ftype = n ? TableKit.Editable.types[n] : ftype; + } + ftype.edit(cell); + data.active = true; + }, + types : {}, + addCellEditor : function(o) { + if(o && o.name) { TableKit.Editable.types[o.name] = o; } + } +}; + +TableKit.Editable.CellEditor = Class.create(); +TableKit.Editable.CellEditor.prototype = { + initialize : function(name, options){ + this.name = name; + this.options = Object.extend({ + element : 'input', + attributes : {name : 'value', type : 'text'}, + selectOptions : [], + showSubmit : true, + submitText : 'OK', + showCancel : true, + cancelText : 'Cancel', + ajaxURI : null, + ajaxOptions : null + }, options || {}); + }, + edit : function(cell) { + cell = $(cell); + var op = this.options; + var table = cell.up('table'); + + var form = $(document.createElement("form")); + form.id = cell.id + '-form'; + form.addClassName(TableKit.option('formClassName', table.id)[0]); + form.onsubmit = this._submit.bindAsEventListener(this); + + var field = document.createElement(op.element); + $H(op.attributes).each(function(v){ + field[v.key] = v.value; + }); + switch(op.element) { + case 'input': + case 'textarea': + field.value = TableKit.getCellText(cell); + break; + + case 'select': + var txt = TableKit.getCellText(cell); + $A(op.selectOptions).each(function(v){ + field.options[field.options.length] = new Option(v[0], v[1]); + if(txt === v[1]) { + field.options[field.options.length-1].selected = 'selected'; + } + }); + break; + } + form.appendChild(field); + if(op.element === 'textarea') { + form.appendChild(document.createElement("br")); + } + if(op.showSubmit) { + var okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = op.submitText; + okButton.className = 'editor_ok_button'; + form.appendChild(okButton); + } + if(op.showCancel) { + var cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(op.cancelText)); + cancelLink.onclick = this._cancel.bindAsEventListener(this); + cancelLink.className = 'editor_cancel'; + form.appendChild(cancelLink); + } + cell.innerHTML = ''; + cell.appendChild(form); + }, + _submit : function(e) { + var cell = Event.findElement(e,'td'); + var form = Event.findElement(e,'form'); + Event.stop(e); + this.submit(cell,form); + }, + submit : function(cell, form) { + var op = this.options; + form = form ? form : cell.down('form'); + var head = $(TableKit.getHeaderCells(null, cell)[TableKit.getCellIndex(cell)]); + var row = cell.up('tr'); + var table = cell.up('table'); + var s = '&row=' + (TableKit.getRowIndex(row)+1) + '&cell=' + (TableKit.getCellIndex(cell)+1) + '&id=' + row.id + '&field=' + head.id + '&' + Form.serialize(form); + this.ajax = new Ajax.Updater(cell, op.ajaxURI || TableKit.option('editAjaxURI', table.id)[0], Object.extend(op.ajaxOptions || TableKit.option('editAjaxOptions', table.id)[0], { + postBody : s, + onComplete : function() { + var data = TableKit.cells[cell.id]; + data.active = false; + data.refresh = true; // mark cell cache for refreshing, in case cell contents has changed and sorting is applied + } + })); + }, + _cancel : function(e) { + var cell = Event.findElement(e,'td'); + Event.stop(e); + this.cancel(cell); + }, + cancel : function(cell) { + this.ajax = null; + var data = TableKit.cells[cell.id]; + cell.innerHTML = data.htmlContent; + data.htmlContent = ''; + data.active = false; + }, + ajax : null +}; + +TableKit.Editable.textInput = function(n,attributes) { + TableKit.Editable.addCellEditor(new TableKit.Editable.CellEditor(n, { + element : 'input', + attributes : Object.extend({name : 'value', type : 'text'}, attributes||{}) + })); +}; +TableKit.Editable.textInput('text-input'); + +TableKit.Editable.multiLineInput = function(n,attributes) { + TableKit.Editable.addCellEditor(new TableKit.Editable.CellEditor(n, { + element : 'textarea', + attributes : Object.extend({name : 'value', rows : '5', cols : '20'}, attributes||{}) + })); +}; +TableKit.Editable.multiLineInput('multi-line-input'); + +TableKit.Editable.selectInput = function(n,attributes,selectOptions) { + TableKit.Editable.addCellEditor(new TableKit.Editable.CellEditor(n, { + element : 'select', + attributes : Object.extend({name : 'value'}, attributes||{}), + 'selectOptions' : selectOptions + })); +}; + +/* +TableKit.Bench = { + bench : [], + start : function(){ + TableKit.Bench.bench[0] = new Date().getTime(); + }, + end : function(s){ + TableKit.Bench.bench[1] = new Date().getTime(); + alert(s + ' ' + ((TableKit.Bench.bench[1]-TableKit.Bench.bench[0])/1000)+' seconds.') //console.log(s + ' ' + ((TableKit.Bench.bench[1]-TableKit.Bench.bench[0])/1000)+' seconds.') + TableKit.Bench.bench = []; + } +} */ + +if(window.FastInit) { + FastInit.addOnLoad(TableKit.load); +} else { + Event.observe(window, 'load', TableKit.load); +} diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am b/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am new file mode 100644 index 00000000..4a7ff58d --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am @@ -0,0 +1,21 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/subcontrollers +app_PYTHON = \ + __init__.py \ + group.py \ + ipacontroller.py \ + ipapolicy.py \ + policy.py \ + user.py \ + delegation.py \ + principal.py \ + $(NULL) + +EXTRA_DIST = \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/__init__.py b/ipa-server/ipa-gui/ipagui/subcontrollers/__init__.py new file mode 100644 index 00000000..143f486c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py b/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py new file mode 100644 index 00000000..3f80da52 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py @@ -0,0 +1,415 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +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 ipagui.forms.delegate +import ipa.aci + +import ldap.dn +import operator + +log = logging.getLogger(__name__) + +aci_fields = ['*', 'aci'] + +delegate_form = ipagui.forms.delegate.DelegateForm() + +class DelegationController(IPAController): + + @expose() + @identity.require(identity.not_anonymous()) + def index(self, tg_errors=None): + raise turbogears.redirect("/delegate/list") + + @expose("ipagui.templates.delegatenew") + @identity.require(identity.in_group("admins")) + def new(self): + """Display delegate page""" + client = self.get_ipaclient() + delegate = {} + delegate['source_group_cn'] = "Please choose:" + delegate['dest_group_cn'] = "Please choose:" + + return dict(form=delegate_form, delegate=delegate) + + @expose() + @identity.require(identity.in_group("admins")) + def create(self, **kw): + """Creates a new delegation""" + self.restrict_post() + client = self.get_ipaclient() + + if kw.get('submit', '').startswith('Cancel'): + turbogears.flash("Add delegation cancelled") + raise turbogears.redirect('/delegate/list') + + # Try to handle the case where the user entered just some data + # into the source/dest group name but didn't do a Find. We'll do + # our best to see if a group by that name exists and if so, use it. + dest_group_dn = kw.get('dest_group_dn') + dest_group_cn = kw.get('dest_group_cn') + if not dest_group_dn and dest_group_cn: + try: + group = client.get_entry_by_cn(dest_group_cn, ['dn']) + kw['dest_group_dn'] = group.dn + except: + kw['dest_group_cn'] = "Please choose:" + source_group_dn = kw.get('source_group_dn') + source_group_cn = kw.get('source_group_cn') + if not source_group_dn and source_group_cn: + try: + group = client.get_entry_by_cn(source_group_cn, ['dn']) + kw['source_group_dn'] = group.dn + except: + kw['source_group_cn'] = "Please choose:" + tg_errors, kw = self.delegatevalidate(**kw) + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + return dict(form=delegate_form, delegate=kw, + 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 isinstance(new_aci.attrs, basestring): + 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']) + aci_entry.setValue('aci', new_aci.export_to_string()) + + client.update_entry(aci_entry) + + # Now add to the editors group so they can make changes in the UI + try: + group = client.get_entry_by_cn("editors") + client.add_group_to_group(new_aci.source_group, group.dn) + except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_EMPTY_MODLIST): + # This is ok, ignore it + pass + + except ipaerror.IPAError, e: + turbogears.flash("Delgate add failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=delegate_form, delegate=kw, + tg_template='ipagui.templates.delegatenew') + + turbogears.flash("delegate created") + raise turbogears.redirect('/delegate/list') + + @expose("ipagui.templates.delegateedit") + @identity.require(identity.in_group("admins")) + def edit(self, acistr, tg_errors=None): + """Display delegate page""" + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + + client = self.get_ipaclient() + + try: + aci_entry = client.get_aci_entry(aci_fields) + aci = ipa.aci.ACI(acistr) + group_dn_to_cn = ipa.aci.extract_group_cns([aci], client) + + delegate = aci.to_dict() + delegate['source_group_dn'] = delegate['source_group'] + delegate['source_group_cn'] = group_dn_to_cn[delegate['source_group_dn']] + delegate['dest_group_dn'] = delegate['dest_group'] + delegate['dest_group_cn'] = group_dn_to_cn[delegate['dest_group_dn']] + + return dict(form=delegate_form, delegate=delegate) + except (SyntaxError, ipaerror.IPAError), e: + turbogears.flash("Delegation edit failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect('/delegate/list') + + + @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 delegation cancelled") + raise turbogears.redirect('/delegate/list') + + # Try to handle the case where the user entered just some data + # into the source/dest group name but didn't do a Find. We'll do + # our best to see if a group by that name exists and if so, use it. + dest_group_cn = kw.get('dest_group_cn') + if dest_group_cn: + try: + group = client.get_entry_by_cn(dest_group_cn, ['dn']) + kw['dest_group_dn'] = group.dn + except: + # This _notfound value is used in delegatevalidate() + kw['dest_group_cn_notfound'] = True + source_group_cn = kw.get('source_group_cn') + if source_group_cn: + try: + group = client.get_entry_by_cn(source_group_cn, ['dn']) + kw['source_group_dn'] = group.dn + except: + # This _notfound value is used in delegatevalidate() + kw['source_group_cn_notfound'] = True + + tg_errors, kw = self.delegatevalidate(**kw) + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + return dict(form=delegate_form, delegate=kw, + tg_template='ipagui.templates.delegateedit') + + try: + aci_entry = client.get_aci_entry(aci_fields) + + 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] + + try : + old_aci_index = aci_str_list.index(kw['orig_acistr']) + except ValueError: + turbogears.flash("Delegation update failed:<br />" + + "The delegation you were attempting to update has been " + + "concurrently modified. Please cancel the edit " + + "and try editing the delegation again.") + return dict(form=delegate_form, delegate=kw, + tg_template='ipagui.templates.delegateedit') + + 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 isinstance(new_aci.attrs, basestring): + new_aci.attrs = [new_aci.attrs] + new_aci_str = new_aci.export_to_string() + + new_aci_str_list = copy.copy(aci_str_list) + old_aci = ipa.aci.ACI(new_aci_str_list[old_aci_index]) + new_aci_str_list[old_aci_index] = new_aci_str + aci_entry.setValue('aci', new_aci_str_list) + + client.update_entry(aci_entry) + + if new_aci.source_group != old_aci.source_group: + aci_list = [] + last = True + for aci_str in new_aci_str_list: + try: + aci = ipa.aci.ACI(aci_str) + if aci.source_group == old_aci.source_group: + last = False + break + except SyntaxError: + # ignore aci_str's that ACI can't parse + pass + if last: + group = client.get_entry_by_cn("editors") + client.remove_member_from_group(old_aci.source_group, group.dn) + + # Now add to the editors group so they can make changes in the UI + try: + group = client.get_entry_by_cn("editors") + client.add_group_to_group(new_aci.source_group, group.dn) + except ipa.ipaerror.exception_for(ipa.ipaerror.LDAP_EMPTY_MODLIST): + # This is ok, ignore it + pass + + + turbogears.flash("delegate updated") + raise turbogears.redirect('/delegate/list') + except (SyntaxError, ipaerror.IPAError), e: + turbogears.flash("Delegation update failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=delegate_form, delegate=kw, + tg_template='ipagui.templates.delegateedit') + + @expose("ipagui.templates.delegatelist") + @identity.require(identity.not_anonymous()) + def list(self): + """Display delegate page""" + client = self.get_ipaclient() + + try: + aci_entry = client.get_aci_entry(aci_fields) + except ipaerror.IPAError, e: + turbogears.flash("Delegation list failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect('/') + + 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] + + aci_list = [] + for aci_str in aci_str_list: + try: + aci = ipa.aci.ACI(aci_str) + aci_list.append(aci) + except SyntaxError: + # ignore aci_str's that ACI can't parse + 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: + aci.attrs = map(lambda name: + ipagui.forms.delegate.aci_name_to_label.get(name, name), + aci.attrs) + + return dict(aci_list=aci_list, group_dn_to_cn=group_dn_to_cn, + fields=ipagui.forms.delegate.DelegateFields()) + + @expose() + @identity.require(identity.in_group("admins")) + def delete(self, acistr): + """Display delegate page""" + self.restrict_post() + client = self.get_ipaclient() + + try: + aci_entry = client.get_aci_entry(aci_fields) + + 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] + + try : + old_aci_index = aci_str_list.index(acistr) + except ValueError: + turbogears.flash("Delegation deletion failed:<br />" + + "The delegation you were attempting to delete has been " + + "concurrently modified.") + raise turbogears.redirect('/delegate/list') + + old_aci = ipa.aci.ACI(aci_str_list[old_aci_index]) + new_aci_str_list = copy.copy(aci_str_list) + del new_aci_str_list[old_aci_index] + aci_entry.setValue('aci', new_aci_str_list) + + client.update_entry(aci_entry) + + aci_list = [] + last = True + for aci_str in new_aci_str_list: + try: + aci = ipa.aci.ACI(aci_str) + if aci.source_group == old_aci.source_group: + last = False + break + except SyntaxError: + # ignore aci_str's that ACI can't parse + pass + if last: + group = client.get_entry_by_cn("editors") + client.remove_member_from_group(old_aci.source_group, group.dn) + + turbogears.flash("delegate deleted") + raise turbogears.redirect('/delegate/list') + except (SyntaxError, ipaerror.IPAError), e: + turbogears.flash("Delegation deletion failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect('/delegate/list') + + @expose("ipagui.templates.delegategroupsearch") + @identity.require(identity.not_anonymous()) + def group_search(self, **kw): + """Searches for groups and displays list of results in a table. + This method is used for the ajax search on the delegation pages.""" + client = self.get_ipaclient() + + groups = [] + groups_counter = 0 + searchlimit = 100 + criteria = kw.get('criteria') + if criteria != None and len(criteria) > 0: + try: + groups = client.find_groups(criteria.encode('utf-8'), None, + searchlimit) + groups_counter = groups[0] + groups = groups[1:] + except ipaerror.IPAError, e: + turbogears.flash("search failed: " + str(e)) + + return dict(groups=groups, criteria=criteria, + which_group=kw.get('which_group'), + counter=groups_counter) + + @validate(form=delegate_form) + @identity.require(identity.not_anonymous()) + def delegatevalidate(self, tg_errors=None, **kw): + # We are faking this because otherwise it shows up as one huge + # block of color in the UI when it has a not empty validator. + if not tg_errors: + tg_errors = {} + if not kw.get('attrs'): + tg_errors['attrs'] = _("Please select at least one value") + if kw.get('dest_group_cn_notfound'): + tg_errors['dest_group_dn'] = _("Group not found") + if kw.get('source_group_cn_notfound'): + tg_errors['source_group_dn'] = _("Group not found") + cherrypy.request.validation_errors = tg_errors + return tg_errors, kw diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/group.py b/ipa-server/ipa-gui/ipagui/subcontrollers/group.py new file mode 100644 index 00000000..6196d13d --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/group.py @@ -0,0 +1,484 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +from pickle import dumps, loads +from base64 import b64encode, b64decode +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 +import ipa.config +import ipa.group +from ipa.entity import utf8_encode_values +from ipa import ipaerror +import ipagui.forms.group + +log = logging.getLogger(__name__) + +group_new_form = ipagui.forms.group.GroupNewForm() +group_edit_form = ipagui.forms.group.GroupEditForm() + +group_fields = ['*', 'nsAccountLock'] + +class GroupController(IPAController): + + + ######### + # Group # + ######### + + @expose() + @identity.require(identity.not_anonymous()) + def index(self, tg_errors=None): + raise turbogears.redirect("/group/list") + + @expose("ipagui.templates.groupnew") + @identity.require(identity.in_group("admins")) + def new(self, tg_errors=None): + """Displays the new group form""" + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + + client = self.get_ipaclient() + + return dict(form=group_new_form, group={}) + + @expose() + @identity.require(identity.in_group("admins")) + def create(self, **kw): + """Creates a new group""" + self.restrict_post() + client = self.get_ipaclient() + + if kw.get('submit') == 'Cancel': + turbogears.flash("Add group cancelled") + raise turbogears.redirect('/') + + tg_errors, kw = self.groupcreatevalidate(**kw) + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + return dict(form=group_new_form, group=kw, + tg_template='ipagui.templates.groupnew') + + # + # Create the group itself + # + try: + new_group = ipa.group.Group() + new_group.setValue('cn', kw.get('cn')) + 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/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=group_new_form, group=kw, + tg_template='ipagui.templates.groupnew') + + # + # NOTE: from here on, the group now exists. + # on any error, we redirect to the _edit_ group page. + # this code does data setup, similar to groupedit() + # + 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 = [] + + # store a copy of the original group for the update later + group_data = b64encode(dumps(group_dict)) + member_data = b64encode(dumps(member_dicts)) + group_dict['group_orig'] = group_data + group_dict['member_data'] = member_data + + # preserve group add info in case of errors + group_dict['dnadd'] = kw.get('dnadd') + group_dict['dn_to_info_json'] = kw.get('dn_to_info_json') + + # + # Add members + # + failed_adds = [] + try: + dnadds = kw.get('dnadd') + if dnadds != None: + if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)): + dnadds = [dnadds] + failed_adds = client.add_members_to_group( + utf8_encode_values(dnadds), group.dn) + kw['dnadd'] = failed_adds + except ipaerror.IPAError, e: + failed_adds = dnadds + + if len(failed_adds) > 0: + message = "Group successfully created.<br />" + message += "There was an error adding group members.<br />" + message += "Failures have been preserved in the add/remove lists." + turbogears.flash(message) + return dict(form=group_edit_form, group=group_dict, + members=member_dicts, + tg_template='ipagui.templates.groupedit') + + turbogears.flash("%s added!" % kw.get('cn')) + raise turbogears.redirect('/group/show', cn=kw.get('cn')) + + @expose("ipagui.templates.dynamiceditsearch") + @identity.require(identity.not_anonymous()) + def edit_search(self, **kw): + """Searches for users+groups and displays list of results in a table. + This method is used for the ajax search on the group edit page.""" + client = self.get_ipaclient() + + users = [] + groups = [] + counter = 0 + searchlimit = 100 + criteria = kw.get('criteria') + if criteria != None and len(criteria) > 0: + try: + users = client.find_users(criteria.encode('utf-8'), None, searchlimit) + users_counter = users[0] + users = users[1:] + + groups = client.find_groups(criteria.encode('utf-8'), None, + searchlimit) + groups_counter = groups[0] + groups = groups[1:] + + if users_counter < 0 or groups_counter < 0: + counter = -1 + else: + counter = users_counter + groups_counter + except ipaerror.IPAError, e: + turbogears.flash("search failed: " + str(e)) + + return dict(users=users, groups=groups, criteria=criteria, + counter=counter) + + + @expose("ipagui.templates.groupedit") + @identity.require(identity.in_group("admins")) + def edit(self, cn, tg_errors=None): + """Displays the edit group form""" + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + + client = self.get_ipaclient() + + try: + group = client.get_entry_by_cn(cn, group_fields) + + group_dict = group.toDict() + + # + # convert members to users, for easier manipulation on the page + # + + members = client.group_members(group.dn, ['dn', 'givenname', 'sn', 'uid', 'cn'], 1) + members = members[1:] + members.sort(self.sort_group_member) + + # Map users into an array of dicts, which can be serialized + # (so we don't have to do this on each round trip) + member_dicts = map(lambda member: member.toDict(), members) + + indirect_members = client.group_members(group.dn, ['dn', 'givenname', 'sn', 'uid', 'cn'], 2) + indirect_members = indirect_members[1:] + indirect_members.sort(self.sort_group_member) + + # add our own flag + for i in range(len(indirect_members)): + indirect_members[i].setValue('inherited', True) + + # Map users into an array of dicts, which can be serialized + # (so we don't have to do this on each round trip) + indirect_members_dicts = map(lambda member: member.toDict(), indirect_members) + + member_dicts = member_dicts + indirect_members_dicts + + # store a copy of the original group for the update later + group_data = b64encode(dumps(group_dict)) + member_data = b64encode(dumps(member_dicts)) + group_dict['group_orig'] = group_data + group_dict['member_data'] = member_data + + return dict(form=group_edit_form, group=group_dict, members=member_dicts) + except ipaerror.IPAError, e: + turbogears.flash("Group edit failed: " + str(e)) + raise turbogears.redirect('/group/show', uid=cn) + + @expose() + @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,basestring)): + cn = [cn] + turbogears.flash("Edit group cancelled") + 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/>" + + "Please see the messages below for details.") + return dict(form=group_edit_form, group=kw, members=member_dicts, + tg_template='ipagui.templates.groupedit') + + group_modified = False + + # + # Update group itself + # + try: + orig_group_dict = loads(b64decode(kw.get('group_orig'))) + + new_group = ipa.group.Group(orig_group_dict) + if new_group.description != kw.get('description'): + group_modified = True + new_group.setValue('description', kw.get('description')) + if kw.get('editprotected') == 'true': + new_gid = str(kw.get('gidnumber')) + 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 + # need to make sure a subsequent submit doesn't try to update + # the group again. + # + kw['group_orig'] = b64encode(dumps(new_group.toDict())) + except ipaerror.IPAError, e: + turbogears.flash("Group update failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + 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 + try: + 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 + except ipaerror.IPAError, e: + turbogears.flash("Group status change failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=group_edit_form, group=kw, members=member_dicts, + tg_template='ipagui.templates.groupedit') + + # + # Add members + # + failed_adds = [] + try: + dnadds = kw.get('dnadd') + if dnadds != None: + if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)): + dnadds = [dnadds] + 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("Updating group membership failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=group_edit_form, group=kw, members=member_dicts, + tg_template='ipagui.templates.groupedit') + + # + # Remove members + # + failed_dels = [] + try: + dndels = kw.get('dndel') + if dndels != None: + if not(isinstance(dndels,list) or isinstance(dndels,tuple)): + dndels = [dndels] + 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("Updating group membership failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=group_edit_form, group=kw, members=member_dicts, + tg_template='ipagui.templates.groupedit') + + # + # TODO - check failed ops to see if it's because of another update. + # handle "someone else already did it" errors better - perhaps + # not even as an error + # TODO - update the Group Members list. + # (note that we have to handle the above todo first, or else + # there will be an error message, but the add/del lists will + # be empty) + # + if (len(failed_adds) > 0) or (len(failed_dels) > 0): + message = "There was an error updating group members.<br />" + message += "Failures have been preserved in the add/remove lists." + if group_modified: + message = "Group Details successfully updated.<br />" + message + turbogears.flash(message) + return dict(form=group_edit_form, group=kw, members=member_dicts, + tg_template='ipagui.templates.groupedit') + + 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") + @identity.require(identity.not_anonymous()) + def list(self, **kw): + """Search for groups and display results""" + client = self.get_ipaclient() + + groups = None + # counter = 0 + criteria = kw.get('criteria') + if criteria != None and len(criteria) > 0: + try: + groups = client.find_groups(criteria.encode('utf-8'), None, 0, 2) + counter = groups[0] + groups = groups[1:] + if counter == -1: + 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) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect("/group/list") + + return dict(groups=groups, criteria=criteria, + fields=ipagui.forms.group.GroupFields()) + + @expose("ipagui.templates.groupshow") + @identity.require(identity.not_anonymous()) + def show(self, cn): + """Retrieve a single group for display""" + client = self.get_ipaclient() + + try: + group = client.get_entry_by_cn(cn, group_fields) + group_dict = group.toDict() + + # + # convert members to users, for display on the page + # + + members = client.group_members(group.dn, ['dn', 'givenname', 'sn', 'uid', 'cn'], 1) + members = members[1:] + members.sort(self.sort_group_member) + member_dicts = map(lambda member: member.toDict(), members) + + indirect_members = client.group_members(group.dn, ['dn', 'givenname', 'sn', 'uid', 'cn'], 2) + indirect_members = indirect_members[1:] + indirect_members.sort(self.sort_group_member) + + # add our own flag + for i in range(len(indirect_members)): + indirect_members[i].setValue('inherited', True) + + # Map users into an array of dicts, which can be serialized + # (so we don't have to do this on each round trip) + indirect_members_dicts = map(lambda member: member.toDict(), indirect_members) + + member_dicts = member_dicts + indirect_members_dicts + logging.info("%s" % member_dicts) + + return dict(group=group_dict, fields=ipagui.forms.group.GroupFields(), + members = member_dicts) + except ipaerror.IPAError, e: + turbogears.flash("Group show failed: " + str(e)) + raise turbogears.redirect("/") + + @expose() + @identity.require(identity.not_anonymous()) + def delete(self, dn): + """Delete group.""" + self.restrict_post() + client = self.get_ipaclient() + + try: + client.delete_group(dn) + + turbogears.flash("group deleted") + raise turbogears.redirect('/group/list') + except (SyntaxError, ipaerror.IPAError), e: + turbogears.flash("Group deletion failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect('/group/list') + + @validate(form=group_new_form) + @identity.require(identity.not_anonymous()) + def groupcreatevalidate(self, tg_errors=None, **kw): + return tg_errors, kw + + @validate(form=group_edit_form) + @identity.require(identity.not_anonymous()) + def groupupdatevalidate(self, tg_errors=None, **kw): + return tg_errors, kw + diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/ipacontroller.py b/ipa-server/ipa-gui/ipagui/subcontrollers/ipacontroller.py new file mode 100644 index 00000000..db7f04cb --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/ipacontroller.py @@ -0,0 +1,92 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import os +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 + +import ipa.ipaclient +from ipaserver import funcs +import ipa.config + +log = logging.getLogger(__name__) + +ipa.config.init_config() + +class IPAController(controllers.Controller): + def restrict_post(self): + if cherrypy.request.method != "POST": + turbogears.flash("This method only accepts posts") + raise turbogears.redirect("/") + + def get_ipaclient(self): + transport = funcs.IPAServer() + client = ipa.ipaclient.IPAClient(transport) + client.set_krbccache(os.environ["KRB5CCNAME"]) + return client + + def utf8_encode(self, value): + if value != None: + value = value.encode('utf-8') + return value + + def sort_group_member(self, a, b): + """Comparator function used for sorting group members.""" + if a.getValue('uid') and b.getValue('uid'): + if a.getValue('sn') == b.getValue('sn'): + if a.getValue('givenName') == b.getValue('givenName'): + if a.getValue('uid') == b.getValue('uid'): + return 0 + elif a.getValue('uid') < b.getValue('uid'): + return -1 + else: + return 1 + elif a.getValue('givenName') < b.getValue('givenName'): + return -1 + else: + return 1 + elif a.getValue('sn') < b.getValue('sn'): + return -1 + else: + return 1 + elif a.getValue('uid'): + return -1 + elif b.getValue('uid'): + return 1 + else: + if a.getValue('cn') == b.getValue('cn'): + return 0 + elif a.getValue('cn') < b.getValue('cn'): + return -1 + else: + return 1 + + def sort_by_cn(self, a, b): + """Comparator function used for sorting groups.""" + if a.getValue('cn') == b.getValue('cn'): + return 0 + elif a.getValue('cn') < b.getValue('cn'): + return -1 + else: + return 1 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 00000000..1db062b4 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py @@ -0,0 +1,208 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +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 +from ipagui.helpers import ipahelper + +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) + + # Load potential multi-valued fields + if isinstance(ipapolicy_dict.get('ipauserobjectclasses',''), basestring): + ipapolicy_dict['ipauserobjectclasses'] = [ipapolicy_dict.get('ipauserobjectclasses')] + ipapolicy_dict['userobjectclasses'] = ipahelper.setup_mv_fields(ipapolicy_dict.get('ipauserobjectclasses'), 'ipauserobjectclasses') + + if isinstance(ipapolicy_dict.get('ipagroupobjectclasses',''), basestring): + ipapolicy_dict['ipagroupobjectclasses'] = [ipapolicy_dict.get('ipagroupobjectclasses')] + ipapolicy_dict['groupobjectclasses'] = ipahelper.setup_mv_fields(ipapolicy_dict.get('ipagroupobjectclasses'), 'ipagroupobjectclasses') + + 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') + + # Fix incoming multi-valued fields we created for the form + kw = ipahelper.fix_incoming_fields(kw, 'ipauserobjectclasses', 'userobjectclasses') + kw = ipahelper.fix_incoming_fields(kw, 'ipagroupobjectclasses', 'groupobjectclasses') + + 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.getValues('ipasearchtimelimit')) != str(kw.get('ipasearchtimelimit')): + policy_modified = True + new_ipapolicy.setValue('ipasearchtimelimit', kw.get('ipasearchtimelimit')) + if str(new_ipapolicy.getValues('ipasearchrecordslimit')) != str(kw.get('ipasearchrecordslimit')): + policy_modified = True + new_ipapolicy.setValue('ipasearchrecordslimit', kw.get('ipasearchrecordslimit')) + if new_ipapolicy.getValues('ipausersearchfields') != kw.get('ipausersearchfields'): + policy_modified = True + new_ipapolicy.setValue('ipausersearchfields', kw.get('ipausersearchfields')) + if new_ipapolicy.getValues('ipagroupsearchfields') != kw.get('ipagroupsearchfields'): + policy_modified = True + new_ipapolicy.setValue('ipagroupsearchfields', kw.get('ipagroupsearchfields')) + if str(new_ipapolicy.getValues('ipapwdexpadvnotify')) != str(kw.get('ipapwdexpadvnotify')): + policy_modified = True + new_ipapolicy.setValue('ipapwdexpadvnotify', kw.get('ipapwdexpadvnotify')) + if str(new_ipapolicy.getValues('ipamaxusernamelength')) != str(kw.get('ipamaxusernamelength')): + policy_modified = True + new_ipapolicy.setValue('ipamaxusernamelength', kw.get('ipamaxusernamelength')) + if new_ipapolicy.getValues('ipahomesrootdir') != kw.get('ipahomesrootdir'): + policy_modified = True + new_ipapolicy.setValue('ipahomesrootdir', kw.get('ipahomesrootdir')) + if new_ipapolicy.getValues('ipadefaultloginshell') != kw.get('ipadefaultloginshell'): + policy_modified = True + new_ipapolicy.setValue('ipadefaultloginshell', kw.get('ipadefaultloginshell')) + if new_ipapolicy.getValues('ipadefaultprimarygroup') != kw.get('ipadefaultprimarygroup'): + policy_modified = True + new_ipapolicy.setValue('ipadefaultprimarygroup', kw.get('ipadefaultprimarygroup')) +# if new_ipapolicy.getValues('ipauserobjectclasses') != kw.get('ipauserobjectclasses'): +# policy_modified = True +# new_ipapolicy.setValue('ipauserobjectclasses', kw.get('ipauserobjectclasses')) +# if new_ipapolicy.getValues('ipagroupobjectclasses') != kw.get('ipagroupobjectclasses'): +# policy_modified = True +# new_ipapolicy.setValue('ipagroupobjectclasses', kw.get('ipagroupobjectclasses')) + if new_ipapolicy.getValues('ipadefaultemaildomain') != kw.get('ipadefaultemaildomain'): + policy_modified = True + new_ipapolicy.setValue('ipadefaultemaildomain', kw.get('ipadefaultemaildomain')) + + if policy_modified: + rv = client.update_ipa_config(new_ipapolicy) + + # Now check the password policy for updates + if str(new_password.getValues('krbmaxpwdlife')) != str(kw.get('krbmaxpwdlife')): + password_modified = True + new_password.setValue('krbmaxpwdlife', str(kw.get('krbmaxpwdlife'))) + if str(new_password.getValues('krbminpwdlife')) != str(kw.get('krbminpwdlife')): + password_modified = True + new_password.setValue('krbminpwdlife', str(kw.get('krbminpwdlife'))) + if str(new_password.getValues('krbpwdhistorylength')) != str(kw.get('krbpwdhistorylength')): + password_modified = True + new_password.setValue('krbpwdhistorylength', str(kw.get('krbpwdhistorylength'))) + if str(new_password.getValues('krbpwdmindiffchars')) != str(kw.get('krbpwdmindiffchars')): + password_modified = True + new_password.setValue('krbpwdmindiffchars', str(kw.get('krbpwdmindiffchars'))) + if str(new_password.getValues('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) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + 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 00000000..8b905335 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py @@ -0,0 +1,49 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +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/principal.py b/ipa-server/ipa-gui/ipagui/subcontrollers/principal.py new file mode 100644 index 00000000..3c3d9463 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/principal.py @@ -0,0 +1,193 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +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 ipagui.forms.principal + +import ldap.dn + +log = logging.getLogger(__name__) + +principal_new_form = ipagui.forms.principal.PrincipalNewForm() +principal_fields = ['*'] + +class PrincipalController(IPAController): + + @expose() + @identity.require(identity.in_group("admins")) + def index(self, tg_errors=None): + raise turbogears.redirect("/principal/list") + + @expose("ipagui.templates.principalnew") + @identity.require(identity.in_group("admins")) + def new(self, tg_errors=None): + """Displays the new service principal form""" + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + + client = self.get_ipaclient() + + return dict(form=principal_new_form, principal={}) + + @expose() + @identity.require(identity.in_group("admins")) + def create(self, **kw): + """Creates a service principal group""" + self.restrict_post() + client = self.get_ipaclient() + + if kw.get('submit') == 'Cancel': + turbogears.flash("Add principal cancelled") + raise turbogears.redirect('/') + + tg_errors, kw = self.principalcreatevalidate(**kw) + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + return dict(form=principal_new_form, principal=kw, + tg_template='ipagui.templates.principalnew') + + principal_name = "" + hostname = kw.get('hostname') + # + # Create the principal itself + # + try: + if kw.get('service') == "other": + service = kw.get('other') + if not service: + turbogears.flash("Service type must be provided") + return dict(form=principal_new_form, principal=kw, + tg_template='ipagui.templates.principalnew') + else: + service = kw.get('service') + + # The realm is added by add_service_principal + principal_name = utf8_encode_values(service + "/" + kw.get('hostname')) + + rv = client.add_service_principal(principal_name, 0) + except ipaerror.exception_for(ipaerror.LDAP_DUPLICATE): + turbogears.flash("Service principal '%s' already exists" % + principal_name) + return dict(form=principal_new_form, principal=kw, + tg_template='ipagui.templates.principalnew') + except ipaerror.IPAError, e: + turbogears.flash("Service principal add failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=principal_new_form, principal=kw, + tg_template='ipagui.templates.principalnew') + + turbogears.flash("%s added!" % principal_name) + raise turbogears.redirect('/principal/list', hostname=hostname) + + @expose("ipagui.templates.principallist") + @identity.require(identity.not_anonymous()) + def list(self, **kw): + """Searches for service principals and displays list of results""" + client = self.get_ipaclient() + + principals = None + counter = 0 + hostname = kw.get('hostname') + if hostname != None and len(hostname) > 0: + try: + principals = client.find_service_principal(hostname.encode('utf-8'), principal_fields, 0, 2) + counter = principals[0] + principals = principals[1:] + + if counter == -1: + turbogears.flash("These results are truncated.<br />" + + "Please refine your search and try again.") + + # For each entry break out service type and hostname + for i in range(len(principals)): + (service,host) = principals[i].krbprincipalname.split('/') + h = host.split('@') + principals[i].setValue('service', service) + principals[i].setValue('hostname', h[0]) + + except ipaerror.IPAError, e: + turbogears.flash("principal list failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect("/principal/list") + + return dict(principals=principals, hostname=hostname, fields=ipagui.forms.principal.PrincipalFields()) + + @expose("ipagui.templates.principalshow") + @identity.require(identity.not_anonymous()) + def show(self, **kw): + """Display a single service principal""" + + try: + princ = kw['principal'] + princ_dn = kw['principal_dn'] + except KeyError, e: + turbogears.flash("Principal show failed. Unable to find key %s" % e) + raise turbogears.redirect("/principal/list") + + principal = {} + + try: + # The principal info is passed in. Not going to both to re-query this. + (service,host) = princ.split('/') + h = host.split('@') + principal['service'] = service + principal['hostname'] = h[0] + principal['principal_dn'] = princ_dn + + return dict(principal=principal) + except: + turbogears.flash("Principal show failed %s" % princ) + raise turbogears.redirect("/") + + @expose() + @identity.require(identity.in_group("admins")) + def delete(self, principal): + """Delete a service principal""" + self.restrict_post() + client = self.get_ipaclient() + + print "Deleting %s" % principal + + try: + client.delete_service_principal(principal) + + turbogears.flash("Service principal deleted") + raise turbogears.redirect('/principal/list') + except (SyntaxError, ipaerror.IPAError), e: + turbogears.flash("Service principal deletion failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect('/principal/list') + + @validate(form=principal_new_form) + @identity.require(identity.not_anonymous()) + def principalcreatevalidate(self, tg_errors=None, **kw): + return tg_errors, kw diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/user.py b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py new file mode 100644 index 00000000..d8fabb6b --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py @@ -0,0 +1,854 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import re +import random +from pickle import dumps, loads +from base64 import b64encode, b64decode +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 +import ipa.user +from ipa.entity import utf8_encode_values +from ipa import ipaerror +import ipagui.forms.user +import ipa.config +from ipagui.helpers import ipahelper + +log = logging.getLogger(__name__) + +password_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +user_new_form = ipagui.forms.user.UserNewForm() +user_edit_form = ipagui.forms.user.UserEditForm() + +user_fields = ['*', 'nsAccountLock'] + +class UserController(IPAController): + + def __init__(self, *args, **kw): + super(UserController,self).__init__(*args, **kw) +# self.load_custom_fields() + + def get_email_domain(self): + client = self.get_ipaclient() + + conf = client.get_ipa_config() + email_domain = conf.ipadefaultemaildomain + + return email_domain + + def load_custom_fields(self): + + 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'].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) + + user_new_form.validator.add_field(s['field'], validator) + user_edit_form.validator.add_field(s['field'], validator) + + def initialize_mv_fields(self, user_dict): + """We use a separate attribute to store multi-values while on + the edit page. It is important that this be at least []. If + it is None it will cause an error to be thrown.""" + + # Load potential multi-valued fields + if isinstance(user_dict['cn'], basestring): + user_dict['cn'] = [user_dict['cn']] + user_dict['cns'] = ipahelper.setup_mv_fields(user_dict['cn'], 'cn') + + if isinstance(user_dict.get('telephonenumber',''), basestring): + user_dict['telephonenumber'] = [user_dict.get('telephonenumber')] + user_dict['telephonenumbers'] = ipahelper.setup_mv_fields(user_dict.get('telephonenumber'), 'telephonenumber') + + if isinstance(user_dict.get('facsimiletelephonenumber',''), basestring): + user_dict['facsimiletelephonenumber'] = [user_dict.get('facsimiletelephonenumber')] + user_dict['facsimiletelephonenumbers'] = ipahelper.setup_mv_fields(user_dict.get('facsimiletelephonenumber'), 'facsimiletelephonenumber') + + if isinstance(user_dict.get('mobile',''), basestring): + user_dict['mobile'] = [user_dict.get('mobile')] + user_dict['mobiles'] = ipahelper.setup_mv_fields(user_dict.get('mobile'), 'mobile') + + if isinstance(user_dict.get('pager',''), basestring): + user_dict['pager'] = [user_dict.get('pager')] + user_dict['pagers'] = ipahelper.setup_mv_fields(user_dict.get('pager'), 'pager') + + if isinstance(user_dict.get('homephone',''), basestring): + user_dict['homephone'] = [user_dict.get('homephone')] + user_dict['homephones'] = ipahelper.setup_mv_fields(user_dict.get('homephone'), 'homephone') + + return user_dict + + @expose() + def index(self): + raise turbogears.redirect("/user/list") + + @expose("ipagui.templates.usernew") + @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.") + + return dict(form=user_new_form, user={}) + + @expose() + @identity.require(identity.in_any_group("admins","editors")) + def create(self, **kw): + """Creates a new user""" + self.restrict_post() + client = self.get_ipaclient() + + if kw.get('submit') == 'Cancel': + turbogears.flash("Add user cancelled") + raise turbogears.redirect('/user/list') + + # Fix incoming multi-valued fields we created for the form + kw = ipahelper.fix_incoming_fields(kw, 'cn', 'cns') + kw = ipahelper.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers') + kw = ipahelper.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers') + kw = ipahelper.fix_incoming_fields(kw, 'mobile', 'mobiles') + kw = ipahelper.fix_incoming_fields(kw, 'pager', 'pagers') + kw = ipahelper.fix_incoming_fields(kw, 'homephone', 'homephones') + + tg_errors, kw = self.usercreatevalidate(**kw) + + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + return dict(form=user_new_form, user=kw, + tg_template='ipagui.templates.usernew') + + # + # Create the user itself + # + try: + new_user = ipa.user.User() + new_user.setValueNotEmpty('title', kw.get('title')) + new_user.setValueNotEmpty('givenname', kw.get('givenname')) + new_user.setValueNotEmpty('sn', kw.get('sn')) + new_user.setValueNotEmpty('cn', kw.get('cn')) + new_user.setValueNotEmpty('displayname', kw.get('displayname')) + new_user.setValueNotEmpty('initials', kw.get('initials')) + + new_user.setValueNotEmpty('uid', kw.get('uid')) + new_user.setValueNotEmpty('loginshell', kw.get('loginshell')) + new_user.setValueNotEmpty('gecos', kw.get('gecos')) + + new_user.setValueNotEmpty('mail', kw.get('mail')) + new_user.setValueNotEmpty('telephonenumber', kw.get('telephonenumber')) + new_user.setValueNotEmpty('facsimiletelephonenumber', + kw.get('facsimiletelephonenumber')) + new_user.setValueNotEmpty('mobile', kw.get('mobile')) + new_user.setValueNotEmpty('pager', kw.get('pager')) + new_user.setValueNotEmpty('homephone', kw.get('homephone')) + + new_user.setValueNotEmpty('street', kw.get('street')) + new_user.setValueNotEmpty('l', kw.get('l')) + new_user.setValueNotEmpty('st', kw.get('st')) + new_user.setValueNotEmpty('postalcode', kw.get('postalcode')) + + new_user.setValueNotEmpty('ou', kw.get('ou')) + new_user.setValueNotEmpty('businesscategory', kw.get('businesscategory')) + new_user.setValueNotEmpty('description', kw.get('description')) + new_user.setValueNotEmpty('employeetype', kw.get('employeetype')) + if kw.get('manager'): + new_user.setValueNotEmpty('manager', kw.get('manager')) + new_user.setValueNotEmpty('roomnumber', kw.get('roomnumber')) + if kw.get('secretary'): + new_user.setValueNotEmpty('secretary', kw.get('secretary')) + + new_user.setValueNotEmpty('carlicense', kw.get('carlicense')) + new_user.setValueNotEmpty('labeleduri', kw.get('labeleduri')) + + for custom_field in user_new_form.custom_fields: + new_user.setValueNotEmpty(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("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) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=user_new_form, user=kw, + tg_template='ipagui.templates.usernew') + + # + # NOTE: from here on, the user account now exists. + # on any error, we redirect to the _edit_ user page. + # this code does data setup, similar to useredit() + # + user = client.get_user_by_uid(kw['uid'], user_fields) + user_dict = user.toDict() + + user_groups_dicts = [] + user_groups_data = b64encode(dumps(user_groups_dicts)) + + # store a copy of the original user for the update later + user_data = b64encode(dumps(user_dict)) + user_dict['user_orig'] = user_data + user_dict['user_groups_data'] = user_groups_data + + # preserve group add info in case of errors + user_dict['dnadd'] = kw.get('dnadd') + user_dict['dn_to_info_json'] = kw.get('dn_to_info_json') + + # + # Set the Password + # + if kw.get('krbprincipalkey'): + try: + client.modifyPassword(user_dict['krbprincipalname'], "", kw.get('krbprincipalkey')) + except ipaerror.IPAError, e: + 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, + user_groups=user_groups_dicts, + tg_template='ipagui.templates.useredit') + + # + # Add groups + # + failed_adds = [] + try: + dnadds = kw.get('dnadd') + cherrypy.session['uid'] = user_dict.get('uid') + + # remove the default group from failed add + if dnadds != None: + try: + conf=client.get_ipa_config() + default_cn="cn=%s" % conf.getValue('ipadefaultprimarygroup') + + if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)): + dnadds = [dnadds] + + for d in dnadds: + e = d.find(default_cn) + if e >= 0: + dnadds.remove(d) + except: + pass + + if len(dnadds) > 0: + failed_adds = client.add_groups_to_user( + utf8_encode_values(dnadds), user.dn) + kw['dnadd'] = failed_adds + except ipaerror.IPAError, e: + failed_adds = dnadds + + if len(failed_adds) > 0: + 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) + + # Setup any multi-value fields, otherwise you'll get: + # 'NoneType' object is not iterable + user_dict = self.initialize_mv_fields(user_dict) + return dict(form=user_edit_form, user=user_dict, + user_groups=user_groups_dicts, + tg_template='ipagui.templates.useredit') + + turbogears.flash("%s added!" % kw['uid']) + print "Succeeded " + raise turbogears.redirect('/user/show', uid=kw['uid']) + + @expose("ipagui.templates.dynamiceditsearch") + @identity.require(identity.not_anonymous()) + def edit_search(self, **kw): + """Searches for groups and displays list of results in a table. + This method is used for the ajax search on the user edit page.""" + client = self.get_ipaclient() + + groups = [] + groups_counter = 0 + searchlimit = 100 + criteria = kw.get('criteria') + if criteria != None and len(criteria) > 0: + try: + groups = client.find_groups(criteria.encode('utf-8'), None, + searchlimit) + groups_counter = groups[0] + groups = groups[1:] + except ipaerror.IPAError, e: + turbogears.flash("search failed: " + str(e)) + + return dict(users=None, groups=groups, criteria=criteria, + counter=groups_counter) + + + @expose("ipagui.templates.useredit") + @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.") + + client = self.get_ipaclient() + + try: + if uid is not None: + user = client.get_user_by_uid(uid, user_fields) + elif principal is not None: + principal = principal + "@" + ipa.config.config.default_realm + user = client.get_user_by_principal(principal, user_fields) + else: + turbogears.flash("User edit failed: No uid or principal provided") + raise turbogears.redirect('/') + user_dict = user.toDict() + + user_dict = self.initialize_mv_fields(user_dict) + + # Edit shouldn't fill in the password field. + if user_dict.has_key('krbprincipalkey'): + del(user_dict['krbprincipalkey']) + + user_dict['uid_hidden'] = user_dict.get('uid') + + user_groups = client.get_groups_by_member(user.dn, ['dn', 'cn']) + user_groups.sort(self.sort_by_cn) + user_groups_dicts = map(lambda group: group.toDict(), user_groups) + user_groups_data = b64encode(dumps(user_groups_dicts)) + + # store a copy of the original user for the update later + user_data = b64encode(dumps(user_dict)) + user_dict['user_orig'] = user_data + user_dict['user_groups_data'] = user_groups_data + + # grab manager and secretary names + if user.manager: + try: + user_manager = client.get_entry_by_dn(user.manager, + ['givenname', 'sn', 'uid']) + user_dict['manager_cn'] = "%s %s" % ( + user_manager.getValue('givenname', ''), + user_manager.getValue('sn', '')) + except (ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), + ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR)): + pass + if user.secretary: + try: + user_secretary = client.get_entry_by_dn(user.secretary, + ['givenname', 'sn', 'uid']) + user_dict['secretary_cn'] = "%s %s" % ( + user_secretary.getValue('givenname', ''), + user_secretary.getValue('sn', '')) + except (ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), + ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR)): + pass + + # Set the uid we're editing in the session. If it doesn't match + # later the update will not be processed + cherrypy.session['uid'] = user_dict.get('uid') + + # Hack. The admin user doesn't have inetorgperson as an + # objectclass so don't require the givenName attribute if + # this objectclass doesn't exist in the record. + oc = [x.lower() for x in user_dict.get('objectclass')] + try: + p = oc.index('inetorgperson') + except ValueError: + # This entry doesn't have inetorgperson so don't require gn + user_edit_form.validator.fields.get('givenname').not_empty=False + + return dict(form=user_edit_form, user=user_dict, + user_groups=user_groups_dicts) + except ipaerror.IPAError, e: + if uid is None: + uid = principal + turbogears.flash("User edit failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect('/user/show', uid=uid) + + @expose() + @identity.require(identity.not_anonymous()) + def update(self, **kw): + """Updates an existing user""" + self.restrict_post() + client = self.get_ipaclient() + + if not kw.get('uid'): + kw['uid'] = kw.get('uid_hidden') + + if kw.get('submit') == 'Cancel Edit': + turbogears.flash("Edit user cancelled") + raise turbogears.redirect('/user/show', uid=kw.get('uid')) + + edituid = cherrypy.session.get('uid') + if edituid and edituid != kw.get('uid') and edituid != kw.get('uid_hidden'): + turbogears.flash("Something went wrong. You last viewed %s but are trying to update %s" % (kw.get('uid'), edituid)) + raise turbogears.redirect('/user/show', uid=kw.get('uid')) + + # Fix incoming multi-valued fields we created for the form + kw = ipahelper.fix_incoming_fields(kw, 'cn', 'cns') + kw = ipahelper.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers') + kw = ipahelper.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers') + kw = ipahelper.fix_incoming_fields(kw, 'mobile', 'mobiles') + kw = ipahelper.fix_incoming_fields(kw, 'pager', 'pagers') + kw = ipahelper.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_hidden') != 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')) + + if (kw.get('uid_hidden') == turbogears.identity.current.display_name and + kw.get('uid') != kw.get('uid_hidden')): + turbogears.flash("You cannot change your own login name.") + raise turbogears.redirect('/user/show', uid=kw.get('uid_hidden')) + + # Decode the group data, in case we need to round trip + user_groups_dicts = loads(b64decode(kw.get('user_groups_data'))) + + tg_errors, kw = self.userupdatevalidate(**kw) + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + return dict(form=user_edit_form, user=kw, + user_groups=user_groups_dicts, + tg_template='ipagui.templates.useredit') + + # We don't want to inadvertantly add this to a record + try: + del kw['uid_hidden'] + except KeyError: + pass + + password_change = False + user_modified = False + + # + # Update the user itself + # + 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.setValueNotEmpty('title', kw.get('title')) + new_user.setValueNotEmpty('givenname', kw.get('givenname')) + new_user.setValueNotEmpty('sn', kw.get('sn')) + new_user.setValueNotEmpty('cn', kw.get('cn')) + new_user.setValueNotEmpty('displayname', kw.get('displayname')) + new_user.setValueNotEmpty('initials', kw.get('initials')) + + new_user.setValueNotEmpty('loginshell', kw.get('loginshell')) + new_user.setValueNotEmpty('gecos', kw.get('gecos')) + + new_user.setValueNotEmpty('mail', kw.get('mail')) + new_user.setValueNotEmpty('telephonenumber', kw.get('telephonenumber')) + new_user.setValueNotEmpty('facsimiletelephonenumber', + kw.get('facsimiletelephonenumber')) + new_user.setValueNotEmpty('mobile', kw.get('mobile')) + new_user.setValueNotEmpty('pager', kw.get('pager')) + new_user.setValueNotEmpty('homephone', kw.get('homephone')) + + new_user.setValueNotEmpty('street', kw.get('street')) + new_user.setValueNotEmpty('l', kw.get('l')) + new_user.setValueNotEmpty('st', kw.get('st')) + new_user.setValueNotEmpty('postalcode', kw.get('postalcode')) + + new_user.setValueNotEmpty('ou', kw.get('ou')) + new_user.setValueNotEmpty('businesscategory', kw.get('businesscategory')) + new_user.setValueNotEmpty('description', kw.get('description')) + new_user.setValueNotEmpty('employeetype', kw.get('employeetype')) + new_user.setValueNotEmpty('manager', kw.get('manager')) + new_user.setValueNotEmpty('roomnumber', kw.get('roomnumber')) + new_user.setValueNotEmpty('secretary', kw.get('secretary')) + + new_user.setValueNotEmpty('carlicense', kw.get('carlicense')) + new_user.setValueNotEmpty('labeleduri', kw.get('labeleduri')) + + if kw.get('editprotected') == 'true': + if kw.get('krbprincipalkey'): + password_change = True + new_user.setValueNotEmpty('uidnumber', str(kw.get('uidnumber'))) + new_user.setValueNotEmpty('gidnumber', str(kw.get('gidnumber'))) + new_user.setValueNotEmpty('homedirectory', str(kw.get('homedirectory'))) + new_user.setValueNotEmpty('uid', str(kw.get('uid'))) + + for custom_field in user_edit_form.custom_fields: + new_user.setValueNotEmpty(custom_field.name, + kw.get(custom_field.name, '')) + + rv = client.update_user(new_user) + # + # If the user update succeeds, but below operations fail, we + # need to make sure a subsequent submit doesn't try to update + # the user again. + # + user_modified = True + kw['user_orig'] = b64encode(dumps(new_user.toDict())) + except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST), e: + # could be a password change + # could be groups change + # too much work to figure out unless someone really screams + pass + except ipaerror.IPAError, e: + turbogears.flash("User update failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=user_edit_form, user=kw, + user_groups=user_groups_dicts, + tg_template='ipagui.templates.useredit') + + # + # Password change + # + try: + if password_change: + rv = client.modifyPassword(orig_user_dict['krbprincipalname'], "", kw.get('krbprincipalkey')) + except ipaerror.IPAError, e: + turbogears.flash("User password change failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + return dict(form=user_edit_form, user=kw, + user_groups=user_groups_dicts, + tg_template='ipagui.templates.useredit') + except Exception, e: + turbogears.flash("User password change failed: " + str(e)) + return dict(form=user_edit_form, user=kw, + user_groups=user_groups_dicts, + tg_template='ipagui.templates.useredit') + + # + # Add groups + # + failed_adds = [] + try: + dnadds = kw.get('dnadd') + if dnadds != None: + if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)): + dnadds = [dnadds] + failed_adds = client.add_groups_to_user( + utf8_encode_values(dnadds), new_user.dn) + kw['dnadd'] = failed_adds + except ipaerror.IPAError, e: + failed_adds = dnadds + + # + # Remove groups + # + failed_dels = [] + try: + dndels = kw.get('dndel') + if dndels != None: + if not(isinstance(dndels,list) or isinstance(dndels,tuple)): + dndels = [dndels] + failed_dels = client.remove_groups_from_user( + utf8_encode_values(dndels), new_user.dn) + kw['dndel'] = failed_dels + except ipaerror.IPAError, e: + failed_dels = dndels + + if (len(failed_adds) > 0) or (len(failed_dels) > 0): + message = "There was an error updating groups.<br />" + message += "Failures have been preserved in the add/remove lists." + if user_modified: + message = "User Details successfully updated.<br />" + message + if password_change: + message = "User password successfully updated.<br />" + message + turbogears.flash(message) + return dict(form=user_edit_form, user=kw, + 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].get('desc','') + ". " + e.detail[0].get('info','')) + 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']) + + + @expose("ipagui.templates.userlist") + @identity.require(identity.not_anonymous()) + def list(self, **kw): + """Searches for users and displays list of results""" + client = self.get_ipaclient() + + users = None + counter = 0 + uid = kw.get('uid') + if uid != None and len(uid) > 0: + try: + users = client.find_users(uid.encode('utf-8'), user_fields) + counter = users[0] + users = users[1:] + if counter == -1: + 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) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect("/user/list") + + return dict(users=users, uid=uid, fields=ipagui.forms.user.UserFields()) + + + @expose("ipagui.templates.usershow") + @identity.require(identity.not_anonymous()) + 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) + user_groups = client.get_groups_by_member(user.dn, ['cn']) + user_groups.sort(self.sort_by_cn) + user_reports = client.get_users_by_manager(user.dn, + ['givenname', 'sn', 'uid']) + user_reports.sort(self.sort_group_member) + + user_manager = None + user_secretary = None + try: + if user.manager: + user_manager = client.get_entry_by_dn(user.manager, + ['givenname', 'sn', 'uid']) + except (ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), + ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR)): + pass + + try: + if user.secretary: + user_secretary = client.get_entry_by_dn(user.secretary, + ['givenname', 'sn', 'uid']) + except (ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), + ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR)): + pass + + return dict(user=user.toDict(), fields=ipagui.forms.user.UserFields(), + 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) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect("/") + + @expose() + @identity.require(identity.not_anonymous()) + def delete(self, uid): + """Delete user.""" + self.restrict_post() + client = self.get_ipaclient() + + try: + client.delete_user(uid) + + turbogears.flash("user deleted") + raise turbogears.redirect('/user/list') + except (SyntaxError, ipaerror.IPAError), e: + turbogears.flash("User deletion failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + raise turbogears.redirect('/user/list') + + @validate(form=user_new_form) + @identity.require(identity.not_anonymous()) + def usercreatevalidate(self, tg_errors=None, **kw): + return tg_errors, kw + + @validate(form=user_edit_form) + @identity.require(identity.not_anonymous()) + def userupdatevalidate(self, tg_errors=None, **kw): + return tg_errors, kw + + # @expose() + def generate_password(self): + password = "" + generator = random.SystemRandom() + for char in range(8): + password += generator.choice(password_chars) + + return password + + @expose() + @identity.require(identity.not_anonymous()) + def suggest_uid(self, givenname, sn): + # filter illegal uid characters out + givenname = re.sub(r'[^a-zA-Z_\-0-9]', "", givenname) + sn = re.sub(r'[^a-zA-Z_\-0-9]', "", sn) + + if (len(givenname) == 0) or (len(sn) == 0): + return "" + + client = self.get_ipaclient() + + givenname = givenname.lower() + sn = sn.lower() + + uid = givenname[0] + sn[:7] + try: + client.get_user_by_uid(uid) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + return uid + + uid = givenname[:7] + sn[0] + try: + client.get_user_by_uid(uid) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + return uid + + uid = (givenname + sn)[:8] + try: + client.get_user_by_uid(uid) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + return uid + + uid = sn[:8] + try: + client.get_user_by_uid(uid) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + return uid + + suffix = 2 + template = givenname[0] + sn[:7] + while suffix < 20: + uid = template[:8 - len(str(suffix))] + str(suffix) + try: + client.get_user_by_uid(uid) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + return uid + suffix += 1 + + return "" + + @expose() + @identity.require(identity.not_anonymous()) + def suggest_email(self, givenname, sn): + # remove illegal email characters + givenname = re.sub(r'[^a-zA-Z0-9!#\$%\*/?\|\^\{\}`~&\'\+\-=_]', "", givenname) + sn = re.sub(r'[^a-zA-Z0-9!#\$%\*/?\|\^\{\}`~&\'\+\-=_]', "", sn) + + if (len(givenname) == 0) or (len(sn) == 0): + return "" + + client = self.get_ipaclient() + + givenname = givenname.lower() + sn = sn.lower() + + email = "%s.%s@%s" % (givenname, sn, self.get_email_domain()) + try: + client.get_user_by_email(email) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + return email + + email = "%s@%s" % (self.suggest_uid(givenname, sn), self.get_email_domain()) + try: + client.get_user_by_email(email) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + return email + + return "" + + @expose("ipagui.templates.userselectsearch") + @identity.require(identity.not_anonymous()) + def user_select_search(self, **kw): + """Searches for users and displays list of results in a table. + This method is used for the ajax search for managers + and secrectary on the user pages.""" + client = self.get_ipaclient() + + users = [] + users_counter = 0 + searchlimit = 100 + criteria = kw.get('criteria') + if criteria != None and len(criteria) > 0: + try: + users = client.find_users(criteria.encode('utf-8'), None, + searchlimit) + users_counter = users[0] + users = users[1:] + except ipaerror.IPAError, e: + turbogears.flash("search failed: " + str(e) + "<br/>" + e.detail[0].get('desc','') + ". " + e.detail[0].get('info','')) + + return dict(users=users, criteria=criteria, + which_select=kw.get('which_select'), + counter=users_counter) diff --git a/ipa-server/ipa-gui/ipagui/templates/Makefile.am b/ipa-server/ipa-gui/ipagui/templates/Makefile.am new file mode 100644 index 00000000..ddc8666c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/Makefile.am @@ -0,0 +1,55 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/templates +app_PYTHON = \ + __init__.py \ + $(NULL) + +app_DATA = \ + delegateedit.kid \ + delegateform.kid \ + delegategroupsearch.kid \ + delegatelayout.kid \ + delegatelist.kid \ + delegatenew.kid \ + dynamiceditsearch.kid \ + groupeditform.kid \ + groupedit.kid \ + grouplayout.kid \ + grouplist.kid \ + groupnewform.kid \ + groupnew.kid \ + groupshow.kid \ + ipapolicyeditform.kid \ + ipapolicyedit.kid \ + ipapolicyshow.kid \ + loginfailed.kid \ + master.kid \ + not_found.kid \ + policyindex.kid \ + policylayout.kid \ + principallayout.kid \ + principallist.kid \ + principalshow.kid \ + principalnewform.kid \ + principalnew.kid \ + usereditform.kid \ + useredit.kid \ + userlayout.kid \ + userlist.kid \ + usernewform.kid \ + usernew.kid \ + userselectsearch.kid \ + usershow.kid \ + welcome.kid \ + unhandled_exception.kid \ + $(NULL) + +EXTRA_DIST = \ + $(app_DATA) \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/templates/__init__.py b/ipa-server/ipa-gui/ipagui/templates/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/__init__.py diff --git a/ipa-server/ipa-gui/ipagui/templates/delegateedit.kid b/ipa-server/ipa-gui/ipagui/templates/delegateedit.kid new file mode 100644 index 00000000..d9f6c3c4 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegateedit.kid @@ -0,0 +1,33 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'delegatelayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Edit Delegation</title> +</head> +<body> + + <h1 class="accesscontrol">Edit Delegation</h1> + + ${form.display(action=tg.url("/delegate/update"), value=delegate, + actionname='Update')} + +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/delegateform.kid b/ipa-server/ipa-gui/ipagui/templates/delegateform.kid new file mode 100644 index 00000000..71d08a4f --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegateform.kid @@ -0,0 +1,213 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<div xmlns:py="http://purl.org/kid/ns#" + class="simpleroster"> + + <?python searchurl = tg.url('/delegate/group_search') ?> + + <script type="text/javascript"> + function lostFocus(which_group) { + /* The user has left the field, save what they put in there in case + * they don't do a Find. */ + group_cn_field = $('form_' + which_group + '_group_cn'); + group_criteria_field = $(which_group + '_criteria') + group_cn_field.value = group_criteria_field.value + } + + function enterDoSearch(e, which_group) { + var keyPressed; + if (window.event) { + keyPressed = window.event.keyCode; + } else { + keyPressed = e.which; + } + + if (keyPressed == 13) { + return doSearch(which_group); + } else { + return true; + } + } + + function doSearch(which_group) { + $(which_group + '_searchresults').update("Searching..."); + new Ajax.Updater(which_group + '_searchresults', + '${searchurl}', + { asynchronous:true, + parameters: { criteria: $(which_group + '_criteria').value, + which_group: which_group}, + evalScripts: true }); + return false; + } + + function selectGroup(which_group, group_dn, group_cn) { + group_dn_field = $('form_' + which_group + '_group_dn'); + group_cn_field = $('form_' + which_group + '_group_cn'); + group_cn_span = $(which_group + '_group_cn'); + + group_dn_field.value = group_dn; + group_cn_field.value = group_cn; + group_cn_span.update(group_cn); + + new Effect.Fade($(which_group + '_searcharea'), {duration: 0.25}); + new Effect.Appear($(which_group + '_change_link'), {duration: 0.25}); + } + + function confirmDelete() { + if (confirm("Are you sure you want to delete this delegation?")) { + $('deleteform').submit(); + } + return false; + } + </script> + + <form style="display:none" id='deleteform' + method="post" action="${tg.url('/delegate/delete')}"> + <input type="hidden" name="acistr" value="${value.get('orig_acistr')}" /> + </form> + + <form action="${action}" name="${name}" method="${method}" class="tableform"> + + <input type="submit" class="submitbutton" name="submit" + value="${actionname} Delegation"/> + <input type="submit" class="submitbutton" name="submit" + value="Cancel ${actionname}"/> + <span py:if='actionname == "Update"'> + <input type="button" class="deletebutton" + value="Delete Delegation" + onclick="return confirmDelete();" + /> + </span> + + <div py:for="field in hidden_fields" + py:replace="field.display(value_for(field), **params_for(field))" + /> + +<h2>Delegation Details</h2> + + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th valign="top"> + <label class="fieldlabel" for="${delegate_fields.name.field_id}" + py:content="delegate_fields.name.label" />: + </th> + <td> + <span py:replace="delegate_fields.name.display(value_for(delegate_fields.name))" /> + <span py:if="tg.errors.get('name')" class="fielderror" + py:content="tg.errors.get('name')" /> + </td> + </tr> + <tr> + <th valign="top"> + <label class="fieldlabel" for="${delegate_fields.source_group_cn.field_id}" + py:content="delegate_fields.source_group_cn.label" />: + </th> + <td> + <div> + <span id='source_group_cn'>${value_for(delegate_fields.source_group_cn)}</span> + <a href="#" id='source_change_link' + onclick="new Effect.Appear($('source_searcharea'), {duration: 0.25}); + new Effect.Fade(this, {duration: 0.25}); + return false;">change</a> + <span py:if="tg.errors.get('source_group_dn')" class="fielderror" + py:content="tg.errors.get('source_group_dn')" /> + </div> + <div id="source_searcharea" style="display:none"> + <input class="requiredfield" id="source_criteria" type="text" + onkeypress="return enterDoSearch(event, 'source');" onblur="return lostFocus('source');"/> + <input class="searchbutton" type="button" value="Find" + onclick="return doSearch('source');" + /> + <div id="source_searchresults"> + </div> + </div> + </td> + </tr> + <tr> + <th valign="top"> + <label class="fieldlabel" for="${delegate_fields.attrs.field_id}" + py:content="delegate_fields.attrs.label" />: + </th> + <td valign="top"> + <span py:if="tg.errors.get('attrs')" class="fielderror" + py:content="tg.errors.get('attrs')" /> + <span py:replace="delegate_fields.attrs.display(value_for(delegate_fields.attrs))" /> + </td> + </tr> + <tr> + <th valign="top"> + <label class="fieldlabel" for="${delegate_fields.dest_group_cn.field_id}" + py:content="delegate_fields.dest_group_cn.label" />: + </th> + <td> + <div> + <span id='dest_group_cn'>${value_for(delegate_fields.dest_group_cn)}</span> + <a href="#" id='dest_change_link' + onclick="new Effect.Appear($('dest_searcharea'), {duration: 0.25}); + new Effect.Fade(this, {duration: 0.25}); + return false;">change</a> + <span py:if="tg.errors.get('dest_group_dn')" class="fielderror" + py:content="tg.errors.get('dest_group_dn')" /> + </div> + <div id="dest_searcharea" style="display:none"> + <div> + <input class="requiredfield" id="dest_criteria" type="text" + onkeypress="return enterDoSearch(event, 'dest');" onblur="return lostFocus('dest');"/> + <input class="searchbutton" type="button" value="Find" + onclick="return doSearch('dest');" + /> + </div> + <div id="dest_searchresults"> + </div> + </div> + </td> + </tr> + </table> + +<hr /> + + <input type="submit" class="submitbutton" name="submit" + value="${actionname} Delegation"/> + <input type="submit" class="submitbutton" name="submit" + value="Cancel ${actionname}"/> + <span py:if='actionname == "Update"'> + <input type="button" class="deletebutton" + value="Delete Delegation" + onclick="return confirmDelete();" + /> + </span> + + <script py:if="not value.get('source_group_dn')" + type="text/javascript"> + new Effect.Appear($('source_searcharea'), {duration: 0.25}); + new Effect.Fade($('source_change_link'), {duration: 0.25}); + </script> + <script py:if="not value.get('dest_group_dn')" + type="text/javascript"> + new Effect.Appear($('dest_searcharea'), {duration: 0.25}); + new Effect.Fade($('dest_change_link'), {duration: 0.25}); + </script> + + </form> + + + <script type="text/javascript"> + document.getElementById("form_name").focus(); + </script> + +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid b/ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid new file mode 100644 index 00000000..f8f8b5c6 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid @@ -0,0 +1,48 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<div xmlns:py="http://purl.org/kid/ns#"> + +<?python +from ipagui.helpers import ipahelper +?> + <div py:if='(groups != None) and (len(groups) > 0)'> + <div id="search-results-count"> + ${len(groups)} results returned: + <span py:if="counter < 0"> + (truncated) + </span> + </div> + + <div py:for="group in groups"> + <?python + group_dn_esc = ipahelper.javascript_string_escape(group.dn) + group_cn_esc = ipahelper.javascript_string_escape(group.cn) + which_group_esc = ipahelper.javascript_string_escape(which_group) + ?> + + ${group.cn} + <a href="" + onclick="selectGroup('${which_group_esc}', '${group_dn_esc}', '${group_cn_esc}'); + return false;" + >select</a> + </div> + </div> + <div py:if='(groups != None) and (len(groups) == 0)'> + No results found for "${criteria}" + </div> +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid b/ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid new file mode 100644 index 00000000..7ece6cf9 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid @@ -0,0 +1,34 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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/delegatelist.kid b/ipa-server/ipa-gui/ipagui/templates/delegatelist.kid new file mode 100644 index 00000000..81ecfeb0 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegatelist.kid @@ -0,0 +1,93 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'delegatelayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Delegations</title> +</head> +<body> + +<?python +from ipagui.helpers import ipahelper +?> + + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script> + + <script type="text/javascript"> + function editDelegation(acistr) { + $('edit_acistr').value = acistr; + $('editform').submit(); + return false; + } + </script> + + <form style="display:none" id='editform' + method="post" action="${tg.url('/delegate/edit')}"> + <input type="hidden" id="edit_acistr" name="acistr" value="" /> + </form> + <h1 class="accesscontrol">Delegations</h1> + + <table id="resultstable" class="details sortable resizable"> + <thead> + <tr> + <th>${fields.name.label}</th> + <th>${fields.source_group_cn.label}</th> + <th>${fields.attrs.label}</th> + <th>${fields.dest_group_cn.label}</th> + </tr> + </thead> + <tbody> + <tr py:for='aci in aci_list'> + <?python + source_cn = group_dn_to_cn.get(aci.source_group) + dest_cn = group_dn_to_cn.get(aci.dest_group) + acistr = aci.orig_acistr + acistr_esc = ipahelper.javascript_string_escape(acistr) + ?> + <td> + <a href="#" onclick="return editDelegation('${acistr_esc}');" + >${aci.name}</a> + </td> + <td> + <a href="${tg.url('/group/show', cn=source_cn)}" + >${source_cn}</a> + </td> + <td> + ${", ".join(aci.attrs)} + </td> + <td> + <a href="${tg.url('/group/show', cn=dest_cn)}" + >${dest_cn}</a> + </td> + </tr> + </tbody> + </table> + + <table border="0"> + <tbody> + <tr> + <td> + <a href="${tg.url('/delegate/new')}">add new delegation</a><br /> + </td> + </tr> + </tbody> + </table> +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/delegatenew.kid b/ipa-server/ipa-gui/ipagui/templates/delegatenew.kid new file mode 100644 index 00000000..12ba9e36 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/delegatenew.kid @@ -0,0 +1,31 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'delegatelayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Add Delegation</title> +</head> +<body> + <h1 class="accesscontrol">Add Delegation</h1> + + ${form.display(action=tg.url("/delegate/create"), value=delegate, + actionname='Add')} +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/dynamiceditsearch.kid b/ipa-server/ipa-gui/ipagui/templates/dynamiceditsearch.kid new file mode 100644 index 00000000..2407f665 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/dynamiceditsearch.kid @@ -0,0 +1,97 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<div xmlns:py="http://purl.org/kid/ns#"> + +<?python +from ipagui.helpers import ipahelper +# +# This file is used to render the results from an AJAX search onto a page. +# It has many expectations of the page being rendered into: +# - Source page must have included dynamicedit.js and followed all of its +# requirements +# +?> + <div id="search-results-count"> + </div> + <?python + criteria_esc = ipahelper.javascript_string_escape(criteria) + ?> + <script type="text/javascript"> + search_string = "${criteria_esc}"; + results_counter = 0; + </script> + <?python search_div_counter = 1 ?> + <div py:for="entities in (users, groups)"> + <div py:if='(entities != None) and (len(entities) > 0)'> + <div py:for="entity in entities" id="search-${search_div_counter}"> + <?python + ent_dn_esc = ipahelper.javascript_string_escape(entity.dn) + ent_uid = entity.uid + if ent_uid: + ent_name = "%s %s" % (entity.getValue('givenName', ''), + entity.getValue('sn', '')) + ent_descr = "(%s)" % entity.uid + ent_type = "user" + else: + ent_name = entity.cn + ent_descr = "[group]" + ent_type = "group" + ent_name_esc = ipahelper.javascript_string_escape(ent_name) + ent_descr_esc = ipahelper.javascript_string_escape(ent_descr) + ent_type_esc = ipahelper.javascript_string_escape(ent_type) + ?> + <span id="search-info-${search_div_counter}"></span> + <script type="text/javascript"> + if ((added_hash["${ent_dn_esc}"] == 1) || + (member_hash["${ent_dn_esc}"] == 1)) { + $("search-${search_div_counter}").style.display = 'none'; + } else { + results_counter = results_counter + 1; + } + + renderMemberInfo($('search-info-${search_div_counter}'), + new MemberDisplayInfo('${ent_name_esc}', + '${ent_descr_esc}', + '${ent_type_esc}')); + </script> + <a href="" + onclick="addmemberHandler(this, '${ent_dn_esc}', + new MemberDisplayInfo('${ent_name_esc}', + '${ent_descr_esc}', + '${ent_type_esc}')); + return false;" + >add</a> + <?python + search_div_counter = search_div_counter + 1 + ?> + </div> + </div> + </div> + <script type="text/javascript"> + if (results_counter == 0) { + var message = "No results found for '" + search_string + "'"; + } else { + var message = results_counter + " results found:"; + } + $('search-results-count').appendChild(document.createTextNode(message)); + </script> + <script py:if="counter < 0"> + $('search-results-count').appendChild(document.createTextNode( + " (truncated)")); + </script> +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/groupedit.kid b/ipa-server/ipa-gui/ipagui/templates/groupedit.kid new file mode 100644 index 00000000..9614770f --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/groupedit.kid @@ -0,0 +1,36 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'grouplayout.kid'"> +<head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> + <title>Edit Group</title> +</head> +<body> + <div id="details"> + <h1 class="usergroup">Edit Group</h1> +<input type="checkbox" id="toggleprotected_checkbox" + onclick="toggleProtectedFields(this);"> + <span class="small">edit protected fields</span> + </input> + + ${form.display(action=tg.url('/group/update'), value=group, members=members)} +</div> +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid b/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid new file mode 100644 index 00000000..78f76b0a --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid @@ -0,0 +1,289 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<div xmlns:py="http://purl.org/kid/ns#" + class="simpleroster"> + + <form style="display:none" id='deleteform' + method="post" action="${tg.url('/group/delete')}"> + <input type="hidden" name="dn" value="${value.get('dn')}" /> + </form> + + <form action="${action}" name="${name}" method="${method}" class="tableform" + onsubmit="preSubmit()" > + + <input type="submit" class="submitbutton" name="submit" + value="Update Group"/> + <input type="submit" class="submitbutton" name="submit" + value="Cancel Edit" /> + <input type="button" class="deletebutton" + value="Delete Group" + onclick="return confirmDelete();" + /> + + +<?python +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 = ''; + } + } + + function doSearch() { + $('searchresults').update("Searching..."); + new Ajax.Updater('searchresults', + '${searchurl}', + { asynchronous:true, + parameters: { criteria: $('criteria').value }, + evalScripts: true }); + return false; + } + + function confirmDelete() { + if (confirm("Are you sure you want to delete this group?")) { + $('deleteform').submit(); + } + return false; + } + </script> + + <div py:for="field in hidden_fields" + py:replace="field.display(value_for(field), **params_for(field))" + /> + + <h2 class="formsection">Group Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${group_fields.cn.field_id}" + py:content="group_fields.cn.label" />: + </th> + <td> + <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')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${group_fields.description.field_id}" + py:content="group_fields.description.label" />: + </th> + <td> + <span py:replace="group_fields.description.display(value_for(group_fields.description))" /> + <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> + + <tr> + <th> + <label class="fieldlabel" for="${group_fields.gidnumber.field_id}" + py:content="group_fields.gidnumber.label" />: + </th> + <td> + <span py:replace="group_fields.gidnumber.display(value_for(group_fields.gidnumber))" /> + <span py:if="tg.errors.get('gidnumber')" class="fielderror" + py:content="tg.errors.get('gidnumber')" /> + + <script type="text/javascript"> + document.getElementById('form_gidnumber').disabled = true; + </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> + <h2 class="formsection">Group Members</h2> + + <div class="floatlist"> + <div class="floatheader">To Remove:</div> + <div id="delmembers"> + </div> + </div> + + <div> + <?python div_counter = 1 ?> + <div py:for="member in members" id="member-${div_counter}"> + <?python + member_dn = member.get('dn') + member_dn_esc = ipahelper.javascript_string_escape(member_dn) + + member_uid = member.get('uid') + member_inherited = member.get('inherited') + if member_uid: + member_name = "%s %s" % (member.get('givenName', ''), + member.get('sn', '')) + member_descr = "(%s)" % member.get('uid') + if member_inherited: + member_type = "iuser" + else: + member_type = "user" + else: + member_name = member.get('cn') + member_descr = "[group]" + if member_inherited: + member_type = "igroup" + else: + member_type = "group" + member_name_esc = ipahelper.javascript_string_escape(member_name) + member_descr_esc = ipahelper.javascript_string_escape(member_descr) + member_type_esc = ipahelper.javascript_string_escape(member_type) + ?> + <span id="member-info-${div_counter}"></span> + <script type="text/javascript"> + renderMemberInfo($('member-info-${div_counter}'), + new MemberDisplayInfo('${member_name_esc}', + '${member_descr_esc}', + '${member_type_esc}')); + </script> + <a py:if="member_inherited != True" href="#" + onclick="removememberHandler(this, '${member_dn_esc}', + new MemberDisplayInfo('${member_name_esc}', + '${member_descr_esc}', + '${member_type_esc}')); + return false;" + >remove</a> + <script type="text/javascript"> + dn_to_member_div_id['${member_dn_esc}'] = "member-${div_counter}"; + member_hash["${member_dn_esc}"] = 1; + </script> + <?python + div_counter = div_counter + 1 + ?> + </div> + <!-- a space here to prevent an empty div --> + </div> + + </div> + + <div style="clear:both"> + <h2 class="formsection">Add Members</h2> + + <div class="floatlist"> + <div class="floatheader">To Add:</div> + <div id="newmembers"> + </div> + </div> + + <div> + <div id="search"> + <input id="criteria" type="text" name="criteria" + onkeypress="return enterDoSearch(event);" /> + <input class="searchbutton" type="button" value="Find" + onclick="return doSearch();" + /> + </div> + <div id="searchresults"> + </div> + </div> + </div> +<hr /> + <input type="submit" class="submitbutton" name="submit" + value="Update Group"/> + <input type="submit" class="submitbutton" name="submit" + value="Cancel Edit" /> + <input type="button" class="deletebutton" + value="Delete Group" + onclick="return confirmDelete();" + /> + </form> + + <script type="text/javascript"> + /* + * This section restores the contents of the add and remove lists + * dynamically if we have to refresh the page + */ + if ($('form_dn_to_info_json').value != "") { + dn_to_info_hash = new Hash($('form_dn_to_info_json').value.evalJSON()); + } + + if ($('form_editprotected').value != "") { + $('toggleprotected_checkbox').checked = true; + toggleProtectedFields($('toggleprotected_checkbox')); + } + </script> + + <?python + dnadds = value.get('dnadd', []) + if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)): + dnadds = [dnadds] + + dndels = value.get('dndel', []) + if not(isinstance(dndels,list) or isinstance(dndels,tuple)): + dndels = [dndels] + ?> + + <script py:for="dnadd in dnadds"> + <?python + dnadd_esc = ipahelper.javascript_string_escape(dnadd) + ?> + var dn = "${dnadd_esc}"; + var info = dn_to_info_hash[dn]; + var newdiv = addmember(dn, info); + if (newdiv != null) { + newdiv.style.display = 'block'; + } + </script> + + <script py:for="dndel in dndels"> + <?python + dndel_esc = ipahelper.javascript_string_escape(dndel) + ?> + var dn = "${dndel_esc}"; + var info = dn_to_info_hash[dn]; + var newdiv = removemember(dn, info); + newdiv.style.display = 'block'; + orig_div_id = dn_to_member_div_id[dn] + $(orig_div_id).style.display = 'none'; + </script> + +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/grouplayout.kid b/ipa-server/ipa-gui/ipagui/templates/grouplayout.kid new file mode 100644 index 00000000..fe013ca0 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/grouplayout.kid @@ -0,0 +1,40 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="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 id="sidebar"> + <h2>Tools</h2> + <a href="${tg.url('/group/index')}">Add Group</a><br/> + <a href="${tg.url('/group/index')}">Find Group</a><br/> + <a href="${tg.url('/group/index')}">List Groups</a><br/> + </div> --> +</body> + +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/grouplist.kid b/ipa-server/ipa-gui/ipagui/templates/grouplist.kid new file mode 100644 index 00000000..00fd03aa --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/grouplist.kid @@ -0,0 +1,93 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'grouplayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Find Groups</title> +</head> +<body> +<div id="details"> + <h1 class="usergroup">Find Groups</h1> + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script> + <div id="search"> + <form action="${tg.url('/group/list')}" method="get"> + <input id="criteria" type="text" name="criteria" value="${criteria}" /> + <input type="submit" value="Find Groups"/> + </form> + <script type="text/javascript"> + document.getElementById("criteria").focus(); + </script> + </div> + <div py:if='(groups != None) and (len(groups) > 0)'> + <h2>${len(groups)} results returned:</h2> + <table id="resultstable" class="details sortable resizable" cellspacing="0"> + <thead> + <tr> + <th> + ${fields.cn.label} + </th> + <th> + ${fields.description.label} + </th> + </tr> + </thead> + <tbody> + <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> + <td> + ${group.description} + </td> + </tr> + </tbody> + </table> + </div> + <div py:if='(groups != None) and (len(groups) == 0)'> + <h2>No results found for "${criteria}"</h2> + </div> + <div class="instructions" py:if='groups == None'> + <p> + Search automatically looks across multiple fields. If you want to find + Joe in Finance, try typing "joe finance" into the search box. + </p> + <p> + Exact matches are listed first, followed by partial matches. If your search + is too broad, you will get a warning that the search returned too many + results. Try being more specific. + </p> + <p> + The results that come back are sortable. Simply click on a column + header to sort on that header. A triangle will indicate the sorted + column, along with its direction. Clicking and dragging between headers + will allow you to resize the header. + </p> + </div> +</div> +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/groupnew.kid b/ipa-server/ipa-gui/ipagui/templates/groupnew.kid new file mode 100644 index 00000000..f5c83cae --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/groupnew.kid @@ -0,0 +1,32 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'grouplayout.kid'"> +<head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> + <title>Add Group</title> +</head> +<body> +<div id="details"> + <h1 class="usergroup">Add Group</h1> + + ${form.display(action=tg.url('/group/create'), value=group)} +</div> +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid b/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid new file mode 100644 index 00000000..ef321079 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid @@ -0,0 +1,149 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<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="Add Group"/> + +<?python +from ipagui.helpers import ipahelper +?> + + <script type="text/javascript" charset="utf-8" + src="${tg.url('/static/javascript/dynamicedit.js')}"></script> + + <?python searchurl = tg.url('/group/edit_search') ?> + + <script type="text/javascript"> + function doSearch() { + $('searchresults').update("Searching..."); + new Ajax.Updater('searchresults', + '${searchurl}', + { asynchronous:true, + parameters: { criteria: $('criteria').value }, + evalScripts: true }); + return false; + } + </script> + + <div py:for="field in hidden_fields" + py:replace="field.display(value_for(field), **params_for(field))" + /> + + <h2 class="formsection">Group Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${group_fields.cn.field_id}" + py:content="group_fields.cn.label" />: + </th> + <td> + <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')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${group_fields.description.field_id}" + py:content="group_fields.description.label" />: + </th> + <td> + <span py:replace="group_fields.description.display(value_for(group_fields.description))" /> + <span py:if="tg.errors.get('description')" class="fielderror" + py:content="tg.errors.get('description')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${group_fields.gidnumber.field_id}" + py:content="group_fields.gidnumber.label" />: + </th> + <td> + Generated by server + </td> + </tr> + </table> + + <div style="clear:both"> + <h2 class="formsection">Add Members</h2> + + <div class="floatlist"> + <div class="floatheader">To Add:</div> + <div id="newmembers"> + </div> + </div> + + <div> + <div id="search"> + <input id="criteria" type="text" name="criteria" + onkeypress="return enterDoSearch(event);" /> + <input type="button" value="Find" + onclick="return doSearch();" + /> + </div> + <div id="searchresults"> + </div> + </div> + </div> + +<hr /> + + <input type="submit" class="submitbutton" name="submit" value="Add Group"/> + + </form> + + <script type="text/javascript"> + document.getElementById("form_cn").focus(); + </script> + + <script type="text/javascript"> + /* + * This section restores the contents of the add and remove lists + * dynamically if we have to refresh the page + */ + if ($('form_dn_to_info_json').value != "") { + dn_to_info_hash = new Hash($('form_dn_to_info_json').value.evalJSON()); + } + </script> + + <?python + dnadds = value.get('dnadd', []) + if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)): + dnadds = [dnadds] + ?> + + <script py:for="dnadd in dnadds"> + <?python + dnadd_esc = ipahelper.javascript_string_escape(dnadd) + ?> + var dn = "${dnadd_esc}"; + var info = dn_to_info_hash[dn]; + var newdiv = addmember(dn, info); + if (newdiv != null) { + newdiv.style.display = 'block'; + } + </script> + +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/groupshow.kid b/ipa-server/ipa-gui/ipagui/templates/groupshow.kid new file mode 100644 index 00000000..d0ca6982 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/groupshow.kid @@ -0,0 +1,131 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'grouplayout.kid'"> +<head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> + <title>View Group</title> +</head> +<body> +<?python +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 class="usergroup">View Group</h1> + + <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" /> + + <h2 class="formsection">Group Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.cn.label" />: + </th> + <td>${group.get("cn")}</td> + </tr> + + <tr> + <th> + <label class="fieldlabel" py:content="fields.description.label" />: + </th> + <td>${group.get("description")}</td> + </tr> + + <tr> + <th> + <label class="fieldlabel" py:content="fields.gidnumber.label" />: + </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> + <div py:for="member in members"> + <?python + + member_uid = member.get('uid') + member_inherited = member.get('inherited') + if member_uid: + member_cn = "%s %s" % (member.get('givenName', ''), member.get('sn', '')) + member_desc = "(%s)" % member_uid + if member_inherited: + member_type = "iuser" + else: + member_type = "user" + view_url = tg.url('/user/show', uid=member_uid) + else: + mem = member.get('cn') + if isinstance(mem, list): + mem = mem[0] + member_cn = "%s" % mem + member_desc = "[group]" + if member_inherited: + member_type = "igroup" + else: + member_type = "group" + view_url = tg.url('/group/show', cn=member_cn) + ?> + <span py:if='member_type == "user"'> + <b> + <a href="${view_url}" + >${member_cn}</a> ${member_desc} + </b> + </span> + <span py:if='member_type == "iuser"'> + <a href="${view_url}" + >${member_cn}</a> ${member_desc} + </span> + <span py:if='member_type == "group"'> + <b> + <i> + <a href="${view_url}" + >${member_cn}</a> ${member_desc} + </i> + </b> + </span> + <span py:if='member_type == "igroup"'> + <i> + <a href="${view_url}" + >${member_cn}</a> ${member_desc} + </i> + </span> + </div> + + <br/> +<hr /> + <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> +</body> +</html> 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 00000000..6b071d2d --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid @@ -0,0 +1,32 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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 class="policy">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 00000000..a608829c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid @@ -0,0 +1,280 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<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> + <script type="text/javascript" charset="utf-8" + src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.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> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipadefaultemaildomain.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipadefaultemaildomain.display(value_for(ipapolicy_fields.ipadefaultemaildomain))" /> + <span py:if="tg.errors.get('ipadefaultemaildomain')" class="fielderror" + py:content="tg.errors.get('ipadefaultemaildomain')" /> + </td> + </tr> + <!-- + <tr> + <th> + <label class="fieldlabel" for="${ipapolicy_fields.userobjectclasses.field_id}" + py:content="ipapolicy_fields.userobjectclasses.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${ipapolicy_fields.userobjectclasses.field_id}"> + <tbody> + <?python repetition = 0 + fld_index = 0 + fld_error = tg.errors.get('ipauserobjectclasses') + ?> + <tr py:for="fld in value_for(ipapolicy_fields.ipauserobjectclasses)" + id="${ipapolicy_fields.userobjectclasses.field_id}_${repetition}" + class="${ipapolicy_fields.userobjectclasses.field_class}"> + + <td py:for="field in ipapolicy_fields.userobjectclasses.fields"> + <span><input class="textfield" type="text" id="${ipapolicy_fields.userobjectclasses.field_id}_${repetition}_ipauserobjectclasses" name="userobjectclasses-${repetition}.ipauserobjectclasses" value="${fld}"/></span> + <span py:if="fld_error and fld_error[fld_index]" class="fielderror" + py:content="tg.errors.get('ipauserobjectclasses')" /> + </td> + <?python fld_index = fld_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${ipapolicy_fields.userobjectclasses.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${ipapolicy_fields.userobjectclasses.field_id}_doclink" href="javascript:ExpandingForm.addItem('${ipapolicy_fields.userobjectclasses.field_id}');">Add User Object Class</a> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" for="${ipapolicy_fields.groupobjectclasses.field_id}" + py:content="ipapolicy_fields.groupobjectclasses.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${ipapolicy_fields.groupobjectclasses.field_id}"> + <tbody> + <?python repetition = 0 + fld_index = 0 + fld_error = tg.errors.get('ipagroupobjectclasses') + ?> + <tr py:for="fld in value_for(ipapolicy_fields.ipagroupobjectclasses)" + id="${ipapolicy_fields.groupobjectclasses.field_id}_${repetition}" + class="${ipapolicy_fields.groupobjectclasses.field_class}"> + + <td py:for="field in ipapolicy_fields.groupobjectclasses.fields"> + <span><input class="textfield" type="text" id="${ipapolicy_fields.groupobjectclasses.field_id}_${repetition}_ipagroupobjectclasses" name="groupobjectclasses-${repetition}.ipagroupobjectclasses" value="${fld}"/></span> + <span py:if="fld_error and fld_error[fld_index]" class="fielderror" + py:content="tg.errors.get('ipagroupobjectclasses')" /> + </td> + <?python fld_index = fld_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${ipapolicy_fields.groupobjectclasses.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${ipapolicy_fields.groupobjectclasses.field_id}_doclink" href="javascript:ExpandingForm.addItem('${ipapolicy_fields.groupobjectclasses.field_id}');">Add Group Object Class</a> + </td> + </tr> + --> + </table> + + <hr/> + + <input type="submit" class="submitbutton" name="submit" + value="Update Policy"/> + <input type="submit" class="submitbutton" name="submit" + value="Cancel Edit" /> + </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 00000000..3549a9f1 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid @@ -0,0 +1,188 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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 class="policy">Manage IPA Policy</h1> + <input class="submitbutton" type="button" + onclick="document.location.href='${edit_url}'" + value="Edit Policy" /> + + <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> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipadefaultemaildomain.label" />: + </th> + <td>${ipapolicy.get("ipadefaultemaildomain")}</td> + </tr> + <!-- + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipauserobjectclasses.label" />: + </th> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = ipapolicy.get("ipauserobjectclasses", '') + 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.ipagroupobjectclasses.label" />: + </th> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = ipapolicy.get("ipagroupobjectclasses", '') + if isinstance(values, str): + values = [values] + ?> + <tr py:for="index in range(len(values))"> + <td>${values[index]}</td> + </tr> + </tbody> + </table> + </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 new file mode 100644 index 00000000..064cebbb --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid @@ -0,0 +1,41 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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>Permission Denied</title> +</head> + +<body> + <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> +</body> + +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/master.kid b/ipa-server/ipa-gui/ipagui/templates/master.kid new file mode 100644 index 00000000..d8b34142 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/master.kid @@ -0,0 +1,121 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<?python import sitetemplate ?> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" py:extends="sitetemplate"> + +<head py:match="item.tag=='{http://www.w3.org/1999/xhtml}head'" py:attrs="item.items()"> + <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/> + <title py:replace="''">Your title goes here</title> + <meta py:replace="item[:]"/> + <style type="text/css" media="all"> + @import "${tg.url('/static/css/style_platform.css')}"; + @import "${tg.url('/static/css/style_platform-objects.css')}"; + @import "${tg.url('/static/css/style_freeipa.css')}"; + </style> + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/prototype.js')}"></script> + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/scriptaculous.js?load=effects')}"></script> + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/ipautil.js')}"></script> +</head> + +<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()"> + + <div id="head"> + <h1><a href="${tg.url('/')}">Free IPA</a></h1> + <div id="headerinfo"> + <div id="searchbar"> + <form action="${tg.url('/topsearch')}" method="post"> + <select name="searchtype"> + <option>Users</option> + <option>Groups</option> + </select> + <input class="searchtext" id="topsearchbox" type="text" + name="searchvalue" + value="Type search terms here." + onfocus="clearsearch()" /> + <input type="submit" value="Search"/> + </form> + <script type="text/javascript"> + function clearsearch() { + topsearchbox = document.getElementById('topsearchbox'); + topsearchbox.onfocus = null; + topsearchbox.value = ""; + } + </script> + </div> + </div> +</div> + <div id="navbar"> +<!-- hiding the tabs + <ul> + <li><a href="#">Overview</a></li> + <li class="active"><a href="#">Users</a></li> + <li><a href="#">Groups</a></li> + <li><a href="#">Resources</a></li> + <li><a href="#">Policy</a></li> + <li><a href="#">Search</a></li> + </ul> +--> + <div id="login"> + <div py:if="tg.config('identity.on') and not defined('logging_in')" id="pageLogin"> + <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> + </div> + + + </div> + + <div id="content"> + <div id="sidebar"> + <h2>Tasks</h2> + <ul> + <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 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('/principal/new')}">Add Service Principal</a></li> + <li><a href="${tg.url('/principal/list')}">Find Service Principal</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 py:if="not tg.identity.anonymous"><a href="${tg.url('/user/edit/', principal=tg.identity.user.display_name)}">Self Service</a></li> + </ul> + <ul py:if="'admins' in tg.identity.groups"> + <li><a href="${tg.url('/delegate/list')}">Delegations</a></li> + </ul> + </div> + + <div py:replace="[item.text]+item[:]"></div> + + + </div> + +</body> + +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/not_found.kid b/ipa-server/ipa-gui/ipagui/templates/not_found.kid new file mode 100644 index 00000000..0bc4c05c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/not_found.kid @@ -0,0 +1,37 @@ +<!-- + Copyright (C) 2007-2008 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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>Page Not Found</title> +</head> + +<body> + <div id="main_content"> + <h1>Page Not Found</h1> + <div py:if='message'> + ${message} + </div> + + </div> + +</body> +</html> 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 00000000..081b942d --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/policyindex.kid @@ -0,0 +1,48 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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 class="policy">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 00000000..7ece6cf9 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/policylayout.kid @@ -0,0 +1,34 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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/principallayout.kid b/ipa-server/ipa-gui/ipagui/templates/principallayout.kid new file mode 100644 index 00000000..62ec92bc --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principallayout.kid @@ -0,0 +1,36 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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/principallist.kid b/ipa-server/ipa-gui/ipagui/templates/principallist.kid new file mode 100644 index 00000000..9dc627ea --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principallist.kid @@ -0,0 +1,82 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'principallayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Find Service Principals</title> +</head> +<body> + <h1 class="system">Find Service Principals</h1> + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script> + <div id="search"> + <form action="${tg.url('/principal/list')}" method="get"> + <input id="hostname" type="text" name="hostname" value="${hostname}" /> + <input class="searchbutton" type="submit" value="Find Service Principals"/> + </form> + <script type="text/javascript"> + document.getElementById("hostname").focus(); + </script> + </div> + <div py:if='(principals != None) and (len(principals) > 0)'> + <h2>${len(principals)} results returned:</h2> + <table id="resultstable" class="details sortable resizable" cellspacing="0"> + <thead> + <tr> + <th> + Hostname + </th> + <th> + Service + </th> + </tr> + </thead> + <tbody> + <tr py:for="principal in principals"> + <td> + <a href="${tg.url('/principal/show',principal=principal.krbprincipalname,principal_dn=principal.dn)}" + >${principal.hostname}</a> + </td> + <td> + <a href="${tg.url('/principal/show',principal=principal.krbprincipalname,principal_dn=principal.dn)}" + >${principal.service}</a> + </td> + </tr> + </tbody> + </table> + </div> + <div id="alertbox" py:if='(principals != None) and (len(principals) == 0)'> + <p>No results found for "${hostname}"</p> + </div> + + <div class="instructions" py:if='principals == None'> + <p> + Exact matches are listed first, followed by partial matches. If your search + is too broad, you will get a warning that the search returned too many + results. Try being more specific. + </p> + <p> + The results that come back are sortable. Simply click on a column + header to sort on that header. A triangle will indicate the sorted + column, along with its direction. Clicking and dragging between headers + will allow you to resize the header. + </p> + </div> +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/principalnew.kid b/ipa-server/ipa-gui/ipagui/templates/principalnew.kid new file mode 100644 index 00000000..b7e02891 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principalnew.kid @@ -0,0 +1,30 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'principallayout.kid'"> +<head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> + <title>Add Service Principal</title> +</head> +<body> + <h1 class="system">Add Service Principal</h1> + + ${form.display(action=tg.url('/principal/create'), value=principal)} +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid b/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid new file mode 100644 index 00000000..fe865b52 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid @@ -0,0 +1,119 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<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="Add Principal"/> + +<?python +from ipagui.helpers import ipahelper +?> + + <script type="text/javascript" charset="utf-8" + src="${tg.url('/static/javascript/dynamicedit.js')}"></script> + + <?python searchurl = tg.url('/principal/edit_search') ?> + + <script type="text/javascript"> + function toggleOther(field) { + otherField = document.getElementById('form_other'); + var e=document.getElementById(field).value; + if ( e == "other") { + otherField.disabled = false; + } else { + otherField.disabled =true; + } + } + + function doSearch() { + $('searchresults').update("Searching..."); + new Ajax.Updater('searchresults', + '${searchurl}', + { asynchronous:true, + parameters: { criteria: $('criteria').value }, + evalScripts: true }); + return false; + } + </script> + + <div py:for="field in hidden_fields" + py:replace="field.display(value_for(field), **params_for(field))" + /> + + <h2 class="formsection">Service Principal Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${principal_fields.hostname.field_id}" + py:content="principal_fields.hostname.label" />: + </th> + <td> + <span py:replace="principal_fields.hostname.display(value_for(principal_fields.hostname))" /> + <span py:if="tg.errors.get('hostname')" class="fielderror" + py:content="tg.errors.get('hostname')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${principal_fields.service.field_id}" + py:content="principal_fields.service.label" />: + </th> + <td> + <span py:replace="principal_fields.service.display(value_for(principal_fields.service))" /> + <span py:if="tg.errors.get('service')" class="fielderror" + py:content="tg.errors.get('service')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${principal_fields.other.field_id}" + py:content="principal_fields.other.label" />: + </th> + <td> + <span py:replace="principal_fields.other.display(value_for(principal_fields.other))" /> + <span py:if="tg.errors.get('other')" class="fielderror" + py:content="tg.errors.get('other')" /> + <script type="text/javascript"> + var e=document.getElementById('form_service').value; + if ( e != "other") { + document.getElementById('form_other').disabled = true; + } + </script> + + </td> + </tr> + + </table> + +<hr /> + + <input type="submit" class="submitbutton" name="submit" value="Add Principal"/> + + </form> + + <script type="text/javascript"> + document.getElementById("form_hostname").focus(); + </script> + +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/principalshow.kid b/ipa-server/ipa-gui/ipagui/templates/principalshow.kid new file mode 100644 index 00000000..5904f034 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principalshow.kid @@ -0,0 +1,70 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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>View Service Principal</title> +</head> +<body> + + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script> + + <script type="text/javascript" charset="utf-8"> + function confirmDelete() { + if (confirm("Are you sure you want to delete this service principal?")) { + $('deleteform').submit(); + } + return false; + } + </script> + + <form id='deleteform' + method="post" action="${tg.url('/principal/delete')}"> + + <input type="hidden" name="principal" value="${principal.get('principal_dn')}" /> + + <input type="submit" class="submitbutton" + value="Delete Principal" + onclick="return confirmDelete();" + /> + + <h1 class="system">View Service Principal</h1> + + <h2 class="formsection">Principal</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel">Host</label>: + </th> + <td>${principal.get("hostname")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel">Service</label>: + </th> + <td>${principal.get("service")}</td> + </tr> + </table> + </form> + +<hr /> + +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/unhandled_exception.kid b/ipa-server/ipa-gui/ipagui/templates/unhandled_exception.kid new file mode 100644 index 00000000..89aececc --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/unhandled_exception.kid @@ -0,0 +1,48 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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>Error</title> +</head> + +<body> + <div id="main_content"> + <h1>An unexpected error occured</h1> + + <div py:if='message'> + <b>Message:</b> + <pre>${message}</pre> + </div> + + <div py:if='error_msg'> + <b>HTTP Error Message:</b> + <pre>${error_msg}</pre> + </div> + + <div py:if='details'> + <b>Stack Trace:</b> + <pre>${details}</pre> + </div> + </div> + +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/useredit.kid b/ipa-server/ipa-gui/ipagui/templates/useredit.kid new file mode 100644 index 00000000..9633b53a --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/useredit.kid @@ -0,0 +1,57 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'userlayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Edit User</title> +</head> +<body> + + <div> + <div style="float:right"> + <input type="checkbox" id="toggleprotected_checkbox" + onclick="toggleProtectedFields(this);"> + <span class="small">edit protected fields</span> + </input> + </div> + <h1 class="user">Edit User</h1> + </div> + +<?python +from ipagui.helpers import userhelper +pw_expires_days = userhelper.password_expires_in(user.get("krbPasswordExpiration")) +pw_expires_soon = userhelper.password_expires_soon(pw_expires_days) +pw_is_expired = userhelper.password_is_expired(pw_expires_days) +if pw_expires_days != 1: + days_suffix = "s" +else: + days_suffix = "" +?> + + <div py:if='pw_expires_soon' class="warning_message"> + ${user.get("uid")}'s password will expire in ${pw_expires_days} day${days_suffix} + </div> + <div py:if='pw_is_expired' class="warning_message"> + ${user.get("uid")}'s password has expired + </div> + + ${form.display(action=tg.url('/user/update'), value=user, user_groups=user_groups)} +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/usereditform.kid b/ipa-server/ipa-gui/ipagui/templates/usereditform.kid new file mode 100644 index 00000000..b2a3c39c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/usereditform.kid @@ -0,0 +1,949 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<div xmlns:py="http://purl.org/kid/ns#" + class="simpleroster"> + + <form style="display:none" id='deleteform' + method="post" action="${tg.url('/user/delete')}"> + <input type="hidden" name="uid" value="${value.get('uid')}" /> + </form> + + <form action="${action}" name="${name}" method="${method}" class="tableform" + onsubmit="preSubmit()"> + + <input type="submit" class="submitbutton" name="submit" + value="Update User"/> + <input type="submit" class="submitbutton" name="submit" + value="Cancel Edit" /> + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups and tg.identity.display_name != value.get('uid')" + type="button" class="submitbutton" + value="Delete User" + onclick="return confirmDelete();" + /> + +<?python +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('/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') + selectSearchurl = tg.url('/user/user_select_search') + ?> + + <script type="text/javascript"> + function toggleProtectedFields(checkbox) { + passwordField = document.getElementById('form_krbprincipalkey'); + passwordConfirmField = document.getElementById('form_krbprincipalkey_confirm'); + uidField = document.getElementById('form_uid'); + uidnumberField = document.getElementById('form_uidnumber'); + gidnumberField = document.getElementById('form_gidnumber'); + homedirectoryField = document.getElementById('form_homedirectory'); + if (checkbox.checked) { + passwordField.disabled = false; + passwordConfirmField.disabled = false; + uidField.disabled = false; + uidnumberField.disabled = false; + gidnumberField.disabled = false; + homedirectoryField.disabled = false; + $('form_editprotected').value = 'true'; + } else { + passwordField.disabled = true; + passwordConfirmField.disabled = true; + uidField.disabled = true; + uidnumberField.disabled = true; + gidnumberField.disabled = true; + homedirectoryField.disabled = true; + $('form_editprotected').value = ''; + } + } + + function warnRDN() { + if (confirm("Are you sure you want to change the login name?\nThis can have unexpected results. Additionally, a password change will be required.")) { + return true; + } + return false; + } + + function doSearch() { + $('searchresults').update("Searching..."); + new Ajax.Updater('searchresults', + '${searchurl}', + { asynchronous:true, + parameters: { criteria: $('criteria').value }, + evalScripts: true }); + return false; + } + + // override dynamicedit.js version + // we don't need to show [group] nor italize groups + function renderMemberInfo(newdiv, info) { + if (info.type == "group") { + newdiv.appendChild(document.createTextNode( + info.name + " ")); + } + } + + function doSelectSearch(which_select) { + $(which_select + '_searchresults').update("Searching..."); + new Ajax.Updater(which_select + '_searchresults', + '${selectSearchurl}', + { asynchronous:true, + parameters: { criteria: $(which_select + '_criteria').value, + which_select: which_select}, + evalScripts: true }); + return false; + } + + function confirmDelete() { + if (confirm("Are you sure you want to delete this person?")) { + $('deleteform').submit(); + } + return false; + } + </script> + + + <div py:for="field in hidden_fields" + py:replace="field.display(value_for(field), **params_for(field))" + /> + + <h2 class="formsection">Identity Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.title.field_id}" + py:content="user_fields.title.label" />: + </th> + <td> + <span py:replace="user_fields.title.display(value_for(user_fields.title))" /> + <span py:if="tg.errors.get('title')" class="fielderror" + py:content="tg.errors.get('title')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.givenname.field_id}" + py:content="user_fields.givenname.label" />: + </th> + <td> + <span py:replace="user_fields.givenname.display(value_for(user_fields.givenname))" /> + <span py:if="tg.errors.get('givenname')" class="fielderror" + py:content="tg.errors.get('givenname')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.sn.field_id}" + py:content="user_fields.sn.label" />: + </th> + <td> + <span py:replace="user_fields.sn.display(value_for(user_fields.sn))" /> + <span py:if="tg.errors.get('sn')" class="fielderror" + py:content="tg.errors.get('sn')" /> + </td> + </tr> + + <tr> + <th> + <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 Full Name</a> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.displayname.field_id}" + py:content="user_fields.displayname.label" />: + </th> + <td> + <span py:replace="user_fields.displayname.display(value_for(user_fields.displayname))" /> + <span py:if="tg.errors.get('displayname')" class="fielderror" + py:content="tg.errors.get('displayname')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.initials.field_id}" + py:content="user_fields.initials.label" />: + </th> + <td> + <span py:replace="user_fields.initials.display(value_for(user_fields.initials))" /> + <span py:if="tg.errors.get('initials')" class="fielderror" + py:content="tg.errors.get('initials')" /> + + </td> + </tr> + </table> + + <h2 class="formsection">Account Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.nsAccountLock.field_id}" + py:content="user_fields.nsAccountLock.label" />: + </th> + <td> + <span py:replace="user_fields.nsAccountLock.display(value_for(user_fields.nsAccountLock))" /> + <span py:if="tg.errors.get('nsAccountLock')" class="fielderror" + py:content="tg.errors.get('nsAccountLock')" /> + <script py:if="tg.identity.display_name == value.get('uid')" type="text/javascript"> + document.getElementById('form_nsAccountLock').disabled = true; + </script> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.uid.field_id}" + py:content="user_fields.uid.label" />: + </th> + <td> + <span py:replace="user_fields.uid.display( + value_for(user_fields.uid))" /> + <span py:if="tg.errors.get('uid')" class="fielderror" + py:content="tg.errors.get('uid')" /> + + <script type="text/javascript"> + document.getElementById('form_uid').disabled = true; + </script> + </td> + </tr> + + <tr> + <th valign="top"> + <label class="fieldlabel" for="${user_fields.krbprincipalkey.field_id}" + py:content="user_fields.krbprincipalkey.label" />: + </th> + <td valign="top"> + <span py:replace="user_fields.krbprincipalkey.display(value_for(user_fields.krbprincipalkey))" /> + <span py:if="tg.errors.get('krbprincipalkey')" class="fielderror" + py:content="tg.errors.get('krbprincipalkey')" /> + + <script type="text/javascript"> + document.getElementById('form_krbprincipalkey').disabled = true; + </script> + + <!-- + <span id="password_text">********</span> + <input id="genpassword_button" type="button" value="Generate Password" + disabled="true" + onclick="new Ajax.Request('${tg.url('/user/generate_password')}', + { + method: 'get', + onSuccess: function(transport) { + document.getElementById('form_krbprincipalkey').value = + transport.responseText; + } + });" /> + <br /> + <input type="checkbox" + onclick="togglePassword(this);"><span class="xsmall">edit</span></input> + <script type="text/javascript"> + document.getElementById('form_krbprincipalkey').style.display='none'; + + function togglePassword(checkbox) { + passwordField = document.getElementById('form_krbprincipalkey'); + passwordText = document.getElementById('password_text'); + passwordButton = document.getElementById('genpassword_button'); + if (checkbox.checked) { + passwordField.style.display='inline'; + passwordText.style.display='none'; + passwordButton.disabled=false; + } else { + passwordField.style.display='none'; + passwordText.style.display='inline'; + passwordButton.disabled=true; + } + } + </script> + --> + </td> + </tr> + + <tr> + <th valign="top"> + <label class="fieldlabel" for="${user_fields.krbprincipalkey_confirm.field_id}" + py:content="user_fields.krbprincipalkey_confirm.label" />: + </th> + <td valign="top"> + <span py:replace="user_fields.krbprincipalkey_confirm.display( + value_for(user_fields.krbprincipalkey_confirm))" /> + <span py:if="tg.errors.get('krbprincipalkey_confirm')" class="fielderror" + py:content="tg.errors.get('krbprincipalkey_confirm')" /> + + <script type="text/javascript"> + document.getElementById('form_krbprincipalkey_confirm').disabled = true; + </script> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.uidnumber.field_id}" + py:content="user_fields.uidnumber.label" />: + </th> + <td> + <span py:replace="user_fields.uidnumber.display( + value_for(user_fields.uidnumber))" /> + <span py:if="tg.errors.get('uidnumber')" class="fielderror" + py:content="tg.errors.get('uidnumber')" /> + + <script type="text/javascript"> + document.getElementById('form_uidnumber').disabled = true; + </script> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.gidnumber.field_id}" + py:content="user_fields.gidnumber.label" />: + </th> + <td> + <span py:replace="user_fields.gidnumber.display( + value_for(user_fields.gidnumber))" /> + <span py:if="tg.errors.get('gidnumber')" class="fielderror" + py:content="tg.errors.get('gidnumber')" /> + + <script type="text/javascript"> + document.getElementById('form_gidnumber').disabled = true; + </script> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.homedirectory.field_id}" + py:content="user_fields.homedirectory.label" />: + </th> + <td> + <span py:replace="user_fields.homedirectory.display( + value_for(user_fields.homedirectory))" /> + <span py:if="tg.errors.get('homedirectory')" class="fielderror" + py:content="tg.errors.get('homedirectory')" /> + + <script type="text/javascript"> + document.getElementById('form_homedirectory').disabled = true; + </script> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.loginshell.field_id}" + py:content="user_fields.loginshell.label" />: + </th> + <td> + <span py:replace="user_fields.loginshell.display( + value_for(user_fields.loginshell))" /> + <span py:if="tg.errors.get('loginshell')" class="fielderror" + py:content="tg.errors.get('loginshell')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.gecos.field_id}" + py:content="user_fields.gecos.label" />: + </th> + <td> + <span py:replace="user_fields.gecos.display( + value_for(user_fields.gecos))" /> + <span py:if="tg.errors.get('gecos')" class="fielderror" + py:content="tg.errors.get('gecos')" /> + </td> + </tr> + </table> + + <h2 class="formsection">Contact Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.mail.field_id}" + py:content="user_fields.mail.label" />: + </th> + <td> + <span py:replace="user_fields.mail.display(value_for(user_fields.mail))" /> + <span py:if="tg.errors.get('mail')" class="fielderror" + py:content="tg.errors.get('mail')" /> + </td> + </tr> + + <tr> + <th> + <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.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.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.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.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> + + <h2 class="formsection">Mailing Address</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.street.field_id}" + py:content="user_fields.street.label" />: + </th> + <td> + <span py:replace="user_fields.street.display(value_for(user_fields.street))" /> + <span py:if="tg.errors.get('street')" class="fielderror" + py:content="tg.errors.get('street')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.roomnumber.field_id}" + py:content="user_fields.roomnumber.label" />: + </th> + <td> + <span py:replace="user_fields.roomnumber.display(value_for(user_fields.roomnumber))" /> + <span py:if="tg.errors.get('roomnumber')" class="fielderror" + py:content="tg.errors.get('roomnumber')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.l.field_id}" + py:content="user_fields.l.label" />: + </th> + <td> + <span py:replace="user_fields.l.display(value_for(user_fields.l))" /> + <span py:if="tg.errors.get('l')" class="fielderror" + py:content="tg.errors.get('l')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.st.field_id}" + py:content="user_fields.st.label" />: + </th> + <td> + <span py:replace="user_fields.st.display(value_for(user_fields.st))" /> + <span py:if="tg.errors.get('st')" class="fielderror" + py:content="tg.errors.get('st')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.postalcode.field_id}" + py:content="user_fields.postalcode.label" />: + </th> + <td> + <span py:replace="user_fields.postalcode.display(value_for(user_fields.postalcode))" /> + <span py:if="tg.errors.get('postalcode')" class="fielderror" + py:content="tg.errors.get('postalcode')" /> + </td> + </tr> + </table> + + <h2 class="formsection">Employee Information</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.ou.field_id}" + py:content="user_fields.ou.label" />: + </th> + <td> + <span py:replace="user_fields.ou.display(value_for(user_fields.ou))" /> + <span py:if="tg.errors.get('ou')" class="fielderror" + py:content="tg.errors.get('ou')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.businesscategory.field_id}" + py:content="user_fields.businesscategory.label" />: + </th> + <td> + <span py:replace="user_fields.businesscategory.display(value_for(user_fields.businesscategory))" /> + <span py:if="tg.errors.get('businesscategory')" class="fielderror" + py:content="tg.errors.get('businesscategory')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.description.field_id}" + py:content="user_fields.description.label" />: + </th> + <td> + <span py:replace="user_fields.description.display(value_for(user_fields.description))" /> + <span py:if="tg.errors.get('description')" class="fielderror" + py:content="tg.errors.get('description')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.employeetype.field_id}" + py:content="user_fields.employeetype.label" />: + </th> + <td> + <span py:replace="user_fields.employeetype.display(value_for(user_fields.employeetype))" /> + <span py:if="tg.errors.get('employeetype')" class="fielderror" + py:content="tg.errors.get('employeetype')" /> + </td> + </tr> + + <tr> + <th valign="top"> + <label class="fieldlabel" for="${user_fields.manager.field_id}" + py:content="user_fields.manager.label" />: + </th> + <td valign="top"> + <div> + <span id='manager_select_cn'>${value_for(user_fields.manager_cn)}</span> + <span py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name != value.get('uid')" id='manager_links'> + <a href="#" onclick="return clearSelect('manager');">clear</a> + <a href="#" onclick="return startSelect('manager');">change</a> + </span> + <span py:if="tg.errors.get('manager')" class="fielderror" + py:content="tg.errors.get('manager')" /> + </div> + <div id="manager_searcharea" style="display:none"> + <div> + <input id="manager_criteria" type="text" + onkeypress="return enterDoSelectSearch(event, 'manager');" /> + <input type="button" value="Find" + onclick="return doSelectSearch('manager');" + /> + </div> + <div id="manager_searchresults"> + </div> + </div> + </td> + </tr> + + <tr> + <th valign="top"> + <label class="fieldlabel" for="${user_fields.secretary.field_id}" + py:content="user_fields.secretary.label" />: + </th> + <td valign="top"> + <div> + <span id='secretary_select_cn'>${value_for(user_fields.secretary_cn)}</span> + <span id='secretary_links'> + <a href="#" onclick="return clearSelect('secretary');">clear</a> + <a href="#" onclick="return startSelect('secretary');">change</a> + </span> + <span py:if="tg.errors.get('secretary')" class="fielderror" + py:content="tg.errors.get('secretary')" /> + </div> + <div id="secretary_searcharea" style="display:none"> + <div> + <input id="secretary_criteria" type="text" + onkeypress="return enterDoSelectSearch(event, 'secretary');" /> + <input type="button" value="Find" + onclick="return doSelectSearch('secretary');" + /> + </div> + <div id="secretary_searchresults"> + </div> + </div> + </td> + </tr> + </table> + + <h2 class="formsection">Misc Information</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.carlicense.field_id}" + py:content="user_fields.carlicense.label" />: + </th> + <td> + <span py:replace="user_fields.carlicense.display(value_for(user_fields.carlicense))" /> + <span py:if="tg.errors.get('carlicense')" class="fielderror" + py:content="tg.errors.get('carlicense')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.labeleduri.field_id}" + py:content="user_fields.labeleduri.label" />: + </th> + <td> + <span py:replace="user_fields.labeleduri.display(value_for(user_fields.labeleduri))" /> + <span py:if="tg.errors.get('labeleduri')" class="fielderror" + py:content="tg.errors.get('labeleduri')" /> + </td> + </tr> + </table> + + <div py:if='len(custom_fields) > 0'> + <h2 class="formsection" >Custom Fields</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr py:for='custom_field in custom_fields'> + <th> + <label class="fieldlabel" for="${custom_field.field_id}" + py:content="custom_field.label" />: + </th> + <td> + <span py:replace="custom_field.display(value_for(custom_field))" /> + <span py:if="tg.errors.get(custom_field.name)" class="fielderror" + py:content="tg.errors.get(custom_field.name)" /> + </td> + </tr> + </table> + </div> + + + <div> + <h2 class="formsection">Groups</h2> + + <div class="floatlist"> + <div py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name != value.get('uid')" class="floatheader">To Remove:</div> + <div id="delmembers"> + </div> + </div> + + <div> + <?python div_counter = 1 ?> + <div py:for="group in user_groups" id="member-${div_counter}"> + <?python + group_dn = group.get('dn') + group_dn_esc = ipahelper.javascript_string_escape(group_dn) + + group_name = group.get('cn') + group_descr = "[group]" + group_type = "group" + + group_name_esc = ipahelper.javascript_string_escape(group_name) + group_descr_esc = ipahelper.javascript_string_escape(group_descr) + group_type_esc = ipahelper.javascript_string_escape(group_type) + ?> + <span id="member-info-${div_counter}"></span> + <script type="text/javascript"> + renderMemberInfo($('member-info-${div_counter}'), + new MemberDisplayInfo('${group_name_esc}', + '${group_descr_esc}', + '${group_type_esc}')); + </script> + <a py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name != value.get('uid')" href="#" + onclick="removememberHandler(this, '${group_dn_esc}', + new MemberDisplayInfo('${group_name_esc}', + '${group_descr_esc}', + '${group_type_esc}')); + return false;" + >remove</a> + <script type="text/javascript"> + dn_to_member_div_id['${group_dn_esc}'] = "member-${div_counter}"; + member_hash["${group_dn_esc}"] = 1; + </script> + <?python + div_counter = div_counter + 1 + ?> + </div> + <!-- a space here to prevent an empty div --> + </div> + + </div> + + <div py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name != value.get('uid')" style="clear:both"> + <h2 class="formsection">Add Groups</h2> + + <div class="floatlist"> + <div class="floatheader">To Add:</div> + <div id="newmembers"> + </div> + </div> + + <div> + <div id="search"> + <input id="criteria" type="text" name="criteria" + onkeypress="return enterDoSearch(event);" /> + <input type="button" value="Find" + onclick="return doSearch();" + /> + </div> + <div id="searchresults"> + </div> + </div> + </div> + + <hr/> + + <input type="submit" class="submitbutton" name="submit" + value="Update User"/> + <input type="submit" class="submitbutton" name="submit" + value="Cancel Edit" /> + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups and tg.identity.display_name != value.get('uid')" + type="button" class="submitbutton" + value="Delete User" + onclick="return confirmDelete();" + /> + + </form> + + <script type="text/javascript"> + /* + * This section restores the contents of the add and remove lists + * dynamically if we have to refresh the page + */ + if ($('form_dn_to_info_json').value != "") { + dn_to_info_hash = new Hash($('form_dn_to_info_json').value.evalJSON()); + } + + if ($('form_editprotected').value != "") { + $('toggleprotected_checkbox').checked = true; + toggleProtectedFields($('toggleprotected_checkbox')); + } + </script> + + <?python + dnadds = value.get('dnadd', []) + if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)): + dnadds = [dnadds] + + dndels = value.get('dndel', []) + if not(isinstance(dndels,list) or isinstance(dndels,tuple)): + dndels = [dndels] + ?> + + <script py:for="dnadd in dnadds"> + <?python + dnadd_esc = ipahelper.javascript_string_escape(dnadd) + ?> + var dn = "${dnadd_esc}"; + var info = dn_to_info_hash[dn]; + var newdiv = addmember(dn, info); + if (newdiv != null) { + newdiv.style.display = 'block'; + } + </script> + + <script py:for="dndel in dndels"> + <?python + dndel_esc = ipahelper.javascript_string_escape(dndel) + ?> + var dn = "${dndel_esc}"; + var info = dn_to_info_hash[dn]; + var newdiv = removemember(dn, info); + newdiv.style.display = 'block'; + orig_div_id = dn_to_member_div_id[dn] + $(orig_div_id).style.display = 'none'; + </script> + +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/userlayout.kid b/ipa-server/ipa-gui/ipagui/templates/userlayout.kid new file mode 100644 index 00000000..7625ffdd --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/userlayout.kid @@ -0,0 +1,41 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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 id="sidebar"> + <h2>Tools</h2> + <a href="${tg.url('/user/new')}">Add User</a><br/> + <a href="${tg.url('/user/list')}">Find Users</a><br/> + </div> --> + </div> +</body> + +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/userlist.kid b/ipa-server/ipa-gui/ipagui/templates/userlist.kid new file mode 100644 index 00000000..5dfe2bde --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/userlist.kid @@ -0,0 +1,118 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'userlayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Find Users</title> +</head> +<body> + <h1 class="user">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 Users"/> + </form> + <script type="text/javascript"> + document.getElementById("uid").focus(); + </script> + </div> + <div py:if='(users != None) and (len(users) > 0)'> + <h2>${len(users)} results returned:</h2> + <table id="resultstable" class="details sortable resizable" cellspacing="0"> + <thead> + <tr> + <th> + User + </th> + <th> + Phone + </th> + <th> + Unit + </th> + <th> + Job Title + </th> + </tr> + </thead> + <tbody> + <tr py:for="user in users" py:if="user.nsAccountLock != 'true'"> + <td> + <a + href="${tg.url('/user/show',uid=user.uid)}" + py:content="u'%s %s (%s)' % (user.givenName, user.sn, user.uid)" + /> + </td> + <td> + ${user.telephoneNumber} + </td> + <td> + ${user.ou} + </td> + <td> + ${user.title} + </td> + </tr> + </tbody> + <tbody> + <tr id="inactive" py:for="user in users" py:if="user.nsAccountLock == 'true'"> + <td> + <a + href="${tg.url('/user/show',uid=user.uid)}" + py:content="u'%s %s (%s)' % (user.givenName, user.sn, user.uid)" + /> + </td> + <td> + ${user.telephoneNumber} + </td> + <td> + ${user.ou} + </td> + <td> + ${user.title} + </td> + </tr> + </tbody> + </table> + </div> + <div id="alertbox" py:if='(users != None) and (len(users) == 0)'> + <p>No results found for "${uid}"</p> + </div> + + <div class="instructions" py:if='users == None'> + <p> + Search automatically looks across multiple fields. If you want to find + Joe in Finance, try typing "joe finance" into the search box. + </p> + <p> + Exact matches are listed first, followed by partial matches. If your search + is too broad, you will get a warning that the search returned too many + results. Try being more specific. + </p> + <p> + The results that come back are sortable. Simply click on a column + header to sort on that header. A triangle will indicate the sorted + column, along with its direction. Clicking and dragging between headers + will allow you to resize the header. + </p> + </div> +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/usernew.kid b/ipa-server/ipa-gui/ipagui/templates/usernew.kid new file mode 100644 index 00000000..bbb74324 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/usernew.kid @@ -0,0 +1,30 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'userlayout.kid'"> +<head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> + <title>Add User</title> +</head> +<body> + <h1 class="user">Add User</h1> + + ${form.display(action=tg.url("/user/create"), value=user)} +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/usernewform.kid b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid new file mode 100644 index 00000000..cd924a72 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid @@ -0,0 +1,842 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<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="Add User"/> + +<?python +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('/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') + selectSearchurl = tg.url('/user/user_select_search') + ?> + + <script type="text/javascript"> + function doSearch() { + $('searchresults').update("Searching..."); + new Ajax.Updater('searchresults', + '${searchurl}', + { asynchronous:true, + parameters: { criteria: $('criteria').value }, + evalScripts: true }); + return false; + } + + // override dynamicedit.js version + // we don't need to show [group] nor italize groups + function renderMemberInfo(newdiv, info) { + if (info.type == "group") { + newdiv.appendChild(document.createTextNode( + info.name + " ")); + } + } + function doSelectSearch(which_select) { + $(which_select + '_searchresults').update("Searching..."); + new Ajax.Updater(which_select + '_searchresults', + '${selectSearchurl}', + { asynchronous:true, + parameters: { criteria: $(which_select + '_criteria').value, + which_select: which_select}, + evalScripts: true }); + return false; + } + </script> + + <div py:for="field in hidden_fields" + py:replace="field.display(value_for(field), **params_for(field))" + /> + + <h2 class="formsection">Identity Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.title.field_id}" + py:content="user_fields.title.label" />: + </th> + <td> + <span py:replace="user_fields.title.display(value_for(user_fields.title))" /> + <span py:if="tg.errors.get('title')" class="fielderror" + py:content="tg.errors.get('title')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.givenname.field_id}" + py:content="user_fields.givenname.label" />: + </th> + <td> + <span py:replace="user_fields.givenname.display(value_for(user_fields.givenname))" /> + <span py:if="tg.errors.get('givenname')" class="fielderror" + py:content="tg.errors.get('givenname')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.sn.field_id}" + py:content="user_fields.sn.label" />: + </th> + <td> + <span py:replace="user_fields.sn.display(value_for(user_fields.sn))" /> + <span py:if="tg.errors.get('sn')" class="fielderror" + py:content="tg.errors.get('sn')" /> + <script type="text/javascript"> + var uid_suggest = ""; + var mail_suggest = ""; + var cn_suggest = ""; + var displayname_suggest = ""; + var initials_suggest = ""; + + function autofill(self) { + var givenname = $('form_givenname'); + var sn = $('form_sn'); + if ((givenname.value == "") || (sn.value == "")) { + return; + } + + var uid = $('form_uid'); + var mail = $('form_mail'); + var cn = $('form_cns_0_cn'); + var displayname = $('form_displayname'); + var initials = $('form_initials'); + + if ((cn.value == "") || (cn.value == cn_suggest)) { + cn.value = givenname.value + " " + sn.value; + cn_suggest = cn.value; + new Effect.Highlight(cn); + } + + if ((displayname.value == "") || + (displayname.value == displayname_suggest)) { + displayname.value = givenname.value + " " + sn.value; + displayname_suggest = displayname.value; + new Effect.Highlight(displayname); + } + + if ((initials.value == "") || + (initials.value == initials_suggest)) { + initials.value = givenname.value[0] + sn.value[0]; + initials_suggest = initials.value; + new Effect.Highlight(initials); + } + + if ((uid.value == "") || (uid.value == uid_suggest)) { + new Ajax.Request('${tg.url('/user/suggest_uid')}', { + method: 'get', + parameters: {'givenname': givenname.value, 'sn': sn.value}, + onSuccess: function(transport) { + uid.value = transport.responseText; + uid_suggest = uid.value; + new Effect.Highlight(uid); + } + }); + } + + if ((mail.value == "") || (mail.value == mail_suggest)) { + new Ajax.Request('${tg.url('/user/suggest_email')}', { + method: 'get', + parameters: {'givenname': givenname.value, 'sn': sn.value}, + onSuccess: function(transport) { + mail.value = transport.responseText; + mail_suggest = mail.value; + new Effect.Highlight(mail); + } + }); + } + } + + document.getElementById('form_givenname').onchange = autofill; + document.getElementById('form_sn').onchange = autofill; + </script> + </td> + </tr> + + <tr> + <th> + <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') + 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 Full Name</a> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.displayname.field_id}" + py:content="user_fields.displayname.label" />: + </th> + <td> + <span py:replace="user_fields.displayname.display(value_for(user_fields.displayname))" /> + <span py:if="tg.errors.get('displayname')" class="fielderror" + py:content="tg.errors.get('displayname')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.initials.field_id}" + py:content="user_fields.initials.label" />: + </th> + <td> + <span py:replace="user_fields.initials.display(value_for(user_fields.initials))" /> + <span py:if="tg.errors.get('initials')" class="fielderror" + py:content="tg.errors.get('initials')" /> + + </td> + </tr> + </table> + + <h2 class="formsection">Account Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.nsAccountLock.field_id}" + py:content="user_fields.nsAccountLock.label" />: + </th> + <td> + <span py:replace="user_fields.nsAccountLock.display(value_for(user_fields.nsAccountLock))" /> + <span py:if="tg.errors.get('nsAccountLock')" class="fielderror" + py:content="tg.errors.get('nsAccountLock')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.uid.field_id}" + py:content="user_fields.uid.label" />: + </th> + <td> + <span py:replace="user_fields.uid.display(value_for(user_fields.uid))" /> + <span py:if="tg.errors.get('uid')" class="fielderror" + py:content="tg.errors.get('uid')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.krbprincipalkey.field_id}" + py:content="user_fields.krbprincipalkey.label" />: + </th> + <td> + <span py:replace="user_fields.krbprincipalkey.display(value_for(user_fields.krbprincipalkey))" /> + <span py:if="tg.errors.get('krbprincipalkey')" class="fielderror" + py:content="tg.errors.get('krbprincipalkey')" /> + + <!-- + <input type="button" value="Generate Password" + onclick="new Ajax.Request('${tg.url('/user/generate_password')}', + { + method: 'get', + onSuccess: function(transport) { + document.getElementById('form_krbprincipalkey').value = + transport.responseText; + } + });" /> + --> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.krbprincipalkey_confirm.field_id}" + py:content="user_fields.krbprincipalkey_confirm.label" />: + </th> + <td> + <span py:replace="user_fields.krbprincipalkey_confirm.display( + value_for(user_fields.krbprincipalkey_confirm))" /> + <span py:if="tg.errors.get('krbprincipalkey_confirm')" class="fielderror" + py:content="tg.errors.get('krbprincipalkey_confirm')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.uidnumber.field_id}" + py:content="user_fields.uidnumber.label" />: + </th> + <td> + Generated by server + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.gidnumber.field_id}" + py:content="user_fields.gidnumber.label" />: + </th> + <td> + Generated by server + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.homedirectory.field_id}" + py:content="user_fields.homedirectory.label" />: + </th> + <td> + Generated by server + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.loginshell.field_id}" + py:content="user_fields.loginshell.label" />: + </th> + <td> + <span py:replace="user_fields.loginshell.display( + value_for(user_fields.loginshell))" /> + <span py:if="tg.errors.get('loginshell')" class="fielderror" + py:content="tg.errors.get('loginshell')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.gecos.field_id}" + py:content="user_fields.gecos.label" />: + </th> + <td> + <span py:replace="user_fields.gecos.display( + value_for(user_fields.gecos))" /> + <span py:if="tg.errors.get('gecos')" class="fielderror" + py:content="tg.errors.get('gecos')" /> + </td> + </tr> + </table> + + <h2 class="formsection">Contact Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.mail.field_id}" + py:content="user_fields.mail.label" />: + </th> + <td> + <span py:replace="user_fields.mail.display(value_for(user_fields.mail))" /> + <span py:if="tg.errors.get('mail')" class="fielderror" + py:content="tg.errors.get('mail')" /> + </td> + </tr> + + <tr> + <th> + <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> + + </table> + + <h2 class="formsection">Mailing Address</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.street.field_id}" + py:content="user_fields.street.label" />: + </th> + <td> + <span py:replace="user_fields.street.display(value_for(user_fields.street))" /> + <span py:if="tg.errors.get('street')" class="fielderror" + py:content="tg.errors.get('street')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.roomnumber.field_id}" + py:content="user_fields.roomnumber.label" />: + </th> + <td> + <span py:replace="user_fields.roomnumber.display(value_for(user_fields.roomnumber))" /> + <span py:if="tg.errors.get('roomnumber')" class="fielderror" + py:content="tg.errors.get('roomnumber')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.l.field_id}" + py:content="user_fields.l.label" />: + </th> + <td> + <span py:replace="user_fields.l.display(value_for(user_fields.l))" /> + <span py:if="tg.errors.get('l')" class="fielderror" + py:content="tg.errors.get('l')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.st.field_id}" + py:content="user_fields.st.label" />: + </th> + <td> + <span py:replace="user_fields.st.display(value_for(user_fields.st))" /> + <span py:if="tg.errors.get('st')" class="fielderror" + py:content="tg.errors.get('st')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.postalcode.field_id}" + py:content="user_fields.postalcode.label" />: + </th> + <td> + <span py:replace="user_fields.postalcode.display(value_for(user_fields.postalcode))" /> + <span py:if="tg.errors.get('postalcode')" class="fielderror" + py:content="tg.errors.get('postalcode')" /> + </td> + </tr> + </table> + + <h2 class="formsection">Employee Information</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.ou.field_id}" + py:content="user_fields.ou.label" />: + </th> + <td> + <span py:replace="user_fields.ou.display(value_for(user_fields.ou))" /> + <span py:if="tg.errors.get('ou')" class="fielderror" + py:content="tg.errors.get('ou')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.businesscategory.field_id}" + py:content="user_fields.businesscategory.label" />: + </th> + <td> + <span py:replace="user_fields.businesscategory.display(value_for(user_fields.businesscategory))" /> + <span py:if="tg.errors.get('businesscategory')" class="fielderror" + py:content="tg.errors.get('businesscategory')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.description.field_id}" + py:content="user_fields.description.label" />: + </th> + <td> + <span py:replace="user_fields.description.display(value_for(user_fields.description))" /> + <span py:if="tg.errors.get('description')" class="fielderror" + py:content="tg.errors.get('description')" /> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.employeetype.field_id}" + py:content="user_fields.employeetype.label" />: + </th> + <td> + <span py:replace="user_fields.employeetype.display(value_for(user_fields.employeetype))" /> + <span py:if="tg.errors.get('employeetype')" class="fielderror" + py:content="tg.errors.get('employeetype')" /> + </td> + </tr> + + <tr> + <th valign="top"> + <label class="fieldlabel" for="${user_fields.manager.field_id}" + py:content="user_fields.manager.label" />: + </th> + <td valign="top"> + <div> + <span id='manager_select_cn'>${value_for(user_fields.manager)}</span> + <span id='manager_links'> + <a href="#" onclick="return clearSelect('manager');">clear</a> + <a href="#" onclick="return startSelect('manager');">change</a> + </span> + <span py:if="tg.errors.get('manager')" class="fielderror" + py:content="tg.errors.get('manager')" /> + </div> + <div id="manager_searcharea" style="display:none"> + <div> + <input id="manager_criteria" type="text" + onkeypress="return enterDoSelectSearch(event, 'manager');" /> + <input type="button" value="Find" + onclick="return doSelectSearch('manager');" + /> + </div> + <div id="manager_searchresults"> + </div> + </div> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.secretary.field_id}" + py:content="user_fields.secretary.label" />: + </th> + <td> + <div> + <span id='secretary_select_cn'>${value_for(user_fields.secretary)}</span> + <span id='secretary_links'> + <a href="#" onclick="return clearSelect('secretary');">clear</a> + <a href="#" onclick="return startSelect('secretary');">change</a> + </span> + <span py:if="tg.errors.get('secretary')" class="fielderror" + py:content="tg.errors.get('secretary')" /> + </div> + <div id="secretary_searcharea" style="display:none"> + <div> + <input id="secretary_criteria" type="text" + onkeypress="return enterDoSelectSearch(event, 'secretary');" /> + <input type="button" value="Find" + onclick="return doSelectSearch('secretary');" + /> + </div> + <div id="secretary_searchresults"> + </div> + </div> + </td> + </tr> + </table> + + <h2 class="formsection">Misc Information</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.carlicense.field_id}" + py:content="user_fields.carlicense.label" />: + </th> + <td> + <span py:replace="user_fields.carlicense.display(value_for(user_fields.carlicense))" /> + <span py:if="tg.errors.get('carlicense')" class="fielderror" + py:content="tg.errors.get('carlicense')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.labeleduri.field_id}" + py:content="user_fields.labeleduri.label" />: + </th> + <td> + <span py:replace="user_fields.labeleduri.display(value_for(user_fields.labeleduri))" /> + <span py:if="tg.errors.get('labeleduri')" class="fielderror" + py:content="tg.errors.get('labeleduri')" /> + </td> + </tr> + </table> + + <div py:if='len(custom_fields) > 0'> + <div class="formsection" >Custom Fields</div> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr py:for='custom_field in custom_fields'> + <th> + <label class="fieldlabel" for="${custom_field.field_id}" + py:content="custom_field.label" />: + </th> + <td> + <span py:replace="custom_field.display(value_for(custom_field))" /> + <span py:if="tg.errors.get(custom_field.name)" class="fielderror" + py:content="tg.errors.get(custom_field.name)" /> + </td> + </tr> + </table> + </div> + + <div style="clear:both"> + <h2 class="formsection">Add Groups</h2> + + + <div class="floatlist"> + <div class="floatheader">To Add:</div> + <div id="newmembers"> + </div> + </div> + + <div> + <div id="search"> + <input id="criteria" type="text" name="criteria" + onkeypress="return enterDoSearch(event);" /> + <input class="searchbutton" type="button" value="Find" + onclick="return doSearch();" + /> + </div> + <div id="searchresults"> + </div> + </div> + </div> + +<hr /> +<input type="submit" class="submitbutton" name="submit" value="Add User"/> + + </form> + + <script type="text/javascript"> + document.getElementById("form_title").focus(); + </script> + + <script type="text/javascript"> + /* + * This section restores the contents of the add and remove lists + * dynamically if we have to refresh the page + */ + if ($('form_dn_to_info_json').value != "") { + dn_to_info_hash = new Hash($('form_dn_to_info_json').value.evalJSON()); + } + </script> + + <?python + dnadds = value.get('dnadd', []) + if not(isinstance(dnadds,list) or isinstance(dnadds,tuple)): + dnadds = [dnadds] + ?> + + <script py:for="dnadd in dnadds"> + <?python + dnadd_esc = ipahelper.javascript_string_escape(dnadd) + ?> + var dn = "${dnadd_esc}"; + var info = dn_to_info_hash[dn]; + var newdiv = addmember(dn, info); + if (newdiv != null) { + newdiv.style.display = 'block'; + } + </script> + +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/userselectsearch.kid b/ipa-server/ipa-gui/ipagui/templates/userselectsearch.kid new file mode 100644 index 00000000..b8a60ed5 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/userselectsearch.kid @@ -0,0 +1,53 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<div xmlns:py="http://purl.org/kid/ns#"> + +<?python +from ipagui.helpers import ipahelper +?> + <div py:if='(users != None) and (len(users) > 0)'> + <div id="search-results-count"> + ${len(users)} results returned: + <span py:if="counter < 0"> + (truncated) + </span> + </div> + + <div py:for="user in users"> + <?python + user_name = "%s %s" % (user.getValue('givenName', ''), + user.getValue('sn', '')) + user_descr = "(%s)" % user.uid + + user_dn_esc = ipahelper.javascript_string_escape(user.dn) + user_name_esc = ipahelper.javascript_string_escape(user_name) + user_descr_esc = ipahelper.javascript_string_escape(user_descr) + which_select_esc = ipahelper.javascript_string_escape(which_select) + ?> + + ${user_name} ${user_descr} + <a href="" + onclick="doSelect('${which_select_esc}', '${user_dn_esc}', '${user_name_esc}'); + return false;" + >select</a> + </div> + </div> + <div py:if='(users != None) and (len(users) == 0)'> + No results found for "${criteria}" + </div> +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/usershow.kid b/ipa-server/ipa-gui/ipagui/templates/usershow.kid new file mode 100644 index 00000000..0c2582b2 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/usershow.kid @@ -0,0 +1,399 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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="'userlayout.kid'"> +<head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> + <title>View User</title> +</head> +<body> +<?python +edit_url = tg.url('/user/edit', uid=user.get('uid')) +?> + <h1 class="user">View User</h1> + + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name == user.get('uid')" + class="submitbutton" type="button" + onclick="document.location.href='${edit_url}'" + value="Edit User" /> + +<?python +from ipagui.helpers import userhelper +pw_expires_days = userhelper.password_expires_in(user.get("krbPasswordExpiration")) +pw_expires_soon = userhelper.password_expires_soon(pw_expires_days) +pw_is_expired = userhelper.password_is_expired(pw_expires_days) +if pw_expires_days != 1: + days_suffix = "s" +else: + days_suffix = "" +?> + + <div id="alertbox" py:if='pw_expires_soon' class="warning_message"> + ${user.get("uid")}'s password will expire in ${pw_expires_days} day${days_suffix} + </div> + <div id="alertbox" py:if='pw_is_expired' class="warning_message"> + ${user.get("uid")}'s password has expired + </div> + + <h2 class="formsection">Identity Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.title.label" />: + </th> + <td>${user.get("title")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.givenname.label" />: + </th> + <td>${user.get("givenname")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.sn.label" />: + </th> + <td>${user.get("sn")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.cn.label" />: + </th> + <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> + <label class="fieldlabel" py:content="fields.displayname.label" />: + </th> + <td>${user.get("displayname")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.initials.label" />: + </th> + <td>${user.get("initials")}</td> + </tr> + </table> + + <h2 class="formsection">Account Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.nsAccountLock.label" />: + </th> + <td>${userhelper.account_status_display(user.get("nsAccountLock"))}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.uid.label" />: + </th> + <td>${user.get("uid")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.uidnumber.label" />: + </th> + <td>${user.get("uidnumber")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.gidnumber.label" />: + </th> + <td>${user.get("gidnumber")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.homedirectory.label" />: + </th> + <td>${user.get("homedirectory")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.loginshell.label" />: + </th> + <td>${user.get("loginshell")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.gecos.label" />: + </th> + <td>${user.get("gecos")}</td> + </tr> + </table> + + <h2 class="formsection">Contact Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.mail.label" />: + </th> + <td><a py:if="user.get('mail')" + href="mailto:${user.get('mail')}">${user.get("mail")}</a></td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.telephonenumber.label" />: + </th> + <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> + <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> + <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> + <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> + <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> + + <h2 class="formsection">Mailing Address</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.street.label" />: + </th> + <td>${user.get("street")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.roomnumber.label" />: + </th> + <td>${user.get("roomnumber")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.l.label" />: + </th> + <td>${user.get("l")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.st.label" />: + </th> + <td>${user.get("st")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.postalcode.label" />: + </th> + <td>${user.get("postalcode")}</td> + </tr> + </table> + + <h2 class="formsection">Employee Information</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ou.label" />: + </th> + <td>${user.get("ou")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.businesscategory.label" />: + </th> + <td>${user.get("businesscategory")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.description.label" />: + </th> + <td>${user.get("description")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.employeetype.label" />: + </th> + <td>${user.get("employeetype")}</td> + </tr> + <tr py:if='user_manager'> + <th> + <label class="fieldlabel" py:content="fields.manager.label" />: + </th> + <td> + <a href="${tg.url('/user/show', uid=user_manager.uid)}" + >${user_manager.givenname} ${user_manager.sn}</a> + </td> + </tr> + <tr py:if='user_secretary'> + <th> + <label class="fieldlabel" py:content="fields.secretary.label" />: + </th> + <td> + <a href="${tg.url('/user/show', uid=user_secretary.uid)}" + >${user_secretary.givenname} ${user_secretary.sn}</a> + </td> + </tr> + </table> + + <h2 class="formsection">Misc Information</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.carlicense.label" />: + </th> + <td>${user.get("carlicense")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.labeleduri.label" />: + </th> + <td> + <a py:if="user.get('labeleduri')" + href="${user.get('labeleduri')}">${user.get('labeleduri')}</a> + </td> + </tr> + </table> + + <div py:if='len(fields.custom_fields) > 0'> + <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> + <label class="fieldlabel" for="${custom_field.field_id}" + py:content="custom_field.label" />: + </th> + <td> + ${user.get(custom_field.name)} + </td> + </tr> + </table> + </div> + + <h2 class="formsection" py:if='len(user_reports) > 0'>Direct Reports</h2> + <ol py:if="len(user_reports) > 0"> + <li py:for="report in user_reports"> + <a href="${tg.url('/user/show', uid=report.uid)}" + >${report.givenname} ${report.sn}</a> + </li> + </ol> + + <h2 class="formsection">Groups</h2> + <div py:for="group in user_groups"> + <a href="${tg.url('/group/show', cn=group.cn)}">${group.cn}</a> + </div> + + <br/> +<hr /> + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name == user.get('uid')" + class="submitbutton" type="button" + onclick="document.location.href='${edit_url}'" + value="Edit User" /> +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/welcome.kid b/ipa-server/ipa-gui/ipagui/templates/welcome.kid new file mode 100644 index 00000000..ce3b444c --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/welcome.kid @@ -0,0 +1,53 @@ +<!-- + Copyright (C) 2007 Red Hat + see file 'COPYING' for use and warranty information + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2 only + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +--> +<!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>Welcome</title> +</head> +<body> + <div id="details"> + <div id="alertbox" py:if="value_of('tg_flash', None)"> + <p py:content="XML(tg_flash)"></p></div> + <h1>Welcome to Free IPA</h1> + + <noscript> + <span class="warning_message"> + This site makes heavy use of JavaScript.<br /> + Please enable JavaScript in your browser to make sure all pages function properly. + </span> + </noscript> + + <p> +IPA is used to manage Identity, Policy, and Auditing for your organization. + </p> + <p> + To get started, you can use the search box in the top right to find + users or groups you need to work on. Search automatically looks + across multiple fields. If you want to find Joe in Finance, try typing + "joe finance" into the search box. + </p> + <p> + Alternatively, select a task from the right sidebar. + </p> + </div> + +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/tests/Makefile.am b/ipa-server/ipa-gui/ipagui/tests/Makefile.am new file mode 100644 index 00000000..bf06ef2f --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/tests/Makefile.am @@ -0,0 +1,16 @@ +NULL = + +appdir = $(IPA_DATA_DIR)/ipagui/tests +app_PYTHON = \ + __init__.py \ + test_controllers.py \ + test_model.py \ + $(NULL) + +EXTRA_DIST = \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + *.pyc \ + Makefile.in diff --git a/ipa-server/ipa-gui/ipagui/tests/__init__.py b/ipa-server/ipa-gui/ipagui/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/tests/__init__.py diff --git a/ipa-server/ipa-gui/ipagui/tests/test_controllers.py b/ipa-server/ipa-gui/ipagui/tests/test_controllers.py new file mode 100644 index 00000000..fc4014d0 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/tests/test_controllers.py @@ -0,0 +1,49 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import unittest +import turbogears +from turbogears import testutil +from ipagui.controllers import Root +import cherrypy + +cherrypy.root = Root() + +class TestPages(unittest.TestCase): + + def setUp(self): + turbogears.startup.startTurboGears() + + def tearDown(self): + """Tests for apps using identity need to stop CP/TG after each test to + stop the VisitManager thread. + See http://trac.turbogears.org/turbogears/ticket/1217 for details. + """ + turbogears.startup.stopTurboGears() + + def test_method(self): + "the index method should return a string called now" + import types + result = testutil.call(cherrypy.root.index) + assert type(result["now"]) == types.StringType + + def test_indextitle(self): + "The indexpage should have the right title" + testutil.createRequest("/") + response = cherrypy.response.body[0].lower() + assert "<title>welcome to turbogears</title>" in response + diff --git a/ipa-server/ipa-gui/ipagui/tests/test_model.py b/ipa-server/ipa-gui/ipagui/tests/test_model.py new file mode 100644 index 00000000..5bfb2315 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/tests/test_model.py @@ -0,0 +1,39 @@ +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# If your project uses a database, you can set up database tests +# similar to what you see below. Be sure to set the db_uri to +# an appropriate uri for your testing database. sqlite is a good +# choice for testing, because you can use an in-memory database +# which is very fast. + +from turbogears import testutil, database +# from ipagui.model import YourDataClass, User + +# database.set_db_uri("sqlite:///:memory:") + +# class TestUser(testutil.DBTest): +# def get_model(self): +# return User +# def test_creation(self): +# "Object creation should set the name" +# obj = User(user_name = "creosote", +# email_address = "spam@python.not", +# display_name = "Mr Creosote", +# password = "Wafer-thin Mint") +# assert obj.display_name == "Mr Creosote" + |