summaryrefslogtreecommitdiffstats
path: root/ipa-server/ipa-gui/ipagui
diff options
context:
space:
mode:
Diffstat (limited to 'ipa-server/ipa-gui/ipagui')
-rw-r--r--ipa-server/ipa-gui/ipagui/Makefile.am30
-rw-r--r--ipa-server/ipa-gui/ipagui/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/config/Makefile.am20
-rw-r--r--ipa-server/ipa-gui/ipagui/config/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/config/app.cfg104
-rw-r--r--ipa-server/ipa-gui/ipagui/config/log.cfg32
-rw-r--r--ipa-server/ipa-gui/ipagui/controllers.py135
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/Makefile.am19
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/delegate.py110
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/group.py89
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/ipapolicy.py87
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/principal.py55
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/user.py207
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/Makefile.am17
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/__init__.py1
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/ipahelper.py88
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/userhelper.py46
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/validators.py92
-rw-r--r--ipa-server/ipa-gui/ipagui/json.py27
-rw-r--r--ipa-server/ipa-gui/ipagui/model.py26
-rw-r--r--ipa-server/ipa-gui/ipagui/proxyprovider.py176
-rw-r--r--ipa-server/ipa-gui/ipagui/proxyvisit.py42
-rw-r--r--ipa-server/ipa-gui/ipagui/release.py16
-rw-r--r--ipa-server/ipa-gui/ipagui/static/Makefile.am12
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/Makefile.am17
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/style_freeipa.css62
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/style_platform-objects.css19
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/style_platform.css517
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/Makefile.am29
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/branding/Makefile.am15
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/branding/logo.pngbin0 -> 8560 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/down.gifbin0 -> 57 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/favicon.icobin0 -> 3638 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/header_inner.pngbin0 -> 37537 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/info.pngbin0 -> 2889 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/logo.pngbin0 -> 8560 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/Makefile.am25
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-accesscontrol.pngbin0 -> 2093 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-channel.pngbin0 -> 702 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-channels.pngbin0 -> 702 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-content.pngbin0 -> 1735 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-media.pngbin0 -> 1569 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-overview.pngbin0 -> 1101 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-policy.pngbin0 -> 1471 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-system.pngbin0 -> 1591 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-user.pngbin0 -> 1005 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-usergroup.pngbin0 -> 1349 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/objects/object-virtualsystem.pngbin0 -> 2089 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/ok.pngbin0 -> 25753 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/Makefile.am21
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-content.pngbin0 -> 194 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active.pngbin0 -> 610 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active_fullsize.pngbin0 -> 634 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-navbar.pngbin0 -> 253 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-navbar_fullsize.pngbin0 -> 251 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background-sidebar.pngbin0 -> 170 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/template/background.pngbin0 -> 184 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/tg_under_the_hood.pngbin0 -> 4010 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/under_the_hood_blue.pngbin0 -> 2667 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/images/up.gifbin0 -> 56 bytes
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/Makefile.am21
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/dynamicedit.js217
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/dynamicselect.js70
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/effects.js1094
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/ipautil.js24
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/prototype.js3277
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/scriptaculous.js58
-rw-r--r--ipa-server/ipa-gui/ipagui/static/javascript/tablekit.js848
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am21
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/__init__.py1
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py415
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/group.py484
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/ipacontroller.py92
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py208
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/policy.py49
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/principal.py193
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/user.py854
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/Makefile.am55
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegateedit.kid33
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegateform.kid213
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegategroupsearch.kid48
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegatelayout.kid34
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegatelist.kid93
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/delegatenew.kid31
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/dynamiceditsearch.kid97
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupedit.kid36
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupeditform.kid289
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/grouplayout.kid40
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/grouplist.kid93
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupnew.kid32
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupnewform.kid149
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupshow.kid131
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid32
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid280
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid188
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/loginfailed.kid41
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/master.kid121
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/not_found.kid37
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/policyindex.kid48
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/policylayout.kid34
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principallayout.kid36
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principallist.kid82
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principalnew.kid30
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principalnewform.kid119
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/principalshow.kid70
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/unhandled_exception.kid48
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/useredit.kid57
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usereditform.kid949
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userlayout.kid41
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userlist.kid118
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usernew.kid30
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usernewform.kid842
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userselectsearch.kid53
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usershow.kid399
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/welcome.kid53
-rw-r--r--ipa-server/ipa-gui/ipagui/tests/Makefile.am16
-rw-r--r--ipa-server/ipa-gui/ipagui/tests/__init__.py0
-rw-r--r--ipa-server/ipa-gui/ipagui/tests/test_controllers.py49
-rw-r--r--ipa-server/ipa-gui/ipagui/tests/test_model.py39
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
new file mode 100644
index 00000000..ebabed79
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/branding/logo.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/down.gif b/ipa-server/ipa-gui/ipagui/static/images/down.gif
new file mode 100644
index 00000000..c527b4e6
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/down.gif
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/favicon.ico b/ipa-server/ipa-gui/ipagui/static/images/favicon.ico
new file mode 100644
index 00000000..bafbff92
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/favicon.ico
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/header_inner.png b/ipa-server/ipa-gui/ipagui/static/images/header_inner.png
new file mode 100644
index 00000000..2b2d87d5
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/header_inner.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/info.png b/ipa-server/ipa-gui/ipagui/static/images/info.png
new file mode 100644
index 00000000..329c523f
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/info.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/logo.png b/ipa-server/ipa-gui/ipagui/static/images/logo.png
new file mode 100644
index 00000000..ebabed79
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/logo.png
Binary files differ
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
new file mode 100644
index 00000000..bddec41b
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-accesscontrol.png
Binary files differ
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
new file mode 100644
index 00000000..7fd37c4e
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channel.png
Binary files differ
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
new file mode 100644
index 00000000..7fd37c4e
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-channels.png
Binary files differ
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
new file mode 100644
index 00000000..608a19e5
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-content.png
Binary files differ
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
new file mode 100644
index 00000000..0220fc05
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-media.png
Binary files differ
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
new file mode 100644
index 00000000..a320b9c8
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-overview.png
Binary files differ
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
new file mode 100644
index 00000000..eb55f453
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-policy.png
Binary files differ
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
new file mode 100644
index 00000000..82b09a5d
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-system.png
Binary files differ
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
new file mode 100644
index 00000000..bf294efd
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-user.png
Binary files differ
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
new file mode 100644
index 00000000..7338ad2e
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-usergroup.png
Binary files differ
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
new file mode 100644
index 00000000..8f2befca
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/objects/object-virtualsystem.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/ok.png b/ipa-server/ipa-gui/ipagui/static/images/ok.png
new file mode 100644
index 00000000..fee6751c
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/ok.png
Binary files differ
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
new file mode 100644
index 00000000..082f10ae
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-content.png
Binary files differ
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
new file mode 100644
index 00000000..1b088501
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active.png
Binary files differ
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
new file mode 100644
index 00000000..756a1e61
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar-active_fullsize.png
Binary files differ
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
new file mode 100644
index 00000000..2c6a2de4
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar.png
Binary files differ
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
new file mode 100644
index 00000000..72a71063
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-navbar_fullsize.png
Binary files differ
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
new file mode 100644
index 00000000..4eaadbbc
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background-sidebar.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/template/background.png b/ipa-server/ipa-gui/ipagui/static/images/template/background.png
new file mode 100644
index 00000000..96ead97f
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/template/background.png
Binary files differ
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
new file mode 100644
index 00000000..bc9c79cc
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/tg_under_the_hood.png
Binary files differ
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
new file mode 100644
index 00000000..90e84b72
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/under_the_hood_blue.png
Binary files differ
diff --git a/ipa-server/ipa-gui/ipagui/static/images/up.gif b/ipa-server/ipa-gui/ipagui/static/images/up.gif
new file mode 100644
index 00000000..c5ef548a
--- /dev/null
+++ b/ipa-server/ipa-gui/ipagui/static/images/up.gif
Binary files differ
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+ },
+ unescapeHTML: function() {
+ return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/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 &lt; 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 &lt; 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>
+ &nbsp; <!-- 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) &gt; 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>
+ &nbsp; <!-- 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) &gt; 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 &lt; 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) &gt; 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) &gt; 0'>Direct Reports</h2>
+ <ol py:if="len(user_reports) &gt; 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"
+