]> git.ipfire.org Git - people/jschlag/pbs.git/commitdiff
Second import.
authorMichael Tremer <michael.tremer@ipfire.org>
Sun, 28 Oct 2012 19:14:17 +0000 (20:14 +0100)
committerMichael Tremer <michael.tremer@ipfire.org>
Sun, 28 Oct 2012 19:14:17 +0000 (20:14 +0100)
185 files changed:
.gitignore
backend/__init__.py
backend/arches.py [new file with mode: 0644]
backend/base.py
backend/bugtracker.py [new file with mode: 0644]
backend/build.py [deleted file]
backend/builders.py
backend/builds.py [new file with mode: 0644]
backend/cache.py [new file with mode: 0644]
backend/constants.py
backend/database.py
backend/distribution.py
backend/keys.py [new file with mode: 0644]
backend/logs.py [new file with mode: 0644]
backend/main.py [new file with mode: 0644]
backend/managers.py
backend/messages.py
backend/mirrors.py [new file with mode: 0644]
backend/misc.py
backend/packages.py
backend/repository.py
backend/sessions.py [new file with mode: 0644]
backend/settings.py
backend/sources.py
backend/updates.py [new file with mode: 0644]
backend/uploads.py
backend/users.py
data/static/css/bootstrap-responsive.min.css [new file with mode: 0644]
data/static/css/bootstrap.min.css [new file with mode: 0644]
data/static/css/bootstrap.min.css.old [new file with mode: 0644]
data/static/css/build-table.css [new file with mode: 0644]
data/static/css/commits-table.css [new file with mode: 0644]
data/static/css/jobs-list.css [new file with mode: 0644]
data/static/css/jobs-table.css [new file with mode: 0644]
data/static/css/log.css [new file with mode: 0644]
data/static/css/packages-files-table.css [new file with mode: 0644]
data/static/css/packages-table.css [new file with mode: 0644]
data/static/css/style.css
data/static/css/style.css.backup [new file with mode: 0644]
data/static/css/watchers-sidebar-table.css [new file with mode: 0644]
data/static/images/glyphicons-halflings-white.png [new file with mode: 0644]
data/static/images/glyphicons-halflings.png [new file with mode: 0644]
data/static/images/icons/build-aborted.png [new symlink]
data/static/images/icons/builder-offline.png [moved from data/static/images/icons/slave-offline.png with 100% similarity]
data/static/images/icons/builder-online.png [moved from data/static/images/icons/slave-online.png with 100% similarity]
data/static/img [new symlink]
data/static/js/bootstrap.min.js [new file with mode: 0644]
data/static/js/jquery-1.7.1.min.js [new file with mode: 0644]
data/static/js/jquery-1.7.2.min.js [new file with mode: 0644]
data/static/js/jquery.js [new symlink]
data/static/js/pbs.js
data/templates/advanced.html [new file with mode: 0644]
data/templates/base-form1.html [new file with mode: 0644]
data/templates/base-form2.html [new file with mode: 0644]
data/templates/base.html
data/templates/build-bugs.html [new file with mode: 0644]
data/templates/build-delete.html [new file with mode: 0644]
data/templates/build-detail.html
data/templates/build-detail_old.html [new file with mode: 0644]
data/templates/build-filter.html
data/templates/build-index.html [new file with mode: 0644]
data/templates/build-list.html
data/templates/build-manage.html [new file with mode: 0644]
data/templates/build-queue.html [new file with mode: 0644]
data/templates/build-schedule-rebuild.html [deleted file]
data/templates/build-schedule-test.html
data/templates/build-state.html [new file with mode: 0644]
data/templates/builder-delete.html
data/templates/builder-detail.html
data/templates/builder-edit.html
data/templates/builder-list.html
data/templates/builder-new.html
data/templates/builder-pass.html
data/templates/builds-watchers-add.html [new file with mode: 0644]
data/templates/builds-watchers-list.html [new file with mode: 0644]
data/templates/distro-detail.html
data/templates/distro-edit.html
data/templates/distro-list.html
data/templates/distro-source-commit-detail.html [new file with mode: 0644]
data/templates/distro-source-commit-reset.html [new file with mode: 0644]
data/templates/distro-source-commits.html [new file with mode: 0644]
data/templates/distro-source-detail.html [new file with mode: 0644]
data/templates/distro-update-detail.html [new file with mode: 0644]
data/templates/distro-update-edit.html [new file with mode: 0644]
data/templates/docs-base.html
data/templates/docs-build.html
data/templates/docs-index.html
data/templates/docs-users.html
data/templates/docs-whatsthis.html [new file with mode: 0644]
data/templates/error-403.html [new file with mode: 0644]
data/templates/error-404.html
data/templates/error-500.html [deleted file]
data/templates/error.html
data/templates/index.html
data/templates/job-schedule-rebuild.html [new file with mode: 0644]
data/templates/job-schedule-test.html [new file with mode: 0644]
data/templates/jobs-abort.html [new file with mode: 0644]
data/templates/jobs-buildroot.html [new file with mode: 0644]
data/templates/jobs-detail.html [new file with mode: 0644]
data/templates/keys-delete.html [new file with mode: 0644]
data/templates/keys-import.html [new file with mode: 0644]
data/templates/keys-list.html [new file with mode: 0644]
data/templates/login-successful.html
data/templates/login.html
data/templates/logout.html
data/templates/mirrors-delete.html [new file with mode: 0644]
data/templates/mirrors-detail.html [new file with mode: 0644]
data/templates/mirrors-edit.html [new file with mode: 0644]
data/templates/mirrors-list.html [new file with mode: 0644]
data/templates/mirrors-new.html [new file with mode: 0644]
data/templates/modules/bugs-table.html [new file with mode: 0644]
data/templates/modules/build-headline.html [new file with mode: 0644]
data/templates/modules/build-offset.html
data/templates/modules/build-state-warnings.html [new file with mode: 0644]
data/templates/modules/build-table.html
data/templates/modules/build-table_old.html [new file with mode: 0644]
data/templates/modules/comments-table.html
data/templates/modules/commits-table.html [new file with mode: 0644]
data/templates/modules/files-table.html
data/templates/modules/footer.html [new file with mode: 0644]
data/templates/modules/jobs-list.html [new file with mode: 0644]
data/templates/modules/jobs-table.html [new file with mode: 0644]
data/templates/modules/log-entry-comment.html [new file with mode: 0644]
data/templates/modules/log-entry.html [new file with mode: 0644]
data/templates/modules/log-files-table.html [new file with mode: 0644]
data/templates/modules/log.html [new file with mode: 0644]
data/templates/modules/maintainer.html [new file with mode: 0644]
data/templates/modules/modal-base.html [new file with mode: 0644]
data/templates/modules/modal-build-comment.html [new file with mode: 0644]
data/templates/modules/modal-build-push.html [new file with mode: 0644]
data/templates/modules/modal-build-unpush.html [new file with mode: 0644]
data/templates/modules/package-files-table.html [deleted file]
data/templates/modules/package-header.html [new file with mode: 0644]
data/templates/modules/package-table.html [deleted file]
data/templates/modules/packages-files-table.html [new file with mode: 0644]
data/templates/modules/packages-table.html [new file with mode: 0644]
data/templates/modules/repository-table.html
data/templates/modules/source-table.html
data/templates/modules/updates-table.html [new file with mode: 0644]
data/templates/modules/watchers-sidebar-table.html [new file with mode: 0644]
data/templates/package-detail-list.html
data/templates/package-detail.html
data/templates/package-detail_old.html [new file with mode: 0644]
data/templates/package-list.html [deleted file]
data/templates/package-properties.html [new file with mode: 0644]
data/templates/packages-list.html [new file with mode: 0644]
data/templates/register-activation-fail.html
data/templates/register-activation-success.html
data/templates/register.html
data/templates/repository-detail.html
data/templates/repository-edit.html [new file with mode: 0644]
data/templates/search-form.html
data/templates/search-results.html
data/templates/source-detail.html [deleted file]
data/templates/statistics-main.html [new file with mode: 0644]
data/templates/updates-index.html [new file with mode: 0644]
data/templates/uploads-list.html [new file with mode: 0644]
data/templates/user-forgot-password.html [new file with mode: 0644]
data/templates/user-impersonation.html [new file with mode: 0644]
data/templates/user-list.html
data/templates/user-profile-builds.html [new file with mode: 0644]
data/templates/user-profile-edit.html
data/templates/user-profile-passwd-ok.html [new file with mode: 0644]
data/templates/user-profile-passwd.html [new file with mode: 0644]
data/templates/user-profile.html
hub/__init__.py [moved from master/__init__.py with 67% similarity]
hub/handlers.py [new file with mode: 0644]
master/handlers.py [deleted file]
pakfire-hub [new file with mode: 0644]
pakfire-master [deleted file]
web/__init__.py
web/handlers.py
web/handlers_auth.py
web/handlers_base.py
web/handlers_builders.py
web/handlers_builds.py [new file with mode: 0644]
web/handlers_distro.py [new file with mode: 0644]
web/handlers_jobs.py [new file with mode: 0644]
web/handlers_keys.py [new file with mode: 0644]
web/handlers_mirrors.py [new file with mode: 0644]
web/handlers_packages.py [new file with mode: 0644]
web/handlers_search.py
web/handlers_updates.py [new file with mode: 0644]
web/handlers_users.py
web/ui_modules.py

index 49cbba568097554deee96cf585309c0b21940540..edc24a401ed125a2d5b97b3e6bc5d3076b36a898 100644 (file)
@@ -1,2 +1,2 @@
-.py[co]
-.mo
+*.py[co]
+*.mo
index 45c331d39ef672084409d149d3dbe281c590cba5..e8b8fe846311698112acc7d410196a30c8e37787 100644 (file)
@@ -1,67 +1,3 @@
 #!/usr/bin/python
 
-import database
-import settings
-import uploads
-
-from build import Builds
-from builders import Builders
-from distribution import Distributions
-from messages import Messages
-from packages import Packages
-from repository import Repositories
-from sources import Sources
-from users import Users
-
-# Database access.
-MYSQL_SERVER = "mysql.ipfire.org"
-MYSQL_USER   = "pakfire"
-MYSQL_DB     = "pakfire"
-
-class Pakfire(object):
-       def __init__(self):
-               self.db = database.Connection(MYSQL_SERVER, MYSQL_DB, user=MYSQL_USER)
-
-               # Global pakfire settings (from database).
-               self.settings = settings.Settings(self)
-
-               self.builds = Builds(self)
-               self.builders = Builders(self)
-               self.distros = Distributions(self)
-               self.messages = Messages(self)
-               self.packages = Packages(self)
-               self.repos = Repositories(self)
-               self.sources = Sources(self)
-               self.uploads = uploads.Uploads(self)
-               self.users = Users(self)
-
-       def __del__(self):
-               if self.db:
-                       self.db.close()
-                       del self.db
-
-       def logger(self, message, text=None, build=None, pkg=None):
-               if not build and not pkg:
-                       raise Exception, "need to give at least one parameter for log"
-
-               log_id = self.db.execute("INSERT INTO `log`(message, text) VALUES(%s, %s)", message, text)
-
-               query = "UPDATE `log` SET %s = %%s WHERE id = %%s"
-
-               if build:
-                       self.db.execute(query % "build_id", build.id, log_id)
-
-                       if build.host:
-                               self.db.execute(query % "host_id", build.host.id, log_id)
-
-                       pkg = getattr(build, "pkg", None)
-
-               if pkg:
-                       self.db.execute(query % "pkg_id", pkg.id, log_id)
-                       self.db.execute(query % "source_id", pkg.source.id, log_id)
-
-               return log_id
-
-       @property
-       def log(self, limit=100):
-               return self.db.query("SELECT * FROM log ORDER BY time DESC LIMIT %s", limit)
+from main import Pakfire
diff --git a/backend/arches.py b/backend/arches.py
new file mode 100644 (file)
index 0000000..eee8afe
--- /dev/null
@@ -0,0 +1,101 @@
+#!/usr/bin/python
+
+import base
+
+class Arches(base.Object):
+       def get_all(self):
+               arches = self.db.query("SELECT id FROM arches WHERE `binary` = 'Y'")
+
+               return sorted([Arch(self.pakfire, a.id) for a in arches])
+
+       def get_name_by_id(self, id):
+               arch = self.db.get("SELECT name FROM arches WHERE id = %s", id)
+
+               return arch.name
+
+       def get_id_by_name(self, name):
+               arch = self.db.get("SELECT id FROM arches WHERE name = %s", name)
+
+               if arch:
+                       return arch.id
+
+       def get_by_name(self, name):
+               id = self.get_id_by_name(name)
+
+               if id:
+                       return self.get_by_id(id)
+
+       def get_by_id(self, id):
+               return Arch(self.pakfire, id)
+
+       def exists(self, id):
+               arch = self.db.get("SELECT id FROM arches WHERE id = %s", id)
+
+               if arch:
+                       return True
+
+               return False
+
+       def expand(self, arches):
+               args = []
+
+               if arches == "all":
+                       query = "SELECT id FROM arches WHERE name != 'noarch'"
+               else:
+                       query = []
+
+                       for arch in arches.split():
+                               args.append(arch)
+                               query.append("name = %s")
+
+                       query = "SELECT id FROM arches WHERE (%s)" % " OR ".join(query)
+
+               return sorted([self.get_by_id(a.id) for a in self.db.query(query, *args)])
+
+
+class Arch(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
+
+               # The ID of this architecture.
+               self.id = id
+
+               # Cache data.
+               self._data = None
+
+       def __cmp__(self, other):
+               return cmp(self.prio, other.prio)
+
+       @property
+       def data(self):
+               if self._data is None:
+                       cache_key = "arch_%s" % self.id
+
+                       # Search for the data in the cache.
+                       # If nothing was found, we get everything from the database.
+                       data = self.cache.get(cache_key)
+                       if not data:
+                               data = self.db.get("SELECT * FROM arches WHERE id = %s", self.id)
+
+                               # Store the data in the cache.
+                               self.cache.set(cache_key, data)
+
+                       self._data = data
+                       assert self._data
+
+               return self._data
+
+       @property
+       def name(self):
+               return self.data.name
+
+       @property
+       def prio(self):
+               return self.data.prio
+
+       @property
+       def build_type(self):
+               if self.name == "src":
+                       return "source"
+
+               return "binary"
index 7aeafcfe748ce2adb8ec62a671518ec5d5e80446..4d8d92541bc7fd7cc06b26efdd0597ca3d046d56 100644 (file)
@@ -17,3 +17,14 @@ class Object(object):
                # Shortcut to settings.
                if hasattr(self.pakfire, "settings"):
                        self.settings = self.pakfire.settings
+
+       @property
+       def cache(self):
+               """
+                       Shortcut to the cache.
+               """
+               return self.pakfire.cache
+
+       @property
+       def geoip(self):
+               return self.pakfire.geoip
diff --git a/backend/bugtracker.py b/backend/bugtracker.py
new file mode 100644 (file)
index 0000000..793e1e0
--- /dev/null
@@ -0,0 +1,193 @@
+#!/usr/bin/python
+
+import tornado.database
+import xmlrpclib
+
+import base
+
+class BugzillaBug(base.Object):
+       def __init__(self, bugzilla, bug_id):
+               base.Object.__init__(self, bugzilla.pakfire)
+               self.bugzilla = bugzilla
+
+               self.bug_id = bug_id
+               self._data  = None
+
+       def __cmp__(self, other):
+               return cmp(self.bug_id, other.bug_id)
+
+       def call(self, *args, **kwargs):
+               args = (("Bug",) + args)
+
+               return self.bugzilla.call(*args, **kwargs)
+
+       @property
+       def id(self):
+               return self.bug_id
+
+       @property
+       def cache_id(self):
+               return "bug_%s" % self.id
+
+       def clear_cache(self):
+               self.cache.delete(self.cache_id)
+
+       @property
+       def data(self):
+               if self._data is None:
+                       data = self.cache.get(self.cache_id)
+                       if not data:
+                               data = self.call("get", ids=[self.id,])["bugs"][0]
+                               self.cache.set(self.cache_id, data, 120)
+
+                       self._data = data
+               return self._data
+
+       @property
+       def url(self):
+               return self.bugzilla.bug_url(self.id)
+
+       @property
+       def summary(self):
+               return self.data.get("summary")
+
+       @property
+       def assignee(self):
+               return self.data.get("assigned_to")
+
+       @property
+       def status(self):
+               return self.data.get("status")
+
+       @property
+       def resolution(self):
+               return self.data.get("resolution")
+
+       @property
+       def is_closed(self):
+               return not self.data["is_open"]
+
+       def set_status(self, status, resolution=None, comment=None):
+               kwargs = { "status" : status }
+               if resolution:
+                       kwargs["resolution"] = resolution
+               if comment:
+                       kwargs["comment"] = { "body" : comment }
+
+               self.call("update", ids=[self.id,], **kwargs)
+               self.clear_cache()
+
+       
+
+class Bugzilla(base.Object):
+       def __init__(self, pakfire):
+               base.Object.__init__(self, pakfire)
+
+               # Open the connection to the server.
+               self.server = xmlrpclib.ServerProxy(self.url, use_datetime=True)
+
+               # Cache the credentials.
+               self.__credentials = {
+                       "Bugzilla_login"    : self.user,
+                       "Bugzilla_password" : self.password,
+               }
+
+       def call(self, *args, **kwargs):
+               # Add authentication information.
+               kwargs.update(self.__credentials)
+
+               method = self.server
+               for arg in args:
+                       method = getattr(method, arg)
+
+               return method(kwargs)
+
+       def bug_url(self, bugid):
+               url = self.settings.get("bugzilla_url", None)
+
+               try:
+                       return url % { "bugid" : bugid }
+               except:
+                       return "#"
+
+       def enter_url(self, component):
+               args = {
+                       "product"   : self.settings.get("bugzilla_product", ""),
+                       "component" : component,
+               }
+
+               url = self.settings.get("bugzilla_url_new")
+
+               return url % args
+
+       def buglist_url(self, component):
+               args = {
+                       "product"   : self.settings.get("bugzilla_product", ""),
+                       "component" : component,
+               }
+
+               url = self.settings.get("bugzilla_url_buglist")
+
+               return url % args
+
+       @property
+       def url(self):
+               return self.settings.get("bugzilla_url_xmlrpc", None)
+
+       @property
+       def user(self):
+               return self.settings.get("bugzilla_xmlrpc_user", "")
+
+       @property
+       def password(self):
+               return self.settings.get("bugzilla_xmlrpc_password")
+
+       def get_bug(self, bug_id):
+               try:
+                       bug = BugzillaBug(self, bug_id)
+                       s = bug.status
+
+               except xmlrpclib.Fault:
+                       return None
+
+               return bug
+
+       def find_users(self, pattern):
+               users = self.cache.get("users_%s" % pattern)
+               if users is None:
+                       users = self.call("User", "get", match=[pattern,])
+                       users = users["users"]
+
+                       self.cache.set("users_%s" % pattern, users)
+
+               return users
+
+       def find_user(self, pattern):
+               users = self.find_users(pattern)
+
+               if not users:
+                       return
+
+               elif len(users) > 1:
+                       raise Exception, "Got more than one result."
+
+               return users[0]
+
+       def get_bugs_from_component(self, component, closed=False):
+               kwargs = {
+                       "product"   : self.settings.get("bugzilla_product", ""),
+                       "component" : component,
+               }
+
+               query = self.call("Bug", "search", include_fields=["id"], **kwargs)
+
+               bugs = []
+               for bug in query["bugs"]:
+                       bug = self.get_bug(bug["id"])
+
+                       if not bug.is_closed == closed:
+                               continue
+
+                       bugs.append(bug)
+
+               return bugs
diff --git a/backend/build.py b/backend/build.py
deleted file mode 100644 (file)
index 80f6c1e..0000000
+++ /dev/null
@@ -1,553 +0,0 @@
-#!/usr/bin/python
-
-import datetime
-import logging
-import uuid
-import time
-import tornado.locale
-
-import base
-import packages
-
-from constants import *
-
-def Build(pakfire, id):
-       """
-               Proxy function that returns the right object depending on the type
-               of the build.
-       """
-       build = pakfire.db.get("SELECT type FROM builds WHERE id = %s", id)
-       if not build:
-               raise Exception, "Build not found"
-
-       for cls in (BinaryBuild, SourceBuild):
-               if not build.type == cls.type:
-                       continue
-
-               return cls(pakfire, id)
-
-       raise Exception, "Unknown type: %s" % build.type
-
-
-class Builds(base.Object):
-       """
-               Object that represents all builds.
-       """
-
-       def get_by_id(self, id):
-               return Build(self.pakfire, id)
-
-       def get_by_uuid(self, uuid):
-               build = self.db.get("SELECT id FROM builds WHERE uuid = %s LIMIT 1", uuid)
-
-               if build:
-                       return Build(self.pakfire, build.id)
-
-       def get_latest(self, state=None, builder=None, limit=10, type=None):
-               query = "SELECT id FROM builds"
-
-               where = []
-               if builder:
-                       where.append("host = '%s'" % builder)
-
-               if state:
-                       where.append("state = '%s'" % state) 
-
-               if type:
-                       where.append("type = '%s'" % type)
-
-               if where:
-                       query += " WHERE %s" % " AND ".join(where)
-
-               query += " ORDER BY updated DESC LIMIT %s"
-
-               builds = self.db.query(query, limit)
-
-               return [Build(self.pakfire, b.id) for b in builds]
-
-       def get_by_pkgid(self, pkg_id):
-               builds = self.db.query("""SELECT builds.id as id FROM builds
-                       LEFT JOIN builds_binary ON builds_binary.id = builds.build_id
-                       WHERE builds_binary.pkg_id = %s AND type = 'binary'""", pkg_id)
-
-               return [Build(self.pakfire, b.id) for b in builds]
-
-       def get_by_host(self, host_id):
-               builds = self.db.query("SELECT id FROM builds WHERE host = %s", host_id)
-
-               return [Build(self.pakfire, b.id) for b in builds]
-
-       def get_by_source(self, source_id):
-               builds = self.db.query("""SELECT builds.id as id FROM builds
-                       LEFT JOIN builds_source ON builds_source.id = builds.build_id
-                       WHERE builds_source.source_id = %s""", source_id)
-
-               return [Build(self.pakfire, b.id) for b in builds]
-
-       def get_active(self, type=None, host_id=None):
-               running_states = ("dispatching", "running", "uploading",)
-
-               query = "SELECT id FROM builds WHERE (%s)" % \
-                       " OR ".join(["state = '%s'" % s for s in running_states])
-
-               if type:
-                       query += " AND type = '%s'" % type
-
-               if host_id:
-                       query += " AND host = %s" % host_id
-
-               builds = self.db.query(query)
-
-               return [Build(self.pakfire, b.id) for b in builds]
-
-       def get_all_but_finished(self):
-               builds = self.db.query("SELECT id FROM builds WHERE"
-                       " NOT state = 'finished' AND NOT state = 'permanently_failed'")
-
-               return [Build(self.pakfire, b.id) for b in builds]
-
-       def get_next(self, type=None, arches=None, limit=None, offset=None):
-               query = "SELECT builds.id as id, build_id FROM builds"
-
-               wheres = ["state = 'pending'", "start_not_before <= NOW()",]
-
-               if type:
-                       wheres.append("type = '%s'" % type)
-
-               if type == "binary" and arches:
-                       query += " LEFT JOIN builds_binary ON builds.build_id = builds_binary.id"
-                       arches = ["builds_binary.arch='%s'" % a for a in arches]
-
-                       wheres.append("(%s)" % " OR ".join(arches))
-               elif arches:
-                       raise Exception, "Cannot use arches when type is not 'binary'"
-
-               if wheres:
-                       query += " WHERE %s" % " AND ".join(wheres)
-
-               # Choose the oldest one at first.
-               query += " ORDER BY priority DESC, time_added ASC"
-
-               if limit:
-                       if offset:
-                               query += " LIMIT %s,%s" % (limit, offset)
-                       else:
-                               query += " LIMIT %s" % limit
-
-               builds = [Build(self.pakfire, b.id) for b in self.db.query(query)]
-
-               if limit == 1 and builds:
-                       return builds[0]
-
-               return builds
-
-       def count(self, state=None):
-               query = "SELECT COUNT(*) as c FROM builds"
-
-               wheres = []
-               if state:
-                       wheres.append("state = '%s'" % state)
-
-               if wheres:
-                       query += " WHERE %s" % " AND ".join(wheres)
-
-               result = self.db.get(query)
-
-               return result.c
-
-       def average_build_time(self):
-               result = self.db.get("SELECT AVG(time_finished - time_started) as average"
-                       " FROM builds WHERE type = 'binary' AND time_started IS NOT NULL"
-                       " AND time_finished IS NOT NULL")
-
-               return result.average or 0
-
-
-class _Build(base.Object):
-       STATE2LOG = {
-               "pending"                               : LOG_BUILD_STATE_PENDING,
-               "dispatching"                   : LOG_BUILD_STATE_DISPATCHING,
-               "running"                               : LOG_BUILD_STATE_RUNNING,
-               "failed"                                : LOG_BUILD_STATE_FAILED,
-               "permanently_failed"    : LOG_BUILD_STATE_PERM_FAILED,
-               "dependency_error"              : LOG_BUILD_STATE_DEP_ERROR,
-               "waiting"                               : LOG_BUILD_STATE_WAITING,
-               "finished"                              : LOG_BUILD_STATE_FINISHED,
-               "unknown"                               : LOG_BUILD_STATE_UNKNOWN,
-               "uploading"                             : LOG_BUILD_STATE_UPLOADING,
-       }
-
-       def __init__(self, pakfire, id):
-               base.Object.__init__(self, pakfire)
-               self.id = id
-
-               self._data = self.db.get("SELECT * FROM builds WHERE id = %s LIMIT 1", self.id)
-
-       def set(self, key, value):
-               self.db.execute("UPDATE builds SET %s = %%s WHERE id = %%s" % key, value, self.id)
-               self._data[key] = value
-
-       @property
-       def build_id(self):
-               return self._data.build_id
-
-       def get_state(self):
-               return self._data.state
-
-       def set_state(self, state):
-               try:
-                       log = self.STATE2LOG[state]
-               except KeyErrror:
-                       raise Exception, "Trying to set an invalid build state: %s" % state
-
-               # Setting state.
-               self.set("state", state)
-
-               # Inform everybody what happened to the build job.
-               if state == "finished":
-                       self.db.execute("UPDATE builds SET time_finished = NOW()"
-                               " WHERE id = %s", self.id)
-
-                       self.send_finished_message()
-
-               elif state == "failed":
-                       self.send_failed_message()
-
-                       self.db.execute("UPDATE builds SET time_started = NULL,"
-                               " time_finished = NULL WHERE id = %s LIMIT 1", self.id)
-
-               elif state == "pending":
-                       self.retries += 1
-
-               elif state in ("dispatching", "running",):
-                       self.db.execute("UPDATE builds SET time_started = NOW(),"
-                               " time_finished = NULL WHERE id = %s LIMIT 1", self.id)
-
-               # Log the state change.
-               self.logger(log)
-
-       state = property(get_state, set_state)
-
-       @property
-       def finished(self):
-               return self.state == "finished"
-
-       def get_message(self):
-               return self._data.message
-
-       def set_message(self, message):
-               self.set("message", message)
-
-       message = property(get_message, set_message)
-
-       @property
-       def uuid(self):
-               return self._data.uuid
-
-       def get_host(self):
-               return self.pakfire.builders.get_by_id(self._data.host)
-
-       def set_host(self, host):
-               builder = self.pakfire.builders.get_by_name(host)
-
-               self.set("host", builder.id)
-
-       host = property(get_host, set_host)
-
-       def get_retries(self):
-               return self._data.retries
-
-       def set_retries(self, retries):
-               self.set("retries", retries)
-
-       retries = property(get_retries, set_retries)
-
-       @property
-       def time_added(self):
-               return self._data.time_added
-
-       @property
-       def time_started(self):
-               return self._data.time_started
-
-       @property
-       def time_finished(self):
-               return self._data.time_finished
-
-       @property
-       def duration(self):
-               if not self.time_finished or not self.time_started:
-                       return
-
-               return self.time_finished - self.time_started
-
-       def get_priority(self):
-               return self._data.priority
-
-       def set_priority(self, value):
-               self.set("priority", value)
-
-       priority = property(get_priority, set_priority)
-
-       @property
-       def log(self):
-               return self.db.query("SELECT * FROM log WHERE build_id = %s ORDER BY time DESC, id DESC", self.id)
-
-       def logger(self, message, text=""):
-               self.pakfire.logger(message, text, build=self)
-
-       @property
-       def source(self):
-               return self.pkg.source
-
-       @property
-       def files(self):
-               files = []
-
-               for p in self.db.query("SELECT id, type FROM package_files WHERE build_id = %s", self.uuid):
-                       for p_class in (packages.SourcePackageFile, packages.BinaryPackageFile, packages.LogFile):
-                               if p.type == p_class.type:
-                                       p = p_class(self.pakfire, p.id)
-                                       break
-                       else:
-                               continue
-
-                       files.append(p)
-
-               return sorted(files)
-
-       @property
-       def packagefiles(self):
-               return [f for f in self.files if isinstance(f, packages.PackageFile)]
-
-       @property
-       def logfiles(self):
-               return [f for f in self.files if isinstance(f, packages.LogFile)]
-
-       @property
-       def recipients(self):
-               return []
-
-       def send_finished_message(self):
-               info = {
-                       "build_name" : self.name,
-                       "build_host" : self.host.name,
-                       "build_uuid" : self.uuid,
-               }
-
-               self.pakfire.messages.send_to_all(self.recipients, MSG_BUILD_FINISHED_SUBJECT,
-                       MSG_BUILD_FINISHED, info)
-
-       def send_failed_message(self):
-               build_host = "--"
-               if self.host:
-                       build_host = self.host.name
-
-               info = {
-                       "build_name" : self.name,
-                       "build_host" : build_host,
-                       "build_uuid" : self.uuid,
-               }
-
-               self.pakfire.messages.send_to_all(self.recipients, MSG_BUILD_FAILED_SUBJECT,
-                       MSG_BUILD_FAILED, info)
-
-       def keepalive(self):
-               """
-                       This function is used to prevent build jobs from getting stuck on
-                       something.
-               """
-
-               # Get the seconds since we are running.
-               try:
-                       time_running = datetime.datetime.utcnow() - self.time_started
-                       time_running = time_running.total_seconds()
-               except:
-                       time_running = 0
-
-               if self.state == "dispatching":
-                       # If the dispatching is running more than 15 minutes, we set the
-                       # build to be failed.
-                       if time_running >= 900:
-                               self.state = "failed"
-
-               elif self.state in ("running", "uploading"):
-                       # If the build is running/uploading more than 24 hours, we kill it.
-                       if time_running >= 3600 * 24:
-                               self.state = "failed"
-
-               elif self.state == "dependency_error":
-                       # Resubmit job when it has waited for twelve hours.
-                       if time_running >= 3600 * 12:
-                               self.state = "pending"
-
-               elif self.state == "failed":
-                       # Automatically resubmit jobs that failed after one day.
-                       if time_running >= 3600 * 24:
-                               self.state = "pending"
-
-       def schedule_rebuild(self, offset):
-               # You cannot do this if the build job has already finished.
-               if self.finished:
-                       return
-
-               self.db.execute("UPDATE builds SET start_not_before = NOW() + %s"
-                       " WHERE id = %s LIMIT 1", offset, self.id)
-               self.state = "pending"
-
-
-class BinaryBuild(_Build):
-       type = "binary"
-
-       def __init__(self, *args, **kwargs):
-               _Build.__init__(self, *args, **kwargs)
-
-               _data = self.db.get("SELECT * FROM builds_binary WHERE id = %s", self.build_id)
-               del _data["id"]
-               self._data.update(_data)
-
-               self.pkg = self.pakfire.packages.get_by_id(self.pkg_id)
-
-       @classmethod
-       def new(cls, pakfire, pkg, arch):
-               now = datetime.datetime.utcnow()
-
-               build_id = pakfire.db.execute("INSERT INTO builds_binary(pkg_id, arch)"
-                       " VALUES(%s, %s)", pkg.id, arch)
-
-               id = pakfire.db.execute("INSERT INTO builds(uuid, build_id, time_added)"
-                       " VALUES(%s, %s, %s)", uuid.uuid4(), build_id, now)
-
-               build = cls(pakfire, id)
-               build.logger(LOG_BUILD_CREATED)
-
-               return build
-
-       @property
-       def name(self):
-               return "%s.%s" % (self.pkg.friendly_name, self.arch)
-
-       @property
-       def arch(self):
-               return self._data.arch
-
-       @property
-       def distro(self):
-               return self.pkg.distro
-
-       @property
-       def pkg_id(self):
-               return self._data.pkg_id
-
-       @property
-       def source_build(self):
-               return self.pkg.source_build
-
-       @property
-       def recipients(self):
-               l = set()
-
-               # Get all recipients from the source build (like committer and author).
-               for r in self.source_build.recipients:
-                       l.add(r)
-
-               # Add the package maintainer.
-               l.add(self.pkg.maintainer)
-
-               return l
-
-       def add_log(self, filename):
-               self.pkg.add_log(filename, self)
-
-       def schedule_test(self, offset):
-               pass # XXX TBD
-
-
-class SourceBuild(_Build):
-       type = "source"
-
-       def __init__(self, *args, **kwargs):
-               _Build.__init__(self, *args, **kwargs)
-
-               _data = self.db.get("SELECT * FROM builds_source WHERE id = %s", self.build_id)
-               del _data["id"]
-               self._data.update(_data)
-
-       @classmethod
-       def new(cls, pakfire, source_id, revision, author, committer, subject, body, date):
-               now = datetime.datetime.utcnow()
-
-               # Check if the revision does already exist. If so, just return.
-               if pakfire.db.query("SELECT id FROM builds_source WHERE revision = %s", revision):
-                       logging.warning("There is already a source build job for rev %s" % revision)
-                       return
-
-               build_id = pakfire.db.execute("INSERT INTO builds_source(source_id,"
-                       " revision, author, committer, subject, body, date) VALUES(%s, %s, %s, %s,"
-                       " %s, %s, %s)", source_id, revision, author, committer, subject, body, date)
-
-               id = pakfire.db.execute("INSERT INTO builds(uuid, type, build_id, time_added)"
-                       " VALUES(%s, %s, %s, %s)", uuid.uuid4(), "source", build_id, now)
-
-               build = cls(pakfire, id)
-               build.logger(LOG_BUILD_CREATED)
-
-               # Source builds are immediately pending.
-               build.state = "pending"
-
-               return build
-
-       @property
-       def arch(self):
-               return "src"
-
-       @property
-       def name(self):
-               s = "%s:" % self.source.name
-
-               if self.commit_subject:
-                       s += " %s" % self.commit_subject[:60]
-                       if len(self.commit_subject) > 60:
-                               s += "..."
-               else:
-                       s += self.revision[:7]
-
-               return s
-
-       @property
-       def source(self):
-               return self.pakfire.sources.get_by_id(self._data.source_id)
-
-       @property
-       def revision(self):
-               return self._data.revision
-
-       @property
-       def commit_author(self):
-               return self._data.author
-
-       @property
-       def commit_committer(self):
-               return self._data.committer
-
-       @property
-       def commit_subject(self):
-               return self._data.subject
-
-       @property
-       def commit_body(self):
-               return self._data.body
-
-       @property
-       def commit_date(self):
-               return self._data.date
-
-       @property
-       def recipients(self):
-               l = [self.commit_author, self.commit_committer,]
-
-               return set(l)
-
-       def send_finished_message(self):
-               # We do not send finish messages on source build jobs.
-               pass
index 4f250751e3e705d825558100a0175087e987554f..41595f72c415c25a8ee55df1cffa2412aa843d0b 100644 (file)
@@ -8,10 +8,37 @@ import string
 import time
 
 import base
+import logs
+
+from users import generate_password_hash, check_password_hash, generate_random_string
 
 class Builders(base.Object):
+       def auth(self, name, passphrase):
+               # If either name or passphrase is None, we don't check at all.
+               if None in (name, passphrase):
+                       return
+
+               # Search for the hostname in the database.
+               # The builder must not be deleted.
+               builder = self.db.get("SELECT id FROM builders WHERE name = %s AND \
+                       NOT status = 'deleted'", name)
+
+               if not builder:
+                       return
+
+               # Get the whole Builder object from the database.
+               builder = self.get_by_id(builder.id)
+
+               # If the builder was not found or the passphrase does not match,
+               # you have bad luck.
+               if not builder or not builder.validate_passphrase(passphrase):
+                       return
+
+               # Otherwise we return the Builder object.
+               return builder
+
        def get_all(self):
-               builders = self.db.query("SELECT id FROM builders WHERE deleted = 'N' ORDER BY name")
+               builders = self.db.query("SELECT id FROM builders WHERE NOT status = 'deleted' ORDER BY name")
 
                return [Builder(self.pakfire, b.id) for b in builders]
 
@@ -41,6 +68,53 @@ class Builders(base.Object):
 
                return sorted(arches)
 
+       def get_load(self):
+               slots = 1
+               running_jobs = 0
+
+               for builder in self.get_all():
+                       if not builder.state == "online":
+                               continue
+
+                       slots += builder.max_jobs
+                       running_jobs += len(builder.get_active_jobs(uploads=False))
+
+               return int(running_jobs * 100 / slots)
+
+       def get_history(self, limit=None, offset=None, builder=None, user=None):
+               query = "SELECT * FROM builders_history"
+               args  = []
+
+               conditions = []
+
+               if builder:
+                       conditions.append("builder_id = %s")
+                       args.append(builder.id)
+
+               if user:
+                       conditions.append("user_id = %s")
+                       args.append(user.id)
+
+               if conditions:
+                       query += " WHERE %s" % " AND ".join(conditions)
+
+               query += " ORDER BY time DESC"
+
+               if limit:
+                       if offset:
+                               query += " LIMIT %s,%s"
+                               args  += [offset, limit,]
+                       else:
+                               query += " LIMIT %s"
+                               args  += [limit,]
+
+               entries = []
+               for entry in self.db.query(query, *args):
+                       entry = logs.BuilderLogEntry(self.pakfire, entry)
+                       entries.append(entry)
+
+               return entries
+
 
 class Builder(base.Object):
        def __init__(self, pakfire, id):
@@ -48,98 +122,311 @@ class Builder(base.Object):
 
                self.id = id
 
-               self.data = self.db.get("SELECT * FROM builders WHERE id = %s", self.id)
+               # Cache.
+               self._data = None
+               self._active_jobs = None
+               self._arches = None
+               self._disabled_arches = None
 
        def __cmp__(self, other):
                return cmp(self.id, other.id)
 
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = \
+                               self.db.get("SELECT *, NOW() - time_keepalive AS updated \
+                                       FROM builders WHERE id = %s", self.id)
+
+               return self._data
+
        @classmethod
-       def new(cls, pakfire, name):
-               id = pakfire.db.execute("INSERT INTO builders(name) VALUES(%s)", name)
+       def create(cls, pakfire, name, user=None, log=True):
+               """
+                       Creates a new builder.
+               """
+               builder_id = pakfire.db.execute("INSERT INTO builders(name, time_created) \
+                       VALUES(%s, NOW())", name)
 
-               builder = cls(pakfire, id)
-               builder.regenerate_passphrase()
+               # Create Builder object.
+               builder = cls(pakfire, builder_id)
 
-               return builder
+               # Generate a new passphrase.
+               passphrase = builder.regenerate_passphrase()
+
+               # Log what we have done.
+               if log:
+                       builder.log("created", user=user)
+
+               # The Builder object and the passphrase are returned.
+               return builder, passphrase
+
+       def log(self, action, user=None):
+               user_id = None
+               if user:
+                       user_id = user.id
+
+               self.db.execute("INSERT INTO builders_history(builder_id, action, user_id, time) \
+                       VALUES(%s, %s, %s, NOW())", self.id, action, user_id)
 
        def set(self, key, value):
                self.db.execute("UPDATE builders SET %s = %%s WHERE id = %%s LIMIT 1" % key,
                        value, self.id)
                self.data[key] = value
 
-       def delete(self):
-               self.set("disabled", "Y")
-               self.set("deleted",  "Y")
-
        def regenerate_passphrase(self):
-               source = string.ascii_letters + string.digits
-               passphrase = "".join(random.sample(source * 30, 20))
+               """
+                       Generates a new random passphrase and stores it as a salted hash
+                       to the database.
+
+                       The new passphrase is returned to be sent to the user (once).
+               """
+               # Generate a random string with 20 chars.
+               passphrase = generate_random_string(length=20)
+
+               # Create salted hash.
+               passphrase_hash = generate_password_hash(passphrase)
+
+               # Store the hash in the database.
+               self.db.execute("UPDATE builders SET passphrase = %s WHERE id = %s",
+                       passphrase_hash, self.id)
 
-               self.set("passphrase", passphrase)
+               # Return the clear-text passphrase.
+               return passphrase
 
        def validate_passphrase(self, passphrase):
-               return self.passphrase == passphrase
+               """
+                       Compare the given passphrase with the one stored in the database.
+               """
+               return check_password_hash(passphrase, self.data.passphrase)
+
+       @property
+       def description(self):
+               return self.data.description or ""
+
+       @property
+       def status(self):
+               return self.data.status
+
+       def update_description(self, description):
+               self.db.execute("UPDATE builders SET description = %s, time_updated = NOW() \
+                       WHERE id = %s", description, self.id)
+
+               if self._data:
+                       self._data["description"] = description
+
+       @property
+       def keepalive(self):
+               """
+                       Returns time of last keepalive message from this host.
+               """
+               return self.data.time_keepalive
+
+       def update_keepalive(self, loadavg, free_space):
+               """
+                       Update the keepalive timestamp of this machine.
+               """
+               if free_space is None:
+                       free_space = 0
+
+               self.db.execute("UPDATE builders SET time_keepalive = NOW(), loadavg = %s, \
+                       free_space = %s WHERE id = %s", loadavg, free_space, self.id)
+
+               logging.debug("Builder %s updated it keepalive status: %s" \
+                       % (self.name, loadavg))
+
+       def needs_update(self):
+               query = self.db.get("SELECT time_updated, NOW() - time_updated \
+                       AS seconds FROM builders WHERE id = %s", self.id)
+
+               # If there has been no update at all, we will need a new one.
+               if query.time_updated is None:
+                       return True
+
+               # Require an update after the data is older than 24 hours.
+               return query.seconds >= 24*3600
+
+       def update_info(self, arches, cpu_model, cpu_count, memory, pakfire_version=None, host_key_id=None):
+               # Update architecture information.
+               self.update_arches(arches)
+
+               # Update all the rest.
+               self.db.execute("UPDATE builders SET time_updated = NOW(), \
+                       pakfire_version = %s, cpu_model = %s, cpu_count = %s, memory = %s, \
+                       host_key_id = %s \
+                       WHERE id = %s", pakfire_version or "", cpu_model, cpu_count, memory,
+                       host_key_id, self.id)
+
+       def update_arches(self, arches):
+               # Get all arches this builder does currently support.
+               supported_arches = [a.name for a in self.get_arches()]
+
+               # Noarch is always supported.
+               if not "noarch" in arches:
+                       arches.append("noarch")
+
+               arches_add = []
+               for arch in arches:
+                       if arch in supported_arches:
+                               supported_arches.remove(arch)
+                               continue
+
+                       arches_add.append(arch)
+               arches_rem = supported_arches
+
+               for arch_name in arches_add:
+                       arch = self.pakfire.arches.get_by_name(arch_name)
+                       if not arch:
+                               logging.info("Client sent unknown architecture: %s" % arch_name)
+                               continue
+
+                       self.db.execute("INSERT INTO builders_arches(builder_id, arch_id) \
+                               VALUES(%s, %s)", self.id, arch.id)
+
+               for arch_name in arches_rem:
+                       arch = self.pakfire.arches.get_by_name(arch_name)
+                       assert arch
+
+                       self.db.execute("DELETE FROM builders_arches WHERE builder_id = %s \
+                               AND arch_id = %s", self.id, arch.id)
 
-       def update_info(self, loadavg, cpu_model, memory, arches):
-               self.set("loadavg", loadavg)
-               self.set("cpu_model", cpu_model)
-               self.set("memory", memory)
-               self.set("arches", arches)
+       def update_overload(self, overload):
+               if overload:
+                       overload = "Y"
+               else:
+                       overload = "N"
+
+               self.db.execute("UPDATE builders SET overload = %s WHERE id = %s",
+                       overload, self.id)
+               self._data["overload"] = overload
+
+               logging.debug("Builder %s updated it overload status to %s" % \
+                       (self.name, self.overload))
 
        def get_enabled(self):
-               return not self.disabled
+               return self.status == "enabled"
 
        def set_enabled(self, value):
+               # XXX deprecated
+
                if value:
-                       value = "N"
+                       value = "enabled"
                else:
-                       value = "Y"
+                       value = "disabled"
 
-               self.set("disabled", value)
+               self.set_status(value)
 
        enabled = property(get_enabled, set_enabled)
 
        @property
        def disabled(self):
-               return self.data.disabled == "Y"
+               return not self.enabled
+
+       def set_status(self, status, user=None, log=True):
+               assert status in ("created", "enabled", "disabled", "deleted")
+
+               if self.status == status:
+                       return
+
+               self.db.execute("UPDATE builders SET status = %s WHERE id = %s",
+                       status, self.id)
+
+               if self._data:
+                       self._data["status"] = status
+
+               if log:
+                       self.log(status, user=user)
+
+       def get_arches(self, enabled=None):
+               """
+                       A list of architectures that are supported by this builder.
+               """
+               if enabled is True:
+                       enabled = "Y"
+               elif enabled is False:
+                       enabled = "N"
+               else:
+                       enabled = None
+
+               query = "SELECT arch_id AS id FROM builders_arches WHERE builder_id = %s"
+               args  = [self.id,]
+
+               if enabled:
+                       query += " AND enabled = %s"
+                       args.append(enabled)
+
+               # Get all other arches from the database.
+               arches = []
+               for arch in self.db.query(query, *args):
+                       arch = self.pakfire.arches.get_by_id(arch.id)
+                       arches.append(arch)
+
+               # Save a sorted list of supported architectures.
+               arches.sort()
+
+               return arches
 
        @property
        def arches(self):
-               arches = ["noarch",]
+               if self._arches is None:
+                       self._arches = self.get_arches(enabled=True)
 
-               if self.build_src:
-                       arches.append("src")
+               return self._arches
 
-               if self.data.arches:
-                       arches += self.data.arches.split()
+       @property
+       def disabled_arches(self):
+               if self._disabled_arches is None:
+                       self._disabled_arches = self.get_arches(enabled=False)
 
-               return sorted(arches)
+               return self._disabled_arches
 
-       def get_build_src(self):
-               return self.data.build_src == "Y"
+       def set_arch_status(self, arch, enabled):
+               if enabled:
+                       enabled = "Y"
+               else:
+                       enabled = "N"
+
+               self.db.execute("UPDATE builders_arches SET enabled = %s \
+                       WHERE builder_id = %s AND arch_id = %s", enabled, self.id, arch.id)
+
+               # Reset the arch cache.
+               self._arches = None
 
-       def set_build_src(self, value):
+       def get_build_release(self):
+               return self.data.build_release == "Y"
+
+       def set_build_release(self, value):
                if value:
                        value = "Y"
                else:
                        value = "N"
 
-               self.set("build_src", value)
+               self.db.execute("UPDATE builders SET build_release = %s WHERE id = %s",
+                       value, self.id)
+
+               # Update the cache.
+               if self._data:
+                       self._data["build_release"] = value
 
-       build_src = property(get_build_src, set_build_src)
+       build_release = property(get_build_release, set_build_release)
 
-       def get_build_bin(self):
-               return self.data.build_bin == "Y"
+       def get_build_scratch(self):
+               return self.data.build_scratch == "Y"
 
-       def set_build_bin(self, value):
+       def set_build_scratch(self, value):
                if value:
                        value = "Y"
                else:
                        value = "N"
 
-               self.set("build_bin", value)
+               self.db.execute("UPDATE builders SET build_scratch = %s WHERE id = %s",
+                       value, self.id)
+
+               # Update the cache.
+               if self._data:
+                       self._data["build_scratch"] = value
 
-       build_bin = property(get_build_bin, set_build_bin)
+       build_scratch = property(get_build_scratch, set_build_scratch)
 
        def get_build_test(self):
                return self.data.build_test == "Y"
@@ -150,10 +437,30 @@ class Builder(base.Object):
                else:
                        value = "N"
 
-               self.set("build_test", value)
+               self.db.execute("UPDATE builders SET build_test = %s WHERE id = %s",
+                       value, self.id)
+
+               # Update the cache.
+               if self._data:
+                       self._data["build_test"] = value
 
        build_test = property(get_build_test, set_build_test)
 
+       @property
+       def build_types(self):
+               ret = []
+
+               if self.build_release:
+                       ret.append("release")
+
+               if self.build_scratch:
+                       ret.append("scratch")
+
+               if self.build_test:
+                       ret.append("test")
+
+               return ret
+
        def get_max_jobs(self):
                return self.data.max_jobs
 
@@ -176,35 +483,67 @@ class Builder(base.Object):
 
        @property
        def loadavg(self):
-               if not self.status == "ONLINE":
-                       return 0
+               if self.state == "online":
+                       return self.data.loadavg
 
-               return self.data.loadavg
+       @property
+       def load1(self):
+               try:
+                       load1, load5, load15 = self.loadavg.split(", ")
+               except:
+                       return None
+
+               return load1
+
+       @property
+       def pakfire_version(self):
+               return self.data.pakfire_version or ""
 
        @property
        def cpu_model(self):
-               return self.data.cpu_model
+               return self.data.cpu_model or ""
+
+       @property
+       def cpu_count(self):
+               return self.data.cpu_count
 
        @property
        def memory(self):
-               return self.data.memory * 1024
+               return self.data.memory
 
        @property
-       def status(self):
+       def free_space(self):
+               return self.data.free_space or 0
+
+       @property
+       def overload(self):
+               return self.data.overload == "Y"
+
+       @property
+       def host_key_id(self):
+               return self.data.host_key_id
+
+       @property
+       def state(self):
                if self.disabled:
-                       return "DISABLED"
+                       return "disabled"
 
-               threshhold = datetime.datetime.utcnow() - datetime.timedelta(minutes=6)
+               if self.data.time_keepalive is None:
+                       return "offline"
 
-               if self.data.updated < threshhold:
-                       return "OFFLINE"
+               if self.data.updated >= 5*60:
+                       return "offline"
 
-               return "ONLINE"
+               return "online"
 
-       @property
-       def builds(self):
-               return self.pakfire.builds.get_by_host(self.id)
+       def get_active_jobs(self, uploads=True):
+               if self._active_jobs is None:
+                       self._active_jobs = \
+                               self.pakfire.jobs.get_active(host_id=self.id, uploads=uploads)
 
-       @property
-       def active_builds(self):
-               return self.pakfire.builds.get_active(host_id=self.id)
+               return self._active_jobs
+
+       def get_history(self, *args, **kwargs):
+               kwargs["builder"] = self
+
+               return self.pakfire.builders.get_history(*args, **kwargs)
diff --git a/backend/builds.py b/backend/builds.py
new file mode 100644 (file)
index 0000000..e52de54
--- /dev/null
@@ -0,0 +1,2041 @@
+#!/usr/bin/python
+
+import datetime
+import hashlib
+import logging
+import os
+import re
+import shutil
+import uuid
+
+import pakfire
+import pakfire.config
+import pakfire.packages
+
+import base
+import builders
+import logs
+import packages
+import repository
+import updates
+import users
+
+from constants import *
+
+def import_from_package(_pakfire, filename, distro=None, commit=None, type="release",
+               arches=None, check_for_duplicates=True, owner=None):
+
+       if distro is None:
+               distro = commit.source.distro
+
+       assert distro
+
+       # Open the package file to read some basic information.
+       pkg = pakfire.packages.open(None, None, filename)
+
+       if check_for_duplicates:
+               if distro.has_package(pkg.name, pkg.epoch, pkg.version, pkg.release):
+                       logging.warning("Duplicate package detected: %s. Skipping." % pkg)
+                       return
+
+       # Open the package and add it to the database.
+       pkg = packages.Package.open(_pakfire, filename)
+       logging.debug("Created new package: %s" % pkg)
+
+       # Associate the package to the processed commit.
+       if commit:
+               pkg.commit = commit
+
+       # Create a new build object from the package which
+       # is always a release build.
+       build = Build.create(_pakfire, pkg, type=type, owner=owner, distro=distro)
+       logging.debug("Created new build job: %s" % build)
+
+       # Create all automatic jobs.
+       build.create_autojobs(arches=arches)
+
+       return pkg, build
+
+
+class Builds(base.Object):
+       def get_by_id(self, id):
+               return Build(self.pakfire, id)
+
+       def get_by_uuid(self, uuid):
+               build = self.db.get("SELECT id FROM builds WHERE uuid = %s LIMIT 1", uuid)
+
+               if build:
+                       return self.get_by_id(build.id)
+
+       def get_all(self, limit=50):
+               query = "SELECT id FROM builds ORDER BY time_created DESC"
+
+               if limit:
+                       query += " LIMIT %d" % limit
+
+               return [self.get_by_id(b.id) for b in self.db.query(query)]
+
+       def get_by_user_iter(self, user, type=None, public=None, order_by="name"):
+               args = []
+               conditions = []
+
+               if not type or type == "scratch":
+                       # On scratch builds the user id equals the owner id.
+                       conditions.append("(builds.type = 'scratch' AND owner_id = %s)")
+                       args.append(user.id)
+
+               elif not type or type == "release":
+                       pass # TODO
+
+               if public is True:
+                       conditions.append("public = 'Y'")
+               elif public is False:
+                       conditions.append("public = 'N'")
+
+               query = "SELECT builds.id AS id FROM builds \
+                       JOIN packages ON builds.pkg_id = packages.id"
+
+               if conditions:
+                       query += " WHERE %s" % " AND ".join(conditions)
+
+               if order_by == "name":
+                       query += " ORDER BY packages.name ASC"
+               elif order_by == "date":
+                       query += " ORDER BY builds.time_created DESC"
+
+               for build in self.db.query(query, *args):
+                       yield Build(self.pakfire, build.id)
+
+       def get_by_name(self, name, type=None, public=None, user=None):
+               args = [name,]
+               conditions = [
+                       "packages.name = %s",
+               ]
+
+               if type:
+                       conditions.append("builds.type = %s")
+                       args.append(type)
+
+               or_conditions = []
+               if public is True:
+                       or_conditions.append("public = 'Y'")
+               elif public is False:
+                       or_conditions.append("public = 'N'")
+
+               if user and not user.is_admin():
+                       or_conditions.append("builds.owner_id = %s")
+                       args.append(user.id)
+
+               query = "SELECT builds.id AS id FROM builds \
+                       JOIN packages ON builds.pkg_id = packages.id"
+
+               if or_conditions:
+                       conditions.append(" OR ".join(or_conditions))
+
+               if conditions:
+                       query += " WHERE %s" % " AND ".join(conditions)
+
+               query += " ORDER BY packages.name,packages.epoch,packages.version,packages.release,id ASC"
+
+               return sorted([Build(self.pakfire, b.id) for b in self.db.query(query, *args)])
+
+       def get_latest_by_name(self, name, type=None, public=None):
+               if type is None:
+                       types = ("release", "scratch")
+               else:
+                       types = (type,)
+
+               query = "SELECT builds.id AS id FROM builds \
+                       JOIN packages ON builds.pkg_id = packages.id \
+                       WHERE builds.type = %s AND packages.name = %s"
+               args = [name,]
+
+               if public is True:
+                       query += " AND builds.public = 'Y'"
+               elif public is False:
+                       query += " AND builds.public = 'N'"
+
+               for type in types:
+                       res = self.db.query(query, type, *args)
+                       if not res:
+                               continue
+
+                       builds = [Build(self.pakfire, b.id) for b in res]
+                       builds.sort(reverse=True)
+
+                       return builds[0]
+
+       def count(self):
+               count = self.cache.get("builds_count")
+               if count is None:
+                       builds = self.db.get("SELECT COUNT(*) AS count FROM builds")
+
+                       count = builds.count
+                       self.cache.set("builds_count", count, 3600 / 4)
+
+               return count
+
+       def needs_test(self, threshold, arch, limit=None, randomize=False):
+               query = "SELECT id FROM builds \
+                       WHERE NOT EXISTS \
+                               (SELECT * FROM jobs WHERE \
+                                       jobs.build_id = builds.id AND \
+                                       (jobs.state != 'finished' OR \
+                                       jobs.time_finished >= %s) \
+                               ) \
+                       AND EXISTS \
+                               (SELECT * FROM jobs WHERE \
+                                       jobs.build_id = builds.id AND \
+                                       jobs.arch_id = %s AND \
+                                       jobs.type = 'build' AND \
+                                       jobs.state = 'finished' AND \
+                                       jobs.time_finished < %s \
+                               ) \
+                       AND builds.type = 'release' AND NOT builds.state = 'broken'"
+               args  = [threshold, arch.id, threshold]
+
+               if randomize:
+                       query += " ORDER BY RAND()"
+
+               if limit:
+                       query += " LIMIT %s"
+                       args.append(limit)
+
+               return [Build(self.pakfire, b.id) for b in self.db.query(query, *args)]
+
+       def get_obsolete(self, repo=None):
+               """
+                       Get all obsoleted builds.
+
+                       If repo is True: which are in any repository.
+                       If repo is some Repository object: which are in this repository.
+               """
+               args = []
+
+               if repo is None:
+                       query = "SELECT id FROM builds WHERE state = 'obsolete'"
+
+               else:
+                       query = "SELECT build_id AS id FROM repositories_builds \
+                               JOIN builds ON builds.id = repositories_builds.build_id \
+                               WHERE builds.state = 'obsolete'"
+
+                       if repo and not repo is True:
+                               query += " AND repositories_builds.repo_id = %s"
+                               args.append(repo.id)
+
+               res = self.db.query(query, *args)
+
+               builds = []
+               for build in res:
+                       build = Build(self.pakfire, build.id)
+                       builds.append(build)
+
+               return builds
+
+
+class Build(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
+
+               # ID of this build
+               self.id = id
+
+               # Cache data.
+               self._data = None
+               self._jobs = None
+               self._jobs_test = None
+               self._depends_on = None
+               self._pkg = None
+               self._credits = None
+               self._owner = None
+               self._update = None
+               self._repo = None
+               self._distro = None
+
+       def __repr__(self):
+               return "<%s id=%s %s>" % (self.__class__.__name__, self.id, self.pkg)
+
+       def __cmp__(self, other):
+               assert self.pkg
+               assert other.pkg
+
+               return cmp(self.pkg, other.pkg)
+
+       @property
+       def cache_key(self):
+               return "build_%s" % self.id
+
+       def clear_cache(self):
+               """
+                       Clear the stored data from the cache.
+               """
+               self.cache.delete(self.cache_key)
+
+       @classmethod
+       def create(cls, pakfire, pkg, type="release", owner=None, distro=None, public=True):
+               assert type in ("release", "scratch", "test")
+               assert distro, "You need to specify the distribution of this build."
+
+               if public:
+                       public = "Y"
+               else:
+                       public = "N"
+
+               # Check if scratch build has an owner.
+               if type == "scratch" and not owner:
+                       raise Exception, "Scratch builds require an owner"
+
+               # Set the default priority of this build.
+               if type == "release":
+                       priority = 0
+
+               elif type == "scratch":
+                       priority = 1
+
+               elif type == "test":
+                       priority = -1
+
+               id = pakfire.db.execute("""
+                       INSERT INTO builds(uuid, pkg_id, type, distro_id, time_created, public, priority)
+                       VALUES(%s, %s, %s, %s, NOW(), %s, %s)""", "%s" % uuid.uuid4(), pkg.id,
+                       type, distro.id, public, priority)
+
+               # Set the owner of this buildgroup.
+               if owner:
+                       pakfire.db.execute("UPDATE builds SET owner_id = %s WHERE id = %s",
+                               owner.id, id)
+
+               build = cls(pakfire, id)
+
+               # Log that the build has been created.
+               build.log("created", user=owner)
+
+               # Create directory where the files live.
+               if not os.path.exists(build.path):
+                       os.makedirs(build.path)
+
+               # Move package file to the directory of the build.
+               source_path = os.path.join(build.path, "src")
+               build.pkg.move(source_path)
+
+               # Generate an update id.
+               build.generate_update_id()
+
+               # Obsolete all other builds with the same name to track updates.
+               build.obsolete_others()
+
+               # Search for possible bug IDs in the commit message.
+               build.search_for_bugs()
+
+               return build
+
+       def delete(self):
+               """
+                       Deletes this build including all jobs, packages and the source
+                       package.
+               """
+               # If the build is in a repository, we need to remove it.
+               if self.repo:
+                       self.repo.rem_build(self)
+
+               for job in self.jobs + self.test_jobs:
+                       job.delete()
+
+               if self.pkg:
+                       self.pkg.delete()
+
+               # Delete everything related to this build.
+               self.__delete_bugs()
+               self.__delete_comments()
+               self.__delete_history()
+               self.__delete_watchers()
+
+               # Delete the build itself.
+               self.db.execute("DELETE FROM builds WHERE id = %s", self.id)
+               self.clear_cache()
+
+       def __delete_bugs(self):
+               """
+                       Delete all associated bugs.
+               """
+               self.db.execute("DELETE FROM builds_bugs WHERE build_id = %s", self.id)
+
+       def __delete_comments(self):
+               """
+                       Delete all comments.
+               """
+               self.db.execute("DELETE FROM builds_comments WHERE build_id = %s", self.id)
+
+       def __delete_history(self):
+               """
+                       Delete the repository history.
+               """
+               self.db.execute("DELETE FROM repositories_history WHERE build_id = %s", self.id)
+
+       def __delete_watchers(self):
+               """
+                       Delete all watchers.
+               """
+               self.db.execute("DELETE FROM builds_watchers WHERE build_id = %s", self.id)
+
+       def reset(self):
+               """
+                       Resets the whole build so it can start again (as it has never
+                       been started).
+               """
+               for job in self.jobs:
+                       job.reset()
+
+               #self.__delete_bugs()
+               self.__delete_comments()
+               self.__delete_history()
+               self.__delete_watchers()
+
+               self.state = "building"
+
+               # XXX empty log
+
+       @property
+       def data(self):
+               """
+                       Lazy fetching of data for this object.
+               """
+               if self._data is None:
+                       data = self.cache.get(self.cache_key)
+                       if not data:
+                               # Fetch the whole row in one call.
+                               data = self.db.get("SELECT * FROM builds WHERE id = %s", self.id)
+                               self.cache.set(self.cache_key, data)
+
+                       self._data = data
+                       assert self._data
+
+               return self._data
+
+       @property
+       def info(self):
+               """
+                       A set of information that is sent to the XMLRPC client.
+               """
+               return { "uuid" : self.uuid }
+
+       def log(self, action, user=None, bug_id=None):
+               user_id = None
+               if user:
+                       user_id = user.id
+
+               self.db.execute("INSERT INTO builds_history(build_id, action, user_id, time, bug_id) \
+                       VALUES(%s, %s, %s, NOW(), %s)", self.id, action, user_id, bug_id)
+
+       @property
+       def uuid(self):
+               """
+                       The UUID of this build.
+               """
+               return self.data.uuid
+
+       @property
+       def pkg(self):
+               """
+                       Get package that is to be built in the build.
+               """
+               if self._pkg is None:
+                       self._pkg = packages.Package(self.pakfire, self.data.pkg_id)
+
+               return self._pkg
+
+       @property
+       def name(self):
+               return "%s-%s" % (self.pkg.name, self.pkg.friendly_version)
+
+       @property
+       def type(self):
+               """
+                       The type of this build.
+               """
+               return self.data.type
+
+       @property
+       def owner_id(self):
+               """
+                       The ID of the owner of this build.
+               """
+               return self.data.owner_id
+
+       @property
+       def owner(self):
+               """
+                       The owner of this build.
+               """
+               if not self.owner_id:
+                       return
+
+               if self._owner is None:
+                       self._owner = self.pakfire.users.get_by_id(self.owner_id)
+                       assert self._owner
+
+               return self._owner
+
+       @property
+       def distro_id(self):
+               return self.data.distro_id
+
+       @property
+       def distro(self):
+               if self._distro is None:
+                       self._distro = self.pakfire.distros.get_by_id(self.distro_id)
+                       assert self._distro
+
+               return self._distro
+
+       @property
+       def user(self):
+               if self.type == "scratch":
+                       return self.owner
+
+       def get_depends_on(self):
+               if self.data.depends_on and self._depends_on is None:
+                       self._depends_on = Build(self.pakfire, self.data.depends_on)
+
+               return self._depends_on
+
+       def set_depends_on(self, build):
+               self.db.execute("UPDATE builds SET depends_on = %s WHERE id = %s",
+                       build.id, self.id)
+               self.clear_cache()
+
+               # Update cache.
+               self._depends_on = build
+               self._data["depends_on"] = build.id
+
+       depends_on = property(get_depends_on, set_depends_on)
+
+       @property
+       def created(self):
+               return self.data.time_created
+
+       @property
+       def public(self):
+               """
+                       Is this build public?
+               """
+               return self.data.public == "Y"
+
+       #@property
+       #def state(self):
+       #       # Cache all states.
+       #       states = [j.state for j in self.jobs]
+       #
+       #       target_state = "unknown"
+       #
+       #       # If at least one job has failed, the whole build has failed.
+       #       if "failed" in states:
+       #               target_state = "failed"
+       #
+       #       # It at least one of the jobs is still running, the whole
+       #       # build is in running state.
+       #       elif "running" in states:
+       #               target_state = "running"
+       #
+       #       # If all jobs are in the finished state, we turn into finished
+       #       # state as well.
+       #       elif all([s == "finished" for s in states]):
+       #               target_state = "finished"
+       #
+       #       return target_state
+
+       def auto_update_state(self):
+               """
+                       Check if the state of this build can be updated and perform
+                       the change if possible.
+               """
+               # Do not change the broken/obsolete state automatically.
+               if self.state in ("broken", "obsolete"):
+                       return
+
+               if self.repo and self.repo.type == "stable":
+                       self.update_state("stable")
+                       return
+
+               # If any of the build jobs are finished, the build will be put in testing
+               # state.
+               for job in self.jobs:
+                       if job.state == "finished":
+                               self.update_state("testing")
+                               break
+
+       def update_state(self, state, user=None, remove=False):
+               assert state in ("stable", "testing", "obsolete", "broken")
+
+               self.db.execute("UPDATE builds SET state = %s WHERE id = %s", state, self.id)
+
+               if self._data:
+                       self._data["state"] = state
+               self.clear_cache()
+
+               # In broken state, the removal from the repository is forced and
+               # all jobs that are not finished yet will be aborted.
+               if state == "broken":
+                       remove = True
+
+                       for job in self.jobs:
+                               if job.state in ("new", "pending", "running", "dependency_error"):
+                                       job.state = "aborted"
+
+               # If this build is in a repository, it will leave it.
+               if remove and self.repo:
+                               self.repo.rem_build(self)
+
+               # If a release build is now in testing state, we put it into the
+               # first repository of the distribution.
+               elif self.type == "release" and state == "testing":
+                       # If the build is not in a repository, yet and if there is
+                       # a first repository, we put the build there.
+                       if not self.repo and self.distro.first_repo:
+                               self.distro.first_repo.add_build(self, user=user)
+
+       @property
+       def state(self):
+               return self.data.state
+
+       def obsolete_others(self):
+               if not self.type == "release":
+                       return
+
+               for build in self.pakfire.builds.get_by_name(self.pkg.name, type="release"):
+                       # Don't modify ourself.
+                       if self.id == build.id:
+                               continue
+
+                       # Don't touch broken builds.
+                       if build.state in ("obsolete", "broken"):
+                               continue
+
+                       # Obsolete the build.
+                       build.update_state("obsolete")
+
+       def set_severity(self, severity):
+               self.db.execute("UPDATE builds SET severity = %s WHERE id = %s", state, self.id)
+
+               if self._data:
+                       self._data["severity"] = severity
+               self.clear_cache()
+
+       def get_severity(self):
+               return self.data.severity
+
+       severity = property(get_severity, set_severity)
+
+       @property
+       def commit(self):
+               if self.pkg and self.pkg.commit:
+                       return self.pkg.commit
+
+       def update_message(self, msg):
+               self.db.execute("UPDATE builds SET message = %s WHERE id = %s", msg, self.id)
+
+               if self._data:
+                       self._data["message"] = msg
+               self.clear_cache()
+
+       def has_perm(self, user):
+               """
+                       Check, if the given user has the right to perform administrative
+                       operations on this build.
+               """
+               if user is None:
+                       return False
+
+               if user.is_admin():
+                       return True
+
+               # Check if the user is allowed to manage packages from the critical path.
+               if self.critical_path and not user.has_perm("manage_critical_path"):
+                       return False
+
+               # Search for maintainers...
+
+               # Scratch builds.
+               if self.type == "scratch":
+                       # The owner of a scratch build has the right to do anything with it.
+                       if self.owner_id == user.id:
+                               return True
+
+               # Release builds.
+               elif self.type == "release":
+                       # The maintainer also is allowed to manage the build.
+                       if self.pkg.maintainer == user:
+                               return True
+
+               # Deny permission for all other cases.
+               return False
+
+       @property
+       def message(self):
+               message = ""
+
+               if self.data.message:
+                       message = self.data.message
+
+               elif self.commit:
+                       if self.commit.message:
+                               message = "\n".join((self.commit.subject, self.commit.message))
+                       else:
+                               message = self.commit.subject
+
+                       prefix = "%s: " % self.pkg.name
+                       if message.startswith(prefix):
+                               message = message[len(prefix):]
+
+               return message
+
+       def get_priority(self):
+               return self.data.priority
+
+       def set_priority(self, priority):
+               assert priority in (-2, -1, 0, 1, 2)
+
+               self.db.execute("UPDATE builds SET priority = %s WHERE id = %s", priority,
+                       self.id)
+               self.clear_cache()
+
+               if self._data:
+                       self._data["priority"] = priority
+
+       priority = property(get_priority, set_priority)
+
+       @property
+       def path(self):
+               path = []
+               if self.type == "scratch":
+                       path.append(BUILD_SCRATCH_DIR)
+                       path.append(self.uuid)
+
+               elif self.type == "release":
+                       path.append(BUILD_RELEASE_DIR)
+                       path.append("%s/%s-%s-%s" % \
+                               (self.pkg.name, self.pkg.epoch, self.pkg.version, self.pkg.release))
+
+               else:
+                       raise Exception, "Unknown build type: %s" % self.type
+                       
+               return os.path.join(*path)
+
+       @property
+       def source_filename(self):
+               return os.path.basename(self.pkg.path)
+
+       @property
+       def download_prefix(self):
+               return "/".join((self.pakfire.settings.get("download_baseurl"), "packages"))
+
+       @property
+       def source_download(self):
+               return "/".join((self.download_prefix, self.pkg.path))
+
+       @property
+       def source_hash_sha512(self):
+               return self.pkg.hash_sha512
+
+       @property
+       def link(self):
+               # XXX maybe this should rather live in a uimodule.
+               # zlib-1.2.3-2.ip3 [src, i686, blah...]
+               s = """<a class="state_%s %s" href="/build/%s">%s</a>""" % \
+                       (self.state, self.type, self.uuid, self.name)
+
+               s_jobs = []
+               for job in self.jobs:
+                       s_jobs.append("""<a class="state_%s %s" href="/job/%s">%s</a>""" % \
+                               (job.state, job.type, job.uuid, job.arch.name))
+
+               if s_jobs:
+                       s += " [%s]" % ", ".join(s_jobs)
+
+               return s
+
+       @property
+       def supported_arches(self):
+               return self.pkg.supported_arches
+
+       @property
+       def critical_path(self):
+               return self.pkg.critical_path
+
+       def get_jobs(self, type=None):
+               """
+                       Returns a list of jobs of this build.
+               """
+               return self.pakfire.jobs.get_by_build(self.id, self, type=type)
+
+       @property
+       def jobs(self):
+               """
+                       Get a list of all build jobs that are in this build.
+               """
+               if self._jobs is None:
+                       self._jobs = self.get_jobs(type="build")
+
+               return self._jobs
+
+       @property
+       def test_jobs(self):
+               if self._jobs_test is None:
+                       self._jobs_test = self.get_jobs(type="test")
+
+               return self._jobs_test
+
+       @property
+       def all_jobs_finished(self):
+               ret = True
+
+               for job in self.jobs:
+                       if not job.state == "finished":
+                               ret = False
+                               break
+
+               return ret
+
+       def create_autojobs(self, arches=None, type="build"):
+               jobs = []
+
+               # Arches may be passed to this function. If not we use all arches
+               # this package supports.
+               if arches is None:
+                       arches = self.supported_arches
+
+               # Create a new job for every given archirecture.
+               for arch in self.pakfire.arches.expand(arches):
+                       # Don't create jobs for src.
+                       if arch.name == "src":
+                               continue
+
+                       job = self.add_job(arch, type=type)
+                       jobs.append(job)
+
+               # Return all newly created jobs.
+               return jobs
+
+       def add_job(self, arch, type="build"):
+               job = Job.create(self.pakfire, self, arch, type=type)
+
+               # Add new job to cache.
+               if self._jobs:
+                       self._jobs.append(job)
+
+               return job
+
+       ## Update stuff
+
+       @property
+       def update_id(self):
+               if not self.type == "release":
+                       return
+
+               # Generate an update ID if none does exist, yet.
+               self.generate_update_id()
+
+               s = [
+                       "%s" % self.distro.name.replace(" ", "").upper(),
+                       "%04d" % (self.data.update_year or 0),
+                       "%04d" % (self.data.update_num or 0),
+               ]
+
+               return "-".join(s)
+
+       def generate_update_id(self):
+               if not self.type == "release":
+                       return
+
+               if self.data.update_num:
+                       return
+
+               update = self.db.get("SELECT update_num AS num FROM builds \
+                       WHERE update_year = YEAR(NOW()) ORDER BY update_num DESC LIMIT 1")
+
+               if update:
+                       update_num = update.num + 1
+               else:
+                       update_num = 1
+
+               self.db.execute("UPDATE builds SET update_year = YEAR(NOW()), update_num = %s \
+                       WHERE id = %s", update_num, self.id)
+
+       ## Comment stuff
+
+       def get_comments(self, limit=10, offset=0):
+               query = "SELECT * FROM builds_comments \
+                       JOIN users ON builds_comments.user_id = users.id \
+                       WHERE build_id = %s     ORDER BY time_created ASC"
+
+               comments = []
+               for comment in self.db.query(query, self.id):
+                       comment = logs.CommentLogEntry(self.pakfire, comment)
+                       comments.append(comment)
+
+               return comments
+
+       def add_comment(self, user, text, credit):
+               # Add the new comment to the database.
+               id = self.db.execute("INSERT INTO \
+                       builds_comments(build_id, user_id, text, credit, time_created) \
+                       VALUES(%s, %s, %s, %s, NOW())",
+                       self.id, user.id, text, credit)
+
+               # Update the credit cache.
+               if not self._credits is None:
+                       self._credits += credit
+
+               # Send the new comment to all watchers and stuff.
+               self.send_comment_message(id)
+
+               # Return the ID of the newly created comment.
+               return id
+
+       @property
+       def score(self):
+               # XXX UPDATE THIS
+               if self._credits is None:
+                       # Get the sum of the credits from the database.
+                       query = self.db.get(
+                               "SELECT SUM(credit) as credits FROM builds_comments WHERE build_id = %s",
+                               self.id
+                       )
+
+                       self._credits = query.credits or 0
+
+               return self._credits
+
+       @property
+       def credits(self):
+               # XXX COMPAT
+               return self.score
+
+       def get_commenters(self):
+               users = self.db.query("SELECT DISTINCT users.id AS id FROM builds_comments \
+                       JOIN users ON builds_comments.user_id = users.id \
+                       WHERE builds_comments.build_id = %s AND NOT users.deleted = 'Y' \
+                       AND NOT users.activated = 'Y' ORDER BY users.id", self.id)
+
+               return [users.User(self.pakfire, u.id) for u in users]
+
+       def send_comment_message(self, comment_id):
+               comment = self.db.get("SELECT * FROM builds_comments WHERE id = %s",
+                       comment_id)
+
+               assert comment
+               assert comment.build_id == self.id
+
+               # Get user who wrote the comment.
+               user = self.pakfire.users.get_by_id(comment.user_id)
+
+               format = {
+                       "build_name" : self.name,
+                       "user_name"  : user.realname,
+               }
+
+               # XXX create beautiful message
+
+               self.pakfire.messages.send_to_all(self.message_recipients,
+                       N_("%(user_name)s commented on %(build_name)s"),
+                       comment.text, format)
+
+       ## Logging stuff
+
+       def get_log(self, comments=True, repo=True, limit=None):
+               entries = []
+
+               if comments:
+                       entries += self.get_comments(limit=limit)
+
+               if repo:
+                       entries += self.get_repo_moves(limit=limit)
+
+               # Sort all entries in chronological order.
+               entries.sort()
+
+               if limit:
+                       entries = entries[:limit]
+
+               return entries
+
+       ## Watchers stuff
+
+       def get_watchers(self):
+               query = self.db.query("SELECT DISTINCT user_id AS id FROM builds_watchers \
+                       JOIN users ON builds_watchers.user_id = users.id \
+                       WHERE builds_watchers.build_id = %s AND NOT users.deleted = 'Y' \
+                       AND users.activated = 'Y' ORDER BY users.id", self.id)
+
+               return [users.User(self.pakfire, u.id) for u in query]
+
+       def add_watcher(self, user):
+               # Don't add a user twice.
+               if user in self.get_watchers():
+                       return
+
+               self.db.execute("INSERT INTO builds_watchers(build_id, user_id) \
+                       VALUES(%s, %s)", self.id, user.id)
+
+       @property
+       def message_recipients(self):
+               ret = []
+
+               for watcher in self.get_watchers():
+                       ret.append("%s <%s>" % (watcher.realname, watcher.email))
+
+               return ret
+
+       @property
+       def update(self):
+               if self._update is None:
+                       update = self.db.get("SELECT update_id AS id FROM updates_builds \
+                               WHERE build_id = %s", self.id)
+
+                       if update:
+                               self._update = updates.Update(self.pakfire, update.id)
+
+               return self._update
+
+       @property
+       def repo(self):
+               if self._repo is None:
+                       repo = self.db.get("SELECT repo_id AS id FROM repositories_builds \
+                               WHERE build_id = %s", self.id)
+
+                       if repo:
+                               self._repo = repository.Repository(self.pakfire, repo.id)
+
+               return self._repo
+
+       def get_repo_moves(self, limit=None):
+               query = "SELECT * FROM repositories_history \
+                       WHERE build_id = %s ORDER BY time ASC"
+
+               actions = []
+               for action in self.db.query(query, self.id):
+                       action = logs.RepositoryLogEntry(self.pakfire, action)
+                       actions.append(action)
+
+               return actions
+
+       @property
+       def is_loose(self):
+               if self.repo:
+                       return False
+
+               return True
+
+       @property
+       def repo_time(self):
+               repo = self.db.get("SELECT time_added FROM repositories_builds \
+                       WHERE build_id = %s", self.id)
+
+               if repo:
+                       return repo.time_added
+
+       def get_auto_move(self):
+               return self.data.auto_move == "Y"
+
+       def set_auto_move(self, state):
+               if state:
+                       state = "Y"
+               else:
+                       state = "N"
+
+               self.db.execute("UPDATE builds SET auto_move = %s WHERE id = %s", self.id)
+               if self._data:
+                       self._data["auto_move"] = state
+
+       auto_move = property(get_auto_move, set_auto_move)
+
+       @property
+       def can_move_forward(self):
+               if not self.repo:
+                       return False
+
+               # If there is no next repository, we cannot move anything.
+               next_repo = self.repo.next()
+
+               if not next_repo:
+                       return False
+
+               # If the needed amount of score is reached, we can move forward.
+               if self.score >= next_repo.score_needed:
+                       return True
+
+               # If the repository does not require a minimal time,
+               # we can move forward immediately.
+               if not self.repo.time_min:
+                       return True
+
+               query = self.db.get("SELECT NOW() - time_added AS duration FROM repositories_builds \
+                       WHERE build_id = %s", self.id)
+               duration = query.duration
+
+               if duration >= self.repo.time_min:
+                       return True
+
+               return False
+
+       ## Bugs
+
+       def get_bug_ids(self):
+               query = self.db.query("SELECT bug_id FROM builds_bugs \
+                       WHERE build_id = %s", self.id)
+
+               return [b.bug_id for b in query]
+
+       def add_bug(self, bug_id, user=None, log=True):
+               # Check if this bug is already in the list of bugs.
+               if bug_id in self.get_bug_ids():
+                       return
+
+               self.db.execute("INSERT INTO builds_bugs(build_id, bug_id) \
+                       VALUES(%s, %s)", self.id, bug_id)
+
+               # Log the event.
+               if log:
+                       self.log("bug_added", user=user, bug_id=bug_id)
+
+       def rem_bug(self, bug_id, user=None, log=True):
+               self.db.execute("DELETE FROM builds_bugs WHERE build_id = %s AND \
+                       bug_id = %s", self.id, bug_id)
+
+               # Log the event.
+               if log:
+                       self.log("bug_removed", user=user, bug_id=bug_id)
+
+       def search_for_bugs(self):
+               if not self.commit:
+                       return
+
+               pattern = re.compile(r"(bug\s?|#)(\d+)")
+
+               for txt in (self.commit.subject, self.commit.message):
+                       for bug in re.finditer(pattern, txt):
+                               try:
+                                       bugid = int(bug.group(2))
+                               except ValueError:
+                                       continue
+
+                               # Check if a bug with the given ID exists in BZ.
+                               bug = self.pakfire.bugzilla.get_bug(bugid)
+                               if not bug:
+                                       continue
+
+                               self.add_bug(bugid)
+
+       def get_bugs(self):
+               bugs = []
+               for bug_id in self.get_bug_ids():
+                       bug = self.pakfire.bugzilla.get_bug(bug_id)
+                       if not bug:
+                               continue
+
+                       bugs.append(bug)
+
+               return bugs
+
+       def _update_bugs_helper(self, repo):
+               """
+                       This function takes a new status and generates messages that
+                       are appended to all bugs.
+               """
+               try:
+                       kwargs = BUG_MESSAGES[repo.type].copy()
+               except KeyError:
+                       return
+
+               baseurl = self.pakfire.settings.get("baseurl", "")
+               args = {
+                       "build_url"    : "%s/build/%s" % (baseurl, self.uuid),
+                       "distro_name"  : self.distro.name,
+                       "package_name" : self.name,
+                       "repo_name"    : repo.name,
+               }
+               kwargs["comment"] = kwargs["comment"] % args
+
+               self.update_bugs(**kwargs)
+
+       def _update_bug(self, bug_id, status=None, resolution=None, comment=None):
+               self.db.execute("INSERT INTO builds_bugs_updates(bug_id, status, resolution, comment, time) \
+                       VALUES(%s, %s, %s, %s, NOW())", bug_id, status, resolution, comment)
+
+       def update_bugs(self, status, resolution=None, comment=None):
+               # Update all bugs linked to this build.
+               for bug_id in self.get_bug_ids():
+                       self._update_bug(bug_id, status=status, resolution=resolution, comment=comment)
+
+
+class Jobs(base.Object):
+       def get_by_id(self, id):
+               return Job(self.pakfire, id)
+
+       def get_by_uuid(self, uuid):
+               job = self.db.get("SELECT id FROM jobs WHERE uuid = %s", uuid)
+
+               if job:
+                       return self.get_by_id(job.id)
+
+       def get_by_build(self, build_id, build=None, type=None):
+               """
+                       Get all jobs in the specifies build.
+               """
+               query = "SELECT id FROM jobs WHERE build_id = %s"
+               args = [build_id,]
+
+               if type:
+                       query += " AND type = %s"
+                       args.append(type)
+
+               # Get IDs of all builds in this group.
+               jobs = []
+               for job in self.db.query(query, *args):
+                       job = Job(self.pakfire, job.id)
+
+                       # If the Build object was set, we set it so it won't be retrieved
+                       # from the database again.
+                       if build:
+                               job._build = build
+
+                       jobs.append(job)
+
+               # Return sorted list of jobs.
+               return sorted(jobs)
+
+       def get_active(self, host_id=None, uploads=True):
+               running_states = ["dispatching", "running"]
+
+               if uploads:
+                       running_states.append("uploading")
+
+               query = "SELECT id FROM jobs WHERE (%s)" % \
+                       " OR ".join(["state = '%s'" % s for s in running_states])
+
+               if host_id:
+                       query += " AND builder_id = %s" % host_id
+
+               query += " ORDER BY time_started DESC"
+
+               return [Job(self.pakfire, j.id) for j in self.db.query(query)]
+
+       def get_next_iter(self, arches=None, limit=None, offset=None, type=None, states=["pending", "new"], max_tries=None):
+               args = []
+               conditions = [
+                       "(start_not_before IS NULL OR start_not_before <= NOW())",
+               ]
+
+               if type:
+                       conditions.append("jobs.type = %s")
+                       args.append(type)
+
+               if states:
+                       conditions.append("(%s)" % " OR ".join(["jobs.state = %s" for state in states]))
+                       args += states
+
+               if arches:
+                       conditions.append("(%s)" % " OR ".join(["jobs.arch_id = %s" for a in arches]))
+                       args += [a.id for a in arches]
+
+               # Only return jobs with up to max_tries tries.
+               if max_tries:
+                       conditions.append("jobs.tries <= %s")
+                       args.append(max_tries)
+
+               query = "SELECT jobs.id AS id FROM jobs \
+                       JOIN builds ON jobs.build_id = builds.id"
+
+               if conditions:
+                       query += " WHERE %s" % " AND ".join(conditions)
+
+               # Choose the oldest one at first, but prefer real builds instead of
+               # test builds.
+               query += " ORDER BY \
+                       CASE \
+                               WHEN jobs.type = 'build' THEN 0 \
+                               WHEN jobs.type = 'test'  THEN 1 \
+                       END, \
+                       builds.priority DESC, jobs.time_created ASC"
+
+               if limit:
+                       if offset:
+                               query += " LIMIT %s,%s"
+                               args += [limit, offset]
+                       else:
+                               query += " LIMIT %s"
+                               args += [limit]
+
+               for job in self.db.query(query, *args):
+                       yield Job(self.pakfire, job.id)
+
+       def get_next(self, *args, **kwargs):
+               jobs = []
+
+               # Fetch all objects right now.
+               for job in self.get_next_iter(*args, **kwargs):
+                       jobs.append(job)
+
+               return jobs
+
+       def get_latest(self, builder=None, limit=10):
+               query = "SELECT id FROM jobs"
+
+               #where = ["time_finished IS NOT NULL",]
+               where = ["(state = 'finished' OR state = 'failed')"]
+               if builder:
+                       where.append("builder = '%s'" % builder)
+
+               if where:
+                       query += " WHERE %s" % " AND ".join(where)
+
+               query += " ORDER BY time_finished DESC LIMIT %s"
+
+               return [Job(self.pakfire, j.id) for j in self.db.query(query, limit)]
+
+       def get_average_build_time(self):
+               """
+                       Returns the average build time of all finished builds from the
+                       last 3 months.
+               """
+               cache_key = "jobs_avg_build_time"
+
+               build_time = self.cache.get(cache_key)
+               if not build_time:
+                       result = self.db.get("SELECT AVG(time_finished - time_started) as average \
+                               FROM jobs WHERE type = 'build' AND state = 'finished' AND \
+                               time_finished >= DATE_SUB(NOW(), INTERVAL 3 MONTH)")
+
+                       build_time = result.average or 0
+                       self.cache.set(cache_key, build_time, 3600)
+
+               return build_time
+
+       def count(self, *states):
+               states = sorted(states)
+
+               cache_key = "jobs_count_%s" % ("-".join(states) or "all")
+
+               count = self.cache.get(cache_key)
+               if count is None:
+                       query = "SELECT COUNT(*) AS count FROM jobs"
+                       args  = []
+
+                       if states:
+                               query += " WHERE %s" % " OR ".join("state = %s" for s in states)
+                               args += states
+
+                       jobs = self.db.get(query, *args)
+
+                       count = jobs.count
+                       self.cache.set(cache_key, count, 60)
+
+               return count
+
+
+class Job(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
+
+               # The ID of this Job object.
+               self.id = id
+
+               # Cache the data of this object.
+               self._data = None
+               self._build = None
+               self._builder = None
+               self._packages = None
+               self._logfiles = None
+
+       def __str__(self):
+               return "<%s id=%s %s>" % (self.__class__.__name__, self.id, self.name)
+
+       def __cmp__(self, other):
+               if self.type == "build" and other.type == "test":
+                       return  -1
+               elif self.type == "test" and other.type == "build":
+                       return 1
+
+               if self.build_id == other.build_id:
+                       return cmp(self.arch, other.arch)
+
+               ret = cmp(self.pkg, other.pkg)
+
+               if not ret:
+                       ret = cmp(self.time_created, other.time_created)
+
+               return ret
+
+       @property
+       def distro(self):
+               assert self.build.distro
+               return self.build.distro
+
+       @property
+       def cache_key(self):
+               return "job_%s" % self.id
+
+       def clear_cache(self):
+               """
+                       Clear the stored data from the cache.
+               """
+               self.cache.delete(self.cache_key)
+
+       @classmethod
+       def create(cls, pakfire, build, arch, type="build"):
+               id = pakfire.db.execute("INSERT INTO jobs(uuid, type, build_id, arch_id, time_created) \
+                       VALUES(%s, %s, %s, %s, NOW())", "%s" % uuid.uuid4(), type, build.id, arch.id)
+
+               job = Job(pakfire, id)
+               job.log("created")
+
+               # Set cache for Build object.
+               job._build = build
+
+               # Jobs are by default in state "new" and wait for being checked
+               # for dependencies. Packages that do have no build dependencies
+               # can directly be forwarded to "pending" state.
+               if not job.pkg.requires:
+                       job.state = "pending"
+
+               return job
+
+       def delete(self):
+               self.__delete_buildroots()
+               self.__delete_history()
+               self.__delete_packages()
+               self.__delete_logfiles()
+
+               # Delete the job itself.
+               self.db.execute("DELETE FROM jobs WHERE id = %s", self.id)
+               self.clear_cache()
+
+       def __delete_buildroots(self):
+               """
+                       Removes all buildroots.
+               """
+               self.db.execute("DELETE FROM jobs_buildroots WHERE job_id = %s", self.id)
+
+       def __delete_history(self):
+               """
+                       Removes all references in the history to this build job.
+               """
+               self.db.execute("DELETE FROM jobs_history WHERE job_id = %s", self.id)
+
+       def __delete_packages(self):
+               """
+                       Deletes all uploaded files from the job.
+               """
+               for pkg in self.packages:
+                       pkg.delete()
+
+               self.db.execute("DELETE FROM jobs_packages WHERE job_id = %s", self.id)
+
+       def __delete_logfiles(self):
+               for logfile in self.logfiles:
+                       self.db.execute("INSERT INTO queue_delete(path) VALUES(%s)", logfile.path)
+
+       def reset(self, user=None):
+               self.__delete_buildroots()
+               self.__delete_packages()
+               self.__delete_history()
+               self.__delete_logfiles()
+
+               self.state = "new"
+               self.log("reset", user=user)
+
+       @property
+       def data(self):
+               if self._data is None:
+                       data = self.cache.get(self.cache_key)
+                       if not data:
+                               data = self.db.get("SELECT * FROM jobs WHERE id = %s", self.id)
+                               self.cache.set(self.cache_key, data)
+
+                       self._data = data
+                       assert self._data
+
+               return self._data
+
+       ## Logging stuff
+
+       def log(self, action, user=None, state=None, builder=None, test_job=None):
+               user_id = None
+               if user:
+                       user_id = user.id
+
+               builder_id = None
+               if builder:
+                       builder_id = builder.id
+
+               test_job_id = None
+               if test_job:
+                       test_job_id = test_job.id
+
+               self.db.execute("INSERT INTO jobs_history(job_id, action, state, user_id, \
+                       time, builder_id, test_job_id) VALUES(%s, %s, %s, %s, NOW(), %s, %s)",
+                       self.id, action, state, user_id, builder_id, test_job_id)
+
+       def get_log(self, limit=None, offset=None, user=None):
+               query = "SELECT * FROM jobs_history"
+
+               conditions = ["job_id = %s",]
+               args  = [self.id,]
+
+               if user:
+                       conditions.append("user_id = %s")
+                       args.append(user.id)
+
+               if conditions:
+                       query += " WHERE %s" % " AND ".join(conditions)
+
+               query += " ORDER BY time DESC"
+
+               if limit:
+                       if offset:
+                               query += " LIMIT %s,%s"
+                               args  += [offset, limit,]
+                       else:
+                               query += " LIMIT %s"
+                               args  += [limit,]
+
+               entries = []
+               for entry in self.db.query(query, *args):
+                       entry = logs.JobLogEntry(self.pakfire, entry)
+                       entries.append(entry)
+
+               return entries
+
+       @property
+       def uuid(self):
+               return self.data.uuid
+
+       @property
+       def type(self):
+               return self.data.type
+
+       @property
+       def build_id(self):
+               return self.data.build_id
+
+       @property
+       def build(self):
+               if self._build is None:
+                       self._build = self.pakfire.builds.get_by_id(self.build_id)
+                       assert self._build
+
+               return self._build
+
+       @property
+       def related_jobs(self):
+               ret = []
+
+               for job in self.build.jobs:
+                       if job == self:
+                               continue
+
+                       ret.append(job)
+
+               return ret
+
+       @property
+       def pkg(self):
+               return self.build.pkg
+
+       @property
+       def name(self):
+               return "%s-%s.%s" % (self.pkg.name, self.pkg.friendly_version, self.arch.name)
+
+       def get_state(self):
+               return self.data.state
+
+       def set_state(self, state, user=None, log=True):
+               # Nothing to do if the state remains.
+               if not self.state == state:
+                       self.db.execute("UPDATE jobs SET state = %s WHERE id = %s", state, self.id)
+                       self.clear_cache()
+
+                       # Log the event.
+                       if log and not state == "new":
+                               self.log("state_change", state=state, user=user)
+
+                       # Update cache.
+                       if self._data:
+                               self._data["state"] = state
+
+               # Always clear the message when the status is changed.
+               self.update_message(None)
+
+               # Update some more informations.
+               if state == "dispatching":
+                       # Set start time.
+                       self.db.execute("UPDATE jobs SET time_started = NOW(), time_finished = NULL \
+                               WHERE id = %s", self.id)
+
+               elif state == "pending":
+                       self.db.execute("UPDATE jobs SET tries = tries + 1, time_started = NULL, \
+                               time_finished = NULL WHERE id = %s", self.id)
+
+               elif state in ("aborted", "dependency_error", "finished", "failed"):
+                       # Set finish time.
+                       self.db.execute("UPDATE jobs SET time_finished = NOW() WHERE id = %s",
+                               self.id)
+
+                       # Send messages to the user.
+                       if state == "finished":
+                               self.send_finished_message()
+
+                       elif state == "failed":
+                               # Remove all package files if a job is set to failed state.
+                               self.__delete_packages()
+
+                               self.send_failed_message()
+
+               # Automatically update the state of the build (not on test builds).
+               if self.type == "build":
+                       self.build.auto_update_state()
+
+       state = property(get_state, set_state)
+
+       @property
+       def message(self):
+               return self.data.message
+
+       def update_message(self, msg):
+               self.db.execute("UPDATE jobs SET message = %s WHERE id = %s",
+                       msg, self.id)
+               self.clear_cache()
+
+               if self._data:
+                       self._data["message"] = msg
+
+       @property
+       def builder_id(self):
+               return self.data.builder_id
+
+       def get_builder(self):
+               if not self.builder_id:
+                       return
+
+               if self._builder is None:
+                       self._builder = builders.Builder(self.pakfire, self.builder_id)
+                       assert self._builder
+
+               return self._builder
+
+       def set_builder(self, builder, user=None):
+               self.db.execute("UPDATE jobs SET builder_id = %s WHERE id = %s",
+                       builder.id, self.id)
+
+               # Update cache.
+               if self._data:
+                       self._data["builder_id"] = builder.id
+               self.clear_cache()
+
+               self._builder = builder
+
+               # Log the event.
+               if user:
+                       self.log("builder_assigned", builder=builder, user=user)
+
+       builder = property(get_builder, set_builder)
+
+       @property
+       def arch_id(self):
+               return self.data.arch_id
+
+       @property
+       def arch(self):
+               return self.pakfire.arches.get_by_id(self.arch_id)
+
+       @property
+       def duration(self):
+               if not self.time_started:
+                       return 0
+
+               if self.time_finished:
+                       delta = self.time_finished - self.time_started
+               else:
+                       delta = datetime.datetime.utcnow() - self.time_started
+
+               return delta.total_seconds()
+
+       @property
+       def time_created(self):
+               return self.data.time_created
+
+       @property
+       def time_started(self):
+               return self.data.time_started
+
+       @property
+       def time_finished(self):
+               return self.data.time_finished
+
+       @property
+       def tries(self):
+               return self.data.tries
+
+       @property
+       def packages(self):
+               if self._packages is None:
+                       self._packages = []
+
+                       query = "SELECT pkg_id AS id FROM jobs_packages \
+                               JOIN packages ON packages.id = jobs_packages.pkg_id \
+                               WHERE jobs_packages.job_id = %s ORDER BY packages.name"
+
+                       for pkg in self.db.query(query, self.id):
+                               pkg = packages.Package(self.pakfire, pkg.id)
+                               pkg._job = self
+
+                               self._packages.append(pkg)
+
+               return self._packages
+
+       def get_pkg_by_uuid(self, uuid):
+               pkg = self.db.get("SELECT packages.id FROM packages \
+                       JOIN jobs_packages ON jobs_packages.pkg_id = packages.id \
+                       WHERE jobs_packages.job_id = %s AND packages.uuid = %s",
+                       self.id, uuid)
+
+               if not pkg:
+                       return
+
+               pkg = packages.Package(self.pakfire, pkg.id)
+               pkg._job = self
+
+               return pkg
+
+       @property
+       def logfiles(self):
+               if self._logfiles is None:
+                       self._logfiles = []
+
+                       for log in self.db.query("SELECT id FROM logfiles WHERE job_id = %s", self.id):
+                               log = logs.LogFile(self.pakfire, log.id)
+                               log._job = self
+
+                               self._logfiles.append(log)
+
+               return self._logfiles
+
+       def add_file(self, filename):
+               """
+                       Add the specified file to this job.
+
+                       The file is copied to the right directory by this function.
+               """
+               assert os.path.exists(filename)
+
+               if filename.endswith(".log"):
+                       self._add_file_log(filename)
+
+               elif filename.endswith(".%s" % PACKAGE_EXTENSION):
+                       # It is not allowed to upload packages on test builds.
+                       if self.type == "test":
+                               return
+
+                       self._add_file_package(filename)
+
+       def _add_file_log(self, filename):
+               """
+                       Attach a log file to this job.
+               """
+               target_dirname = os.path.join(self.build.path, "logs")
+
+               if self.type == "test":
+                       i = 1
+                       while True:
+                               target_filename = os.path.join(target_dirname,
+                                       "test.%s.%s.%s.log" % (self.arch.name, i, self.tries))
+
+                               if os.path.exists(target_filename):
+                                       i += 1
+                               else:
+                                       break
+               else:
+                       target_filename = os.path.join(target_dirname,
+                               "build.%s.%s.log" % (self.arch.name, self.tries))
+
+               # Make sure the target directory exists.
+               if not os.path.exists(target_dirname):
+                       os.makedirs(target_dirname)
+
+               # Calculate a SHA512 hash from that file.
+               f = open(filename, "rb")
+               h = hashlib.sha512()
+               while True:
+                       buf = f.read(BUFFER_SIZE)
+                       if not buf:
+                               break
+
+                       h.update(buf)
+               f.close()
+
+               # Copy the file to the final location.
+               shutil.copy2(filename, target_filename)
+
+               # Create an entry in the database.
+               self.db.execute("INSERT INTO logfiles(job_id, path, filesize, hash_sha512) \
+                       VALUES(%s, %s, %s, %s)", self.id, os.path.relpath(target_filename, PACKAGES_DIR),
+                       os.path.getsize(target_filename), h.hexdigest())
+
+       def _add_file_package(self, filename):
+               # Open package (creates entry in the database).
+               pkg = packages.Package.open(self.pakfire, filename)
+
+               # Move package to the build directory.
+               pkg.move(os.path.join(self.build.path, self.arch.name))
+
+               # Attach the package to this job.
+               self.db.execute("INSERT INTO jobs_packages(job_id, pkg_id) VALUES(%s, %s)",
+                       self.id, pkg.id)
+
+       def get_aborted_state(self):
+               return self.data.aborted_state
+
+       def set_aborted_state(self, state):
+               self.db.execute("UPDATE jobs SET aborted_state = %s WHERE id = %s",
+                       state, self.id)
+               self.clear_cache()
+
+               if self._data:
+                       self._data["aborted_state"] = state
+
+       aborted_state = property(get_aborted_state, set_aborted_state)
+
+       @property
+       def message_recipients(self):
+               l = []
+
+               # Add all people watching the build.
+               l += self.build.message_recipients
+
+               # Add the package maintainer on release builds.
+               if self.build.type == "release":
+                       maint = self.pkg.maintainer
+
+                       if isinstance(maint, users.User):
+                               l.append("%s <%s>" % (maint.realname, maint.email))
+                       elif maint:
+                               l.append(maint)
+
+                       # XXX add committer and commit author.
+
+               # Add the owner of the scratch build on scratch builds.
+               elif self.build.type == "scratch" and self.build.user:
+                       l.append("%s <%s>" % \
+                               (self.build.user.realname, self.build.user.email))
+
+               return set(l)
+
+       def save_buildroot(self, pkgs):
+               rows = []
+
+               for pkg_name, pkg_uuid in pkgs:
+                       rows.append((self.id, self.tries, pkg_uuid, pkg_name))
+
+               # Cleanup old stuff first (for rebuilding packages).
+               self.db.execute("DELETE FROM jobs_buildroots WHERE job_id = %s AND tries = %s",
+                       self.id, self.tries)
+
+               self.db.executemany("INSERT INTO \
+                       jobs_buildroots(job_id, tries, pkg_uuid, pkg_name) \
+                       VALUES(%s, %s, %s, %s)", rows)
+
+       def has_buildroot(self, tries=None):
+               if tries is None:
+                       tries = self.tries
+
+               res = self.db.get("SELECT COUNT(*) AS num FROM jobs_buildroots \
+                       WHERE jobs_buildroots.job_id = %s AND jobs_buildroots.tries = %s \
+                       ORDER BY pkg_name", self.id, tries)
+
+               if res:
+                       return res.num
+
+               return 0
+
+       def get_buildroot(self, tries=None):
+               if tries is None:
+                       tries = self.tries
+
+               rows = self.db.query("SELECT * FROM jobs_buildroots \
+                       WHERE jobs_buildroots.job_id = %s AND jobs_buildroots.tries = %s \
+                       ORDER BY pkg_name", self.id, tries)
+
+               pkgs = []
+               for row in rows:
+                       # Search for this package in the packages table.
+                       pkg = self.pakfire.packages.get_by_uuid(row.pkg_uuid)
+                       pkgs.append((row.pkg_name, row.pkg_uuid, pkg))
+
+               return pkgs
+
+       def send_finished_message(self):
+               # Send no finished mails for test jobs.
+               if self.type == "test":
+                       return
+
+               logging.debug("Sending finished message for job %s to %s" % \
+                       (self.name, ", ".join(self.message_recipients)))
+
+               info = {
+                       "build_name" : self.name,
+                       "build_host" : self.builder.name,
+                       "build_uuid" : self.uuid,
+               }
+
+               self.pakfire.messages.send_to_all(self.message_recipients,
+                       MSG_BUILD_FINISHED_SUBJECT, MSG_BUILD_FINISHED, info)
+
+       def send_failed_message(self):
+               logging.debug("Sending failed message for job %s to %s" % \
+                       (self.name, ", ".join(self.message_recipients)))
+
+               build_host = "--"
+               if self.builder:
+                       build_host = self.builder.name
+
+               info = {
+                       "build_name" : self.name,
+                       "build_host" : build_host,
+                       "build_uuid" : self.uuid,
+               }
+
+               self.pakfire.messages.send_to_all(self.message_recipients,
+                       MSG_BUILD_FAILED_SUBJECT, MSG_BUILD_FAILED, info)
+
+       def set_start_time(self, start_time):
+               if start_time is None:
+                       return
+
+               self.db.execute("UPDATE jobs SET start_not_before = NOW() + %s \
+                       WHERE id = %s LIMIT 1", start_time, self.id)
+
+       def schedule(self, type, start_time=None, user=None):
+               assert type in ("rebuild", "test")
+
+               if type == "rebuild":
+                       if self.state == "finished":
+                               return
+
+                       self.set_state("new", user=user, log=False)
+                       self.set_start_time(start_time)
+
+                       # Log the event.
+                       self.log("schedule_rebuild", user=user)
+
+               elif type == "test":
+                       if not self.state == "finished":
+                               return
+
+                       # Create a new job with same build and arch.
+                       job = self.create(self.pakfire, self.build, self.arch, type="test")
+                       job.set_start_time(start_time)
+
+                       # Log the event.
+                       self.log("schedule_test_job", test_job=job, user=user)
+
+                       return job
+
+       def schedule_test(self, start_not_before=None, user=None):
+               # XXX to be removed
+               return self.schedule("test", start_time=start_not_before, user=user)
+
+       def schedule_rebuild(self, start_not_before=None, user=None):
+               # XXX to be removed
+               return self.schedule("rebuild", start_time=start_not_before, user=user)
+
+       def get_build_repos(self):
+               """
+                       Returns a list of all repositories that should be used when
+                       building this job.
+               """
+               repo_ids = self.db.query("SELECT repo_id FROM jobs_repos WHERE job_id = %s",
+                       self.id)
+
+               if not repo_ids:
+                       return self.distro.get_build_repos()
+
+               repos = []
+               for repo in self.distro.repositories:
+                       if repo.id in [r.id for r in repo_ids]:
+                               repos.append(repo)
+
+               return repos or self.distro.get_build_repos()
+
+       def get_repo_config(self):
+               """
+                       Get repository configuration file that is sent to the builder.
+               """
+               confs = []
+
+               for repo in self.get_build_repos():
+                       confs.append(repo.get_conf())
+
+               return "\n\n".join(confs)
+
+       def get_config(self):
+               """
+                       Get configuration file that is sent to the builder.
+               """
+               confs = []
+
+               # Add the distribution configuration.
+               confs.append(self.distro.get_config())
+
+               # Then add all repositories for this build.
+               confs.append(self.get_repo_config())
+
+               return "\n\n".join(confs)
+
+       def used_by(self):
+               if not self.packages:
+                       return []
+
+               conditions = []
+               args = []
+
+               for pkg in self.packages:
+                       conditions.append(" pkg_uuid = %s")
+                       args.append(pkg.uuid)
+
+               query = "SELECT DISTINCT job_id AS id FROM jobs_buildroots"
+               query += " WHERE %s" % " OR ".join(conditions)
+
+               job_ids = self.db.query(query, *args)
+
+               print job_ids
+
+       def resolvdep(self):
+               config = pakfire.config.Config(files=["general.conf"])
+               config.parse(self.get_config())
+
+               # The filename of the source file.
+               filename = os.path.join(PACKAGES_DIR, self.build.pkg.path)
+               assert os.path.exists(filename), filename
+
+               # Create a new pakfire instance with the configuration for
+               # this build.
+               p = pakfire.Pakfire(mode="server", config=config, arch=self.arch.name)
+
+               # Try to solve the build dependencies.
+               try:
+                       solver = p.resolvdep(filename)
+
+               # Catch dependency errors and log the problem string.
+               except DependencyError, e:
+                       self.state = "dependency_error"
+                       self.update_message(e)
+
+               else:
+                       # If the build dependencies can be resolved, we set the build in
+                       # pending state.
+                       if solver.status is True:
+                               if self.state in ("failed",):
+                                       return
+
+                               self.state = "pending"
diff --git a/backend/cache.py b/backend/cache.py
new file mode 100644 (file)
index 0000000..0aac46f
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/python
+
+import logging
+import memcache
+
+import base
+
+class Client(memcache.Client):
+       def debuglog(self, str):
+               logging.debug("MemCached: %s" % str)
+
+
+class Cache(base.Object):
+       key_prefix = "pbs_"
+
+       def __init__(self, pakfire):
+               base.Object.__init__(self, pakfire)
+
+               logging.info("Initializing memcache...")
+
+               # Fetching servers from the database configuration.
+               servers = self.settings.get("memcache_servers", "")
+               self.servers = servers.split()
+
+               logging.info("  Using servers: %s" % ", ".join(self.servers))
+
+               self._memcache = Client(self.servers, debug=1)
+
+       def get(self, key):
+               key = "".join((self.key_prefix, key))
+
+               return self._memcache.get(key)
+
+       def set(self, key, val, time=60, min_compress_len=0):
+               key = "".join((self.key_prefix, key))
+
+               return self._memcache.set(key, val, time=time,
+                       min_compress_len=min_compress_len)
+
+       def delete(self, key, time=0):
+               key = "".join((self.key_prefix, key))
+
+               return self._memcache.delete(key, time=time)
index 8b5afa91683172ee79a8db68af40c163b8861330..84d76de9da41f3fc7a988cd4b9554c0f184a188e 100644 (file)
@@ -1,38 +1,22 @@
 #!/usr/bin/python
 
-N_ = lambda x: x
+import os.path
 
-# NEVER EVER CHANGE ONE OF THE IDS!
-LOG_BUILD_CREATED                              = 1
-LOG_BUILD_STATE_PENDING                        = 2
-LOG_BUILD_STATE_DISPATCHING            = 3
-LOG_BUILD_STATE_RUNNING                        = 4
-LOG_BUILD_STATE_FAILED                 = 5
-LOG_BUILD_STATE_PERM_FAILED            = 6
-LOG_BUILD_STATE_DEP_ERROR              = 7
-LOG_BUILD_STATE_WAITING                        = 8
-LOG_BUILD_STATE_FINISHED               = 9
-LOG_BUILD_STATE_UNKNOWN                        = 10
-LOG_BUILD_STATE_UPLOADING              = 11
-
-LOG_PKG_CREATED                                        = 20
-
-
-LOG2MSG = {
-       LOG_BUILD_CREATED                       : N_("Build job created"),
-       LOG_BUILD_STATE_PENDING         : N_("Build job is now pending"),
-       LOG_BUILD_STATE_DISPATCHING     : N_("Build job is dispatching"),
-       LOG_BUILD_STATE_RUNNING         : N_("Build job is running"),
-       LOG_BUILD_STATE_FAILED          : N_("Build job has failed"),
-       LOG_BUILD_STATE_PERM_FAILED     : N_("Build job has permanently failed"),
-       LOG_BUILD_STATE_DEP_ERROR       : N_("Build job has dependency errors"),
-       LOG_BUILD_STATE_WAITING         : N_("Build job is waiting for the source package"),
-       LOG_BUILD_STATE_FINISHED        : N_("Build job is finished"),
-       LOG_BUILD_STATE_UNKNOWN         : N_("Build job has an unknown state"),
-       LOG_BUILD_STATE_UPLOADING       : N_("Build job is uploading"),
-}
+# Import all constants from the pakfire module.
+from pakfire.constants import *
+
+PAKFIRE_DIR  = "/pakfire"
+PACKAGES_DIR = os.path.join(PAKFIRE_DIR, "packages")
+BUILD_RELEASE_DIR = os.path.join(PACKAGES_DIR, "release")
+BUILD_SCRATCH_DIR = os.path.join(PACKAGES_DIR, "scratch")
+REPOS_DIR    = os.path.join(PAKFIRE_DIR, "repositories")
+SOURCES_DIR  = os.path.join(PAKFIRE_DIR, "sources")
+
+UPLOADS_DIR  = "/var/tmp/pakfire/uploads"
 
-UPLOADS_DIR = "/var/tmp/pakfire/uploads"
+BUFFER_SIZE = 1024 * 100 # 100kb
+
+N_ = lambda x: x
 
 MSG_BUILD_FAILED_SUBJECT = N_("[%(build_name)s] Build job failed.")
 MSG_BUILD_FAILED = N_("""\
@@ -46,7 +30,7 @@ Here is more information about the incident:
     Build host: %(build_host)s
 
 Click on this link to get all details about the build:
-    http://pakfire.ipfire.org/build/%(build_uuid)s
+    https://pakfire.ipfire.org/job/%(build_uuid)s
 
 Sincerely,
     The Pakfire Build Service""")
@@ -59,7 +43,42 @@ The build job "%(build_name)s" has finished.
 If you are the maintainer, it is up to you to push it to one or more repositories.
 
 Click on this link to get all details about the build:
-    http://pakfire.ipfire.org/build/%(build_uuid)s
+    https://pakfire.ipfire.org/job/%(build_uuid)s
 
 Sincerely,
     The Pakfire Build Service""")
+
+# Bug update messages.
+BUG_TESTING_MSG = """\
+%(package_name)s has been pushed to the %(distro_name)s %(repo_name)s repository.
+
+You can provide feedback for this build here:
+  %(build_url)s"""
+
+BUG_UNSTABLE_MSG = """\
+%(package_name)s has been pushed to the %(distro_name)s %(repo_name)s repository.
+
+You can provide feedback for this build here:
+  %(build_url)s"""
+
+BUG_STABLE_MSG = """\
+%(package_name)s has been pushed to the %(distro_name)s %(repo_name)s repository.
+
+If problems still persist, please make note of it in this bug report."""
+
+BUG_MESSAGES = {
+       "testing" : {
+               "status"  : "MODIFIED",
+               "comment" : BUG_TESTING_MSG,
+       },
+
+       "unstable" : {
+               "status"  : "ON_QA",
+               "comment" : BUG_UNSTABLE_MSG,
+       },
+
+       "stable" : {
+               "status"  : "CLOSED", "resolution" : "FIXED",
+               "comment" : BUG_STABLE_MSG,
+       },
+}
index 463674ebeb5bdfedd0067b756a9e8db43352adc3..f52115080836cdd4b65b83ab968177566db7895f 100644 (file)
@@ -3,52 +3,17 @@
 import logging
 import tornado.database
 
+Row = tornado.database.Row
+
 class Connection(tornado.database.Connection):
        def __init__(self, *args, **kwargs):
                logging.debug("Creating new database connection: %s" % args[1])
 
                tornado.database.Connection.__init__(self, *args, **kwargs)
 
-       def update(self, table, item_id, **items):
-               query = "UPDATE %s SET " % table
-
-               if not items:
-                       return
-       
-               keys = []
-               for k, v in items.items():
-                       # Never update id
-                       if k == "id":
-                               continue
-
-                       keys.append("`%s`='%s'" % (k, v))
-
-               query += ", ".join(keys)
-               query += " WHERE id=%s" % item_id
-
-               return self.execute(query)
-
-       def insert(self, table, **items):
-               query = "INSERT INTO %s" % table
-
-               keys = []
-               vals = []
-
-               for k, v in items.items():
-                       # Never insert id
-                       if k == "id":
-                               continue
-
-                       keys.append(k)
-                       vals.append("`%s`" % v)
-
-               query += "(%s)"% ", ".join(keys)
-               query += " VALUES(%s)" % ", ".join(vals)
-
-               return self.execute(query)
-
        def _execute(self, cursor, query, parameters):
-               logging.debug("Executing query: %s" % (query % parameters))
+               msg = "Executing query: %s" % (query % parameters)
+               logging.debug(" ".join(msg.split()))
 
                return tornado.database.Connection._execute(self, cursor, query, parameters)
 
index 4e6f2296c23f5d570aaa3a12956571b5cd66bd1e..b7d0c2e695b043768b5fe595e52951404f17f8b3 100644 (file)
@@ -1,7 +1,15 @@
 #!/usr/bin/python
 
+import logging
+
+import arches
 import base
-from repository import Repository
+import builds
+import packages
+import sources
+import updates
+
+from repository import Repository, RepositoryAux
 
 class Distributions(base.Object):
        def get_all(self):
@@ -21,22 +29,39 @@ class Distributions(base.Object):
                if distro:
                        return Distribution(self.pakfire, distro.id)
 
+       def get_by_ident(self, ident):
+               return self.get_by_name(ident)
+
+       def get_default(self):
+               # XXX a bit ugly
+               return self.get_by_ident("ipfire3")
+
 
 class Distribution(base.Object):
        def __init__(self, pakfire, id):
                base.Object.__init__(self, pakfire)
                self.id = id
 
-               self._data = \
-                       self.db.get("SELECT * FROM distributions WHERE id = %s", self.id)
+               self._data = None
+               self._arches = None
+               self._sources = None
 
        def __repr__(self):
                return "<%s %s>" % (self.__class__.__name__, self.name)
 
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = self.db.get("SELECT * FROM distributions WHERE id = %s", self.id)
+
+               return self._data
+
        def set(self, key, value):
                self.db.execute("UPDATE distributions SET %s = %%s WHERE id = %%s" % key,
                        value, self.id)
-               self._data[key] = value
+
+               if self._data:
+                       self._data[key] = value
 
        @property
        def info(self):
@@ -44,48 +69,161 @@ class Distribution(base.Object):
                        "name"        : self.name,
                        "sname"       : self.sname,
                        "slogan"      : self.slogan,
-                       "arches"      : self.arches,
                        "vendor"      : self.vendor,
+                       "contact"     : self.contact,
                        "description" : self.description,
                }
 
+       def get_config(self):
+               try:
+                       name, release = self.name.split()
+               except:
+                       name = self.name
+                       release = "N/A"
+
+               lines = [
+                       "[distro]",
+                       "name = %s" % name,
+                       "release = %s" % release,
+                       "slogan = %s" % self.slogan,
+                       "",
+                       "vendor = %s" % self.vendor,
+                       "contact = %s" % self.contact,
+               ]
+
+               return "\n".join(lines)
+
        @property
        def name(self):
-               return self._data.name
+               return self.data.name
 
        @property
        def sname(self):
-               return self._data.sname
+               return self.data.sname
 
        @property
-       def slogan(self):
-               return self._data.slogan
+       def identifier(self):
+               return self.sname
 
        @property
-       def arches(self):
-               arches = self._data.arches.split()
+       def slogan(self):
+               return self.data.slogan
+
+       def get_arches(self):
+               if self._arches is None:
+                       _arches = self.db.query("SELECT arch_id AS id FROM distro_arches \
+                               WHERE distro_id = %s", self.id)
+
+                       self._arches = []
+                       for arch in _arches:
+                               arch = arches.Arch(self.pakfire, arch.id)
+                               self._arches.append(arch)
 
-               return sorted(arches)
+                       # Sort architectures by their priority.
+                       self._arches.sort()
+
+               return self._arches
+
+       def set_arches(self, _arches):
+               self.db.execute("DELETE FROM distro_arches WHERE distro_id = %s", self.id)
+
+               for arch in _arches:
+                       self.db.execute("INSERT INTO distro_arches(distro_id, arch_id) \
+                               VALUES(%s, %s)", self.id, arch.id)
+
+               self._arches = _arches
+
+       arches = property(get_arches, set_arches)
 
        @property
        def vendor(self):
-               return self._data.vendor
+               return self.data.vendor
+
+       def get_contact(self):
+               return self.data.contact
+
+       def set_contact(self, contact):
+               self.db.execute("UPDATE distributions SET contact = %s WHERE id = %s",
+                       contact, self.id)
+
+               if self._data:
+                       self._data["contact"] = contact
+
+       contact = property(get_contact, set_contact)
+
+       def get_tag(self):
+               return self.data.tag
+
+       def set_tag(self, tag):
+               self.db.execute("UPDATE distributions SET tag = %s WHERE id = %s",
+                       tag, self.id)
+
+               if self._data:
+                       self._data["tag"] = tag
+
+       tag = property(get_tag, set_tag)
 
        @property
        def description(self):
-               return self._data.description or ""
+               return self.data.description or ""
 
        @property
        def repositories(self):
-               repos = self.db.query("SELECT id FROM repositories WHERE distro_id = %s", self.id)
+               _repos = self.db.query("SELECT id FROM repositories WHERE distro_id = %s", self.id)
+
+               repos = []
+               for repo in _repos:
+                       repo = Repository(self.pakfire, repo.id)
+                       repo._distro = self
 
-               return sorted([Repository(self.pakfire, r.id) for r in repos])
+                       repos.append(repo)
+
+               return sorted(repos)
+
+       @property
+       def repositories_aux(self):
+               _repos = self.db.query("SELECT id FROM repositories_aux \
+                       WHERE status = 'enabled' AND distro_id = %s", self.id)
+
+               repos = []
+               for repo in _repos:
+                       repo = RepositoryAux(self.pakfire, repo.id)
+                       repo._distro = self
+
+                       repos.append(repo)
+
+               return sorted(repos)
 
        def get_repo(self, name):
                repo = self.db.get("SELECT id FROM repositories WHERE distro_id = %s AND name = %s",
                        self.id, name)
 
-               return Repository(self.pakfire, repo.id)
+               if not repo:
+                       return
+
+               repo = Repository(self.pakfire, repo.id)
+               repo._distro = self
+
+               return repo
+
+       def get_build_repos(self):
+               repos = []
+               
+               for repo in self.repositories:
+                       if repo.enabled_for_builds:
+                               repos.append(repo)
+
+               # Add all aux. repositories.
+               repos += self.repositories_aux
+
+               return repos
+
+       @property
+       def first_repo(self):
+               repos = self.repositories
+
+               if repos:
+                       return self.repositories[-1]
 
        @property
        def comprehensive_repositories(self):
@@ -95,10 +233,54 @@ class Distribution(base.Object):
                return Repository.new(self.pakfire, self, name, description)
 
        @property
-       def sources(self):
-               return self.pakfire.sources.get_by_distro(self)
+       def log(self):
+               return [] # TODO
+
+       def has_package(self, name, epoch, version, release):
+               #pkg = self.db.get("SELECT packages.id AS id FROM packages \
+               #       JOIN builds ON packages.id = builds.pkg_id \
+               #       JOIN sources_commits ON packages.commit_id = sources_commits.id \
+               #       JOIN sources ON sources_commits.source_id = sources.id \
+               #       WHERE builds.type = 'release' AND sources.distro_id = %s \
+               #       AND packages.name = %s AND packages.epoch = %s \
+               #       AND packages.version = %s AND packages.release = %s LIMIT 1",
+               #       self.id, name, epoch, version, release)
+
+               pkg = self.db.get("SELECT p.id AS id FROM packages p \
+                       JOIN builds b ON p.id = b.pkg_id \
+                       WHERE b.type = 'release' AND b.distro_id = %s AND \
+                       p.name = %s AND p.epoch = %s AND p.version = %s AND p.release = %s \
+                       LIMIT 1", self.id, name, epoch, version, release)
+
+               if not pkg:
+                       logging.debug("Package %s-%s:%s-%s does not exist, yet." % \
+                               (name, epoch, version, release))
+                       return
+
+               logging.debug("Package %s-%s:%s-%s does already exist." % \
+                       (name, epoch, version, release))
+
+               return packages.Package(self.pakfire, pkg.id)
+
+       def delete_package(self, name):
+               pass # XXX figure out what to do at this place
 
        @property
-       def log(self):
-               return self.db.query("SELECT * FROM log WHERE distro_id = %s ORDER BY time DESC", self.id)
+       def sources(self):
+               if self._sources is None:
+                       self._sources = []
+
+                       for source in self.db.query("SELECT id FROM sources WHERE distro_id = %s", self.id):
+                               source = sources.Source(self.pakfire, source.id)
+                               self._sources.append(source)
+
+                       self._sources.sort()
+
+               return self._sources
+
+       def get_source(self, ident):
+               for source in self.sources:
+                       if not source.identifier == ident:
+                               continue
 
+                       return source
diff --git a/backend/keys.py b/backend/keys.py
new file mode 100644 (file)
index 0000000..03f40e3
--- /dev/null
@@ -0,0 +1,208 @@
+#!/usr/bin/python
+
+import datetime
+import gpgme
+import io
+import os
+import shutil
+import tempfile
+
+import base
+
+def read_key(data):
+       data = str(data)
+       data = io.BytesIO(data)
+
+       tmpdir = tempfile.mkdtemp()
+       os.environ["GNUPGHOME"] = tmpdir
+
+       try:
+               ctx = gpgme.Context()
+               res = ctx.import_(data)
+
+               assert len(res.imports) == 1
+               (fpr, trash_a, trash_b) = res.imports[0]
+
+               key = ctx.get_key(fpr)
+               assert key
+
+               return fpr, key
+
+       finally:
+               shutil.rmtree(tmpdir)
+               del os.environ["GNUPGHOME"]
+
+
+class Keys(base.Object):
+       def create(self, *args, **kwargs):
+               return Key.create(self.pakfire, *args, **kwargs)
+
+       def get_all(self):
+               query = self.db.query("SELECT id FROM `keys` ORDER BY uids")
+
+               keys = []
+               for key in query:
+                       key = Key(self.pakfire, key.id)
+                       keys.append(key)
+
+               return keys
+
+       def get_by_id(self, id):
+               key = self.db.get("SELECT id FROM `keys` WHERE id = %s", id)
+               if not key:
+                       return
+
+               return Key(self.pakfire, key.id)
+
+       def get_by_fpr(self, fpr):
+               fpr = "%%%s" % fpr
+
+               key = self.db.get("SELECT id FROM `keys` WHERE fingerprint LIKE %s", fpr)
+               if not key:
+                       return
+
+               return Key(self.pakfire, key.id)
+
+
+class Key(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
+
+               self.id = id
+
+               # Cache.
+               self._data = None
+               self._subkeys = None
+
+       @property
+       def keys(self):
+               return self.pakfire.keys
+
+       @classmethod
+       def create(cls, pakfire, data):
+               fingerprint, key = read_key(data)
+
+               # Search for duplicates and just update them.
+               k = pakfire.keys.get_by_fpr(fingerprint)
+               if k:
+                       k.update(data)
+                       return k
+
+               # Insert new into the database.
+               key_id = pakfire.db.execute("INSERT INTO `keys`(fingerprint, uids, data) \
+                       VALUES(%s, %s, %s)", fingerprint, ", ".join([u.uid for u in key.uids]), data)
+
+               key = cls(pakfire, key_id)
+               key.update(data)
+
+               return key
+
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = self.db.get("SELECT * FROM `keys` WHERE id = %s", self.id)
+                       assert self._data
+
+               return self._data
+
+       def update(self, data):
+               fingerprint, key = read_key(data)
+
+               # First, delete all subkeys.
+               self.db.execute("DELETE FROM keys_subkeys WHERE key_id = %s", self.id)
+
+               for subkey in key.subkeys:
+                       time_created = datetime.datetime.fromtimestamp(subkey.timestamp)
+                       if subkey.expires:
+                               time_expires = datetime.datetime.fromtimestamp(subkey.expires)
+                       else:
+                               time_expires = None # Key does never expire.
+
+                       algo = None
+                       if subkey.pubkey_algo == gpgme.PK_RSA:
+                               algo = "RSA/%s" % subkey.length
+
+                       self.db.execute("INSERT INTO keys_subkeys(key_id, fingerprint, \
+                               time_created, time_expires, algo) VALUES(%s, %s, %s, %s, %s)",
+                               self.id, subkey.keyid, time_created, time_expires, algo)
+
+               self.db.execute("UPDATE `keys` SET fingerprint = %s, uids = %s, data = %s WHERE id = %s",
+                       fingerprint, ", ".join([u.uid for u in key.uids]), data, self.id)
+
+       def can_be_deleted(self):
+               ret = self.db.query("SELECT id FROM repositories WHERE key_id = %s", self.id)
+
+               if ret:
+                       return False
+
+               return True
+
+       def delete(self):
+               assert self.can_be_deleted()
+
+               self.db.execute("DELETE FROM `keys_subkeys` WHERE key_id = %s", self.id)
+               self.db.execute("DELETE FROM `keys` WHERE id = %s", self.id)
+
+       @property
+       def fingerprint(self):
+               return self.data.fingerprint[-16:]
+
+       @property
+       def uids(self):
+               return self.data.uids.split(", ")
+
+       @property
+       def key(self):
+               return self.data.data
+
+       @property
+       def subkeys(self):
+               if self._subkeys is None:
+                       self._subkeys = []
+
+                       query = self.db.query("SELECT * FROM keys_subkeys WHERE key_id = %s ORDER BY time_created", self.id)
+
+                       for subkey in query:
+                               subkey = Subkey(self.pakfire, subkey.id)
+                               self._subkeys.append(subkey)
+
+               return self._subkeys
+
+
+class Subkey(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
+
+               self.id = id
+
+               # Cache.
+               self._data = None
+
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = self.db.get("SELECT *, time_expires - NOW() AS expired \
+                               FROM keys_subkeys WHERE id = %s", self.id)
+                       assert self._data
+
+               return self._data
+
+       @property
+       def fingerprint(self):
+               return self.data.fingerprint
+
+       @property
+       def time_created(self):
+               return self.data.time_created
+
+       @property
+       def time_expires(self):
+               return self.data.time_expires
+
+       @property
+       def expired(self):
+               return self.data.expired <= 0
+
+       @property
+       def algo(self):
+               return self.data.algo
diff --git a/backend/logs.py b/backend/logs.py
new file mode 100644 (file)
index 0000000..df3a90e
--- /dev/null
@@ -0,0 +1,384 @@
+#!/usr/bin/python
+
+import os
+
+import base
+import builds
+
+_ = lambda x: x
+
+class LogEntry(base.Object):
+       type = None
+       system_msg = True
+
+       def __init__(self, pakfire, data):
+               base.Object.__init__(self, pakfire)
+
+               self.data = data
+
+               self._user = None
+
+       def __cmp__(self, other):
+               return cmp(other.time, self.time)
+
+       @property
+       def time(self):
+               return self.data.time
+
+       def get_user(self):
+               return None
+
+       @property
+       def user(self):
+               if self._user is None:
+                       self._user = self.get_user()
+
+               return self._user
+
+       def get_title(self, user=None):
+               return None
+
+       def get_message(self, user=None):
+               raise NotImplementedError
+
+       def get_footer(self, user=None):
+               return None
+
+
+class CommentLogEntry(LogEntry):
+       type = "comment"
+       system_msg = False
+
+       @property
+       def time(self):
+               return self.data.time_created
+
+       @property
+       def credit(self):
+               return self.data.credit
+
+       @property
+       def vote(self):
+               if self.credit > 0:
+                       return "up"
+               elif self.credit < 0:
+                       return "down"
+
+               return "none"
+
+       def get_user(self):
+               user_id = getattr(self.data, "user_id", None)
+
+               if user_id is None:
+                       return
+
+               return self.pakfire.users.get_by_id(self.data.user_id)
+
+       def get_message(self, user=None):
+               return self.data.text
+
+
+class RepositoryLogEntry(LogEntry):
+       type = "repo"
+
+       def get_message(self, user=None):
+               msg = _("Unknown action.")
+
+               # See if we have done the action by ourself.
+               you = self.user == user
+
+               args = {}
+
+               # Add information about the user.
+               if self.user:
+                       args["user"] = self.user.realname
+               else:
+                       args["user"] = _("Unknown")
+
+               # Add information about the repositories.
+               if self.data.from_repo_id:
+                       repo = self.pakfire.repos.get_by_id(self.data.from_repo_id)
+                       args["from_repo"] = repo.name
+               else:
+                       args["from_repo"] = _("N/A")
+
+               if self.data.to_repo_id:
+                       repo = self.pakfire.repos.get_by_id(self.data.to_repo_id)
+                       args["to_repo"] = repo.name
+               else:
+                       args["to_repo"] = _("N/A")
+
+               action = self.data.action
+
+               if action == "added":
+                       if not self.user:
+                               msg = _("This build was pushed to the repository '%(to_repo)s'.")
+                       elif you:
+                               msg = _("You pushed this build to the repository '%(to_repo)s'.")
+                       else:
+                               msg = _("%(user)s pushed this build to the repository '%(to_repo)s'.")
+
+               elif action == "removed":
+                       if not self.user:
+                               msg = _("This build was unpushed from the repository '%(from_repo)s'.")
+                       elif you:
+                               msg = _("You unpushed this build from the repository '%(from_repo)s'.")
+                       else:
+                               msg = _("%(user)s unpushed this build from the repository '%(from_repo)s'.")
+
+               elif action == "moved":
+                       if not self.user:
+                               msg = _("This build was pushed from the repository '%(from_repo)s' to '%(to_repo)s'.")
+                       elif you:
+                               msg = _("You pushed this build from the repository '%(from_repo)s' to '%(to_repo)s'.")
+                       else:
+                               msg = _("%(user)s pushed this build from the repository '%(from_repo)s' to '%(to_repo)s'.")
+
+               return msg % args
+
+
+class BuilderLogEntry(LogEntry):
+       type = "builder"
+
+       def get_builder(self):
+               assert self.data.builder_id
+
+               return self.pakfire.builders.get_by_id(self.data.builder_id)
+
+       def get_message(self, user=None):
+               msg = _("Unknown action.")
+
+               # See if we have done the action by ourself.
+               you = self.user == user
+
+               builder = self.get_builder()
+               assert builder
+
+               args = {
+                       "builder" : builder.hostname,
+               }
+
+               # Add information about the user.
+               if self.user:
+                       args["user"] = self.user.realname
+               else:
+                       args["user"] = _("Unknown")
+
+               action = self.data.action
+
+               if action == "enabled":
+                       if not self.user:
+                               msg = _("Builder '%(builder)s' has been enabled.")
+                       elif you:
+                               msg = _("You enabled builder '%(builder)s'.")
+                       else:
+                               msg = _("%(user)s enabled builder '%(builder)s'.")
+
+               elif action == "disabled":
+                       if not self.user:
+                               msg = _("Builder '%(builder)s' was has been disabled.")
+                       elif you:
+                               msg = _("You disabled builder '%(builder)s'.")
+                       else:
+                               msg = _("%(user)s disabled builder '%(builder)s'.")
+
+               elif action == "deleted":
+                       if you:
+                               msg = _("You deleted builder '%(builder)s'.")
+                       else:
+                               msg = _("%(user)s deleted builder '%(builder)s'.")
+
+               elif action == "created":
+                       if you:
+                               msg = _("You created builder '%(builder)s'.")
+                       else:
+                               msg = _("%(user)s created builder '%(builder)s'.")
+
+               return msg % args
+
+
+class JobLogEntry(LogEntry):
+       type = "job"
+
+       def get_job(self):
+               assert self.data.job_id
+
+               return self.pakfire.jobs.get_by_id(self.data.job_id)
+
+       def get_message(self, user=None):
+               msg = _("Unknown action.")
+
+               # See if we have done the action by ourself.
+               you = self.user == user
+
+               job = self.get_job()
+               assert job
+
+               args = {
+                       "job"   : job.name,
+                       "state" : self.data.state,
+               }
+
+               # Add information about the user.
+               if self.user:
+                       args["user"] = self.user.realname
+               else:
+                       args["user"] = _("Unknown")
+
+               action = self.data.action
+
+               if action == "created":
+                       if not self.user:
+                               msg = _("Job '%(job)s' has been created.")
+                       elif you:
+                               msg = _("You created job '%(job)s'.")
+                       else:
+                               msg = _("%(user)s created job '%(job)s'.")
+
+               elif action == "state_change":
+                       if not self.user:
+                               msg = _("Job '%(job)s' has changed its state to: %(state)s.")
+                       elif you:
+                               msg = _("You changed the state of job '%(job)s' to: %(state)s.")
+                       else:
+                               msg = _("%(user)s changed the state of job '%(job)s' to: %(state)s.")
+
+               elif action == "reset":
+                       if not self.user:
+                               msg = _("Job '%(job)s' has been reset.")
+                       elif you:
+                               msg = _("You reset job '%(job)s'.")
+                       else:
+                               msg = _("%(user)s has reset job '%(job)s'.")
+
+               elif action == "schedule_rebuild":
+                       if not self.user:
+                               msg = _("Job '%(job)s' has been scheduled for rebuild.")
+                       elif you:
+                               msg = _("You scheduled job '%(job)s' for rebuild.")
+                       else:
+                               msg = _("%(user)s scheduled job '%(job)s' for rebuild.")
+
+               elif action == "schedule_test_job":
+                       # XXX add link to the test job
+
+                       if not self.user:
+                               msg = _("A test job for '%(job)s' has been scheduled.")
+                       elif you:
+                               msg = _("You scheduled a test job for '%(job)s'.")
+                       else:
+                               msg = _("%(user)s scheduled a test job for '%(job)s'.")
+
+               return msg % args
+
+
+class MirrorLogEntry(LogEntry):
+       type = "mirror"
+
+       def get_user(self):
+               if self.data.user_id:
+                       return self.pakfire.users.get_by_id(self.data.user_id)
+
+       def get_mirror(self):
+               assert self.data.mirror_id
+
+               return self.pakfire.mirrors.get_by_id(self.data.mirror_id)
+
+       def get_message(self, user=None):
+               msg = _("Unknown action.")
+
+               # See if we have done the action by ourself.
+               you = self.user == user
+
+               mirror = self.get_mirror()
+               assert mirror
+
+               args = {
+                       "mirror" : mirror.hostname,
+               }
+
+               # Add information about the user.
+               if self.user:
+                       args["user"] = self.user.realname
+               else:
+                       args["user"] = _("Unknown")
+
+               action = self.data.action
+
+               if action == "enabled":
+                       if not self.user:
+                               msg = _("Mirror '%(mirror)s' has been enabled.")
+                       elif you:
+                               msg = _("You enabled mirror '%(mirror)s'.")
+                       else:
+                               msg = _("%(user)s enabled mirror '%(mirror)s'.")
+
+               elif action == "disabled":
+                       if not self.user:
+                               msg = _("Mirror '%(mirror)s' has been disabled.")
+                       elif you:
+                               msg = _("You disabled mirror '%(mirror)s'.")
+                       else:
+                               msg = _("%(user)s disabled mirror '%(mirror)s'.")
+
+               elif action == "deleted":
+                       if you:
+                               msg = _("You deleted mirror '%(mirror)s'.")
+                       else:
+                               msg = _("%(user)s deleted mirror '%(mirror)s'.")
+
+               elif action == "created":
+                       if you:
+                               msg = _("You created mirror '%(mirror)s'.")
+                       else:
+                               msg = _("%(user)s created mirror '%(mirror)s'.")
+
+               return msg % args
+
+
+class LogFile(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
+
+               # Save the ID of the item.
+               self.id = id
+
+               # Cache.
+               self._data = None
+               self._job = None
+
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = self.db.get("SELECT * FROM logfiles WHERE id = %s", self.id)
+                       assert self._data
+
+               return self._data
+
+       @property
+       def name(self):
+               return os.path.basename(self.path)
+
+       @property
+       def path(self):
+               return self.data.path
+
+       @property
+       def job(self):
+               if self._job is None:
+                       self._job = self.pakfire.jobs.get_by_id(self.data.job_id)
+                       assert self._job
+
+               return self._job
+
+       @property
+       def build(self):
+               return self.job.build
+
+       @property
+       def download_url(self):
+               return "/".join((self.build.download_prefix, self.path))
+
+       @property
+       def filesize(self):
+               return self.data.filesize
diff --git a/backend/main.py b/backend/main.py
new file mode 100644 (file)
index 0000000..9eee852
--- /dev/null
@@ -0,0 +1,103 @@
+#!/usr/bin/python
+
+import logging
+import os
+import pakfire
+
+import arches
+import bugtracker
+import builders
+import builds
+import cache
+import database
+import distribution
+import keys
+import logs
+import messages
+import mirrors
+import packages
+import repository
+import settings
+import sources
+import updates
+import uploads
+import users
+
+from constants import *
+
+# Database access.
+MYSQL_SERVER   = "mysql-master.ipfire.org"
+MYSQL_USER     = "pakfire"
+MYSQL_DB       = "pakfire"
+MYSQL_GEOIP_DB = "geoip"
+
+class Pakfire(object):
+       def __init__(self):
+               self.db = database.Connection(MYSQL_SERVER, MYSQL_DB, user=MYSQL_USER)
+
+               # Global pakfire settings (from database).
+               self.settings = settings.Settings(self)
+
+               self.arches      = arches.Arches(self)
+               self.builds      = builds.Builds(self)
+               self.cache       = cache.Cache(self)
+               self.geoip       = mirrors.GeoIP(self, MYSQL_SERVER, MYSQL_GEOIP_DB,
+                                                               user=MYSQL_USER)
+               self.jobs        = builds.Jobs(self)
+               self.builders    = builders.Builders(self)
+               self.distros     = distribution.Distributions(self)
+               self.keys        = keys.Keys(self)
+               self.messages    = messages.Messages(self)
+               self.mirrors     = mirrors.Mirrors(self)
+               self.packages    = packages.Packages(self)
+               self.repos       = repository.Repositories(self)
+               self.sources     = sources.Sources(self)
+               self.updates     = updates.Updates(self)
+               self.uploads     = uploads.Uploads(self)
+               self.users       = users.Users(self)
+
+               # Open a connection to bugzilla.
+               self.bugzilla    = bugtracker.Bugzilla(self)
+
+               # A pool to store strings (for comparison).
+               self.pool        = pakfire.satsolver.Pool("dummy")
+
+       def __del__(self):
+               if self.db:
+                       self.db.close()
+                       del self.db
+
+       def cleanup_files(self):
+               query = self.db.query("SELECT * FROM queue_delete")
+
+               for row in query:
+                       if not row.path:
+                               continue
+
+                       path = os.path.join(PACKAGES_DIR, row.path)
+
+                       try:
+                               logging.debug("Removing %s..." % path)
+                               os.unlink(path)
+                       except OSError, e:
+                               logging.error("Could not remove %s: %s" % (path, e))
+
+                       while True:                     
+                               path = os.path.dirname(path)
+
+                               # Stop if we are running outside of the tree.
+                               if not path.startswith(PACKAGES_DIR):
+                                       break
+
+                               # If the directory is not empty, we cannot remove it.
+                               if os.path.exists(path) and os.listdir(path):
+                                       break
+
+                               try:
+                                       logging.debug("Removing %s..." % path)
+                                       os.rmdir(path)
+                               except OSError, e:
+                                       logging.error("Could not remove %s: %s" % (path, e))
+                                       break
+
+                       self.db.execute("DELETE FROM queue_delete WHERE id = %s", row.id)
index 7153e2a12d9881327510420d0a1a2b55ee6a21db..79f891895a903897167f8c1bf38173dbee0f8632 100644 (file)
@@ -1,9 +1,27 @@
 #!/usr/bin/python
 
+import datetime
 import logging
+import multiprocessing
+import os
+import shutil
+import subprocess
+import tempfile
+import time
 import tornado.ioloop
 
+import pakfire
+import pakfire.api
+import pakfire.config
+from pakfire.constants import *
+
 import base
+import builds
+import main
+import packages
+import sources
+
+from constants import *
 
 managers = []
 
@@ -40,6 +58,53 @@ class Manager(base.Object):
                raise NotImplementedError
 
 
+       # Helper functions
+
+       def wait_for_processes(self):
+               ALIVE_CHECK_INTERVAL = 0.5
+
+               logging.debug("There are %s process(es) in the queue." % len(self.processes))
+
+               # Nothing to do, if there are no processes running.
+               if not self.processes:
+                       return
+
+               # Get the currently running? process.
+               process = self.processes[0]
+
+               # If the first process is running, everything is okay and
+               # we'll have to wait.
+               if process.is_alive():
+                       return ALIVE_CHECK_INTERVAL
+
+               # If the process has not been run, it is started now.
+               if process.exitcode is None:
+                       logging.debug("Process %s is being started..." % process)
+
+                       process.start()
+                       return ALIVE_CHECK_INTERVAL
+
+               # If the process has stopped working we check why...
+               elif process.exitcode == 0:
+                       logging.debug("Process %s has successfully finished." % process)
+
+               elif process.exitcode > 0:
+                       logging.error("Process %s has exited with code: %s" % \
+                               (process, process.exitcode))
+
+               elif process.exitcode < 0:
+                       logging.error("Process %s has ended with signal %s" % \
+                               (process, process.exitcode))
+
+               # Remove process from process queue.
+               self.processes.remove(process)
+
+               # If there are still processes in the queue, we start this function
+               # again...
+               if self.processes:
+                       return self.wait_for_processes()
+
+
 class MessagesManager(Manager):
        @property
        def messages(self):
@@ -60,12 +125,207 @@ class MessagesManager(Manager):
        def do(self):
                logging.info("Sending a bunch of messages.")
 
-               # Send up to 10 messages and return.
-               self.messages.send_messages(limit=10)
+               # Send up to 25 messages and return.
+               i = 0
+               for msg in self.messages.get_all(limit=25):
+                       try:
+                               self.messages.send_msg(msg)
+
+                       except Exception, e:
+                               logging.critical("There was an error sending mail: %s" % e)
+                               raise
+
+                       else:
+                               # If everything was okay, we can delete the message in the database.
+                               self.messages.delete(msg.id)
+                               i += 1
+
+               count = self.messages.count
+
+               logging.debug("Successfully sent %s message(s). %s still in queue." \
+                       % (i, count))
+
+               # If there are still mails left, we start again as soon as possible.
+               if count:
+                       return 0
+
+               return self.settings.get_int("messages_interval", 10)
 
 
 managers.append(MessagesManager)
 
+class BugsUpdateManager(Manager):
+       @property
+       def timeout(self):
+               return self.settings.get_int("bugzilla_update_interval", 60)
+
+       def do(self):
+               # Get up to ten updates.
+               query = self.db.query("SELECT * FROM builds_bugs_updates \
+                       WHERE error = 'N' ORDER BY time LIMIT 10")
+
+               # XXX CHECK IF BZ IS ACTUALLY REACHABLE AND WORKING
+
+               for update in query:
+                       try:
+                               bug = self.pakfire.bugzilla.get_bug(update.bug_id)
+                               if not bug:
+                                       logging.error("Bug #%s does not exist." % update.bug_id)
+                                       continue
+
+                               # Set the changes.
+                               bug.set_status(update.status, update.resolution, update.comment)
+
+                       except Exception, e:
+                               # If there was an error, we save that and go on.
+                               self.db.execute("UPDATE builds_bugs_updates SET error = 'Y', error_msg = %s \
+                                       WHERE id = %s", "%s" % e, update.id)
+
+                       else:
+                               # Remove the update when it has been done successfully.
+                               self.db.execute("DELETE FROM builds_bugs_updates WHERE id = %s", update.id)
+
+
+managers.append(BugsUpdateManager)     
+
+class GitRepo(base.Object):
+       def __init__(self, pakfire, id, mode="normal"):
+               base.Object.__init__(self, pakfire)
+
+               assert mode in ("normal", "bare", "mirror")
+
+               # Get the source object.
+               self.source = sources.Source(pakfire, id)
+               self.mode = mode
+
+       @property
+       def path(self):
+               return os.path.join("/var/cache/pakfire/git-repos", self.source.identifier, self.mode)
+
+       def git(self, cmd, path=None):
+               if not path:
+                       path = self.path
+
+               cmd = "cd %s && git %s" % (path, cmd)
+
+               logging.debug("Running command: %s" % cmd)
+
+               return subprocess.check_output(["/bin/sh", "-c", cmd])
+
+       @property
+       def cloned(self):
+               """
+                       Say if the repository is already cloned.
+               """
+               return os.path.exists(self.path)
+
+       def clone(self):
+               if self.cloned:
+                       return
+
+               path = os.path.dirname(self.path)
+               repo = os.path.basename(self.path)
+
+               # Create the repository home directory if not exists.
+               if not os.path.exists(path):
+                       os.makedirs(path)
+
+               command = ["clone"]
+               if self.mode == "bare":
+                       command.append("--bare")
+               elif self.mode == "mirror":
+                       command.append("--mirror")
+
+               command.append(self.source.url)
+               command.append(repo)
+
+               # Clone the repository.
+               try:
+                       self.git(" ".join(command), path=path)
+               except Exception:
+                       shutil.rmtree(self.path)
+                       raise
+
+       def fetch(self):
+               # Make sure, the repository was already cloned.
+               if not self.cloned:
+                       self.clone()
+
+               self.git("fetch")
+
+       def rev_list(self, revision=None):
+               if not revision:
+                       if self.source.head_revision:
+                               revision = self.source.head_revision.revision
+                       else:
+                               revision = self.source.start_revision
+
+               command = "rev-list %s..%s" % (revision, self.source.branch)
+
+               # Get all merge commits.
+               merges = self.git("%s --merges" % command).splitlines()
+
+               revisions = []
+               for commit in self.git(command).splitlines():
+                       # Check if commit is a normal commit or merge commit.
+                       merge = commit in merges
+
+                       revisions.append((commit, merge))
+
+               return [r for r in reversed(revisions)]
+
+       def import_revisions(self):
+               # Get all pending revisions.
+               revisions = self.rev_list()
+
+               for revision, merge in revisions:
+                       # Actually import the revision.
+                       self._import_revision(revision, merge)
+
+       def _import_revision(self, revision, merge):
+               logging.debug("Going to import revision %s (merge: %s)." % (revision, merge))
+
+               rev_author    = self.git("log -1 --format=\"%%an <%%ae>\" %s" % revision)
+               rev_committer = self.git("log -1 --format=\"%%cn <%%ce>\" %s" % revision)
+               rev_subject   = self.git("log -1 --format=\"%%s\" %s" % revision)
+               rev_body      = self.git("log -1 --format=\"%%b\" %s" % revision)
+               rev_date      = self.git("log -1 --format=\"%%at\" %s" % revision)
+               rev_date      = datetime.datetime.utcfromtimestamp(float(rev_date))
+
+               # Convert strings properly. No idea why I have to do that.
+               #rev_author    = rev_author.decode("latin-1").strip()
+               #rev_committer = rev_committer.decode("latin-1").strip()
+               #rev_subject   = rev_subject.decode("latin-1").strip()
+               #rev_body      = rev_body.decode("latin-1").rstrip()
+
+               # Create a new commit object in the database
+               commit = sources.Commit.create(self.pakfire, self.source, revision,
+                       rev_author, rev_committer, rev_subject, rev_body, rev_date)
+
+       def checkout(self, revision, update=False):
+               for update in (0, 1):
+                       if update:
+                               self.fetch()
+
+                       try:
+                               self.git("checkout %s" % revision)
+
+                       except subprocess.CalledProcessError:
+                               if not update:
+                                       continue
+
+                               raise
+
+       def changed_files(self, revision):
+               files = self.git("diff --name-only %s^ %s" % (revision, revision))
+
+               return [os.path.join(self.path, f) for f in files.splitlines()]
+
+       def get_all_files(self):
+               files = self.git("ls-files")
+
+               return [os.path.join(self.path, f) for f in files.splitlines()]
+
 
 class SourceManager(Manager):
        @property
@@ -78,10 +338,12 @@ class SourceManager(Manager):
 
        def do(self):
                for source in self.sources.get_all():
+                       repo = GitRepo(self.pakfire, source.id, mode="mirror")
+
                        # If the repository is not yet cloned, we need to make a local
                        # clone to work with.
-                       if not source.is_cloned():
-                               source.clone()
+                       if not repo.cloned:
+                               repo.clone()
 
                                # If we have cloned a new repository, we exit to not get over
                                # the time treshold.
@@ -89,19 +351,219 @@ class SourceManager(Manager):
 
                        # Otherwise we just fetch updates.
                        else:
-                               source.fetch()
+                               repo.fetch()
 
                        # Import all new revisions.
-                       source.import_revisions()
-
-                       # If there are revisions left, we exit and want be called immediately
-                       # again.
-                       if source._git_rev_list():
-                               return 0
+                       repo.import_revisions()
 
 
 managers.append(SourceManager)
 
+class DistManager(Manager):
+       process = None
+
+       first_run = True
+
+       def get_next_commit(self):
+               commits = self.pakfire.sources.get_pending_commits(limit=1)
+
+               if not commits:
+                       return
+
+               return commits[0]
+
+       @property
+       def timeout(self):
+               # If there are commits standing in line, we try to restart as soon
+               # as possible.
+               if self.get_next_commit():
+                       return 0
+
+               # Otherwise we wait at least for a minute.
+               return 60
+
+       def do(self):
+               if self.first_run:
+                       self.first_run = False
+
+                       self.process = self.init_repos()
+
+               if self.process:
+                       # If the process is still running, we check back in a couple of
+                       # seconds.
+                       if self.process.is_alive():
+                               return 1
+
+                       # The process has finished its work. Clear everything up and
+                       # go on.
+                       self.commit = self.process = None
+
+               # Search for a new commit to proceed with.
+               self.commit = commit = self.get_next_commit()
+
+               # If no commit is there, we just wait for a minute.
+               if not commit:
+                       return 60
+
+               # Got a commit to process.
+               commit.state = "running"
+
+               logging.debug("Processing commit %s: %s" % (commit.revision, commit.subject))
+
+               # Get the repository of this commit.
+               repo = GitRepo(self.pakfire, commit.source_id)
+
+               # Make sure, it is checked out.
+               if not repo.cloned:
+                       repo.clone()
+
+               # Navigate to the right revision.
+               repo.checkout(commit.revision)
+
+               # Get all changed makefiles.
+               deleted_files = []
+               updated_files = []
+
+               for file in repo.changed_files(commit.revision):
+                       # Don't care about files that are not a makefile.
+                       if not file.endswith(".%s" % MAKEFILE_EXTENSION):
+                               continue
+
+                       if os.path.exists(file):
+                               updated_files.append(file)
+                       else:
+                               deleted_files.append(file)
+
+               self.process = self.fork(commit_id=commit.id, updated_files=updated_files,
+                       deleted_files=deleted_files)
+
+               return 1
+
+       def fork(self, source_id=None, commit_id=None, updated_files=[], deleted_files=[]):
+               # Create the Process object.
+               process = multiprocessing.Process(
+                       target=self._process,
+                       args=(source_id, commit_id, updated_files, deleted_files)
+               )
+
+               # The process is running in daemon mode so it will try to kill
+               # all child processes when exiting.
+               process.daemon = True
+
+               # Start the process.
+               process.start()
+               logging.debug("Started new process pid=%s." % process.pid)
+
+               return process
+
+       def init_repos(self):
+               # Create the Process object.
+               process = multiprocessing.Process(
+                       target=self._init_repos,
+               )
+
+               # The process is running in daemon mode so it will try to kill
+               # all child processes when exiting.
+               #process.daemon = True
+
+               # Start the process.
+               process.start()
+               logging.debug("Started new process pid=%s." % process.pid)
+
+               return process
+
+       def _init_repos(self):
+               _pakfire = main.Pakfire()
+               sources = _pakfire.sources.get_all()
+
+               for source in sources:
+                       if source.revision:
+                               continue
+
+                       repo = GitRepo(_pakfire, source.id)
+                       if not repo.cloned:
+                               repo.clone()
+
+                       files = repo.get_all_files()
+
+                       for file in files:
+                               if not file.endswith(".%s" % MAKEFILE_EXTENSION):
+                                       continue
+
+                               #files = [f for f in files if f.endswith(".%s" % MAKEFILE_EXTENSION)]
+
+                               process = self.fork(source_id=source.id, updated_files=[file,], deleted_files=[])
+
+                               while process.is_alive():
+                                       time.sleep(1)
+                                       continue
+
+       @staticmethod
+       def _process(source_id, commit_id, updated_files, deleted_files):
+               _pakfire = main.Pakfire()
+
+               commit = None
+               source = None
+
+               if commit_id:
+                       commit = _pakfire.sources.get_commit_by_id(commit_id)
+                       assert commit
+
+                       source = commit.source
+
+               if source_id and not source:
+                       source = _pakfire.sources.get_by_id(source_id)
+
+               assert source
+
+               if updated_files:
+                       # Create a temporary directory where to put all the files
+                       # that are generated here.
+                       pkg_dir = tempfile.mkdtemp()
+
+                       try:
+                               config = pakfire.config.Config(["general.conf",])
+                               config.parse(source.distro.get_config())
+
+                               p = pakfire.Pakfire(mode="server", config=config)
+
+                               pkgs = []
+                               for file in updated_files:
+                                       try:
+                                               pkg_file = p.dist(file, pkg_dir)
+                                               pkgs.append(pkg_file)
+                                       except:
+                                               raise
+
+                               # Import all packages in one swoop.
+                               for pkg in pkgs:
+                                       # Import the package file and create a build out of it.
+                                       builds.import_from_package(_pakfire, pkg,
+                                               distro=source.distro, commit=commit, type="release")
+
+                       except:
+                               if commit:
+                                       commit.state = "failed"
+
+                               raise
+
+                       finally:
+                               if os.path.exists(pkg_dir):
+                                       shutil.rmtree(pkg_dir)
+
+                       for file in deleted_files:
+                               # Determine the name of the package.
+                               name = os.path.basename(file)
+                               name = name[:len(MAKEFILE_EXTENSION) + 1]
+
+                               if commit:
+                                       commit.distro.delete_package(name)
+
+                       if commit:
+                               commit.state = "finished"
+
+
+managers.append(DistManager)
 
 class BuildsManager(Manager):
        @property
@@ -109,13 +571,21 @@ class BuildsManager(Manager):
                return self.settings.get_int("build_keepalive_interval", 900)
 
        def do(self):
-               for build in self.pakfire.builds.get_all_but_finished():
-                       logging.debug("Processing unfinished build: %s" % build.name)
-                       build.keepalive()
+               threshold = datetime.datetime.utcnow() - datetime.timedelta(hours=72)
 
+               for job in self.pakfire.jobs.get_next_iter(type="build", max_tries=9, states=["failed"]):
+                       if job.build.state == "broken":
+                               continue
 
-managers.append(BuildsManager)
+                       if not job.time_finished or job.time_finished > threshold:
+                               continue
 
+                       # Restart the job.
+                       logging.info("Restarting build job: %s" % job)
+                       job.set_state("new", log=False)
+
+
+managers.append(BuildsManager)
 
 class UploadsManager(Manager):
        @property
@@ -128,33 +598,327 @@ class UploadsManager(Manager):
 
 managers.append(UploadsManager)
 
-#class NotificationManager(Manager):
-#      @property
-#      def timeout(self):
-#              return Settings().get_int("notification_interval")
-#
-#      def do(self):
-#              tasks = self.tasks.get(type="notification", state="pending")
-#
-#              for task in tasks:
-#                      logging.debug("Running task %s" % task)
-#
-#                      task.run()
-#
-#managers.append(NotificationManager)
-#
-#
-#class RepositoryUpdateManager(Manager):
-#      @property
-#      def timeout(self):
-#              return Settings().get_int("repository_update_interval")
-#
-#      def do(self):
-#              tasks = self.tasks.get(type="repository_update", state="pending")
-#
-#              for task in tasks:
-#                      logging.debug("Running task %s" % task)
-#
-#                      task.run()
-#
-#managers.append(RepositoryUpdateManager)
+class RepositoryManager(Manager):
+       processes = []
+
+       @property
+       def timeout(self):
+               return self.settings.get_int("repository_update_interval", 600)
+
+       def do(self):
+               for process in self.processes[:]:
+                       # If the first process is running, everything is okay and
+                       # we'll have to wait.
+                       if process.is_alive():
+                               return 0.5
+
+                       # If the process has not been run, it is started now.
+                       if process.exitcode is None:
+                               logging.debug("Process %s is being started..." % process)
+
+                               process.start()
+                               return 1
+
+                       # If the process has stopped working we check why...
+                       else:
+                               if process.exitcode == 0:
+                                       logging.debug("Process %s has successfully finished." % process)
+
+                               elif process.exitcode > 0:
+                                       logging.error("Process %s has exited with code: %s" % \
+                                               (process, process.exitcode))
+
+                               elif process.exitcode < 0:
+                                       logging.error("Process %s has ended with signal %s" % \
+                                               (process, process.exitcode))
+
+                               # Remove process from process queue.
+                               self.processes.remove(process)
+
+                               # Start the loop again if there any processes left
+                               # that need to be started.
+                               if self.processes:
+                                       continue
+
+                               # Otherwise wait some time and start from the beginning.
+                               else:
+                                       return self.settings.get_int("repository_update_interval", 600)
+
+               for distro in self.pakfire.distros.get_all():
+                       for repo in distro.repositories:
+                               # Skip repostories that do not need an update at all.
+                               if not repo.needs_update():
+                                       logging.info("Repository %s - %s is already up to date." % (distro.name, repo.name))
+                                       continue
+
+                               # Create the Process object.
+                               process = multiprocessing.Process(
+                                       target=self._process,
+                                       args=(repo.id,)
+                               )
+
+                               # Run the process in daemon mode.
+                               process.daemon = True
+
+                               # Add the process to the process queue.
+                               self.processes.append(process)
+
+               # XXX DEVEL
+               #if self.processes:
+               #       return 0
+               #else:
+               #       return
+
+               # Create dependency updater after all repositories have been
+               # updated.
+               #jobs = self.pakfire.jobs.get_next_iter(states=["new", "dependency_error", "failed",])
+
+               #for job in jobs:
+               #       process = multiprocessing.Process(
+               #               target=self._dependency_update_process,
+               #               args=(job.id,)
+               #       )
+               #       process.daemon = True
+               #       self.processes.append(process)
+
+               # Start again as soon as possible.
+               #if self.processes:
+               #       return 0
+
+       @staticmethod
+       def _process(repo_id):
+               _pakfire = main.Pakfire()
+
+               repo = _pakfire.repos.get_by_id(repo_id)
+               assert repo
+
+               logging.info("Going to update repository %s..." % repo.name)
+
+               # Update the timestamp when we started at last.
+               repo.updated()
+
+               # Find out for which arches this repository is created.
+               arches = repo.arches
+
+               # Add the source repository.
+               arches.append(_pakfire.arches.get_by_name("src"))
+
+               for arch in arches:
+                       changed = False
+
+                       # Get all packages that are to be included in this repository.
+                       pkgs = repo.get_packages(arch)
+
+                       # Log all packages.
+                       for pkg in pkgs:
+                               logging.info("  %s" % pkg)
+
+                       repo_path = os.path.join(
+                               REPOS_DIR,
+                               repo.distro.identifier,
+                               repo.identifier,
+                               arch.name
+                       )
+
+                       if not os.path.exists(repo_path):
+                               os.makedirs(repo_path)
+
+                       source_files = []
+                       remove_files = []
+
+                       for filename in os.listdir(repo_path):
+                               path = os.path.join(repo_path, filename)
+
+                               if not os.path.isfile(path):
+                                       continue
+
+                               remove_files.append(path)
+
+                       for pkg in pkgs:
+                               filename = os.path.basename(pkg.path)
+
+                               source_file = os.path.join(PACKAGES_DIR, pkg.path)
+                               target_file = os.path.join(repo_path, filename)
+
+                               # Do not add duplicate files twice.
+                               if source_file in source_files:
+                                       #logging.warning("Duplicate file detected: %s" % source_file)
+                                       continue
+
+                               source_files.append(source_file)
+
+                               try:
+                                       remove_files.remove(target_file)
+                               except ValueError:
+                                       changed = True
+
+                       if remove_files:
+                               changed = True
+
+                       # If nothing in the repository data has changed, there
+                       # is nothing to do.
+                       if changed:
+                               logging.info("The repository has updates...")
+                       else:
+                               logging.info("Nothing to update.")
+                               continue
+
+                       # Find the key to sign the package.
+                       key_id = None
+                       if repo.key:
+                               key_id = repo.key.fingerprint
+
+                       # Create package index.
+                       pakfire.api.repo_create(repo_path, source_files,
+                               name="%s - %s.%s" % (repo.distro.name, repo.name, arch.name),
+                               key_id=key_id, type=arch.build_type, mode="server")
+
+                       # Remove files afterwards.
+                       for file in remove_files:
+                               file = os.path.join(repo_path, file)
+
+                               try:
+                                       os.remove(file)
+                               except OSError:
+                                       logging.warning("Could not remove %s." % file)
+
+       @staticmethod
+       def _dependency_update_process(job_id):
+               _pakfire = main.Pakfire()
+
+               job = _pakfire.jobs.get_by_id(job_id)
+               assert job
+
+               job.resolvdep()
+
+
+managers.append(RepositoryManager)
+
+class TestManager(Manager):
+       @property
+       def timeout(self):
+               return 300
+
+       @property
+       def test_threshold(self):
+               threshold_days = self.pakfire.settings.get_int("test_threshold_days", 14)
+
+               return datetime.datetime.utcnow() - datetime.timedelta(days=threshold_days)
+
+       def do(self):
+               max_queue_length = self.pakfire.settings.get_int("test_queue_limit", 10)
+
+               # Get a list with all feasible architectures.
+               arches = self.pakfire.arches.get_all()
+               noarch = self.pakfire.arches.get_by_name("noarch")
+               if noarch:
+                       arches.append(noarch)
+
+               for arch in arches:
+                       # Get the job queue for each architecture.
+                       queue = self.pakfire.jobs.get_next(arches=[arch,])
+
+                       # Skip adding new jobs if there are more too many jobs in the queue.
+                       limit = max_queue_length - len(queue)
+                       if limit <= 0:
+                               logging.debug("Already too many jobs in queue of %s to create tests." % arch.name)
+                               continue
+
+                       # Get a list of builds, with potentially need a test build.
+                       # Randomize the output and do not return more jobs than we are
+                       # allowed to put into the build queue.
+                       builds = self.pakfire.builds.needs_test(self.test_threshold,
+                               arch=arch, limit=limit, randomize=True)
+
+                       if not builds:
+                               logging.debug("No builds needs a test for %s." % arch.name)
+                               continue
+
+                       # Search for the job with the right architecture in each
+                       # build and schedule a test job.
+                       for build in builds:
+                               for job in build.jobs:
+                                       if not job.arch == arch:
+                                               continue
+
+                                       job.schedule("test")
+                                       break
+
+
+managers.append(TestManager)
+
+
+class DependencyChecker(Manager):
+       processes = []
+
+       @property
+       def timeout(self):
+               return self.settings.get_int("dependency_checker_interval", 30)
+
+       def do(self):
+               if self.processes:
+                       return self.wait_for_processes()
+
+               return self.search_jobs()
+
+       def search_jobs(self):
+               # Find the jobs who need the update the most.
+               job_ids = []
+
+               # Get all jobs in new state, no matter how many these are.
+               query = self.db.query("SELECT id FROM jobs WHERE state = 'new'")
+               for job in query:
+                       job_ids.append(job.id)
+
+               # If there are no jobs to check, search deeper.
+               if not job_ids:
+                       query = self.db.query("SELECT id FROM jobs \
+                               WHERE state = 'dependency_error' AND time_finished < DATE_SUB(NOW(), INTERVAL 5 MINUTE) \
+                               ORDER BY time_finished LIMIT 50")
+
+                       for job in query:
+                               job_ids.append(job.id)
+
+               # Create a subprocess for every single job we have to work on.
+               for job_id in job_ids:
+                       process = multiprocessing.Process(
+                               target=self._process, args=(job_id,)
+                       )
+                       process.daemon = True
+                       self.processes.append(process)
+
+               # Start immediately again if we have running subprocesses.
+               if self.processes:              
+                       return 0
+
+       @staticmethod
+       def _process(job_id):
+               # Create a new pakfire instance.
+               _pakfire = main.Pakfire()
+
+               # Get the build job we are working on.
+               job = _pakfire.jobs.get_by_id(job_id)
+               assert job
+
+               # Check if the job status has changed in the meanwhile.
+               if not job.state in ("new", "dependency_error", "failed"):
+                       logging.warning("Job status has already changed: %s - %s" % (job.name, job.state))
+                       return
+
+               # Resolve the dependencies.
+               job.resolvdep()
+
+
+managers.append(DependencyChecker)
+
+
+class DeleteManager(Manager):
+       @property
+       def timeout(self):
+               return 300
+
+       def do(self):
+               self.pakfire.cleanup_files()
+
+
+managers.append(DeleteManager)
index c37a0760c84da131effb56441a3cbc0728ff6f53..8572fb26eb5e2dbb502fb8c2394ffcb3246ffff3 100644 (file)
@@ -70,22 +70,6 @@ class Messages(base.Object):
                        # Add the message to the queue that it is sent.
                        self.add(recipient, _subject, _body)
 
-       def send_messages(self, limit=10):
-               i = 0
-               for msg in self.get_all(limit=10):
-                       try:
-                               self.send_msg(msg)
-                       except Exception, e:
-                               logging.debug("There was an error sending mail: %s" % e)
-                               raise
-                       else:
-                               # If everything was okay, we can delete the message in the database.
-                               self.delete(msg.id)
-                               i += 1
-
-               logging.debug("Successfully sent %s message(s). %s still in queue." \
-                       % (i, self.count))
-
        @staticmethod
        def send_msg(msg):
                if not msg.to:
diff --git a/backend/mirrors.py b/backend/mirrors.py
new file mode 100644 (file)
index 0000000..fb05f01
--- /dev/null
@@ -0,0 +1,368 @@
+#!/usr/bin/python
+
+import logging
+import math
+import socket
+import tornado.database
+
+import base
+import logs
+
+class GeoIP(object):
+       db = None
+
+       def __init__(self, pakfire, server, name, user):
+               self.pakfire = pakfire
+
+               if self.db is None:
+                       self.db = tornado.database.Connection(
+                               server, name, user=user
+                       )
+
+                       logging.info("Creating database connection to GeoIP database.")
+
+       @property
+       def cache(self):
+               return self.pakfire.cache
+
+       def __encode_ip(self, addr):
+               # We get a tuple if there were proxy headers.
+               addr = addr.split(", ")
+               if addr:
+                       addr = addr[-1]
+
+               # ip is calculated as described in http://ipinfodb.com/ip_database.php
+               a1, a2, a3, a4 = addr.split(".")
+
+               return int(((int(a1) * 256 + int(a2)) * 256 + int(a3)) * 256 + int(a4) + 100)
+
+       def get_all(self, addr):
+               addr = self.__encode_ip(addr)
+
+               mem_id = "geoip-all-%s" % addr
+               ret = self.cache.get(mem_id)
+
+               if not ret:
+                       ret = self.db.get("SELECT * FROM locations \
+                               JOIN ip_group_city ON ip_group_city.location = locations.id \
+                               WHERE ip_group_city.ip_start <= %s \
+                               ORDER BY ip_group_city.ip_start DESC LIMIT 1", addr)
+
+                       self.cache.set(mem_id, ret, 3600)
+
+               # If location was not determinable
+               if ret.latitude == 0 and ret.longitude == 0:
+                       return None
+
+               return ret
+
+
+class Mirrors(base.Object):
+       def get_all(self):
+               mirrors = []
+
+               for mirror in self.db.query("SELECT id FROM mirrors \
+                               WHERE NOT status = 'deleted' ORDER BY hostname"):
+                       mirror = Mirror(self.pakfire, mirror.id)
+                       mirrors.append(mirror)
+
+               return mirrors
+
+       def count(self, status=None):
+               query = "SELECT COUNT(*) AS count FROM mirrors"
+               args  = []
+
+               if status:
+                       query += " WHERE status = %s"
+                       args.append(status)
+
+               query = self.db.get(query, *args)
+
+               return query.count
+
+       def get_random(self, limit=None):
+               query = "SELECT id FROM mirrors WHERE status = 'enabled' ORDER BY RAND()"
+               args  = []
+
+               if limit:
+                       query += " LIMIT %s"
+                       args.append(limit)
+
+               mirrors = []
+               for mirror in self.db.query(query, *args):
+                       mirror = Mirror(self.pakfire, mirror.id)
+                       mirrors.append(mirror)
+
+               return mirrors
+
+       def get_by_id(self, id):
+               mirror = self.db.get("SELECT id FROM mirrors WHERE id = %s", id)
+               if not mirror:
+                       return
+
+               return Mirror(self.pakfire, mirror.id)
+
+       def get_by_hostname(self, hostname):
+               mirror = self.db.get("SELECT id FROM mirrors WHERE NOT status = 'deleted' \
+                       AND hostname = %s", hostname)
+
+               if not mirror:
+                       return
+
+               return Mirror(self.pakfire, mirror.id)
+
+       def get_for_location(self, addr):
+               distance = 10
+
+               # Get all mirrors in here.
+               _mirrors = self.get_all()
+
+               mirrors = []
+               while len(mirrors) <= 2 and distance <= 270:
+                       for mirror in _mirrors:
+                               if not mirror.enabled:
+                                       continue
+
+                               if mirror in mirrors:
+                                       continue
+
+                               # Cannot calc the distance for mirrors when their location is unknown.
+                               if mirror.location is None:
+                                       continue
+
+                               if mirror.distance_to(addr) <= distance:
+                                       mirrors.append(mirror)
+
+                       distance *= 1.2
+
+               return mirrors
+
+       def get_history(self, limit=None, offset=None, mirror=None, user=None):
+               query = "SELECT * FROM mirrors_history"
+               args  = []
+
+               conditions = []
+
+               if mirror:
+                       conditions.append("mirror_id = %s")
+                       args.append(mirror.id)
+
+               if user:
+                       conditions.append("user_id = %s")
+                       args.append(user.id)
+
+               if conditions:
+                       query += " WHERE %s" % " AND ".join(conditions)
+
+               query += " ORDER BY time DESC"
+
+               if limit:
+                       if offset:
+                               query += " LIMIT %s,%s"
+                               args  += [offset, limit,]
+                       else:
+                               query += " LIMIT %s"
+                               args  += [limit,]
+
+               entries = []
+               for entry in self.db.query(query, *args):
+                       entry = logs.MirrorLogEntry(self.pakfire, entry)
+                       entries.append(entry)
+
+               return entries
+
+
+class Mirror(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
+
+               self.id = id
+
+               # Cache.
+               self._data = None
+               self._location = None
+
+       def __cmp__(self, other):
+               return cmp(self.id, other.id)
+
+       @classmethod
+       def create(cls, pakfire, hostname, path="", owner=None, contact=None, user=None):
+               id = pakfire.db.execute("INSERT INTO mirrors(hostname, path, owner, contact) \
+                       VALUES(%s, %s, %s, %s)", hostname, path, owner, contact)
+
+               mirror = cls(pakfire, id)
+               mirror.log("created", user=user)
+
+               return mirror
+
+       def log(self, action, user=None):
+               user_id = None
+               if user:
+                       user_id = user.id
+
+               self.db.execute("INSERT INTO mirrors_history(mirror_id, action, user_id, time) \
+                       VALUES(%s, %s, %s, NOW())", self.id, action, user_id)
+
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = \
+                               self.db.get("SELECT * FROM mirrors WHERE id = %s", self.id)
+
+               return self._data
+
+       def set_status(self, status, user=None):
+               assert status in ("enabled", "disabled", "deleted")
+
+               if self.status == status:
+                       return
+
+               self.db.execute("UPDATE mirrors SET status = %s WHERE id = %s",
+                       status, self.id)
+
+               if self._data:
+                       self._data["status"] = status
+
+               # Log the status change.
+               self.log(status, user=user)
+
+       def set_hostname(self, hostname):
+               if self.hostname == hostname:
+                       return
+
+               self.db.execute("UPDATE mirrors SET hostname = %s WHERE id = %s",
+                       hostname, self.id)
+
+               if self._data:
+                       self._data["hostname"] = hostname
+
+       hostname = property(lambda self: self.data.hostname, set_hostname)
+
+       @property
+       def path(self):
+               return self.data.path
+
+       def set_path(self, path):
+               if self.path == path:
+                       return
+
+               self.db.execute("UPDATE mirrors SET path = %s WHERE id = %s",
+                       path, self.id)
+
+               if self._data:
+                       self._data["path"] = path
+
+       path = property(lambda self: self.data.path, set_path)
+
+       @property
+       def url(self):
+               ret = "http://%s" % self.hostname
+
+               if self.path:
+                       path = self.path
+
+                       if not self.path.startswith("/"):
+                               path = "/%s" % path
+
+                       if self.path.endswith("/"):
+                               path = path[:-1]
+
+                       ret += path
+
+               return ret
+
+       def set_owner(self, owner):
+               if self.owner == owner:
+                       return
+
+               self.db.execute("UPDATE mirrors SET owner = %s WHERE id = %s",
+                       owner, self.id)
+
+               if self._data:
+                       self._data["owner"] = owner
+
+       owner = property(lambda self: self.data.owner or "", set_owner)
+
+       def set_contact(self, contact):
+               if self.contact == contact:
+                       return
+
+               self.db.execute("UPDATE mirrors SET contact = %s WHERE id = %s",
+                       contact, self.id)
+
+               if self._data:
+                       self._data["contact"] = contact
+
+       contact = property(lambda self: self.data.contact or "", set_contact)
+
+       @property
+       def status(self):
+               return self.data.status
+
+       @property
+       def enabled(self):
+               return self.status == "enabled"
+
+       @property
+       def check_status(self):
+               return self.data.check_status
+
+       @property
+       def last_check(self):
+               return self.data.last_check
+
+       @property
+       def address(self):
+               return socket.gethostbyname(self.hostname)
+
+       @property
+       def location(self):
+               if self._location is None:
+                       self._location = self.geoip.get_all(self.address)
+
+               return self._location
+
+       @property
+       def country_code(self):
+               if self.location:
+                       return self.location.country_code
+                       
+               return "UNKNOWN"
+
+       @property
+       def latitude(self):
+               if self.location:
+                       return self.location.latitude
+
+               return 0
+
+       @property
+       def longitude(self):
+               if self.location:
+                       return self.location.longitude
+
+               return 0
+
+       def distance_to(self, addr):
+               location = self.geoip.get_all(addr)
+               if not location:
+                       return 0
+
+               #if location.country_code.lower() in self.prefer_for_countries:
+               #       return 0
+
+               distance_vector = (
+                       self.latitude - location.latitude,
+                       self.longitude - location.longitude
+               )
+
+               distance = 0
+               for i in distance_vector:
+                       distance += i**2
+
+               return math.sqrt(distance)
+
+       def get_history(self, *args, **kwargs):
+               kwargs["mirror"] = self
+
+               return self.pakfire.mirrors.get_history(*args, **kwargs)
index b88cb185f510e0bb8d4290939729b4222d474b59..8e730c3ac59f0def757d69b4d87446e6964ba878 100644 (file)
@@ -3,24 +3,86 @@
 from __future__ import division
 
 import hashlib
+import os
+import re
 import tarfile
 
-def friendly_size(s):
-       units = ("B", "K", "M", "G", "T")
+from tornado.escape import xhtml_escape
+
+from constants import *
+
+def format_size(s):
+       units = ("B", "k", "M", "G", "T")
 
        i = 0
        while s >= 1024 and i < len(units):
                s /= 1024
                i += 1
 
-       return "%.1f %s" % (s, units[i])
+       return "%d%s" % (round(s), units[i])
 
-def calc_hash1(filename):
-       f = open(filename)
+def friendly_time(t):
+       if not t:
+               return "--:--"
+
+       t = int(t)
+       ret = []
+
+       if t >= 604800:
+               ret.append("%s w" % (t // 604800))
+               t %= 604800
+
+       if t >= 86400:
+               ret.append("%s d" % (t // 38400))
+               t %= 86400
+
+       if t >= 3600:
+               ret.append("%s h" % (t // 3600))
+               t %= 3600
+
+       if t >= 60:
+               ret.append("%s m" % (t // 60))
+               t %= 60
+
+       if t:
+               ret.append("%s s" % t)
+
+       return " ".join(ret)
+
+def format_email(email):
+       m = re.match(r"(.*) <(.*)>", email)
+       if m:
+               fmt = {
+                       "name" : xhtml_escape(m.group(1)),
+                       "mail" : xhtml_escape(m.group(2)),
+               }
+       else:
+               fmt = {
+                       "name" : xhtml_escape(email),
+                       "mail" : xhtml_escape(email),
+               }
+
+       return """<a class="email" href="mailto:%(mail)s">%(name)s</a>""" % fmt
+
+def format_filemode(filetype, filemode):
+       if filetype == 2:
+               prefix = "l"
+       elif filetype == 5:
+               prefix = "d"
+       else:
+               prefix = "-"
+
+       return prefix + tarfile.filemode(filemode)[1:]
+
+def calc_hash(filename, algo="sha512"):
+       assert algo in hashlib.algorithms
+       assert os.path.exists(filename)
+
+       f = open(filename, "rb")
+       h = hashlib.new(algo)
 
-       h = hashlib.sha1()
        while True:
-               buf = f.read(1024)
+               buf = f.read(BUFFER_SIZE)
                if not buf:
                        break
 
@@ -30,6 +92,10 @@ def calc_hash1(filename):
 
        return h.hexdigest()
 
+def calc_hash1(filename):
+       # XXX COMPAT FUNCTION
+       # to be removed
+       return calc_hash(filename, "sha1")
 
 def guess_filetype(filename):
        # XXX very cheap check. Need to do better here.
index f7491676dbb20516f9566ebd897cd617221a8d62..cd77616a072054624efda9ef56847c09f82d98c8 100644 (file)
 #!/usr/bin/python
 
+import datetime
+import logging
 import os
 import shutil
-import time
-import urlparse
-import uuid
 
+import pakfire
+import pakfire.packages as packages
+
+import arches
 import base
-import build
+import builds
+import database
+import misc
+import sources
 
 from constants import *
 
 class Packages(base.Object):
-       def get_by_id(self, id):
-               return Package(self.pakfire, id)
-
-       def get_all_names(self):
-               names = self.db.query("SELECT DISTINCT name FROM packages ORDER BY name")
-
-               return [n.name for n in names]
-
-       def get_by_name(self, name):
-               pkgs = self.db.query("SELECT id FROM packages WHERE name = %s ORDER BY id ASC", name)
+       def get_all_names(self, public=None, user=None, states=None):
+               query = "SELECT DISTINCT name, summary FROM packages \
+                       JOIN builds ON builds.pkg_id = packages.id \
+                       WHERE packages.type = 'source'"
+
+               conditions = []
+               args = []
+
+               if public in (True, False):
+                       if public is True:
+                               public = "Y"
+                       elif public is False:
+                               public = "N"
+
+                       conditions.append("builds.public = %s")
+                       args.append(public)
+
+               if user and not user.is_admin():
+                       conditions.append("builds.owner_id = %s")
+                       args.append(user.id)
+
+               if states:
+                       for state in states:
+                               conditions.append("builds.state = %s")
+                               args.append(state)
+
+               if conditions:
+                       query += " AND (%s)" % " OR ".join(conditions)
+               
+               query += " ORDER BY packages.name"
+
+               return [(n.name, n.summary) for n in self.db.query(query, *args)]
+
+       def get_by_uuid(self, uuid):
+               pkg = self.db.get("SELECT id FROM packages WHERE uuid = %s LIMIT 1", uuid)
+               if not pkg:
+                       return
+
+               return Package(self.pakfire, pkg.id)
+
+       def search(self, pattern, limit=None):
+               """
+                       Searches for packages that do match the query.
+
+                       This function does not work for UUIDs or filenames.
+               """
+               query = "SELECT id FROM packages WHERE type = 'source' AND \
+                       (name LIKE %s OR MATCH(name, summary, description) AGAINST(%s)) \
+                       GROUP BY name"
+
+               pkgs = []
+               for pkg in self.db.query(query, pattern, pattern):
+                       pkg = Package(self.pakfire, pkg.id)
+                       pkgs.append(pkg)
+
+                       if limit and len(pkgs) >= limit:
+                               break
 
-               return [Package(self.pakfire, pkg.id) for pkg in pkgs]
+               return pkgs
 
-       find_by_name = get_by_name
+       def search_by_filename(self, filename, limit=None):
+               query = "SELECT filelists.* FROM filelists \
+                       JOIN packages ON filelists.pkg_id = packages.id \
+                       WHERE filelists.name = %s ORDER BY packages.build_time DESC"
+               args = [filename,]
 
-       def search(self, query):
-               # Search for an exact match
-               pkgs = self.db.query("SELECT DISTINCT name, summary FROM packages"
-                       " WHERE name = %s LIMIT 1", query)
+               if limit:
+                       query += " LIMIT %s"
+                       args.append(limit)
 
-               if not pkgs:
-                       query = "%%%s%%" % query
-                       pkgs = self.db.query("SELECT DISTINCT name, summary FROM packages"
-                       " WHERE name LIKE %s OR summary LIKE %s OR description LIKE %s"
-                       " ORDER BY name", query, query, query)
+               files = []
+               for result in self.db.query(query, *args):
+                       pkg = Package(self.pakfire, result.pkg_id)
+                       files.append((pkg, result))
 
-               return pkgs
+               return files
 
-       def get_by_tuple(self, name, epoch, version, release):
-               pkg = self.db.get("""SELECT id FROM packages WHERE name = %s AND
-                       epoch = %s AND version = %s AND `release` = %s LIMIT 1""",
-                       name, epoch, version, release)
+       def get_avg_build_times(self, name):
+               query = "SELECT jobs.arch_id AS arch_id, \
+                               AVG(jobs.time_finished - jobs.time_started) AS build_time \
+                       FROM jobs \
+                               JOIN builds ON jobs.build_id = builds.id \
+                               JOIN packages ON builds.pkg_id = packages.id \
+                       WHERE packages.name = %s \
+                               AND jobs.state = 'finished' \
+                               AND NOT jobs.time_started IS NULL \
+                               AND NOT jobs.time_finished IS NULL \
+                       GROUP BY jobs.arch_id"
 
-               if pkg:
-                       return Package(self.pakfire, pkg.id)
+               ret = []
+               for row in self.db.query(query, name):
+                       arch = arches.Arch(self.pakfire, row.arch_id)
+                       ret.append((arch, row.build_time))
 
-       def get_with_file_by_uuid(self, uuid):
-               file = self.db.get("SELECT id, type, pkg_id FROM package_files WHERE uuid = %s LIMIT 1", uuid)
+               # Sorts the list by the priority of the arches.
+               ret.sort()
 
-               if not file:
-                       return None, None
+               return ret
 
-               pkg = Package(self.pakfire, file.pkg_id)
 
-               if file.type == SourcePackageFile.type:
-                       file = SourcePackageFile(self.pakfire, file.id)
+class Package(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
 
-               elif file.type == BinaryPackageFile.type:
-                       file = BinaryPackageFile(self.pakfire, file.id)
+               # The ID of the package.
+               self.id = id
 
-               return pkg, file
+               # Cache.
+               self._data = None
+               self._deps = None
+               self._arch = None
+               self._filelist = None
+               self._job = None
+               self._commit = None
+               self._properties = None
+               self._maintainer = None
 
-       def get_comments(self, limit=50):
-               comments = self.db.query("""SELECT * FROM package_comments
-                       ORDER BY time DESC LIMIT %s""", limit)
+       def __repr__(self):
+               return "<%s %s>" % (self.__class__.__name__, self.friendly_name)
 
-               return comments
+       def __cmp__(self, other):
+               return pakfire.util.version_compare(self.pakfire,
+                       self.friendly_name, other.friendly_name)
 
+       @classmethod
+       def open(cls, pakfire, path):
+               # Just check if the file really does exist.
+               assert os.path.exists(path)
+
+               file = packages.open(None, None, path)
+
+               # Get architecture from the database.
+               arch = pakfire.arches.get_by_name(file.arch)
+               assert arch, "Unknown architecture: %s" % file.arch
+
+               hash_sha512 = misc.calc_hash(path, "sha512")
+               assert hash_sha512
+
+               query = [
+                       ("name",        file.name),
+                       ("epoch",       file.epoch),
+                       ("version",     file.version),
+                       ("release",     file.release),
+                       ("type",        file.type),
+                       ("arch",        arch.id),
+
+                       ("groups",      " ".join(file.groups)),
+                       ("maintainer",  file.maintainer),
+                       ("license",     file.license),
+                       ("url",         file.url),
+                       ("summary",     file.summary),
+                       ("description", file.description),
+                       ("size",        file.size),
+                       ("uuid",        file.uuid),
+
+                       # Build information.
+                       ("build_id",    file.build_id),
+                       ("build_host",  file.build_host),
+                       ("build_time",  datetime.datetime.utcfromtimestamp(file.build_time)),
+
+                       # File "metadata".
+                       ("path",        path),
+                       ("filesize",    os.path.getsize(path)),
+                       ("hash_sha512", hash_sha512),
+               ]
+
+               if file.type == "source":
+                       query.append(("supported_arches", file.supported_arches))
+
+               keys = []
+               vals = []
+               for key, val in query:
+                       keys.append("`%s`" % key)
+                       vals.append(val)
+
+               _query = "INSERT INTO packages(%s)" % ", ".join(keys)
+               _query += " VALUES(%s)" % ", ".join("%s" for v in vals)
+
+               # Create package entry in the database.
+               id = pakfire.db.execute(_query, *vals)
+
+               # Dependency information.
+               deps = []
+               for d in file.prerequires:
+                       deps.append((id, "prerequires", d))
+
+               for d in file.requires:
+                       deps.append((id, "requires", d))
+
+               for d in file.provides:
+                       deps.append((id, "provides", d))
+
+               for d in file.conflicts:
+                       deps.append((id, "conflicts", d))
+
+               for d in file.obsoletes:
+                       deps.append((id, "obsoletes", d))
+
+               if deps:
+                       pakfire.db.executemany("INSERT INTO packages_deps(pkg_id, type, what) \
+                               VALUES(%s, %s, %s)", deps)
+
+               # Add all files to filelists table.
+               filelist = []
+               for f in file.filelist:
+                       if f.config:
+                               config = "Y"
+                       else:
+                               config = "N"
 
-class Package(base.Object):
-       """
-               This class represents a package (like source package) that is passed to
-               the buildsystem.
+                       # Convert mtime to integer.
+                       try:
+                               mtime = int(f.mtime)
+                       except ValueError:
+                               mtime = 0
 
-               New objects of this are created by the new() method.
-       """
+                       filelist.append((id, f.name, f.size, f.hash1, f.type, config, f.mode,
+                               f.user, f.group, datetime.datetime.utcfromtimestamp(mtime),
+                               f.capabilities))
 
-       def __init__(self, pakfire, id):
-               base.Object.__init__(self, pakfire)
-               self.id = id
+               pakfire.db.executemany("INSERT INTO filelists(pkg_id, name, size, hash_sha512, \
+                       type, config, mode, user, `group`, mtime, capabilities) \
+                       VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", filelist)
 
-               self._data = self.db.get("SELECT * FROM packages WHERE id = %s", self.id)
+               # Return the newly created object.
+               return cls(pakfire, id)
 
-       def __cmp__(self, other):
-               # Order alphabetically.
-               return cmp(self.friendly_name, other.friendly_name)
+       def delete(self):
+               self.db.execute("INSERT INTO queue_delete(path) VALUES(%s)", self.path)
 
-       @classmethod
-       def new(cls, pakfire, file, build):
-               # Check if the package does already exist in the database.
-               pkg = pakfire.packages.get_by_tuple(file.name, file.epoch, file.version,
-                       file.release)
+               # Delete all files from the filelist.
+               self.db.execute("DELETE FROM filelists WHERE pkg_id = %s", self.id)
 
-               if pkg:
-                       return pkg
+               # Delete the package.
+               self.db.execute("DELETE FROM packages WHERE id = %s", self.id)
 
-               id = pakfire.db.execute("INSERT INTO packages(name, epoch, version,"
-                       " `release`, groups, maintainer, license, url, summary, description,"
-                       " supported_arches, source_build) VALUES(%s, %s, %s, %s, %s, %s, %s, %s,"
-                       " %s, %s, %s, %s)", file.name, file.epoch, file.version, file.release,
-                       " ".join(file.groups), file.maintainer, file.license, file.url,
-                       file.summary, file.description, file.supported_arches, build.id)
+               # Remove cached data.
+               self._data = {}
 
-               pkg = cls(pakfire, id)
-               pkg.add_file(file, build)
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = \
+                               self.db.get("SELECT * FROM packages WHERE id = %s", self.id)
+                       assert self._data, "Cannot fetch package %s: %s" % (self.id, self._data)
 
-               # Create all needed build jobs.
-               pkg.create_builds()
+               return self._data
 
-               return pkg
+       @property
+       def uuid(self):
+               return self.data.uuid
 
        @property
        def name(self):
-               return self._data.get("name")
+               return self.data.name
 
        @property
        def epoch(self):
-               return self._data.get("epoch")
+               return self.data.epoch
 
        @property
        def version(self):
-               return self._data.get("version")
+               return self.data.version
 
        @property
        def release(self):
-               return self._data.get("release")
+               return self.data.release
+
+       @property
+       def arch(self):
+               if self._arch is None:
+                       self._arch = self.pakfire.arches.get_by_id(self.data.arch)
+                       assert self._arch
+
+               return self._arch
+
+       @property
+       def type(self):
+               return self.data.type
 
        @property
        def friendly_name(self):
-               return "%s-%s" % (self.name, self.friendly_version)
+               return "%s-%s.%s" % (self.name, self.friendly_version, self.arch.name)
 
        @property
        def friendly_version(self):
@@ -143,397 +312,322 @@ class Package(base.Object):
                return s
 
        @property
-       def distro(self):
-               return self.source.distro
-
-       def get_state(self):
-               return self._data.get("state")
+       def groups(self):
+               return self.data.groups.split()
 
-       def set_state(self, state):
-               self.db.execute("UPDATE packages SET state = %s WHERE id = %s", state, self.id)
-               self._data["state"] = state
+       @property
+       def maintainer(self):
+               if self._maintainer is None:
+                       self._maintainer = self.data.maintainer
 
-               if state == "finished":
-                       # Add package to all comprehensive repositories.
-                       for repo in self.distro.comprehensive_repositories:
-                               self.add_to_repository(repo)
+                       # Search if there is a user account for this person.
+                       user = self.pakfire.users.find_maintainer(self._maintainer)
+                       if user:
+                               self._maintainer = user
 
-       state = property(get_state, set_state)
+               return self._maintainer
 
        @property
-       def summary(self):
-               return self._data.get("summary")
+       def license(self):
+               return self.data.license
 
        @property
-       def description(self):
-               return self._data.get("description")
+       def url(self):
+               return self.data.url
 
        @property
-       def groups(self):
-               return self._data.get("groups")
+       def summary(self):
+               return self.data.summary
 
        @property
-       def url(self):
-               return self._data.get("url")
+       def description(self):
+               return self.data.description
 
        @property
-       def maintainer(self):
-               return self._data.get("maintainer")
+       def supported_arches(self):
+               return self.data.supported_arches
 
        @property
-       def license(self):
-               return self._data.get("license")
+       def size(self):
+               return self.data.size
 
        @property
-       def source(self):
-               return self.source_build.source
-
-       def get_files(self, type=None):
-               files = []
+       def deps(self):
+               if self._deps is None:
+                       query = self.db.query("SELECT type, what FROM packages_deps WHERE pkg_id = %s", self.id)
 
-               query = "SELECT id, type FROM package_files WHERE pkg_id = %s"
-               if type:
-                       query += " AND type = '%s'" % type
+                       self._deps = []
+                       for row in query:
+                               self._deps.append((row.type, row.what))
 
-               for p in self.db.query(query, self.id):
-                       for p_class in (SourcePackageFile, BinaryPackageFile, LogFile):
-                               if p.type == p_class.type:
-                                       p = p_class(self.pakfire, p.id)
-                                       break
-                       else:
-                               continue
-
-                       files.append(p)
-
-               return files
+               return self._deps
 
        @property
-       def packagefiles(self):
-               return [f for f in self.get_files() if isinstance(f, PackageFile)]
+       def prerequires(self):
+               return [d[1] for d in self.deps if d[0] == "prerequires"]
 
        @property
-       def logfiles(self):
-               return [f for f in self.get_files() if isinstance(f, LogFile)]
+       def requires(self):
+               return [d[1] for d in self.deps if d[0] == "requires"]
 
        @property
-       def sourcefile(self):
-               sourcefiles = [f for f in self.get_files() if isinstance(f, SourcePackageFile)]
-
-               assert len(sourcefiles) <= 1
-
-               if sourcefiles:
-                       return sourcefiles[0]
+       def provides(self):
+               return [d[1] for d in self.deps if d[0] == "provides"]
 
        @property
-       def log(self):
-               return self.db.query("SELECT * FROM log WHERE pkg_id = %s AND"
-                       " build_id IS NULL ORDER BY time DESC", self.id)
-
-       def add_file(self, pkg, build):
-               path = os.path.join(
-                       self.name,
-                       "%s-%s-%s" % (self.epoch, self.version, self.release),
-                       build.arch,
-                       os.path.basename(pkg.filename))
-               abspath = os.path.join(self.source.targetpath, path)
+       def conflicts(self):
+               return [d[1] for d in self.deps if d[0] == "conflicts"]
 
-               if os.path.exists(abspath):
-                       # Check if file is already in the database and return the id.
-                       file = self.db.get("SELECT id FROM package_files WHERE path = %s LIMIT 1", path)
-                       if file:
-                               return file.id
+       @property
+       def obsoletes(self):
+               return [d[1] for d in self.deps if d[0] == "obsoletes"]
 
-                       os.unlink(abspath)
+       @property
+       def commit_id(self):
+               return self.data.commit_id
 
-               # Save the data to a file.
-               dirname = os.path.dirname(abspath)
-               if not os.path.exists(dirname):
-                       os.makedirs(dirname)
+       def get_commit(self):
+               if not self.commit_id:
+                       return
 
-               # Copy file to target directory.
-               shutil.copy(pkg.filename, abspath)
+               if self._commit is None:
+                       self._commit = sources.Commit(self.pakfire, self.commit_id)
 
-               id = self.db.execute("INSERT INTO package_files(path, pkg_id, source_id,"
-                       " type, arch, summary, description, requires, provides, obsoletes,"
-                       " conflicts, url, license, maintainer, size, hash1, build_host,"
-                       " build_id, build_time, uuid) VALUES(%s, %s, %s, %s, %s, %s, %s, %s,"
-                       " %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
-                       path, self.id, build.source.id, pkg.type, pkg.arch, pkg.summary,
-                       pkg.description, " ".join(pkg.requires), " ".join(pkg.provides),
-                       " ".join(pkg.obsoletes), " ".join(pkg.conflicts), pkg.url, pkg.license,
-                       pkg.maintainer, pkg.size, pkg.hash1, pkg.build_host, pkg.build_id,
-                       pkg.build_time, pkg.uuid)
+               return self._commit
 
-               # Add package filelist
-               self.db.executemany("INSERT INTO filelists(pkgfile_id, name, size, hash1)"
-                       " VALUES(%s, %s, %s, %s)", [(id, f, 0, "0"*40) for f in pkg.filelist])
+       def set_commit(self, commit):
+               self.db.execute("UPDATE packages SET commit_id = %s WHERE id = %s",
+                       commit.id, self.id)
+               self._commit = commit
 
-               return id
+       commit = property(get_commit, set_commit)
 
-       def add_log(self, filename, build):
-               path = os.path.join(
-                       self.name,
-                       "%s-%s-%s" % (self.epoch, self.version, self.release),
-                       "logs/build.%s.%s.log" % (build.arch, build.retries))
-               abspath = os.path.join(self.source.targetpath, path)
+       @property
+       def distro(self):
+               if not self.commit:
+                       return
 
-               # Save the data to a file.
-               dirname = os.path.dirname(abspath)
-               if not os.path.exists(dirname):
-                       os.makedirs(dirname)
+               # XXX THIS CANNOT RETURN None
 
-               # Move file to target directory.
-               shutil.copy(filename, abspath)
+               return self.commit.distro
 
-               self.db.execute("INSERT INTO package_files(path, source_id, pkg_id,"
-                       " type, arch, build_id) VALUES(%s, %s, %s, %s, %s, %s)", path,
-                       self.source.id, self.id, "log", build.arch, build.uuid)
+       @property
+       def build_id(self):
+               return self.data.build_id
 
        @property
-       def supported_arches(self):
-               supported = self._data.get("supported_arches") or ""
-               supported = supported.split()
+       def build_host(self):
+               return self.data.build_host
 
-               arches = []
+       @property
+       def build_time(self):
+               return self.data.build_time
 
-               if "all" in supported:
-                       # Inherit all supported architectures from the distribution.
-                       arches += self.distro.arches
-                       supported.remove("all")
+       @property
+       def path(self):
+               return self.data.path
 
-               excludes = [a[1:] for a in supported if a.startswith("-")]
+       @property
+       def hash_sha512(self):
+               return self.data.hash_sha512
 
-               if excludes:
-                       _arches = []
+       @property
+       def filesize(self):
+               return self.data.filesize
 
-                       for arch in arches:
-                               if arch in excludes:
-                                       continue
+       def move(self, target_dir):
+               # Create directory if it does not exist, yet.
+               if not os.path.exists(target_dir):
+                       os.makedirs(target_dir)
 
-                               _arches.append(arch)
+               # Make full path where to put the file.
+               target = os.path.join(target_dir, os.path.basename(self.path))
 
-                       arches = _arches
+               # Copy the file to the target directory (keeping metadata).
+               shutil.move(self.path, target)
 
-               includes = [a[1:] for a in supported if not a.startswith("-")]
+               # Update file path in the database.
+               self.db.execute("UPDATE packages SET path = %s WHERE id = %s",
+                       os.path.relpath(target, PACKAGES_DIR), self.id)
+               self._data["path"] = target
 
-               # Add explicitely included architectures.
-               arches += includes
+       @property
+       def build(self):
+               build = self.db.get("SELECT id FROM builds \
+                       WHERE type = 'release' AND pkg_id = %s", self.id)
 
-               return arches
+               if build:
+                       return builds.Build(self.pakfire, build.id)
 
-       def create_builds(self):
-               builds = []
+               if self.job:
+                       return self.job.build
 
-               for arch in self.supported_arches:
-                       b = build.BinaryBuild.new(self.pakfire, self, arch)
+       @property
+       def job(self):
+               if self._job is None:
+                       job = self.db.get("SELECT job_id AS id FROM jobs_packages \
+                               WHERE pkg_id = %s", self.id)
 
-                       builds.append(b)
+                       if job:
+                               self._job = builds.Job(self.pakfire, job.id)
 
-               return builds
+               return self._job
 
        @property
-       def builds(self):
-               if not hasattr(self, "_builds"):
-                       self._builds = self.pakfire.builds.get_by_pkgid(self.id)
+       def filelist(self):
+               if self._filelist is None:
+                       self._filelist = \
+                               self.db.query("SELECT * FROM filelists WHERE pkg_id = %s ORDER BY name",
+                                       self.id)
 
-                       if self.source_build:
-                               self._builds.append(self.source_build)
+               return self._filelist
 
-               return self._builds
+       def get_file(self):
+               if os.path.exists(self.path):
+                       return pakfire.packages.open(self.path)
 
-       @property
-       def source_build(self):
-               return self.pakfire.builds.get_by_id(self._data.source_build)
+       ## properties
+
+       _default_properties = {
+               "critical_path" : False,
+               "priority"      : 0,
+       }
 
-       def update(self):
-               if not self.state == "finished":
-                       # Check if all builds are finished and set package state to finished, too.
-                       for build in self.builds:
-                               if not build.finished:
-                                       return
+       def update_property(self, key, value):
+               assert self._default_properties.has_key(key), "Unknown key: %s" % key
 
-                       self.state = "finished"
+               #print self.db.execute("UPDATE packages_properties SET 
 
        @property
-       def repositories(self):
-               repos = self.db.query("SELECT repo_id FROM repository_packages WHERE pkg_id = %s", self.id)
+       def properties(self):
+               if self._properties is None:
+                       self._properties = \
+                               self.db.get("SELECT * FROM packages_properties WHERE name = %s", self.name)
 
-               return [self.pakfire.repos.get_by_id(r.id) for r in repos]
+                       if not self._properties:
+                               self._properties = database.Row(self._default_properties)
 
-       def add_to_repository(self, repo):
-               #repo.add_package(self)
-               repo.register_action("add", self.id)
+               return self._properties
 
        @property
-       def comments(self):
-               comments = self.db.query("""SELECT * FROM package_comments
-                       WHERE pkg_id = %s ORDER BY time DESC""", self.id)
+       def critical_path(self):
+               return self.properties.get("critical_path", "N") == "Y"
 
-               return comments
 
-       def comment(self, user_id, text, vote):
-               id = self.db.execute("""INSERT INTO package_comments(pkg_id, user_id,
-                       text, vote) VALUES(%s, %s, %s, %s)""", self.id, user_id, text, vote)
+# XXX DEAD CODE
 
-               vote2credit = { "up" : 1, "down" : -1, "none" : 0, }
-               try:
-                       self.credits = self.credits + vote2credit[vote]
+class File(base.Object):
+       def __init__(self, pakfire, path):
+               base.Object.__init__(self, pakfire)
 
-               except KeyError:
-                       pass
+               assert os.path.exists(path)
+               self.path = path
 
-               return id
+       def calc_hash(self, method="sha512"):
+               h = None
 
-       def get_credits(self):
-               return self._data.credits
+               if method == "sha512":
+                       h = hashlib.sha512()
 
-       def set_credits(self, credits):
-               self.db.execute("UPDATE packages SET credits = %s WHERE id = %s",
-                       credits, self.id)
-               self._data["credits"] = credits
+               if h is None:
+                       raise Exception, "Not a valid hash method: %s" % method
 
-       credits = property(get_credits, set_credits)
+               logging.debug("Calculating %s hash for %s" % (method, self.path))
 
-       def get_actions(self):
-               return self.pakfire.repos.get_actions_by_pkgid(self.id)
+               f = open(self.path, "rb")
+               while True:
+                       buf = f.read(BUFFER_SIZE)
+                       if not buf:
+                               break
 
+                       h.update(buf)
+               f.close()
 
-class File(base.Object):
-       type = None
+               return h.hexdigest()
 
+
+class DatabaseFile(File):
        def __init__(self, pakfire, id):
                base.Object.__init__(self, pakfire)
-               self.id = id
-
-               self._data = \
-                       self.db.get("SELECT * FROM package_files WHERE id = %s", self.id)
-
 
-       def __repr__(self):
-               return "<%s %s>" % (self.__class__.__name__, self.name)
-
-       def __cmp__(self, other):
-               return cmp(self.name, other.name)
-
-       @property
-       def name(self):
-               return os.path.basename(self.path)
-
-       @property
-       def path(self):
-               path = self._data.get("path")
+               self.id = id
 
-               if path.startswith("/"):
-                       path = path[1:]
+               # Cache.
+               self._data = None
 
-               return path
+       def fetch_data(self):
+               raise NotImplementedError
 
        @property
-       def abspath(self):
-               return os.path.join(self.source.targetpath, self.path)
+       def data(self):
+               if self._data is None:
+                       self._data = self.fetch_data()
 
-       @property
-       def download(self):
-               path = self.abspath.split("/")
+               return self._data
 
-               while path:
-                       if path[0] == "packages":
-                               break
-                       path = path[1:]
 
-               return "%s/%s" % (self.pakfire.settings.get("baseurl"), "/".join(path))
+class DatabaseSourceFile(DatabaseFile):
+       def fetch_data(self):
+               return self.db.get("SELECT * FROM files_src WHERE id = %s", self.id)
 
-       @property
-       def source(self):
-               return self.pakfire.sources.get_by_id(self._data.source_id)
 
 
 class PackageFile(File):
-       @property
-       def filelist(self):
-               return self.db.query("SELECT name, size, hash1 FROM filelists"
-                       " WHERE pkgfile_id = %s", self.id)
-
-       @property
-       def hash1(self):
-               return self._data.get("hash1")
+       type = None
 
-       @property
-       def uuid(self):
-               return self._data.get("uuid")
+       def __init__(self, pakfire, path):
+               File.__init__(self, pakfire, path)
 
-       @property
-       def summary(self):
-               return self._data.get("summary")
+               # Open the package file for reading.
+               self.pkg = packages.open(None, None, self.path)
+               assert self.pkg.type == self.type
 
-       @property
-       def description(self):
-               return self._data.get("description")
-
-       @property
-       def license(self):
-               return self._data.get("license")
+       def to_database(self):
+               raise NotImplementedError
 
        @property
-       def size(self):
-               return self._data.get("size")
+       def uuid(self):
+               return self.pkg.uuid
 
        @property
-       def url(self):
-               return self._data.get("url")
+       def requires(self):
+               return self.pkg.requires
 
        @property
-       def maintainer(self):
-               return self._data.get("maintainer")
+       def build_host(self):
+               return self.pkg.build_host
 
        @property
        def build_id(self):
-               return self._data.get("build_id")
-
-       @property
-       def build_host(self):
-               return self._data.get("build_host")
+               return self.pkg.build_id
 
        @property
        def build_time(self):
-               return self._data.get("build_time")
+               build_time = self.pkg.build_time
 
-       @property
-       def build_date(self):
-               return time.strftime("%a, %d %b %Y %H:%M:%S +0000",
-                       time.gmtime(self.build_time))
+               return datetime.datetime.utcfromtimestamp(build_time)
 
-       @property
-       def provides(self):
-               return self._data.get("provides").split()
 
-       @property
-       def requires(self):
-               requires = self._data.get("requires").split()
-
-               return sorted(requires)
-
-       @property
-       def obsoletes(self):
-               obsoletes = self._data.get("obsoletes").split()
+class PackageSourceFile(PackageFile):
+       type = "source"
 
-               return sorted(obsoletes)
+       def to_database(self):
+               # MUST BE RELATIVE TO BASEDIR
+               path = self.path
 
-       @property
-       def conflicts(self):
-               conflicts = self._data.get("conflicts").split()
+               id = self.db.execute("""
+                       INSERT INTO packages(path, uuid, requires, hash_sha512,
+                               build_host, build_id, build_time)
+                       VALUES(%s, %s, %s, %s, %s, %s, %s)""",
+                       path, self.uuid, "\n".join(self.requires), self.calc_hash("sha512"),
+                       self.build_host, self.build_id, self.build_time
+               )
 
-               return sorted(conflicts)
+               # Return the newly created object.
+               return DatabaseSourceFile(self.pakfire, id)
 
 
-class BinaryPackageFile(PackageFile):
+class PackageBinaryFile(PackageFile):
        type = "binary"
 
 
-class SourcePackageFile(PackageFile):
-       type = "source"
-
-
-class LogFile(File):
-       type = "log"
-
index 71a4a4ea88e2b541f7aaa9a7faa78c747d1d05ea..dd95586e21544f4829d80c242ed1dfb96740f54f 100644 (file)
@@ -1,6 +1,10 @@
 #!/usr/bin/python
 
+import os.path
+
 import base
+import builds
+import logs
 import packages
 
 class Repositories(base.Object):
@@ -27,20 +31,26 @@ class Repositories(base.Object):
 
                return [Repository(self.pakfire, r.id) for r in repos]
 
-       def get_action_by_id(self, action_id):
-               action = self.db.get("SELECT id, repo_id FROM repository_actions WHERE id = %s"
-                       " LIMIT 1" , action_id)
-
-               assert action
+       def get_history(self, limit=None, offset=None, build=None, repo=None, user=None):
+               query = "SELECT * FROM repositories_history"
+               args  = []
 
-               repo = self.get_by_id(action.repo_id)
+               query += " ORDER BY time DESC"
 
-               return RepoAction(self.pakfire, repo, action.id)
+               if limit:
+                       if offset:
+                               query += " LIMIT %s,%s"
+                               args  += [offset, limit,]
+                       else:
+                               query += " LIMIT %s"
+                               args  += [limit,]
 
-       def get_actions_by_pkgid(self, pkgid):
-               actions = self.db.query("SELECT id FROM repository_actions WHERE pkg_id = %s", pkg_id)
+               entries = []
+               for entry in self.db.query(query, *args):
+                       entry = logs.RepositoryLogEntry(self.pakfire, entry)
+                       entries.append(entry)
 
-               return [RepoAction(self.pakfire, self, a.id) for a in actions]
+               return entries
 
 
 class Repository(base.Object):
@@ -48,39 +58,63 @@ class Repository(base.Object):
                base.Object.__init__(self, pakfire)
                self.id = id
 
-               self.data = self.db.get("SELECT * FROM repositories WHERE id = %s", self.id)
+               # Cache.
+               self._data = None
+               self._next = None
+               self._prev = None
+               self._key  = None
+               self._distro = None
+
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = \
+                               self.db.get("SELECT * FROM repositories WHERE id = %s", self.id)
+
+               return self._data
 
        def __cmp__(self, other):
-               if not self.upstream_id or not other.upstream_id:
+               if other is None:
+                       return 1
+
+               if self.id == other.id:
                        return 0
 
-               if self.id == other.upstream_id:
+               elif self.id == other.parent_id:
+                       return 1
+
+               elif self.parent_id == other.id:
                        return -1
 
-               elif self.upstream_id == other.id:
-                       return 1
+               return 1
 
        def next(self):
-               repo = self.db.get("SELECT id FROM repositories WHERE upstream = %s"
-                       " LIMIT 1", self.id)
+               if self._next is None:
+                       repo = self.db.get("SELECT id FROM repositories \
+                               WHERE parent_id = %s LIMIT 1", self.id)
 
-               if repo:
-                       return self.pakfire.repos.get_by_id(repo.id)
+                       if not repo:
+                               return
 
-       def last(self):
-               if self.upstream_id:
-                       return self.pakfire.repos.get_by_id(self.upstream_id)
+                       self._next = Repository(self.pakfire, repo.id)
 
-       @property
-       def upstream(self):
-               return self.last()
+               return self._next
+
+       def prev(self):
+               if not self.parent_id:
+                       return
+
+               if self._prev is None:
+                       self._prev = Repository(self.pakfire, self.parent_id)
+
+               return self._prev
 
        @property
-       def is_upstream(self):
-               return not self.upstream
+       def parent(self):
+               return self.prev()
 
        @classmethod
-       def new(cls, pakfire, distro, name, description):
+       def create(cls, pakfire, distro, name, description):
                id = pakfire.db.execute("INSERT INTO repositories(distro_id, name, description)"
                        " VALUES(%s, %s, %s)", distro.id, name, description)
 
@@ -88,7 +122,11 @@ class Repository(base.Object):
 
        @property
        def distro(self):
-               return self.pakfire.distros.get_by_id(self.data.distro_id)
+               if self._distro is None:
+                       self._distro = self.pakfire.distros.get_by_id(self.data.distro_id)
+                       assert self._distro
+
+               return self._distro
 
        @property
        def info(self):
@@ -100,188 +138,339 @@ class Repository(base.Object):
                }
 
        @property
-       def comprehensive(self):
-               return not self.upstream_id
+       def url(self):
+               url = os.path.join(
+                       self.settings.get("repository_baseurl", "http://pakfire.ipfire.org/repositories/"),
+                       self.distro.identifier,
+                       self.identifier,
+                       "%{arch}"
+               )
+
+               return url
+
+       @property
+       def mirrorlist(self):
+               url = os.path.join(
+                       self.settings.get("mirrorlist_baseurl", "https://pakfire.ipfire.org/"),
+                       "distro", self.distro.identifier,
+                       "repo", self.identifier,
+                       "mirrorlist?arch=%{arch}"
+               )
+
+               return url
+
+       def get_conf(self):
+               prioritymap = {
+                       "stable"   : 500,
+                       "unstable" : 200,
+                       "testing"  : 100,
+               }
+
+               try:
+                       priority = prioritymap[self.type]
+               except KeyError:
+                       priority = None
+
+               lines = [
+                       "[repo:%s]" % self.identifier,
+                       "description = %s - %s" % (self.distro.name, self.summary),
+                       "enabled = 1",
+                       "baseurl = %s" % self.url,
+                       "mirrors = %s" % self.mirrorlist,
+               ]
+
+               if priority:
+                       lines.append("priority = %s" % priority)
+
+               return "\n".join(lines)
 
        @property
        def name(self):
                return self.data.name
 
        @property
-       def description(self):
-               return self.data.description
+       def identifier(self):
+               return self.name.lower()
 
        @property
-       def upstream_id(self):
-               return self.data.upstream
+       def type(self):
+               return self.data.type
 
        @property
-       def arches(self):
-               return self.distro.arches
+       def summary(self):
+               lines = self.description.splitlines()
 
-       def get_needs_update(self):
-               return self.data.needs_update
+               if lines:
+                       return lines[0]
 
-       def set_needs_update(self, val):
-               if val:
-                       val = "Y"
-               else:
-                       val = "N"
+               return "N/A"
 
-               self.db.execute("UPDATE repositories SET needs_update = %s WHERE id = %s",
-                       val, self.id)
+       @property
+       def description(self):
+               return self.data.description or ""
 
-       needs_update = property(get_needs_update, set_needs_update)
+       @property
+       def parent_id(self):
+               return self.data.parent_id
 
        @property
-       def credits_needed(self):
-               return self.data.credits_needed
+       def key(self):
+               if not self.data.key_id:
+                       return
 
-       def add_package(self, pkg_id):
-               id = self.db.execute("INSERT INTO repository_packages(repo_id, pkg_id)"
-                       " VALUES(%s, %s)", self.id, pkg_id)
+               if self._key is None:
+                       self._key = self.pakfire.keys.get_by_id(self.data.key_id)
+                       assert self._key
 
-               # Mark, that the repository was altered and needs an update.
-               self.needs_update = True
+               return self._key
 
-               return id
+       @property
+       def arches(self):
+               return self.distro.arches
+
+       @property
+       def mirrored(self):
+               return self.data.mirrored == "Y"
 
-       def get_packages(self, _query=None):
-               query = "SELECT pkg_id FROM repository_packages WHERE repo_id = %s"
-               if _query:
-                       query = "%s %s" % (query, _query)
+       def get_enabled_for_builds(self):
+               return self.data.enabled_for_builds == "Y"
 
-               pkgs = self.db.query(query, self.id)
+       def set_enabled_for_builds(self, state):
+               if state:
+                       state = "Y"
+               else:
+                       state = "N"
 
-               return sorted([packages.Package(self.pakfire, p.pkg_id) for p in pkgs])
+               self.db.execute("UPDATE repositories SET enabled_for_builds = %s WHERE id = %s",
+                       state, self.id)
 
-       @property
-       def packages(self):
-               return self.get_packages()
+               if self._data:
+                       self._data["enabled_for_builds"] = state
+
+       enabled_for_builds = property(get_enabled_for_builds, set_enabled_for_builds)
 
        @property
-       def waiting_packages(self):
-               return self.get_packages("AND state = 'waiting'")
+       def score_needed(self):
+               return self.data.score_needed
 
        @property
-       def pushed_packages(self):
-               return self.get_packages("AND state = 'pushed'")
+       def time_min(self):
+               return self.data.time_min
 
        @property
-       def log(self):
-               return self.db.query("SELECT * FROM log WHERE repo_id = %s ORDER BY time DESC", self.id)
+       def time_max(self):
+               return self.data.time_max
 
-       def register_action(self, action, pkg_id, old_pkg_id=None):
-               assert action in ("add", "remove",)
-               assert pkg_id is not None
+       def _log_build(self, action, build, from_repo=None, to_repo=None, user=None):
+               user_id = None
+               if user:
+                       user_id = user.id
 
-               id = self.db.execute("INSERT INTO repository_actions(action, repo_id, pkg_id)"
-                       " VALUES(%s, %s, %s)", action, self.id, pkg_id)
+               from_repo_id = None
+               if from_repo:
+                       from_repo_id = from_repo.id
 
-               # On upstream repositories, all actions are done immediately.
-               if self.is_upstream:
-                       action = RepoAction(self.pakfire, self, id)
-                       action.run()
+               to_repo_id = None
+               if to_repo:
+                       to_repo_id = to_repo.id
 
-               return id
+               self.db.execute("INSERT INTO repositories_history(action, build_id, from_repo_id, to_repo_id, user_id, time) \
+                       VALUES(%s, %s, %s, %s, %s, NOW())", action, build.id, from_repo_id, to_repo_id, user_id)
 
-       def has_actions(self):
-               actions = self.db.get("SELECT COUNT(*) as c FROM repository_actions"
-                       " WHERE repo_id = %s", self.id)
+       def add_build(self, build, user=None, log=True):
+               self.db.execute("INSERT INTO repositories_builds(repo_id, build_id, time_added)"
+                       " VALUES(%s, %s, NOW())", self.id, build.id)
 
-               if actions.c:
-                       return True
+               # Update bug status.
+               build._update_bugs_helper(self)
 
-               return False
+               if log:
+                       self._log_build("added", build, to_repo=self, user=user)
 
-       def get_actions(self):
-               actions = self.db.query("SELECT id FROM repository_actions"
-                       " WHERE repo_id = %s ORDER BY time_added ASC", self.id)
+       def rem_build(self, build, user=None, log=True):
+               self.db.execute("DELETE FROM repositories_builds \
+                       WHERE repo_id = %s AND build_id = %s", self.id, build.id)
 
-               return [RepoAction(self.pakfire, self, a.id) for a in actions]
+               if log:
+                       self._log_build("removed", build, from_repo=self, user=user)
 
+       def move_build(self, build, to_repo, user=None, log=True):
+               self.db.execute("UPDATE repositories_builds SET repo_id = %s, time_added = NOW() \
+                       WHERE repo_id = %s AND build_id = %s", to_repo.id, self.id, build.id)
 
-class RepoAction(base.Object):
-       def __init__(self, pakfire, repo, id):
-               base.Object.__init__(self, pakfire)
+               # Update bug status.
+               build._update_bugs_helper(to_repo)
 
-               self.repo = repo
-               self.id = id
+               if log:
+                       self._log_build("moved", build, from_repo=self, to_repo=to_repo,
+                               user=user)
 
-               self.data = self.db.get("SELECT * FROM repository_actions WHERE id = %s"
-                       " LIMIT 1", self.id)
-               assert self.data
-               assert self.data.repo_id == self.repo.id
+       def build_count(self):
+               query = self.db.get("SELECT COUNT(*) AS count FROM repositories_builds \
+                       WHERE repo_id = %s", self.id)
 
-       @property
-       def action(self):
-               return self.data.action
+               if query:
+                       return query.count
 
-       @property
-       def pkg_id(self):
-               return self.data.pkg_id
+       def get_builds(self, limit=None, offset=None):
+               query = "SELECT build_id AS id FROM repositories_builds \
+                       WHERE repo_id = %s ORDER BY time_added DESC"
+               args  = [self.id,]
 
-       @property
-       def pkg(self):
-               return self.pakfire.packages.get_by_id(self.pkg_id)
+               if limit:
+                       if offset:
+                               query += " LIMIT %s,%s"
+                               args  += [offset, limit,]
+                       else:
+                               query += " LIMIT %s"
+                               args  += [limit,]
+
+               _builds = []
+               for build in self.db.query(query, *args):
+                       build = builds.Build(self.pakfire, build.id)
+                       build._repo = self
+
+                       _builds.append(build)
+
+               return _builds
+
+       def get_packages(self, arch):
+               if arch.name == "src":
+                       pkgs = self.db.query("SELECT packages.id AS id FROM packages \
+                               JOIN builds ON builds.pkg_id = packages.id \
+                               JOIN repositories_builds ON builds.id = repositories_builds.build_id \
+                               WHERE packages.arch = %s AND repositories_builds.repo_id = %s",
+                               arch.id, self.id)
 
-       @property
-       def credits_needed(self):
-               return self.repo.credits_needed - self.pkg.credits
+               else:
+                       noarch = self.pakfire.arches.get_by_name("noarch")
+                       assert noarch
 
-       @property
-       def time_added(self):
-               return self.data.time_added
-
-       def delete(self, who=None):
-               """
-                       Delete ourself from the database.
-               """
-               if who and not self.have_permission(who):
-                       raise Exception, "Insufficient permissions"
-
-               self.db.execute("DELETE FROM repository_actions WHERE id = %s LIMIT 1", self.id)
-
-       def have_permission(self, who):
-               """
-                       Check if "who" has the permission to perform this action.
-               """
-               if who is None:
-                       return True
+                       pkgs = self.db.query("SELECT packages.id AS id FROM packages \
+                               JOIN jobs_packages ON jobs_packages.pkg_id = packages.id \
+                               JOIN jobs ON jobs_packages.job_id = jobs.id \
+                               JOIN builds ON builds.id = jobs.build_id \
+                               JOIN repositories_builds ON builds.id = repositories_builds.build_id \
+                               WHERE (jobs.arch_id = %s OR jobs.arch_id = %s) AND \
+                               repositories_builds.repo_id = %s",
+                               arch.id, noarch.id, self.id)
 
-               # Admins are always allowed to do all actions.
-               if who.is_admin():
-                       return True
+               return sorted([packages.Package(self.pakfire, p.id) for p in pkgs])
+
+       @property
+       def packages(self):
+               return self.get_packages()
 
-               # If the maintainer matches, he is also allowed.
-               if who.email == self.pkg.maintainer_email:
+       def get_unpushed_builds(self):
+               query = self.db.query("SELECT build_id FROM repositories_builds \
+                       WHERE repo_id = %s AND \
+                       time_added > (SELECT last_update FROM repositories WHERE id = %s)",
+                       self.id, self.id)
+
+               ret = []
+               for row in query:
+                       b = builds.Build(self.pakfire, row.build_id)
+                       ret.append(b)
+
+               return ret
+
+       def get_obsolete_builds(self):
+               #query = self.db.query("SELECT build_id AS id FROM repositories_builds \
+               #       JOIN builds ON repositories.build_id = builds.id \
+               #       WHERE repositories_builds.repo_id = %s AND builds.state = 'obsolete'",
+               #       self.id)
+               #
+               #ret = []
+               #for row in query:
+               #       b = builds.Build(self.pakfire, row.id)
+               #       ret.append(b)
+               #
+               #return ret
+               return self.pakfire.builds.get_obsolete(self)
+
+       def needs_update(self):
+               if self.get_unpushed_builds:
                        return True
 
-               # Everybody else is denied.
                return False
 
-       def is_doable(self):
-               return self.credits_needed == 0
+       def updated(self):
+               self.db.execute("UPDATE repositories SET last_update = NOW() \
+                       WHERE id = %s", self.id)
 
-       def run(self, who=None):
-               if who and not self.have_permission(who):
-                       raise Exception, "Insufficient permissions"
+       def get_history(self, **kwargs):
+               kwargs.update({
+                       "repo" : self,
+               })
 
-               if self.action == "add":
-                       self.repo.add_package(self.pkg_id)
+               return self.pakfire.repos.get_history(**kwargs)
 
-               elif self.action == "remove":
-                       self.db.excute("DELETE FROM repository_packages WHERE repo_id = %s"
-                               " AND pkg_id = %s LIMIT 1", self.repo.id, self.pkg_id)
+       def get_build_times(self):
+               noarch = self.pakfire.arches.get_by_name("noarch")
+               assert noarch
+
+               times = []
+               for arch in self.pakfire.arches.get_all():
+                       time = self.db.get("SELECT SUM(jobs.time_finished - jobs.time_started) AS time FROM jobs \
+                               JOIN builds ON builds.id = jobs.build_id \
+                               JOIN repositories_builds ON builds.id = repositories_builds.build_id \
+                               WHERE (jobs.arch_id = %s OR jobs.arch_id = %s) AND \
+                               repositories_builds.repo_id = %s", arch.id, noarch.id, self.id)
+
+                       times.append((arch, time.time))
+
+               return times
 
-               else:
-                       raise Exception, "Invalid action"
 
-               # If the action was started by an upstream repository, we add it so
-               # the next one.
-               next = self.repo.next()
-               if next:
-                       next.register_action(self.action, self.pkg_id)
+class RepositoryAux(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
+
+               self.id = id
+
+               # Cache.
+               self._data = None
+               self._distro = None
 
-               # Delete ourself.
-               self.delete(who)
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = self.db.get("SELECT * FROM repositories_aux WHERE id = %s", self.id)
+                       assert self._data
+
+               return self._data
+
+       @property
+       def name(self):
+               return self.data.name
+
+       @property
+       def description(self):
+               return self.data.description or ""
+
+       @property
+       def url(self):
+               return self.data.url
+
+       @property
+       def identifier(self):
+               return self.name.lower()
+
+       @property
+       def distro(self):
+               if self._distro is None:
+                       self._distro = self.pakfire.distros.get_by_id(self.data.distro_id)
+                       assert self._distro
+
+               return self._distro
+
+       def get_conf(self):
+               lines = [
+                       "[repo:%s]" % self.identifier,
+                       "description = %s - %s" % (self.distro.name, self.name),
+                       "enabled = 1",
+                       "baseurl = %s" % self.url,
+                       "priority = 0",
+               ]
+
+               return "\n".join(lines)
diff --git a/backend/sessions.py b/backend/sessions.py
new file mode 100644 (file)
index 0000000..7c8260c
--- /dev/null
@@ -0,0 +1,116 @@
+#!/usr/bin/python
+
+import uuid
+
+import base
+import users
+
+class Sessions(base.Object):
+       def get(self, session_id):
+               try:
+                       session = Session(self.pakfire, session_id)
+               except:
+                       return
+
+               return session
+
+
+class Session(base.Object):
+       def __init__(self, pakfire, session_id):
+               base.Object.__init__(self, pakfire)
+
+               # Save the session_id.
+               self.id = session_id
+
+               self.data = self.db.get("SELECT * FROM sessions WHERE session_id = %s \
+                       AND valid_until >= NOW()", self.id)
+
+               if not self.data:
+                       raise Exception, "No such session: %s" % self.id
+
+               # Cache.
+               self._user = None
+               self._impersonated_user = None
+
+               # Update the valid time of the session.
+               #self.update()
+
+       @staticmethod
+       def has_session(pakfire, session_id):
+               if self.db.get("SELECT session_id FROM sessions WHERE session_id = %s \
+                               AND valid_until >= NOW()", session_id):
+                       return True
+
+               return False
+
+       def refresh(self):
+               self.db.execute("UPDATE sessions SET valid_until = DATE_ADD(NOW(), INTERVAL 3 DAY) \
+                       WHERE session_id = %s", self.id)
+
+       def destroy(self):
+               self.db.execute("DELETE FROM sessions WHERE session_id = %s", self.id)
+
+       @staticmethod
+       def cleanup(pakfire):
+               # Remove all sessions that are no more valid.
+               pakfire.db.execute("DELETE FROM sessions WHERE valid_until < NOW()")
+
+       @property
+       def user(self):
+               if self._user is None:
+                       self._user = users.User(self.pakfire, self.data.user_id)
+
+               return self._user
+
+       @property
+       def impersonated_user(self):
+               if not self.data.impersonated_user_id:
+                       return
+
+               if self._impersonated_user is None:
+                       self._impersonated_user = \
+                               users.User(self.pakfire, self.data.impersonated_user_id)
+
+               return self._impersonated_user
+
+       def start_impersonation(self, user):
+               assert self.user.is_admin(), "Only admins can impersonate other users."
+
+               # You cannot impersonate yourself.
+               if self.user == user:
+                       return
+
+               self.db.execute("UPDATE sessions SET impersonated_user_id = %s \
+                       WHERE session_id = %s", user.id, self.id)
+
+       def stop_impersonation(self):
+               self.db.execute("UPDATE sessions SET impersonated_user_id = NULL \
+                       WHERE session_id = %s", self.id)
+
+       @classmethod
+       def create(cls, pakfire, user):
+               """
+                       Creates a new session in the data.
+
+                       The user is not checked and it is assumed that the user exsists
+                       and has the right to log in.
+               """
+               # Check if user has too much open sessions.
+               sessions = pakfire.db.get("SELECT COUNT(*) as count FROM sessions \
+                       WHERE user_id = %s AND valid_until >= NOW()", user.id)
+
+               sessions_max = pakfire.settings.get_int("sessions_max", 0)
+
+               if sessions.count >= sessions_max:
+                       raise Exception, "User exceeded maximum number of allowed sessions"
+
+               # Create a new session in the database.
+               session_id = "%s" % uuid.uuid4()
+
+               pakfire.db.execute("""
+                       INSERT INTO sessions(session_id, user_id, creation_time, valid_until)
+                       VALUES(%s, %s, NOW(), DATE_ADD(NOW(), INTERVAL 3 DAY))
+               """, session_id, user.id)
+
+               return cls(pakfire, session_id)
+
index 150f56d1825a52db4a0ef2abb4f2e7c100d7a710..5a0ad405262a40cf9ab7e391ed9911d3f6251bbd 100644 (file)
@@ -3,6 +3,10 @@
 import base
 
 class Settings(base.Object):
+       """
+               The cache is not available here.
+       """
+
        def query(self, key):
                return self.db.get("SELECT * FROM settings WHERE k = %s", key)
 
index 063c997372e28be7801227e41eb574ecff792665..c6de5d3602ecdaee003f71586bbc0dd34fc5b94e 100644 (file)
@@ -4,9 +4,11 @@ import datetime
 import logging
 import os
 import subprocess
+import tornado.database
 
 import base
-import build
+import builds
+import packages
 
 class Sources(base.Object):
        def get_all(self):
@@ -15,10 +17,7 @@ class Sources(base.Object):
                return [Source(self.pakfire, s.id) for s in sources]
 
        def get_by_id(self, id):
-               source = self.db.get("SELECT id FROM sources WHERE id = %s", id)
-
-               if source:
-                       return Source(self.pakfire, source.id)
+               return Source(self.pakfire, id)
 
        def get_by_distro(self, distro):
                sources = self.db.query("SELECT id FROM sources WHERE distro_id = %s", distro.id)
@@ -30,93 +29,187 @@ class Sources(base.Object):
 
                return self.db.execute(query, revision, source_id)
 
+       def get_pending_commits(self, limit=None):
+               query = "SELECT id FROM sources_commits WHERE state = 'pending' ORDER BY id ASC"
+               args = []
 
-class Source(base.Object):
+               if limit:
+                       query += " LIMIT %s"
+                       args.append(limit)
+
+               rows = self.db.query(query, *args)
+
+               commits = []
+               for row in rows:
+                       commit = Commit(self.pakfire, row.id)
+                       commits.append(commit)
+
+               return commits
+
+       def get_commit_by_id(self, commit_id):
+               commit = self.db.get("SELECT id FROM sources_commits WHERE id = %s", commit_id)
+
+               if commit:
+                       return Commit(self.pakfire, commit.id)
+
+
+class Commit(base.Object):
        def __init__(self, pakfire, id):
                base.Object.__init__(self, pakfire)
 
                self.id = id
 
-               self._data = self.db.get("SELECT * FROM sources WHERE id = %s", self.id)
+               # Cache.
+               self._data = None
+               self._source = None
+               self._packages = None
+
+       @classmethod
+       def create(cls, pakfire, source, revision, author, committer, subject, body, date):
+               try:
+                       id = pakfire.db.execute("INSERT INTO sources_commits(source_id, revision, \
+                               author, committer, subject, body, date) VALUES(%s, %s, %s, %s, %s, %s, %s)",
+                               source.id, revision, author, committer, subject, body, date)
+               except tornado.database.IntegrityError:
+                       # If the commit (apperently) already existed, we return nothing.
+                       return
 
-       def __cmp__(self, other):
-               return cmp(self.id, other.id)
+               return cls(pakfire, id)
 
        @property
-       def info(self):
-               return {
-                       "id"         : self.id,
-                       "name"       : self.name,
-                       "url"        : self.url,
-                       "path"       : self.path,
-                       "targetpath" : self.targetpath,
-                       "revision"   : self.revision,
-                       "branch"     : self.branch,
-               }
+       def data(self):
+               if self._data is None:
+                       data = self.db.get("SELECT * FROM sources_commits WHERE id = %s", self.id)
 
-       def _git(self, cmd, path=None):
-               if not path:
-                       path = self.path
+                       self._data = data
+                       assert self._data
 
-               cmd = "cd %s && git %s" % (path, cmd)
+               return self._data
 
-               logging.debug("Running command: %s" % cmd)
+       @property
+       def revision(self):
+               return self.data.revision
+
+       @property
+       def source_id(self):
+               return self.data.source_id
+
+       @property
+       def source(self):
+               if self._source is None:
+                       self._source = Source(self.pakfire, self.source_id)
+
+               return self._source
+
+       @property
+       def distro(self):
+               """
+                       A shortcut to the distribution this commit
+                       belongs to.
+               """
+               return self.source.distro
 
-               return subprocess.check_output(["/bin/sh", "-c", cmd])
+       def get_state(self):
+               return self.data.state
 
-       def _git_rev_list(self, revision=None):
-               if not revision:
-                       revision = self.revision
+       def set_state(self, state):
+               self.db.execute("UPDATE sources_commits SET state = %s WHERE id = %s",
+                       state, self.id)
 
-               command = "rev-list %s..origin/%s" % (revision, self.branch)
+               if self._data:
+                       self._data["state"] = state
 
-               # Get all merge commits.
-               merges = self._git("%s --merges" % command)
-               merges = merges.splitlines()
+       state = property(get_state, set_state)
 
-               revisions = []
-               for commit in self._git(command).splitlines():
-                       # Check if commit is a normal commit or merge commit.
-                       merge = commit in merges
+       @property
+       def author(self):
+               return self.data.author
+
+       @property
+       def committer(self):
+               return self.data.committer
+
+       @property
+       def subject(self):
+               return self.data.subject
+
+       @property
+       def message(self):
+               return self.data.body.strip()
+
+       @property
+       def date(self):
+               return self.data.date
+
+       @property
+       def packages(self):
+               if self._packages is None:
+                       self._packages = []
 
-                       revisions.append((commit, merge))
+                       for pkg in self.db.query("SELECT id FROM packages WHERE commit_id = %s", self.id):
+                               pkg = packages.Package(self.pakfire, pkg.id)
+                               self._packages.append(pkg)
 
-               return [r for r in reversed(revisions)]
+                       self._packages.sort()
 
-       def is_cloned(self):
+               return self._packages
+
+       def reset(self):
                """
-                       Say if the repository is already cloned.
+                       Removes all packages that have been created by this commit and
+                       resets the state so it will be processed again.
                """
-               return os.path.exists(self.path)
+               # Remove all packages and corresponding builds.
+               for pkg in self.packages:
+                       # Check if there is a build associated with the package.
+                       # If so, the whole build will be deleted.
+                       if pkg.build:
+                               pkg.build.delete()
 
-       def clone(self):
-               if self.is_cloned():
-                       return
+                       else:
+                               # Delete the package.
+                               pkg.delete()
+
+               # Clear the cache.
+               self._packages = None
 
-               if not os.path.exists(dirname):
-                       os.makedirs(dirname)
+               # Reset the state to 'pending'.
+               self.state = "pending"
 
-               self._git("clone --bare %s %s" % (self.url, basename), path=dirname)
 
-       def fetch(self):
-               if not self.is_cloned():
-                       raise Exception, "Repository was not cloned, yet."
+class Source(base.Object):
+       def __init__(self, pakfire, id):
+               base.Object.__init__(self, pakfire)
+
+               self.id = id
+
+               self._data = None
+               self._head_revision = None
+
+       @property
+       def data(self):
+               if self._data is None:
+                       data = self.db.get("SELECT * FROM sources WHERE id = %s", self.id)
 
-               self._git("fetch")
+                       self._data = data
+                       assert self._data
 
-       def import_revisions(self):
-               # Get all pending revisions.
-               revisions = self._git_rev_list()
+               return self._data
 
-               for revision, merge in revisions:
-                       # If the revision is not a merge, we do import it.
-                       if not merge:
-                               self._import_revision(revision)
+       def __cmp__(self, other):
+               return cmp(self.id, other.id)
 
-                       # Save revision in database.
-                       self.db.execute("UPDATE sources SET revision = %s WHERE id = %s",
-                               revision, self.id)
-                       self._data["revision"] = revision
+       @property
+       def info(self):
+               return {
+                       "id"         : self.id,
+                       "name"       : self.name,
+                       "url"        : self.url,
+                       "path"       : self.path,
+                       "targetpath" : self.targetpath,
+                       "revision"   : self.revision,
+                       "branch"     : self.branch,
+               }
 
        def _import_revision(self, revision):
                logging.debug("Going to import revision: %s" % revision)
@@ -140,27 +233,27 @@ class Source(base.Object):
 
        @property
        def name(self):
-               return self._data.name
+               return self.data.name
 
        @property
-       def url(self):
-               return self._data.url
+       def identifier(self):
+               return self.data.identifier
 
        @property
-       def path(self):
-               return self._data.path
+       def url(self):
+               return self.data.url
 
        @property
-       def targetpath(self):
-               return self._data.targetpath
+       def gitweb(self):
+               return self.data.gitweb
 
        @property
        def revision(self):
-               return self._data.revision
+               return self.data.revision
 
        @property
        def branch(self):
-               return self._data.branch
+               return self.data.branch
 
        @property
        def builds(self):
@@ -168,4 +261,58 @@ class Source(base.Object):
 
        @property
        def distro(self):
-               return self.pakfire.distros.get_by_id(self._data.distro_id)
+               return self.pakfire.distros.get_by_id(self.data.distro_id)
+
+       @property
+       def start_revision(self):
+               return self.data.revision
+
+       @property
+       def head_revision(self):
+               if self._head_revision is None:
+                       commit = self.db.get("SELECT id FROM sources_commits \
+                               WHERE source_id = %s ORDER BY id DESC LIMIT 1", self.id)
+
+                       if commit:
+                               self._head_revision = Commit(self.pakfire, commit.id)
+
+               return self._head_revision
+
+       @property
+       def num_commits(self):
+               ret = self.db.get("SELECT COUNT(*) AS num FROM sources_commits \
+                       WHERE source_id = %s", self.id)
+
+               return ret.num
+
+       def get_commits(self, limit=None, offset=None):
+               query = "SELECT id FROM sources_commits WHERE source_id = %s \
+                       ORDER BY id DESC"
+               args = [self.id,]
+
+               if limit:
+                       if offset:
+                               query += " LIMIT %s,%s"
+                               args += [offset, limit]
+                       else:
+                               query += " LIMIT %s"
+                               args += [limit,]
+
+               commits = []
+               for commit in self.db.query(query, *args):
+                       commit = Commit(self.pakfire, commit.id)
+                       commits.append(commit)
+
+               return commits
+
+       def get_commit(self, revision):
+               commit = self.db.get("SELECT id FROM sources_commits WHERE source_id = %s \
+                       AND revision = %s LIMIT 1", self.id, revision)
+
+               if not commit:
+                       return
+
+               commit = Commit(self.pakfire, commit.id)
+               commit._source = self
+
+               return commit
diff --git a/backend/updates.py b/backend/updates.py
new file mode 100644 (file)
index 0000000..9768452
--- /dev/null
@@ -0,0 +1,99 @@
+#!/usr/bin/python
+
+import base
+import builds
+import distribution
+
+class Updates(base.Object):
+       def __init__(self, pakfire):
+               base.Object.__init__(self, pakfire)
+
+       def get(self, type, distro=None, limit=None, offset=None):
+               assert type in ("stable", "unstable", "testing")
+
+               query = "SELECT * FROM repositories_builds \
+                       JOIN builds ON builds.id = repositories_builds.build_id \
+                       WHERE builds.type = 'release' AND \
+                               repositories_builds.repo_id IN \
+                                       (SELECT id FROM repositories WHERE type = %s)"
+               args = [type,]
+
+               if distro:
+                       query += " AND builds.distro_id = %s"
+                       args.append(distro.id)
+
+               query += " ORDER BY time_added DESC"
+
+               if limit:
+                       if offset:
+                               query += " LIMIT %s,%s"
+                               args += [offset, limit]
+                       else:
+                               query += " LIMIT %s"
+                               args.append(limit)
+
+               updates = []
+               for row in self.db.query(query, *args):
+                       update = Update(self.pakfire, row)
+                       updates.append(update)
+
+               return updates
+
+       def get_latest(self, type):
+               return self.get(type=type, limit=5)
+
+
+
+class Update(base.Object):
+       def __init__(self, pakfire, data):
+               base.Object.__init__(self, pakfire)
+
+               self.data = data
+
+               self._build = None
+
+       @property
+       def build(self):
+               if self._build is None:
+                       self._build = self.pakfire.builds.get_by_id(self.data.build_id)
+                       assert self._build
+
+               return self._build
+
+       @property
+       def name(self):
+               return self.build.name
+
+       @property
+       def description(self):
+               return self.build.message
+
+       @property
+       def summary(self):
+               line = None
+               for line in self.description.splitlines():
+                       if not line:
+                               continue
+
+                       break
+
+               if len(line) >= 60:
+                       line = "%s..." % line[:60]
+
+               return line
+
+       @property
+       def severity(self):
+               return self.build.severity
+
+       @property
+       def distro(self):
+               return self.build.distro
+
+       @property
+       def score(self):
+               return self.build.score
+
+       @property
+       def when(self):
+               return self.data.time_added
index eb31b2b5e57a500cfe80edc93ec475fecc254d6e..a702da7de9ed083e999d3bc85b771a6ecf1eb321 100644 (file)
@@ -1,11 +1,16 @@
 #!/usr/bin/python
 
+from __future__ import division
+
+import datetime
 import hashlib
 import logging
 import os
-import pakfire.packages
+import shutil
 import uuid
 
+import pakfire.packages
+
 import base
 import misc
 import packages
@@ -18,11 +23,8 @@ class Uploads(base.Object):
 
                return Upload(self.pakfire, upload.id)
 
-       def new(self, *args, **kwargs):
-               return Upload.new(self.pakfire, *args, **kwargs)
-
        def get_all(self):
-               uploads = self.db.query("SELECT id FROM uploads")
+               uploads = self.db.query("SELECT id FROM uploads ORDER BY time_started DESC")
 
                return [Upload(self.pakfire, u.id) for u in uploads]
 
@@ -39,11 +41,19 @@ class Upload(base.Object):
                self.data = self.db.get("SELECT * FROM uploads WHERE id = %s", self.id)
 
        @classmethod
-       def new(cls, pakfire, builder, filename, size, hash):
-               _uuid = uuid.uuid4()
+       def create(cls, pakfire, filename, size, hash, builder=None, user=None):
+               assert builder or user
 
-               id = pakfire.db.execute("INSERT INTO uploads(uuid, builder, filename, size, hash)"
-                       " VALUES(%s, %s, %s, %s, %s)", _uuid, builder.id, filename, size, hash)
+               id = pakfire.db.execute("INSERT INTO uploads(uuid, filename, size, hash) \
+                       VALUES(%s, %s, %s, %s)", "%s" % uuid.uuid4(), filename, size, hash)
+
+               if builder:
+                       pakfire.db.execute("UPDATE uploads SET builder_id = %s WHERE id = %s",
+                               builder.id, id)
+
+               elif user:
+                       pakfire.db.execute("UPDATE uploads SET user_id = %s WHERE id = %s",
+                               user.id, id)
 
                upload = cls(pakfire, id)
 
@@ -74,75 +84,100 @@ class Upload(base.Object):
        def path(self):
                return os.path.join(UPLOADS_DIR, self.uuid, self.filename)
 
+       @property
+       def size(self):
+               return self.data.size
+
+       @property
+       def progress(self):
+               return self.data.progress / self.size
+
        @property
        def builder(self):
-               return self.pakfire.builders.get_by_id(self.data.builder)
+               if self.data.builder_id:
+                       return self.pakfire.builders.get_by_id(self.data.builder_id)
+
+       @property
+       def user(self):
+               if self.data.user_id:
+                       return self.pakfire.users.get_by_id(self.data.user_id)
 
        def append(self, data):
+               # Check if the filesize was exceeded.
+               size = os.path.getsize(self.path) + len(data)
+               if size > self.data.size:
+                       raise Exception, "Given filesize was exceeded for upload %s" % self.uuid
+
                logging.debug("Writing %s bytes to %s" % (len(data), self.path))
 
                f = open(self.path, "ab")
                f.write(data)
                f.close()
 
+               self.db.execute("UPDATE uploads SET progress = %s WHERE id = %s",
+                       size, self.id)
+
        def validate(self):
+               size = os.path.getsize(self.path)
+               if not size == self.data.size:
+                       logging.error("Filesize is not okay: %s" % (self.uuid))
+                       return False
+
                # Calculate a hash to validate the upload.
                hash = misc.calc_hash1(self.path)
 
-               ret = self.hash == hash
-
-               if not ret:
+               if not self.hash == hash:
                        logging.error("Hash did not match: %s != %s" % (self.hash, hash))
+                       return False
+
+               return True
 
-               return ret
+       def finished(self):
+               """
+                       Update the status of the upload in the database to "finished".
+               """
+               # Check if the file was completely uploaded and the hash is correct.
+               # If not, the upload has failed.
+               if not self.validate():
+                       return False
+
+               self.db.execute("UPDATE uploads SET finished = 'Y', time_finished = NOW() \
+                       WHERE id = %s", self.id)
+
+               return True
 
        def remove(self):
                # Remove the uploaded data.
-               if os.path.exists(self.path):
-                       os.unlink(self.path)
+               path = os.path.dirname(self.path)
+               if os.path.exists(path):
+                       shutil.rmtree(path, ignore_errors=True)
 
                # Delete the upload from the database.
                self.db.execute("DELETE FROM uploads WHERE id = %s", self.id)
 
-       def time_start(self):
-               return self.data.time_start
-
-       def commit(self, build):
-               # Find out what kind of file this is.
-               filetype = misc.guess_filetype(self.path)
-
-               # If the filetype is unhandled, we remove the file and raise an
-               # exception.
-               if filetype == "unknown":
-                       self.remove()
-                       raise Exception, "Cannot handle unknown file."
-
-               # If file is a package we open it and insert its information to the
-               # database.
-               if filetype == "pkg":
-                       logging.debug("%s is a package file." % self.path)
-                       file = pakfire.packages.open(None, None, self.path)
-
-                       if file.type == "source":
-                               packages.Package.new(self.pakfire, file, build)
-
-                       elif file.type == "binary":
-                               build.pkg.add_file(file, build)
-
-               elif filetype == "log":
-                       build.add_log(self.path)
-
-               # Finally, remove the upload.
-               self.remove()
+       @property
+       def time_started(self):
+               return self.data.time_started
 
-       def cleanup(self):
+       @property
+       def time_running(self):
                # Get the seconds since we are running.
                try:
-                       time_running = datetime.datetime.utcnow() - self.time_start
+                       time_running = datetime.datetime.utcnow() - self.time_started
                        time_running = time_running.total_seconds()
                except:
                        time_running = 0
 
-               # Remove uploads that are older than 24 hours.
-               if time_running >= 3600 * 24:
+               return time_running
+
+       @property
+       def speed(self):
+               if not self.time_running:
+                       return 0
+
+               return self.data.progress / self.time_running
+
+       def cleanup(self):
+               # Remove uploads that are older than 2 hours.
+               if self.time_running >= 3600 * 2:
                        self.remove()
index 0dbe24637a7ac3e4a8011ebfb4b775b3963531b7..c86b796dc90cb7233253ce22960ba53b7a5bca98 100644 (file)
@@ -2,7 +2,9 @@
 
 import hashlib
 import logging
+import pytz
 import random
+import re
 import string
 import urllib
 
@@ -10,21 +12,127 @@ import tornado.locale
 
 import base
 
+# A list of possible random characters.
+random_chars = string.ascii_letters + string.digits
+
+def generate_random_string(length=16):
+       """
+               Return a string with random chararcters A-Za-z0-9 with given length.
+       """
+       return "".join([random.choice(random_chars) for i in range(length)])
+
+
+def generate_password_hash(password, salt=None, algo="sha512"):
+       """
+               This function creates a salted digest of the given password.
+       """
+       # Generate the salt (length = 16) of none was given.
+       if salt is None:
+               salt = generate_random_string(length=16)
+
+       # Compute the hash.
+       # <SALT> + <PASSWORD>
+       if not algo in hashlib.algorithms:
+               raise Exception, "Unsupported password hash algorithm: %s" % algo
+
+       # Calculate the digest.
+       h = hashlib.new(algo)
+       h.update(salt)
+       h.update(password)
+
+       # Output string is of kind "<algo>$<salt>$<hash>".
+       return "$".join((algo, salt, h.hexdigest()))
+
+def check_password_hash(password, password_hash):
+       """
+               Check a plain-text password with the given digest.
+       """
+       # Handle plaintext passwords (plain$<password>).
+       if password_hash.startswith("plain$"):
+               return password_hash[6:] == password
+
+       try:
+               algo, salt, digest = password_hash.split("$", 2)
+       except ValueError:
+               logging.warning("Unknown password hash: %s" % password_hash)
+               return False
+
+       # Re-generate the password hash and compare the result.
+       return password_hash == generate_password_hash(password, salt=salt, algo=algo)
+
+def check_password_strength(password):
+       score = 0
+       accepted = False
+
+       # Empty passwords cannot be used.
+       if len(password) == 0:
+               return False, 0
+
+       # Passwords with less than 6 characters are also too weak.
+       if len(password) < 6:
+               return False, 1
+
+       # Password with at least 8 characters are secure.
+       if len(password) >= 8:
+               score += 1
+
+       # 10 characters are even more secure.
+       if len(password) >= 10:
+               score += 1
+
+       # Digits in the password are good.
+       if re.search("\d+", password):
+               score += 1
+
+       # Check for lowercase AND uppercase characters.
+       if re.search("[a-z]", password) and re.search("[A-Z]", password):
+               score += 1
+
+       # Search for special characters.
+       if re.search(".[!,@,#,$,%,^,&,*,?,_,~,-,(,)]", password):
+               score += 1
+
+       if score >= 3:
+               accepted = True
+
+       return accepted, score
+
+def maintainer_split(s):
+       m = re.match(r"(.*) <(.*)>", s)
+       if m:
+               name, email = m.groups()
+       else:
+               name, email = None, None
+
+       return name, email
+
 class Users(base.Object):
-       def auth(self, name, passphrase):
-               # If either name or passphrase is None, we don't check at all.
-               if None in (name, passphrase):
+       def auth(self, name, password):
+               # If either name or password is None, we don't check at all.
+               if None in (name, password):
                        return
 
-               user = self.db.get("""SELECT id FROM users WHERE name = %s
-                       AND passphrase = SHA1(%s) AND activated = 'Y' AND deleted = 'N'""",
-                       name, passphrase)
+               # Search for the username in the database.
+               # The user must not be deleted and must be activated.
+               user = self.db.get("SELECT id FROM users WHERE name = %s AND \
+                       activated = 'Y' AND deleted = 'N'", name)
 
-               if user:
-                       return User(self.pakfire, user.id)
+               if not user:
+                       return
 
-       def register(self, name, passphrase, email, realname, locale=None):
-               return User.new(self.pakfire, name, passphrase, email, realname, locale)
+               # Get the whole User object from the database.
+               user = self.get_by_id(user.id)
+
+               # If the user was not found or the password does not match,
+               # you aren't lucky.
+               if not user or not user.check_password(password):
+                       return
+
+               # Otherwise we return the User object.
+               return user
+
+       def register(self, name, password, email, realname, locale=None):
+               return User.new(self.pakfire, name, password, email, realname, locale)
 
        def name_is_used(self, name):
                users = self.db.query("SELECT id FROM users WHERE name = %s", name)
@@ -35,7 +143,7 @@ class Users(base.Object):
                return False
 
        def email_is_used(self, email):
-               users = self.db.query("SELECT id FROM users WHERE email = %s", email)
+               users = self.db.query("SELECT id FROM users_emails WHERE email = %s", email)
 
                if users:
                        return True
@@ -49,10 +157,7 @@ class Users(base.Object):
                return [User(self.pakfire, u.id) for u in users]
 
        def get_by_id(self, id):
-               user = self.db.get("SELECT id FROM users WHERE id = %s LIMIT 1", id)
-
-               if user:
-                       return User(self.pakfire, user.id)
+               return User(self.pakfire, id)
 
        def get_by_name(self, name):
                user = self.db.get("SELECT id FROM users WHERE name = %s LIMIT 1", name)
@@ -61,27 +166,88 @@ class Users(base.Object):
                        return User(self.pakfire, user.id)
 
        def get_by_email(self, email):
-               user = self.db.get("SELECT id FROM users WHERE email = %s LIMIT 1", email)
+               user = self.db.get("SELECT user_id AS id FROM users_emails \
+                       WHERE email = %s LIMIT 1", email)
 
                if user:
                        return User(self.pakfire, user.id)
 
+       def count(self):
+               count = self.cache.get("users_count")
+               if count is None:
+                       users = self.db.get("SELECT COUNT(*) AS count FROM users \
+                               WHERE activated = 'Y' AND deleted = 'N'")
+
+                       count = users.count
+                       self.cache.set("users_count", count, 3600)
+
+               return count
+
+       def search(self, pattern, limit=None):
+               query = "SELECT id FROM users \
+                       WHERE (name LIKE %s OR MATCH(name, realname) AGAINST(%s)) \
+                               AND activated = 'Y' AND deleted = 'N'"
+               args  = [pattern, pattern,]
+
+               if limit:
+                       query += " LIMIT %s"
+                       args.append(limit)
+
+               users = []
+               for user in self.db.query(query, *args):
+                       user = User(self.pakfire, user.id)
+                       users.append(user)
+
+               return users
+
+       def find_maintainer(self, s):
+               if not s:
+                       return
+
+               name, email = maintainer_split(s)
+               if not email:
+                       return
+
+               user = self.db.get("SELECT user_id FROM users_emails WHERE email = %s LIMIT 1", email)
+               if not user:
+                       return
+
+               return self.get_by_id(user.user_id)
+
 
 class User(base.Object):
        def __init__(self, pakfire, id):
                base.Object.__init__(self, pakfire)
                self.id = id
 
-               self.data = self.db.get("SELECT * FROM users WHERE id = %s" % self.id)
+               # Cache.
+               self._data = None
+               self._emails = None
+               self._perms = None
 
        def __cmp__(self, other):
-               return cmp(self.id, other.id)
+               if other is None:
+                       return 1
+
+               if isinstance(other, unicode):
+                       return cmp(self.email, other)
+
+               if self.id == other.id:
+                       return 0
+
+               return cmp(self.realname, other.realname)
 
        @classmethod
        def new(cls, pakfire, name, passphrase, email, realname, locale=None):
+               id = pakfire.db.execute("INSERT INTO users(name, passphrase, realname) \
+                       VALUES(%s, %s, %s)", name, generate_password_hash(passphrase), realname)
+
+               # Add email address.
+               pakfire.db.execute("INSERT INTO users_emails(user_id, email, `primary`) \
+                       VALUES(%s, %s, 'Y')", id, email)
 
-               id = pakfire.db.execute("""INSERT INTO users(name, passphrase, email, realname)
-                       VALUES(%s, SHA1(%s), %s, %s)""", name, passphrase, email, realname)
+               # Create row in permissions table.
+               pakfire.db.execute("INSERT INTO users_permissions(user_id) VALUES(%s)", id)
 
                user = cls(pakfire, id)
 
@@ -93,18 +259,34 @@ class User(base.Object):
 
                return user
 
+       @property
+       def data(self):
+               if self._data is None:
+                       self._data = self.db.get("SELECT * FROM users WHERE id = %s" % self.id)
+                       assert self._data, "User %s not found." % self.id
+
+               return self._data
+
        def delete(self):
                self.db.execute("UPDATE users SET deleted = 'Y' WHERE id = %s", self.id)
+               self._data = None
 
        def activate(self):
-               self.db.execute("UPDATE users SET activated = 'Y' WHERE id = %s", self.id)
+               self.db.execute("UPDATE users SET activated = 'Y', activation_code = NULL \
+                       WHERE id = %s", self.id)
+
+       def check_password(self, password):
+               """
+                       Compare the given password with the one stored in the database.
+               """
+               return check_password_hash(password, self.data.passphrase)
 
        def set_passphrase(self, passphrase):
                """
                        Update the passphrase the users uses to log on.
                """
-               self.db.execute("UPDATE users SET passphrase = SHA1(%s) WHERE id = %s",
-                       passphrase, self.id)
+               self.db.execute("UPDATE users SET passphrase = %s WHERE id = %s",
+                       generate_password_hash(passphrase), self.id)
 
        passphrase = property(lambda x: None, set_passphrase)
 
@@ -129,20 +311,35 @@ class User(base.Object):
        def name(self):
                return self.data.name
 
+       @property
+       def firstname(self):
+               firstname, rest = self.realname.split(" ", 1)
+
+               return firstname
+
        def get_email(self):
-               return self.data.email
+               if self._emails is None:
+                       self._emails = self.db.query("SELECT * FROM users_emails WHERE user_id = %s", self.id)
+                       assert self._emails
+
+               for email in self._emails:
+                       if not email.primary == "Y":
+                               continue
+
+                       return email.email
 
        def set_email(self, email):
                if email == self.email:
                        return
 
-               self.db.execute("""UPDATE users SET email = %s, activated = 'N'
-                       WHERE id = %s""", email, self.id)
+               self.db.execute("UPDATE users_emails SET email = %s \
+                       WHERE user_id = %s AND primary = 'Y'", email, self.id)
+
+               self.db.execute("UPDATE users SET activated  'N' WHERE id = %s",
+                       email, self.id)
 
-               self.data.update({
-                       "email" : email,
-                       "activated" : "N",
-               })
+               # Reset cache.
+               self._data = self._emails = None
 
                # Inform the user, that he or she has to re-activate the account.
                self.send_activation_mail()
@@ -169,6 +366,27 @@ class User(base.Object):
 
        locale = property(get_locale, set_locale)
 
+       def get_timezone(self, tz=None):
+               if tz is None:
+                       tz = self.data.timezone or ""
+
+               try:
+                       tz = pytz.timezone(tz)
+               except pytz.UnknownTimeZoneError:
+                       tz = pytz.timezone("UTC")
+
+               return tz
+
+       def set_timezone(self, timezone):
+               if not timezone is None:
+                       tz = self.get_timezone(timezone)
+                       timezone = tz.zone
+
+               self.db.execute("UPDATE users SET timezone = %s WHERE id = %s",
+                       timezone, self.id)
+
+       timezone = property(get_timezone, set_timezone)
+
        @property
        def activated(self):
                return self.data.activated == "Y"
@@ -191,6 +409,29 @@ class User(base.Object):
        def is_tester(self):
                return self.state == "tester"
 
+       @property
+       def perms(self):
+               if self._perms is None:
+                       self._perms = \
+                               self.db.get("SELECT * FROM users_permissions WHERE user_id = %s", self.id)
+
+               return self._perms
+
+       def has_perm(self, perm):
+               """
+                       Returns True if the user has the requested permission.
+               """
+               # Admins have the permission for everything.
+               if self.is_admin():
+                       return True
+
+               # Exception for voting. All testers are allowed to vote.
+               if perm == "vote" and self.is_tester():
+                       return True
+
+               # All others must be checked individually.
+               return self.perms.get(perm, "N") == "Y"
+
        def send_activation_mail(self):
                logging.debug("Sending activation mail to %s" % self.email)
 
@@ -210,23 +451,33 @@ class User(base.Object):
                message += "\n"*2
                message += _("To activate your account, please click on the link below.")
                message += "\n"*2
-               message += "    http://pakfire.ipfire.org/user/%(name)s/activate/%(activation_code)s" \
-                       % { "name" : self.name, "activation_code" : self.activation_code, }
+               message += "    %(baseurl)s/user/%(name)s/activate?code=%(activation_code)s" \
+                       % { "baseurl" : self.settings.get("baseurl"), "name" : self.name,
+                               "activation_code" : self.activation_code, }
                message += "\n"*2
                message += "Sincerely,\n    The Pakfire Build Service"
 
                self.pakfire.messages.add("%s <%s>" % (self.realname, self.email), subject, message)
 
-       @property
-       def comments(self, limit=5):
-               comments = self.db.query("""SELECT * FROM package_comments
-                       WHERE user_id = %s ORDER BY time DESC LIMIT %s""", self.id, limit)
+       def get_comments(self, limit=5):
+               comments = self.db.query("""SELECT * FROM builds_comments
+                       WHERE user_id = %s ORDER BY time_created DESC LIMIT %s""", self.id, limit)
 
                return comments
 
        @property
        def log(self):
-               log = self.db.query("SELECT * FROM log WHERE user_id = %s ORDER BY time DESC",
-                       self.id)
+               return self.get_history(limit=15)
+
+       def get_history(self, limit=None):
+               return [] # XXX TODO
+
+
+# Some testing code.
+if __name__ == "__main__":
+       for password in ("1234567890", "abcdefghij"):
+               digest = generate_password_hash(password)
+
+               print "%s %s" % (password, digest)
+               print "  Matches? %s" % check_password_hash(password, digest)
 
-               return log
diff --git a/data/static/css/bootstrap-responsive.min.css b/data/static/css/bootstrap-responsive.min.css
new file mode 100644 (file)
index 0000000..a1aadc2
--- /dev/null
@@ -0,0 +1,12 @@
+.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";}
+.clearfix:after{clear:both;}
+.hide-text{overflow:hidden;text-indent:100%;white-space:nowrap;}
+.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;}
+.hidden{display:none;visibility:hidden;}
+.visible-phone{display:none;}
+.visible-tablet{display:none;}
+.visible-desktop{display:block;}
+.hidden-phone{display:block;}
+.hidden-tablet{display:block;}
+.hidden-desktop{display:none;}
+@media (max-width:767px){.visible-phone{display:block;} .hidden-phone{display:none;} .hidden-desktop{display:block;} .visible-desktop{display:none;}}@media (min-width:768px) and (max-width:979px){.visible-tablet{display:block;} .hidden-tablet{display:none;} .hidden-desktop{display:block;} .visible-desktop{display:none;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:17px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{position:absolute;top:10px;left:10px;right:10px;width:auto;margin:0;}.modal.fade.in{top:auto;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top{margin-left:-20px;margin-right:-20px;} .container{width:auto;} .row-fluid{width:100%;} .row{margin-left:0;} .row>[class*="span"],.row-fluid>[class*="span"]{float:none;display:block;width:auto;margin:0;} .thumbnails [class*="span"]{width:auto;} input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} .input-prepend input[class*="span"],.input-append input[class*="span"]{width:auto;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:20px;} .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.762430939%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid > .span12{width:99.999999993%;} .row-fluid > .span11{width:91.436464082%;} .row-fluid > .span10{width:82.87292817100001%;} .row-fluid > .span9{width:74.30939226%;} .row-fluid > .span8{width:65.74585634900001%;} .row-fluid > .span7{width:57.182320438000005%;} .row-fluid > .span6{width:48.618784527%;} .row-fluid > .span5{width:40.055248616%;} .row-fluid > .span4{width:31.491712705%;} .row-fluid > .span3{width:22.928176794%;} .row-fluid > .span2{width:14.364640883%;} .row-fluid > .span1{width:5.801104972%;} input,textarea,.uneditable-input{margin-left:0;} input.span12, textarea.span12, .uneditable-input.span12{width:714px;} input.span11, textarea.span11, .uneditable-input.span11{width:652px;} input.span10, textarea.span10, .uneditable-input.span10{width:590px;} input.span9, textarea.span9, .uneditable-input.span9{width:528px;} input.span8, textarea.span8, .uneditable-input.span8{width:466px;} input.span7, textarea.span7, .uneditable-input.span7{width:404px;} input.span6, textarea.span6, .uneditable-input.span6{width:342px;} input.span5, textarea.span5, .uneditable-input.span5{width:280px;} input.span4, textarea.span4, .uneditable-input.span4{width:218px;} input.span3, textarea.span3, .uneditable-input.span3{width:156px;} input.span2, textarea.span2, .uneditable-input.span2{width:94px;} input.span1, textarea.span1, .uneditable-input.span1{width:32px;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top{position:static;margin-bottom:17px;} .navbar-fixed-top .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .navbar .nav-collapse{clear:left;} .navbar .nav{float:none;margin:0 0 8.5px;} .navbar .nav>li{float:none;} .navbar .nav>li>a{margin-bottom:2px;} .navbar .nav>.divider-vertical{display:none;} .navbar .nav .nav-header{color:#999999;text-shadow:none;} .navbar .nav>li>a,.navbar .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .navbar .dropdown-menu li+li a{margin-bottom:2px;} .navbar .nav>li>a:hover,.navbar .dropdown-menu a:hover{background-color:#222222;} .navbar .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .navbar .dropdown-menu:before,.navbar .dropdown-menu:after{display:none;} .navbar .dropdown-menu .divider{display:none;} .navbar-form,.navbar-search{float:none;padding:8.5px 15px;margin:8.5px 0;border-top:1px solid #222222;border-bottom:1px solid #222222;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);} .navbar .nav.pull-right{float:none;margin-left:0;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;} .btn-navbar{display:block;} .nav-collapse{overflow:hidden;height:0;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:30px;} .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.564102564%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid > .span12{width:100%;} .row-fluid > .span11{width:91.45299145300001%;} .row-fluid > .span10{width:82.905982906%;} .row-fluid > .span9{width:74.358974359%;} .row-fluid > .span8{width:65.81196581200001%;} .row-fluid > .span7{width:57.264957265%;} .row-fluid > .span6{width:48.717948718%;} .row-fluid > .span5{width:40.170940171000005%;} .row-fluid > .span4{width:31.623931624%;} .row-fluid > .span3{width:23.076923077%;} .row-fluid > .span2{width:14.529914530000001%;} .row-fluid > .span1{width:5.982905983%;} input,textarea,.uneditable-input{margin-left:0;} input.span12, textarea.span12, .uneditable-input.span12{width:1160px;} input.span11, textarea.span11, .uneditable-input.span11{width:1060px;} input.span10, textarea.span10, .uneditable-input.span10{width:960px;} input.span9, textarea.span9, .uneditable-input.span9{width:860px;} input.span8, textarea.span8, .uneditable-input.span8{width:760px;} input.span7, textarea.span7, .uneditable-input.span7{width:660px;} input.span6, textarea.span6, .uneditable-input.span6{width:560px;} input.span5, textarea.span5, .uneditable-input.span5{width:460px;} input.span4, textarea.span4, .uneditable-input.span4{width:360px;} input.span3, textarea.span3, .uneditable-input.span3{width:260px;} input.span2, textarea.span2, .uneditable-input.span2{width:160px;} input.span1, textarea.span1, .uneditable-input.span1{width:60px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;}}
diff --git a/data/static/css/bootstrap.min.css b/data/static/css/bootstrap.min.css
new file mode 100644 (file)
index 0000000..356943f
--- /dev/null
@@ -0,0 +1,689 @@
+article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
+audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
+audio:not([controls]){display:none;}
+html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
+a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
+a:hover,a:active{outline:0;}
+sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;}
+sup{top:-0.5em;}
+sub{bottom:-0.25em;}
+img{height:auto;border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;}
+button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;}
+button,input{*overflow:visible;line-height:normal;}
+button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;}
+button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
+input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;}
+input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;}
+textarea{overflow:auto;vertical-align:top;}
+.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";}
+.clearfix:after{clear:both;}
+.hide-text{overflow:hidden;text-indent:100%;white-space:nowrap;}
+.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;}
+body{margin:0;font-family:"Ubuntu","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:17px;color:#333333;background-color:#ffffff;}
+a{color:#880400;text-decoration:none;}
+a:hover{color:#3c0200;text-decoration:underline;}
+.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";}
+.row:after{clear:both;}
+[class*="span"]{float:left;margin-left:20px;}
+.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
+.span12{width:940px;}
+.span11{width:860px;}
+.span10{width:780px;}
+.span9{width:700px;}
+.span8{width:620px;}
+.span7{width:540px;}
+.span6{width:460px;}
+.span5{width:380px;}
+.span4{width:300px;}
+.span3{width:220px;}
+.span2{width:140px;}
+.span1{width:60px;}
+.offset12{margin-left:980px;}
+.offset11{margin-left:900px;}
+.offset10{margin-left:820px;}
+.offset9{margin-left:740px;}
+.offset8{margin-left:660px;}
+.offset7{margin-left:580px;}
+.offset6{margin-left:500px;}
+.offset5{margin-left:420px;}
+.offset4{margin-left:340px;}
+.offset3{margin-left:260px;}
+.offset2{margin-left:180px;}
+.offset1{margin-left:100px;}
+.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";}
+.row-fluid:after{clear:both;}
+.row-fluid>[class*="span"]{float:left;margin-left:2.127659574%;}
+.row-fluid>[class*="span"]:first-child{margin-left:0;}
+.row-fluid > .span12{width:99.99999998999999%;}
+.row-fluid > .span11{width:91.489361693%;}
+.row-fluid > .span10{width:82.97872339599999%;}
+.row-fluid > .span9{width:74.468085099%;}
+.row-fluid > .span8{width:65.95744680199999%;}
+.row-fluid > .span7{width:57.446808505%;}
+.row-fluid > .span6{width:48.93617020799999%;}
+.row-fluid > .span5{width:40.425531911%;}
+.row-fluid > .span4{width:31.914893614%;}
+.row-fluid > .span3{width:23.404255317%;}
+.row-fluid > .span2{width:14.89361702%;}
+.row-fluid > .span1{width:6.382978723%;}
+.container{margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";}
+.container:after{clear:both;}
+.container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";}
+.container-fluid:after{clear:both;}
+p{margin:0 0 8.5px;font-family:"Ubuntu","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:17px;}p small{font-size:13px;color:#999999;}
+.lead{margin-bottom:17px;font-size:20px;font-weight:200;line-height:25.5px;}
+h1,h2,h3,h4,h5,h6{margin:0;font-family:inherit;font-weight:bold;color:inherit;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;}
+h1{font-size:30px;line-height:34px;}h1 small{font-size:18px;}
+h2{font-size:24px;line-height:34px;}h2 small{font-size:18px;}
+h3{line-height:25.5px;font-size:18px;}h3 small{font-size:14px;}
+h4,h5,h6{line-height:17px;}
+h4{font-size:14px;}h4 small{font-size:12px;}
+h5{font-size:12px;}
+h6{font-size:11px;color:#999999;text-transform:uppercase;}
+.page-header{padding-bottom:16px;margin:17px 0;border-bottom:1px solid #eeeeee;}
+.page-header h1{line-height:1;}
+ul,ol{padding:0;margin:0 0 8.5px 25px;}
+ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
+ul{list-style:disc;}
+ol{list-style:decimal;}
+li{line-height:17px;}
+ul.unstyled,ol.unstyled{margin-left:0;list-style:none;}
+dl{margin-bottom:17px;}
+dt,dd{line-height:17px;}
+dt{font-weight:bold;line-height:16px;}
+dd{margin-left:8.5px;}
+.dl-horizontal dt{float:left;clear:left;width:120px;text-align:right;}
+.dl-horizontal dd{margin-left:130px;}
+hr{margin:17px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;}
+strong{font-weight:bold;}
+em{font-style:italic;}
+.muted{color:#999999;}
+abbr[title]{border-bottom:1px dotted #ddd;cursor:help;}
+abbr.initialism{font-size:90%;text-transform:uppercase;}
+blockquote{padding:0 0 0 15px;margin:0 0 17px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:21.25px;}
+blockquote small{display:block;line-height:17px;color:#999999;}blockquote small:before{content:'\2014 \00A0';}
+blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;}
+q:before,q:after,blockquote:before,blockquote:after{content:"";}
+address{display:block;margin-bottom:17px;line-height:17px;font-style:normal;}
+small{font-size:100%;}
+cite{font-style:normal;}
+code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,"Courier New",monospace;font-size:14px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
+code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;}
+pre{display:block;padding:8px;margin:0 0 8.5px;font-size:13.875px;line-height:17px;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;white-space:pre;white-space:pre-wrap;word-break:break-all;word-wrap:break-word;}pre.prettyprint{margin-bottom:17px;}
+pre code{padding:0;color:inherit;background-color:transparent;border:0;}
+.pre-scrollable{max-height:340px;overflow-y:scroll;}
+form{margin:0 0 17px;}
+fieldset{padding:0;margin:0;border:0;}
+legend{display:block;width:100%;padding:0;margin-bottom:25.5px;font-size:22.5px;line-height:34px;color:#333333;border:0;border-bottom:1px solid #eee;}legend small{font-size:12.75px;color:#999999;}
+label,input,button,select,textarea{font-size:15px;font-weight:normal;line-height:17px;}
+input,button,select,textarea{font-family:"Ubuntu","Helvetica Neue",Helvetica,Arial,sans-serif;}
+label{display:block;margin-bottom:5px;color:#333333;}
+input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:17px;padding:4px;margin-bottom:9px;font-size:15px;line-height:17px;color:#555555;border:1px solid #cccccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
+.uneditable-textarea{width:auto;height:auto;}
+label input,label textarea,label select{display:block;}
+input[type="image"],input[type="checkbox"],input[type="radio"]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border:0 \9;}
+input[type="image"]{border:0;}
+input[type="file"]{width:auto;padding:initial;line-height:initial;border:initial;background-color:#ffffff;background-color:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
+input[type="button"],input[type="reset"],input[type="submit"]{width:auto;height:auto;}
+select,input[type="file"]{height:28px;*margin-top:4px;line-height:28px;}
+input[type="file"]{line-height:18px \9;}
+select{width:220px;background-color:#ffffff;}
+select[multiple],select[size]{height:auto;}
+input[type="image"]{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
+textarea{height:auto;}
+input[type="hidden"]{display:none;}
+.radio,.checkbox{padding-left:18px;}
+.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;}
+.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;}
+.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;}
+.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;}
+input,textarea{-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;}
+input:focus,textarea:focus{border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);outline:0;outline:thin dotted \9;}
+input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
+.input-mini{width:60px;}
+.input-small{width:90px;}
+.input-medium{width:150px;}
+.input-large{width:210px;}
+.input-xlarge{width:270px;}
+.input-xxlarge{width:530px;}
+input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{float:none;margin-left:0;}
+input,textarea,.uneditable-input{margin-left:0;}
+input.span12, textarea.span12, .uneditable-input.span12{width:930px;}
+input.span11, textarea.span11, .uneditable-input.span11{width:850px;}
+input.span10, textarea.span10, .uneditable-input.span10{width:770px;}
+input.span9, textarea.span9, .uneditable-input.span9{width:690px;}
+input.span8, textarea.span8, .uneditable-input.span8{width:610px;}
+input.span7, textarea.span7, .uneditable-input.span7{width:530px;}
+input.span6, textarea.span6, .uneditable-input.span6{width:450px;}
+input.span5, textarea.span5, .uneditable-input.span5{width:370px;}
+input.span4, textarea.span4, .uneditable-input.span4{width:290px;}
+input.span3, textarea.span3, .uneditable-input.span3{width:210px;}
+input.span2, textarea.span2, .uneditable-input.span2{width:130px;}
+input.span1, textarea.span1, .uneditable-input.span1{width:50px;}
+input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#eeeeee;border-color:#ddd;cursor:not-allowed;}
+.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;}
+.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;border-color:#c09853;}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:0 0 6px #dbc59e;-moz-box-shadow:0 0 6px #dbc59e;box-shadow:0 0 6px #dbc59e;}
+.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;}
+.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;}
+.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;border-color:#b94a48;}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:0 0 6px #d59392;-moz-box-shadow:0 0 6px #d59392;box-shadow:0 0 6px #d59392;}
+.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;}
+.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;}
+.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;border-color:#468847;}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:0 0 6px #7aba7b;-moz-box-shadow:0 0 6px #7aba7b;box-shadow:0 0 6px #7aba7b;}
+.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;}
+input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;}
+.form-actions{padding:16px 20px 17px;margin-top:17px;margin-bottom:17px;background-color:#eeeeee;border-top:1px solid #ddd;*zoom:1;}.form-actions:before,.form-actions:after{display:table;content:"";}
+.form-actions:after{clear:both;}
+.uneditable-input{display:block;background-color:#ffffff;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;}
+:-moz-placeholder{color:#999999;}
+::-webkit-input-placeholder{color:#999999;}
+.help-block,.help-inline{color:#555555;}
+.help-block{display:block;margin-bottom:8.5px;}
+.help-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding-left:5px;}
+.input-prepend,.input-append{margin-bottom:5px;}.input-prepend input,.input-append input,.input-prepend select,.input-append select,.input-prepend .uneditable-input,.input-append .uneditable-input{*margin-left:0;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-prepend input:focus,.input-append input:focus,.input-prepend select:focus,.input-append select:focus,.input-prepend .uneditable-input:focus,.input-append .uneditable-input:focus{position:relative;z-index:2;}
+.input-prepend .uneditable-input,.input-append .uneditable-input{border-left-color:#ccc;}
+.input-prepend .add-on,.input-append .add-on{display:inline-block;width:auto;min-width:16px;height:17px;padding:4px 5px;font-weight:normal;line-height:17px;text-align:center;text-shadow:0 1px 0 #ffffff;vertical-align:middle;background-color:#eeeeee;border:1px solid #ccc;}
+.input-prepend .add-on,.input-append .add-on,.input-prepend .btn,.input-append .btn{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
+.input-prepend .active,.input-append .active{background-color:#a9dba9;border-color:#46a546;}
+.input-prepend .add-on,.input-prepend .btn{margin-right:-1px;}
+.input-append input,.input-append select .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
+.input-append .uneditable-input{border-left-color:#eee;border-right-color:#ccc;}
+.input-append .add-on,.input-append .btn{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
+.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
+.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
+.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
+.search-query{padding-left:14px;padding-right:14px;margin-bottom:0;-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;}
+.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;margin-bottom:0;}
+.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;}
+.form-search label,.form-inline label{display:inline-block;}
+.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0;}
+.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle;}
+.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-left:0;margin-right:3px;}
+.control-group{margin-bottom:8.5px;}
+legend+.control-group{margin-top:17px;-webkit-margin-top-collapse:separate;}
+.form-horizontal .control-group{margin-bottom:17px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";}
+.form-horizontal .control-group:after{clear:both;}
+.form-horizontal .control-label{float:left;width:140px;padding-top:5px;text-align:right;}
+.form-horizontal .controls{margin-left:160px;*display:inline-block;*margin-left:0;*padding-left:20px;}
+.form-horizontal .help-block{margin-top:8.5px;margin-bottom:0;}
+.form-horizontal .form-actions{padding-left:160px;}
+table{max-width:100%;border-collapse:collapse;border-spacing:0;background-color:transparent;}
+.table{width:100%;margin-bottom:17px;}.table th,.table td{padding:8px;line-height:17px;text-align:left;vertical-align:top;border-top:1px solid #dddddd;}
+.table th{font-weight:bold;}
+.table thead th{vertical-align:bottom;}
+.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0;}
+.table tbody+tbody{border-top:2px solid #dddddd;}
+.table-condensed th,.table-condensed td{padding:4px 5px;}
+.table-bordered{border:1px solid #dddddd;border-left:0;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd;}
+.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;}
+.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;}
+.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;}
+.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;}
+.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;}
+.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
+.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;}
+table .span1{float:none;width:44px;margin-left:0;}
+table .span2{float:none;width:124px;margin-left:0;}
+table .span3{float:none;width:204px;margin-left:0;}
+table .span4{float:none;width:284px;margin-left:0;}
+table .span5{float:none;width:364px;margin-left:0;}
+table .span6{float:none;width:444px;margin-left:0;}
+table .span7{float:none;width:524px;margin-left:0;}
+table .span8{float:none;width:604px;margin-left:0;}
+table .span9{float:none;width:684px;margin-left:0;}
+table .span10{float:none;width:764px;margin-left:0;}
+table .span11{float:none;width:844px;margin-left:0;}
+table .span12{float:none;width:924px;margin-left:0;}
+table .span13{float:none;width:1004px;margin-left:0;}
+table .span14{float:none;width:1084px;margin-left:0;}
+table .span15{float:none;width:1164px;margin-left:0;}
+table .span16{float:none;width:1244px;margin-left:0;}
+table .span17{float:none;width:1324px;margin-left:0;}
+table .span18{float:none;width:1404px;margin-left:0;}
+table .span19{float:none;width:1484px;margin-left:0;}
+table .span20{float:none;width:1564px;margin-left:0;}
+table .span21{float:none;width:1644px;margin-left:0;}
+table .span22{float:none;width:1724px;margin-left:0;}
+table .span23{float:none;width:1804px;margin-left:0;}
+table .span24{float:none;width:1884px;margin-left:0;}
+[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;line-height:14px;vertical-align:text-top;background-image:url("../images/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;*margin-right:.3em;}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0;}
+.icon-white{background-image:url("../images/glyphicons-halflings-white.png");}
+.icon-glass{background-position:0 0;}
+.icon-music{background-position:-24px 0;}
+.icon-search{background-position:-48px 0;}
+.icon-envelope{background-position:-72px 0;}
+.icon-heart{background-position:-96px 0;}
+.icon-star{background-position:-120px 0;}
+.icon-star-empty{background-position:-144px 0;}
+.icon-user{background-position:-168px 0;}
+.icon-film{background-position:-192px 0;}
+.icon-th-large{background-position:-216px 0;}
+.icon-th{background-position:-240px 0;}
+.icon-th-list{background-position:-264px 0;}
+.icon-ok{background-position:-288px 0;}
+.icon-remove{background-position:-312px 0;}
+.icon-zoom-in{background-position:-336px 0;}
+.icon-zoom-out{background-position:-360px 0;}
+.icon-off{background-position:-384px 0;}
+.icon-signal{background-position:-408px 0;}
+.icon-cog{background-position:-432px 0;}
+.icon-trash{background-position:-456px 0;}
+.icon-home{background-position:0 -24px;}
+.icon-file{background-position:-24px -24px;}
+.icon-time{background-position:-48px -24px;}
+.icon-road{background-position:-72px -24px;}
+.icon-download-alt{background-position:-96px -24px;}
+.icon-download{background-position:-120px -24px;}
+.icon-upload{background-position:-144px -24px;}
+.icon-inbox{background-position:-168px -24px;}
+.icon-play-circle{background-position:-192px -24px;}
+.icon-repeat{background-position:-216px -24px;}
+.icon-refresh{background-position:-240px -24px;}
+.icon-list-alt{background-position:-264px -24px;}
+.icon-lock{background-position:-287px -24px;}
+.icon-flag{background-position:-312px -24px;}
+.icon-headphones{background-position:-336px -24px;}
+.icon-volume-off{background-position:-360px -24px;}
+.icon-volume-down{background-position:-384px -24px;}
+.icon-volume-up{background-position:-408px -24px;}
+.icon-qrcode{background-position:-432px -24px;}
+.icon-barcode{background-position:-456px -24px;}
+.icon-tag{background-position:0 -48px;}
+.icon-tags{background-position:-25px -48px;}
+.icon-book{background-position:-48px -48px;}
+.icon-bookmark{background-position:-72px -48px;}
+.icon-print{background-position:-96px -48px;}
+.icon-camera{background-position:-120px -48px;}
+.icon-font{background-position:-144px -48px;}
+.icon-bold{background-position:-167px -48px;}
+.icon-italic{background-position:-192px -48px;}
+.icon-text-height{background-position:-216px -48px;}
+.icon-text-width{background-position:-240px -48px;}
+.icon-align-left{background-position:-264px -48px;}
+.icon-align-center{background-position:-288px -48px;}
+.icon-align-right{background-position:-312px -48px;}
+.icon-align-justify{background-position:-336px -48px;}
+.icon-list{background-position:-360px -48px;}
+.icon-indent-left{background-position:-384px -48px;}
+.icon-indent-right{background-position:-408px -48px;}
+.icon-facetime-video{background-position:-432px -48px;}
+.icon-picture{background-position:-456px -48px;}
+.icon-pencil{background-position:0 -72px;}
+.icon-map-marker{background-position:-24px -72px;}
+.icon-adjust{background-position:-48px -72px;}
+.icon-tint{background-position:-72px -72px;}
+.icon-edit{background-position:-96px -72px;}
+.icon-share{background-position:-120px -72px;}
+.icon-check{background-position:-144px -72px;}
+.icon-move{background-position:-168px -72px;}
+.icon-step-backward{background-position:-192px -72px;}
+.icon-fast-backward{background-position:-216px -72px;}
+.icon-backward{background-position:-240px -72px;}
+.icon-play{background-position:-264px -72px;}
+.icon-pause{background-position:-288px -72px;}
+.icon-stop{background-position:-312px -72px;}
+.icon-forward{background-position:-336px -72px;}
+.icon-fast-forward{background-position:-360px -72px;}
+.icon-step-forward{background-position:-384px -72px;}
+.icon-eject{background-position:-408px -72px;}
+.icon-chevron-left{background-position:-432px -72px;}
+.icon-chevron-right{background-position:-456px -72px;}
+.icon-plus-sign{background-position:0 -96px;}
+.icon-minus-sign{background-position:-24px -96px;}
+.icon-remove-sign{background-position:-48px -96px;}
+.icon-ok-sign{background-position:-72px -96px;}
+.icon-question-sign{background-position:-96px -96px;}
+.icon-info-sign{background-position:-120px -96px;}
+.icon-screenshot{background-position:-144px -96px;}
+.icon-remove-circle{background-position:-168px -96px;}
+.icon-ok-circle{background-position:-192px -96px;}
+.icon-ban-circle{background-position:-216px -96px;}
+.icon-arrow-left{background-position:-240px -96px;}
+.icon-arrow-right{background-position:-264px -96px;}
+.icon-arrow-up{background-position:-289px -96px;}
+.icon-arrow-down{background-position:-312px -96px;}
+.icon-share-alt{background-position:-336px -96px;}
+.icon-resize-full{background-position:-360px -96px;}
+.icon-resize-small{background-position:-384px -96px;}
+.icon-plus{background-position:-408px -96px;}
+.icon-minus{background-position:-433px -96px;}
+.icon-asterisk{background-position:-456px -96px;}
+.icon-exclamation-sign{background-position:0 -120px;}
+.icon-gift{background-position:-24px -120px;}
+.icon-leaf{background-position:-48px -120px;}
+.icon-fire{background-position:-72px -120px;}
+.icon-eye-open{background-position:-96px -120px;}
+.icon-eye-close{background-position:-120px -120px;}
+.icon-warning-sign{background-position:-144px -120px;}
+.icon-plane{background-position:-168px -120px;}
+.icon-calendar{background-position:-192px -120px;}
+.icon-random{background-position:-216px -120px;}
+.icon-comment{background-position:-240px -120px;}
+.icon-magnet{background-position:-264px -120px;}
+.icon-chevron-up{background-position:-288px -120px;}
+.icon-chevron-down{background-position:-313px -119px;}
+.icon-retweet{background-position:-336px -120px;}
+.icon-shopping-cart{background-position:-360px -120px;}
+.icon-folder-close{background-position:-384px -120px;}
+.icon-folder-open{background-position:-408px -120px;}
+.icon-resize-vertical{background-position:-432px -119px;}
+.icon-resize-horizontal{background-position:-456px -118px;}
+.dropdown{position:relative;}
+.dropdown-toggle{*margin-bottom:-3px;}
+.dropdown-toggle:active,.open .dropdown-toggle{outline:0;}
+.caret{display:inline-block;width:0;height:0;vertical-align:top;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000000;opacity:0.3;filter:alpha(opacity=30);content:"";}
+.dropdown .caret{margin-top:8px;margin-left:2px;}
+.dropdown:hover .caret,.open.dropdown .caret{opacity:1;filter:alpha(opacity=100);}
+.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;float:left;display:none;min-width:160px;padding:4px 0;margin:0;list-style:none;background-color:#ffffff;border-color:#ccc;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:1px;-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;*border-right-width:2px;*border-bottom-width:2px;}.dropdown-menu.pull-right{right:0;left:auto;}
+.dropdown-menu .divider{height:1px;margin:7.5px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;}
+.dropdown-menu a{display:block;padding:3px 15px;clear:both;font-weight:normal;line-height:17px;color:#333333;white-space:nowrap;}
+.dropdown-menu li>a:hover,.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;background-color:#880400;}
+.dropdown.open{*z-index:1000;}.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);}
+.dropdown.open .dropdown-menu{display:block;}
+.pull-right .dropdown-menu{left:auto;right:0;}
+.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000000;content:"\2191";}
+.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px;}
+.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);}
+.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
+.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
+.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;}
+.collapse{-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-ms-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;position:relative;overflow:hidden;height:0;}.collapse.in{height:auto;}
+.close{float:right;font-size:20px;font-weight:bold;line-height:17px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;opacity:0.4;filter:alpha(opacity=40);cursor:pointer;}
+.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 10px 4px;margin-bottom:0;font-size:15px;line-height:17px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-ms-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(top, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);border:1px solid #cccccc;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;*margin-left:.3em;}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;}
+.btn:active,.btn.active{background-color:#cccccc \9;}
+.btn:first-child{*margin-left:0;}
+.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;}
+.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
+.btn.active,.btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;outline:0;}
+.btn.disabled,.btn[disabled]{cursor:default;background-image:none;background-color:#e6e6e6;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
+.btn-large{padding:9px 14px;font-size:17px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
+.btn-large [class^="icon-"]{margin-top:1px;}
+.btn-small{padding:5px 9px;font-size:13px;line-height:15px;}
+.btn-small [class^="icon-"]{margin-top:-1px;}
+.btn-mini{padding:2px 6px;font-size:13px;line-height:13px;}
+.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;}
+.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255, 255, 255, 0.75);}
+.btn-primary{background-color:#881200;background-image:-moz-linear-gradient(top, #880400, #882600);background-image:-ms-linear-gradient(top, #880400, #882600);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#880400), to(#882600));background-image:-webkit-linear-gradient(top, #880400, #882600);background-image:-o-linear-gradient(top, #880400, #882600);background-image:linear-gradient(top, #880400, #882600);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#880400', endColorstr='#882600', GradientType=0);border-color:#882600 #882600 #3c1100;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#882600;}
+.btn-primary:active,.btn-primary.active{background-color:#551800 \9;}
+.btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;}
+.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;}
+.btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;}
+.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;}
+.btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;}
+.btn-success:active,.btn-success.active{background-color:#408140 \9;}
+.btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;}
+.btn-info:active,.btn-info.active{background-color:#24748c \9;}
+.btn-inverse{background-color:#414141;background-image:-moz-linear-gradient(top, #555555, #222222);background-image:-ms-linear-gradient(top, #555555, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222));background-image:-webkit-linear-gradient(top, #555555, #222222);background-image:-o-linear-gradient(top, #555555, #222222);background-image:linear-gradient(top, #555555, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#222222;}
+.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9;}
+button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;}
+button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px;}
+button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px;}
+button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px;}
+.btn-group{position:relative;*zoom:1;*margin-left:.3em;}.btn-group:before,.btn-group:after{display:table;content:"";}
+.btn-group:after{clear:both;}
+.btn-group:first-child{*margin-left:0;}
+.btn-group+.btn-group{margin-left:5px;}
+.btn-toolbar{margin-top:8.5px;margin-bottom:8.5px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;}
+.btn-group .btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
+.btn-group .btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;}
+.btn-group .btn:last-child,.btn-group .dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;}
+.btn-group .btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;}
+.btn-group .btn.large:last-child,.btn-group .large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;}
+.btn-group .btn:hover,.btn-group .btn:focus,.btn-group .btn:active,.btn-group .btn.active{z-index:2;}
+.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;}
+.btn-group .dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:3px;*padding-bottom:3px;}
+.btn-group .btn-mini.dropdown-toggle{padding-left:5px;padding-right:5px;*padding-top:1px;*padding-bottom:1px;}
+.btn-group .btn-small.dropdown-toggle{*padding-top:4px;*padding-bottom:4px;}
+.btn-group .btn-large.dropdown-toggle{padding-left:12px;padding-right:12px;}
+.btn-group.open{*z-index:1000;}.btn-group.open .dropdown-menu{display:block;margin-top:1px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
+.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);}
+.btn .caret{margin-top:7px;margin-left:0;}
+.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100);}
+.btn-mini .caret{margin-top:5px;}
+.btn-small .caret{margin-top:6px;}
+.btn-large .caret{margin-top:6px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
+.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:0.75;filter:alpha(opacity=75);}
+.alert{padding:8px 35px 8px 14px;margin-bottom:17px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#c09853;}
+.alert-heading{color:inherit;}
+.alert .close{position:relative;top:-2px;right:-21px;line-height:18px;}
+.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847;}
+.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48;}
+.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;}
+.alert-block{padding-top:14px;padding-bottom:14px;}
+.alert-block>p,.alert-block>ul{margin-bottom:0;}
+.alert-block p+p{margin-top:5px;}
+.nav{margin-left:0;margin-bottom:17px;list-style:none;}
+.nav>li>a{display:block;}
+.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;}
+.nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:17px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;}
+.nav li+.nav-header{margin-top:9px;}
+.nav-list{padding-left:15px;padding-right:15px;margin-bottom:0;}
+.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
+.nav-list>li>a{padding:3px 15px;}
+.nav-list>.active>a,.nav-list>.active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#880400;}
+.nav-list [class^="icon-"]{margin-right:2px;}
+.nav-list .divider{height:1px;margin:7.5px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;}
+.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";}
+.nav-tabs:after,.nav-pills:after{clear:both;}
+.nav-tabs>li,.nav-pills>li{float:left;}
+.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;}
+.nav-tabs{border-bottom:1px solid #ddd;}
+.nav-tabs>li{margin-bottom:-1px;}
+.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:17px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;}
+.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
+.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
+.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#ffffff;background-color:#880400;}
+.nav-stacked>li{float:none;}
+.nav-stacked>li>a{margin-right:0;}
+.nav-tabs.nav-stacked{border-bottom:0;}
+.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
+.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}
+.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}
+.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;}
+.nav-pills.nav-stacked>li>a{margin-bottom:3px;}
+.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;}
+.nav-tabs .dropdown-menu,.nav-pills .dropdown-menu{margin-top:1px;border-width:1px;}
+.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#880400;border-bottom-color:#880400;margin-top:6px;}
+.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#3c0200;border-bottom-color:#3c0200;}
+.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;border-bottom-color:#333333;}
+.nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;}
+.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;}
+.nav .open .caret,.nav .open.active .caret,.nav .open a:hover .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:1;filter:alpha(opacity=100);}
+.tabs-stacked .open>a:hover{border-color:#999999;}
+.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";}
+.tabbable:after{clear:both;}
+.tab-content{display:table;width:100%;}
+.tabs-below .nav-tabs,.tabs-right .nav-tabs,.tabs-left .nav-tabs{border-bottom:0;}
+.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;}
+.tab-content>.active,.pill-content>.active{display:block;}
+.tabs-below .nav-tabs{border-top:1px solid #ddd;}
+.tabs-below .nav-tabs>li{margin-top:-1px;margin-bottom:0;}
+.tabs-below .nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below .nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;}
+.tabs-below .nav-tabs .active>a,.tabs-below .nav-tabs .active>a:hover{border-color:transparent #ddd #ddd #ddd;}
+.tabs-left .nav-tabs>li,.tabs-right .nav-tabs>li{float:none;}
+.tabs-left .nav-tabs>li>a,.tabs-right .nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;}
+.tabs-left .nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;}
+.tabs-left .nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;}
+.tabs-left .nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;}
+.tabs-left .nav-tabs .active>a,.tabs-left .nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;}
+.tabs-right .nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;}
+.tabs-right .nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;}
+.tabs-right .nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;}
+.tabs-right .nav-tabs .active>a,.tabs-right .nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;}
+.navbar{*position:relative;*z-index:2;overflow:visible;margin-bottom:17px;}
+.navbar-inner{padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);}
+.navbar .container{width:auto;}
+.btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#222222;}
+.btn-navbar:active,.btn-navbar.active{background-color:#080808 \9;}
+.btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);}
+.btn-navbar .icon-bar+.icon-bar{margin-top:3px;}
+.nav-collapse.collapse{height:auto;}
+.navbar{color:#999999;}.navbar .brand:hover{text-decoration:none;}
+.navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;}
+.navbar .navbar-text{margin-bottom:0;line-height:40px;}
+.navbar .btn,.navbar .btn-group{margin-top:5px;}
+.navbar .btn-group .btn{margin-top:0;}
+.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";}
+.navbar-form:after{clear:both;}
+.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;}
+.navbar-form input,.navbar-form select{display:inline-block;margin-bottom:0;}
+.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;}
+.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;}
+.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;background-color:#626262;border:1px solid #151515;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query:-moz-placeholder{color:#cccccc;}
+.navbar-search .search-query::-webkit-input-placeholder{color:#cccccc;}
+.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;}
+.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0;}
+.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
+.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
+.navbar-fixed-top{top:0;}
+.navbar-fixed-bottom{bottom:0;}
+.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;}
+.navbar .nav.pull-right{float:right;}
+.navbar .nav>li{display:block;float:left;}
+.navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}
+.navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;}
+.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;}
+.navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;}
+.navbar .nav.pull-right{margin-left:10px;margin-right:0;}
+.navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;}
+.navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;}
+.navbar-fixed-bottom .dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0, 0, 0, 0.2);border-bottom:0;bottom:-7px;top:auto;}
+.navbar-fixed-bottom .dropdown-menu:after{border-top:6px solid #ffffff;border-bottom:0;bottom:-6px;top:auto;}
+.navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;}
+.navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);}
+.navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;}
+.navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;}
+.navbar .nav.pull-right .dropdown-menu,.navbar .nav .dropdown-menu.pull-right{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before,.navbar .nav .dropdown-menu.pull-right:before{left:auto;right:12px;}
+.navbar .nav.pull-right .dropdown-menu:after,.navbar .nav .dropdown-menu.pull-right:after{left:auto;right:13px;}
+.breadcrumb{padding:7px 14px;margin:0 0 17px;list-style:none;background-color:#fbfbfb;background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 0 #ffffff;}
+.breadcrumb .divider{padding:0 5px;color:#999999;}
+.breadcrumb .active a{color:#333333;}
+.pagination{height:34px;margin:17px 0;}
+.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);}
+.pagination li{display:inline;}
+.pagination a{float:left;padding:0 14px;line-height:32px;text-decoration:none;border:1px solid #ddd;border-left-width:0;}
+.pagination a:hover,.pagination .active a{background-color:#f5f5f5;}
+.pagination .active a{color:#999999;cursor:default;}
+.pagination .disabled span,.pagination .disabled a,.pagination .disabled a:hover{color:#999999;background-color:transparent;cursor:default;}
+.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
+.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
+.pagination-centered{text-align:center;}
+.pagination-right{text-align:right;}
+.pager{margin-left:0;margin-bottom:17px;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";}
+.pager:after{clear:both;}
+.pager li{display:inline;}
+.pager a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
+.pager a:hover{text-decoration:none;background-color:#f5f5f5;}
+.pager .next a{float:right;}
+.pager .previous a{float:left;}
+.pager .disabled a,.pager .disabled a:hover{color:#999999;background-color:#fff;cursor:default;}
+.modal-open .dropdown-menu{z-index:2050;}
+.modal-open .dropdown.open{*z-index:2050;}
+.modal-open .popover{z-index:2060;}
+.modal-open .tooltip{z-index:2070;}
+.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;}
+.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);}
+.modal{position:fixed;top:50%;left:50%;z-index:1050;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}
+.modal.fade.in{top:50%;}
+.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;}
+.modal-body{overflow-y:auto;max-height:400px;padding:15px;}
+.modal-form{margin-bottom:0;}
+.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";}
+.modal-footer:after{clear:both;}
+.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0;}
+.modal-footer .btn-group .btn+.btn{margin-left:-1px;}
+.tooltip{position:absolute;z-index:1020;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);}
+.tooltip.top{margin-top:-2px;}
+.tooltip.right{margin-left:2px;}
+.tooltip.bottom{margin-top:2px;}
+.tooltip.left{margin-left:-2px;}
+.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
+.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
+.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
+.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
+.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.tooltip-arrow{position:absolute;width:0;height:0;}
+.popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px;}.popover.top{margin-top:-5px;}
+.popover.right{margin-left:5px;}
+.popover.bottom{margin-top:5px;}
+.popover.left{margin-left:-5px;}
+.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
+.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
+.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
+.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
+.popover .arrow{position:absolute;width:0;height:0;}
+.popover-inner{padding:3px;width:280px;overflow:hidden;background:#000000;background:rgba(0, 0, 0, 0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);}
+.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;}
+.popover-content{padding:14px;background-color:#ffffff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;}
+.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";}
+.thumbnails:after{clear:both;}
+.thumbnails>li{float:left;margin:0 0 17px 20px;}
+.thumbnail{display:block;padding:4px;line-height:1;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);}
+a.thumbnail:hover{border-color:#880400;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);}
+.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;}
+.thumbnail .caption{padding:9px;}
+.label{padding:1px 4px 2px;font-size:12.69px;font-weight:bold;line-height:13px;color:#ffffff;vertical-align:middle;white-space:nowrap;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
+.label:hover{color:#ffffff;text-decoration:none;}
+.label-important{background-color:#b94a48;}
+.label-important:hover{background-color:#953b39;}
+.label-warning{background-color:#f89406;}
+.label-warning:hover{background-color:#c67605;}
+.label-success{background-color:#468847;}
+.label-success:hover{background-color:#356635;}
+.label-info{background-color:#3a87ad;}
+.label-info:hover{background-color:#2d6987;}
+.label-inverse{background-color:#333333;}
+.label-inverse:hover{background-color:#1a1a1a;}
+.badge{padding:1px 9px 2px;font-size:13.875px;font-weight:bold;white-space:nowrap;color:#ffffff;background-color:#999999;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;}
+.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;}
+.badge-error{background-color:#b94a48;}
+.badge-error:hover{background-color:#953b39;}
+.badge-warning{background-color:#f89406;}
+.badge-warning:hover{background-color:#c67605;}
+.badge-success{background-color:#468847;}
+.badge-success:hover{background-color:#356635;}
+.badge-info{background-color:#3a87ad;}
+.badge-info:hover{background-color:#2d6987;}
+.badge-inverse{background-color:#333333;}
+.badge-inverse:hover{background-color:#1a1a1a;}
+@-webkit-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}.progress{overflow:hidden;height:18px;margin-bottom:18px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-ms-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(top, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.progress .bar{width:0%;height:18px;color:#ffffff;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-ms-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(top, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-ms-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;}
+.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;}
+.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;}
+.progress-danger .bar{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);}
+.progress-danger.progress-striped .bar{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
+.progress-success .bar{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);}
+.progress-success.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
+.progress-info .bar{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);}
+.progress-info.progress-striped .bar{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
+.progress-warning .bar{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);}
+.progress-warning.progress-striped .bar{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
+.accordion{margin-bottom:17px;}
+.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.accordion-heading{border-bottom:0;}
+.accordion-heading .accordion-toggle{display:block;padding:8px 15px;}
+.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;}
+.carousel{position:relative;margin-bottom:17px;line-height:1;}
+.carousel-inner{overflow:hidden;width:100%;position:relative;}
+.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-ms-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;}
+.carousel .item>img{display:block;line-height:1;}
+.carousel .active,.carousel .next,.carousel .prev{display:block;}
+.carousel .active{left:0;}
+.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;}
+.carousel .next{left:100%;}
+.carousel .prev{left:-100%;}
+.carousel .next.left,.carousel .prev.right{left:0;}
+.carousel .active.left{left:-100%;}
+.carousel .active.right{left:100%;}
+.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;}
+.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);}
+.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:10px 15px 5px;background:#333333;background:rgba(0, 0, 0, 0.75);}
+.carousel-caption h4,.carousel-caption p{color:#ffffff;}
+.hero-unit{padding:60px;margin-bottom:30px;background-color:#eeeeee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;color:inherit;letter-spacing:-1px;}
+.hero-unit p{font-size:18px;font-weight:200;line-height:25.5px;color:inherit;}
+.pull-right{float:right;}
+.pull-left{float:left;}
+.hide{display:none;}
+.show{display:block;}
+.invisible{visibility:hidden;}
diff --git a/data/static/css/bootstrap.min.css.old b/data/static/css/bootstrap.min.css.old
new file mode 100644 (file)
index 0000000..30dcae0
--- /dev/null
@@ -0,0 +1,632 @@
+article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
+audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
+audio:not([controls]){display:none;}
+html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
+a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
+a:hover,a:active{outline:0;}
+sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;}
+sup{top:-0.5em;}
+sub{bottom:-0.25em;}
+img{max-width:100%;height:auto;border:0;-ms-interpolation-mode:bicubic;}
+button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;}
+button,input{*overflow:visible;line-height:normal;}
+button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;}
+button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
+input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;}
+input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;}
+textarea{overflow:auto;vertical-align:top;}
+.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";}
+.clearfix:after{clear:both;}
+body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;}
+a{color:#0088cc;text-decoration:none;}
+a:hover{color:#005580;text-decoration:underline;}
+.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";}
+.row:after{clear:both;}
+[class*="span"]{float:left;margin-left:20px;}
+.span1{width:60px;}
+.span2{width:140px;}
+.span3{width:220px;}
+.span4{width:300px;}
+.span5{width:380px;}
+.span6{width:460px;}
+.span7{width:540px;}
+.span8{width:620px;}
+.span9{width:700px;}
+.span10{width:780px;}
+.span11{width:860px;}
+.span12,.container{width:940px;}
+.offset1{margin-left:100px;}
+.offset2{margin-left:180px;}
+.offset3{margin-left:260px;}
+.offset4{margin-left:340px;}
+.offset5{margin-left:420px;}
+.offset6{margin-left:500px;}
+.offset7{margin-left:580px;}
+.offset8{margin-left:660px;}
+.offset9{margin-left:740px;}
+.offset10{margin-left:820px;}
+.offset11{margin-left:900px;}
+.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";}
+.row-fluid:after{clear:both;}
+.row-fluid>[class*="span"]{float:left;margin-left:2.127659574%;}
+.row-fluid>[class*="span"]:first-child{margin-left:0;}
+.row-fluid>.span1{width:6.382978723%;}
+.row-fluid>.span2{width:14.89361702%;}
+.row-fluid>.span3{width:23.404255317%;}
+.row-fluid>.span4{width:31.914893614%;}
+.row-fluid>.span5{width:40.425531911%;}
+.row-fluid>.span6{width:48.93617020799999%;}
+.row-fluid>.span7{width:57.446808505%;}
+.row-fluid>.span8{width:65.95744680199999%;}
+.row-fluid>.span9{width:74.468085099%;}
+.row-fluid>.span10{width:82.97872339599999%;}
+.row-fluid>.span11{width:91.489361693%;}
+.row-fluid>.span12{width:99.99999998999999%;}
+.container{width:940px;margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";}
+.container:after{clear:both;}
+.container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";}
+.container-fluid:after{clear:both;}
+p{margin:0 0 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;}p small{font-size:11px;color:#999999;}
+.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;}
+h1,h2,h3,h4,h5,h6{margin:0;font-weight:bold;color:#333333;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;}
+h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;}
+h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;}
+h3{line-height:27px;font-size:18px;}h3 small{font-size:14px;}
+h4,h5,h6{line-height:18px;}
+h4{font-size:14px;}h4 small{font-size:12px;}
+h5{font-size:12px;}
+h6{font-size:11px;color:#999999;text-transform:uppercase;}
+.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;}
+.page-header h1{line-height:1;}
+ul,ol{padding:0;margin:0 0 9px 25px;}
+ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
+ul{list-style:disc;}
+ol{list-style:decimal;}
+li{line-height:18px;}
+ul.unstyled,ol.unstyled{margin-left:0;list-style:none;}
+dl{margin-bottom:18px;}
+dt,dd{line-height:18px;}
+dt{font-weight:bold;}
+dd{margin-left:9px;}
+hr{margin:18px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;}
+strong{font-weight:bold;}
+em{font-style:italic;}
+.muted{color:#999999;}
+abbr{font-size:90%;text-transform:uppercase;border-bottom:1px dotted #ddd;cursor:help;}
+blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;}
+blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';}
+blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;}
+q:before,q:after,blockquote:before,blockquote:after{content:"";}
+address{display:block;margin-bottom:18px;line-height:18px;font-style:normal;}
+small{font-size:100%;}
+cite{font-style:normal;}
+code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
+code{padding:3px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;}
+pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12px;line-height:18px;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;white-space:pre;white-space:pre-wrap;word-break:break-all;word-wrap:break-word;}pre.prettyprint{margin-bottom:18px;}
+pre code{padding:0;color:inherit;background-color:transparent;border:0;}
+.pre-scrollable{max-height:340px;overflow-y:scroll;}
+form{margin:0 0 18px;}
+fieldset{padding:0;margin:0;border:0;}
+legend{display:block;width:100%;padding:0;margin-bottom:27px;font-size:19.5px;line-height:36px;color:#333333;border:0;border-bottom:1px solid #eee;}legend small{font-size:13.5px;color:#999999;}
+label,input,button,select,textarea{font-size:13px;font-weight:normal;line-height:18px;}
+input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;}
+label{display:block;margin-bottom:5px;color:#333333;}
+input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:18px;padding:4px;margin-bottom:9px;font-size:13px;line-height:18px;color:#555555;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
+.uneditable-textarea{width:auto;height:auto;}
+label input,label textarea,label select{display:block;}
+input[type="image"],input[type="checkbox"],input[type="radio"]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border:0 \9;}
+input[type="image"]{border:0;}
+input[type="file"]{width:auto;padding:initial;line-height:initial;border:initial;background-color:#ffffff;background-color:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
+input[type="button"],input[type="reset"],input[type="submit"]{width:auto;height:auto;}
+select,input[type="file"]{height:28px;*margin-top:4px;line-height:28px;}
+input[type="file"]{line-height:18px \9;}
+select{width:220px;background-color:#ffffff;}
+select[multiple],select[size]{height:auto;}
+input[type="image"]{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
+textarea{height:auto;}
+input[type="hidden"]{display:none;}
+.radio,.checkbox{padding-left:18px;}
+.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;}
+.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;}
+.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;}
+.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;}
+input,textarea{-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;}
+input:focus,textarea:focus{border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);outline:0;outline:thin dotted \9;}
+input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
+.input-mini{width:60px;}
+.input-small{width:90px;}
+.input-medium{width:150px;}
+.input-large{width:210px;}
+.input-xlarge{width:270px;}
+.input-xxlarge{width:530px;}
+input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{float:none;margin-left:0;}
+input.span1,textarea.span1,.uneditable-input.span1{width:50px;}
+input.span2,textarea.span2,.uneditable-input.span2{width:130px;}
+input.span3,textarea.span3,.uneditable-input.span3{width:210px;}
+input.span4,textarea.span4,.uneditable-input.span4{width:290px;}
+input.span5,textarea.span5,.uneditable-input.span5{width:370px;}
+input.span6,textarea.span6,.uneditable-input.span6{width:450px;}
+input.span7,textarea.span7,.uneditable-input.span7{width:530px;}
+input.span8,textarea.span8,.uneditable-input.span8{width:610px;}
+input.span9,textarea.span9,.uneditable-input.span9{width:690px;}
+input.span10,textarea.span10,.uneditable-input.span10{width:770px;}
+input.span11,textarea.span11,.uneditable-input.span11{width:850px;}
+input.span12,textarea.span12,.uneditable-input.span12{width:930px;}
+input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#f5f5f5;border-color:#ddd;cursor:not-allowed;}
+.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;}
+.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;border-color:#c09853;}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:0 0 6px #dbc59e;-moz-box-shadow:0 0 6px #dbc59e;box-shadow:0 0 6px #dbc59e;}
+.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;}
+.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;}
+.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;border-color:#b94a48;}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:0 0 6px #d59392;-moz-box-shadow:0 0 6px #d59392;box-shadow:0 0 6px #d59392;}
+.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;}
+.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;}
+.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;border-color:#468847;}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:0 0 6px #7aba7b;-moz-box-shadow:0 0 6px #7aba7b;box-shadow:0 0 6px #7aba7b;}
+.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;}
+input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;}
+.form-actions{padding:17px 20px 18px;margin-top:18px;margin-bottom:18px;background-color:#f5f5f5;border-top:1px solid #ddd;}
+.uneditable-input{display:block;background-color:#ffffff;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;}
+:-moz-placeholder{color:#999999;}
+::-webkit-input-placeholder{color:#999999;}
+.help-block{display:block;margin-top:5px;margin-bottom:0;color:#999999;}
+.help-inline{display:inline-block;*display:inline;*zoom:1;margin-bottom:9px;vertical-align:middle;padding-left:5px;}
+.input-prepend,.input-append{margin-bottom:5px;*zoom:1;}.input-prepend:before,.input-append:before,.input-prepend:after,.input-append:after{display:table;content:"";}
+.input-prepend:after,.input-append:after{clear:both;}
+.input-prepend input,.input-append input,.input-prepend .uneditable-input,.input-append .uneditable-input{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-prepend input:focus,.input-append input:focus,.input-prepend .uneditable-input:focus,.input-append .uneditable-input:focus{position:relative;z-index:2;}
+.input-prepend .uneditable-input,.input-append .uneditable-input{border-left-color:#ccc;}
+.input-prepend .add-on,.input-append .add-on{float:left;display:block;width:auto;min-width:16px;height:18px;margin-right:-1px;padding:4px 5px;font-weight:normal;line-height:18px;color:#999999;text-align:center;text-shadow:0 1px 0 #ffffff;background-color:#f5f5f5;border:1px solid #ccc;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
+.input-prepend .active,.input-append .active{background-color:#a9dba9;border-color:#46a546;}
+.input-prepend .add-on{*margin-top:1px;}
+.input-append input,.input-append .uneditable-input{float:left;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
+.input-append .uneditable-input{border-left-color:#eee;border-right-color:#ccc;}
+.input-append .add-on{margin-right:0;margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
+.input-append input:first-child{*margin-left:-160px;}.input-append input:first-child+.add-on{*margin-left:-21px;}
+.search-query{padding-left:14px;padding-right:14px;margin-bottom:0;-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;}
+.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input{display:inline-block;margin-bottom:0;}
+.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;}
+.form-search label,.form-inline label,.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{display:inline-block;}
+.form-search .input-append .add-on,.form-inline .input-prepend .add-on,.form-search .input-append .add-on,.form-inline .input-prepend .add-on{vertical-align:middle;}
+.form-search .radio,.form-inline .radio,.form-search .checkbox,.form-inline .checkbox{margin-bottom:0;vertical-align:middle;}
+.control-group{margin-bottom:9px;}
+legend+.control-group{margin-top:18px;-webkit-margin-top-collapse:separate;}
+.form-horizontal .control-group{margin-bottom:18px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";}
+.form-horizontal .control-group:after{clear:both;}
+.form-horizontal .control-label{float:left;width:140px;padding-top:5px;text-align:right;}
+.form-horizontal .controls{margin-left:160px;}
+.form-horizontal .form-actions{padding-left:160px;}
+table{max-width:100%;border-collapse:collapse;border-spacing:0;}
+.table{width:100%;margin-bottom:18px;}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #ddd;}
+.table th{font-weight:bold;}
+.table thead th{vertical-align:bottom;}
+.table thead:first-child tr th,.table thead:first-child tr td{border-top:0;}
+.table tbody+tbody{border-top:2px solid #ddd;}
+.table-condensed th,.table-condensed td{padding:4px 5px;}
+.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th+th,.table-bordered td+td,.table-bordered th+td,.table-bordered td+th{border-left:1px solid #ddd;}
+.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;}
+.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;}
+.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;}
+.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;}
+.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;}
+.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
+.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;}
+table .span1{float:none;width:44px;margin-left:0;}
+table .span2{float:none;width:124px;margin-left:0;}
+table .span3{float:none;width:204px;margin-left:0;}
+table .span4{float:none;width:284px;margin-left:0;}
+table .span5{float:none;width:364px;margin-left:0;}
+table .span6{float:none;width:444px;margin-left:0;}
+table .span7{float:none;width:524px;margin-left:0;}
+table .span8{float:none;width:604px;margin-left:0;}
+table .span9{float:none;width:684px;margin-left:0;}
+table .span10{float:none;width:764px;margin-left:0;}
+table .span11{float:none;width:844px;margin-left:0;}
+table .span12{float:none;width:924px;margin-left:0;}
+[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;*margin-right:.3em;}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0;}
+.icon-white{background-image:url("../img/glyphicons-halflings-white.png");}
+.icon-glass{background-position:0 0;}
+.icon-music{background-position:-24px 0;}
+.icon-search{background-position:-48px 0;}
+.icon-envelope{background-position:-72px 0;}
+.icon-heart{background-position:-96px 0;}
+.icon-star{background-position:-120px 0;}
+.icon-star-empty{background-position:-144px 0;}
+.icon-user{background-position:-168px 0;}
+.icon-film{background-position:-192px 0;}
+.icon-th-large{background-position:-216px 0;}
+.icon-th{background-position:-240px 0;}
+.icon-th-list{background-position:-264px 0;}
+.icon-ok{background-position:-288px 0;}
+.icon-remove{background-position:-312px 0;}
+.icon-zoom-in{background-position:-336px 0;}
+.icon-zoom-out{background-position:-360px 0;}
+.icon-off{background-position:-384px 0;}
+.icon-signal{background-position:-408px 0;}
+.icon-cog{background-position:-432px 0;}
+.icon-trash{background-position:-456px 0;}
+.icon-home{background-position:0 -24px;}
+.icon-file{background-position:-24px -24px;}
+.icon-time{background-position:-48px -24px;}
+.icon-road{background-position:-72px -24px;}
+.icon-download-alt{background-position:-96px -24px;}
+.icon-download{background-position:-120px -24px;}
+.icon-upload{background-position:-144px -24px;}
+.icon-inbox{background-position:-168px -24px;}
+.icon-play-circle{background-position:-192px -24px;}
+.icon-repeat{background-position:-216px -24px;}
+.icon-refresh{background-position:-240px -24px;}
+.icon-list-alt{background-position:-264px -24px;}
+.icon-lock{background-position:-287px -24px;}
+.icon-flag{background-position:-312px -24px;}
+.icon-headphones{background-position:-336px -24px;}
+.icon-volume-off{background-position:-360px -24px;}
+.icon-volume-down{background-position:-384px -24px;}
+.icon-volume-up{background-position:-408px -24px;}
+.icon-qrcode{background-position:-432px -24px;}
+.icon-barcode{background-position:-456px -24px;}
+.icon-tag{background-position:0 -48px;}
+.icon-tags{background-position:-25px -48px;}
+.icon-book{background-position:-48px -48px;}
+.icon-bookmark{background-position:-72px -48px;}
+.icon-print{background-position:-96px -48px;}
+.icon-camera{background-position:-120px -48px;}
+.icon-font{background-position:-144px -48px;}
+.icon-bold{background-position:-167px -48px;}
+.icon-italic{background-position:-192px -48px;}
+.icon-text-height{background-position:-216px -48px;}
+.icon-text-width{background-position:-240px -48px;}
+.icon-align-left{background-position:-264px -48px;}
+.icon-align-center{background-position:-288px -48px;}
+.icon-align-right{background-position:-312px -48px;}
+.icon-align-justify{background-position:-336px -48px;}
+.icon-list{background-position:-360px -48px;}
+.icon-indent-left{background-position:-384px -48px;}
+.icon-indent-right{background-position:-408px -48px;}
+.icon-facetime-video{background-position:-432px -48px;}
+.icon-picture{background-position:-456px -48px;}
+.icon-pencil{background-position:0 -72px;}
+.icon-map-marker{background-position:-24px -72px;}
+.icon-adjust{background-position:-48px -72px;}
+.icon-tint{background-position:-72px -72px;}
+.icon-edit{background-position:-96px -72px;}
+.icon-share{background-position:-120px -72px;}
+.icon-check{background-position:-144px -72px;}
+.icon-move{background-position:-168px -72px;}
+.icon-step-backward{background-position:-192px -72px;}
+.icon-fast-backward{background-position:-216px -72px;}
+.icon-backward{background-position:-240px -72px;}
+.icon-play{background-position:-264px -72px;}
+.icon-pause{background-position:-288px -72px;}
+.icon-stop{background-position:-312px -72px;}
+.icon-forward{background-position:-336px -72px;}
+.icon-fast-forward{background-position:-360px -72px;}
+.icon-step-forward{background-position:-384px -72px;}
+.icon-eject{background-position:-408px -72px;}
+.icon-chevron-left{background-position:-432px -72px;}
+.icon-chevron-right{background-position:-456px -72px;}
+.icon-plus-sign{background-position:0 -96px;}
+.icon-minus-sign{background-position:-24px -96px;}
+.icon-remove-sign{background-position:-48px -96px;}
+.icon-ok-sign{background-position:-72px -96px;}
+.icon-question-sign{background-position:-96px -96px;}
+.icon-info-sign{background-position:-120px -96px;}
+.icon-screenshot{background-position:-144px -96px;}
+.icon-remove-circle{background-position:-168px -96px;}
+.icon-ok-circle{background-position:-192px -96px;}
+.icon-ban-circle{background-position:-216px -96px;}
+.icon-arrow-left{background-position:-240px -96px;}
+.icon-arrow-right{background-position:-264px -96px;}
+.icon-arrow-up{background-position:-289px -96px;}
+.icon-arrow-down{background-position:-312px -96px;}
+.icon-share-alt{background-position:-336px -96px;}
+.icon-resize-full{background-position:-360px -96px;}
+.icon-resize-small{background-position:-384px -96px;}
+.icon-plus{background-position:-408px -96px;}
+.icon-minus{background-position:-433px -96px;}
+.icon-asterisk{background-position:-456px -96px;}
+.icon-exclamation-sign{background-position:0 -120px;}
+.icon-gift{background-position:-24px -120px;}
+.icon-leaf{background-position:-48px -120px;}
+.icon-fire{background-position:-72px -120px;}
+.icon-eye-open{background-position:-96px -120px;}
+.icon-eye-close{background-position:-120px -120px;}
+.icon-warning-sign{background-position:-144px -120px;}
+.icon-plane{background-position:-168px -120px;}
+.icon-calendar{background-position:-192px -120px;}
+.icon-random{background-position:-216px -120px;}
+.icon-comment{background-position:-240px -120px;}
+.icon-magnet{background-position:-264px -120px;}
+.icon-chevron-up{background-position:-288px -120px;}
+.icon-chevron-down{background-position:-313px -119px;}
+.icon-retweet{background-position:-336px -120px;}
+.icon-shopping-cart{background-position:-360px -120px;}
+.icon-folder-close{background-position:-384px -120px;}
+.icon-folder-open{background-position:-408px -120px;}
+.icon-resize-vertical{background-position:-432px -119px;}
+.icon-resize-horizontal{background-position:-456px -118px;}
+.dropdown{position:relative;}
+.dropdown-toggle{*margin-bottom:-3px;}
+.dropdown-toggle:active,.open .dropdown-toggle{outline:0;}
+.caret{display:inline-block;width:0;height:0;text-indent:-99999px;*text-indent:0;vertical-align:top;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000000;opacity:0.3;filter:alpha(opacity=30);content:"\2193";}
+.dropdown .caret{margin-top:8px;margin-left:2px;}
+.dropdown:hover .caret,.open.dropdown .caret{opacity:1;filter:alpha(opacity=100);}
+.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;float:left;display:none;min-width:160px;_width:160px;padding:4px 0;margin:0;list-style:none;background-color:#ffffff;border-color:#ccc;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:1px;-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;*border-right-width:2px;*border-bottom-width:2px;}.dropdown-menu.bottom-up{top:auto;bottom:100%;margin-bottom:2px;}
+.dropdown-menu .divider{height:1px;margin:5px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;}
+.dropdown-menu a{display:block;padding:3px 15px;clear:both;font-weight:normal;line-height:18px;color:#555555;white-space:nowrap;}
+.dropdown-menu li>a:hover,.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;background-color:#0088cc;}
+.dropdown.open{*z-index:1000;}.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);}
+.dropdown.open .dropdown-menu{display:block;}
+.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);}
+.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;}
+.collapse{-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-ms-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;position:relative;overflow:hidden;height:0;}.collapse.in{height:auto;}
+.close{float:right;font-size:20px;font-weight:bold;line-height:18px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;opacity:0.4;filter:alpha(opacity=40);cursor:pointer;}
+.btn{display:inline-block;padding:4px 10px 4px;margin-bottom:0;font-size:13px;line-height:18px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-ms-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(top, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);*margin-left:.3em;}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;}
+.btn:active,.btn.active{background-color:#cccccc \9;}
+.btn:first-child{*margin-left:0;}
+.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;}
+.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
+.btn.active,.btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;outline:0;}
+.btn.disabled,.btn[disabled]{cursor:default;background-image:none;background-color:#e6e6e6;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
+.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
+.btn-large [class^="icon-"]{margin-top:1px;}
+.btn-small{padding:5px 9px;font-size:11px;line-height:16px;}
+.btn-small [class^="icon-"]{margin-top:-1px;}
+.btn-mini{padding:2px 6px;font-size:11px;line-height:14px;}
+.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;}
+.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-dark.active{color:rgba(255, 255, 255, 0.75);}
+.btn-primary{background-color:#006dcc;background-image:-moz-linear-gradient(top, #0088cc, #0044cc);background-image:-ms-linear-gradient(top, #0088cc, #0044cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));background-image:-webkit-linear-gradient(top, #0088cc, #0044cc);background-image:-o-linear-gradient(top, #0088cc, #0044cc);background-image:linear-gradient(top, #0088cc, #0044cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);border-color:#0044cc #0044cc #002a80;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#0044cc;}
+.btn-primary:active,.btn-primary.active{background-color:#003399 \9;}
+.btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;}
+.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;}
+.btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;}
+.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;}
+.btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;}
+.btn-success:active,.btn-success.active{background-color:#408140 \9;}
+.btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;}
+.btn-info:active,.btn-info.active{background-color:#24748c \9;}
+.btn-inverse{background-color:#393939;background-image:-moz-linear-gradient(top, #454545, #262626);background-image:-ms-linear-gradient(top, #454545, #262626);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#454545), to(#262626));background-image:-webkit-linear-gradient(top, #454545, #262626);background-image:-o-linear-gradient(top, #454545, #262626);background-image:linear-gradient(top, #454545, #262626);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#454545', endColorstr='#262626', GradientType=0);border-color:#262626 #262626 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#262626;}
+.btn-inverse:active,.btn-inverse.active{background-color:#0c0c0c \9;}
+button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;}
+button.btn.large,input[type="submit"].btn.large{*padding-top:7px;*padding-bottom:7px;}
+button.btn.small,input[type="submit"].btn.small{*padding-top:3px;*padding-bottom:3px;}
+.btn-group{position:relative;*zoom:1;*margin-left:.3em;}.btn-group:before,.btn-group:after{display:table;content:"";}
+.btn-group:after{clear:both;}
+.btn-group:first-child{*margin-left:0;}
+.btn-group+.btn-group{margin-left:5px;}
+.btn-toolbar{margin-top:9px;margin-bottom:9px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;}
+.btn-group .btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
+.btn-group .btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;}
+.btn-group .btn:last-child,.btn-group .dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;}
+.btn-group .btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;}
+.btn-group .btn.large:last-child,.btn-group .large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;}
+.btn-group .btn:hover,.btn-group .btn:focus,.btn-group .btn:active,.btn-group .btn.active{z-index:2;}
+.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;}
+.btn-group .dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:5px;*padding-bottom:5px;}
+.btn-group.open{*z-index:1000;}.btn-group.open .dropdown-menu{display:block;margin-top:1px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
+.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);}
+.btn .caret{margin-top:7px;margin-left:0;}
+.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100);}
+.btn-primary .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;opacity:0.75;filter:alpha(opacity=75);}
+.btn-small .caret{margin-top:4px;}
+.alert{padding:8px 35px 8px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.alert,.alert-heading{color:#c09853;}
+.alert .close{position:relative;top:-2px;right:-21px;line-height:18px;}
+.alert-success{background-color:#dff0d8;border-color:#d6e9c6;}
+.alert-success,.alert-success .alert-heading{color:#468847;}
+.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;}
+.alert-danger,.alert-error,.alert-danger .alert-heading,.alert-error .alert-heading{color:#b94a48;}
+.alert-info{background-color:#d9edf7;border-color:#bce8f1;}
+.alert-info,.alert-info .alert-heading{color:#3a87ad;}
+.alert-block{padding-top:14px;padding-bottom:14px;}
+.alert-block>p,.alert-block>ul{margin-bottom:0;}
+.alert-block p+p{margin-top:5px;}
+.nav{margin-left:0;margin-bottom:18px;list-style:none;}
+.nav>li>a{display:block;}
+.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;}
+.nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:18px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;}
+.nav li+.nav-header{margin-top:9px;}
+.nav-list{padding-left:14px;padding-right:14px;margin-bottom:0;}
+.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
+.nav-list>li>a{padding:3px 15px;}
+.nav-list .active>a,.nav-list .active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;}
+.nav-list [class^="icon-"]{margin-right:2px;}
+.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";}
+.nav-tabs:after,.nav-pills:after{clear:both;}
+.nav-tabs>li,.nav-pills>li{float:left;}
+.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;}
+.nav-tabs{border-bottom:1px solid #ddd;}
+.nav-tabs>li{margin-bottom:-1px;}
+.nav-tabs>li>a{padding-top:9px;padding-bottom:9px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;}
+.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
+.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
+.nav-pills .active>a,.nav-pills .active>a:hover{color:#ffffff;background-color:#0088cc;}
+.nav-stacked>li{float:none;}
+.nav-stacked>li>a{margin-right:0;}
+.nav-tabs.nav-stacked{border-bottom:0;}
+.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
+.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}
+.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}
+.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;}
+.nav-pills.nav-stacked>li>a{margin-bottom:3px;}
+.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;}
+.nav-tabs .dropdown-menu,.nav-pills .dropdown-menu{margin-top:1px;border-width:1px;}
+.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#0088cc;margin-top:6px;}
+.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#005580;}
+.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;}
+.nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;}
+.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;}
+.nav .open .caret,.nav .open.active .caret,.nav .open a:hover .caret{border-top-color:#ffffff;opacity:1;filter:alpha(opacity=100);}
+.tabs-stacked .open>a:hover{border-color:#999999;}
+.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";}
+.tabbable:after{clear:both;}
+.tab-content{overflow:hidden;}
+.tabs-below .nav-tabs,.tabs-right .nav-tabs,.tabs-left .nav-tabs{border-bottom:0;}
+.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;}
+.tab-content>.active,.pill-content>.active{display:block;}
+.tabs-below .nav-tabs{border-top:1px solid #ddd;}
+.tabs-below .nav-tabs>li{margin-top:-1px;margin-bottom:0;}
+.tabs-below .nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below .nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;}
+.tabs-below .nav-tabs .active>a,.tabs-below .nav-tabs .active>a:hover{border-color:transparent #ddd #ddd #ddd;}
+.tabs-left .nav-tabs>li,.tabs-right .nav-tabs>li{float:none;}
+.tabs-left .nav-tabs>li>a,.tabs-right .nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;}
+.tabs-left .nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;}
+.tabs-left .nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;}
+.tabs-left .nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;}
+.tabs-left .nav-tabs .active>a,.tabs-left .nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;}
+.tabs-right .nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;}
+.tabs-right .nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;}
+.tabs-right .nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;}
+.tabs-right .nav-tabs .active>a,.tabs-right .nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;}
+.navbar{overflow:visible;margin-bottom:18px;}
+.navbar-inner{padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);}
+.btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#222222;}
+.btn-navbar:active,.btn-navbar.active{background-color:#080808 \9;}
+.btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);}
+.btn-navbar .icon-bar+.icon-bar{margin-top:3px;}
+.nav-collapse.collapse{height:auto;}
+.navbar .brand:hover{text-decoration:none;}
+.navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;}
+.navbar .navbar-text{margin-bottom:0;line-height:40px;color:#999999;}.navbar .navbar-text a:hover{color:#ffffff;background-color:transparent;}
+.navbar .btn,.navbar .btn-group{margin-top:5px;}
+.navbar .btn-group .btn{margin-top:0;}
+.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";}
+.navbar-form:after{clear:both;}
+.navbar-form input,.navbar-form select{display:inline-block;margin-top:5px;margin-bottom:0;}
+.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;}
+.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;}
+.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;}
+.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;color:rgba(255, 255, 255, 0.75);background:#666;background:rgba(255, 255, 255, 0.3);border:1px solid #111;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query :-moz-placeholder{color:#eeeeee;}
+.navbar-search .search-query::-webkit-input-placeholder{color:#eeeeee;}
+.navbar-search .search-query:hover{color:#ffffff;background-color:#999999;background-color:rgba(255, 255, 255, 0.5);}
+.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;}
+.navbar-fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030;}
+.navbar-fixed-top .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
+.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;}
+.navbar .nav.pull-right{float:right;}
+.navbar .nav>li{display:block;float:left;}
+.navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}
+.navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;}
+.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;}
+.navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;}
+.navbar .nav.pull-right{margin-left:10px;margin-right:0;}
+.navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;}
+.navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;}
+.navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;}
+.navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);}
+.navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;}
+.navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;}
+.navbar .nav.pull-right .dropdown-menu{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before{left:auto;right:12px;}
+.navbar .nav.pull-right .dropdown-menu:after{left:auto;right:13px;}
+.breadcrumb{padding:7px 14px;margin:0 0 18px;background-color:#fbfbfb;background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline-block;text-shadow:0 1px 0 #ffffff;}
+.breadcrumb .divider{padding:0 5px;color:#999999;}
+.breadcrumb .active a{color:#333333;}
+.pagination{height:36px;margin:18px 0;}
+.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);}
+.pagination li{display:inline;}
+.pagination a{float:left;padding:0 14px;line-height:34px;text-decoration:none;border:1px solid #ddd;border-left-width:0;}
+.pagination a:hover,.pagination .active a{background-color:#f5f5f5;}
+.pagination .active a{color:#999999;cursor:default;}
+.pagination .disabled a,.pagination .disabled a:hover{color:#999999;background-color:transparent;cursor:default;}
+.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
+.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
+.pagination-centered{text-align:center;}
+.pagination-right{text-align:right;}
+.pager{margin-left:0;margin-bottom:18px;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";}
+.pager:after{clear:both;}
+.pager li{display:inline;}
+.pager a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
+.pager a:hover{text-decoration:none;background-color:#f5f5f5;}
+.pager .next a{float:right;}
+.pager .previous a{float:left;}
+.modal-open .dropdown-menu{z-index:2050;}
+.modal-open .dropdown.open{*z-index:2050;}
+.modal-open .popover{z-index:2060;}
+.modal-open .tooltip{z-index:2070;}
+.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;}
+.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);}
+.modal{position:fixed;top:50%;left:50%;z-index:1050;max-height:500px;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}
+.modal.fade.in{top:50%;}
+.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;}
+.modal-body{padding:15px;}
+.modal-body .modal-form{margin-bottom:0;}
+.modal-footer{padding:14px 15px 15px;margin-bottom:0;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";}
+.modal-footer:after{clear:both;}
+.modal-footer .btn{float:right;margin-left:5px;margin-bottom:0;}
+.tooltip{position:absolute;z-index:1020;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);}
+.tooltip.top{margin-top:-2px;}
+.tooltip.right{margin-left:2px;}
+.tooltip.bottom{margin-top:2px;}
+.tooltip.left{margin-left:-2px;}
+.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
+.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
+.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
+.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
+.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.tooltip-arrow{position:absolute;width:0;height:0;}
+.popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px;}.popover.top{margin-top:-5px;}
+.popover.right{margin-left:5px;}
+.popover.bottom{margin-top:5px;}
+.popover.left{margin-left:-5px;}
+.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
+.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
+.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
+.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
+.popover .arrow{position:absolute;width:0;height:0;}
+.popover-inner{padding:3px;width:280px;overflow:hidden;background:#000000;background:rgba(0, 0, 0, 0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);}
+.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;}
+.popover-content{padding:14px;background-color:#ffffff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;}
+.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";}
+.thumbnails:after{clear:both;}
+.thumbnails>li{float:left;margin:0 0 18px 20px;}
+.thumbnail{display:block;padding:4px;line-height:1;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);}
+a.thumbnail:hover{border-color:#0088cc;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);}
+.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;}
+.thumbnail .caption{padding:9px;}
+.label{padding:2px 4px 3px;font-size:11.049999999999999px;font-weight:bold;color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
+.label:hover{color:#ffffff;text-decoration:none;}
+.label-important{background-color:#b94a48;}
+.label-important:hover{background-color:#953b39;}
+.label-warning{background-color:#f89406;}
+.label-warning:hover{background-color:#c67605;}
+.label-success{background-color:#468847;}
+.label-success:hover{background-color:#356635;}
+.label-info{background-color:#3a87ad;}
+.label-info:hover{background-color:#2d6987;}
+@-webkit-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}.progress{overflow:hidden;height:18px;margin-bottom:18px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-ms-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(top, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.progress .bar{width:0%;height:18px;color:#ffffff;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-ms-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(top, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-ms-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;}
+.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;}
+.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;}
+.progress-danger .bar{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);}
+.progress-danger.progress-striped .bar{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
+.progress-success .bar{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);}
+.progress-success.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
+.progress-info .bar{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);}
+.progress-info.progress-striped .bar{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
+.accordion{margin-bottom:18px;}
+.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.accordion-heading{border-bottom:0;}
+.accordion-heading .accordion-toggle{display:block;padding:8px 15px;}
+.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;}
+.carousel{position:relative;margin-bottom:18px;line-height:1;}
+.carousel-inner{overflow:hidden;width:100%;position:relative;}
+.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-ms-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;}
+.carousel .item>img{display:block;line-height:1;}
+.carousel .active,.carousel .next,.carousel .prev{display:block;}
+.carousel .active{left:0;}
+.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;}
+.carousel .next{left:100%;}
+.carousel .prev{left:-100%;}
+.carousel .next.left,.carousel .prev.right{left:0;}
+.carousel .active.left{left:-100%;}
+.carousel .active.right{left:100%;}
+.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;}
+.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);}
+.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:10px 15px 5px;background:#333333;background:rgba(0, 0, 0, 0.75);}
+.carousel-caption h4,.carousel-caption p{color:#ffffff;}
+.hero-unit{padding:60px;margin-bottom:30px;background-color:#f5f5f5;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;}
+.hero-unit p{font-size:18px;font-weight:200;line-height:27px;}
+.pull-right{float:right;}
+.pull-left{float:left;}
+.hide{display:none;}
+.show{display:block;}
+.invisible{visibility:hidden;}
diff --git a/data/static/css/build-table.css b/data/static/css/build-table.css
new file mode 100644 (file)
index 0000000..0e73071
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+
+       CSS attributes for the build table.
+
+*/
+
+/* Scratch builds are shown in italic font. */
+ul.builds li.build a.scratch {
+       font-style: italic;
+}
+
+table.builds tr.build {
+       line-height: 175%;
+}
+
+table.builds tr.build td.name {
+       width: 20em;
+}
+
+table.builds tr.build td.name a.scratch {
+       font-style: italic;
+}
+
+table.builds tr.build td.name a.unknown {
+       color: #515151;
+}
+
+table.builds tr.build td.name a.failed {
+       color: red;
+}
+
+table.builds tr.build td.name a.running {
+       color: yellow;
+}
+
+table.builds tr.build td.name a.finished {
+       color: green;
+}
+
+table.builds tr.build td.user {
+       width: 10em;
+}
+
+table.builds tr.build td.jobs a.pending {
+       color: #515151;
+}
+
+table.builds tr.build td.jobs a.running {
+       color: yellow;
+}
+
+table.builds tr.build td.jobs a.dependency_error {
+       color: blue;
+}
+
+table.builds tr.build td.jobs a.failed {
+       color: red;
+}
+
+table.builds tr.build td.jobs a.dispatching {
+       color: yellow;
+}
+
+table.builds tr.build td.jobs a.uploading {
+       color: yellow;
+}
+
+table.builds tr.build td.jobs a.finished {
+       color: green;
+}
diff --git a/data/static/css/commits-table.css b/data/static/css/commits-table.css
new file mode 100644 (file)
index 0000000..80c1c33
--- /dev/null
@@ -0,0 +1,39 @@
+
+
+table.commits {
+       width: 95%;
+}
+
+table.commits tr.commit {
+       background-color: #e1e1e1;
+}
+
+table.commits tr.commit td.revision {
+       font-family: monospace;
+
+       width: 10%;
+}
+
+table.commits tr.commit td.author {
+       width: 20%;
+}
+
+table.commits tr.commit td.subject {
+       width: 70%;
+}
+
+/* States */
+
+table.commits tr.state-running {
+       background-color: yellow;
+}
+
+table.commits tr.state-failed {
+       background-color: red;
+       color: white;
+}
+
+table.commits tr.state-finished {
+       background-color: green;
+       color: white;
+}
diff --git a/data/static/css/jobs-list.css b/data/static/css/jobs-list.css
new file mode 100644 (file)
index 0000000..271cc47
--- /dev/null
@@ -0,0 +1,10 @@
+/*
+
+       CSS attributes for the job list.
+
+*/
+
+/* Scratch builds are shown in italic font. */
+ul.jobs li.job a.scratch {
+       font-style: italic;
+}
diff --git a/data/static/css/jobs-table.css b/data/static/css/jobs-table.css
new file mode 100644 (file)
index 0000000..9b83c95
--- /dev/null
@@ -0,0 +1,82 @@
+
+/*
+       General table attributes.
+*/
+
+table.jobs {
+       width: 95%;
+       margin: 1em;
+
+       border: 1px solid #66000f;
+}
+
+table.jobs th {
+       text-align: center;
+
+       background-color: #707081;
+       color: white;
+       font-weight: normal;
+
+       border-bottom: 1px solid #66000f;
+}
+
+table.jobs td {
+}
+
+table.jobs tr.job {
+
+}
+
+table.jobs td.aborted {
+       background-color: black;
+       color: white;
+}
+
+table.jobs td.failed {
+       background-color: #cc0000;
+       color: white;
+}
+
+table.jobs td.failed a {
+
+}
+
+table.jobs td.finished {
+       background-color: #66ff66;
+       color: black;
+}
+
+table.jobs td.pending {
+       background-color: #ffcc33;
+       color: black;
+}
+
+table.jobs td.supported_arches {
+       border-top: 1px solid black;
+       text-align: center;
+}
+
+/* Positioning of texts in the cells. */
+
+table.jobs td.icon {
+       text-align: center;
+}
+
+table.jobs td.duration {
+       text-align: right;
+}
+
+table.jobs td.arch {
+       text-align: center;
+}
+
+table.jobs .state {
+       text-align: center;
+
+       border-left:  1px solid black;
+       border-right: 1px solid black;
+}
+
+table.jobs td.host {
+       text-align: center;
+}
diff --git a/data/static/css/log.css b/data/static/css/log.css
new file mode 100644 (file)
index 0000000..abbe664
--- /dev/null
@@ -0,0 +1,33 @@
+div.log {
+       width: 95%;
+
+       border: 1px dotted #515151;
+}
+
+div.log div.log-entry {
+       border: 1px solid black;
+}
+
+div.log div.log-entry div.log-title a {
+       font-weight: bold;
+}
+
+div.log div.log-entry div.log-message {
+       margin: 0.4em 1em 0.4em 1em;
+}
+
+div.log div.log-entry div.log-footer {
+       font-style: italic;
+}
+
+div.log div.log-entry div.comment-vote {
+
+}
+
+div.log div.log-entry div.comment-vote-up {
+       color: #33ff66;
+}
+
+div.log div.log-entry div.comment-vote-down {
+       color: #ee2222;
+}
diff --git a/data/static/css/packages-files-table.css b/data/static/css/packages-files-table.css
new file mode 100644 (file)
index 0000000..168223a
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+
+       CSS attributes for the files table.
+
+*/
+
+table.filelist {
+       width: 95%;
+}
+
+table.filelist tr {
+       font-family: "monospace";
+}
+
+table.filelist tr td {
+       padding-left: 0.5em;
+       padding-right: 0.5em;
+}
+
+table.filelist tr td.mode {
+       text-align: right;
+       width: 6em;
+}
+
+table.filelist tr td.owner {
+       width: 6em;
+}
+
+table.filelist tr td.size {
+       width: 2em;
+       text-align: right;
+}
+
+table.filelist tr td.name {
+
+}
diff --git a/data/static/css/packages-table.css b/data/static/css/packages-table.css
new file mode 100644 (file)
index 0000000..c5b9433
--- /dev/null
@@ -0,0 +1,12 @@
+/*
+
+       CSS attributes for the packages table.
+
+*/
+
+table.packages {
+       width: 60%;
+       margin: 1em;
+
+       border: 1px solid #515151;
+}
index 855ce34d749dba71a25dc8d19382cf368879b897..e06542699266db072fdf902e46602634b66164a1 100644 (file)
-
 body {
-       margin: 0;
-       padding: 0;
-       background: #EEEBEC url(../images/img01.jpg) repeat-x left top;
-       font-family: "Verdana", "Deja-Vu Sans", "Bitstream Vera Sans", sans-serif;
-       font-size: 14px;
-       color: #7F7F81;
-}
-
-h1, h2, h3 {
-       margin: 0;
-       padding: 0;
-       font-weight: normal;
-       color: #515151;
-}
-
-h1 {
-       font-size: 1.6em;
-       padding-bottom: 0.2em;
-}
-
-h2 {
-       font-size: 1.4em;
-}
-
-h3 {
-       font-size: 1.4em;
-}
-
-p, ul, ol {
-       margin-top: 0;
-       line-height: 180%;
-}
-
-ul, ol {
-}
-
-a {
-       text-decoration: underline;
-       color: #D90000;
-}
-
-a:hover {
-       text-decoration: none;
-}
-
-img.border {
-       border: 6px solid #E1F1F6;
-}
-
-img.alignleft {
-       float: left;
-       margin-right: 25px;
-}
-
-img.alignright {
-       float: right;
-}
-
-img.aligncenter {
-       margin: 0px auto;
-}
-
-#wrapper {
-       background: url(../images/img01.jpg) repeat-x left top;
-}
-
-/* Header */
-
-#header {
-       width: 980px;
-       height: 51px;
-       margin: 0px auto;
-}
-
-#logo {
-       width: 980px;
-       height: 123px;
-       margin: 0px auto;
-}
-
-#logo p {
-       padding: 55px 0px 0px 30px;
-       color: #a1a1a1;
-}
-
-#logo h1 {
-       float: left;
-       padding: 35px 10px 0px 30px;
-       letter-spacing: -1px;
-       font-size: 42px;
-       color: #FFFFFF;
-}
-
-#logo a {
-       text-decoration: none;
-       color: #FFFFFF;
-}
-
-#user {
-       float: right;
-       padding-top: 0.2em;
-       padding-bottom: 0.2em;
-       padding-right: 1em;
-       padding-left: 1em;
-       margin-right: 0.5em;
+       position: relative;
+       padding-top: 50px;
+
+       background-image: linear-gradient(
+               bottom,
+               #000000 0%,
+               #880400 57%
+       );
+       background-image: -o-linear-gradient(
+               bottom,
+               #000000 0%,
+               #880400 57%
+       );
+       background-image: -moz-linear-gradient(
+               bottom,
+               #000000 0%,
+               #880400 57%
+       );
+       background-image: -webkit-linear-gradient(
+               bottom,
+               #000000 0%,
+               #880400 57
+       );
+       background-image: -ms-linear-gradient(
+               bottom,
+               #000000 0%,
+               #880400 57%
+       );
+       background-image: -webkit-gradient(
+               linear,
+               left bottom,
+               left top,
+               color-stop(0, #000000),
+               color-stop(0.57, #880400)
+       );
+       background-attachment: fixed;
+}
+
+.container-body {
        background-color: white;
-       text-align: right;
-}
-
-#user span {
-       font-weight: bold;
-}
-
-#user a {
-       text-decoration: underline;
-       color: #515151;
-}
-
-#user a:hover {
-       text-decoration: none;
-}
-
-/* Menu */
-
-#menu {
-       width: 980px;
-       height: 28px;
-       margin: 0 auto;
-       padding: 0px 0px 0px 0px;
-}
-
-#menu ul {
-       margin: 0;
-       padding: 0px 0px 0px 4px;
-       list-style: none;
-       line-height: normal;
-}
-
-#menu li {
-       float: left;
-}
-
-#menu a {
-       display: block;
-       float: left;
-       height: 22px;
-       padding: 6px 25px 0px 25px;
-       background: #515151;
-       text-decoration: none;
-       font-size: 12px;
-       font-weight: normal;
-       color: #FFFFFF;
-       border: none;
-}
-
-#menu a:hover {
-       background: #66000f;
-       text-decoration: underline;
-}
-
-#menu li.current_page_item {
-       background: url(../images/img09.jpg) no-repeat left top;
-}
-
-#menu .current_page_item a {
-       background: url(../images/img10.jpg) no-repeat right top;
-}
-
-#menu a:hover {
-       text-decoration: none;
-}
-
-#menu li.search {
-       float: right;
-}
-
-#search {
-       margin-top: 2px;
-       margin-bottom: 2px;
-       border: 1px solid #e1e1e1;
-       width: 200px;
-       height: 20px;
-}
-
-/* Page */
-
-#page {
-       width: 980px;
-       margin: 0 auto;
-       padding: 0;
-       background: #FFFFFF;
-}
-
-#page-bgtop {
-}
-
-#page-bgbtm {
-       margin: 0px;
-       padding: 20px 20px 0px 40px;
-}
-
-/* Content */
-
-#content {
-       float: left;
-       width: 720px;
-       padding: 0px 20px 0px 0px;
-       border-right: 1px dotted #d1d1d1;
-}
-
-/* Sidebar */
-
-#sidebar {
-       float: right;
-       width: 150px;
-       padding: 0px 0px 0px 0px;
-}
-
-#sidebar ul {
-       margin: 0;
-       padding: 0;
-       list-style: none;
-}
-
-#sidebar li {
-       margin: 0;
-       padding: 0;
-}
-
-#sidebar li ul {
-       margin: 0px 15px;
-       padding-bottom: 30px;
-}
-
-#sidebar li li {
-       padding-left: 15px;
-       line-height: 35px;
-       border-bottom: 1px solid #CCC2A9;
-}
-
-#sidebar li li span {
-       display: block;
-       margin-top: -20px;
-       padding: 0;
-       font-size: 11px;
-       font-style: italic;
-}
-
-#sidebar p {
-       margin: 0 0px;
-       padding: 0px 20px 20px 20px;
-       text-align: justify;
-}
-
-#sidebar a {
-       border: none;
-       color: #7F7F81;
-}
-
-#sidebar a:hover {
-       text-decoration: underline;
-       color: #D90000;
-}
-
-div.submenu ul {
-       margin: 0;
-       padding: 0px 0px 0px 4px;
-       list-style: none;
-       line-height: normal;
-}
-
-div.submenu li {
-       float: left;
-       padding-right: 1px;
-}
-
-div.submenu a {
-       display: block;
-       float: left;
-       height: 18px;
-       padding: 3px 20px 0px 20px;
-       background: #515151;
-       text-decoration: none;
-       font-size: 12px;
-       font-weight: normal;
-       color: #FFFFFF;
-       border: none;
-}
-
-div.submenu a:hover {
-       text-decoration: underline;
-       background: #ffffff;
-       color: #515151;
-}
-
-/* Footer */
-
-#footer-wrapper {
-       overflow: hidden;
-       padding: 0px 0px 0px 0px;
-       background: #2D2722 url(../images/img02.jpg) repeat-x left top;
-}
-
-#footer {
-       clear: both;
-       width: 980px;
-       height: 60px;
-       margin: 20px auto 0px auto;
-}
-
-#footer p {
-       margin: 0;
-       padding: 30px 0px 0px 0px;
-       line-height: normal;
-       font-size: 10px;
-       text-align: center;
-       color: #61544B;
-}
-
-#footer a {
-       color: #61544B;
-}
-
-ul.style1 {
-       margin: 0px;
-       padding: 0px;
-       list-style: none;
-       line-height: normal;
-       background: url(../images/img04.jpg) repeat left top;
-       text-align: right;
-}
-
-ul.style1 li {
-       height: 23px;
-       padding: 7px 20px 0px 20px;
-       font-size: 12px;
-}
-
-ul.style1 a {
-       float: left;
-       color: #6E6E6E;
-}
-
-#two-columns {
-       overflow: hidden;
-       width: 920px;
-       margin: 0px auto;
-       padding: 30px 30px 0px 30px;
-}
-
-#two-columns h2 {
-       padding: 0px 0px 20px 0px;
-       font-size: 24px;
-       color: #3B3B3B;
-}
-
-#column1 {
-       float: left;
-       width: 310px;
-}
-
-#column2 {
-       float: right;
-       width: 560px;
-}
-
-p.pkg-summary {
-       margin: 1em;
-       padding: 1em;
-       background-color: #e1e1e1;
-       border: 1px solid #515151;
-       font-style: italic;
-}
-
-ul.alphabet {
-       padding: 1em 1em 1em 1em;
-       list-style: none;
-}
-
-ul.alphabet li {
-       float: left;
-}
-
-ul.alphabet a {
-       display: block;
-       padding-left: 4px;
-}
-
-ul.builds {
-       list-style: none;
-       margin-top: 1em;
-}
-
-ul.builds li {
-       height: 40px;
-}
-
-ul.builds a.build {
-       margin-left: -2em;
-       padding-left: 3em;
-       padding-top: 8px;
-       padding-bottom: 8px;
-}
-
-ul.builds a.failed {
-       background: url("../images/icons/build-failed.png") no-repeat 0 50%;
-}
-
-ul.builds a.pending {
-       background: url("../images/icons/build-pending.png") no-repeat 0 50%;
-}
-
-ul.builds a.dependency_error {
-       background: url("../images/icons/build-dependency_error.png") no-repeat 0 50%;
-}
-
-ul.builds a.finished {
-       background: url("../images/icons/build-finished.png") no-repeat 0 50%;
-}
-
-ul.builds a.running {
-       background: url("../images/icons/build-running.png") no-repeat 0 50%;
-}
-
-ul.builds a.unknown {
-       background: url("../images/icons/build-unknown.png") no-repeat 0 50%;
-}
-
-ul.builds a.uploading {
-       background: url("../images/icons/build-uploading.png") no-repeat 0 50%;
-}
-
-ul.builds a.dispatching {
-       background: url("../images/icons/build-dispatching.png") no-repeat 0 50%;
-}
-
-ul.builds a.waiting {
-       background: url("../images/icons/build-waiting.png") no-repeat 0 50%;
-}
-
-ul.builders {
-       list-style: none;
-}
-
-ul.builders li {
-       height: 40px;
-}
-
-ul.builders a.builder {
-       margin-left: -2em;
-       padding-left: 3em;
-       padding-top: 8px;
-       padding-bottom: 8px;
-}
-
-ul.builders a.offline {
-       background: url("../images/icons/slave-offline.png") no-repeat 0 50%;
-}
-
-ul.builders a.online {
-       background: url("../images/icons/slave-online.png") no-repeat 0 50%;
-}
-
-ul.builders a.disabled {
-       background: url("../images/icons/builder-disabled.png") no-repeat 0 50%;
-}
-
-/* Comments */
-
-div.comments {
-
-}
-
-div.comments div.comment {
-       border: 1px solid #e1e1e1;
-}
-
-div.comments div.comment span {
-       padding-left: 1em;
-       padding-bottom: 0.2em;
-       font-style: italic;
-}
-
-div.comments div.comment p.text {
-       padding: 1em;
-       color: #515151;
-}
-
-div.comments div.none {
-       border-left: 10px solid #e1e1e1;
-}
-
-div.comments div.up {
-       border-left: 10px solid #99FF66;
-}
-
-div.comments div.down {
-       border-left: 10px solid #ff3333;
-}
-
-div.add-comment {
-       background: #e1e1e1;
-       border: 1px solid #515151;
-       margin: 1em;
-       padding: 1em;
-}
-
-/* Repo actions */
-div.repo-actions {
-}
-
-div.repo-actions div.action {
-
-}
-
-div.repo-actions div.action {
-
-}
-
-div.repo-actions div.action p.buttons {
-       border: 1px solid black;
-}
-
-div.repo-actions div.add {
-       background-color: #99ff66;
-}
-
-table td.buttons {
-       text-align: right;
-}
-
-p.important {
-       color: white;
-       padding: 0.2em;
-       background-color: #ff3333;
-}
-
-p.focus {
-       text-align: center;
-       font-size: 3em;
-       font-weight: bold;
-}
-
-p.buttons {
-       text-align: right;
-}
-
-img.avatar {
-       float: right;
-       border: 1px solid #e1e1e1;
-       padding: 0.5em;
-}
-
-/* FORMS */
-
-table.form {
-}
-
-table.form td {
-       height: 2em;
-}
-
-table.form2 {
-       width: 80%;
-}
-
-table.form2 td.col1 {
-       width: 40%
-}
-
-table.form2 td.col2 {
-       width: 60%;
-}
-
-table.form3 {
-       width: 100%;
-}
-
-table.form3 td.col1 {
-       width: 30%;
-}
-
-table.form3 td.col2 {
-       width: 30%;
-}
-
-table.form3 td.col3 {
-       width: 40%;
-       font-style: italic;
-}
-
-table.file-list {
-       width: 80%;
-}
-
-table.file-list td.name {
-       width: 70%;
-}
 
-table.file-list td.actions {
-       width: 30%;
+       padding: 15px;
 }
diff --git a/data/static/css/style.css.backup b/data/static/css/style.css.backup
new file mode 100644 (file)
index 0000000..d8ab6f8
--- /dev/null
@@ -0,0 +1,891 @@
+
+body {
+       margin: 0;
+       padding: 0;
+       background: #EEEBEC url(../images/img01.jpg) repeat-x left top;
+       font-family: "Verdana", "Deja-Vu Sans", "Bitstream Vera Sans", sans-serif;
+       font-size: 14px;
+       color: #707081;
+}
+
+h1, h2, h3, h4 {
+       margin: 0;
+       padding: 0;
+       font-weight: normal;
+       color: #515151;
+}
+
+h1 {
+       font-size: 1.6em;
+       padding-bottom: 0.4em;
+}
+
+h2 {
+       font-size: 1.4em;
+       margin-left: 0.2em;
+       padding-top: 0.4em;
+       padding-bottom: 0.4em;
+}
+
+h3 {
+       font-size: 1.2em;
+       margin-left: 0.4em;
+}
+
+h4 {
+       font-size: 1.1em;
+       margin-left: 0.4em;
+}
+
+h1 span, h2 span, h3 span {
+       font-size: 0.7em;
+       color: #515151;
+}
+
+p, ul, ol {
+       margin-top: 0;
+       margin-left: 0.6em;
+       line-height: 175%;
+}
+
+ul, ol {
+}
+
+a {
+       text-decoration: underline;
+       /* color: #D90000; */
+       color: #66000f;
+}
+
+a:hover {
+       text-decoration: none;
+}
+
+table {
+       border-collapse: collapse;
+}
+
+img.border {
+       border: 6px solid #E1F1F6;
+}
+
+img.alignleft {
+       float: left;
+       margin-right: 25px;
+}
+
+img.alignright {
+       float: right;
+}
+
+img.aligncenter {
+       margin: 0px auto;
+}
+
+#wrapper {
+       background: url(../images/img01.jpg) repeat-x left top;
+}
+
+/* Header */
+
+#logo {
+       width: 980px;
+       height: 123px;
+       margin: 0px auto;
+}
+
+#logo p {
+       padding: 55px 0px 0px 30px;
+       color: #a1a1a1;
+}
+
+#logo h1 {
+       float: left;
+       padding: 35px 10px 0px 30px;
+       letter-spacing: -1px;
+       font-size: 42px;
+       color: #FFFFFF;
+}
+
+#logo a {
+       text-decoration: none;
+       color: #FFFFFF;
+}
+
+#user {
+       float: right;
+       padding-top: 0.2em;
+       padding-bottom: 0.2em;
+       padding-right: 1em;
+       padding-left: 1em;
+       margin-right: 0.5em;
+       background-color: white;
+       text-align: right;
+}
+
+#user span {
+       font-weight: bold;
+}
+
+#user a {
+       text-decoration: underline;
+       color: #515151;
+}
+
+#user a:hover {
+       text-decoration: none;
+}
+
+/* Menu */
+
+#menu {
+       width: 980px;
+       height: 28px;
+       margin: 0 auto;
+       padding: 0px 0px 0px 0px;
+}
+
+#menu ul {
+       margin: 0;
+       padding: 0px 0px 0px 4px;
+       list-style: none;
+       line-height: normal;
+}
+
+#menu li {
+       float: left;
+}
+
+#menu a {
+       display: block;
+       float: left;
+       height: 22px;
+       padding: 6px 25px 0px 25px;
+       background: #515151;
+       text-decoration: none;
+       font-size: 12px;
+       font-weight: normal;
+       color: #FFFFFF;
+       border: none;
+}
+
+#menu a:hover {
+       background: #66000f;
+       text-decoration: underline;
+}
+
+#menu li.current_page_item {
+       background: url(../images/img09.jpg) no-repeat left top;
+}
+
+#menu .current_page_item a {
+       background: url(../images/img10.jpg) no-repeat right top;
+}
+
+#menu a:hover {
+       text-decoration: none;
+}
+
+#menu li.search {
+       float: right;
+}
+
+#search {
+       margin-top: 2px;
+       margin-bottom: 2px;
+       border: 1px solid #e1e1e1;
+       width: 200px;
+       height: 20px;
+}
+
+div.searchbox {
+       margin: 1em;
+       padding: 1em;
+
+       background-color: #919191;
+       text-align: center;
+}
+
+/* Page */
+
+#page {
+       width: 980px;
+       margin: 0 auto;
+       padding: 0;
+       background: #FFFFFF;
+}
+
+#page-bgtop {
+}
+
+#page-bgbtm {
+       margin: 0px;
+       padding: 20px 20px 0px 40px;
+}
+
+/* Content */
+
+#content {
+       float: left;
+       width: 720px;
+       padding: 0px 20px 0px 0px;
+       border-right: 1px dotted #d1d1d1;
+}
+
+#content table {
+       margin-left: 0.6em;
+}
+
+#content p.right {
+       text-align: right;
+}
+
+/* Sidebar */
+
+#sidebar {
+       float: right;
+       width: 150px;
+       padding: 0px 0px 0px 0px;
+}
+
+#sidebar ul {
+       margin: 0;
+       padding: 0;
+       list-style: none;
+}
+
+#sidebar li {
+       margin: 0;
+       padding: 0;
+}
+
+#sidebar li ul {
+       margin: 0px 15px;
+       padding-bottom: 30px;
+}
+
+#sidebar li li {
+       padding-left: 15px;
+       line-height: 35px;
+       border-bottom: 1px solid #CCC2A9;
+}
+
+#sidebar li li span {
+       display: block;
+       margin-top: -20px;
+       padding: 0;
+       font-size: 11px;
+       font-style: italic;
+}
+
+#sidebar ul.actions {
+}
+
+#sidebar ul.actions li.admin {
+       font-style: italic;
+}
+
+#sidebar p {
+       margin: 0 0px;
+       padding: 0px 20px 20px 20px;
+       text-align: justify;
+}
+
+#sidebar a {
+       border: none;
+       color: #7F7F81;
+}
+
+#sidebar a:hover {
+       text-decoration: underline;
+       color: #D90000;
+}
+
+div.submenu ul {
+       margin: 0;
+       padding: 0px 0px 0px 4px;
+       list-style: none;
+       line-height: normal;
+}
+
+div.submenu li {
+       float: left;
+       padding-right: 1px;
+}
+
+div.submenu a {
+       display: block;
+       float: left;
+       height: 18px;
+       padding: 3px 20px 0px 20px;
+       background: #515151;
+       text-decoration: none;
+       font-size: 12px;
+       font-weight: normal;
+       color: #FFFFFF;
+       border: none;
+}
+
+div.submenu a:hover {
+       text-decoration: underline;
+       background: #ffffff;
+       color: #515151;
+}
+
+/* Footer */
+
+#footer-wrapper {
+       overflow: hidden;
+       padding: 0px 0px 0px 0px;
+       background: #2D2722 url(../images/img02.jpg) repeat-x left top;
+}
+
+#footer {
+       clear: both;
+       width: 980px;
+       height: 60px;
+       margin: 20px auto 0px auto;
+}
+
+#footer p {
+       margin: 0;
+       padding: 30px 0px 0px 0px;
+       line-height: normal;
+       font-size: 10px;
+       text-align: center;
+       color: #61544B;
+}
+
+#footer a {
+       color: #61544B;
+}
+
+ul.style1 {
+       margin: 0px;
+       padding: 0px;
+       list-style: none;
+       line-height: normal;
+       background: url(../images/img04.jpg) repeat left top;
+       text-align: right;
+}
+
+ul.style1 li {
+       height: 23px;
+       padding: 7px 20px 0px 20px;
+       font-size: 12px;
+}
+
+ul.style1 a {
+       float: left;
+       color: #6E6E6E;
+}
+
+#two-columns {
+       overflow: hidden;
+       width: 920px;
+       margin: 0px auto;
+       padding: 30px 30px 0px 30px;
+}
+
+#two-columns h2 {
+       padding: 0px 0px 20px 0px;
+       font-size: 24px;
+       color: #3B3B3B;
+}
+
+#column1 {
+       float: left;
+       width: 310px;
+}
+
+#column2 {
+       float: right;
+       width: 560px;
+}
+
+/* Message boxes. */
+
+div.message {
+       margin: 1em;
+       padding: 1em;
+}
+
+div.message span {
+       font-size: 1.3em;
+       display: block;
+
+       margin-bottom: 0.5em;
+}
+
+div.message-notice {
+       border: 2px solid #66ff99;
+       background-color: #33ff66;
+}
+
+div.message-warning {
+       border: 2px solid #ffcc33;
+       background-color: #ffee66;
+}
+
+div.message-error, div.message-critical {
+       border: 2px solid #cc2222;
+       background-color: #ee2222;
+       color: white;
+}
+
+div.message-error a, div.message-critical a {
+       color: white;
+}
+
+div.pkg-summary, div.distro-slogan {
+       margin-bottom: 1em;
+       text-align: right;
+       font-variant: small-caps;
+       font-style: italic;
+       font-size: 1.1em;
+}
+
+div.pkg-description {
+       margin: 1em;
+       padding: 1em;
+       background-color: #e1e1e1;
+       border: 1px solid #515151;
+       line-height: 175%;
+       font-style: italic;
+}
+
+div.builder-description, div.distro-description, div.update-description {
+       margin: 1em;
+       padding: 1em;
+       border: 1px dotted #515151;
+       background-color: #e1e1e1;
+}
+
+div.builder-description textarea {
+       width: 100%;
+       height: 10em;
+}
+
+div.builder-description div.buttons {
+       text-align: right;
+}
+
+div.login-form {
+       margin-left: 15em;
+}
+
+div.login-form table.login {
+       margin-bottom: 1em;
+}
+
+div.login-form table.login td {
+       padding: 0.2em;
+}
+
+div.links {
+       text-align: right;
+}
+
+ul.alphabet {
+       padding: 1em 1em 1em 1em;
+       list-style: none;
+}
+
+ul.alphabet li {
+       float: left;
+}
+
+ul.alphabet a {
+       display: block;
+       padding-left: 4px;
+}
+
+a.broken {
+       text-decoration: line-through;
+}
+
+ul.jobs {
+       list-style: none;
+       margin-top: 1em;
+}
+
+ul.jobs li {
+       height: 40px;
+}
+
+ul.jobs a.job {
+       margin-left: -2em;
+       padding-left: 3em;
+       padding-top: 8px;
+       padding-bottom: 8px;
+}
+
+ul.jobs a.aborted {
+       background: url("../images/icons/build-aborted.png") no-repeat 0 50%;
+}
+
+ul.jobs a.failed {
+       background: url("../images/icons/build-failed.png") no-repeat 0 50%;
+}
+
+ul.jobs a.failed {
+       background: url("../images/icons/build-failed.png") no-repeat 0 50%;
+}
+
+ul.jobs a.pending {
+       background: url("../images/icons/build-pending.png") no-repeat 0 50%;
+}
+
+ul.jobs a.dependency_error {
+       background: url("../images/icons/build-dependency_error.png") no-repeat 0 50%;
+}
+
+ul.jobs a.finished {
+       background: url("../images/icons/build-finished.png") no-repeat 0 50%;
+}
+
+ul.jobs a.running {
+       background: url("../images/icons/build-running.png") no-repeat 0 50%;
+}
+
+ul.jobs a.unknown {
+       background: url("../images/icons/build-unknown.png") no-repeat 0 50%;
+}
+
+ul.jobs a.uploading {
+       background: url("../images/icons/build-uploading.png") no-repeat 0 50%;
+}
+
+ul.jobs a.dispatching {
+       background: url("../images/icons/build-dispatching.png") no-repeat 0 50%;
+}
+
+ul.jobs a.waiting {
+       background: url("../images/icons/build-waiting.png") no-repeat 0 50%;
+}
+
+/* Builders table */
+
+table.builders {
+       width: 35em;
+       margin: 1em;
+}
+
+table.builders tr.builder {
+       height: 40px;
+}
+
+table.builders tr.builder td.icon, table.builders th.icon {
+       width: 4em;
+}
+
+table.builders tr.builder td.name, table.builders th.name {
+       width: 15em;
+}
+
+table.builders tr.builder td.load, table.builders th.load {
+       text-align: right;
+       width: 5em;
+}
+
+table.builders tr.builder td.jobs, table.builders th.jobs {
+       text-align: right;
+       width: 10em;
+}
+
+/* Overloaded builders are shown in red. */
+
+span.overload {
+       font-weight: bold;
+       color: #cc0000;
+}
+
+/* Comments */
+
+div.comments {
+
+}
+
+div.comments div.comment {
+       border: 1px solid #e1e1e1;
+}
+
+div.comments div.comment span {
+       padding-left: 1em;
+       padding-bottom: 0.2em;
+       font-style: italic;
+}
+
+div.comments div.comment p.text {
+       padding: 1em;
+       color: #515151;
+}
+
+div.comments div.none {
+       border-left: 10px solid #e1e1e1;
+}
+
+div.comments div.up {
+       border-left: 10px solid #99FF66;
+}
+
+div.comments div.down {
+       border-left: 10px solid #ff3333;
+}
+
+div.add-comment {
+       background: #e1e1e1;
+       border: 1px solid #515151;
+       margin: 1em;
+       padding: 1em;
+}
+
+/* Repo actions */
+div.repo-actions {
+}
+
+div.repo-actions div.action {
+
+}
+
+div.repo-actions div.action {
+
+}
+
+div.repo-actions div.action p.buttons {
+       border: 1px solid black;
+}
+
+div.repo-actions div.add {
+       background-color: #99ff66;
+}
+
+table td.buttons {
+       padding-top: 1em;
+       text-align: right;
+}
+
+p.important {
+       color: white;
+       padding: 0.2em;
+       background-color: #ff3333;
+}
+
+p.focus {
+       text-align: center;
+       font-size: 3em;
+       font-weight: bold;
+}
+
+p.buttons {
+       text-align: right;
+}
+
+span.arch-disabled {
+       text-decoration: line-through;
+}
+
+div.avatar {
+       text-align: center;
+}
+
+div.avatar img {
+       border: 1px solid #e1e1e1;
+       padding: 0.5em;
+}
+
+/* FORMS */
+
+table.form {
+       margin-left: 0.4em;
+}
+
+table.form td {
+       height: 1.8em;
+}
+
+table.form td.pkg-description {
+       padding: 1em;
+       border: 1px dotted #515151;
+       background-color: #e1e1e1;
+       text-align: justify;
+}
+
+table.form td.build-message {
+       padding: 1em;
+       border: 1px dotted #515151;
+}
+
+table.form textarea {
+       width: 100%;
+       height: 8em;
+}
+
+table.form2 {
+       width: 95%;
+}
+
+table.form2 td.col1 {
+       width: 30%
+}
+
+table.form2 td.col2 {
+       width: 70%;
+}
+
+table.form3 {
+       width: 95%;
+}
+
+table.form3 td.col1 {
+       width: 30%;
+}
+
+table.form3 td.col2 {
+       width: 30%;
+}
+
+table.form3 td.col3 {
+       width: 40%;
+       font-style: italic;
+}
+
+table.file-list {
+       width: 80%;
+}
+
+table.file-list td.name {
+       width: 70%;
+}
+
+table.file-list td.actions {
+       width: 30%;
+}
+
+table.jobs {
+       width: 95%;
+
+       margin: 1em;
+       border: 1px solid #515151;
+}
+
+table.jobs th {
+       text-align: center;
+}
+
+table.jobs td.duration {
+       text-align: right;
+}
+
+table.jobs td.arch {
+       text-align: center;
+}
+
+table.jobs td.state {
+       text-align: center;
+}
+
+table.jobs td.host {
+       text-align: center;
+}
+
+/* UPLOADS */
+
+table.uploads {
+       width: 95%;
+}
+
+table.uploads tr.upload .size {
+       text-align: right;
+}
+
+table.uploads tr.upload .speed {
+       text-align: right;
+}
+
+table.uploads tr.progress td.bar p {
+       background-color: #e1e1e1;
+}
+
+table.uploads tr.progress td.percentage {
+       text-align: right;
+}
+
+/* BUILD TIMES */
+
+table.build-times {
+       border: 1px solid #e1e1e1;
+       width: 33%;
+}
+
+table.build-times td {
+       padding: 0.2em;
+       border: 1px solid #e1e1e1;
+}
+
+table.build-times td.arch {
+       font-weight: bold;
+}
+
+table.build-times td.time {
+       text-align: right;
+}
+
+/* Mirrors */
+
+table.mirrors {
+       width: 95%;
+       border: 1px solid #e1e1e1;
+}
+
+table.mirrors tr.nearby {
+       background-color: #e1e1e1;
+}
+
+table.mirrors td.DOWN {
+       background-color: #ee2222;
+       color: white;
+}
+
+table.mirrors td.UP {
+       background-color: #33ff66;
+}
+
+table.mirrors td.DISABLED {
+       background-color: black;
+       color: white;
+}
+
+table.mirrors td.country_code {
+       width: 2em;
+       text-align: center;
+}
+
+table.mirrors td.hostname {
+       text-align: center;
+}
+
+table.mirrors td.owner {
+       text-align: left;
+}
+
+/* Builders */
+
+table.build-types {
+       width: 100%;
+}
+
+table.build-types td {
+       width: 33%;
+
+       text-align: center;
+       color: white;
+}
+
+table.build-types td.on {
+       background-color: green;
+}
+
+table.build-types td.off {
+       background-color: red;
+}
diff --git a/data/static/css/watchers-sidebar-table.css b/data/static/css/watchers-sidebar-table.css
new file mode 100644 (file)
index 0000000..af5c82e
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+
+       Watchers sidebar table.
+
+*/
+
+h2.watchers span {
+       font-size: 0.7em;
+       color: #515151;
+}
+
+/* XXX move the list items a little bit to the right. */
+ul.watchers {
+}
+
+ul.watchers li {
+       list-style: square;
+       list-style-position: inside;
+}
diff --git a/data/static/images/glyphicons-halflings-white.png b/data/static/images/glyphicons-halflings-white.png
new file mode 100644 (file)
index 0000000..a20760b
Binary files /dev/null and b/data/static/images/glyphicons-halflings-white.png differ
diff --git a/data/static/images/glyphicons-halflings.png b/data/static/images/glyphicons-halflings.png
new file mode 100644 (file)
index 0000000..92d4445
Binary files /dev/null and b/data/static/images/glyphicons-halflings.png differ
diff --git a/data/static/images/icons/build-aborted.png b/data/static/images/icons/build-aborted.png
new file mode 120000 (symlink)
index 0000000..c6f53e4
--- /dev/null
@@ -0,0 +1 @@
+builder-disabled.png
\ No newline at end of file
diff --git a/data/static/img b/data/static/img
new file mode 120000 (symlink)
index 0000000..ba28150
--- /dev/null
@@ -0,0 +1 @@
+images/
\ No newline at end of file
diff --git a/data/static/js/bootstrap.min.js b/data/static/js/bootstrap.min.js
new file mode 100644 (file)
index 0000000..b8a1789
--- /dev/null
@@ -0,0 +1,2 @@
+/**\n* Bootstrap.js by @fat & @mdo\n* Copyright 2012 Twitter, Inc.\n* http://www.apache.org/licenses/LICENSE-2.0.txt\n*/
+!function(a){a(function(){"use strict",a.support.transition=function(){var b=document.body||document.documentElement,c=b.style,d=c.transition!==undefined||c.WebkitTransition!==undefined||c.MozTransition!==undefined||c.MsTransition!==undefined||c.OTransition!==undefined;return d&&{end:function(){var b="TransitionEnd";return a.browser.webkit?b="webkitTransitionEnd":a.browser.mozilla?b="transitionend":a.browser.opera&&(b="oTransitionEnd"),b}()}}()})}(window.jQuery),!function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype={constructor:c,close:function(b){function f(){e.trigger("closed").remove()}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),e.trigger("close"),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger("close").removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()}},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype={constructor:b,setState:function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},toggle:function(){var a=this.$element.parent('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")}},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.carousel.defaults,c),this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(){return this.interval=setInterval(a.proxy(this.next,this),this.options.interval),this},to:function(b){var c=this.$element.find(".active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(){return clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this;this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h]();if(e.hasClass("active"))return;return!a.support.transition&&this.$element.hasClass("slide")?(this.$element.trigger("slide"),d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")):(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.trigger("slide"),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})),f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=typeof c=="object"&&c;e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):typeof c=="string"||(c=f.slide)?e[c]():e.cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find(".in"),e;d&&d.length&&(e=d.data("collapse"),d.collapse("hide"),e||d.data("collapse",null)),this.$element[b](0),this.transition("addClass","show","shown"),this.$element[b](this.$element[0][c])},hide:function(){var a=this.dimension();this.reset(this.$element[a]()),this.transition("removeClass","hide","hidden"),this.$element[a](0)},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[a?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){c=="show"&&e.reset(),e.$element.trigger(d)};this.$element.trigger(c)[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();a(e).collapse(f)})})}(window.jQuery),!function(a){function d(){a(b).parent().removeClass("open")}"use strict";var b='[data-toggle="dropdown"]',c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),e=c.attr("data-target"),f,g;return e||(e=c.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,"")),f=a(e),f.length||(f=c.parent()),g=f.hasClass("open"),d(),!g&&f.toggleClass("open"),!1}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api",d),a("body").on("click.dropdown.data-api",b,c.prototype.toggle)})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('<div class="modal-backdrop '+d+'" />').appendTo(document.body),this.options.backdrop!="static"&&this.$backdrop.click(a.proxy(this.hide,this)),e&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),e?this.$backdrop.one(a.support.transition.end,b):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,a.proxy(f,this)):f.call(this)):b&&b()}function f(){this.$backdrop.remove(),this.$backdrop=null}function g(){var b=this;this.isShown&&this.options.keyboard?a(document).on("keyup.dismiss.modal",function(a){a.which==27&&b.hide()}):this.isShown||a(document).off("keyup.dismiss.modal")}"use strict";var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this))};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this;if(this.isShown)return;a("body").addClass("modal-open"),this.isShown=!0,this.$element.trigger("show"),g.call(this),e.call(this,function(){var c=a.support.transition&&b.$element.hasClass("fade");!b.$element.parent().length&&b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in"),c?b.$element.one(a.support.transition.end,function(){b.$element.trigger("shown")}):b.$element.trigger("shown")})},hide:function(b){b&&b.preventDefault();if(!this.isShown)return;var e=this;this.isShown=!1,a("body").removeClass("modal-open"),g.call(this),this.$element.trigger("hide").removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?c.call(this):d.call(this)}},a.fn.modal=function(c){return this.each(function(){var d=a(this),e=d.data("modal"),f=a.extend({},a.fn.modal.defaults,d.data(),typeof c=="object"&&c);e||d.data("modal",e=new b(this,f)),typeof c=="string"?e[c]():f.show&&e.show()})},a.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},a.fn.modal.Constructor=b,a(function(){a("body").on("click.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({},e.data(),c.data());b.preventDefault(),e.modal(f)})})}(window.jQuery),!function(a){"use strict";var b=function(a,b){this.init("tooltip",a,b)};b.prototype={constructor:b,init:function(b,c,d){var e,f;this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.enabled=!0,this.options.trigger!="manual"&&(e=this.options.trigger=="hover"?"mouseenter":"focus",f=this.options.trigger=="hover"?"mouseleave":"blur",this.$element.on(e,this.options.selector,a.proxy(this.enter,this)),this.$element.on(f,this.options.selector,a.proxy(this.leave,this))),this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(b){return b=a.extend({},a.fn[this.type].defaults,b,this.$element.data()),b.delay&&typeof b.delay=="number"&&(b.delay={show:b.delay,hide:b.delay}),b},enter:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);!c.options.delay||!c.options.delay.show?c.show():(c.hoverState="in",setTimeout(function(){c.hoverState=="in"&&c.show()},c.options.delay.show))},leave:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);!c.options.delay||!c.options.delay.hide?c.hide():(c.hoverState="out",setTimeout(function(){c.hoverState=="out"&&c.hide()},c.options.delay.hide))},show:function(){var a,b,c,d,e,f,g;if(this.hasContent()&&this.enabled){a=this.tip(),this.setContent(),this.options.animation&&a.addClass("fade"),f=typeof this.options.placement=="function"?this.options.placement.call(this,a[0],this.$element[0]):this.options.placement,b=/in/.test(f),a.remove().css({top:0,left:0,display:"block"}).appendTo(b?this.$element:document.body),c=this.getPosition(b),d=a[0].offsetWidth,e=a[0].offsetHeight;switch(b?f.split(" ")[1]:f){case"bottom":g={top:c.top+c.height,left:c.left+c.width/2-d/2};break;case"top":g={top:c.top-e,left:c.left+c.width/2-d/2};break;case"left":g={top:c.top+c.height/2-e/2,left:c.left-d};break;case"right":g={top:c.top+c.height/2-e/2,left:c.left+c.width}}a.css(g).addClass(f).addClass("in")}},setContent:function(){var a=this.tip();a.find(".tooltip-inner").html(this.getTitle()),a.removeClass("fade in top bottom left right")},hide:function(){function d(){var b=setTimeout(function(){c.off(a.support.transition.end).remove()},500);c.one(a.support.transition.end,function(){clearTimeout(b),c.remove()})}var b=this,c=this.tip();c.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d():c.remove()},fixTitle:function(){var a=this.$element;(a.attr("title")||typeof a.attr("data-original-title")!="string")&&a.attr("data-original-title",a.attr("title")||"").removeAttr("title")},hasContent:function(){return this.getTitle()},getPosition:function(b){return a.extend({},b?{top:0,left:0}:this.$element.offset(),{width:this.$element[0].offsetWidth,height:this.$element[0].offsetHeight})},getTitle:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||(typeof c.title=="function"?c.title.call(b[0]):c.title),a=(a||"").toString().replace(/(^\s*|\s*$)/,""),a},tip:function(){return this.$tip=this.$tip||a(this.options.template)},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(){this[this.tip().hasClass("in")?"hide":"show"]()}},a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("tooltip"),f=typeof c=="object"&&c;e||d.data("tooltip",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.defaults={animation:!0,delay:0,selector:!1,placement:"top",trigger:"hover",title:"",template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'}}(window.jQuery),!function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype,{constructor:b,setContent:function(){var b=this.tip(),c=this.getTitle(),d=this.getContent();b.find(".popover-title")[a.type(c)=="object"?"append":"html"](c),b.find(".popover-content > *")[a.type(d)=="object"?"append":"html"](d),b.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-content")||(typeof c.content=="function"?c.content.call(b[0]):c.content),a=a.toString().replace(/(^\s*|\s*$)/,""),a},tip:function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip}}),a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("popover"),f=typeof c=="object"&&c;e||d.data("popover",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.defaults=a.extend({},a.fn.tooltip.defaults,{placement:"right",content:"",template:'<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>'})}(window.jQuery),!function(a){function b(b,c){var d=a.proxy(this.process,this),e=a(b).is("body")?a(window):a(b),f;this.options=a.extend({},a.fn.scrollspy.defaults,c),this.$scrollElement=e.on("scroll.scroll.data-api",d),this.selector=(this.options.target||(f=a(b).attr("href"))&&f.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=a("body").on("click.scroll.data-api",this.selector,d),this.refresh(),this.process()}"use strict",b.prototype={constructor:b,refresh:function(){this.targets=this.$body.find(this.selector).map(function(){var b=a(this).attr("href");return/^#\w/.test(b)&&a(b).length?b:null}),this.offsets=a.map(this.targets,function(b){return a(b).position().top})},process:function(){var a=this.$scrollElement.scrollTop()+this.options.offset,b=this.offsets,c=this.targets,d=this.activeTarget,e;for(e=b.length;e--;)d!=c[e]&&a>=b[e]&&(!b[e+1]||a<=b[e+1])&&this.activate(c[e])},activate:function(a){var b;this.activeTarget=a,this.$body.find(this.selector).parent(".active").removeClass("active"),b=this.$body.find(this.selector+'[href="'+a+'"]').parent("li").addClass("active"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active")}},a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("scrollspy"),f=typeof c=="object"&&c;e||d.data("scrollspy",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.defaults={offset:10},a(function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),!function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype={constructor:b,show:function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target"),e,f;d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));if(b.parent("li").hasClass("active"))return;e=c.find(".active a").last()[0],b.trigger({type:"show",relatedTarget:e}),f=a(d),this.activate(b.parent("li"),c),this.activate(f,f.parent(),function(){b.trigger({type:"shown",relatedTarget:e})})},activate:function(b,c,d){function g(){e.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),f?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var e=c.find("> .active"),f=d&&a.support.transition&&e.hasClass("fade");f?e.one(a.support.transition.end,g):g(),e.removeClass("in")}},a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("tab");e||d.data("tab",e=new b(this)),typeof c=="string"&&e[c]()})},a.fn.tab.Constructor=b,a(function(){a("body").on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.typeahead.defaults,c),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.$menu=a(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};b.prototype={constructor:b,select:function(){var a=this.$menu.find(".active").attr("data-value");return this.$element.val(a),this.$element.change(),this.hide()},show:function(){var b=a.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:b.top+b.height,left:b.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(b){var c=this,d,e;return this.query=this.$element.val(),this.query?(d=a.grep(this.source,function(a){if(c.matcher(a))return a}),d=this.sorter(d),d.length?this.render(d.slice(0,this.options.items)).show():this.shown?this.hide():this):this.shown?this.hide():this},matcher:function(a){return~a.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(a){var b=[],c=[],d=[],e;while(e=a.shift())e.toLowerCase().indexOf(this.query.toLowerCase())?~e.indexOf(this.query)?c.push(e):d.push(e):b.push(e);return b.concat(c,d)},highlighter:function(a){return a.replace(new RegExp("("+this.query+")","ig"),function(a,b){return"<strong>"+b+"</strong>"})},render:function(b){var c=this;return b=a(b).map(function(b,d){return b=a(c.options.item).attr("data-value",d),b.find("a").html(c.highlighter(d)),b[0]}),b.first().addClass("active"),this.$menu.html(b),this},next:function(b){var c=this.$menu.find(".active").removeClass("active"),d=c.next();d.length||(d=a(this.$menu.find("li")[0])),d.addClass("active")},prev:function(a){var b=this.$menu.find(".active").removeClass("active"),c=b.prev();c.length||(c=this.$menu.find("li").last()),c.addClass("active")},listen:function(){this.$element.on("blur",a.proxy(this.blur,this)).on("keypress",a.proxy(this.keypress,this)).on("keyup",a.proxy(this.keyup,this)),(a.browser.webkit||a.browser.msie)&&this.$element.on("keydown",a.proxy(this.keypress,this)),this.$menu.on("click",a.proxy(this.click,this)).on("mouseenter","li",a.proxy(this.mouseenter,this))},keyup:function(a){switch(a.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}a.stopPropagation(),a.preventDefault()},keypress:function(a){if(!this.shown)return;switch(a.keyCode){case 9:case 13:case 27:a.preventDefault();break;case 38:a.preventDefault(),this.prev();break;case 40:a.preventDefault(),this.next()}a.stopPropagation()},blur:function(a){var b=this;setTimeout(function(){b.hide()},150)},click:function(a){a.stopPropagation(),a.preventDefault(),this.select()},mouseenter:function(b){this.$menu.find(".active").removeClass("active"),a(b.currentTarget).addClass("active")}},a.fn.typeahead=function(c){return this.each(function(){var d=a(this),e=d.data("typeahead"),f=typeof c=="object"&&c;e||d.data("typeahead",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.typeahead.defaults={source:[],items:8,menu:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>'},a.fn.typeahead.Constructor=b,a(function(){a("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(b){var c=a(this);if(c.data("typeahead"))return;b.preventDefault(),c.typeahead(c.data())})})}(window.jQuery);
\ No newline at end of file
diff --git a/data/static/js/jquery-1.7.1.min.js b/data/static/js/jquery-1.7.1.min.js
new file mode 100644 (file)
index 0000000..198b3ff
--- /dev/null
@@ -0,0 +1,4 @@
+/*! jQuery v1.7.1 jquery.com | jquery.org/license */
+(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function cb(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function ca(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bE.test(a)?d(a,e):ca(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)ca(a+"["+e+"]",b[e],c,d);else d(a,b)}function b_(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function b$(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bT,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=b$(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=b$(a,c,d,e,"*",g));return l}function bZ(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bP),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bC(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bx:by,g=0,h=e.length;if(d>0){if(c!=="border")for(;g<h;g++)c||(d-=parseFloat(f.css(a,"padding"+e[g]))||0),c==="margin"?d+=parseFloat(f.css(a,c+e[g]))||0:d-=parseFloat(f.css(a,"border"+e[g]+"Width"))||0;return d+"px"}d=bz(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0;if(c)for(;g<h;g++)d+=parseFloat(f.css(a,"padding"+e[g]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+e[g]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+e[g]))||0);return d+"px"}function bp(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c+(i[c][d].namespace?".":"")+i[c][d].namespace,i[c][d],i[c][d].data)}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?m(g):h==="function"&&(!a.unique||!o.has(g))&&c.push(g)},n=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,l=j||0,j=0,k=c.length;for(;c&&l<k;l++)if(c[l].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}i=!1,c&&(a.once?e===!0?o.disable():c=[]:d&&d.length&&(e=d.shift(),o.fireWith(e[0],e[1])))},o={add:function(){if(c){var a=c.length;m(arguments),i?k=c.length:e&&e!==!0&&(j=a,n(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){i&&f<=k&&(k--,f<=l&&l--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&o.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(i?a.once||d.push([b,c]):(!a.once||!e)&&n(b,c));return this},fire:function(){o.fireWith(this,arguments);return this},fired:function(){return!!e}};return o};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p,q=c.createElement("div"),r=c.documentElement;q.setAttribute("className","t"),q.innerHTML="   <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="<div "+n+"><div></div></div>"+"<table "+n+" cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="<div style='width:4px;'></div>",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h=null;if(typeof a=="undefined"){if(this.length){h=f.data(this[0]);if(this[0].nodeType===1&&!f._data(this[0],"parsedAttrs")){e=this[0].attributes;for(var i=0,j=e.length;i<j;i++)g=e[i].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),l(this[0],g,h[g]));f._data(this[0],"parsedAttrs",!0)}}return h}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split("."),d[1]=d[1]?"."+d[1]:"";if(c===b){h=this.triggerHandler("getData"+d[1]+"!",[d[0]]),h===b&&this.length&&(h=f.data(this[0],a),h=l(this[0],a,h));return h===b&&d[1]?this.data(d[0]):h}return this.each(function(){var b=f(this),e=[d[0],c];b.triggerHandler("setData"+d[1]+"!",e),f.data(this,a,c),b.triggerHandler("changeData"+d[1]+"!",e)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise()}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h<g;h++)e=d[h],e&&(c=f.propFix[e]||e,f.attr(a,e,""),a.removeAttribute(v?e:c),u.test(e)&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};
+f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=[],j,k,l,m,n,o,p,q,r,s,t;g[0]=c,c.delegateTarget=this;if(e&&!c.target.disabled&&(!c.button||c.type!=="click")){m=f(this),m.context=this.ownerDocument||this;for(l=c.target;l!=this;l=l.parentNode||this){o={},q=[],m[0]=l;for(j=0;j<e;j++)r=d[j],s=r.selector,o[s]===b&&(o[s]=r.quick?H(l,r.quick):m.is(s)),o[s]&&q.push(r);q.length&&i.push({elem:l,matches:q})}}d.length>e&&i.push({elem:this,matches:d.slice(e)});for(j=0;j<i.length&&!c.isPropagationStopped();j++){p=i[j],c.currentTarget=p.elem;for(k=0;k<p.matches.length&&!c.isImmediatePropagationStopped();k++){r=p.matches[k];if(h||!c.namespace&&!r.namespace||c.namespace_re&&c.namespace_re.test(r.namespace))c.data=r.data,c.handleObj=r,n=((f.event.special[r.origType]||{}).handle||r.handler).apply(p.elem,g),n!==b&&(c.result=n,n===!1&&(c.preventDefault(),c.stopPropagation()))}}return c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0)}),d._submit_attached=!0)})},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on.call(this,a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.type+"."+e.namespace:e.type,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.POS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function()
+{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bp)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1></$2>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bn(k[i]);else bn(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||be.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bq=/alpha\([^)]*\)/i,br=/opacity=([^)]*)/,bs=/([A-Z]|^ms)/g,bt=/^-?\d+(?:px)?$/i,bu=/^-?\d/,bv=/^([\-+])=([\-+.\de]+)/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Left","Right"],by=["Top","Bottom"],bz,bA,bB;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bz(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bv.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bz)return bz(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bC(a,b,d);f.swap(a,bw,function(){e=bC(a,b,d)});return e}},set:function(a,b){if(!bt.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cv(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cu("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cu("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cv(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cn.test(h)?(o=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),o?(f._data(this,"toggle"+i,o==="show"?"hide":"show"),j[o]()):j[h]()):(k=co.exec(h),l=j.cur(),k?(m=parseFloat(k[2]),n=k[3]||(f.cssNumber[i]?"":"px"),n!=="px"&&(f.style(this,i,(m||1)+n),l=(m||1)/j.cur()*l,f.style(this,i,l+n)),k[1]&&(m=(k[1]==="-="?-1:1)*m+l),j.custom(l,m,n)):j.custom(l,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:cu("show",1),slideUp:cu("hide",1),slideToggle:cu("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cr||cs(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){e.options.hide&&f._data(e.elem,"fxshow"+e.prop)===b&&f._data(e.elem,"fxshow"+e.prop,e.start)},h()&&f.timers.push(h)&&!cp&&(cp=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cr||cs(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cp),cp=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(["width","height"],function(a,b){f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cy(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.support.fixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.support.fixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window);
\ No newline at end of file
diff --git a/data/static/js/jquery-1.7.2.min.js b/data/static/js/jquery-1.7.2.min.js
new file mode 100644 (file)
index 0000000..16ad06c
--- /dev/null
@@ -0,0 +1,4 @@
+/*! jQuery v1.7.2 jquery.com | jquery.org/license */
+(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"<!doctype html>":"")+"<html><body>"),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function ca(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function b_(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bD.test(a)?d(a,e):b_(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&f.type(b)==="object")for(var e in b)b_(a+"["+e+"]",b[e],c,d);else d(a,b)}function b$(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function bZ(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bS,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bZ(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bZ(a,c,d,e,"*",g));return l}function bY(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bO),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bB(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?1:0,g=4;if(d>0){if(c!=="border")for(;e<g;e+=2)c||(d-=parseFloat(f.css(a,"padding"+bx[e]))||0),c==="margin"?d+=parseFloat(f.css(a,c+bx[e]))||0:d-=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0;return d+"px"}d=by(a,b);if(d<0||d==null)d=a.style[b];if(bt.test(d))return d;d=parseFloat(d)||0;if(c)for(;e<g;e+=2)d+=parseFloat(f.css(a,"padding"+bx[e]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+bx[e]))||0);return d+"px"}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;b.nodeType===1&&(b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?b.outerHTML=a.outerHTML:c!=="input"||a.type!=="checkbox"&&a.type!=="radio"?c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text):(a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value)),b.removeAttribute(f.expando),b.removeAttribute("_submit_attached"),b.removeAttribute("_change_attached"))}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c,i[c][d])}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h,i){var j,k=d==null,l=0,m=a.length;if(d&&typeof d=="object"){for(l in d)e.access(a,c,l,d[l],1,h,f);g=1}else if(f!==b){j=i===b&&e.isFunction(f),k&&(j?(j=c,c=function(a,b,c){return j.call(e(a),c)}):(c.call(a,f),c=null));if(c)for(;l<m;l++)c(a[l],d,j?f.call(a[l],l,c(a[l],d)):f,i);g=1}return g?a:k?c.call(a):m?c(a[0],d):h},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m,n=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?n(g):h==="function"&&(!a.unique||!p.has(g))&&c.push(g)},o=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,j=!0,m=k||0,k=0,l=c.length;for(;c&&m<l;m++)if(c[m].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}j=!1,c&&(a.once?e===!0?p.disable():c=[]:d&&d.length&&(e=d.shift(),p.fireWith(e[0],e[1])))},p={add:function(){if(c){var a=c.length;n(arguments),j?l=c.length:e&&e!==!0&&(k=a,o(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){j&&f<=l&&(l--,f<=m&&m--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&p.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(j?a.once||d.push([b,c]):(!a.once||!e)&&o(b,c));return this},fire:function(){p.fireWith(this,arguments);return this},fired:function(){return!!i}};return p};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p=c.createElement("div"),q=c.documentElement;p.setAttribute("className","t"),p.innerHTML="   <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="<div "+n+"display:block;'><div style='"+t+"0;display:block;overflow:hidden;'></div></div>"+"<table "+n+"' cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="<table><tr><td style='"+t+"0;display:none'></td><td>t</td></tr></table>",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="<div style='width:5px;'></div>",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h,i,j=this[0],k=0,m=null;if(a===b){if(this.length){m=f.data(j);if(j.nodeType===1&&!f._data(j,"parsedAttrs")){g=j.attributes;for(i=g.length;k<i;k++)h=g[k].name,h.indexOf("data-")===0&&(h=f.camelCase(h.substring(5)),l(j,h,m[h]));f._data(j,"parsedAttrs",!0)}}return m}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!";return f.access(this,function(c){if(c===b){m=this.triggerHandler("getData"+e,[d[0]]),m===b&&j&&(m=f.data(j,a),m=l(j,a,m));return m===b&&d[1]?this.data(d[0]):m}d[1]=c,this.each(function(){var b=f(this);b.triggerHandler("setData"+e,d),f.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length<d)return f.queue(this[0],a);return c===b?this:this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise(c)}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,f.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i<g;i++)e=d[i],e&&(c=f.propFix[e]||e,h=u.test(e),h||f.attr(a,e,""),a.removeAttribute(v?e:c),h&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0,coords:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(
+a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:g&&G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=f.event.special[c.type]||{},j=[],k,l,m,n,o,p,q,r,s,t,u;g[0]=c,c.delegateTarget=this;if(!i.preDispatch||i.preDispatch.call(this,c)!==!1){if(e&&(!c.button||c.type!=="click")){n=f(this),n.context=this.ownerDocument||this;for(m=c.target;m!=this;m=m.parentNode||this)if(m.disabled!==!0){p={},r=[],n[0]=m;for(k=0;k<e;k++)s=d[k],t=s.selector,p[t]===b&&(p[t]=s.quick?H(m,s.quick):n.is(t)),p[t]&&r.push(s);r.length&&j.push({elem:m,matches:r})}}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k<j.length&&!c.isPropagationStopped();k++){q=j[k],c.currentTarget=q.elem;for(l=0;l<q.matches.length&&!c.isImmediatePropagationStopped();l++){s=q.matches[l];if(h||!c.namespace&&!s.namespace||c.namespace_re&&c.namespace_re.test(s.namespace))c.data=s.data,c.handleObj=s,o=((f.event.special[s.origType]||{}).handle||s.handler).apply(q.elem,g),o!==b&&(c.result=o,o===!1&&(c.preventDefault(),c.stopPropagation()))}}i.postDispatch&&i.postDispatch.call(this,c);return c.result}},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),d._submit_attached=!0)})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9||d===11){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.globalPOS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")[\\s/>]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f
+.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(f.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(g){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,function(a,b){b.src?f.ajax({type:"GET",global:!1,url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1></$2>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]==="<table>"&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i<u;i++)bn(l[i]);else bn(l);l.nodeType?j.push(l):j=f.merge(j,l)}if(d){g=function(a){return!a.type||be.test(a.type)};for(k=0;j[k];k++){h=j[k];if(e&&f.nodeName(h,"script")&&(!h.type||be.test(h.type)))e.push(h.parentNode?h.parentNode.removeChild(h):h);else{if(h.nodeType===1){var v=f.grep(h.getElementsByTagName("script"),g);j.splice.apply(j,[k+1,0].concat(v))}d.appendChild(h)}}}return j},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bp=/alpha\([^)]*\)/i,bq=/opacity=([^)]*)/,br=/([A-Z]|^ms)/g,bs=/^[\-+]?(?:\d*\.)?\d+$/i,bt=/^-?(?:\d*\.)?\d+(?!px)[^\d\s]+$/i,bu=/^([\-+])=([\-+.\de]+)/,bv=/^margin/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Top","Right","Bottom","Left"],by,bz,bA;f.fn.css=function(a,c){return f.access(this,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)},a,c,arguments.length>1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),(e===""&&f.css(d,"display")==="none"||!f.contains(d.ownerDocument.documentElement,d))&&f._data(d,"olddisplay",cu(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(ct("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(ct("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o,p,q;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]);if((k=f.cssHooks[g])&&"expand"in k){l=k.expand(a[g]),delete a[g];for(i in l)i in a||(a[i]=l[i])}}for(g in a){h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cu(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cm.test(h)?(q=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),q?(f._data(this,"toggle"+i,q==="show"?"hide":"show"),j[q]()):j[h]()):(m=cn.exec(h),n=j.cur(),m?(o=parseFloat(m[2]),p=m[3]||(f.cssNumber[i]?"":"px"),p!=="px"&&(f.style(this,i,(o||1)+p),n=(o||1)/j.cur()*n,f.style(this,i,n+p)),m[1]&&(o=(m[1]==="-="?-1:1)*o+n),j.custom(n,o,p)):j.custom(n,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:ct("show",1),slideUp:ct("hide",1),slideToggle:ct("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a){return a},swing:function(a){return-Math.cos(a*Math.PI)/2+.5}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cq||cr(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){f._data(e.elem,"fxshow"+e.prop)===b&&(e.options.hide?f._data(e.elem,"fxshow"+e.prop,e.start):e.options.show&&f._data(e.elem,"fxshow"+e.prop,e.end))},h()&&f.timers.push(h)&&!co&&(co=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cq||cr(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(co),co=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(cp.concat.apply([],cp),function(a,b){b.indexOf("margin")&&(f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)})}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cv,cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?cv=function(a,b,c,d){try{d=a.getBoundingClientRect()}catch(e){}if(!d||!f.contains(c,a))return d?{top:d.top,left:d.left}:{top:0,left:0};var g=b.body,h=cy(b),i=c.clientTop||g.clientTop||0,j=c.clientLeft||g.clientLeft||0,k=h.pageYOffset||f.support.boxModel&&c.scrollTop||g.scrollTop,l=h.pageXOffset||f.support.boxModel&&c.scrollLeft||g.scrollLeft,m=d.top+k-i,n=d.left+l-j;return{top:m,left:n}}:cv=function(a,b,c){var d,e=a.offsetParent,g=a,h=b.body,i=b.defaultView,j=i?i.getComputedStyle(a,null):a.currentStyle,k=a.offsetTop,l=a.offsetLeft;while((a=a.parentNode)&&a!==h&&a!==c){if(f.support.fixedPosition&&j.position==="fixed")break;d=i?i.getComputedStyle(a,null):a.currentStyle,k-=a.scrollTop,l-=a.scrollLeft,a===e&&(k+=a.offsetTop,l+=a.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(a.nodeName))&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),g=e,e=a.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&d.overflow!=="visible"&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),j=d}if(j.position==="relative"||j.position==="static")k+=h.offsetTop,l+=h.offsetLeft;f.support.fixedPosition&&j.position==="fixed"&&(k+=Math.max(c.scrollTop,h.scrollTop),l+=Math.max(c.scrollLeft,h.scrollLeft));return{top:k,left:l}},f.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){f.offset.setOffset(this,a,b)});var c=this[0],d=c&&c.ownerDocument;if(!d)return null;if(c===d.body)return f.offset.bodyOffset(c);return cv(c,d,d.documentElement)},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window);
\ No newline at end of file
diff --git a/data/static/js/jquery.js b/data/static/js/jquery.js
new file mode 120000 (symlink)
index 0000000..7cbcec3
--- /dev/null
@@ -0,0 +1 @@
+jquery-1.7.2.min.js
\ No newline at end of file
index a294dc10d171c6d2a6250e6103f246e0dcaa58c4..c0de1e6db70a4ed0ffaed96bfae78a61f3220a79 100644 (file)
@@ -1,4 +1,10 @@
 
+$(document).ready(
+       function() {
+               $('.dropdown-toggle').dropdown();
+       }
+);
+
 function getCookie(name) {
     var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
     return r ? r[1] : undefined;
diff --git a/data/templates/advanced.html b/data/templates/advanced.html
new file mode 100644 (file)
index 0000000..795730d
--- /dev/null
@@ -0,0 +1,65 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Advanced options") }}{% end block %}
+
+{% block body %}
+       <h1>{{ _("Advanced options") }}</h1>
+       <p>
+               {{ _("On this page you will find advanced things to do.") }}
+       </p>
+
+       <h2>{{ _("Statistics") }}</h2>
+       <p>
+               {{ _("See a lot of interesting statistics from the build service.") }}
+       </p>
+       <ul>
+               <li>
+                       <a href="/statistics">{{ _("Statistics overview") }}</a>
+               </li>
+       </ul>
+
+       <h2>{{ _("Users") }}</h2>
+       <p>
+               {{ _("See a list of all users.") }}
+       </p>
+       <ul>
+               <li>
+                       <a href="/users">{{ _("User list") }}</a>
+               </li>
+       </ul>
+
+       <h2>{{ _("Mirrors") }}</h2>
+       <ul>
+               <li>
+                       <a href="/mirrors">{{ _("Mirror list") }}</a>
+               </li>
+
+               {% if current_user and current_user.is_admin() %}
+                       <li>
+                               <a href="/mirror/new">{{ _("Add new mirror") }}</a>
+                       </li>
+               {% end %}
+       </ul>
+
+       <h2>{{ _("Logs") }}</h2>
+       <p>
+               {{ _("If you need detailed information about what happended you may want to have a look at the logs.") }}
+       </p>
+       <ul>
+               <a href="/logs">{{ _("Logs") }}</a>
+       </ul>
+
+       {% if current_user and current_user.is_admin() %}
+               <h2>{{ _("Administrator's stuff") }}</h2>
+               <p>
+                       {{ _("These are a bunch of functions only available for administrators.") }}
+               </p>
+               <ul>
+                       <li>
+                               <a href="/uploads">{{ _("Running uploads") }}</a>
+                       </li>
+               </ul>
+       {% end %}
+
+       <div style="clear: both;">&nbsp;</div>
+{% end block %}
diff --git a/data/templates/base-form1.html b/data/templates/base-form1.html
new file mode 100644 (file)
index 0000000..9ca1084
--- /dev/null
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+
+{% block container %}
+       <div class="container container-body" style="width: 620px">
+               {% block body %}{% end block %}
+       </div>
+{% end block %}
diff --git a/data/templates/base-form2.html b/data/templates/base-form2.html
new file mode 100644 (file)
index 0000000..c266de7
--- /dev/null
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+
+{% block container %}
+       <div class="container container-body" style="width: 460px">
+               {% block body %}{% end block %}
+       </div>
+{% end block %}
index df805231bd80186a0175a26aecd44e3c041974ef..49dadbd209601edba70357254eec3647c2568d11 100644 (file)
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<!DOCTYPE html>
+<html lang="en">
        <head>
                <title>{{ hostname }} - {% block title %}{{ _("No title given") }}{% end block %}</title>
                <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+               <meta name="author" content="IPFire.org - Pakfire Development Team" />
 
                <!-- styling stuff -->
+               <link rel="stylesheet" type="text/css" href="{{ static_url("css/bootstrap.min.css") }}" />
                <link rel="stylesheet" type="text/css" href="{{ static_url("css/style.css") }}" />
+               <link href="https://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet" type="text/css">
 
-               <!-- javascript stuff -->
-               <script src="{{ static_url("js/jquery-1.6.min.js") }}"></script>
-               <script src="{{ static_url("js/pbs.js") }}"></script>
+               <!-- disable responsive layout
+               <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+               <link rel="stylesheet" type="text/css" href="{{ static_url("css/bootstrap-responsive.min.css") }}" />
+               -->
        </head>
+
        <body>
-               <div id="wrapper">
-                       <div id="logo">
-                               <div id="user">
-                                       {% if current_user %}
-                                               <a href="/profile"><span>{{ current_user.realname }}</span></a> |
-                                               <a href="/logout">{{ _("Logout") }}</a>
-                                       {% else %}
-                                               <a href="/login">{{ _("Login") }}</a> |
-                                               <a href="/register">{{ _("Register") }}</a>
-                                       {% end %}
-                               </div>
-                               <h1><a href="/">Pakfire</a> build service</h1>
-                               <p>{{ _("A service by the %s.") % """<a href="http://www.ipfire.org/" target="_blank">IPFire Project</a>""" }}</p>
-                       </div>
-                       <div id="menu">
-                               <ul>
-                                       <li>
-                                               <a href="/">{{ _("Index") }}</a>
-                                       </li>
-                                       <li>
-                                               <a href="/packages">{{ _("Packages") }}</a>
-                                       </li>
-                                       <li>
-                                               <a href="/distributions">{{ _("Distributions") }}</a>
-                                       </li>
-                                       <li>
-                                               <a href="/builds">{{ _("Build jobs") }}</a>
-                                       </li>
-                                       <li>
-                                               <a href="/builders">{{ _("Build servers") }}</a>
-                                       </li>
-                                       {% if current_user %}
-                                               <li>
-                                                       <a href="/users">{{ _("Users") }}</a>
-                                               </li>
-                                       {% end %}
-                                       <li>
-                                               <a href="/log">{{ _("Log") }}</a>
-                                       </li>
-                                       <li class="search">
-                                               <form method="GET" action="/search">
-                                                       <input id="search" type="text" name="q" value="{{ _("Search...") }}" />
+               <div class="navbar navbar-fixed-top">
+                       <div class="navbar-inner">
+                               <div class="container">
+                                       <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+                                               <span class="icon-bar"></span>
+                                               <span class="icon-bar"></span>
+                                               <span class="icon-bar"></span>
+                                       </a>
+                                       <a class="brand" href="/">
+                                               {{ _("Pakfire Build Service") }}
+                                               <span class="label label-info">{{ _("BETA") }}</span>
+                                       </a>
+                                       <div class="nav-collapse">
+                                               <ul class="nav">
+                                                       <li>
+                                                               <a href="/packages">{{ _("Packages") }}</a>
+                                                       </li>
+                                                       <li>
+                                                               <a href="/builds">{{ _("Builds") }}</a>
+                                                       </li>
+                                                       <li class="dropdown">
+                                                               <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+                                                                       {{ _("More") }} <b class="caret"></b>
+                                                               </a>
+                                                               <ul class="dropdown-menu">
+                                                                       <li>
+                                                                               <a href="/documents">
+                                                                                       <i class="icon-book"></i>
+                                                                                       {{ _("Documentation") }}
+                                                                               </a>
+                                                                       </li>
+                                                                       <li>
+                                                                               <a href="/search">
+                                                                                       <i class="icon-search"></i>
+                                                                                       {{ _("Search") }}
+                                                                               </a>
+                                                                       </li>
+                                                                       <li class="divider"></li>
+                                                                       <li>
+                                                                               <a href="/distros">
+                                                                                       <i class="icon-star"></i>
+                                                                                       {{ _("Distributions") }}
+                                                                               </a>
+                                                                       </li>
+                                                                       <li>
+                                                                               <a href="/builders">
+                                                                                       <i class="icon-cog"></i>
+                                                                                       {{ _("Builders") }}
+                                                                               </a>
+                                                                       </li>
+                                                                       <li>
+                                                                               <a href="/mirrors">
+                                                                                       <i class="icon-road"></i>
+                                                                                       {{ _("Mirrors") }}
+                                                                               </a>
+                                                                       </li>
+                                                                       <li>
+                                                                               <a href="/keys">
+                                                                                       <i class="icon-barcode"></i>
+                                                                                       {{ _("Key management") }}
+                                                                               </a>
+                                                                       </li>
+
+                                                                       {% if current_user %}
+                                                                               <li>
+                                                                                       <a href="/users">
+                                                                                               <i class="icon-user"></i>
+                                                                                               {{ _("Users") }}
+                                                                                       </a>
+                                                                               </li>
+                                                                       {% end %}
+
+                                                                       <li class="divider"></li>
+                                                                       <li>
+                                                                               <a href="/statistics">
+                                                                                       <i class="icon-align-left"></i>
+                                                                                       {{ _("Statistics") }}
+                                                                               </a>
+                                                                       </li>
+                                                                       <li>
+                                                                               <a href="/advanced">{{ _("Even more...") }}</a>
+                                                                       </li>
+
+                                                                       {% if current_user and current_user.is_admin() %}
+                                                                               <li class="divider"></li>
+                                                                               <li class="nav-header">
+                                                                                       {{ _("Administration") }}
+                                                                               </li>
+                                                                               <li>
+                                                                                       <a href="/uploads">
+                                                                                               <i class="icon-upload"></i>
+                                                                                               {{ _("Uploads") }}
+                                                                                       </a>
+                                                                               </li>
+                                                                       {% end %}
+                                                               </ul>
+                                                       </li>
+                                               </ul>
+                                       
+                                               <ul class="nav pull-right">
+                                                       <li class="divider-vertical"></li>
+
+                                                       {% if current_user %}
+                                                               <li class="dropdown">
+                                                                       <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+                                                                               <i class="icon-user icon-white"></i>
+                                                                               <b class="caret"></b>
+                                                                       </a>
+                                                                       <ul class="dropdown-menu">
+                                                                               <li class="nav-header">
+                                                                                       {{ _("Logged in as") }}
+                                                                               </li>
+                                                                               <li>
+                                                                                       <a href="/profile">
+                                                                                               {{ escape(current_user.realname) }}
+                                                                                       </a>
+                                                                               </li>
+                                                                               {% if session and session.impersonated_user %}
+                                                                                       <li class="nav-header">{{ _("Impersonated by") }}</li>
+                                                                                       <li>
+                                                                                               <a href="/user/{{ escape(session.user.name) }}">
+                                                                                                       <i class="icon-user"></i>
+                                                                                                       {{ escape(session.user.realname) }}
+                                                                                               </a>
+                                                                                       </li>
+                                                                               {% end %}
+                                                                               <li class="divider"></li>
+                                                                               <li>
+                                                                                       <a href="/profile">
+                                                                                               <i class="icon-user"></i>
+                                                                                               {{ _("My profile") }}
+                                                                                       </a>
+                                                                               </li>
+                                                                               <li>
+                                                                                       <a href="/profile/builds">
+                                                                                               <i class="icon-signal"></i>
+                                                                                               {{ _("My builds") }}
+                                                                                       </a>
+                                                                               </li>
+                                                                               <li class="divider"></li>
+
+                                                                               {% if session and session.impersonated_user %}
+                                                                                       <li>
+                                                                                               <a href="/user/impersonate?action=stop">
+                                                                                                       <i class="icon-off"></i>
+                                                                                                       {{ _("End impersonation") }}
+                                                                                               </a>
+                                                                                       </li>
+                                                                               {% else %}
+                                                                                       <li>
+                                                                                               <a href="/logout">
+                                                                                                       <i class="icon-off"></i>
+                                                                                                       {{ _("Logout") }}
+                                                                                               </a>
+                                                                                       </li>
+                                                                               {% end %}
+                                                                       </ul>
+                                                               </li>
+                                                       {% else %}
+                                                               <li>
+                                                                       <a data-toggle="modal" href="#login">{{ _("Login") }}</a>
+                                                               </li>
+                                                       {% end %}
+                                               </ul>
+
+                                               <form class="navbar-search pull-right" method="GET" action="/search">
+                                                       <input type="text" class="search-query" placeholder="{{ _("Search...") }}" name="q">
                                                </form>
-                                       </li>
-                               </ul>
-                       </div>
-                       <!-- <div id="header">
-                               <div id="search">
-                                       <form method="get" action="">
-                                               <fieldset>
-                                                       <input type="text" name="s" id="search-text" size="15" value="enter keywords here..." />
-                                                       <input type="submit" id="search-submit" value="GO" />
-                                               </fieldset>
-                                       </form>
-                               </div>
-                       </div> -->
-                       <div id="page">
-                               <div id="page-bgtop">
-                                       <div id="page-bgbtm">
-                                               <div id="content">
-                                                       {% block body %}EMPTY BODY{% end block %}
-                                               </div>
-                                               <div id="sidebar">
-                                                       {% block sidebar %}
-                                                               <img src="{{ static_url("images/ipfire_tux_128x128.png") }}" alt="IPFire Logo" />
-                                                       {% end block %}
-                                               </div>
-                                               <div style="clear: both;">&nbsp;</div>
                                        </div>
                                </div>
                        </div>
                </div>
 
-               <div id="two-columns">
-                       <div id="column1">
-                               <h2>{{ _("About Pakfire") }}</h2>
-                               <p>
-                                       {{ _("Pakfire is the buildsystem that is used to build the IPFire Linux firewall distribution.") }}
-                                       {{ _("It also installs and updates packages on the IPFire systems.") }}
-                               </p>
+               {% block container %}
+                       <div class="container container-body">
+                               {% block body %}EMPTY BODY{% end block %}
+
+                               {{ modules.Footer() }}
                        </div>
-                       <div id="column2">
-                               <h2>{{ _("Documentation") }}</h2>
-                               <ul>
-                                       <li>
-                                               <a href="/documents">{{ _("Documentation index") }}</a>
-                                       </li>
-                               </ul>
+               {% end block %}
+
+               {% if not current_user %}
+                       <div class="modal hide fade" id="login">
+                               <form id="loginfrm" class="modal-form form-horizontal" method="POST" action="/login">
+                                       {{ xsrf_form_html() }}
+
+                                       <div class="modal-header">
+                                               <a class="close" data-dismiss="modal">&times;</a>
+                                               <h3>{{ _("Welcome to Pakfire Build Service!") }}</h3>
+                                       </div>
+
+                                       <div class="modal-body">
+                                               <fieldset>
+                                                       <div class="control-group">
+                                                               <label class="control-label" for="name">{{ _("Username") }}</label>
+                                                               <div class="controls">
+                                                                       <input type="text" class="input-xlarge" id="name" name="name" />
+                                                               </div>
+                                                       </div>
+
+                                                       <div class="control-group">
+                                                               <label class="control-label" for="pass">{{ _("Password") }}</label>
+                                                               <div class="controls">
+                                                                       <input type="password" class="input-xlarge" id="pass" name="pass" />
+                                                               </div>
+                                                       </div>
+                                               </fieldset>
+
+                                               <p>
+                                                       {{ _("Please type your credentials into the form above in order to login.") }}
+                                               </p>
+
+                                               <hr />
+
+                                               <ul>
+                                                       <li>
+                                                               <a href="/register">
+                                                                       {{ _("Register a new account") }}
+                                                               </a>
+                                                       </li>
+                                                       <li>
+                                                               <a href="/password-recovery">
+                                                                       {{ _("Forgot your password?") }}
+                                                               </a>
+                                                       </li>
+                                               </ul>
+                                       </div>
+
+                                       <div class="modal-footer">
+                                               <button class="btn btn-primary" type="submit">{{ _("Login") }}</button>
+                                               <a class="btn" href="#" data-dismiss="modal">{{ _("Cancel") }}</a>
+                                       </div>
+                               </form>
                        </div>
-               </div>
-               <div id="footer">
-                       <p>Copyright (c) 2011 IPFire.org. {{ _("All rights reserved.") }}</p>
-               </div>
+               {% end %}
+
+               <!-- include javascript files -->
+               <script src="{{ static_url("js/jquery.js") }}"></script>
+               <script src="{{ static_url("js/bootstrap.min.js") }}"></script>
+               <!-- <script src="{{ static_url("js/pbs.js") }}"></script> -->
        </body>
 </html>
diff --git a/data/templates/build-bugs.html b/data/templates/build-bugs.html
new file mode 100644 (file)
index 0000000..772ab38
--- /dev/null
@@ -0,0 +1,169 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Bug list") }}: {{ build.name }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(build.pkg.name) }}">{{ escape(build.pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/build/{{ escape(build.uuid) }}">{{ escape(build.name) }}</a>
+               </li>
+       </ul>
+
+       {{ modules.BuildHeadline(_("Bug list"), build, short=True) }}
+
+       <div class="row">
+               <div class="span8 offset2">
+                       {% if fixed_bugs %}
+                               <h2>{{ _("Fixed bugs") }}</h2>
+                               {{ modules.BugsTable(pkg, fixed_bugs) }}
+                       {% else %}
+                               <p>
+                                       {{ _("No bugs here, yet.") }}
+                                       {{ _("Click below, to add one.") }}
+                               </p>
+                       {% end %}
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span8 offset2">
+                       <div class="btn-toolbar pull-right">
+                               <div class="btn-group">
+                                       <a class="btn" href="#add" data-toggle="modal">
+                                               {{ _("Add") }}
+                                       </a>
+
+                                       {% if fixed_bugs %}
+                                               <a class="btn" href="#rem" data-toggle="modal">
+                                                       {{ _("Remove") }}
+                                               </a>
+                                       {% end %}
+                               </div>
+
+                               <div class="btn-group">
+                                       <a class="btn" href="/build/{{ build.uuid }}">
+                                               {{ _("Back") }}
+                                       </a>
+                               </div>
+                       </div>
+               </div>
+       </div>
+
+       <div class="modal hide fade" id="add">
+               <form class="modal-form form-horizontal" method="POST" action="">
+                       {{ xsrf_form_html() }}
+                       <input type="hidden" name="action" value="add" />
+
+                       <div class="modal-header">
+                               <a class="close" data-dismiss="modal">&times;</a>
+                               <h3>{{ _("Add a bug") }}</h3>
+                       </div>
+
+                       <div class="modal-body">
+                               <fieldset>
+                                       <div class="control-group">
+                                               <label class="control-label" for="bugid">{{ _("Bug ID") }}</label>
+                                               <div class="controls">
+                                                       <div class="input-prepend">
+                                                               <span class="add-on">#</span><input class="span2" id="bugid" name="bugid" size="16" type="text">
+                                                       </div>
+
+                                                       <p class="help-block">
+                                                               {{ _("Enter a bug ID.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+                               </fieldset>
+
+                               {% if open_bugs %}
+                                       <hr />
+                                       <p>
+                                               {{ _("This is a list of more open bugs of this package.") }}
+                                       </p>
+                                       <p>
+                                               {{ _("Maybe you want to pick one of these.") }}
+                                       </p>
+
+                                       <table class="table table-condensed">
+                                               {% for bug in open_bugs %}
+                                                       <tr>
+                                                               <td>
+                                                                       #{{ bug.id }} - {{ bug.status }}
+                                                               </td>
+                                                               <td>
+                                                                       {{ bug.summary }}
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+                                       </table>
+                               {% end %}
+                       </div>
+
+                       <div class="modal-footer">
+                               <button type="submit" class="btn btn-primary">{{ _("Add bug") }}</button>
+                               <a class="btn" href="#" data-dismiss="modal">{{ _("Cancel") }}</a>
+                       </div>
+               </form>
+       </div>
+
+       <div class="modal hide fade" id="rem">
+               <form class="modal-form form-horizontal" method="POST" action="">
+                       {{ xsrf_form_html() }}
+                       <input type="hidden" name="action" value="remove" />
+
+                       <div class="modal-header">
+                               <a class="close" data-dismiss="modal">&times;</a>
+                               <h3>{{ _("Remove a bug") }}</h3>
+                       </div>
+
+                       <div class="modal-body">
+                               <fieldset>
+                                       <div class="control-group">
+                                               <label class="control-label" for="bugid">{{ _("Bug ID") }}</label>
+                                               <div class="controls">
+                                                       <div class="input-prepend">
+                                                               <span class="add-on">#</span><input class="span2" id="bugid" name="bugid" size="16" type="text">
+                                                       </div>
+
+                                                       <p class="help-block">
+                                                               {{ _("Enter a bug ID from the list below.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+                               </fieldset>
+
+                               {% if fixed_bugs %}
+                                       <table class="table table-condensed">
+                                               {% for bug in fixed_bugs %}
+                                                       <tr>
+                                                               <td>
+                                                                       #{{ bug.id }} - {{ bug.status }}
+                                                               </td>
+                                                               <td>
+                                                                       {{ bug.summary }}
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+                                       </table>
+                               {% end %}
+                       </div>
+
+                       <div class="modal-footer">
+                               <button type="submit" class="btn btn-primary">{{ _("Remove bug") }}</button>
+                               <a class="btn" href="#" data-dismiss="modal">{{ _("Cancel") }}</a>
+                       </div>
+               </form>
+       </div>
+{% end block %}
diff --git a/data/templates/build-delete.html b/data/templates/build-delete.html
new file mode 100644 (file)
index 0000000..93e0cb2
--- /dev/null
@@ -0,0 +1,51 @@
+{% extends "base-form1.html" %}
+
+{% block title %}{{ _("Delete build") }}: {{ build.name }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(build.pkg.name) }}">{{ escape(build.pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/build/{{ build.uuid }}">{{ escape(build.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/build/{{ build.uuid }}/delete">{{ _("Delete") }}</a>
+               </li>
+       </ul>
+
+       {{ modules.BuildHeadline(_("Build"), build) }}
+
+       <div class="row">
+               <div class="span8">
+                       <p>
+                               <strong>
+                                       {{ _("You are about to delete build %s.") % escape(build.name) }}
+                               </strong>
+                       </p>
+
+                       <p>
+                               {{ _("Please make sure, that this is the right build you intend to delete.") }}
+                               {{ _("Once a build has been deleted, it can not been recovered.") }}
+                       </p>
+
+                       <hr>
+
+                       <div class="btn-toolbar pull-right">
+                               <a class="btn btn-primary" href="?confirmed=1">{{ _("Delete build") }}</a>
+                               <a class="btn" href="/build/{{ build.uuid }}">{{ _("Cancel") }}</a>
+                       </div>
+               </div>
+       </div>
+{% end block %}
index 7ab1fa6e102505fba112113b1f2a0c7a5ce417ce..008608b0ccff6387516d96b937231b694fe969fd 100644 (file)
 {% block title %}{{ _("Build") }}: {{ build.name }}{% end block %}
 
 {% block body %}
-       {% if build.type == "binary" %}
-               <h1>{{ _("Build") }}: <a href="/package/{{ build.pkg.name }}/{{ build.pkg.epoch }}/{{ build.pkg.version }}/{{ build.pkg.release }}">{{ build.name }}</a></h1>
-       {% elif build.type == "source" %}
-               <h1>{{ _("Build") }}: {{ build.name }}</h1>
-       {% end %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(build.pkg.name) }}">{{ escape(build.pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/build/{{ escape(build.uuid) }}">{{ escape(build.name) }}</a>
+               </li>
+       </ul>
 
-       <table class="form form2">
-               <tr>
-                       <td class="col1">{{ _("ID") }}</td>
-                       <td class="col2">{{ build.uuid }}</td>
-               </tr>
-               <tr>
-                       <td class="col1">{{ _("State") }}</td>
-                       <td class="col2">{{ build.state }}</td>
-               </tr>
-
-               {% if build.type == "binary" %}
-                       <tr>
-                               <td class="col1">{{ _("Package") }}</td>
-                               <td class="col2">
-                                       <a href="/package/{{ build.pkg.name }}/{{ build.pkg.epoch }}/{{ build.pkg.version }}/{{ build.pkg.release }}">{{ build.pkg.friendly_name }}</a>
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Source build") }}</td>
-                               <td class="col2">
-                                       <a href="/build/{{ build.source_build.uuid }}">{{ build.source_build.name }}</a>
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Architecture") }}</td>
-                               <td class="col2">{{ build.arch }}</td>
-                       </tr>
-               {% end %}
-
-               <tr>
-                       <td class="col1">{{ _("Host") }}</td>
-                       {% if build.host %}
-                               <td class="col2">
-                                       <a href="/builder/{{ build.host.name }}">{{ build.host.name }}</a>
-                               </td>
-                       {% else %}
-                               <td class="col2">{{ _("No host assigned, yet.") }}</td>
-                       {% end %}
-               </tr>
-               <tr>
-                       <td class="col1">
-                               {{ _("Priority") }}
-                       </td>
-                       <td class="col2">
-                               {% if build.priority >= 2 %}
-                                       {{ _("Very high") }}
-                               {% elif build.priority == 1 %}
-                                       {{ _("High") }}
-                               {% elif build.priority == 0 %}
-                                       {{ _("Medium") }}
-                               {% elif build.priority == -1 %}
-                                       {{ _("Low") }}
-                               {% elif build.priority <= -2 %}
-                                       {{ _("Very low") }}
-                               {% end %}
-                       </td>
-               </tr>
-       </table>
-       <div style="clear: both;">&nbsp;</div>
-
-       {% if build.type == "source" %}
-               <h2>{{ _("Commit") }}: {{ escape(build.commit_subject) }}</h2>
-               <table class="form form2">
-                       {% if build.commit_body %}
-                               <tr>
-                                       <td colspan="2">
-                                               {{ escape(build.commit_body) }}
-                                       </td>
-                               </tr>
+       {{ modules.BuildHeadline(_("Build"), build) }}
+       {{ modules.BuildStateWarnings(build) }}
+       {{ modules.PackageHeader(pkg) }}
+
+       {% if build.type == "scratch" and build.has_perm(current_user) %}
+               <div class="btn-toolbar pull-right">
+                       <a class="btn btn-danger" href="/build/{{ build.uuid }}/delete">
+                               <i class="icon-trash icon-white"></i>
+                               {{ _("Delete build") }}
+                       </a>
+
+                       {% if current_user.is_admin() %}
+                               <a class="btn btn-danger" href="/build/{{ build.uuid }}/reset">
+                                       {{ _("Reset build") }}
+                               </a>
                        {% end %}
-                       <tr>
-                               <td class="col1">{{ _("Author") }}</td>
-                               <td class="col2">{{ escape(build.commit_author) }}</td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Committer") }}</td>
-                               <td class="col2">{{ escape(build.commit_committer) }}</td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Date") }}</td>
-                               <td class="col2">{{ locale.format_date(build.commit_date or 0, full_format=True) }}</td>
-                       </tr>
-               </table>
-               <div style="clear: both;">&nbsp;</div>
+               </div>
        {% end %}
 
-       <h3>{{ _("Time") }}</h3>
-
-       <table class="form form2">
-               <tr>
-                       <td class="col1">{{ _("Job added") }}</td>
-                       <td class="col2">{{ build.time_added }}</td>
-               </tr>
-               <tr>
-                       <td class="col1">{{ _("Job started") }}</td>
-                       <td class="col2">{{ build.time_started or _("Not started, yet.") }}</td>
-               </tr>
-               <tr>
-                       <td class="col1">{{ _("Job finished") }}</td>
-                       <td class="col2">{{ build.time_finished or _("Not finished, yet.") }}</td>
-               </tr>
-
-               {% if build.duration %}
-                       <tr>
-                               <td class="col1">{{ _("Duration") }}</td>
-                               <td class="col2">{{ build.duration }}</td>
-                       </tr>
-               {% end %}
-       </table>
-       <div style="clear: both;">&nbsp;</div>
-
-       {% if build.packagefiles %}
-               <h3>{{ _("Package files") }}</h3>
-               {{ modules.FilesTable(build.packagefiles) }}
-       {% end %}
+       <div class="row">
+               <div class="span12">
+                       <hr />
+               </div>
+       </div>
+
+       {% if build.type == "release" %}
+               <div class="row">
+                       <div class="span6">
+                               <ul class="nav nav-tabs">
+                                       <li class="active">
+                                               <a href="#update" data-toggle="tab">{{ _("Update") }}</a>
+                                       </li>
+
+                                       <li>
+                                               <a href="#bugs" data-toggle="tab">
+                                                       {{ _("Fixed bugs") }} ({{ len(bugs) }})
+                                               </a>
+                                       </li>
+                               </ul>
+
+                               <div class="tab-content">
+                                       <div class="tab-pane active" id="update">
+                                               <table class="table">
+                                                       <tbody>
+                                                               {% if build.pkg.commit %}
+                                                                       <tr>
+                                                                               <td>{{ _("Commit") }}</td>
+                                                                               <td>
+                                                                                       <a href="/distro/{{ build.distro.identifier }}/source/{{ build.pkg.commit.source.identifier }}/{{ build.pkg.commit.revision }}">{{ build.pkg.commit.revision[:7] }}</a>
+                                                                                       - {{ escape(build.pkg.commit.subject) }}
+                                                                               </td>
+                                                                       </tr>
+                                                               {% end %}
+
+                                                               <tr>
+                                                                       <td>{{ _("Severity") }}</td>
+                                                                       <td>
+                                                                               {% if build.severity is None %}
+                                                                                       {{ _("Unspecified") }}
+                                                                               {% elif build.severity == "security update" %}
+                                                                                       {{ _("Security update") }}
+                                                                               {% elif build.severity == "bugfix update" %}
+                                                                                       {{ _("Bug fix update") }}
+                                                                               {% elif build.severity == "enhancement" %}
+                                                                                       {{ _("Enhancement") }}
+                                                                               {% elif build.severity == "new package" %}
+                                                                                       {{ _("New package") }}
+                                                                               {% else %}
+                                                                                       {{ _("Unhandled: %s") % escape(build.severity) }}
+                                                                               {% end %}
+                                                                       </td>
+                                                               </tr>
+
+                                                               {% if build.message %}
+                                                                       <tr>
+                                                                               <td colspan="2">
+                                                                                       {{ modules.Text(build.message) }}
+                                                                               </td>
+                                                                       </tr>
+                                                               {% end %}
+                                                       </tbody>
+                                               </table>
+                                       </div>
+
+                                       <div class="tab-pane" id="bugs">
+                                               {% if bugs %}
+                                                       {{ modules.BugsTable(pkg, bugs) }}
+                                               {% else %}
+                                                       <p>
+                                                               {{ _("Nothing in here, yet.") }}
+                                                       </p>
+                                               {% end %}
+
+                                               <div class="btn-toolbar pull-right">
+                                                       <div class="btn-group">
+                                                               <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+                                                                       {{ _("Action") }}
+                                                                       <span class="caret"></span>
+                                                               </a>
+
+                                                               <ul class="dropdown-menu">
+                                                                       {% if current_user and build.has_perm(current_user) %}
+                                                                               <li>
+                                                                                       <a href="/build/{{ build.uuid }}/bugs">
+                                                                                               {{ _("Modify bug list") }}
+                                                                                       </a>
+                                                                               </li>
+                                                                               <li class="divider"></li>
+                                                                       {% end %}
+
+                                                                       <li>
+                                                                               <a href="{{ bugtracker.buglist_url(pkg.name) }}" target="_blank">
+                                                                                       {{ _("Show all bugs") }}
+                                                                               </a>
+                                                                       <li>
+                                                                       <li>
+                                                                               <a href="{{ bugtracker.enter_url(pkg.name) }}" target="_blank">
+                                                                                       {{ _("File a new bug") }}
+                                                                               </a>
+                                                                       </li>
+                                                               </ul>
+                                                       </div>
+                                               </div>
+                                       </div>
+                               </div>                          
+                       </div>
+
+                       <div class="span6">
+                               <h3>{{ _("Repository") }}</h3>
+
+                               <table class="table">
+                                       <tbody>
+                                               {% if build.repo %}
+                                                       <tr>
+                                                               <td>
+                                                                       {{ escape(build.distro.name) }} -
+                                                                       <a href="/distro/{{ build.distro.identifier }}/repo/{{ build.repo.identifier }}">{{ build.repo.name }}</a>
+                                                                       {{ _("since %s") % locale.format_date(build.repo_time, relative=False) }}
+                                                               </td>
+                                                       </tr>
+                                               {% else %}
+                                                       <tr>
+                                                               <td>
+                                                                       {{ _("This package does not belong to any repository.") }}
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
 
-       {% if build.logfiles %}
-               <h3>{{ _("Logfiles") }}</h3>
-               {{ modules.FilesTable(build.logfiles) }}
+                                               {% if not build.state == "broken" %}
+                                                       <tr>
+                                                               <td>
+                                                                       {% if current_user and build.has_perm(current_user) %}
+                                                                               <div class="btn-toolbar pull-right">                                                                                    
+                                                                                       {% if current_user.is_admin() or build.can_move_forward %}
+                                                                                               <div class="btn-group">
+                                                                                                       <a class="btn btn-success" href="#push" data-toggle="modal">
+                                                                                                               {{ _("Push") }}
+                                                                                                       </a>
+                                                                                               </div>
+
+                                                                                               {{ modules.Modal("build-push", build=build, current_repo=repo, next_repo=next_repo) }}
+                                                                                       {% end %}
+
+                                                                                       {% if build.repo %}
+                                                                                               <div class="btn-group">
+                                                                                                       <a class="btn btn-danger" href="#unpush" data-toggle="modal">
+                                                                                                               {{ _("Unpush") }}
+                                                                                                       </a>
+                                                                                               </div>
+
+                                                                                               {{ modules.Modal("build-unpush", build=build, repo=repo) }}
+                                                                                       {% end %}
+                                                                               </div>
+                                                                       {% elif build.can_move_forward %}
+                                                                               {{ _("This package may be pushed forward.") }}
+                                                                       {% end %}
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+                                       </tbody>
+                               </table>
+                       </div>
+               </div>
+
+               <div class="row">
+                       <div class="span12">
+                               <hr>
+                       </div>
+               </div>
        {% end %}
+       
+       <div class="row">
+               <div class="span12">
+                       <div class="btn-group pull-right">
+                               <a class="btn btn-primary pull-right" data-toggle="modal" href="#comment" >
+                                       <i class="icon-comment icon-white"></i>
+                                       {{ _("Comment") }}
+                               </a>
+                               <a href="#" class="btn">
+                                       {{ _("Score: %s") % build.credits }}
+                               </a>
+                       </div>
 
-       <h3>{{ _("Log") }}</h3>
-       {{ modules.LogTable(build.log) }}
-{% end block %}
+                       {{ modules.Modal("build-comment", build=build) }}
 
-{% block sidebar %}
-       <h1>{{ _("Actions") }}</h1>
-       <ul>
-               {% if build.state == "failed" %}
-                       <li><a href="/build/schedule/{{ build.uuid }}?type=rebuild">{{ _("Re-submit build") }}</a></li>
-                       <li><a href="?action=perm_failed">{{ _("Mark as permanently failed") }}</a></li>
-               {% end %}
+                       {{ modules.WatchersSidebarTable(build, build.get_watchers()) }}
+                       <br />
+               </div>
+       </div>
 
-               {% if build.type == "binary" and build.state == "finished" %}
-                       <li><a href="/build/schedule/{{ build.uuid }}?type=test">{{ _("Schedule test build") }}</a></li>
-               {% end %}
+       <div class="row">
+               <div class="span12">
+                       {{ modules.Log(log) }}
+               </div>
+       </div>
 
-               <li><a href="/build/priority/{{ build.uuid }}">{{ _("Modify priority") }}</a></li>
-       </ul>
-{% end %}
+       <div class="row">
+               <div class="span12">
+                       <hr>
+
+                       <ul class="nav nav-pills">
+                               <li class="active">
+                                       <a href="#buildjobs" data-toggle="tab">
+                                               {{ _("Build jobs") }}
+                                       </a>
+                               </li>
+                               {% if build.test_jobs %}
+                                       <li>
+                                               <a href="#testjobs" data-toggle="tab">
+                                                       {{ _("Test jobs") }} ({{ len(build.test_jobs) }})
+                                               </a>
+                                       </li>
+                               {% end %}
+                               <li>
+                                       <a href="#properties" data-toggle="tab">
+                                               {{ _("Properties") }}
+                                       </a>
+                               </li>
+                       </ul>
+
+                       <div class="tab-content">
+                               <div class="tab-pane active" id="buildjobs">
+                                       <div class="row">
+                                               <div class="span12">
+                                                       {{ modules.JobsTable(build) }}
+                                               </div>
+                                       </div>
+                               </div>
+
+                               {% if build.test_jobs %}
+                                       <div class="tab-pane" id="testjobs">
+                                               <div class="row">
+                                                       <div class="span12">
+                                                               {{ modules.JobsTable(build, build.test_jobs, type="test") }}
+                                                       </div>
+                                               </div>
+                                       </div>
+                               {% end %}
+
+                               <div class="tab-pane" id="properties">
+                                       <div class="row">
+                                               <div class="span4">
+                                                       <table class="table table-striped">
+                                                               <tr>
+                                                                       <td>{{ _("Created") }}</td>
+                                                                       <td>{{ format_date(build.created, full_format=True) }}</td>
+                                                               </tr>
+                                                               
+                                                               {% if build.owner %}
+                                                                       <tr>
+                                                                               <td>{{ _("Owner") }}</td>
+                                                                               <td>{{ build.owner.realname }}</td>
+                                                                       </tr>
+                                                               {% end %}
+
+                                                               {% if current_user and current_user.is_admin() %}
+                                                                       <tr>
+                                                                               <td>{{ _("Public?") }}</td>
+                                                                               <td>
+                                                                                       {% if build.public %}
+                                                                                               {{ _("Yes") }}
+                                                                                       {% else %}
+                                                                                               {{ _("No") }}
+                                                                                       {% end %}
+                                                                               </td>
+                                                                       </tr>
+                                                               {% end %}
+
+                                                               <tr>
+                                                                       <td>{{ _("Priority") }}</td>
+                                                                       <td>
+                                                                               <a href="/build/{{ build.uuid }}/priority">
+                                                                                       {% if build.priority >= 2 %}
+                                                                                               {{ _("Very high") }}
+                                                                                       {% elif build.priority == 1 %}
+                                                                                               {{ _("High") }}
+                                                                                       {% elif build.priority == 0 %}
+                                                                                               {{ _("Medium") }}
+                                                                                       {% elif build.priority == -1 %}
+                                                                                               {{ _("Low") }}
+                                                                                       {% elif build.priority <= -2 %}
+                                                                                               {{ _("Very low") }}
+                                                                                       {% end %}
+                                                                               </a>
+                                                                       </td>
+                                                               </tr>
+                                                       </table>
+                                               </div>
+
+                                               <div class="span8">
+                                                       <table class="table">
+                                                               <tbody>
+                                                                       <tr>
+                                                                               <td>{{ _("Source package") }}</td>
+                                                                               <td>
+                                                                                       <a href="/package/{{ build.pkg.uuid }}">{{ build.pkg.friendly_name }}</a>
+                                                                               </td>
+                                                                       </tr>
+
+                                                                       {% if build.pkg.requires %}
+                                                                               <tr>
+                                                                                       <td>{{ _("Build dependencies") }}</td>
+                                                                                       <td>
+                                                                                               {{ locale.list(["<a href=\"/search?q=%(r)s\">%(r)s</a>" % { "r" : escape(r) } for r in build.pkg.requires]) }}
+                                                                                       </td>
+                                                                               </tr>
+                                                                       {% end %}
+                                                               </tbody>
+                                                       </table>
+                                               </div>
+                                       </div>
+                               </div>
+                       </div>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/build-detail_old.html b/data/templates/build-detail_old.html
new file mode 100644 (file)
index 0000000..7ab1fa6
--- /dev/null
@@ -0,0 +1,151 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Build") }}: {{ build.name }}{% end block %}
+
+{% block body %}
+       {% if build.type == "binary" %}
+               <h1>{{ _("Build") }}: <a href="/package/{{ build.pkg.name }}/{{ build.pkg.epoch }}/{{ build.pkg.version }}/{{ build.pkg.release }}">{{ build.name }}</a></h1>
+       {% elif build.type == "source" %}
+               <h1>{{ _("Build") }}: {{ build.name }}</h1>
+       {% end %}
+
+       <table class="form form2">
+               <tr>
+                       <td class="col1">{{ _("ID") }}</td>
+                       <td class="col2">{{ build.uuid }}</td>
+               </tr>
+               <tr>
+                       <td class="col1">{{ _("State") }}</td>
+                       <td class="col2">{{ build.state }}</td>
+               </tr>
+
+               {% if build.type == "binary" %}
+                       <tr>
+                               <td class="col1">{{ _("Package") }}</td>
+                               <td class="col2">
+                                       <a href="/package/{{ build.pkg.name }}/{{ build.pkg.epoch }}/{{ build.pkg.version }}/{{ build.pkg.release }}">{{ build.pkg.friendly_name }}</a>
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class="col1">{{ _("Source build") }}</td>
+                               <td class="col2">
+                                       <a href="/build/{{ build.source_build.uuid }}">{{ build.source_build.name }}</a>
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class="col1">{{ _("Architecture") }}</td>
+                               <td class="col2">{{ build.arch }}</td>
+                       </tr>
+               {% end %}
+
+               <tr>
+                       <td class="col1">{{ _("Host") }}</td>
+                       {% if build.host %}
+                               <td class="col2">
+                                       <a href="/builder/{{ build.host.name }}">{{ build.host.name }}</a>
+                               </td>
+                       {% else %}
+                               <td class="col2">{{ _("No host assigned, yet.") }}</td>
+                       {% end %}
+               </tr>
+               <tr>
+                       <td class="col1">
+                               {{ _("Priority") }}
+                       </td>
+                       <td class="col2">
+                               {% if build.priority >= 2 %}
+                                       {{ _("Very high") }}
+                               {% elif build.priority == 1 %}
+                                       {{ _("High") }}
+                               {% elif build.priority == 0 %}
+                                       {{ _("Medium") }}
+                               {% elif build.priority == -1 %}
+                                       {{ _("Low") }}
+                               {% elif build.priority <= -2 %}
+                                       {{ _("Very low") }}
+                               {% end %}
+                       </td>
+               </tr>
+       </table>
+       <div style="clear: both;">&nbsp;</div>
+
+       {% if build.type == "source" %}
+               <h2>{{ _("Commit") }}: {{ escape(build.commit_subject) }}</h2>
+               <table class="form form2">
+                       {% if build.commit_body %}
+                               <tr>
+                                       <td colspan="2">
+                                               {{ escape(build.commit_body) }}
+                                       </td>
+                               </tr>
+                       {% end %}
+                       <tr>
+                               <td class="col1">{{ _("Author") }}</td>
+                               <td class="col2">{{ escape(build.commit_author) }}</td>
+                       </tr>
+                       <tr>
+                               <td class="col1">{{ _("Committer") }}</td>
+                               <td class="col2">{{ escape(build.commit_committer) }}</td>
+                       </tr>
+                       <tr>
+                               <td class="col1">{{ _("Date") }}</td>
+                               <td class="col2">{{ locale.format_date(build.commit_date or 0, full_format=True) }}</td>
+                       </tr>
+               </table>
+               <div style="clear: both;">&nbsp;</div>
+       {% end %}
+
+       <h3>{{ _("Time") }}</h3>
+
+       <table class="form form2">
+               <tr>
+                       <td class="col1">{{ _("Job added") }}</td>
+                       <td class="col2">{{ build.time_added }}</td>
+               </tr>
+               <tr>
+                       <td class="col1">{{ _("Job started") }}</td>
+                       <td class="col2">{{ build.time_started or _("Not started, yet.") }}</td>
+               </tr>
+               <tr>
+                       <td class="col1">{{ _("Job finished") }}</td>
+                       <td class="col2">{{ build.time_finished or _("Not finished, yet.") }}</td>
+               </tr>
+
+               {% if build.duration %}
+                       <tr>
+                               <td class="col1">{{ _("Duration") }}</td>
+                               <td class="col2">{{ build.duration }}</td>
+                       </tr>
+               {% end %}
+       </table>
+       <div style="clear: both;">&nbsp;</div>
+
+       {% if build.packagefiles %}
+               <h3>{{ _("Package files") }}</h3>
+               {{ modules.FilesTable(build.packagefiles) }}
+       {% end %}
+
+       {% if build.logfiles %}
+               <h3>{{ _("Logfiles") }}</h3>
+               {{ modules.FilesTable(build.logfiles) }}
+       {% end %}
+
+       <h3>{{ _("Log") }}</h3>
+       {{ modules.LogTable(build.log) }}
+{% end block %}
+
+{% block sidebar %}
+       <h1>{{ _("Actions") }}</h1>
+       <ul>
+               {% if build.state == "failed" %}
+                       <li><a href="/build/schedule/{{ build.uuid }}?type=rebuild">{{ _("Re-submit build") }}</a></li>
+                       <li><a href="?action=perm_failed">{{ _("Mark as permanently failed") }}</a></li>
+               {% end %}
+
+               {% if build.type == "binary" and build.state == "finished" %}
+                       <li><a href="/build/schedule/{{ build.uuid }}?type=test">{{ _("Schedule test build") }}</a></li>
+               {% end %}
+
+               <li><a href="/build/priority/{{ build.uuid }}">{{ _("Modify priority") }}</a></li>
+       </ul>
+{% end %}
index de4f6775d070204794ae408e2131e0c68d1a9870..3830895874ce624d3052b7683e29b8cc024d8eb8 100644 (file)
@@ -6,18 +6,42 @@
        <h1>{{ _("Filter builds") }}</h1>
        <form method="get" action="/builds">
                <table class="form form3">
+                       <tr>
+                               <td class="col1">
+                                       {{ _("Distribution") }}
+                               </td>
+                               <td class="col2">
+                                       <select name="distro">
+                                               <option value="">{{ _("Any") }}</option>
+
+                                               {% for distro in distros %}
+                                                       <option value="{{ escape(distro.identifier) }}">{{ escape(distro.name) }}</option>
+                                               {% end %}
+                                       </select>
+                               </td>
+                               <td class="col3">
+                                       {{ _("Show only builds in that distribution.") }}
+                               </td>
+                       </tr>
                        <tr>
                                <td class="col1">{{ _("State") }}</td>
                                <td class="col2">
                                        <select name="state">
-                                               <option value="">{{ _("All") }}</option>
+<!-- XXX does not apply to builds <option value="">{{ _("All") }}</option>
                                                <option value="running">{{ _("Running") }}</option>
                                                <option value="pending">{{ _("Pending") }}</option>
                                                <option value="finished">{{ _("Finished") }}</option>
                                                <option value="failed">{{ _("Failed") }}</option>
                                                <option value="permanently_failed">{{ _("Permanently failed") }}</option>
                                                <option value="dispatching">{{ _("Dispatching") }}</option>
-                                               <option value="uploading">{{ _("Uploading") }}</option>
+                                               <option value="uploading">{{ _("Uploading") }}</option> -->
+
+                                               <option value="">{{ _("Any") }}</option>
+                                               <option value="building">{{ _("Building") }}</option>
+                                               <option value="testing">{{ _("Testing") }}</option>
+                                               <option value="stable">{{ _("Stable") }}</option>
+                                               <option value="obsolete">{{ _("Obsolete") }}</option>
+                                               <option value="broken">{{ _("Broken") }}</option>
                                        </select>
                                </td>
                                <td class="col3">
@@ -30,7 +54,7 @@
                                        <select name="builder">
                                                <option value="">{{ _("Any") }}</option>
                                                {% for builder in builders %}
-                                                       <option value="{{ builder.id }}">{{ builder.name }}</option>
+                                                       <option value="{{ escape(builder.name) }}">{{ escape(builder.name) }}</option>
                                                {% end %}
                                        </select>
                                </td>
diff --git a/data/templates/build-index.html b/data/templates/build-index.html
new file mode 100644 (file)
index 0000000..9c1d1a9
--- /dev/null
@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Build list") }}{% end block %}
+
+{% block body %}
+       <h1>{{ _("Build list") }}</h1>
+       {{ modules.BuildTable(builds, show_user=True) }}
+{% end block %}
+
+{% block sidebar %}
+       <h1>{{ _("Actions") }}</h1>
+       <ul>
+               <li><a href="/builds/queue">{{ _("Job queue") }}</a></li>
+               <li><a href="/builds/filter">{{ _("Filter builds") }}</a></li>
+       </ul>
+{% end block %}
index de639c3cfdc56615b71654e00443ab08a2f207a2..8ef9751dad3c94e06639c5cf7a023851b9aeb11b 100644 (file)
@@ -2,9 +2,11 @@
 
 {% block title %}{{ _("Build job list") }}{% end block %}
 
+### UNUSED
+
 {% block body %}
        <h1>{{ _("Build job list") }}</h1>
-       {{ modules.BuildTable(builds) }}
+       {{ modules.BuildTable(builds, show_user=True) }}
 {% end block %}
 
 {% block sidebar %}
diff --git a/data/templates/build-manage.html b/data/templates/build-manage.html
new file mode 100644 (file)
index 0000000..a9d7121
--- /dev/null
@@ -0,0 +1,97 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Manage build %s") % escape(build.name) }}{% end block %}
+
+{% block body %}
+       <div class="page-header">
+               <h1>
+                       {{ _("Manage build") }}: {{ escape(build.name) }}
+                       <small>- {{ _("Distribution") }}: {{ escape(distro.name) }}</small>
+               </h1>
+       </div>
+
+       {% if build.critical_path and not current_user.has_perm("manage_critical_path") %}
+               <div class="alert alert-block alert-error">
+                       <h4 class="alert-heading">{{ _("Permission denied") }}</h4>
+                       {{ _("You do not have the permission to update packages that belong to the <em>critical path</em>.") }}
+               </div>
+       {% end %}
+
+       {% if not build.all_jobs_finished %}
+               <div class="alert alert-block alert-warning">
+                       <h4 class="alert-heading">{{ _("Not all jobs are finished") }}</h4>
+                       <p>
+                               {{ _("Not all jobs of this build are finished, yet.") }}
+                               {{ _("It is <strong>strongly discouraged</strong> to push this build into the next repository.") }}
+                       </p>
+                       <p>
+                               {{ _("However, the build will be automatically unpushed if one or more build jobs fail.") }}
+                       </p>
+               </div>
+       {% end %}
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <input type="hidden" name="action" value="push" />
+
+                               <fieldset>
+                                       <legend>
+                                               {% if mode == "admin" %}
+                                                       {{ _("Push to a repository") }}
+                                               {% elif build.repo %}
+                                                       {{ _("Push to next repository") }}
+                                               {% else %}
+                                                       {{ _("Push to first repository") }}
+                                               {% end %}
+                                       </legend>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="repo_push">{{ _("New repository") }}</label>
+                                               <div class="controls">
+                                                       {% if mode == "admin" %}
+                                                               <select id="repo_push" name="repo">
+                                                                       {% for repo in distro.repositories %}
+                                                                               {% if not build.repo == repo %}
+                                                                                       <option value="{{ repo.identifier }}" {% if repo == next_repo %}selected="selected"{% end %}>
+                                                                                               {{ escape(repo.name) }} - {{ escape(repo.summary) }}
+                                                                                       </option>
+                                                                               {% end %}
+                                                                       {% end %}
+                                                               </select>
+                                                       {% elif next_repo %}
+                                                               <input type="hidden" name="repo" value="{{ next_repo.identifier }}" />
+
+                                                               <a href="/distro/{{ distro.identifier }}/repo/{{ next_repo.identifier }}">{{ escape(next_repo.name) }}</a>
+                                                               - {{ escape(next_repo.summary) }}
+                                                       {% end %}
+
+                                                       <p class="help-block">
+                                                               {{ _("This is the target repository for the build.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="form-actions">
+                                               <button type="submit" class="btn btn-primary">{{ _("Push") }}</button>
+                                               <a class="btn btn-danger" href="/build/{{ build.uuid }}/unpush">{{ _("Unpush") }}</a>
+                                               <a class="btn" href="/build/{{ build.uuid }}">{{ _("Cancel") }}</a>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </div>
+
+       {% if current_user and current_user.is_admin() %}
+               <div class="row">
+                       <div class="span6 offset3">
+                               {% if mode == "admin" %}
+                                       <a class="btn pull-right" href="?mode=user">{{ _("Switch to user mode") }}</a>
+                               {% else %}
+                                       <a class="btn pull-right" href="?mode=admin">{{ _("Switch to admin mode") }}</a>
+                               {% end %}
+                       </div>
+               </div>
+       {% end %}
+{% end block %}
diff --git a/data/templates/build-queue.html b/data/templates/build-queue.html
new file mode 100644 (file)
index 0000000..c06fb37
--- /dev/null
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Builds") }} - {{ _("Job queue") }}{% end block %}
+
+{% block body %}
+       <h1>{{ _("Builds") }}: {{ _("Job queue") }}</h1>
+       <p>
+               {{ _("This is a list of all jobs that are waiting to be processed.") }}
+               {{ _("They one at the top is next.") }}
+       </p>
+
+       {% if jobs %}
+               {{ modules.JobsList(jobs) }}
+       {% else %}
+               {{ _("No jobs to do.") }}
+       {% end %}
+{% end %}
diff --git a/data/templates/build-schedule-rebuild.html b/data/templates/build-schedule-rebuild.html
deleted file mode 100644 (file)
index 0277f4e..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}{{ _("Schedule rebuild for %s") % build.name }}{% end block %}
-
-{% block body %}
-       <h1>{{ _("Schedule rebuild for %s") % build.name }}</h1>
-       <p>
-               {{ _("At this place, you can submit failed build jobs to be built again.") }}
-       </p>
-       <p>
-               {{ _("The build job will be started when a build slot is available but not before the given time.") }}
-       </p>
-
-       {{ modules.BuildOffset() }}
-{% end block %}
index 6ccf0c138c9c0572d1b4d0a745ab91ed3437d5b7..72f65067a6a506304c8d0ffd3a4ad82e6f508fc7 100644 (file)
@@ -3,18 +3,47 @@
 {% block title %}{{ _("Schedule test build for %s") % build.name }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Schedule test build for %s") % build.name }}</h1>
-       <p>
-               {{ _("A test build is used to check if a package builds with the current package set.") }}
-               {{ _("In this way, developers are able to find quality issues fast and without actively searching for them.") }}
-       </p>
-       <p>
-               {{ _("As this build platform only has a limited amount of performance, test builds only have a very less priority.") }}
-               {{ _("However, you can manually request to run a test.") }}
-       </p>
-       <p>
-               {{ _("The build job will be started when a build slot is available but not before the given time.") }}
-       </p>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(build.pkg.name) }}">{{ escape(build.pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/build/{{ escape(build.uuid) }}">{{ escape(build.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/build/{{ build.uuid }}/schedule?type=test">{{ _("Schedule test build") }}</a>
+               </li>
+       </ul>
 
-       {{ modules.BuildOffset() }}
+       <div class="page-header">
+               <h1>{{ _("Schedule test build for %s") % job.name }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("A test build is used to check if a package builds with the current package set.") }}
+                               {{ _("In this way, developers are able to find quality issues fast and without actively searching for them.") }}
+                       </p>
+                       <p>
+                               {{ _("As this build platform only has a limited amount of performance, test builds only have a very less priority.") }}
+                               {{ _("However, you can manually request to run a test.") }}
+                       </p>
+                       <p>
+                               {{ _("The build job will be started when a build slot is available but not before the given time.") }}
+                       </p>
+
+                       {{ modules.BuildOffset() }}
+               </div>
+       </div>
 {% end block %}
diff --git a/data/templates/build-state.html b/data/templates/build-state.html
new file mode 100644 (file)
index 0000000..56766cb
--- /dev/null
@@ -0,0 +1,132 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Build") }}: {{ build.name }}{% end block %}
+
+{% block body %}
+       <h1>{{ _("Build") }}:
+               <a href="/package/{{ build.pkg.name }}">{{ build.pkg.name }}</a>-{{ build.pkg.friendly_version }}
+               {% if build.type == "scratch" %}
+                       <span>{{ _("Scratch build") }}</span>
+               {% end %}
+
+               {% if build.distro %}
+                       - <span>{{ _("Distribution") }}: {{ build.distro.name }}</span>
+               {% end %}
+       </h1>
+
+       <div class="pkg-summary">
+               {{ escape(build.pkg.summary) }}
+       </div>
+
+       <table class="form form2">
+               <tr>
+                       <td class="col1" colspan="2">
+                               {{ _("The state of a build can be either building, testing, stable, obsolete or broken.") }}
+                       </td>
+               </tr>
+       </table>
+       <div style="clear: both;">&nbsp;</div>
+
+       <!-- XXX add some more information about what every single state means -->
+
+       {% if current_user and build.has_perm(current_user) %}
+               {% if build.state in ("building", "testing", "stable") %}
+                       <form method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <table class="form form2">
+                                       <tr>
+                                               <td class="col1" colspan="2">
+                                                       <h2>{{ _("Mark build as obsolete") }}</h2>
+                                                       <p>
+                                                               {{ _("If a package is updated by an other package it should be marked as <em>obsolete</em>.") }}
+                                                               {{ _("For obsolete builds, there will be no test jobs created and it is recommended to remove them from the repositories soon.") }}
+                                                       </p>
+                                               </td>
+                                       </tr>
+
+                                       {% if build.repo %}
+                                               <tr>
+                                                       <td class="col1" colspan="2">
+                                                               <input type="checkbox" name="rem_from_repo" checked="checked" />
+                                                               {{ _("Remove build from the repository it is currently in?") }}
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+
+                                       <tr>
+                                               <td colspan="2" class="buttons">
+                                                       <input type="hidden" name="state" value="obsolete" />
+                                                       <input type="submit" value="{{ _("Mark build as obsolete") }}" />
+                                               </td>
+                                       </tr>
+                               </table>
+                       </form>
+               {% end %}
+
+               {% if build.state == "broken" %}
+                       <form method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <table class="form form2">
+                                       <tr>
+                                               <td class="col1" colspan="2">
+                                                       <h2>{{ _("Unbreak this build") }}</h2>
+
+                                                       <p>
+                                                               {{ _("In case this build has accidentially be marked as broken, it is possible to recover that state.") }}
+                                                       </p>
+                                               </td>
+                                       </tr>
+                                       <tr>
+                                               <td colspan="2" class="buttons">
+                                                       <input type="hidden" name="state" value="unbreak" />
+                                                       <input type="submit" value="{{ _("Unbreak this build") }}" />
+                                               </td>
+                                       </tr>
+                               </table>
+                       </form>
+               {% else %}
+                       <form method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <table class="form form2">
+                                       <tr>
+                                               <td class="col1" colspan="2">
+                                                       <h2>{{ _("Mark build as broken") }}</h2>
+
+                                                       <p>
+                                                               {{ _("If a package does not build or contains <em>serious</em> bugs, it should be marked as broken.") }}
+                                                               {{ _("Those builds can not be added into any repositories and are removed from all repositories they may currently be in.") }}
+                                                       </p>
+                                               </td>
+                                       </tr>
+                                       <tr>
+                                               <td colspan="2" class="buttons">
+                                                       <input type="hidden" name="state" value="broken" />
+                                                       <input type="submit" value="{{ _("Mark build as broken") }}" />
+                                               </td>
+                                       </tr>
+                               </table>
+                       </form>
+               {% end %}
+               <div style="clear: both;">&nbsp;</div>
+       {% end %}
+
+       {% if build.repo %}
+               <table class="form form2">
+                       <tr>
+                               <td class="col1" colspan="2">
+                                       {{ _("Current repository") }}:
+                                       <a href="/distro/{{ build.distro.identifier }}/repo/{{ build.repo.identifier }}">{{ build.repo.name }}</a>
+                                               {{ _("since %s") % locale.format_date(build.repo_time, relative=False) }}
+                               </td>
+                       </tr>
+               </table>
+       {% end %}
+{% end block %}
+
+{% block sidebar %}
+       <h1>{{ _("Actions") }}</h1>
+       <ul>
+               <li><a href="/build/{{ build.uuid }}">{{ _("Back to build") }}</a></li>
+       </ul>
+       <div style="clear: both;">&nbsp;</div>
+{% end %}
index 181258755e8c08a8f0a2853cd55a6adc65ac4dca..9a1952e845e81eaf510c86d05e5c807b7a026226 100644 (file)
@@ -3,14 +3,42 @@
 {% block title %}{{ _("Delete builder %s") % builder.name }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Builder") }}: {{ builder.name }}</h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/builders">{{ _("Builders") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/builder/{{ escape(builder.name) }}">{{ escape(builder.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/builder/{{ escape(builder.name) }}/delete">{{ _("Delete") }}</a>
+               </li>
+       </ul>
 
-       <p>
-               {{ _("You are going to delete the build host <strong>%s</strong>.") % builder.name }}
-       </p>
+       <div class="page-header">
+               <h1>{{ _("Builder") }}: {{ builder.name }}</h1>
+       </div>
 
-       <p>
-               <a href="/builder/delete/{{ builder.name }}?confirmed=1">{{ _("Delete %s") % builder.name }}</a>
-               <a href="/builder/{{ builder.name }}">{{ _("Back") }}</a>
-       </p>
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("You are going to delete the build host <strong>%s</strong>.") % builder.name }}
+                       </p>
+
+                       <div class="btn-toolbar">
+                               <div class="btn-group pull-right">
+                                       <a class="btn btn-danger" href="/builder/{{ escape(builder.name) }}/delete?confirmed=1">
+                                               {{ _("Delete %s") % builder.name }}
+                                       </a>
+                                       <a class="btn" href="/builder/{{ escape(builder.name) }}">{{ _("Cancel") }}</a>
+                               </div>
+                       </div>
+               </div>
+       </div>
 {% end block %}
index fa6594add06de11435f4dbabc8e8f2c6b410c8b6..66627f013d51c6bbf9300406d98cc8c35f3b1a88 100644 (file)
 {% block title %}{{ _("Builder") }}: {{ builder.name }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Builder") }}: {{ builder.name }}</h1>
-       <table class="form form2">
-               <!-- Status -->
-               <tr>
-                       <td class="col1">{{ _("Status") }}</td>
-                       <td class="col2">{{ builder.status }}</td>
-               </tr>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/builders">{{ _("Builders") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/builder/{{ escape(builder.name) }}">{{ escape(builder.name) }}</a>
+               </li>
+       </ul>
+
+       {% if builder.overload %}
+               <div class="alert alert-warning">
+                       <strong>{{ _("Warning") }}</strong>! {{ _("This builder is overloaded.") }}
+                       {{ _("That means it will take no additional jobs although it has not reached its threshold.") }}
+                       {{ _("If the load decreases new jobs will be added automatically.") }}
+               </div>
+       {% end %}
+
+       <div class="page-header">
+               <h1>{{ _("Builder") }}: {{ builder.name }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span4 offset1">
+                       <table class="table">
+                               <tbody>
+                                       <tr>
+                                               <td>{{ _("State") }}</td>
+                                               <td>
+                                                       {% if builder.status == "enabled" %}
+                                                               {{ _("Enabled") }}
+                                                       {% elif builder.status == "disabled" %}
+                                                               {{ _("Disabled") }}
+                                                       {% elif builder.status == "deleted" %}
+                                                               {{ _("Deleted") }}
+                                                       {% else %}
+                                                               {{ _("Unknown status: %s") % escape(builder.status) }}
+                                                       {% end %}
+                                               </td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Parallel builds") }}</td>
+                                               <td>{{ _("One job only.", "Up to %(num)s jobs.", builder.max_jobs) % { "num" : builder.max_jobs } }}</td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("This host builds") }}</td>
+                                               <td>
+                                                       <ul>
+                                                               {% for type in builder.build_types %}
+                                                                       <li>
+                                                                               {% if type == "release" %}
+                                                                                       {{ _("Release builds") }}
+                                                                               {% elif type == "scratch" %}
+                                                                                       {{ _("Scratch builds") }}
+                                                                               {% elif type == "test" %}
+                                                                                       {{ _("Test builds") }}
+                                                                               {% end %}
+                                                                       </li>
+                                                               {% end %}
+                                                       </ul>
+                                               </td>
+                                       </tr>
+                               </tbody>
+                       </table>
+
+                       {% if builder.description %}
+                               <h2>{{ _("Remarks") }}</h2>
+                               <p>
+                                       {{ modules.Text(builder.description) }}
+                               </p>
+                       {% end %}
+               </div>
+
+               <div class="span6">
+                       <table class="table">
+                               <tbody>
+                                       <tr>
+                                               <td>{{ _("Pakfire version") }}</td>
+                                               <td>
+                                                       {{ escape(builder.pakfire_version) or _("N/A") }}
+                                               </td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Supported architectures") }}</td>
+                                               <td>
+                                                       {{ locale.list([a.name for a in builder.get_arches() ]) }}
+
+                                                       {% if builder.disabled_arches %}
+                                                               ({{ _("disabled: %s") % locale.list([a.name for a in builder.disabled_arches]) }})
+                                                       {% end %}
+                                               </td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("CPU model") }}</td>
+                                               <td>
+                                                       {{ escape(builder.cpu_model) or _("Unknown") }}
+                                               </td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("CPU count") }}</td>
+                                               <td>{{ builder.cpu_count }}</td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Memory") }}</td>
+                                               <td>{{ format_size(builder.memory) }}</td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Load average") }}</td>
+                                               <td>
+                                                       {{ escape(builder.loadavg or _("N/A")) }}
+                                                       {% if builder.overload %}
+                                                                       <span class="label label-important">{{ _("Overload") }}</span>
+                                                       {% end %}
+                                               </td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Free disk space") }}</td>
+                                               <td>{{ format_size(builder.free_space * 1024**2) }}</td>
+                                       </tr>
 
-               <!-- Load AVG -->
-               <tr>
-                       <td class="col1">{{ _("Load average") }}</td>
-                       <td class="col2">
-                               {{ builder.loadavg }}
-                       </td>
-               </tr>
+                                       <tr>
+                                               <td>{{ _("Host key") }}</td>
+                                               <td>
+                                                       {{ builder.host_key_id or _("N/A") }}
+                                               </td>
+                                       </tr>                                   
+                               </tbody>
+                       </table>
+               </div>
+       </div>
 
-               <!-- Supported architectures -->
-               <tr>
-                       <td class="col1">{{ _("Supported architectures") }}</td>
-                       <td class="col2">
-                               {{ locale.list(builder.arches) or _("Unknown") }}
-                       </td>
-               </tr>
-       </table>
-       <div style="clear: both;">&nbsp;</div>
-       
-       <h2>{{ _("Configuration") }}</h2>
-       <table class="form form2">
-               <tr>
-                       <td class="col1">{{ _("Builds source packages") }}</td>
-                       <td class="col2">
-                               {% if builder.build_src %}{{ _("Yes") }}{% else %}{{ _("No") }}{% end %}
-                       </td>
-               </td>
-               <tr>
-                       <td class="col1">{{ _("Builds binary packages") }}</td>
-                       <td class="col2">
-                               {% if builder.build_bin %}{{ _("Yes") }}{% else %}{{ _("No") }}{% end %}
-                       </td>
-               </td>
-               <tr>
-                       <td class="col1">{{ _("Runs tests") }}</td>
-                       <td class="col2">
-                               {% if builder.build_test %}{{ _("Yes") }}{% else %}{{ _("No") }}{% end %}
-                       </td>
-               </td>
-               <tr>
-                       <td class="col1">{{ _("Parallel build jobs") }}</td>
-                       <td class="col2">{{ _("One job only.", "Up to %(num)s jobs.", builder.max_jobs) % { "num" : builder.max_jobs } }}</td>
-               </tr>
-       </table>
-       <div style="clear: both;">&nbsp;</div>
+       {% if current_user and current_user.has_perm("maintain_builders") %}
+               <div class="row">
+                       <div class="span10 offset1">
+                               <div class="btn-toolbar">
+                                       <div class="btn-group pull-right">
+                                               {% if builder.enabled %}
+                                                       <a class="btn btn-danger" href="/builder/{{ builder.name }}/disable">
+                                                               {{ _("Disable") }}
+                                                       </a>
+                                               {% else %}
+                                                       <a class="btn btn-success" href="/builder/{{ builder.name }}/enable">
+                                                               {{ _("Enable") }}
+                                                       </a>
+                                               {% end %}
 
-       <h2>{{ _("Host information") }}</h2>
-       <table class="form form2">
-               <tr>
-                       <td class="col1">{{ _("CPU model") }}</td>
-                       <td class="col2">{{ builder.cpu_model or _("Unknown") }}</td>
-               </tr>
-               <tr>
-                       <td class="col1">{{ _("Memory") }}</td>
-                       <td class="col2">{{ friendly_size(builder.memory) }}</td>
-               </tr>
-       </table>
-       <div style="clear: both;">&nbsp;</div>
+                                               <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+                                                       {{ _("Action") }}
+                                                       <span class="caret"></span>
+                                               </a>
+                                               <ul class="dropdown-menu">
+                                                       <li>
+                                                               <a href="/builder/{{ builder.name }}/edit">
+                                                                       <i class="icon-edit"></i>
+                                                                       {{ _("Edit settings") }}
+                                                               </a>
+                                                       </li>
 
-       {% if builder.active_builds %}
-               <h2>{{ _("Currently running builds on this host") }}</h2>
-               {{ modules.BuildTable(builder.active_builds) }}
+                                                       {% if not builder.enabled %}
+                                                               <li>
+                                                                       <a href="/builder/{{ builder.name }}/renew">
+                                                                               <i class="icon-refresh"></i>
+                                                                               {{ _("Renew passphrase") }}
+                                                                       </a>
+                                                               </li>
+                                                       {% end %}
+
+                                                       <li class="divider"></li>
+                                                       <li>
+                                                               <a href="/builder/{{ builder.name }}/delete">
+                                                                       <i class="icon-trash"></i>
+                                                                       {{ _("Delete builder") }}
+                                                               </a>
+                                                       </li>
+                                               </ul>
+                                       </div>
+                               </div>
+                       </div>
+               </div>
        {% end %}
-{% end block %}
 
-{% block sidebar %}
-       <h1>{{ _("Actions") }}</h1>
-       <ul>
-               <li>
-                       <a href="/builds?state=&builder={{ builder.id }}">{{ _("Show all build jobs") }}</a>
-               </li>
-               {% if current_user and current_user.is_admin() %}
-                       <li>
-                               <a href="/builder/edit/{{ builder.name }}">{{ _("Edit builder") }}</a>
-                       </li>
-                       {% if not builder.enabled %}
-                               <li>
-                                       <a href="/builder/renew/{{ builder.name }}">{{ _("Renew passphrase") }}</a>
-                               </li>
-                       {% end %}
-                       <li>
-                               <a href="/builder/delete/{{ builder.name }}">{{ _("Delete builder") }}</a>
-                       </li>
-               {% end %}
-       </ul>
+       {% if builder.get_active_jobs() %}
+               <div class="row">
+                       <div class="span10 offset1">
+                               <h2>{{ _("Currently running builds on this host") }}</h2>
+                               {{ modules.JobsList(builder.get_active_jobs()) }}
+                       </div>
+               </div>
+       {% end %}
+
+       <div class="row">
+               <div class="span10 offset1">
+                       <h2>{{ _("Log") }}</h2>
+                       {{ modules.Log(builder.get_history(limit=20)) }}
+               </div>
+       </div>
 {% end block %}
index 78b675b5d508cc00663b2a98bbc4e59c47f67a4c..36fe7839182cfab5db14d574deafc6f3c8a16bf7 100644 (file)
-{% extends "base.html" %}
+{% extends "base-form1.html" %}
 
-{% block title %}{{ _("Edit builder %s") % builder.hostname }}{% end block %}
+{% block title %}{{ _("Edit builder %s") % escape(builder.hostname) }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Edit builder %s") % builder.hostname }}</h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/builders">{{ _("Builders") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/builder/{{ escape(builder.name) }}">{{ escape(builder.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/builder/{{ escape(builder.name) }}/edit">{{ _("Manage") }}</a>
+               </li>
+       </ul>
 
-       <form method="post" action="">
-               {{ xsrf_form_html() }}
-               <table class="form form3">
-                       <tr>
-                               <td class="col1">{{ _("Name") }}</td>
-                               <td class="col2">
-                                       {{ builder.hostname }}
-                               </td>
-                               <td class="col3">
-                                       {{ _("The hostname cannot be changed.") }}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Enabled") }}</td>
-                               <td class="col2">
-                                       <input type="checkbox" name="enabled" {% if builder.enabled %}checked="checked"{% end %} />
-                               </td>
-                               <td class="col3">
-                                       {{ _("The builder must be enabled in order to process build jobs.") }}
-                               </td>
-                       </tr>
-               </table>
-               <div style="clear: both;">&nbsp;</div>
+       <div class="page-header">
+               <h1>{{ _("Manage builder:") }} {{ escape(builder.hostname) }}</h1>
+       </div>
 
-               <h2>{{ _("Build job settings") }}</h2>
-               <p>
-                       {{ _("These settings do only take effect if the builder is enabled.") }}
-               </p>
-               <table class="form form3">
-                       <tr>
-                               <td class="col1">{{ _("Authorized to build source packages") }}</td>
-                               <td class="col2">
-                                       <input type="checkbox" name="build_src" {%if builder.build_src %}checked="checked"{% end %} />
-                               </td>
-                               <td class="col3">
-                                       {{ _("Only a few build servers are allowed to build source packages.") }}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Authorized to build binary packages") }}</td>
-                               <td class="col2">
-                                       <input type="checkbox" name="build_bin" {%if builder.build_bin %}checked="checked"{% end %} />
-                               </td>
-                               <td class="col3">&nbsp;</td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Authorized to build test packages") }}</td>
-                               <td class="col2">
-                                       <input type="checkbox" name="build_test" {%if builder.build_test %}checked="checked"{% end %} />
-                               </td>
-                               <td class="col3">&nbsp;</td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Maximum number of parallel build jobs") }}</td>
-                               <td class="col2">
-                                       <select name="max_jobs">
-                                               {% for i in (1, 2, 3, 4, 5, 6, 7, 8,) %}
-                                                       <option value="{{ i }}" {% if i == builder.max_jobs %}selected="selected"{% end %}>{{ i }}</option>
-                                               {% end %}
-                                       </select>
-                               </td>
-                               <td class="col3">
-                                       {{ _("This is the number of build jobs that are started in parallel.") }}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td colspan="3" class="buttons">
-                                       <input type="submit" value="{{ _("Save") }}" />
-                               </td>
-                       </tr>
-               </table>
-       </form>
+       <div class="row">
+               <div class="span8">
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <fieldset>
+                                       <div class="control-group">
+                                               <label class="control-label">{{ _("Hostname") }}</label>
+                                               <div class="controls">
+                                                       <span class="input-xlarge uneditable-input">{{ escape(builder.hostname) }}</span>
+                                                       <p class="help-block">
+                                                               {{ _("The hostname cannot be changed.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="enabled">{{ _("Enabled") }}</label>
+                                               <div class="controls">
+                                                       <label class="checkbox">
+                                                               <input type="checkbox" id="enabled" name="enabled" {% if builder.enabled %}checked="checked"{% end %}>
+                                                               {{ _("The builder must be enabled in order to process build jobs.") }}
+                                                       </label>
+                                               </div>
+                                       </div>
+                               </fieldset>
+
+                               <fieldset>
+                                       <legend>{{ _("Build job settings") }}</legend>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="max_jobs">{{ _("Maximum number of parallel build jobs") }}</label>
+                                               <div class="controls">
+                                                       <select id="max_jobs" name="max_jobs">
+                                                               {% for i in range(1, (2 * builder.cpu_count) + 1) %}
+                                                                       <option value="{{ i }}" {% if i == builder.max_jobs %}selected="selected"{% end %}>{{ i }}</option>
+                                                               {% end %}
+                                                       </select>
+
+                                                       <p class="help-block">
+                                                               {{ _("This is the number of build jobs that are started in parallel.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <div class="controls">
+                                                       <label class="checkbox">
+                                                               <input type="checkbox" id="build_release" name="build_release" {%if builder.build_release %}checked="checked"{% end %}>
+                                                               {{ _("Authorized to build release builds.") }}
+                                                       </label>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <div class="controls">
+                                                       <label class="checkbox">
+                                                               <input type="checkbox" id="build_scratch" name="build_scratch" {%if builder.build_scratch %}checked="checked"{% end %}>
+                                                               {{ _("Authorized to build scratch builds.") }}
+                                                       </label>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <div class="controls">
+                                                       <label class="checkbox">
+                                                               <input type="checkbox" id="build_test" name="build_test" {%if builder.build_test %}checked="checked"{% end %}>
+                                                               {{ _("Authorized to build test builds.") }}
+                                                       </label>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="arches">{{ _("Enable host for these architectures") }}</label>
+                                               <div class="controls">
+                                                       <select multiple="multiple" id="arches" name="arches">
+                                                               {% for arch in builder.get_arches() %}
+                                                                       <option value="{{ arch.name }}" {% if arch in builder.arches %}selected="selected"{% end %}>{{ arch.name }}</option>
+                                                               {% end %}
+                                                       </select>
+
+                                                       <p class="help-block">
+                                                               {{ _("Select or deselect the architectures, this builder should build or not.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="form-actions">
+                                               <button type="submit" class="btn btn-primary">{{ _("Save changes") }}</button>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </div>
 {% end block %}
index 8211d0e3c281a3dead82e28aa70d0cb14739e9b8..4831e1b679885dfb248a7d286c839bcbae5d3945 100644 (file)
@@ -3,27 +3,97 @@
 {% block title %}{{ _("Build servers") }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Build servers") }}</h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/builders">{{ _("Builders") }}</a>
+               </li>
+       </ul>
 
-       <p>
-               {{ _("Builders are those, that do all the hard work.") }}
-               {{ _("Build jobs are scheduled to these hosts that they process and send back the result.") }}
-       </p>
+       <div class="page-header">
+               <h1>{{ _("Build servers") }}</h1>
+       </div>
 
-       <ul class="builders">
-               {% for builder in builders %}
-                       <li>
-                               <a class="builder {{ builder.status.lower() }}" href="/builder/{{ builder.name }}">{{ builder.name }}</a>
-                       </li>
-               {% end %}
-       </ul>
-{% end block %}
+       <div class="row">
+               <div class="span12">
+                       <p>
+                               {{ _("Builders are those, that do all the hard work.") }}
+                               {{ _("Build jobs are scheduled to these hosts that they process and send back the result.") }}
+                       </p>
+               </div>
+       </div>
 
-{% if current_user.is_admin() %}
-       {% block sidebar %}
-               <h1>{{ _("Actions") }}</h1>
-               <ul>
-                       <li><a href="/builder/new">{{ _("Create new builder") }}</a></li>
-               </ul>
-       {% end block %}
-{% end %}
+       <div class="row">
+               <div class="span6 offset3">
+                       <table class="table table-striped">
+                               <thead>
+                                       <tr>
+                                               <th>&nbsp;</th>
+                                               <th>{{ _("Hostname") }}</th>
+                                               <th>{{ _("Load") }}</th>
+                                               <th>{{ _("Running jobs") }}</th>
+                                       </tr>
+                               </thead>
+                               <tbody>
+                                       {% for builder in builders %}
+                                               <tr>
+                                                       <td>
+                                                               <img src="{{ static_url("images/icons/builder-%s.png" % builder.state.lower()) }}"
+                                                                       alt="{{ _("State %s") % builder.state }}" />
+                                                       </td>
+                                                       <td>
+                                                               <a href="/builder/{{ builder.name }}">{{ builder.name }}</a>
+                                                               {% if builder.overload %}
+                                                                               <span class="label label-important">{{ _("Overload") }}</span>
+                                                               {% end %}
+                                                               <br />
+                                                               {{ locale.list([a.name for a in builder.arches]) }}
+                                                       </td>
+                                                       <td>
+                                                               {{ escape(builder.load1 or _("N/A")) }}
+                                                       </td>
+                                                       <td>
+                                                               {{ len(builder.get_active_jobs()) }}/{{ builder.max_jobs }}
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+
+                                       <tr>
+                                               <td colspan="3">
+                                                       <div class="progress progress-info">
+                                                               <div class="bar" style="width: {{ load }}%;"></div>
+                                                       </div>
+                                               </td>
+                                               <td>
+                                                       {% if current_user and current_user.is_admin() %}
+                                                               <div class="btn-group pull-right">
+                                                                       <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+                                                                               {{ _("Action") }}
+                                                                               <span class="caret"></span>
+                                                                       </a>
+                                                                       <ul class="dropdown-menu">
+                                                                               <li>
+                                                                                       <a href="/builder/new">{{ _("Create new builder") }}</a>
+                                                                               </li>
+                                                                       </ul>
+                                                               </div>
+                                                       {% end %}
+                                               </td>
+                                       </tr>
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span12">
+                       {% if log %}
+                               <h2>{{ _("Log") }}</h2>
+                               {{ modules.Log(log) }}
+                       {% end %}
+               </div>
+       </div>
+{% end block %}
index e3f23b8d5a4e8f0b7ca620ac4b11a414d25b2f43..519c39349d93cc9b45a6cb8b49b7317ecfcdf46a 100644 (file)
@@ -3,25 +3,45 @@
 {% block title %}{{ _("Create new builder") }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Create a new builder") }}</h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/builders">{{ _("Builders") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/builder/new">{{ _("Create new builder") }}</a>
+               </li>
+       </ul>
 
-       <form method="post" action="">
-               {{ xsrf_form_html() }}
-               <table class="form form3">
-                       <tr>
-                               <td class="col1">{{ _("Name") }}</td>
-                               <td class="col2">
-                                       <input name="name" type="text" length="64" />
-                               </td>
-                               <td class="col3">
-                                       {{ _("Must be the canonical hostname of the machine.") }}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td colspan="3" class="buttons">
-                                       <input type="submit" value="{{ _("Save") }}" />
-                               </td>
-                       </tr>
-               </table>
-       </form>
+       <div class="page-header">
+               <h1>{{ _("Create a new builder") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <fieldset>
+                                       <div class="control-group">
+                                               <label class="control-label" for="name">{{ _("Hostname") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="name" name="name">
+
+                                                       <p class="help-block">
+                                                               {{ _("Enter the canonical hostname of the machine.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="form-actions">
+                                               <button type="submit" class="btn btn-primary">{{ _("Create new builder") }}</button>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </div>
 {% end block %}
index 5215876252cc0cf5b6f865fb8df6e1881dd3e0ba..5a44fb721372cd6330413e0409e906e3efb0151d 100644 (file)
@@ -1,22 +1,47 @@
 {% extends "base.html" %}
 
 {% block body %}
-       <h1>{{ _("Builder") }}: {{ builder.name }}</h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/builders">{{ _("Builders") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/builder/{{ escape(builder.name) }}">{{ escape(builder.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/builder/{{ escape(builder.name) }}/edit">{{ _("Manage") }}</a>
+               </li>
+       </ul>
 
-       <p>
-               {% if action == "new" %}
-                       {{ _("The new host <strong>%s</strong> has been successfully created.") % builder.name }}
-               {% elif action == "update" %}
-                       {{ _("The passphrase for <strong>%s</strong> has been regenerated.") % builder.name }}
-               {% end %}
+       <div class="page-header">
+               <h1>{{ _("Builder") }}: {{ builder.name }}</h1>
+       </div>
 
-               {{ _("For authorization to the Pakfire Master Server there is a passphrase required which must be configured to the host.") }}
-               {{ _("This passphrase is:") }}
-       </p>
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {% if action == "new" %}
+                                       {{ _("The new host <strong>%s</strong> has been successfully created.") % builder.name }}
+                               {% elif action == "update" %}
+                                       {{ _("The passphrase for <strong>%s</strong> has been regenerated.") % builder.name }}
+                               {% end %}
 
-       <p class="focus">{{ builder.passphrase }}</p>
+                               {{ _("For authorization to the Pakfire Master Server there is a passphrase required which must be configured to the host.") }}
+                       </p>
 
-       <p>
-               <a href="/builder/{{ builder.name }}">{{ _("Next") }}</a>
-       </p>
+                       <p>
+                               {{ _("This passphrase is:") }} <strong>{{ passphrase }}</strong>
+                       </p>
+
+                       <p class="pull-right">
+                               <a class="btn btn-primary" href="/builder/{{ builder.name }}">{{ _("Next") }}</a>
+                       </p>
+               </div>
+       </div>
 {% end block %}
diff --git a/data/templates/builds-watchers-add.html b/data/templates/builds-watchers-add.html
new file mode 100644 (file)
index 0000000..014acbe
--- /dev/null
@@ -0,0 +1,90 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Watch build %s") % build.name }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(build.pkg.name) }}">{{ escape(build.pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/build/{{ escape(build.uuid) }}">{{ escape(build.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/build/{{ build.uuid }}/watch">{{ _("Watch") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Watch build %s") % build.name }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span12">
+                       <p>
+                               {{ _("You may here add yourself to the list of watchers of this build.") }}
+                               {{ _("If you do so, you will receive messages about new comments and status updates.") }}
+                       </p>
+               </div>
+       </div>
+
+       {% if not current_user.is_admin() and current_user in watchers %}
+               <div class="alert alert-block">
+                       <h4 class="alert-heading">{{ _("Oops!") }}</h4>
+                       {{ _("You are already watching this build.") }}
+               </div>
+       {% end %}
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+
+                               <fieldset>
+                                       {% if current_user.is_admin() %}
+                                               <div class="control-group">
+                                                       <label class="control-label">{{ _("Choose user") }}</label>
+                                                       <div class="controls">
+                                                               <select name="user_id">
+                                                                       {% if not current_user in watchers %}
+                                                                               <option value="{{ current_user.id }}">{{ _("Myself") }}</option>
+                                                                               <option value="" disabled>--------</option>
+                                                                       {% end %}
+
+                                                                       {% for user in [u for u in users if not u in watchers] %}
+                                                                               <option value="{{ user.id }}">{{ user.realname }}</option>
+                                                                       {% end %}
+                                                               </select>
+                                                       
+                                                               <p class="help-block">
+                                                                       {{ _("Choose a user who should watch this build.") }}
+                                                               </p>
+                                                       </div>
+                                               </div>
+                                       {% else %}
+                                               <div class="control-group">
+                                                       <label class="control-label">{{ _("User") }}</label>
+                                                       <div class="controls">
+                                                               <span class="input-xlarge uneditable-input">{{ escape(current_user.realname) }}</span>
+                                                       </div>
+                                               </div>
+                                       {% end %}
+
+                                       <div class="form-actions">
+                                               <button type="submit" class="btn btn-primary">{{ _("Add watcher") }}</button>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/builds-watchers-list.html b/data/templates/builds-watchers-list.html
new file mode 100644 (file)
index 0000000..8f89ec9
--- /dev/null
@@ -0,0 +1,56 @@
+{% extends "base.html" %}
+
+<!-- XXX I THINK THIS IS UNUSED -->
+
+{% block title %}{{ _("Build") }}: {{ _("Watchers of %s") % build.name }}{% end block %}
+
+{% block body %}
+       <h1>{{ _("Build") }}: {{ _("Watchers of %s") % build.name }}</h1>
+
+       <p>
+               {{ _("This is a list of all users who watch this build.") }}
+               {{ _("If you write a comment or the status of the build is changed, they all will get a message.") }}
+       </p>
+
+       {% if current_user in watchers %}
+               <p>
+                       {{ _("You are already watching this build.") }}
+               </p>
+       {% elif build.owner and current_user == build.owner %}
+               <p>
+                       {{ _("You are the owner of this build. So you don't need to watch it.") }}
+               </p>
+       {% else %}
+               <p>
+                       <a href="/build/{{ build.uuid }}/watch">{{ _("Watch this build.") }}</a>
+               </p>
+       {% end %}
+
+       <table class="form form2">
+               <tr>
+                       <td class="col1">{{ _("Build") }}</td>
+                       <td class="col2">
+                               <a href="/build/{{ build.uuid }}">{{ build.name }}</a>
+                       </td>
+               </tr>
+       </table>
+       <div style="clear: both;">&nbsp;</div>
+
+       <h2>{{ _("List of all watchers") }}</h2>
+       <ul class="watchers">
+               {% for watcher in watchers %}
+                       <li class="user">
+                               <a class="{{ watcher.state }}" href="/user/{{ watcher.name }}">{{ watcher.realname }}</a>
+                       </li>
+               {% end %}
+       </ul>
+{% end block %}
+
+{% block sidebar %}
+       <h1>{{ _("Actions") }}</h1>
+       <ul>
+               <li>
+                       <a href="/build/{{ build.uuid }}">{{ _("Back to build") }}</a>
+               </li>
+       </ul>
+{% end %}
index 18ef0af297b15c27121083f26668c133b2341774..304dbe40da3f306037389654d80f97216f3ec0cf 100644 (file)
 {% extends "base.html" %}
 
+{% block title %}{{ _("Distribution") }}: {{ escape(distro.name) }}{% end block %}
+
 {% block body %}
-       <h1>{{ _("Distribution") }}: {{ distro.name }}</a></h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distros">{{ _("Distributions") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/distro/{{ distro.identifier }}">{{ escape(distro.name) }}</a>
+               </li>
+       </ul>
 
-       <p class="pkg-summary">{{ distro.description }}</p>
+       <div class="page-header">
+               <h1>
+                       {{ _("Distribution") }}: {{ escape(distro.name) }}
+                       <small>{{ escape(distro.slogan) }}</small>
+               </h1>
+       </div>
 
-       <p>
-               {{ _("This distribution is available for %s and maintained by %s.") % (locale.list(distro.arches), distro.vendor) }}
-       </p>
+       <div class="row">
+               <div class="span10 offset1">
+                       <blockquote>
+                               {{ escape(distro.description) }}
+                               <small>{{ escape(distro.vendor) }}</small>
+                       </blockquote>
 
-       <h2>{{ _("Binary repositories") }}</h2>
-       {{ modules.RepositoryTable(distro, distro.repositories) }}
+                       <p>
+                               {{ _("Supported architectures") }}:
+                               {{ locale.list([a.name for a in distro.arches]) or _("None") }}
+                       </p>
 
-       <h2>{{ _("Sources") }}</h2>
-       {{ modules.SourceTable(distro, distro.sources) }}
+                       <hr />
+               </div>
+       </div>
 
-       <h2>{{ _("Log") }}</h2>
-       {{ modules.LogTable(distro.log) }}
-{% end block %}
+       <div class="row">
+               <div class="span10 offset1">
+                       <h2>
+                               {{ _("Binary repositories") }}
+                               <small>({{ len(distro.repositories) }})</small>
+                       </h2>
+                       
+                       <blockquote>
+                               {{ _("A binary repository is a composition of packages that are considered stable, unstable or in testing state by the developers.") }}
+                               <br /><br />
+                               {{ _("Each repository can be enabled individually.") }}
+                               <a href="/documents/enable-repositories">{{ _("Learn how to use them.") }}</a>
+                       </blockquote>
 
-{% block sidebar %}
-       <h1>{{ _("Actions") }}</h1>
-       <ul>
-               <li><a href="/distribution/edit/{{ distro.sname }}">{{ _("Edit distribution") }}</a></li>
-               <li><a href="/distribution/delete/{{ distro.sname }}">{{ _("Delete distribution") }}</a></li>
-       </ul>
+                       {{ modules.RepositoryTable(distro, distro.repositories) }}
+
+                       <hr />
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span10 offset1">
+                       <h2>
+                               {{ _("Source repositories") }}
+                               <small>({{ len(distro.sources) }})</small>
+                       </h2>
+
+                       {{ modules.SourceTable(distro, distro.sources) }}
+               </div>
+       </div>
+
+       {% if current_user and current_user.is_admin() %}
+               <div class="row">
+                       <div class="span10 offset1">
+                               <div class="btn-group pull-right">
+                                       <a class="btn btn-danger dropdown-toggle" data-toggle="dropdown" href="#">
+                                               <i class="icon-star icon-white"></i>
+                                               {{ _("Action") }}
+                                               <span class="caret"></span>
+                                       </a>
+                                       <ul class="dropdown-menu">
+                                               <li>
+                                                       <a href="/distro/{{ distro.identifier }}/edit">
+                                                               {{ _("Edit distribution") }}
+                                                       </a>
+                                               </li>
+                                               <li>
+                                                       <a href="/distro/{{ distro.identifier }}/delete">
+                                                               {{ _("Delete distribution") }}
+                                                       </a>
+                                               </li>
+                                               <li class="divider"></li>
+                                               <li>
+                                                       <a href="/distro/{{ distro.identifier }}/repo/new">
+                                                               {{ _("New binary repository") }}
+                                                       </a>
+                                               </li>
+                                               <li>
+                                                       <a href="/distro/{{ distro.identifier }}/source/new">
+                                                               {{ _("New source repository") }}
+                                                       </a>
+                                               </li>
+                                       </ul>
+                               </div>
+                       </div>
+               </div>
+       {% end %}
 {% end block %}
index 4119f0d98616a419a8e96dfac626727c597dcde3..67d098b351e247d7a51d80637083283eed363ba3 100644 (file)
                                        {{ _("Cannot be changed.") }}
                                </td>
                        </tr>
+                       <tr>
+                               <td class="col1">{{ _("Tag") }}</td>
+                               <td class="col2">
+                                       <input type="text" name="tag" value="{{ distro.tag }}" />
+                               </td>
+                               <td class="col3">
+                                       {{ _("The tag is added to the package release.") }}
+                               </td>
+                       </tr>
                        <tr>
                                <td class="col1">{{ _("Vendor") }}</td>
                                <td class="col2">
                                        {{ _("From whom is the distribution from?") }}
                                </td>
                        </tr>
+                       <tr>
+                               <td class="col1">{{ _("Contact") }}</td>
+                               <td class="col2">
+                                       <input type="text" name="contact" value="{{ distro.contact or "" }}" />
+                               </td>
+                               <td>
+                                       {{ _("The email address from the vendor.") }}
+                               </td>
+                       </tr>
                        <tr>
                                <td class="col1">{{ _("Slogan") }}</td>
                                <td class="col2">
@@ -49,7 +67,7 @@
                                <td class="col2">
                                        <select name="arches" size="4" multiple>
                                                {% for arch in arches %}
-                                                       <option value="{{ arch }}" {% if arch in distro.arches %}selected="selected"{% end %}>{{ arch }}</option>
+                                                       <option value="{{ arch.id }}" {% if arch in distro.arches %}selected="selected"{% end %}>{{ escape(arch.name) }}</option>
                                                {% end %}
                                        </select>
                                </td>
index 9d53143fd14f1c337b5367d597d4be7ac6834e69..013c17ba673cc9af69e2374d284d059416a268f5 100644 (file)
@@ -3,19 +3,69 @@
 {% block title %}{{ _("Distributions") }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Distributions") }}</h1>
-       <ul class="distros">
-               {% for distro in distros %}
-                       <li>
-                               <a href="/distribution/{{ distro.sname }}">{{ distro.name }}</a> - {{ distro.slogan }}
-                       </li>
-               {% end %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/distros">{{ _("Distributions") }}</a>
+               </li>
        </ul>
-{% end block %}
 
-{% block sidebar %}
-       <h1>{{ _("Actions") }}</h1>
-       <ul>
-               <li><a href="">{{ _("Add distribution") }}</a></li>
-       </ul>
+       <div class="page-header">
+               <h1>
+                       {{ _("Distributions") }}
+                       <small>({{ len(distros) }})</small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span10 offset1">
+                       <p>
+                               {{ _("This is a list of all distributions, that are maintained in this build service.") }}
+                               {{ _("You may click on one of them and see more details or jump directly to one of the repositories.") }}
+                       </p>
+
+                       <table class="table">
+                               <thead>
+                                       <tr>
+                                               <th>{{ _("Distribution") }}</th>
+                                               <th>{{ _("Repositories") }}</th>
+                                       </tr>
+                               </thead>
+                               <tbody>
+                                       {% for distro in distros %}
+                                               <tr>
+                                                       <td>
+                                                               <a href="/distro/{{ distro.identifier }}">{{ escape(distro.name) }}</a>
+                                                               <br /><em>{{ escape(distro.slogan) }}</em>
+                                                       </td>
+                                                       <td>
+                                                               <ul class="unstyled">
+                                                                       {% for repo in distro.repositories %}
+                                                                               <li>
+                                                                                       <a href="/distro/{{ distro.identifier }}/repo/{{ repo.identifier }}">{{ escape(repo.name) }}</a>
+                                                                                       - {{ escape(repo.summary) }}
+                                                                               </li>
+                                                                       {% end %}
+                                                               </ul>
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+
+       {% if current_user and current_user.is_admin() %}
+               <div class="row">
+                       <div class="span10 offset1">
+                               <a class="btn btn-danger pull-right" href="/distro/new">
+                                       <i class="icon-star icon-white"></i>
+                                       {{ _("New distribution") }}
+                               </a>
+                       </div>
+               </div>
+       {% end %}
 {% end block %}
diff --git a/data/templates/distro-source-commit-detail.html b/data/templates/distro-source-commit-detail.html
new file mode 100644 (file)
index 0000000..18f19a8
--- /dev/null
@@ -0,0 +1,132 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Commit") }}: {{ commit.revision }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distros">{{ _("Distributions") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distro/{{ distro.identifier }}">{{ escape(distro.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}">
+                               {{ _("Source: %s") % escape(source.name) }}
+                       </a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}/{{ commit.revision }}">
+                               {{ commit.revision[:7] }}
+                       </a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Source") }}: {{ escape(source.name) }}
+                       <br />
+                       <small>
+                               {{ commit.revision[:7] }} - {{ escape(commit.subject) }}
+                       </small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span10 offset1">
+                       <table class="table">
+                               <tbody>
+                                       <tr>
+                                               <td>{{ _("Revision") }}</td>
+                                               <td>{{ escape(commit.revision) }}</td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Date") }}</td>
+                                               <td>{{ format_date(commit.date, full_format=True) }}</td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Author") }}</td>
+                                               <td>{{ format_email(commit.author) }}</td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Committer") }}</td>
+                                               <td>{{ format_email(commit.committer) }}</td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Subject") }}</td>
+                                               <td>{{ escape(commit.subject) }}</td>
+                                       </tr>
+
+                                       {% if commit.message %}
+                                               <tr>
+                                                       <td colspan="2">
+                                                               {{ modules.Text(commit.message, pre=True) }}</pre>
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+
+                                       <tr>
+                                               <td colspan="2">
+                                                       <div class="btn-toolbar pull-right">
+                                                               {% if source.gitweb %}
+                                                                       <div class="btn-group">
+                                                                               <a class="btn" href="{{ escape(source.gitweb) }};a=commitdiff;h={{ commit.revision }}" target="_blank">
+                                                                                       {{ _("Open in gitweb") }}
+                                                                               </a>
+                                                                       </div>
+                                                               {% end %}
+
+                                                               {% if current_user and current_user.is_admin() %}
+                                                                       <div class="btn-group">
+                                                                               <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+                                                                                       <i class="icon-star"></i>
+                                                                                       {{ _("Action") }}
+                                                                                       <span class="caret"></span>
+                                                                               </a>
+                                                                               <ul class="dropdown-menu">
+                                                                                       <li>
+                                                                                               <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}/{{ commit.revision }}/reset">
+                                                                                                       {{ _("Reset commit") }}
+                                                                                               </a>
+                                                                                       </li>
+                                                                               </ul>
+                                                                       </div>
+                                                               {% end %}
+                                                       </div>
+                                               </td>
+                                       </tr>
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span10 offset1">
+                       <h2>{{ _("Packages created from this commit") }}</h2>
+
+                       {% if commit.packages %}
+                               <table class="table table-striped">
+                                       {% for pkg in commit.packages %}
+                                               <tr>
+                                                       <td>
+                                                               <a href="/package/{{ pkg.uuid }}">{{ pkg.friendly_name }}</a>
+                                                       </td>
+                                                       <td>
+                                                               {{ escape(pkg.summary) }}
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+                               </table>
+                       {% else %}
+                               <p>{{ _("There were no packages created from this commit.") }}</p>
+                       {% end %}
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/distro-source-commit-reset.html b/data/templates/distro-source-commit-reset.html
new file mode 100644 (file)
index 0000000..1ffd61b
--- /dev/null
@@ -0,0 +1,78 @@
+{% extends "base.html" %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distros">{{ _("Distributions") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distro/{{ distro.identifier }}">{{ escape(distro.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}">
+                               {{ _("Source: %s") % escape(source.name) }}
+                       </a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}/{{ commit.revision }}">
+                               {{ commit.revision[:7] }}
+                       </a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Source") }}: {{ escape(source.name) }}
+                       <br />
+                       <small>
+                               {{ commit.revision[:7] }} - {{ escape(commit.subject) }}
+                       </small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <div class="alert alert-block alert-important">
+                               <h4 class="alert-heading">{{ _("Danger!") }}</h4>
+                               {{ _("This is a very dangerous action!") }}<br />
+                               {{ _("Don't do it, if you are not absolutely sure what you are doing.") }}
+                       </div>
+
+                       <p>
+                               {{ _("This commit will be reset. Which means all packages associated with it will be deleted, and the commit will be parsed again.") }}
+                               {{ _("This action may cause severe problems and may only be allowed when something went horribly wrong.") }}
+                       </p>
+
+                       <a class="btn btn-danger pull-right" href="?confirmed=1">
+                               {{ _("Reset commit") }}
+                       </a>
+               </div>
+       </div>
+
+       {% if commit.packages %}
+               <div class="row">
+                       <div class="span10 offset1">
+                               <h2>{{ _("These packages will be deleted") }}</h2>
+                               <table class="table table-striped">
+                                       {% for pkg in commit.packages %}
+                                               <tr>
+                                                       <td>
+                                                               <a href="/package/{{ pkg.uuid }}">{{ pkg.friendly_name }}</a>
+                                                       </td>
+                                                       <td>
+                                                               {{ escape(pkg.summary) }}
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+                               </table>
+                       </div>
+               </div>
+       {% end %}
+{% end block %}
diff --git a/data/templates/distro-source-commits.html b/data/templates/distro-source-commits.html
new file mode 100644 (file)
index 0000000..d2bce9c
--- /dev/null
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+
+{% block body %}
+       <h1>
+               {{ _("Distribution") }}: {{ escape(distro.name) }}
+               <span>- {{ _("Source") }}: {{ escape(source.name) }} - {{ _("Commits") }}</span>
+       </h1>
+
+       <p>
+               {{ _("Source repository") }}:
+               <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}">
+                       {{ escape(source.name) }}
+               </a>
+       </p>
+
+       {{ modules.CommitsTable(distro, source, commits) }}
+
+       <div class="links">
+               {% if offset %}
+                       <a href="?offset={{ offset - number }}">{{ _("Previous commits") }}</a>
+               {% end %}
+
+               {% if limit %}
+                       <a href="?offset={{ offset + number }}">{{ _("Next commits") }}</a>
+               {% end %}
+       </div>
+{% end block %}
diff --git a/data/templates/distro-source-detail.html b/data/templates/distro-source-detail.html
new file mode 100644 (file)
index 0000000..a0535c8
--- /dev/null
@@ -0,0 +1,67 @@
+{% extends "base.html" %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distros">{{ _("Distributions") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distro/{{ distro.identifier }}">{{ escape(distro.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}">
+                               {{ _("Source: %s") % escape(source.name) }}
+                       </a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Source") }}: {{ escape(source.name) }}
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span10 offset1">
+                       <table class="table">
+                               <tbody>
+                                       {% if source.gitweb %}
+                                               <tr>
+                                                       <td>{{ _("Gitweb") }}</td>
+                                                       <td>
+                                                               <a href="{{ escape(source.gitweb) }}" target="_blank">
+                                                                       {{ escape(source.gitweb) }}
+                                                               </a>
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+                                       <tr>
+                                               <td>{{ _("Branch") }}</td>
+                                               <td>{{ escape(source.branch) }}</td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Imported commits") }}</td>
+                                               <td>{{ source.num_commits }}</td>
+                                       </tr>
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span10 offset1">
+                       <h2>{{ _("Latest commits") }}</h2>
+                       {{ modules.CommitsTable(distro, source, commits) }}
+
+                       <a class="btn pull-right" href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}/commits">
+                               {{ _("Show all commits") }}
+                       </a>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/distro-update-detail.html b/data/templates/distro-update-detail.html
new file mode 100644 (file)
index 0000000..4c590ba
--- /dev/null
@@ -0,0 +1,52 @@
+{% extends "base.html" %}
+
+{% block body %}
+       <h1>
+               {{ _("Update") }}: {{ update.uuid }}
+               <span>- {{ _("Distribution") }}: {{ distro.name }}</span>
+       </h1>
+
+       <div class="update-description">
+               <strong>{{ _("Summary") }}: {{ escape(update.summary) }}</strong>
+
+               <pre>{{ escape(update.description) }}</pre>
+       </div>
+
+       <table class="form form2">
+               <tr>
+                       <td class="col1">{{ _("Current repository") }}</td>
+                       <td class="col2">
+                               <a href="/distro/{{ distro.identifier }}/repository/{{ repo.name }}">{{ repo.name }}</a>
+
+                               {% if current_user and current_user.is_admin() %}
+                                       <a href="#">Submit</a>
+                               {% end %}
+                       </td>
+               </tr>
+               <tr>
+                       <td class="col1">{{ _("Maintainer") }}</td>
+                       <td class="col2">
+                               <a href="/user/{{ user.name }}">{{ escape(user.realname) }}</a>
+                       </td>
+               </tr>
+               <tr>
+                       <td class="col1">{{ _("Time created") }}</td>
+                       <td class="col2">
+                               {{ locale.format_date(update.time_created, relative=False, full_format=True) }} UTC
+                       </td>
+               </tr>
+       </table>
+       <div style="clear: both;">&nbsp;</div>
+
+       <h2>{{ _("Builds in this update") }}</h2>
+       {{ modules.BuildTable(update.builds) }}
+{% end block %}
+
+{% block sidebar %}
+       <h1>{{ _("Actions") }}</h1>
+       <ul>
+               <li>
+                       <a href="/distro/{{ distro.identifier }}/update/{{ update.year }}/{{ update.num }}/edit">{{ _("Edit") }}</a>
+               </li>
+       </ul>
+{% end block %}
diff --git a/data/templates/distro-update-edit.html b/data/templates/distro-update-edit.html
new file mode 100644 (file)
index 0000000..9a7ce0b
--- /dev/null
@@ -0,0 +1,37 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Edit distribution %s") % distro.name }}{% end block %}
+
+{% block body %}
+       <h1>
+               {% if update %}
+                       {{ _("Edit update %s - %s") % (update.uuid, update.summary) }}
+               {% else %}
+                       {{ _("Create new update") }}
+               {% end %}
+               <span> - {{ _("Distribution") }}: {{ escape(distro.name) }}</span>
+       </h1>
+
+       <form method="post" action="">
+               {{ xsrf_form_html() }}
+
+               <table class="form form3">
+                       <tr>
+                               <td class="col1">{{ _("Builds") }}</td>
+                               <td class="col2" colspan="2">
+                                       <select name="builds" size="5" multiple>
+                                               {% for build in builds %}
+                                                       <option value="{{ build.uuid }}">{{ build.name }}</option>
+                                               {% end %}
+                                       </select>
+                               </td>
+                       </tr>
+                       <tr>
+                               <td colspan="3" class="buttons">
+                                       <input type="submit" value="{{ _("Create update") }}" />
+                               </td>
+                       </tr>
+               </table>
+               <div style="clear: both;">&nbsp;</div>
+       </form>
+{% end block %}
index f7a0dd6fcbfdfffc5dcad5118c316bf809de9049..02a24bb5f6b43ad40fbad3420500e77572af4baa 100644 (file)
@@ -1,5 +1,27 @@
 {% extends "base.html" %}
 
+{% block body %}
+       <div class="row">
+               <div class="span2">
+                       <div class="well" style="padding: 8px 0;">
+                               <ul class="nav nav-list">
+                                       <li class="active">
+                                               <a href="#">
+                                                       <i class="icon-home icon-white"></i>
+                                                       Home
+                                               </a>
+                                       </li>
+                                       <li><a href="#"><i class="icon-book"></i> Library</a></li>
+                                       <li><a href="#"><i class="icon-pencil"></i> Applications</a></li>
+                                       <li><a href="#"><i class="i"></i> Misc</a></li>
+                               </ul>
+                       </div>
+               </div>
+      </div>
+       </div>
+       {% block body %}TEST{% end block %}
+{% end %}
+
 {% block sidebar %}
        <ul>
                <li><a href="/documents">{{ _("All Documents") }}</a></li>
index 6cdfcaca95888e15e41a2a1976bf35d226546fce..63e29e73b2fb6a998583a0547cd5eaf2837d8662 100644 (file)
@@ -1,9 +1,26 @@
 {% extends "docs-base.html" %}
 
-{% block title %}{{ _("Legend of the build states") }}{% end block %}
+{% block title %}{{ _("Documentation") }} - {{ _("Legend of the build states") }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Legend of the build states") }}</h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/documents">{{ _("Documentation") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/documents/builds">{{ _("Builds") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Documentation") }}: {{ _("Legend of the build states") }}</h1>
+       </div>
+
        <p>
                {{ _("Every build that is done by the Pakfire Build Service has to go through several states:") }}
        </p>
index e2553ced53931f2fe354fb8f64c983b47d130315..a9afd24f513d8d6ff430f36eddbbc617a7b33b2b 100644 (file)
@@ -1,27 +1,79 @@
 {% extends "docs-base.html" %}
 
-{% block title %}{{ _("Legend of the build states") }}{% end block %}
+{% block title %}{{ _("Documentation index") }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Documents") }}</h1>
-       <p>
-               {{ _("This is a collection of documents that should be read by everybody who is using this system.") }}
-               {{ _("To make this easy for you, the documents are grouped into two parts.") }}
-       </p>
-
-       <h2>{{ _("Documents for testers") }}</h2>
-       <ul>
-               <li><a href="/documents/users">{{ _("Users") }}</a></li>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/documents">{{ _("Documentation") }}</a>
+               </li>
        </ul>
 
-       <h2>{{ _("Documents for developers") }}</h2>
-       <ul>
-               <li><a href="/documents/builds">{{ _("Builds") }}</a></li>
-               <li><a href="/documents/users">{{ _("Users") }}</a></li>
-       </ul>
+       <div class="page-header">
+               <h1>{{ _("Documents") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6">
+                       <p>
+                               {{ _("This is a collection of documents that should be read by everybody who is using this system.") }}
+                       </p>
+
+                       <ul>
+                               <li>
+                                       <a href="/documents/what-is-the-pakfire-build-service">
+                                               {{ _("What is the pakfire build service?") }}
+                                       </a>
+                               </li>
+                               <li>
+                                       <a href="http://wiki.ipfire.org/devel/pakfire/start" target="_blank">
+                                               {{ _("General pakfire documentation") }}
+                                       </a>
+                               </li>
+                               <li>
+                                       <a href="http://wiki.ipfire.org/devel/packaging_guidelines/start" target="_blank">
+                                               {{ _("Packaging guidelines") }}
+                                       </a>
+                               </li>
+                               <li>
+                                       <a href="/documents/builds">{{ _("Builds") }}</a>
+                               </li>
+                               <li>
+                                       <a href="/documents/users">{{ _("Users") }}</a>
+                               </li>
+                       </ul>
+
+                       <p>
+                               {{ _("This documentation you find at this place is not completed yet.") }}
+                               {{ _("Feel free to make any suggestions.") }}
+                       </p>
+               </div>
+
+               <div class="span6">
+                       <h2>{{ _("Contact") }}</h2>
+                       <p>
+                               {{ _("If you need help using the build service or have some general questions please use our mailing list.") }}
+                               {{ _("You can also talk to the developers and suggest feature enhancements.") }}
+                       </p>
+                       <p class="pull-right">
+                               <a href="http://lists.ipfire.org/mailman/listinfo/pakfire" target="_blank">
+                                       {{ _("Mailing list") }}
+                               </a>
+                       </p>
 
-       <p>
-               {{ _("Technical documentation is available on the wiki:") }}
-               <a href="http://redmine.ipfire.org/projects/pakfire3/wiki" target="_blank">{{ _("Technical documentation") }}</a>.
-       </p>
+                       <h2>{{ _("Bug reports") }}</h2>
+                       <p>
+                               {{ _("Please visit Bugzilla to create bug reports on the Pakfire Build System.") }}
+                       </p>
+                       <p class="pull-right">
+                               <a href="https://bugzilla.ipfire.org/describecomponents.cgi?product=Pakfire" target="_blank">
+                                       Bugzilla
+                               </a>
+                       </p>
+               </div>
+       </div>
 {% end %}
index 1fafd1ffbd6a09caab7a30b3e3ef9dabd828ac76..ee12cd4d47bd76663971383ff569d583d5671194 100644 (file)
@@ -1,8 +1,26 @@
 {% extends "docs-base.html" %}
 
-{% block title %}{{ _("Legend of the build states") }}{% end block %}
+{% block title %}{{ _("Documentation") }} - {{ _("User groups") }}{% end block %}
 
 {% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/documents">{{ _("Documentation") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/documents/users">{{ _("User groups") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Documentation") }}: {{ _("User groups") }}</h1>
+       </div>
+
        <h1>{{ _("Users") }}</h1>
        <p>
                {{ _("All users can join the Pakfire Build Service and are separated into three groups:") }}
@@ -13,7 +31,7 @@
                {{ _("Developers manage this build service and have access to all parts of it.") }}
                {{ _("They are responsible to keep the system running and able to push package updates to the repostories.") }}
        </p>
-       <p class="buttons">
+       <p class="pull-right">
                <a href="/documents/guidelines/developers">{{ _("Guidelines for developers") }}</a>
        </p>
 
@@ -23,7 +41,7 @@
                {{ _("Everyone can become a tester after he or she has proven to know the IPFire system very well.") }}
                {{ _("On these people depends a very huge amount of the quality of the distribution that is made out of the feedback they give.") }}
        </p>
-       <p class="buttons">
+       <p class="pull-right">
                <a href="/documents/guidelines/testers">{{ _("Guidelines for testers") }}</a>
        </p>
 
@@ -34,7 +52,7 @@
        </p>
 
        {% if not current_user %}
-               <p class="buttons">
+               <p class="pull-right">
                        <a href="/register">{{ _("Register") }}</a>
                </p>
        {% end %}
diff --git a/data/templates/docs-whatsthis.html b/data/templates/docs-whatsthis.html
new file mode 100644 (file)
index 0000000..354e33f
--- /dev/null
@@ -0,0 +1,63 @@
+{% extends "docs-base.html" %}
+
+{% block title %}{{ _("Documentation") }} - {{ _("Legend of the build states") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/documents">{{ _("Documentation") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/documents/what-is-the-pakfire-build-service">{{ _("What is the Pakfire Build Service?") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Documentation") }}: {{ _("What is the Pakfire Build Service?") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span8 offset2">
+                       <p>
+                               {{ _("On this page, you will find out what the Pakfire Build Service really is.") }}
+                               {{ _("Read carefully.") }}
+                       </p>
+
+                       <table class="table table-striped">
+                               <thead>
+                                       <tr>
+                                               <th>{{ _("Yeah, that's it!") }}</th>
+                                               <th>{{ _("No, that's not it!") }}</th>
+                                       </tr>
+                               </thead>
+
+                               <tbody>
+                                       <tr>
+                                               <td>
+                                                       <p>
+                                                               {{ _("PBS is a tool where people can give feedback to developers.") }}
+                                                               {{ _("It is possible to leave comments and rate builds.") }}
+                                                       </p>
+                                               </td>
+                                       
+                                               <td>
+                                                       <p>
+                                                               {{ _("PBS is <strong>NOT</strong> a bugtracker.") }}
+                                                               {{ _("To report bugs please use our bugtracker liked below if you want them to get fixed.") }}
+                                                       </p>
+
+                                                       <p>
+                                                               <a class="pull-right" href="http://bugtracker.ipfire.org" target="_blank">{{ _("Bugtracker") }}</a>
+                                                       </p>
+                                               </td>
+                                       </tr>
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+{% end %}
diff --git a/data/templates/error-403.html b/data/templates/error-403.html
new file mode 100644 (file)
index 0000000..9a55b55
--- /dev/null
@@ -0,0 +1,17 @@
+{% extends "error.html" %}
+
+{% block bigbox_headline %}
+       {{ _("Access forbidden") }}
+{% end block %}
+
+{% block bigbox_subtitle %}
+       <small>{{ _("You are not allowed to access this ressource.") }}</small>
+{% end block %}
+
+{% block explanation %}
+       <p>
+               {{ _("Access to the requested page has been denied because you do not have sufficient rights.") }}
+       </p>
+{% end block %}
+
+{% block message %}{% end block %}
index d9d75a333af54bb012ec11716aa6e39eebad9f34..c7a80fbacc22651e9768bc53c0a76440726f1c84 100644 (file)
@@ -1,5 +1,17 @@
 {% extends "error.html" %}
 
-{% block body %}
-       404
+{% block bigbox_headline %}
+       {{ _("404 - Not Found") }}
 {% end block %}
+
+{% block bigbox_subtitle %}
+       <small>{{ _("I could not find what you were searching for.") }}</small>
+{% end block %}
+
+{% block explanation %}
+       <p>
+               {{ _("You may have clicked an expired link or mistyped the address.") }}
+       </p>
+{% end block %}
+
+{% block message %}{% end block %}
diff --git a/data/templates/error-500.html b/data/templates/error-500.html
deleted file mode 100644 (file)
index 01f85d2..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-{% extends "error.html" %}
-
-{% block body %}
-       500
-
-       {% if exception %}
-               <pre>{{ exception }}</pre>
-       {% end %}
-{% end block %}
index 94d9808cc760156cb7ab46e326c0267f3406125e..82ad6f407eed530eb672f73c27f8ebaaaafd768a 100644 (file)
@@ -1 +1,55 @@
 {% extends "base.html" %}
+
+{% block body %}
+       <div class="hero-unit">
+               {% block bigbox %}
+                       <h1>
+                               {% block bigbox_headline %}
+                                       {{ _("Oops! Don't panic.") }}
+                               {% end block %}
+                               <br>
+
+                               {% block bigbox_subtitle %}
+                                       <small>{{ _("An unexpected error happened.") }}</small>
+                               {% end block %}
+                       </h1>
+
+                       {% block explanation %}
+                               <p>
+                                       {{ _("Stay calm and read the text below to find out what went wrong.") }}
+                               </p>
+                       {% end block %}
+               {% end block %}
+       </div>
+
+       {% block message %}
+               <div class="row">
+                       <div class="span12">
+                               <table class="table">
+                                       <tbody>
+                                               <tr>
+                                                       <td>{{ _("Error code") }}</td>
+                                                       <td>{{ code }} - {{ message }}</td>
+                                               </tr>
+
+                                               {% if current_user and current_user.is_admin() and exception %}
+                                                       <tr>
+                                                               <td colspan="2">
+                                                                       {{ _("Exception (traceback):") }}
+                                                                       <br><br>
+                                                                       <pre>{{ escape(exception) }}</pre>
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+                                       </tbody>
+                               </table>
+
+                               <hr>                            
+                               <p>
+                                       {{ _("Please try going back to the previous page and try the action you did again in a moment.") }}
+                                       {{ _("If the error persists, you should consider to get in touch with an administrator.") }}
+                               </p>
+                       </div>
+               </div>
+       {% end block %}
+{% end %}
index 07d46d486ef7e54f717e41b4a540e06f8091cf4e..f904eda3f738fd5bce8a23399d3c309ffd156448 100644 (file)
@@ -3,33 +3,89 @@
 {% block title %}{{ _("Welcome to the Pakfire Build Service") }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Welcome to the Pakfire Build Service") }}</h1>
-       <p>
-               {{ _("This is a service that organizes development and packaging for the IPFire distribution.") }}
-               {{ _("It is used to build and track packages as well as assembling images.") }}
-               <a href="/documents">{{ _("Learn more...") }}</a>
-       </p>
-
-       {% if active_builds %}
-               <h2>{{ _("Ongoing builds") }}</h2>
-               {{ modules.BuildTable(active_builds) }}
-       {% end %}
+       <div class="hero-unit">
+               <h1>
+                       {{ _("Pakfire Build Service") }}
+                       <small>{{ random_slogan() }}</small>
+               </h1>
+
+               {% if current_user %}
+                       <p>
+                               {{ _("Welcome %s! Great to see you again.") % current_user.firstname }}
+                       </p>
+               {% else %}
+                       <p>
+                               {{ _("This is a service that organizes development and packaging for the IPFire distribution.") }}
+                               <br />
+                               {{ _("It is used to build and track packages as well as assembling images.") }}
+                       </p>
+               {% end %}
+
+               <a class="btn btn-large pull-right" href="/documents">
+                       {{ _("Documentation") }}
+               </a>
+       </div>
+
+       {% if updates %}
+               <div class="row">
+                       <div class="span12">
+                               <div class="tabbable">
+                                       <ul class="nav nav-tabs">
+                                               {% for type, updts, active in updates %}
+                                                       <li {% if active %}class="active"{% end %}>
+                                                               <a href="#updates_{{ type }}" data-toggle="tab">
+                                                                       {% if type == "stable" %}
+                                                                               {{ _("Latest stable updates") }}
+                                                                       {% elif type == "unstable" %}
+                                                                               {{ _("Unstable updates") }}
+                                                                       {% elif type == "testing" %}
+                                                                               {{ _("Testing updates") }}
+                                                                       {% end %}
+                                                               </a>
+                                                       </li>
+                                               {% end %}
+                                       </ul>
 
-       {% if next_builds %}
-               <h2>{{ _("Queued builds") }}</h2>
-               {{ modules.BuildTable(next_builds) }}
+                                       <div class="tab-content">
+                                               {% for type, updts, active in updates %}
+                                                       <div class="tab-pane {% if active %}active{% end %}" id="updates_{{ type }}">
+                                                               {{ modules.UpdatesTable(updts) }}
+                                                       </div>
+                                               {% end %}
+                                       </div>
+                               </div>
+
+                               <p class="pull-right">
+                                       <a href="/updates">{{ _("View more updates...") }}</a>
+                               </p>
+
+                               <hr style="clear: both;">
+                       </div>
+               </div>
        {% end %}
 
-       <h2>{{ _("Lately updated builds") }}</h2>
-       {{ modules.BuildTable(latest_builds) }}
-
-       <h2>{{ _("Statistics") }}</h2>
-       <ul>
-               <li>
-                       {{ _("There is currently one pending build job.", "There are currently %(num)s pending build jobs.", counter_pending) % { "num" : counter_pending } }}
-               </li>
-               <li>
-                       {{ _("Average build time is %.1f minutes.") % (average_build_time / 60) }}
-               </li>
-       </ul>
+       <div class="row">
+               <div class="span6">
+                       {% if active_jobs %}
+                               <h3>
+                                       {{ _("Ongoing build jobs") }}
+                                       <small>({{ len(active_jobs) }})</small>
+                               </h3>
+                               {{ modules.JobsList(active_jobs, show_builder=True) }}
+                       {% end %}
+
+                       <a href="/builds/queue">
+                               {% if job_queue %}
+                                       {{ _("There is one job in the job queue.", "There are %(num)s jobs in the job queue.", job_queue) % { "num" : job_queue } }}
+                               {% else %}
+                                       {{ _("There are no jobs in the job queue.") }}
+                               {% end %}
+                       </a>
+               </div>
+
+               <div class="span6">
+                       <h3>{{ _("Lately processed jobs") }}</h3>
+                       {{ modules.JobsList(latest_jobs) }}
+               </div>
+       </div>
 {% end %}
diff --git a/data/templates/job-schedule-rebuild.html b/data/templates/job-schedule-rebuild.html
new file mode 100644 (file)
index 0000000..c5ea97a
--- /dev/null
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Schedule rebuild for %s") % job.name }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(build.pkg.name) }}">{{ escape(build.pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/build/{{ escape(build.uuid) }}">{{ escape(build.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/build/{{ build.uuid }}/schedule?type=rebuild">{{ _("Schedule rebuild") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Schedule rebuild for %s") % job.name }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("At this place, you can submit failed build jobs to be built again.") }}
+                       </p>
+                       <p>
+                               {{ _("The build job will be started when a build slot is available but not before the given time.") }}
+                       </p>
+
+                       {{ modules.BuildOffset() }}
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/job-schedule-test.html b/data/templates/job-schedule-test.html
new file mode 100644 (file)
index 0000000..d86d451
--- /dev/null
@@ -0,0 +1,50 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Schedule test build for %s") % job.name }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(build.pkg.name) }}">{{ escape(build.pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/build/{{ escape(build.uuid) }}">{{ escape(build.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/build/{{ build.uuid }}/schedule?type=test">{{ _("Schedule test build") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Schedule test build for %s") % build.name }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("A test build is used to check if a package builds with the current package set.") }}
+                               {{ _("In this way, developers are able to find quality issues fast and without actively searching for them.") }}
+                       </p>
+                       <p>
+                               {{ _("As this build platform only has a limited amount of performance, test builds only have a very less priority.") }}
+                               {{ _("However, you can manually request to run a test.") }}
+                       </p>
+                       <p>
+                               {{ _("The build job will be started when a build slot is available but not before the given time.") }}
+                               {{ _("Please note, that all other kinds of build are preferred over the test builds.") }}
+                       </p>
+
+                       {{ modules.BuildOffset() }}
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/jobs-abort.html b/data/templates/jobs-abort.html
new file mode 100644 (file)
index 0000000..124357e
--- /dev/null
@@ -0,0 +1,53 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Abort build job %s") % job.name }}{% end block %}
+
+{% block body %}
+       <h1>{{ _("Abort build job %s") % job.name }}</h1>
+       <p>
+               {{ _("You may abort a running build.") }}
+               {{ _("The build server will eventually stop to build the package.") }}
+       </p>
+
+       <form method="post" action="">
+               {{ xsrf_form_html() }}
+               <table class="form form3">
+                       <tr>
+                               <td class="col1">{{ _("Build job") }}</td>
+                               <td class="col2">
+                                       <a href="/job/{{ job.uuid }}">{{ job.name }}</a>
+                               </td>
+                               <td class="col3">
+                                       &nbsp;
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class="col1">{{ _("Start time") }}</td>
+                               <td class="col2">
+                                       {% if job.time_started %}
+                                               {{ locale.format_date(job.time_started, full_format=True) }}
+                                       {% else %}
+                                               {{ _("No started, yet.") }}
+                                       {% end %}
+                               </td>
+                               <td class="col3">
+                                       &nbsp;
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class="col1">{{ _("Build server") }}</td>
+                               <td class="col2">
+                                       <a href="/builder/{{ job.builder.name }}">{{ job.builder.name }}</a>
+                               </td>
+                               <td class="col3">
+                                       &nbsp;
+                               </td>
+                       </tr>
+                       <tr>
+                               <td colspan="3" class="buttons">
+                                       <input type="submit" value="{{ _("Abort build job") }}" />
+                               </td>
+                       </tr>
+               </table>
+       </form>
+{% end block %}
diff --git a/data/templates/jobs-buildroot.html b/data/templates/jobs-buildroot.html
new file mode 100644 (file)
index 0000000..3e8257c
--- /dev/null
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Job buildroot")}}: {{ job.name }}{% end block %}
+
+{% block body %}
+       <h1>{{ _("Job buildroot")}}: {{ job.name }}</h1>
+       <p>
+               {{ _("This is the buildroot of build job %s.") % job.name }}
+       </p>
+       <p>
+               {{ _("The packages listed below were used for the build.") }}
+       </p>
+
+       <ul class="buildroot">
+               {% for name, uuid, pkg in buildroot %}
+                       <li class="package">
+                               {% if pkg %}
+                                       <a href="/package/{{ pkg.uuid }}">{{ pkg.friendly_name }}</a>
+                               {% else %}
+                                       {{ name }} (<span>{{ uuid }}</span>)
+                               {% end %}
+                       </li>
+               {% end %}
+       </ul>
+
+{% end block %}
diff --git a/data/templates/jobs-detail.html b/data/templates/jobs-detail.html
new file mode 100644 (file)
index 0000000..67314fa
--- /dev/null
@@ -0,0 +1,219 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Job") }}: {{ job.name }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(build.pkg.name) }}">{{ escape(build.pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/build/{{ escape(build.uuid) }}">{{ escape(build.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/job/{{ job.uuid }}">{{ escape(job.arch.name) }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Build job") }}: {{ escape(job.name) }}<br />
+                       <small>{{ escape(job.pkg.summary) }}</small>
+               </h1>
+       </div>
+
+       {% if job.message %}
+               <div class="alert alert-block">
+                       {{ "<br />".join(job.message.splitlines()) }}
+               </div>
+       {% end %}
+
+       {% if job.state == "aborted" and job.aborted_state %}
+               <div class="alert alert-block alert-danger">
+                       <span>{{ _("Job has been aborted") }}</span>
+                       <p>
+                               {{ _("This build job is in an aborted state, because the build process crashed unexpectedly.") }}
+                               {{ _("In most cases, there is no log file and you must figure out the issue on your own.") }}
+                       </p>
+                       <p>
+                               {{ _("The error code is:") }}
+
+                               {% if job.aborted_state == -11 %}
+                                       SEGV - {{ _("Segmentation violation") }}
+                               {% else %}
+                                       {{ job.aborted_state }} - {{ _("Unknown") }}
+                               {% end %}
+                       </p>
+
+                       {% if current_user and current_user.is_admin() %}
+                               <p>
+                                       {{ _("You may resubmit the job to try again:") }}
+                                       <a href="/job/{{ job.uuid }}/schedule?type=rebuild">{{ _("Re-submit build") }}</a>
+                               </p>
+                       {% end %}
+               </div>
+       {% end %}
+
+       <div class="row">
+               <div class="span12">    
+                       <ul class="nav nav-pills">
+                               <li class="active">
+                                       <a href="#detail" data-toggle="tab">{{ _("Job details") }}</a>
+                               </li>
+                               <li>
+                                       <a href="#time" data-toggle="tab">{{ _("Time") }}</a>
+                               </li>
+
+                               {% if job.logfiles %}
+                                       <li>
+                                               <a href="#logs" data-toggle="tab">
+                                                       {{ _("Log files") }} ({{ len(job.logfiles) }})
+                                               </a>
+                                       </li>
+                               {% end %}
+                       </ul>
+
+                       <div class="tab-content">
+                               <div class="tab-pane active" id="detail">
+                                       <div class="row">
+                                               <div class="span4">
+                                                       <table class="table table-striped">
+                                                               <tbody>
+                                                                       <tr>
+                                                                               <td>{{ _("State") }}</td>
+                                                                               <td>
+                                                                                       {{ job.state }}
+
+                                                                                       {% if job.state in ("dispatching", "running", "uploading") %}
+                                                                                               <a class="btn btn-mini btn-inverse pull-right" href="/job/{{ job.uuid }}/abort">
+                                                                                                       {{ _("Abort job") }}
+                                                                                               </a>
+                                                                                       {% elif job.state in ("aborted", "failed") %}
+                                                                                               <a class="btn btn-mini btn-success pull-right" href="/job/{{ job.uuid }}/schedule?type=rebuild">
+                                                                                                       {{ _("Restart") }}
+                                                                                               </a>
+                                                                                       {% elif job.state == "finished" %}
+                                                                                               <a class="btn btn-mini pull-right" href="/job/{{ job.uuid }}/schedule?type=test">
+                                                                                                       {{ _("Schedule test build") }}
+                                                                                               </a>
+                                                                                       {% end %}
+                                                                               </td>
+                                                                       </tr>
+                                                                       <tr>
+                                                                               <td>{{ _("Builder") }}</td>
+                                                                               <td>
+                                                                                       {% if job.builder %}
+                                                                                               <a href="/builder/{{ job.builder.name }}">{{ job.builder.name }}</a>
+                                                                                       {% else %}
+                                                                                               {{ _("No host assigned, yet.") }}
+                                                                                       {% end %}
+                                                                               </td>
+                                                                       </tr>
+
+                                                                       {% if job.has_buildroot() %}
+                                                                               <tr>
+                                                                                       <td>{{ _("Buildroot") }}</td>
+                                                                                       <td>
+                                                                                               <a href="/job/{{ job.uuid }}/buildroot?tries={{ job.tries }}">
+                                                                                                       {{ _("%s package", "%s packages", job.has_buildroot()) % job.has_buildroot() }}
+                                                                                               </a>
+                                                                                       </td>
+                                                                               </tr>
+                                                                       {% end %}
+                                                               </tbody>
+                                                       </table>
+                                               </div>
+                                       </div>
+                               </div>
+
+                               <div class="tab-pane" id="time">
+                                       <div class="row">                               
+                                               <div class="span6">
+                                                       <table class="table">
+                                                               <tbody>
+                                                                       {% if job.duration %}
+                                                                               <tr>
+                                                                                       <td>{{ _("Duration") }}</td>
+                                                                                       <td>{{ friendly_time(job.duration) }}</td>
+                                                                               </tr>
+                                                                       {% end %}
+
+                                                                       <tr>
+                                                                               <td>{{ _("Job created") }}</td>
+                                                                               <td>{{ format_date(job.time_created, full_format=True) }}</td>
+                                                                       </tr>
+                                                                       <tr>
+                                                                               <td>{{ _("Job started") }}</td>
+                                                                               <td>
+                                                                                       {% if job.time_started %}
+                                                                                               {{ format_date(job.time_started, full_format=True) }}
+                                                                                       {% else %}
+                                                                                               {{ _("Not started, yet.") }}
+                                                                                       {% end %}
+                                                                               </td>
+                                                                       </tr>
+                                                                       <tr>
+                                                                               <td>{{ _("Job finished") }}</td>
+                                                                               <td>
+                                                                                       {% if job.time_finished %}
+                                                                                               {{ format_date(job.time_finished, full_format=True) }}
+                                                                                       {% else %}
+                                                                                               {{ _("Not finished, yet.") }}
+                                                                                       {% end %}
+                                                                               </td>
+                                                                       </tr>
+                                                               </tbody>
+                                                       </table>
+                                               </div>
+                                       </div>
+                               </div>
+
+                               {% if job.logfiles %}
+                                       <div class="tab-pane" id="logs">
+                                               <div class="row">
+                                                       <div class="span12">
+                                                               {{ modules.LogFilesTable(job, job.logfiles) }}
+                                                       </div>
+                                               </div>
+                                       </div>
+                               {% end %}
+                       </div>
+               </div>
+       </div>
+
+       {% if job.packages %}
+               <div class="row">
+                       <div class="span12">
+                               <hr>
+
+                               <h2>
+                                       {{ _("Package files") }}
+                                       <small>({{ len(job.packages) }})</small>
+                               </h2>
+
+                               {{ modules.PackagesTable(job, job.packages) }}
+                       </div>
+               </div>
+       {% end %}
+
+       {% if log %}
+               <div class="row">
+                       <div class="span12">
+                               <hr>
+
+                               <h2>{{ _("Log") }}</h2>
+                               {{ modules.Log(log) }}
+                       </div>
+               </div>
+       {% end %}
+{% end block %}
diff --git a/data/templates/keys-delete.html b/data/templates/keys-delete.html
new file mode 100644 (file)
index 0000000..c8a9f2e
--- /dev/null
@@ -0,0 +1,47 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Delete key %s") % escape(key.uids) }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/keys">{{ _("Key management") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/key/{{ key.fingerprint }}">{{ escape(key.uids) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/key/{{ key.fingerprint }}/delete">{{ _("Delete") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Key") }}: {{ escape(key.uids) }}
+                       <small>{{ key.fingerprint }}</small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("You are going to delete the key <strong>%s</strong>.") % escape(key.uids) }}
+                       </p>
+
+                       <div class="btn-toolbar">
+                               <div class="btn-group pull-right">
+                                       <a class="btn btn-danger" href="/key/{{ key.fingerprint }}/delete?confirmed=1">
+                                               {{ _("Delete key") }}
+                                       </a>
+                                       <a class="btn" href="/keys">{{ _("Cancel") }}</a>
+                               </div>
+                       </div>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/keys-import.html b/data/templates/keys-import.html
new file mode 100644 (file)
index 0000000..6318652
--- /dev/null
@@ -0,0 +1,47 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Import new key") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/keys">{{ _("Key management") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/key/import">{{ _("Import new key") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Import a new key") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <fieldset>
+                                       <div class="control-group">
+                                               <label class="control-label" for="data">{{ _("Key") }}</label>
+                                               <div class="controls">
+                                                       <textarea name="data" id="data" rows="7"></textarea>
+
+                                                       <p class="help-block">
+                                                               {{ _("Paste the key to import.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="form-actions">
+                                               <button type="submit" class="btn btn-primary">{{ _("Import new key") }}</button>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/keys-list.html b/data/templates/keys-list.html
new file mode 100644 (file)
index 0000000..126ca34
--- /dev/null
@@ -0,0 +1,120 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Import new key") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/keys">{{ _("Key management") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Key management") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span12">
+                       <p>
+                               {{ _("The keys are a very important component when it comes to security.") }}
+                               {{ _("Each package in the Pakfire Build Service is signed to prove its authenticity.") }}
+                       </p>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span12">
+                       <table class="table">
+                               <thead>
+                                       <tr>
+                                               <th colspan="2">&nbsp;</th>
+                                               <th>{{ _("Fingerprint") }}</th>
+                                               <th>{{ _("Created") }}</th>
+                                               <th>{{ _("Expires") }}</th>
+                                       </tr>
+                               </thead>
+
+                               <tbody>
+                                       {% for key in keys %}
+                                               <tr>
+                                                       <td colspan="4">
+                                                               <strong>
+                                                                       {% for uid in key.uids %}
+                                                                               {{ escape(uid) }}<br />
+                                                                       {% end %}
+                                                               </strong>
+                                                       </td>
+                                                       <td>
+                                                               <div class="btn-group">
+                                                                       <a class="btn btn-mini" href="http://pgp.mit.edu:11371/pks/lookup?op=vindex&search=0x{{ key.fingerprint }}" target="_blank">
+                                                                               <i class="icon-search"></i>
+                                                                               {{ _("Lookup") }}
+                                                                       </a>
+                                                                       <a class="btn btn-mini" href="/key/{{ key.fingerprint }}">
+                                                                               <i class="icon-download"></i>
+                                                                               {{ _("Download") }}
+                                                                       </a>
+                                                               </div>
+
+                                                               {% if current_user and current_user.has_perm("manage_keys") and key.can_be_deleted() %}
+                                                                       <br />
+                                                                       <div class="btn-group">
+                                                                               <a class="btn btn-mini btn-danger" href="/key/{{ key.fingerprint }}/delete">
+                                                                                       <i class="icon-trash icon-white"></i>
+                                                                                       {{ _("Remove") }}
+                                                                               </a>
+                                                                       </div>
+                                                               {% end %}
+                                                       </td>
+                                               </tr>
+
+                                               {% for subkey in key.subkeys %}
+                                                       <tr>
+                                                               <td>&nbsp;</td>
+                                                               <td>
+                                                                       {% if subkey.algo %}
+                                                                               {{ _("Subkey") }} ({{ subkey.algo }}):
+                                                                       {% else %}
+                                                                               {{ _("Subkey") }}:
+                                                                       {% end %}
+                                                               </td>
+                                                               <td>{{ subkey.fingerprint }}</td>
+                                                               <td>{{ format_date(subkey.time_created) }}</td>
+                                                               <td>
+                                                                       {% if subkey.time_expires %}
+                                                                               {% if subkey.expired %}
+                                                                                       <i class="icon-warning-sign"></i>
+                                                                               {% end %}
+
+                                                                               {{ format_date(subkey.time_expires, full_format=True) }}
+                                                                       {% else %}
+                                                                               {{ _("This key does not expire.") }}
+                                                                       {% end %}
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+
+                                               <tr>
+                                                       <td colspan="5">&nbsp;</td>
+                                               </tr>
+                                       {% end %}
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+
+       {% if current_user and current_user.has_perm("manage_keys") %}
+               <div class="row">
+                       <div class="span12">
+                               <a class="btn btn-danger pull-right" href="/key/import">
+                                       <i class="icon-star icon-white"></i>
+                                       {{ _("Import new key") }}
+                               </a>
+                       </div>
+               </div>
+       {% end %}
+{% end block %}
index be35756c71f6400fdda10857407372c77b7445ba..e214690f982c67d0c41e63fdb27beb264a305f5e 100644 (file)
@@ -1,16 +1,33 @@
 {% extends "base.html" %}
 
+{% block title %}{{ _("Login successful") }}{% end block %}
+
 {% block body %}
-       <h1>{{ _("Login successful") }}</h1>
-       <p>
-               {{ _("Welcome, %s.") % escape(user.realname) }}
-       </p>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       {{ _("Login successful") }}
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Login successful") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("Welcome, %s.") % escape(user.realname) }}
+                       </p>
 
-       <p>
-               {{ _("Your login to the Pakfire Build Server was successful.") }}
-       </p>
+                       <p>
+                               {{ _("Your login to the Pakfire Build Service was successful.") }}
+                       </p>
 
-       <p>
-               <a href="/">{{ _("Go on") }}</a>
-       </p>
+                       <a class="btn btn-primary pull-right" href="/">{{ _("Start!") }}</a>
+               </div>
+       </div>
 {% end %}
index 47e73b9b2750305f58d830648e405a62d9611d91..435bc0e3341d5d342de0124fa5e0f9e822a66568 100644 (file)
@@ -1,36 +1,50 @@
-{% extends "base.html" %}
+{% extends "base-form2.html" %}
+
+{% block title %}{{ _("Login") }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Login") }}</h1>
+       <div class="page-header">
+               <h1>{{ _("Login") }}</h1>
+       </div>
 
        {% if failed %}
-               <p class="important">
+               <div class="alert alert-block alert-error">
+                       <h4 class="alert-heading">{{ _("Login failed!") }}</h4>
                        {{ _("Username and/or password was wrong. Login failed.") }}
-               </p>
+               </div>
        {% end %}
 
-       <p>
-               {{ _("Please type your username and your password to the form to log in.") }}
-               {{ _("If you have no account, yet you can create a new one.") }}
-               <a href="/register">{{ _("Register a new account.") }}</a>
-       </p>
-
-       <form method="post" action="">
+       <form class="form-horizontal" method="POST" action="">
                {{ xsrf_form_html() }}
-               <table>
-                       <tr>
-                               <td>{{ _("Username") }}</td>
-                               <td><input name="name" type="text" /></td>
-                       </tr>
-                       <tr>
-                               <td>{{ _("Password") }}</td>
-                               <td><input name="pass" type="password" /></td>
-                       </tr>
-                       <tr>
-                               <td colspan="2" class="buttons">
-                                       <input type="submit" value="{{ _("Login") }}" />
-                               </td>
-                       </tr>
-               </table>
+
+               <fieldset>
+                       <div class="control-group">
+                               <label class="control-label" for="name">{{ _("Username") }}</label>
+                               <div class="controls">
+                                       <input type="text" class="input-xlarge" id="name" name="name" />
+                               </div>
+                       </div>
+                       <div class="control-group">
+                               <label class="control-label" for="pass">{{ _("Password") }}</label>
+                               <div class="controls">
+                                       <input type="password" class="input-xlarge" id="pass" name="pass" />
+                               </div>
+                       </div>
+                       <div class="form-actions">
+                               <button type="submit" class="btn btn-primary">{{ _("Login") }}</button>
+                       </div>
+               </fieldset>
        </form>
+
+       <hr>
+
+       <h3>{{ _("You also might want to...") }}</h3>
+       <ul>
+               <li>
+                       <a href="/register">{{ _("Register a new account.") }}</a>
+               </li>
+               <li>
+                       <a href="/password-recovery">{{ _("Recover your password.") }}</a>
+               </li>
+       </ul>
 {% end %}
index 7b8563b71cbc5560b661ca2bbcfd843e84c9c8ac..b7bb91aa6595679834ad32514fd1e76ad337a906 100644 (file)
@@ -1,14 +1,30 @@
 {% extends "base.html" %}
 
+{% block title %}{{ _("Logout successful") }}{% end block %}
+
 {% block body %}
-       <h1>{{ _("Logout successful") }}</h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       {{ _("Logout successful") }}
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Logout successful") }}</h1>
+       </div>
 
-       <p>
-               {{ _("You have successfully logged out from the Pakfire Build Server.") }}
-               {{ _("Have a nice day!") }}
-       </p>
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("You have successfully logged out from the Pakfire Build Server.") }}
+                               {{ _("Have a nice day!") }}
+                       </p>
 
-       <p>
-               <a href="/">{{ _("Go on") }}</a>
-       </p>
+                       <a class="btn btn-primary pull-right" href="/">{{ _("Goodbye!") }}</a>
+               </div>
+       </div>
 {% end %}
diff --git a/data/templates/mirrors-delete.html b/data/templates/mirrors-delete.html
new file mode 100644 (file)
index 0000000..8a471da
--- /dev/null
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Delete mirror %s") % escape(mirror.hostname) }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/mirrors">{{ _("Mirrors") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/mirror/{{ escape(mirror.hostname) }}">{{ escape(mirror.hostname) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/mirror/{{ escape(mirror.hostname) }}/delete">{{ _("Delete") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Mirror") }}: {{ escape(mirror.hostname) }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("You are going to delete the mirror <strong>%s</strong>.") % escape(mirror.hostname) }}
+                       </p>
+
+                       <div class="btn-toolbar">
+                               <div class="btn-group pull-right">
+                                       <a class="btn btn-danger" href="/mirror/{{ escape(mirror.hostname) }}/delete?confirmed=1">
+                                               {{ _("Delete %s") % escape(mirror.hostname) }}
+                                       </a>
+                                       <a class="btn" href="/mirror/{{ escape(mirror.hostname) }}">{{ _("Cancel") }}</a>
+                               </div>
+                       </div>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/mirrors-detail.html b/data/templates/mirrors-detail.html
new file mode 100644 (file)
index 0000000..99cca74
--- /dev/null
@@ -0,0 +1,120 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Mirror: %s") % escape(mirror.hostname) }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/mirrors">{{ _("Mirrors") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/mirror/{{ escape(mirror.hostname) }}">{{ escape(mirror.hostname) }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Mirror: %s") % escape(mirror.hostname) }}
+                       <small>{{ _("hosted by %s") % escape(mirror.owner) }}</small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span5">
+                       <table class="table">
+                               <tbody>
+                                       <tr>
+                                               <td>{{ _("Hostname") }}</td>
+                                               <td>{{ escape(mirror.hostname) }}</td>
+                                       </tr>
+
+                                       {% if current_user and current_user.has_perm("manage_mirrors") %}
+                                               <tr>
+                                                       <td>{{ _("Contact") }}</td>
+                                                       <td>
+                                                               {% if mirror.contact %}
+                                                                       <a href="mailto:{{ escape(mirror.contact) }}">{{ escape(mirror.contact) }}</a>
+                                                               {% else %}
+                                                                       {{ _("N/A") }}
+                                                               {% end %}
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+                               </tbody>
+                       </table>
+
+                       <h2>{{ _("Status information") }}</h2>
+                       <table class="table">
+                               <tbody>
+                                       <tr>
+                                               <td>{{ _("Status") }}</td>
+                                               <td>{{ mirror.status }}</td>
+                                       </tr>
+
+                                       <tr>
+                                               <td>{{ _("Last check") }}</td>
+                                               <td>
+                                                       {% if mirror.last_check %}
+                                                               {{ format_date(mirror.last_check) }}
+                                                       {% else %}
+                                                               {{ _("Never") }}
+                                                       {% end %}
+                                               </td>
+                                       </tr>
+                               </tbody>
+                       </table>
+
+                       {% if current_user and current_user.has_perm("manage_mirrors") %}
+                               <div class="btn-toolbar">
+                                       <div class="btn-group pull-right">
+                                               <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+                                                       {{ _("Action") }}
+                                                       <span class="caret"></span>
+                                               </a>
+                                               <ul class="dropdown-menu">
+                                                       <li>
+                                                               <a href="/mirror/{{ escape(mirror.hostname) }}/edit">
+                                                                       <i class="icon-edit"></i>
+                                                                       {{ _("Edit settings") }}
+                                                               </a>
+                                                       </li>
+
+                                                       <li class="divider"></li>
+                                                       <li>
+                                                               <a href="/mirror/{{ escape(mirror.hostname) }}/delete">
+                                                                       <i class="icon-trash"></i>
+                                                                       {{ _("Delete mirror") }}
+                                                               </a>
+                                                       </li>
+                                               </ul>
+                                       </div>
+                               </div>
+                       {% end %}
+               </div>
+
+               <div class="span7">
+                       <h2>{{ _("Map") }}</h2>
+
+                       {% if mirror.longitude and mirror.latitude %}
+                               <p>
+                                       {{ _("The location of the mirror server is estimated by the IP address.") }}
+                               </p>
+                               <iframe width="525" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"
+                                       src="http://www.openstreetmap.org/export/embed.html?bbox={{ mirror.longitude - 4 }},{{ mirror.latitude - 4 }},{{ mirror.longitude + 4 }},{{ mirror.latitude + 4 }}&amp;layer=mapquest&amp;marker={{ mirror.latitude }},{{ mirror.longitude }}" style="border: 1px solid black">
+                               </iframe>
+                               <p>
+                                       <a href="http://www.openstreetmap.org/?lat={{ mirror.latitude }}&amp;lon={{ mirror.longitude }}&amp;zoom=8&amp;layers=M&amp;mlat={{ mirror.latitude }}&amp;mlon={{ mirror.longitude }}" target="_blank">{{ _("View larger map") }}</a>
+                                       -
+                                       &copy; <a href="http://www.openstreetmap.org/" target="_blank">OpenStreetMap</a> contributors, CC-BY-SA
+                               </p>
+                       {% else %}
+                               {{ _("The location of the mirror server could not be estimated.") }}
+                       {% end %}
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/mirrors-edit.html b/data/templates/mirrors-edit.html
new file mode 100644 (file)
index 0000000..c6ad0fe
--- /dev/null
@@ -0,0 +1,90 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Edit mirror %s") % escape(mirror.hostname) }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/mirrors">{{ _("Mirrors") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/mirror/{{ escape(mirror.hostname) }}">{{ escape(mirror.hostname) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/mirror/{{ escape(mirror.hostname) }}/edit">{{ _("Manage") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Edit mirror: %s") % escape(mirror.hostname) }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <fieldset>
+                                       <div class="control-group">
+                                               <label class="control-label" for="name">{{ _("Hostname") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="name" name="name" value="{{ escape(mirror.hostname) }}">
+
+                                                       <p class="help-block">
+                                                               {{ _("The canonical hostname.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="enabled">{{ _("Enabled") }}</label>
+                                               <div class="controls">
+                                                       <label class="checkbox">
+                                                               <input type="checkbox" id="enabled" name="enabled" {% if mirror.enabled %}checked="checked"{% end %}>
+                                                               {{ _("Only enabled mirrors will be pushed out to the clients.") }}
+                                                       </label>
+                                               </div>
+                                       </div>
+                               </fieldset>
+
+                               <fieldset>
+                                       <legend>{{ _("Contact information") }}</legend>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="owner">{{ _("Owner") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="owner" name="owner" value="{{ escape(mirror.owner) }}">
+
+                                                       <p class="help-block">
+                                                               {{ _("The owner of the mirror server.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="contact">{{ _("Contact address") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="contact" name="contact" value="{{ escape(mirror.contact) }}">
+
+                                                       <p class="help-block">
+                                                               {{ _("An email address to contact an administrator of the mirror.") }}
+                                                               <br />
+                                                               <em>{{ _("This won't be made public.") }}</em>
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="form-actions">
+                                               <button type="submit" class="btn btn-primary">{{ _("Save changes") }}</button>
+                                               <a class="btn" href="/mirror/{{ escape(mirror.hostname) }}">{{ _("Cancel") }}</a>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/mirrors-list.html b/data/templates/mirrors-list.html
new file mode 100644 (file)
index 0000000..ed4d12a
--- /dev/null
@@ -0,0 +1,94 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Mirrors") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/mirrors">{{ _("Mirrors") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Mirrors") }}
+                       <small>({{ len(mirrors) }})</small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span12">
+                       <p>
+                               {{ _("On this page, you will see a list of all mirror servers.") }}
+                       </p>
+
+                       {% if mirrors %}
+                               <table class="table table-striped">
+                                       <thead>
+                                               <tr>
+                                                       <th>&nbsp;</th>
+                                                       <th>{{ _("Hostname") }}</th>
+                                                       <th>{{ _("Owner") }}</th>
+                                                       <th>{{ _("Status") }}</th>
+                                                       <th>{{ _("Last check") }}</th>
+                                               </tr>
+                                       </thead>
+                                       <tbody>
+                                               {% for mirror in mirrors %}
+                                                       <tr>
+                                                               <td>
+                                                                       [{{ escape(mirror.country_code) }}]
+                                                               </td>
+                                                               <td>
+                                                                       <a href="/mirror/{{ escape(mirror.hostname) }}">
+                                                                               {{ escape(mirror.hostname) }}
+                                                                       </a>
+                                                               </td>
+                                                               <td>
+                                                                       {{ escape(mirror.owner or _("N/A")) }}
+                                                               </td>
+                                                               <td>
+                                                                       {{ mirror.check_status }}
+                                                               </td>
+                                                               <td>
+                                                                       {% if mirror.last_check %}
+                                                                               {{ format_date(mirror.last_check, relative=True) }}
+                                                                       {% else %}
+                                                                               {{ _("Unknown") }}
+                                                                       {% end %}
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+                                       </tbody>
+                               </table>
+                       {% else %}
+                               <div class="alert alert-block">
+                                       <h4 class="alert-heading">{{ _("No mirrors") }}</h4>
+                                       {{ _("There are no mirrors configured, yet.") }}
+                               </div>
+                       {% end %}
+               </div>
+       </div>
+
+       {% if current_user and current_user.has_perm("manage_mirrors") %}
+               <div class="row">
+                       <div class="span12">
+                               <a class="btn pull-right" href="/mirror/new">
+                                       <i class="icon-star"></i>
+                                       {{ _("Add new mirror") }}
+                               </a>
+                       </div>
+               </div>
+       {% end %}
+
+       <div class="row">
+               <div class="span12">
+                       <h2>{{ _("Log") }}</h2>
+                       {{ modules.Log(log) }}
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/mirrors-new.html b/data/templates/mirrors-new.html
new file mode 100644 (file)
index 0000000..475b511
--- /dev/null
@@ -0,0 +1,60 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Create new mirror") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/mirrors">{{ _("Mirrors") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/mirror/new">{{ _("New mirror") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Create a new mirror") }}
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <fieldset>
+                                       <div class="control-group {% if hostname_missing %}error{% end %}">
+                                               <label class="control-label" for="name">{{ _("Hostname") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="name" name="name" value="{{ _hostname }}">
+
+                                                       <p class="help-block">
+                                                               {{ _("Enter the canonical hostname of the mirror.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group {% if path_invalid %}error{% end %}">
+                                               <label class="control-label" for="name">{{ _("Path") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="path" name="path" value="{{ path }}">
+
+                                                       <p class="help-block">
+                                                               {{ _("The path to the files on the server.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="form-actions">
+                                               <button type="submit" class="btn btn-primary">{{ _("Create new mirror") }}</button>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/modules/bugs-table.html b/data/templates/modules/bugs-table.html
new file mode 100644 (file)
index 0000000..e38a28c
--- /dev/null
@@ -0,0 +1,18 @@
+<table class="table table-striped">
+       {% for bug in bugs %}
+               <tr>
+                       <td>
+                               <a href="{{ bug.url }}" target="_blank">#{{ bug.id }}</a>
+                               <br />
+                               {{ escape(bug.status) }}
+                       </td>
+                       <td>
+                               {{ escape(bug.summary) }}
+
+                               {% if bug.assignee %}
+                                       <br />{{ format_email(bug.assignee) }}
+                               {% end %}
+                       </td>
+               </tr>
+       {% end %}
+</table>
diff --git a/data/templates/modules/build-headline.html b/data/templates/modules/build-headline.html
new file mode 100644 (file)
index 0000000..debc284
--- /dev/null
@@ -0,0 +1,30 @@
+<div class="page-header">
+       <div class="pull-right">
+               {% if pkg.critical_path %}
+                       <span class="label label-important pull-right">{{ _("Critical path") }}</span>
+               {% end %}
+
+               {% if not short %}      
+                       {% if build.type == "release" %}
+                               <span class="label label-success">{{ _("Release build") }}</span>
+                       {% elif build.type == "scratch" %}
+                               <span class="label label-important">{{ _("Scratch build") }}</span>
+                       {% end %}
+               {% end %}
+       </div>
+
+       <h1>
+               {% if prefix %}
+                       {{ prefix }}:
+               {% end %}
+
+               {% if shorter %}
+                       {{ escape(pkg.name) }}
+               {% else %}
+                       {{ escape(build.name) }}
+               {% end %}
+               <br>
+
+               <small>{{ escape(pkg.summary) }}</small>
+       </h1>
+</div>
index 7f8568f64a53f983bcc1e059b545fcf235b0b7f0..493b09725a81222aff11cfe378ad5da318c5c0d9 100644 (file)
@@ -1,25 +1,25 @@
-<form method="post" action="">
+<form class="form-horizontal" method="POST" action="">
        {{ xsrf_form_html() }}
-       <table class="form form3">
-               <tr>
-                       <td class="col1">{{ _("Start time") }}</td>
-                       <td class="col2">
-                               <select name="offset">
-                                       <option value="0"}>{{ _("As soon as possible") }}</option>
+       <fieldset>
+               <div class="control-group">
+                       <label class="control-label" for="offset">{{ _("Start time") }}</label>
+                       <div class="controls">
+                               <select name="offset" id="offset">
+                                       <option value="0">{{ _("As soon as possible") }}</option>
                                        <option value="300">{{ _("After 5 minutes") }}</option>
                                        <option value="900">{{ _("After 15 minutes") }}</option>
                                        <option value="3600">{{ _("After one hour") }}</option>
                                        <option value="86400">{{ _("After one day") }}</option>
                                </select>
-                       </td>
-                       <td class="col3">
+                       </div>
+
+                       <p class="help-block">
                                {{ _("Set the time after which the build job starts.") }}
-                       </td>
-               </tr>
-               <tr>
-                       <td colspan="3" class="buttons">
-                               <input type="submit" value="{{ _("Schedule build") }}" />
-                       </td>
-               </tr>
-       </table>
+                       </p>
+               </div>
+
+               <div class="form-actions">
+                       <button type="submit" class="btn btn-primary">{{ _("Schedule build") }}</button>
+               </div>
+       </fieldset>
 </form>
diff --git a/data/templates/modules/build-state-warnings.html b/data/templates/modules/build-state-warnings.html
new file mode 100644 (file)
index 0000000..44312e3
--- /dev/null
@@ -0,0 +1,19 @@
+{% if build.state == "broken" %}
+       <div class="alert alert-block alert-danger">
+               <h4 class="alert-heading">{{ _("This build is broken!") }}</h4>
+               <p>
+                       {{ _("This means that the package may cause severe damage on your system and/or does not work at all.") }}
+               </p>
+               <p>
+                       {{ _("It is discouraged to use this package anymore.") }}
+               </p>
+       </div>
+{% elif build.state == "obsolete" %}
+       <div class="alert alert-block alert-warning">
+               <h4 class="alert-heading">{{ _("This build is obsolete!") }}</h4>
+               <p>
+                       {{ _("This means that this package is not up to date anymore.") }}
+                       {{ _("Possibly there is an update that fixes bugs in this release.") }}
+               </p>
+       </div>
+{% end %}
index d1e936c00b2ea4ef8bb7d539045cd6f71dc5dd81..2610fbe934bdd8222c3bc46c6b92922c5508233b 100644 (file)
@@ -1,20 +1,88 @@
-<ul class="builds">
-       {% if builds %}
-               {% for build in builds %}
-                       <li>
-                               {% if build.type == "binary" %}
-                                       <a class="build {{ build.state }}"
-                                               href="/package/{{ build.pkg.name }}/{{ build.pkg.epoch }}/{{ build.pkg.version }}/{{ build.pkg.release }}"
-                                       >{{ build.pkg.friendly_name }}</a>.<a href="/build/{{ build.uuid }}">{{ build.arch }}</a>
-                               {% elif build.type == "source" %}
-                                       <a class="build {{ build.state }}" href="/build/{{ build.uuid }}">{{ build.name }}</a>
-                               {% else %}
-                                       {{ _("Unknown build type.") }}
+{% if builds %}
+       <table class="table table-striped">
+               <thead>
+                       <tr>
+                               <th>{{ _("Build") }}</th>
+                               <th>{{ _("Jobs") }}</th>
+                               {% if show_repo %}
+                                       <th>{{ _("Repository") }}</th>
                                {% end %}
-                       </li>
-               {% end %}
-       {% else %}
-               <li>There are no builds to display.</li>
-       {% end %}
-</ul>
-<div style="clear: both;">&nbsp;</div>
+                               {% if show_user %}
+                                       <th>{{ _("User") }}</th>
+                               {% end %}
+                               {% if show_when %}
+                                       <th>{{ _("When") }}</th>
+                               {% end %}
+                       </tr>
+               </thead>
+               <tbody>
+                       {% for build in builds %}
+                               <tr class="build build_state_{{ build.state }}">
+                                       <td class="name">
+                                               <a class="build {{ build.type }} {{ build.state }}"
+                                                       href="/build/{{ build.uuid }}">{{ build.name }}</a>
+                                       </td>
+
+                                       <td class="jobs">
+                                               {% if build.jobs %}
+                                                       {% for job in build.jobs %}
+                                                                       <a class="job {{ job.state }}" title="{{ job.state }}"
+                                                                               href="/job/{{ job.uuid }}">{{ job.arch.name }}</a>
+                                                       {% end %}
+                                               {% else %}
+                                                       {{ _("This build has got no jobs.") }}
+                                               {% end %}
+                                       </td>
+
+                                       {% if show_repo %}
+                                               <td>
+                                                       {% if build.repo %}
+                                                               <a href="/distro/{{ escape(build.distro.identifier) }}">{{ escape(build.distro.name) }}</a>
+                                                               /
+                                                               <a href="/distro/{{ escape(build.distro.identifier) }}/repo/{{ escape(build.repo.identifier) }}">{{ escape(build.repo.name) }}</a>
+                                                       {% else %}
+                                                               &nbsp;
+                                                       {% end %}
+                                               </td>
+                                       {% end %}
+
+                                       {% if show_user %}
+                                               {% if build.type == "scratch" and build.user %}
+                                                       <td class="user">
+                                                               {{ modules.Maintainer(build.user) }}
+                                                       </td>
+                                               {% elif build.type == "release" %}
+                                                       <td>{{ modules.Maintainer(build.pkg.maintainer) }}</td>
+                                               {% else %}
+                                                       <td>&nbsp;</td>
+                                               {% end %}
+                                       {% end %}
+
+                                       {% if show_when %}
+                                               <td>
+                                                       {{ format_date(build.created, relative=True) }}
+                                               </td>
+                                       {% end %}
+
+                                       {% if show_repo_time %}
+                                               <td class="time">
+                                                       {{ format_date(build.repo_time, relative=False) }}
+                                               </td>
+                                       {% end %}
+
+                                       {% if show_can_move_forward %}
+                                               <td class="can-move-forward">
+                                                       {{ build.can_move_forward }}
+                                               </td>
+                                       {% end %}
+                               </tr>
+                       {% end %}
+               </tbody>
+       </table>
+{% else %}
+       <div class="message message-warning">
+               <span>{{ _("No builds") }}</span>
+               {{ _("There are no builds to show at this place.") }}
+               {{ _("Possibly you need to adjust your search.") }}
+       </div>
+{% end %}
diff --git a/data/templates/modules/build-table_old.html b/data/templates/modules/build-table_old.html
new file mode 100644 (file)
index 0000000..d1e936c
--- /dev/null
@@ -0,0 +1,20 @@
+<ul class="builds">
+       {% if builds %}
+               {% for build in builds %}
+                       <li>
+                               {% if build.type == "binary" %}
+                                       <a class="build {{ build.state }}"
+                                               href="/package/{{ build.pkg.name }}/{{ build.pkg.epoch }}/{{ build.pkg.version }}/{{ build.pkg.release }}"
+                                       >{{ build.pkg.friendly_name }}</a>.<a href="/build/{{ build.uuid }}">{{ build.arch }}</a>
+                               {% elif build.type == "source" %}
+                                       <a class="build {{ build.state }}" href="/build/{{ build.uuid }}">{{ build.name }}</a>
+                               {% else %}
+                                       {{ _("Unknown build type.") }}
+                               {% end %}
+                       </li>
+               {% end %}
+       {% else %}
+               <li>There are no builds to display.</li>
+       {% end %}
+</ul>
+<div style="clear: both;">&nbsp;</div>
index 590769f08713a5fdb47d541733acff11e7a962e0..d96674c7be835e76516dce9983b849221e76a32f 100644 (file)
                                        {% if show_user %}
                                                <a href="/user/{{ comment.user.name }}">{{ _("by %s") % escape(comment.user.realname) }}</a> -
                                        {% end %}
-                                       {{ locale.format_date(comment.time) }}
+                                       {{ locale.format_date(comment.time_created) }}
+
+                                       {% if comment.time_updated %}
+                                               _("Updated") {{ locale.format_date(comment.time_updated) }}
+                                       {% end %}
                                </span>
                        </div>
                {% end %}
diff --git a/data/templates/modules/commits-table.html b/data/templates/modules/commits-table.html
new file mode 100644 (file)
index 0000000..46459fe
--- /dev/null
@@ -0,0 +1,36 @@
+<table class="table table-striped">
+       <thead>
+               <tr>
+                       <th>{{ _("Commit") }}</th>
+                       <th>{{ _("Author") }}</th>
+                       <th>{{ _("Subject") }}</th>
+               </tr>
+       </thead>
+       <tbody>
+               {% for commit in commits %}
+                       <tr>
+                               <td>
+                                       {% if commit.state == "pending" %}
+                                               <i class="icon-share-alt"></i>
+                                       {% elif commit.state == "running" %}
+                                               <i class="icon-cycle"></i>
+                                       {% elif commit.state == "failed" %}
+                                               <i class="icon-exclamation-mark"></i>
+                                       {% end %}
+
+                                       <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}/{{ commit.revision }}">
+                                               {{ commit.revision[:7] }}
+                                       </a>
+                                       <br />
+                                       {{ format_date(commit.date, full_format=True) }}
+                               </td>
+                               <td>
+                                       {{ format_email(commit.author) }}
+                               </td>
+                               <td>
+                                       {{ escape(commit.subject) }}
+                               </td>
+                       </tr>
+               {% end %}
+       </tbody>
+</table>
index 722b723f4c850b435e9e3b6d68672b55afb1c4ca..526dd711eaf0a6c7a5217395eda1aa9d6c5d3994 100644 (file)
@@ -11,4 +11,3 @@
                </tr>
        {% end %}
 </table>
-<div style="clear: both;">&nbsp;</div>
diff --git a/data/templates/modules/footer.html b/data/templates/modules/footer.html
new file mode 100644 (file)
index 0000000..6ba0420
--- /dev/null
@@ -0,0 +1,13 @@
+<hr />
+
+<footer class="footer">
+       <!-- <p>
+               {{ _("Pakfire is the buildsystem that is used to build the IPFire Linux firewall distribution.") }}
+               {{ _("It also installs and updates packages on the IPFire systems.") }}
+       </p> -->
+       <p>
+               &copy; {{ year }} - Pakfire Build Service {{ pakfire_version }} -
+               <a href="http://www.ipfire.org/" target="_blank">IPFire.org</a>.
+               {{ _("Code licensed under the GNU General Public License v3.") }}
+       </p>
+</footer>
diff --git a/data/templates/modules/jobs-list.html b/data/templates/modules/jobs-list.html
new file mode 100644 (file)
index 0000000..ebeb776
--- /dev/null
@@ -0,0 +1,44 @@
+{% if jobs %}
+       <table class="table table-striped">
+               {% for job in jobs %}
+                       <tr>
+                               <td>
+                                       {% if job.state == "dispatching" %}
+                                               <i class="icon-chevron-left"></i>
+                                       {% elif job.state == "running" %}
+                                               <i class="icon-retweet"></i>
+                                       {% elif job.state == "finished" %}
+                                               <i class="icon-ok"></i>
+                                       {% elif job.state == "dependency_error" %}
+                                               <i class="icon-random"></i>
+                                       {% elif job.state == "failed" %}
+                                               <i class="icon-remove"></i>
+                                       {% elif job.state == "uploading" %}
+                                               <i class="icon-chevron-right"></i>
+                                       {% elif job.state == "aborted" %}
+                                               <i class="icon-warning-sign"></i>
+                                       {% end %}
+
+                                       <a href="/build/{{ job.build.uuid }}">
+                                               {{ job.build.name }}</a>.<a href="/job/{{ job.uuid }}">{{ job.arch.name }}</a>
+
+                                       {% if job.build.type == "scratch" %}
+                                               <span class="label label-inverse">S</span>
+                                       {% elif job.type == "test" %}
+                                               <span class="label label-inverse">T</span>
+                                       {% end %}
+                               
+                                       {% if show_builder and job.builder %}
+                                               <br />
+                                               {{ _("Builder") }}:
+                                               <a href="/builder/{{ job.builder.name }}">
+                                                       {{ job.builder.name }}
+                                               </a>
+                                       {% end %}
+                               </td>
+                       </tr>
+               {% end %}
+       </table>
+{% else %}
+       {{ _("No jobs to display.") }}
+{% end %}
diff --git a/data/templates/modules/jobs-table.html b/data/templates/modules/jobs-table.html
new file mode 100644 (file)
index 0000000..614a965
--- /dev/null
@@ -0,0 +1,70 @@
+<table class="table table-striped">
+       <thead>
+               <tr>
+                       <th>{{ _("Arch") }}</th>
+                       <th>{{ _("State") }}</th>
+                       <th>{{ _("Host") }}</th>
+                       <th>{{ _("Duration") }}</th>
+               </tr>
+       </thead>
+       <tbody>
+               {% if jobs %}
+                       {% for job in jobs %}
+                               <tr>
+                                       <td>
+                                               <a href="/job/{{ job.uuid }}">{{ job.arch.name }}</a>
+                                       </td>
+                                       <td>
+                                               {% if job.state == "new" %}
+                                                       {{ _("New") }}
+                                               {% elif job.state == "pending" %}
+                                                       {{ _("Pending") }}
+                                               {% elif job.state == "failed" %}
+                                                       {{ _("Failed") }}
+                                               {% elif job.state == "dispatching" %}
+                                                       {{ _("Dispatching") }}
+                                               {% elif job.state == "finished" %}
+                                                       {{ _("Finished") }}
+                                               {% elif job.state == "running" %}
+                                                       {{ _("Running") }}
+                                               {% elif job.state == "aborted" %}
+                                                       {{ _("Aborted") }}
+                                               {% elif job.state == "dependency_error" %}
+                                                       {{ _("Dependency error") }}
+                                               {% else %}
+                                                       {{ job.state }}
+                                               {% end %}
+                                       </td>
+                                       <td>
+                                               {% if job.builder %}
+                                                       <a href="/builder/{{ job.builder.name }}">{{ job.builder.name }}</a>
+                                               {% else %}
+                                                       {{ _("N/A") }}
+                                               {% end %}
+                                       </td>
+                                       <td>
+                                               {% if job.state == "running" %}
+                                                       {{ _("Running since %s") % friendly_time(job.duration) }}
+                                               {% elif job.duration %}
+                                                       {{ friendly_time(job.duration) }}
+                                               {% else %}
+                                                       {{ _("Not finished, yet.") }}
+                                               {% end %}
+                                       </td>
+                               </tr>
+                       {% end %}
+
+                       {% if not build.supported_arches == "all" %}
+                               <tr>
+                                       <td colspan="4">
+                                               {{ _("This package only supports %s.") % locale.list(build.supported_arches.split()) }}
+                                       </td>
+                               </tr>
+                       {% end %}
+               {% else %}
+                       <tr>
+                               <td colspan="4">{{ _("No jobs, yet.") }}</td>
+                       </tr>
+               {% end %}
+       </tbody>
+</table>
diff --git a/data/templates/modules/log-entry-comment.html b/data/templates/modules/log-entry-comment.html
new file mode 100644 (file)
index 0000000..660c170
--- /dev/null
@@ -0,0 +1,19 @@
+{% extends "log-entry.html" %}
+
+{% block extra-title %}
+       {% if entry.vote == "up" %}
+               <span class="label label-success pull-right"
+                       title="{{ _("This build works for %s.") % entry.user.firstname }}">+{{ entry.credit }}</span>
+       {% elif entry.vote == "down" %}
+               <span class="label label-important pull-right"
+                       title="{{ _("This build does not work for %s.") % entry.user.firstname }}">-1</span>
+       {% end %}
+{% end %}
+
+{% block message %}
+       {% if entry.get_message(current_user) %}
+               {{ modules.Text(entry.get_message(current_user), pre=False) }}
+       {% else %}
+               <em>{{ _("No comment given.") }}</em>
+       {% end %}
+{% end %}
diff --git a/data/templates/modules/log-entry.html b/data/templates/modules/log-entry.html
new file mode 100644 (file)
index 0000000..e9ad66b
--- /dev/null
@@ -0,0 +1,40 @@
+<div class="well well-small">
+       {% block body %}
+               {% block title %}
+                       {% block extra-title %}{% end block %}
+                       <h4>
+                               {% if entry.system_msg %}
+                                       <i class="icon-star"></i>
+                               {% else %}
+                                       <i class="icon-comment"></i>
+                               {% end %}
+
+                               {% if entry.user %}
+                                       {% if current_user == entry.user %}
+                                               <a href="/profile">{{ _("You") }}</a>
+                                       {% else %}
+                                               <a href="/user/{{ escape(entry.user.name) }}">{{ escape(entry.user.realname) }}</a>
+                                       {% end %}
+                               {% else %}
+                                       {{ _("Pakfire Build Service") }}
+                               {% end %}
+                               -
+                               {{ format_date(entry.time) }}
+                       </h4>
+               {% end block %}
+
+               {% block message %}
+                       <p>
+                               {{ modules.Text(entry.get_message(current_user), pre=False) }}
+                       </p>
+               {% end block %}
+
+               {% block footer %}
+                       {% if entry.get_footer(current_user) %}
+                               <p>
+                                       {{ escape(entry.get_footer(current_user)) }}
+                               </p>
+                       {% end %}
+               {% end block %}
+       {% end block %}
+</div>
diff --git a/data/templates/modules/log-files-table.html b/data/templates/modules/log-files-table.html
new file mode 100644 (file)
index 0000000..46e74a4
--- /dev/null
@@ -0,0 +1,24 @@
+<table class="table table-striped">
+       <thead>
+               <tr>
+                       <th>{{ _("Filename") }}</th>
+                       <th>{{ _("Size") }}</th>
+                       <th>&nbsp;</th>
+               </tr>
+       </thead>
+
+       <tbody>
+               {% for file in files %}
+                       <tr>
+                               <td>{{ escape(file.name) }}</td>
+                               <td>{{ format_size(file.filesize) }}</td>
+                               <td>
+                                       <a class="btn btn-mini" href="{{ file.download_url }}">
+                                               <i class="icon-download"></i>
+                                               {{ _("Download") }}
+                                       </a>
+                               </td>
+                       </tr>
+               {% end %}
+       </tbody>
+</table>
diff --git a/data/templates/modules/log.html b/data/templates/modules/log.html
new file mode 100644 (file)
index 0000000..92d2245
--- /dev/null
@@ -0,0 +1,9 @@
+<div class="log">
+       {% for entry in entries %}
+               {% if entry.type == "comment" %}
+                       {{ modules.LogEntryComment(entry, **args) }}
+               {% else %}
+                       {{ modules.LogEntry(entry, **args) }}
+               {% end %}
+       {% end %}
+</div>
diff --git a/data/templates/modules/maintainer.html b/data/templates/modules/maintainer.html
new file mode 100644 (file)
index 0000000..6014987
--- /dev/null
@@ -0,0 +1,12 @@
+{% if type == "string" %}
+       {{ format_email(maintainer) }}
+{% elif type == "user" %}
+       <a href="/user/{{ escape(maintainer.name) }}">
+               {% if maintainer.is_admin() %}
+                       <i class="icon-star"></i>
+               {% else %}
+                       <i class="icon-user"></i>
+               {% end %}
+               {{ escape(maintainer.realname) }}
+       </a>
+{% end %}
diff --git a/data/templates/modules/modal-base.html b/data/templates/modules/modal-base.html
new file mode 100644 (file)
index 0000000..15de33b
--- /dev/null
@@ -0,0 +1,33 @@
+<div class="modal hide fade" id="{% block id %}{% end block %}">
+       <form class="modal-form form-horizontal" method="POST" action="{% block form_action %}{% end block %}">
+               {{ xsrf_form_html() }}
+
+               <div class="modal-header">
+                       {% block header %}
+                               <a class="close" data-dismiss="modal">&times;</a>
+                               <h3>
+                                       {% block title %}{% end block %}
+                               </h3>
+                       {% end block %}
+               </div>
+
+               <div class="modal-body">
+                       {% block body %}{% end block %}
+               </div>
+
+               <div class="modal-footer">
+                       {% block footer %}
+                               <p class="pull-left">
+                                       {% block footer_text %}{% end block %}
+                               </p>
+
+                               <button type="submit" class="btn btn-primary">
+                                       {% block submit_text %}{{ _("Submit") }}{% end block %}
+                               </button>
+                               <a class="btn" data-dismiss="modal">
+                                       {% block close_text %}{{ _("Cancel") }}{% end block %}
+                               </a>
+                       {% end block %}
+               </div>
+       </form>
+</div>
diff --git a/data/templates/modules/modal-build-comment.html b/data/templates/modules/modal-build-comment.html
new file mode 100644 (file)
index 0000000..e8094bd
--- /dev/null
@@ -0,0 +1,60 @@
+{% extends "modal-base.html" %}
+
+{% block id %}comment{% end block %}
+{% block form_action %}/build/{{ build.uuid }}/comment{% end block %}
+
+{% block title %}
+       {% if current_user %}
+               {{ _("Comment on %s") % build.name }}
+       {% else %}
+               {{ _("Log in to comment") }}
+       {% end %}
+{% end block %}
+
+{% block body %}
+       {% if current_user %}
+               <fieldset>
+                       <div class="control-group">
+                               <label class="control-label" for="cmttxt">{{ _("Comment") }}</label>
+                               <div class="controls">
+                                       <textarea class="input-xlarge" id="cmttxt" name="text" rows="8"></textarea>
+                               </div>
+                       </div>
+
+                       {% if current_user.has_perm("vote") %}
+                               <div class="control-group">
+                                       <label class="control-label">{{ _("Vote") }}</label>
+                                       <div class="controls">
+                                               <label class="radio">
+                                                       <input type="radio" name="vote" id="vote1" value="option1" checked>
+                                                       {{ _("Not tested.") }}
+                                               </label>
+                                               <label class="radio">
+                                                       <input type="radio" name="vote" id="vote2" value="up">
+                                                       {{ _("Works for me.") }}
+                                               </label>
+                                               <label class="radio">
+                                                       <input type="radio" name="vote" id="vote3" value="down">
+                                                       {{ _("Does not work.") }}
+                                               </label>
+                                       </div>
+                               </div>
+                       {% end %}
+               </fieldset>
+       {% else %}
+               <p>
+                       {{ _("You need to log in to comment.") }}
+                       {{ _("Click on the button below to do so.") }}
+               </p>
+       {% end %}
+{% end block %}
+
+{% block footer %}
+       {% if current_user %}
+               <button type="submit" class="btn btn-primary">{{ _("Submit comment") }}</button>
+       {% else %}
+               <a class="btn btn-primary" href="/login">{{ _("Login") }}</a>
+       {% end %}
+
+       <a class="btn" href="#" data-dismiss="modal">{{ _("Cancel") }}</a>
+{% end block %}
diff --git a/data/templates/modules/modal-build-push.html b/data/templates/modules/modal-build-push.html
new file mode 100644 (file)
index 0000000..7fb61b1
--- /dev/null
@@ -0,0 +1,60 @@
+{% extends "modal-base.html" %}
+
+{% block id %}push{% end block %}
+{% block form_action %}/build/{{ build.uuid }}/manage{% end block %}
+
+{% block title %}
+       {{ _("Push %s to a repository") % build.name }}
+{% end block %}
+
+{% block body %}
+       <input type="hidden" name="action" value="push" />
+
+       {% if not build.all_jobs_finished %}
+               <div class="alert alert-warning">
+                       <strong>{{ _("Not all jobs are finished!") }}</strong>
+                       {{ _("So it is <em>strongly</em> discouraged to push this build into the next repository.") }}
+               </div>
+
+               <hr>
+       {% end %}
+
+       <fieldset>
+               <div class="control-group">
+                       <label class="control-label">{{ _("New repository") }}</label>
+                       <div class="controls">
+                               <select id="repo" name="repo" {% if not current_user.is_admin() %}disabled{% end %}>
+                                       {% for repo in [r for r in build.distro.repositories if not r == current_repo] %}
+                                               <option value="{{ repo.identifier }}" {% if repo == next_repo %}selected{% end %}>
+                                                       {{ repo.name }} - {{ repo.summary }}
+                                               </option>
+                                       {% end %}
+                               </select>
+
+                               <p class="help-block">
+                                       {{ _("The build will be put into this repository.") }}
+                               </p>
+                       </div>
+               </div>
+       </fieldset>
+
+       <hr>
+
+       <p>
+               {{ _("You are going to push this build into a new repository.") }}
+               {{ _("This means that the build won't be part of the repository it is currently in anymore.") }}
+       </p>
+       <p>
+               {{ _("Please make sure you tested this build well enough that it will keep up with the quality level of the target repository.") }}
+       </p>
+{% end block %}
+
+{% block footer_text %}
+       {% if current_repo %}
+               {{ _("Current repository") }}:
+               <a href="/distro/{{ build.distro.identifier }}/repo/{{ current_repo.identifier }}">
+                       {{ current_repo.name }}
+               </a>
+       {% end %}
+{% end %}
+{% block submit_text %}{{ _("Push") }}{% end block %}
diff --git a/data/templates/modules/modal-build-unpush.html b/data/templates/modules/modal-build-unpush.html
new file mode 100644 (file)
index 0000000..eeacfb7
--- /dev/null
@@ -0,0 +1,38 @@
+{% extends "modal-base.html" %}
+
+{% block id %}unpush{% end block %}
+{% block form_action %}/build/{{ build.uuid }}/manage{% end block %}
+
+{% block title %}{{ _("Unpush %s from a repository") % build.name }}{% end block %}
+
+{% block body %}
+       <input type="hidden" name="action" value="unpush" />
+
+       <fieldset>
+               <div class="control-group">
+                       <label class="control-label">{{ _("Current repository") }}</label>
+                       <div class="controls">
+                               <select id="repo" name="repo" disabled>
+                                       <option value="{{ repo.identifier }}">
+                                               {{ repo.name }} - {{ repo.summary }}
+                                       </option>
+                               </select>
+                       </div>
+               </div>
+       </fieldset>
+
+       <hr>
+
+       <p>
+               {{ _("You are going to unpush this build from its repository.") }}
+               {{ _("This means that the build won't be installable from this repository anymore.") }}
+       </p>
+
+       {% if not build.state in ("obsolete", "broken") %}
+               <p>
+                       {{ _("If you consider this build being obsolete or broken, please don't forget to mark it so.") }}
+               </p>
+       {% end %}
+{% end block %}
+
+{% block submit_text %}{{ _("Unpush") }}{% end block %}
diff --git a/data/templates/modules/package-files-table.html b/data/templates/modules/package-files-table.html
deleted file mode 100644 (file)
index d8987a5..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<table class="file-list">
-       {% for file in files %}
-               <tr>
-                       <td>
-                               {{ file.name }}
-                       </td>
-                       <td>
-                               {{ friendly_size(file.size) }}
-                       </td>
-                       <td>
-                               {{ file.hash1 }}
-                       </td>
-               </tr>
-       {% end %}
-</table>
-<div style="clear: both;">&nbsp;</div>
diff --git a/data/templates/modules/package-header.html b/data/templates/modules/package-header.html
new file mode 100644 (file)
index 0000000..f64597f
--- /dev/null
@@ -0,0 +1,50 @@
+<div class="row">
+       <div class="span6">
+               <table class="table">
+                       <tbody>
+                               {% if pkg.groups %}
+                                       <tr>
+                                               <td>{{ _("Group", "Groups", len(pkg.groups)) }}</td>
+                                               <td>
+                                                       {{ locale.list(pkg.groups) }}
+                                               </td>
+                                       </tr>
+                               {% end %}
+
+                               {% if pkg.url %}
+                                       <tr>
+                                               <td>{{ _("Homepage") }}</td>
+                                               <td>
+                                                       {{ linkify(pkg.url, shorten=True, extra_params='target="_blank"', permitted_protocols=["http", "https", "ftp"]) }}
+                                               </td>
+                                       </tr>
+                               {% end %}
+
+                               {% if pkg.license %}
+                                       <tr>
+                                               <td>{{ _("License") }}</td>
+                                               <td>
+                                                       {{ escape(pkg.license) }}
+                                               </td>
+                                       </tr>
+                               {% end %}
+
+                               {% if pkg.maintainer %}
+                                       <tr>
+                                               <td>{{ _("Maintainer") }}</td>
+                                               <td>
+                                                       {{ modules.Maintainer(pkg.maintainer) }}
+                                               </td>
+                                       </tr>
+                               {% end %}
+                       </tbody>
+               </table>
+       </div>
+
+       <div class="span6">
+               <div class="well">
+                       <h3>{{ _("Description") }}</h3>
+                       {{ escape(pkg.description) }}
+               </div>
+       </div>
+</div>
diff --git a/data/templates/modules/package-table.html b/data/templates/modules/package-table.html
deleted file mode 100644 (file)
index d3e8dc3..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-
-<a name="{{ letter }}"></a>
-<h3>{{ letter.upper() }}</h3>
-
-<table class="package-list">
-       {% for pkg in packages %}
-               <tr>
-                       <td>
-                               <a href="/package/{{ pkg }}">{{ pkg }}</a>
-                       </td>
-               </tr>
-       {% end %}
-</table>
-<div style="clear: both;">&nbsp;</div>
diff --git a/data/templates/modules/packages-files-table.html b/data/templates/modules/packages-files-table.html
new file mode 100644 (file)
index 0000000..f2b6c88
--- /dev/null
@@ -0,0 +1,32 @@
+<table class="table table-striped">
+       <tbody>
+               {% for file in filelist %}
+                       <tr>
+                               <td>
+                                       {{ format_filemode(file.type, file.mode) }}
+                               </td>
+                               <td>
+                                       {{ escape(file.user) }}:{{ escape(file.group) }}
+                               </td>
+                               <td>
+                                       {% if file.size is None %}
+                                               -
+                                       {% else %}
+                                               {{ format_size(file.size) }}
+                                       {% end %}
+                               </td>
+                               <td>
+                                       {{ escape(file.name) }}
+                               </td>
+                               <td>
+                                       <div class="btn-toolbar">
+                                               <div class="btn-group">
+                                                       <a class="btn btn-mini" href="#">V</a>
+                                                       <a class="btn btn-mini" href="#">D</a>
+                                               </div>
+                                       </div>
+                               </td>
+                       </tr>
+               {% end %}
+       </tbody>
+</table>
diff --git a/data/templates/modules/packages-table.html b/data/templates/modules/packages-table.html
new file mode 100644 (file)
index 0000000..68320bc
--- /dev/null
@@ -0,0 +1,29 @@
+<table class="table table-striped">
+       <thead>
+               <tr>
+                       <th>{{ _("Name") }}</th>
+                       <th>{{ _("Version") }}</th>
+                       <th>{{ _("Arch") }}</th>
+                       <th>{{ _("Size") }}</th>
+                       <th>&nbsp;</th>
+               </tr>
+       </thead>
+       <tbody>
+               {% for package in packages %}
+                       <tr>
+                               <td>
+                                       <a href="/package/{{ package.uuid }}">{{ package.name }}</a>
+                               </td>
+                               <td>{{ package.friendly_version }}</td>
+                               <td>{{ package.arch.name }}</td>
+                               <td>{{ format_size(package.filesize) }}</td>
+                               <td>
+                                       <a class="btn btn-mini" href="{{ job.build.download_prefix }}/{{ package.path }}">
+                                               <i class="icon-download"></i>
+                                               {{ _("Download") }}
+                                       </a>
+                               </td>
+                       </tr>
+               {% end %}
+       </tbody>
+</table>
index 12416ab8e09b06bab4cdaa5d286b26f16c93421d..1f707228ab4104e5920c22d6ec9bb6c105bd9d53 100644 (file)
@@ -1,8 +1,29 @@
-<ul class="repositories">
-       {% for repo in repos %}
-               <li>
-                       <a href="/distribution/{{ distro.sname }}/repository/{{ repo.name }}">{{ repo.name }}</a>
-               </li>
-       {% end %}
-</ul>
-<div style="clear: both;">&nbsp;</div>
+<table class="table table-striped">
+       <thead>
+               <tr>
+                       <th>{{ _("Name") }}</th>
+                       <th>{{ _("No. of builds") }}</th>
+                       <th>{{ _("Enabled for builds") }}</th>
+               </tr>
+       </thead>
+       <tbody>
+               {% for repo in repos %}
+                       <tr>
+                               <td>
+                                       <a href="/distro/{{ distro.sname }}/repo/{{ repo.name }}">{{ repo.name }}</a>
+                                       <br />{{ repo.summary or _("N/A") }}
+                               </td>
+                               <td>
+                                       {{ repo.build_count() }}
+                               </td>
+                               <td>
+                                       {% if repo.enabled_for_builds %}
+                                               {{ _("Yes") }}
+                                       {% else %}
+                                               {{ _("No") }}
+                                       {% end %}
+                               </td>
+                       </tr>
+               {% end %}
+       </tbody>
+</table>
index fd73e10ff1b883221528965c022848e6dcad2647..23e61616584237323189b44db81e9a858a3b3698 100644 (file)
@@ -1,7 +1,28 @@
-<ul class="sources">
-       {% for source in sources %}
-               <li>
-                       <a href="/source/{{ source.id }}">{{ source.name }}</a>
-               </li>
-       {% end %}
-</ul>
+<table class="table table-striped">
+       <thead>
+               <tr>
+                       <th>{{ _("Name") }}</th>
+                       <th>{{ _("No. of commits") }}</th>
+                       <th>{{ _("Latest commit") }}</th>
+               </tr>
+       </thead>
+       <tbody>
+               {% for source in sources %}
+                       <tr>
+                               <td>
+                                       <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}">{{ escape(source.name) }}</a>
+                                       <br />{{ _("Branch: %s") % escape(source.branch) }}
+                               </td>
+                               <td>
+                                       {{ source.num_commits }}
+                               </td>
+                               <td>
+                                       <a href="/distro/{{ distro.identifier }}/source/{{ source.identifier }}/{{ source.head_revision.revision }}">
+                                               {{ source.head_revision.revision[:7] }}
+                                       </a> - {{ format_date(source.head_revision.date) }}
+                                       <br />{{ escape(source.head_revision.subject) }}
+                               </td>
+                       </tr>
+               {% end %}
+       </tbody>
+</table>
diff --git a/data/templates/modules/updates-table.html b/data/templates/modules/updates-table.html
new file mode 100644 (file)
index 0000000..39c449d
--- /dev/null
@@ -0,0 +1,24 @@
+<table class="table table-striped">
+       <tbody>
+               {% for update in updates %}
+                       <tr>
+                               <td>
+                                       {% if update.severity == "bugfix" %}
+                                               <i class="icon-ok-sign"></i>
+                                       {% elif update.severity == "enhancement" %}
+                                               <i class="icon-plus-sign"></i>
+                                       {% else %}
+                                               <i class="icon-question-sign"></i>
+                                       {% end %}
+                                       {{ update.build.update_id }}
+                                       -
+                                       <a href="/build/{{ update.build.uuid }}">{{ update.name }}</a>
+                               </td>
+                               <td>
+                                       {{ _("Score:") }} {{ update.score }} - 
+                                       {{ format_date(update.when) }}
+                               </td>
+                       </tr>
+               {% end %}
+       </tbody>
+</table>
diff --git a/data/templates/modules/watchers-sidebar-table.html b/data/templates/modules/watchers-sidebar-table.html
new file mode 100644 (file)
index 0000000..f1c5b23
--- /dev/null
@@ -0,0 +1,64 @@
+<p>
+       <strong>{{ _("Watchers") }}:</strong>
+
+       <a href="#watchers" data-toggle="modal">
+               {% if current_user and current_user in watchers %}
+                       {% if len(watchers) == 1 %}
+                               {{ _("You.") }}
+                       {% elif len(watchers) == 2 %}
+                               {{ _("You and one other.") }}
+                       {% else %}
+                               {{ _("You and %s others.") % (len(watchers) - 1) }}
+                       {% end %}
+               {% else %}
+                       {{ _("One person.", "%(num)s people.", len(watchers)) % { "num" : len(watchers) } }}
+               {% end %}
+       </a>
+</p>
+
+<div class="modal hide fade" id="watchers">
+       <div class="modal-header">
+               <a class="close" data-dismiss="modal">&times;</a>
+               <h3>{{ _("Watchers of %s") % escape(build.name) }}</h3>
+       </div>
+
+       <div class="modal-body">
+               <p>
+                       {{ _("All users who watch this build will be automatically notified about status changes and comments.") }}
+                       {{ _("This is an easy way of staying up to date.") }}
+               </p>
+               <hr />
+
+               {% if watchers %}
+                       <ul>
+                               {% if current_user and current_user in watchers %}
+                                       <li>
+                                               <strong>{{ _("You.") }}</strong>
+                                       </li>
+                               {% end %}
+
+                               {% for watcher in [w for w in watchers if current_user and not w == current_user] %}
+                                       <li>
+                                               <a href="/user/{{ watcher.name }}">{{ watcher.realname }}</a>
+                                       </li>
+                               {% end %}
+                       </ul>
+               {% else %}
+                       <p>
+                               {{ _("Nobody watches this build, yet. Be the first one.") }}
+                       </p>
+               {% end %}
+       </div>
+
+       <div class="modal-footer">
+               {% if current_user %}
+                       {% if not current_user in watchers %}
+                               <a class="btn" href="/build/{{ build.uuid }}/watch">{{ _("Watch this build") }}</a>
+                       {% elif current_user.is_admin() %}
+                               <a class="btn" href="/build/{{ build.uuid }}/watch">{{ _("Add a watcher") }}</a>
+                       {% end %}
+               {% end %}
+
+               <a class="btn btn-primary" href="#" data-dismiss="modal">{{ _("Close") }}</a>
+       </div>
+</div>
index f3ea1f355386857d357b1c8b0621e75e09f5f7ec..de9d25e8b4add0a8bb910f0907b56fb9ca23e856 100644 (file)
 {% block title %}{{ _("Package") }} {{ pkg.name }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Package") }}: {{ pkg.name }}</h1>
-
-       <p class="pkg-summary">
-               {{ pkg.summary }}
-       </p>
-       <p>
-               {{ _("There is one version of %(pkg)s.", "There are %(num)s different versions of %(pkg)s.", len(packages)) % { "num" : len(packages), "pkg" : pkg.name, } }}
-       </p>
-       {{ modules.PackageTable2(packages) }}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/package/{{ escape(pkg.name) }}">{{ escape(pkg.name) }}</a>
+               </li>
+       </ul>
+
+       {{ modules.BuildHeadline(_("Package"), latest_build, shorter=True) }}
+       {{ modules.PackageHeader(pkg) }}
+
+       <div class="row">
+               <div class="span12">
+                       <hr />
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span12">
+                       <ul class="nav nav-pills">
+                               {% if release_builds %}
+                                       <li class="active">
+                                               <a href="#release" data-toggle="tab">
+                                                       {{ _("Release builds") }}
+                                                       <small>({{ len(release_builds) }})</small>
+                                               </a>
+                                       </li>
+                               {% end %}
+
+                               {% if scratch_builds %}
+                                       <li {% if not release_builds %}class="active"{% end %}>
+                                               <a href="#scratch" data-toggle="tab">
+                                                       {{ _("Scratch builds") }}
+                                                       <small>({{ len(scratch_builds) }})</small>
+                                               </a>
+                                       </li>
+                               {% end %}
+                       </ul>
+
+                       <div class="tab-content">
+                               {% if release_builds %}
+                                       <div class="tab-pane active" id="release">
+                                               <div class="row">
+                                                       <div class="span12">
+                                                               {{ modules.BuildTable(release_builds, show_repo=True) }}
+                                                       </div>
+                                               </div>
+                                       </div>
+                               {% end %}
+
+                               {% if scratch_builds %}
+                                       <div class="tab-pane {% if not release_builds %}active{% end %}" id="scratch">
+                                               <div class="row">
+                                                       <div class="span12">
+                                                               {{ modules.BuildTable(scratch_builds, show_user=True) }}
+                                                       </div>
+                                               </div>
+                                       </div>
+                               {% end %}
+                       </div>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span12">
+                       <hr />
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span8">
+                       <h3>{{ _("Open bugs") }}</h3>
+                       {% if bugs %}
+                               {{ modules.BugsTable(pkg, bugs) }}
+                       {% else %}
+                               <blockquote>
+                                       {{ _("There are currently no open bugs for <em>%s</em>.") % escape(pkg.name) }}
+                               </blockquote>
+                       {% end %}
+
+                       <div class="btn-toolbar pull-right">
+                               <div class="btn-group">
+                                       <a class="btn" href="{{ bugtracker.enter_url(pkg.name) }}" target="_blank">
+                                               {{ _("File a new bug") }}
+                                       </a>
+                                       <a class="btn" href="{{ bugtracker.buglist_url(pkg.name) }}" target="_blank">
+                                               {{ _("Show all bugs") }}
+                                       </a>
+                               </div>
+                       </div>
+               </div>
+
+               <div class="span4">
+                       {% if build_times %}
+                               <h3>{{ _("Build times") }}</h3>
+
+                               <table class="table table-striped">
+                                       <tbody>
+                                               {% for arch, build_time in build_times %}
+                                                       <tr>
+                                                               <td class="arch">{{ arch.name }}</td>
+                                                               <td class="time">{{ friendly_time(build_time) }}</td>
+                                                       </tr>
+                                               {% end %}
+                                       </tbody>
+                               </table>
+
+                               <p>
+                                       {{ _("These are the average build times of this package for every architecture.") }}
+                               </p>
+                       {% end %}
+               </div>
+       </div>
 {% end block %}
index e218bd96993f893229f8a33dcfbef22db6f7bf9d..34c4a559f612d3ee90adf66f4df4d165cd8894c8 100644 (file)
 {% extends "base.html" %}
 
-{% block title %}{{ _("Package") }}: {{ pkg.friendly_name }}{% end block %}
+{% block title %}{{ _("Package") }}: {{ escape(pkg.name) }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Package") }}: <a href="/package/{{ pkg.name }}">{{ pkg.name }}</a>-{{ pkg.friendly_version }}</h1>
-
-       <p class="pkg-summary">{{ pkg.description }}</p>
-
-       <table class="form form2">
-               <tr>
-                       <td class="col1">{{ _("URL") }}</td>
-                       <td class="col2">
-                               <a href="{{ pkg.url }}" target="_blank">{{ pkg.url }}</a>
-                       </td>
-               </tr>
-               <tr>
-                       <td class="col1">{{ _("License") }}</td>
-                       <td class="col2">{{ pkg.license }}</td>
-               </tr>
-
-               {% if pkg.maintainer %}
-                       <tr>
-                               <td class="col1">{{ _("Maintainer") }}</td>
-                               <td class="col2">{{ escape(pkg.maintainer) }}</td>
-                       </tr>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(pkg.name) }}">{{ escape(pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               {% if pkg.build %}
+                       <li>
+                               <a href="/build/{{ pkg.build.uuid }}">{{ escape(pkg.build.name) }}</a>
+                               <span class="divider">/</span>
+                       </li>
                {% end %}
+               {% if pkg.job %}
+                       <li>
+                               <a href="/job/{{ pkg.job.uuid }}">{{ escape(pkg.job.arch.name) }}</a>
+                               <span class="divider">/</span>
+                       </li>
+               {% end %}
+               <li class="active">
+                       <a href="/package/{{ pkg.uuid }}">{{ escape(pkg.friendly_name) }}</a>
+               </li>                   
+       </ul>
+
+       <div class="page-header">
+               <div class="pull-right">
+                       {% if pkg.type == "source" %}
+                               <span class="label label-success">{{ _("Source package") }}</span>
+                       {% end %}
+                       {% if pkg.name.endswith("-devel") %}
+                               <span class="label label-info">{{ _("Development package") }}</span>
+                       {% end %}
+                       {% if pkg.name.endswith("-debuginfo") %}
+                               <span class="label">{{ _("Debuginfo package") }}</span>
+                       {% end %}
+               </div>
+
+               <h1>
+                       {{ _("Package") }}: {{ escape(pkg.friendly_name) }}
+                       <br /><small>{{ escape(pkg.summary) }}</small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span4">
+                       <blockquote>{{ escape(pkg.description) }}</blockquote>
+               </div>
 
-               <tr>
-                       <td class="col1">{{ _("Supported architectures") }}</td>
-                       <td class="col2">{{ locale.list(pkg.supported_arches) }}</td>
-               </tr>
-       </table>
-       <div style="clear: both;">&nbsp;</div>
-
-       <h2>{{ _("Comments") }}</h2>
-       <p>
-               {{ _("This package got a total credit count of %s credits.") % pkg.credits }}
-       </p>
-
-       {% if current_user %}
-               <p>
-                       <a id="comment-toggle" href="#">{{ _("Add comment") }}</a>
-                       <script>
-                               /* Initially hide the comment area and show it if the user clicks "add comment". */
-                               $(function() {
-                                       $(".add-comment").hide();
-                                       $("#comment-toggle").toggle(
-                                               function() { $(".add-comment").show(); },
-                                               function() { $(".add-comment").hide(); }
-                                       );
-                               });
-                       </script>
-               </p>
-               <div class="add-comment">
-                       <h4>{{ _("Add comment") }}</h4>
-                       <form method="post" action="">
-                               {{ xsrf_form_html() }}
-                               <input type="hidden" name="action" value="comment" />
-                               <table class="form form2">
+               <div class="span4">
+                       <table class="table">
+                               <tbody>
                                        <tr>
-                                               <td colspan="2">
-                                                       <textarea name="text" cols="90" rows="6"></textarea>
+                                               <td>{{ _("Homepage") }}</td>
+                                               <td>
+                                                       <a href="{{ escape(pkg.url) }}" target="_blank">{{ escape(pkg.url) }}</a>
                                                </td>
                                        </tr>
                                        <tr>
+                                               <td>{{ _("License") }}</td>
                                                <td>
-                                                       {% if current_user.is_tester() or current_user.is_admin() %}
-                                                               {{ _("Vote") }}:
-                                                               <input name="vote" type="radio" value="none" selected="selected" />{{ _("Not tested") }}
-                                                               <input name="vote" type="radio" value="up" />{{ _("Works for me") }}
-                                                               <input name="vote" type="radio" value="down" />{{ _("Doesn't work for me") }}
-                                                       {% end %}
+                                                       {{ escape(pkg.license) }}
                                                </td>
-                                               <td class="buttons">
-                                                       <input name="submit" type="submit" value="{{ _("Submit") }}" />
+                                       </tr>
+                                       {% if pkg.maintainer %}
+                                               <tr>
+                                                       <td>{{ _("Maintainer") }}</td>
+                                                       <td>{{ modules.Maintainer(pkg.maintainer) }}</td>
+                                               </tr>
+                                       {% end %}
+                                       <tr>
+                                               <td>{{ _("Build host") }}</td>
+                                               <td>
+                                                       <a href="/builder/{{ pkg.build_host }}">{{ pkg.build_host }}</a>
                                                </td>
                                        </tr>
-                               </table>
-                       </form>
+                                       <tr>
+                                               <td>{{ _("Build time") }}</td>
+                                               <td>{{ locale.format_date(pkg.build_time, full_format=True) }} UTC</td>
+                                       </tr>
+                               </tbody>
+                       </table>
                </div>
-       {% else %}
-               <p>{{ _("You must be logged in to comment.") }}</p>
-       {% end %}
-       {{ modules.CommentsTable(pkg.comments) }}
 
-       {% if pkg.builds %}
-               <h2>{{ _("Build jobs") }}</h2>
-               {{ modules.BuildTable(pkg.builds) }}
-       {% end %}
+               <div class="span4">
+                       <table class="table">
+                               <tbody>
+                                       {% if pkg.type == "binary" %}
+                                               <tr>
+                                                       <td>{{ _("Source package") }}</td>
+                                                       <td>
+                                                               <a href="/package/{{ pkg.build.pkg.uuid }}">{{ pkg.build.pkg.friendly_name }}</a>
+                                                       </td>
+                                               </tr>
+                                       {% end %}
 
-       {% if pkg.packagefiles %}
-               <h2>{{ _("Package files") }}</h2>
-               {{ modules.FilesTable(pkg.packagefiles) }}
-       {% end %}
+                                       {% if pkg.build %}
+                                               <tr>
+                                                       <td>{{ _("Build") }}</td>
+                                                       <td>
+                                                               <a href="/build/{{ pkg.build.uuid }}">{{ escape(pkg.build.name) }}</a>
+                                                       </td>
+                                               </tr>
+                                       {% end %}
 
-       {% if pkg.logfiles %}
-               <h2>{{ _("Logfiles") }}</h2>
-               {{ modules.FilesTable(pkg.logfiles) }}
-       {% end %}
+                                       {% if pkg.job %}
+                                               <tr>
+                                                       <td>{{ _("Job") }}</td>
+                                                       <td>
+                                                               <a href="/job/{{ pkg.job.uuid }}">{{ escape(pkg.job.name) }}</a>
+                                                       </td>
+                                               </tr>
+                                       {% end %}
 
-       <h2>{{ _("Log") }}</h2>
-       {{ modules.LogTable(pkg.log) }}
-{% end block %}
+                                       <tr>
+                                               <td>{{ _("Size") }}</td>
+                                               <td>
+                                                       {{ format_size(pkg.filesize) }}
+                                                       {% if pkg.type == "binary" %}
+                                                               ({{ _("%(size)s when installed") % { "size" : format_size(pkg.size) } }})
+                                                       {% end %}
+                                               </td>
+                                       </tr>
 
-{% block sidebar %}
-       <h1>{{ _("Actions") }}</h1>
-       <ul>
-               <a href="">{{ _("Package is broken") }}</a>
-       </ul>
+                                       {% if pkg.commit %}
+                                               <tr>
+                                                       <td>{{ _("Commit") }}</td>
+                                                       <td>
+                                                               <a href="/distro/{{ pkg.commit.distro.identifier }}/source/{{ pkg.commit.source.identifier }}/commit/{{ pkg.commit.revision }}">{{ pkg.commit.revision[:7] }}</a>
+                                                               <br />{{ escape(pkg.commit.subject) }}
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span12">
+                       {% if pkg.build %}
+                               <a class="btn pull-right" href="{{ pkg.build.download_prefix }}/{{ pkg.path }}">
+                                       <i class="icon-download"></i>
+                                       {{ _("Download") }}
+                               </a>
+                       {% end %}
+               </div>
+       </div>
+       <hr />
+
+       {% if pkg.type == "binary" %}
+               <div class="row">
+                       <div class="span12">
+                               <h2>{{ _("Dependencies") }}</h2>
+
+                               <div class="tabbable">
+                                       <ul class="nav nav-pills">
+                                               {% if pkg.provides %}
+                                                       <li class="active">
+                                                               <a href="#deps_provides" data-toggle="tab">
+                                                                       {{ _("Provides") }}
+                                                               </a>
+                                                       </li>
+                                               {% end %}
+
+                                               {% if pkg.requires or pkg.prerequires %}
+                                                       <li {% if not pkg.provides %}class="active"{% end %}>
+                                                               <a href="#deps_requires" data-toggle="tab">
+                                                                       {{ _("Requires") }}
+                                                               </a>
+                                                       </li>
+                                               {% end %}
+
+                                               {% if pkg.obsoletes %}
+                                                       <li {% if not any((pkg.provides, pkg.requires, pkg.prerequires)) %}class="active"{% end %}>
+                                                               <a href="#deps_obsoletes" data-toggle="tab">
+                                                                       {{ _("Obsoletes") }}
+                                                               </a>
+                                                       </li>
+                                               {% end %}
+
+                                               {% if pkg.conflicts %}
+                                                       <li {% if not any((pkg.provides, pkg.requires, pkg.prerequires, pkg.obsoletes)) %}class="active"{% end %}>
+                                                               <a href="#deps_conflicts" data-toggle="tab">
+                                                                       {{ _("Conflicts") }}
+                                                               </a>
+                                                       </li>
+                                               {% end %}
+                                       </ul>
+
+                                       <div class="tab-content">
+                                               {% if pkg.provides %}
+                                                       <div class="tab-pane active" id="deps_provides">
+                                                               <ul>
+                                                                       {% for dep in pkg.provides %}
+                                                                               {% if not dep.startswith("uuid(") %}
+                                                                                       <li>{{ escape(dep) }}</li>
+                                                                               {% end %}
+                                                                       {% end %}
+                                                               </ul>
+                                                       </div>
+                                               {% end %}
+
+                                               {% if pkg.requires or pkg.prerequires %}
+                                                       <div class="tab-pane {% if not pkg.provides %}active{% end %}" id="deps_requires">
+                                                               <ul>
+                                                                       {% for dep in pkg.requires %}
+                                                                               <li>{{ escape(dep) }}</li>
+                                                                       {% end %}
+                                                               </ul>
+
+                                                               {% if pkg.prerequires %}
+                                                                       <h3>{{ _("Prerequires") }}</h3>
+
+                                                                       <ul>
+                                                                               {% for dep in pkg.prerequires %}
+                                                                                       <li>{{ escape(dep) }}</li>
+                                                                               {% end %}
+                                                                       </ul>
+                                                               {% end %}
+                                                       </div>
+                                               {% end %}
+
+                                               {% if pkg.obsoletes %}
+                                                       <div class="tab-pane {% if not any((pkg.provides, pkg.requires, pkg.prerequires)) %}active{% end %}" id="deps_obsoletes">
+                                                               <ul>
+                                                                       {% for dep in pkg.obsoletes %}
+                                                                               <li>{{ escape(dep) }}</li>
+                                                                       {% end %}
+                                                               </ul>
+                                                       </div>
+                                               {% end %}
+
+                                               {% if pkg.conflicts %}
+                                                       <div class="tab-pane {% if not any((pkg.provides, pkg.requires, pkg.prerequires, pkg.obsoletes)) %}active{% end %}" id="deps_conflicts">
+                                                               <ul>
+                                                                       {% for dep in pkg.conflicts %}
+                                                                               <li>{{ escape(dep) }}</li>
+                                                                       {% end %}
+                                                               </ul>
+                                                       </div>
+                                               {% end %}
+                                       </div>
+                               </div>
+
+                               <hr>
+                       </div>
+               </div>
+       {% elif pkg.type == "source" %}
+               <div class="row">
+                       <div class="span12">
+                               <h2>{{ _("Build dependencies") }}</h2>
+                               <ul>
+                                       {% for dep in pkg.requires %}
+                                               <li>{{ escape(dep) }}</li>
+                                       {% end %}
+                               </ul>
+                       </div>
+               </div>
+       {% end %}
+
+       {% if pkg.filelist %}
+               <h2>{{ _("Files") }}</h2>
+               {{ modules.PackageFilesTable(pkg, pkg.filelist) }}
+       {% end %}
 {% end block %}
diff --git a/data/templates/package-detail_old.html b/data/templates/package-detail_old.html
new file mode 100644 (file)
index 0000000..e218bd9
--- /dev/null
@@ -0,0 +1,111 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Package") }}: {{ pkg.friendly_name }}{% end block %}
+
+{% block body %}
+       <h1>{{ _("Package") }}: <a href="/package/{{ pkg.name }}">{{ pkg.name }}</a>-{{ pkg.friendly_version }}</h1>
+
+       <p class="pkg-summary">{{ pkg.description }}</p>
+
+       <table class="form form2">
+               <tr>
+                       <td class="col1">{{ _("URL") }}</td>
+                       <td class="col2">
+                               <a href="{{ pkg.url }}" target="_blank">{{ pkg.url }}</a>
+                       </td>
+               </tr>
+               <tr>
+                       <td class="col1">{{ _("License") }}</td>
+                       <td class="col2">{{ pkg.license }}</td>
+               </tr>
+
+               {% if pkg.maintainer %}
+                       <tr>
+                               <td class="col1">{{ _("Maintainer") }}</td>
+                               <td class="col2">{{ escape(pkg.maintainer) }}</td>
+                       </tr>
+               {% end %}
+
+               <tr>
+                       <td class="col1">{{ _("Supported architectures") }}</td>
+                       <td class="col2">{{ locale.list(pkg.supported_arches) }}</td>
+               </tr>
+       </table>
+       <div style="clear: both;">&nbsp;</div>
+
+       <h2>{{ _("Comments") }}</h2>
+       <p>
+               {{ _("This package got a total credit count of %s credits.") % pkg.credits }}
+       </p>
+
+       {% if current_user %}
+               <p>
+                       <a id="comment-toggle" href="#">{{ _("Add comment") }}</a>
+                       <script>
+                               /* Initially hide the comment area and show it if the user clicks "add comment". */
+                               $(function() {
+                                       $(".add-comment").hide();
+                                       $("#comment-toggle").toggle(
+                                               function() { $(".add-comment").show(); },
+                                               function() { $(".add-comment").hide(); }
+                                       );
+                               });
+                       </script>
+               </p>
+               <div class="add-comment">
+                       <h4>{{ _("Add comment") }}</h4>
+                       <form method="post" action="">
+                               {{ xsrf_form_html() }}
+                               <input type="hidden" name="action" value="comment" />
+                               <table class="form form2">
+                                       <tr>
+                                               <td colspan="2">
+                                                       <textarea name="text" cols="90" rows="6"></textarea>
+                                               </td>
+                                       </tr>
+                                       <tr>
+                                               <td>
+                                                       {% if current_user.is_tester() or current_user.is_admin() %}
+                                                               {{ _("Vote") }}:
+                                                               <input name="vote" type="radio" value="none" selected="selected" />{{ _("Not tested") }}
+                                                               <input name="vote" type="radio" value="up" />{{ _("Works for me") }}
+                                                               <input name="vote" type="radio" value="down" />{{ _("Doesn't work for me") }}
+                                                       {% end %}
+                                               </td>
+                                               <td class="buttons">
+                                                       <input name="submit" type="submit" value="{{ _("Submit") }}" />
+                                               </td>
+                                       </tr>
+                               </table>
+                       </form>
+               </div>
+       {% else %}
+               <p>{{ _("You must be logged in to comment.") }}</p>
+       {% end %}
+       {{ modules.CommentsTable(pkg.comments) }}
+
+       {% if pkg.builds %}
+               <h2>{{ _("Build jobs") }}</h2>
+               {{ modules.BuildTable(pkg.builds) }}
+       {% end %}
+
+       {% if pkg.packagefiles %}
+               <h2>{{ _("Package files") }}</h2>
+               {{ modules.FilesTable(pkg.packagefiles) }}
+       {% end %}
+
+       {% if pkg.logfiles %}
+               <h2>{{ _("Logfiles") }}</h2>
+               {{ modules.FilesTable(pkg.logfiles) }}
+       {% end %}
+
+       <h2>{{ _("Log") }}</h2>
+       {{ modules.LogTable(pkg.log) }}
+{% end block %}
+
+{% block sidebar %}
+       <h1>{{ _("Actions") }}</h1>
+       <ul>
+               <a href="">{{ _("Package is broken") }}</a>
+       </ul>
+{% end block %}
diff --git a/data/templates/package-list.html b/data/templates/package-list.html
deleted file mode 100644 (file)
index 0f20e72..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}{{ _("Package list") }}{% end block %}
-
-{% block body %}
-       <h1>{{ _("Package list") }}</h1>
-       <p>
-               {{ _("This is an alphabetically ordered list of all packages in the distribution.") }}
-               {{ _("Click on a link to see further information about the package.") }}
-       </p>
-
-       <ul class="alphabet">
-               <li>{{ _("Quick selection:") }}</li>
-               {% for letter in sorted(packages.keys()) %}
-                       <li><a href="#{{ letter }}">{{ letter.upper() }}</a></li>
-               {% end %}
-       </ul>
-       <div style="clear: both;">&nbsp;</div>
-
-       {% for letter, pkgs in sorted(packages.items()) %}
-               {{ modules.PackageTable(letter, pkgs) }}
-       {% end %}
-{% end block %}
diff --git a/data/templates/package-properties.html b/data/templates/package-properties.html
new file mode 100644 (file)
index 0000000..5ed9e2d
--- /dev/null
@@ -0,0 +1,99 @@
+{% extends "base-form1.html" %}
+
+{% block title %}{{ _("Package") }} {{ pkg.name }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/packages">{{ _("Packages") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/package/{{ escape(pkg.name) }}">{{ escape(pkg.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/package/{{ escape(pkg.name) }}/properties">{{ _("Properties") }}</a>
+               </li>
+       </ul>
+
+       {{ modules.BuildHeadline(None, build, shorter=True) }}
+
+       <div class="row">
+               <div class="span8">
+                       <form class="form form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+
+                               <fieldset>
+                                       <legend>{{ _("Maintainers") }}</legend>
+                                       
+                               </fieldset>
+
+                               <fieldset>
+                                       <legend>{{ _("Default priority") }}</legend>
+                                       <p>
+                                               {{ _("A big benefit of the Pakfire Build Service is, that builds are available to end-users in a very short time.") }}
+                                               {{ _("Some packages might need some extra boost if the build servers are very busy.") }}
+                                       </p>
+                                       <p>
+                                               {{ _("You may set a default priority for all builds of this package.") }}
+                                       </p>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="priority">{{ _("Default priority") }}</label>
+                                               <div class="controls">
+                                                       <select name="priority" id="priority">
+                                                               <option value="2" {% if properties.priority >= 2 %}selected="selected"{% end %}>
+                                                                       {{ _("Very high") }}
+                                                               </option>
+                                                               <option value="1" {% if properties.priority == 1 %}selected="selected"{% end %}>
+                                                                       {{ _("High") }}
+                                                               </option>
+                                                               <option value="0" {% if properties.priority == 0 %}selected="selected"{% end %}>
+                                                                       {{ _("Medium") }}
+                                                               </option>
+                                                               <option value="-1" {% if properties.priority == -1 %}selected="selected"{% end %}>
+                                                                       {{ _("Low") }}
+                                                               </option>
+                                                               <option value="-2" {% if properties.priority <= -2 %}selected="selected"{% end %}>
+                                                                       {{ _("Very low") }}
+                                                               </option>
+                                                       </select>
+                                               </div>
+                                       </div>
+                               </fieldset>
+
+                               <fieldset>
+                                       <legend>{{ _("Critical path") }}</legend>
+                                       <p>
+                                               {{ _("A package that belongs to the critical path is a package that plays a very essential role in the distribution.") }}
+                                               {{ _("If such a package is broken, it may not be possible to boot or recover the system anymore, so we need to be extra sure that these packages work.") }}
+                                       </p>
+                                       <p>
+                                               {{ _("If this package is marked to belong to the critical path, it will need a higher score to pass to the next repository and more.") }}
+                                               <a href="/documents/critical-path">{{ _("Learn more.") }}</a>
+                                       </p>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="critical_path">{{ _("Critical path") }}</label>
+                                               <div class="controls">
+                                                       <label class="checkbox">
+                                                               <input type="checkbox" id="critical_path">
+                                                               {{ _("This package belongs to the critical path") }}
+                                                       </label>
+                                               </div>
+                                       </div>
+                               </fieldset>
+
+                               <div class="form-actions">
+                                       <button type="submit" class="btn btn-primary">{{ _("Save changes") }}</button>
+                                       <a class="btn" href="/package/{{ escape(pkg.name) }}">{{ _("Cancel") }}</a>
+                               </div>
+                       </form>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/packages-list.html b/data/templates/packages-list.html
new file mode 100644 (file)
index 0000000..150a56c
--- /dev/null
@@ -0,0 +1,84 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Package list") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/packages">{{ _("Packages") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Package list") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span5 offset1">
+                       <p>
+                               {{ _("This is an alphabetically ordered list of all packages in the distribution.") }}
+                               {{ _("Click on a link to see further information about the package.") }}
+                       </p>
+               </div>
+
+               <div class="span5">
+                       <div class="btn-group">
+                               <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+                                       {{ _("Selection") }}
+                                       <span class="caret"></span>
+                               </a>
+                               <ul class="dropdown-menu">
+                                       <li>
+                                               <a href="?show=broken">{{ _("Show broken packages") }}</a>
+                                       </li>
+                                       <li>
+                                               <a href="?show=all">{{ _("Show all packages") }}</a>
+                                       </li>
+                               </ul>
+                       </div>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span10 offset1">
+                       <div class="btn-toolbar">
+                               <div class="btn-group">
+                                       {% for letter in sorted(packages.keys()) %}
+                                               <a class="btn" href="#{{ letter }}">{{ letter.upper() }}</a>
+                                       {% end %}
+                               </div>
+                       </div>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span10 offset1">
+                       <table class="table table-striped">
+                               <tbody>
+                                       {% for letter, pkgs in sorted(packages.items()) %}
+                                               <tr>
+                                                       <td colspan="2">
+                                                               <a name="{{ letter }}"></a>
+                                                               <h2>{{ letter.upper() }} <small>({{ len(pkgs) }})</small></h2>
+                                                       </td>
+                                               </tr>
+                                               {% for pkg, summary in pkgs %}
+                                                       <tr>
+                                                               <td>
+                                                                       <a href="/package/{{ pkg }}">{{ pkg }}</a>
+                                                               </td>
+                                                               <td>
+                                                                       {{ escape(summary) }}
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+                                       {% end %}
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+{% end block %}
index eb6c52e3b7a471cef0527158671a65d0bf8dd970..25f8a241bf22a5df28933bfcaa0f9263991e4f5d 100644 (file)
@@ -1,10 +1,29 @@
 {% extends "base.html" %}
 
+{% block title %}{{ _("Account activation failed") }}{% end block %}
+
 {% block body %}
-       <h1>{{ _("Activation failed") }}</h1>
-       <p>
-               {{ _("We are sorry.") }}
-               {{ _("The activation of your account has failed.") }}
-               {{ _("Possibly the registration code is wrong or your registration timed out.") }}
-       </p>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       {{ _("Account activation") }}
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Activation failed") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("We are sorry.") }}
+                               {{ _("The activation of your account has failed.") }}
+                               {{ _("Possibly the registration code is wrong or your registration timed out.") }}
+                       </p>
+               </div>
+       </div>
 {% end %}
index 0043c687e6113978dc507d0f53d3e18a949c4c34..474cfad7efb81d5a26beb8afb5283c2d9ff375c9 100644 (file)
@@ -1,9 +1,28 @@
 {% extends "base.html" %}
 
+{% block title %}{{ _("Account activation successful") }}{% end block %}
+
 {% block body %}
-       <h1>{{ _("Activation successful") }}</h1>
-       <p>
-               {{ _("Your account has been activated, %s.") % user.realname }}
-               {{ _("Have fun!") }}
-       </p>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       {{ _("Account activation") }}
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Activation successful") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <p>
+                               {{ _("Your account has been activated, %s.") % user.realname }}
+                               {{ _("Have fun!") }}
+                       </p>
+               </div>
+       </div>
 {% end %}
index da75c3057ebd1f41e1516f68a5e31b6e179eec16..22a5b13b44180c4bd6fdce93fe49e38a32cc3a46 100644 (file)
-{% extends "base.html" %}
+{% extends "base-form1.html" %}
+
+{% block title %}{{ _("Register a new account") }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Register new account") }}</h1>
-
-       <form method="post" action="">
-               {{ xsrf_form_html() }}
-               <table class="form form3">
-                       <tr>
-                               <td class="col1">{{ _("Name") }}:</td>
-                               <td class="col2">
-                                       <input name="name" type="text" length="64" />
-                               </td>
-                               <td class="col3">
-                                       {{ _("Must be a unique name you login with.") }}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Email") }}:</td>
-                               <td class="col2">
-                                       <input name="email" type="text" length="100" />
-                               </td>
-                               <td class="col3">
-                                       {{ _("Type your email address.") }}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Real name (optional)") }}:</td>
-                               <td class="col2">
-                                       <input name="realname" type="text" length="200" />
-                               </td>
-                               <td class="col3">
-                                       {{ _("Type you firstname and your lastname here.") }}
-                               </td>
-                       </tr>
-               </table>
-               
-               <h2>{{ _("Account security") }}</h2>
-               <table class="form form3">
-                       <tr>
-                               <td class="col1">{{ _("Password") }}:</td>
-                               <td class="col2">
-                                       <input name="pass1" type="password" />
-                               </td>
-                               <td class="col3">
-                                       {{ _("The password is used to secure the login and must be at least 8 characters.") }}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class="col1">{{ _("Confirm") }}:</td>
-                               <td class="col2">
-                                       <input name="pass2" type="password" />
-                               </td>
-                               <td class="col3">&nbsp;</td>
-                       </tr>
-                       <tr>
-                               <td colspan="3" class="buttons">
-                                       <input type="submit" value="{{ _("Register") }}" />
-                               </td>
-                       </tr>
-               </table>
-       </form>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/register">{{ _("Register new account") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Register a new account") }}<br>
+                       <small>{{ _("Join the community!") }}</small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span8">
+                       <p>
+                               {{ _("Signing up to the Pakfire Build Service is free.") }}
+                       </p>
+
+                       <p>
+                               {{ _("Please make sure you have read the guide about what Pakfire Build Service is, and what it is not:") }}
+
+                               <a href="/documents/what-is-the-pakfire-build-service">
+                                       {{ _("What is the Pakfire Build Service?") }}
+                               </a>
+                       </p>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span8">
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+
+                               <legend>{{ _("Registration form") }}</legend>
+
+                               <fieldset>
+                                       <div class="control-group">
+                                               <label class="control-label" for="name">{{ _("Username") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="name" name="name" />
+
+                                                       <p class="help-block">
+                                                               {{ _("Must be a unique name you login with.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="email">{{ _("Email") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="email" name="email" />
+
+                                                       <p class="help-block">
+                                                               {{ _("Type in your email address, which is used to verify the account.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="realname">{{ _("Real name (optional)") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="realname" name="realname" />
+
+                                                       <p class="help-block">
+                                                               {{ _("Type you firstname and your lastname here.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+                               </fieldset>
+
+                               <fieldset>
+                                       <legend>{{ _("Account security") }}</legend>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="pass1">{{ _("Password") }}</label>
+                                               <div class="controls">
+                                                       <input type="password" class="input-xlarge" id="pass1" name="pass1" />
+
+                                                       <p class="help-block">
+                                                               {{ _("The password is used to secure the login and must be at least 8 characters.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <label class="control-label" for="pass2">{{ _("Confirm password") }}</label>
+                                               <div class="controls">
+                                                       <input type="password" class="input-xlarge" id="pass2" name="pass2" />
+
+                                                       <p class="help-block">
+                                                               {{ _("<strong>You</strong> are responsible for your account security!") }}
+                                                               <br />
+                                                               {{ _("Pick a password that is as strong as possible.") }}
+                                                               {{ _("Don't login at unsecure places where people could spy on your password.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+                               </fieldset>
+
+                               <div class="form-actions">
+                                       <button type="submit" class="btn btn-primary">{{ _("Sign up!") }}</button>
+                               </div>
+                       </form>
+               </div>
+       </div>
 {% end block %}
index abca9b4ad36f0624521c3b87ec9915b54d5d3b3a..51137a947ec34fbdec1658b4b404944121bc4828 100644 (file)
 {% extends "base.html" %}
 
+{% block title %}
+       {{ _("Repository") }}: {{ escape(repo.name) }} - {{ _("Distribution") }}: {{ escape(distro.name) }}
+{% end block %}
+
 {% block body %}
-       <h1>
-               {{ _("Repository") }}: {{ repo.name }} -
-               {{ _("from") }} <a href="/distribution/{{ distro.sname }}">{{ distro.name }}</a>
-       </h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distros">{{ _("Distributions") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/distro/{{ distro.identifier }}">{{ escape(distro.name) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/distro/{{ distro.identifier }}/repo/{{ repo.identifier }}">{{ escape(repo.name) }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <div class="pull-right">
+                       {% if repo.type == "stable" %}
+                               <span class="label label-success">
+                                       {{ _("Stable repository") }}
+                               </span>
+                       {% elif repo.type == "unstable" %}
+                               <span class="label label-warning">
+                                       {{ _("Unstable repository") }}
+                               </span>
+                       {% elif repo.type == "testing" %}
+                               <span class="label label-important">
+                                       {{ _("Testing repository") }}
+                               </span>
+                       {% end %}
+
+                       {% if repo.enabled_for_builds %}
+                               <span class="label label-inverse">
+                                       {{ _("Enabled for builds") }}
+                               </span>
+                       {% end %}
+               </div>
+
+               <h1>
+                       {{ _("Repository") }}: {{ escape(repo.name) }}
+                       <small>{{ escape(distro.name) }}</small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span8">
+                       <blockquote>
+                               {{ modules.Text(repo.description, pre=False) }}
+                       </blockquote>
+
+                       <br><br>
+
+                       <table class="table">
+                               <tr>
+                                       <td>{{ _("Repository is enabled for builds?") }}</td>
+                                       <td>
+                                               {% if repo.enabled_for_builds %}
+                                                       {{ _("Yes") }}
+                                               {% else %}
+                                                       {{ _("No") }}
+                                               {% end %}
+                                       </td>
+                               </tr>
 
-       <p class="pkg-summary">{{ repo.description }}</p>
+                               <tr>
+                                       <td>{{ _("Obsolete builds") }}</td>
+                                       <td>
+                                               {{ len(obsolete_builds) }}
+                                       </td>
+                               </tr>
+                       </table>
+               </div>
 
-       <p>
-               {{ _("This repository contains %s packages and is available for %s.") % (len(repo.packages), locale.list(repo.arches)) }}
-       </p>
+               <div class="span4">
+                       <h3>{{ _("Total build time") }}</h3>
 
-       {% if repo.has_actions() %}
-               <h2>{{ _("Pending actions") }}</h2>
-               {{ modules.RepoActionsTable(repo) }}
+                       <table class="table table-striped">
+                               <tbody>
+                                       {% for arch, build_time in build_times %}
+                                               <tr>
+                                                       <td class="arch">{{ arch.name }}</td>
+                                                       <td class="time">{{ friendly_time(build_time) }}</td>
+                                               </tr>
+                                       {% end %}
+                               </tbody>
+                       </table>
+
+                       <p>
+                               {{ _("The table above shows how long it took to build all packages in this repository.") }}
+                       </p>
+               </div>
+       </div>
+
+       {% if current_user and current_user.is_admin() %}
+               <div class="row">
+                       <div class="span12">
+                               <hr>
+
+                               <div class="btn-group pull-right">
+                                       <a class="btn btn-small btn-danger" href="/distro/{{ distro.identifier }}/repo/{{ repo.identifier }}/edit">
+                                               <i class="icon-edit icon-white"></i>
+                                               {{ _("Edit") }}
+                                       </a>
+                                       <a class="btn btn-small btn-danger" href="/distro/{{ distro.identifier }}/repo/{{ repo.identifier }}/delete">
+                                               <i class="icon-trash icon-white"></i>
+                                               {{ _("Delete") }}
+                                       </a>
+                               </div>
+                       </div>
+               </div>
        {% end %}
 
-       <h2>{{ _("Waiting packages") }}</h2>
-       <p>
-               {{ _("These packages are waiting to be published.") }}
-       </p>
-       {{ modules.PackageTable2(repo.waiting_packages) }}
+       {% if unpushed_builds %}
+               <div class="row">
+                       <div class="span12">
+                               <h2>{{ _("Unpushed builds") }}</h2>
+                               <div class="alert">
+                                       {{ _("These builds were already put into this repository, but were not pushed out to the mirror servers, yet.") }}
+                               </div>
+                               {{ modules.BuildTable(unpushed_builds, show_repo_time=True) }}
+                       </div>
+               </div>
+       {% end %}
 
-       <h2>{{ _("Pushed packages") }}</h2>
-       {{ modules.PackageTable2(repo.pushed_packages) }}
+       {% if builds %}
+               <div class="row">
+                       <div class="span12">
+                               <h2>
+                                       {{ _("Builds in this repository") }}
+                                       <small>({{ len(builds) }})</small>
+                               </h2>
+                               {{ modules.BuildTable(builds, show_repo_time=True, show_can_move_forward=True) }}
+                       </div>
+               </div>
+       {% end %}
+
+       {% if obsolete_builds %}
+               <div class="row">
+                       <div class="span12">
+                               <h2>
+                                       {{ _("Obsolete builds") }}
+                                       <small>({{ len(obsolete_builds) }})</small>
+                               </h2>
+                               {{ modules.BuildTable(obsolete_builds) }}
+                       </div>
+               </div>
+       {% end %}
 
-       <h2>{{ _("Log") }}</h2>
-       {{ modules.LogTable(repo.log) }}
 {% end block %}
diff --git a/data/templates/repository-edit.html b/data/templates/repository-edit.html
new file mode 100644 (file)
index 0000000..4f5b21d
--- /dev/null
@@ -0,0 +1,96 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Edit repository %s") % escape(repo.name) }}{% end block %}
+
+{% block body %}
+       <h1>
+               {{ _("Edit repository %s") % escape(repo.name) }}
+               <span>- {{ _("Distribution") }}: {{ escape(distro.name) }}</span>
+       </h1>
+
+       <form method="post" action="">
+               {{ xsrf_form_html() }}
+               <table class="form form3">
+                       <tr>
+                               <td class="col1">{{ _("Name") }}</td>
+                               <td class="col2">
+                                       <input name="name" type="text" value="{{ escape(repo.name) }}" />
+                               </td>
+                               <td class="col3">
+                                       {{ _("The name of the repository.") }}
+                                       {{ _("Must only contain of the lowercase characters.") }}
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class="col1">{{ _("Description") }}</td>
+                               <td colspan="2">                
+                                       <textarea name="description">{{ escape(repo.description) }}</textarea>
+                               </td>
+                       </tr>
+               </table>
+               <div style="clear: both;">&nbsp;</div>
+
+               <h2>{{ _("Score settings") }}</h2>
+               <p>
+                       {{ _("These settings configure the automatic score feature.") }}
+                       {{ _("Builds that gained a certain score are moved to the next repository automatically and removed if the score is too bad.") }}
+               </p>
+               <table class="form form3">
+                       <tr>
+                               <td class="col1">{{ _("Needed score") }}</td>
+                               <td class="col2">
+                                       <input name="name" type="text" value="{{ repo.score_needed }}" />
+                               </td>
+                               <td class="col3">
+                                       {{ _("The score that is needed for builds to automatically be moved into this repository.") }}
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class="col1">{{ _("Minimum time") }}</td>
+                               <td class="col2">
+                                       <input name="name" type="text" value="{{ repo.time_min }}" />
+                               </td>
+                               <td class="col3">
+                                       {{ _("Every build must stay a minimum time in a repository.") }}
+                                       {{ _("This is to ensure that a package gets tested well.") }}
+                                       {{ _("Enter zero to disable the feature.") }}
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class="col1">{{ _("Maximum time") }}</td>
+                               <td class="col2">
+                                       <input name="name" type="text" value="{{ repo.time_max }}" />
+                               </td>
+                               <td class="col3">
+                                       {{ _("If a build is more than a certain amount of time in a repository, it will automatically be removed.") }}
+                                       {{ _("This is to ensure that packages are not forgotten to be pushed.") }}
+                                       {{ _("Enter zero to disable the feature.") }}
+                               </td>
+                       </tr>
+               </table>
+               <div style="clear: both;">&nbsp;</div>
+
+               <h2>{{ _("Build settings") }}</h2>
+               <table class="form form3">
+                       <tr>
+                               <td class="col1">{{ _("Use package for builds?") }}</td>
+                               <td class="col2">
+                                       <input type="checkbox" name="enabled_for_builds" {%if repo.enabled_for_builds %}checked="checked"{% end %} />
+                               </td>
+                               <td class="col3">
+                                       {{ _("Should the package be selected for builds by default?") }}
+                                       {{ _("Use with caution!") }}
+                               </td>
+                       </tr>
+               </table>
+               <div style="clear: both;">&nbsp;</div>
+
+               <table class="form form3">
+                       <tr>
+                               <td colspan="3" class="buttons">
+                                       <input type="submit" value="{{ _("Save") }}" />
+                               </td>
+                       </tr>
+               </table>
+       </form>
+{% end block %}
index 63eb95bad7fcedb51817fb84d4933effe510e8b4..f8afce3012a44a9cf0f6c0f1ab370b740c55ce9a 100644 (file)
@@ -3,9 +3,99 @@
 {% block title %}{{ _("Advanced search") }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Advanced search") }}</h1>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/search">{{ _("Search") }}</a>
+               </li>
+       </ul>
 
-       <p>
-               XXX TO BE DONE
-       </p>
+       {% if pattern %}
+               <div class="page-header">
+                       <h1>{{ _("No search results for '%s'.") % escape(pattern) }}</h1>
+               </div>
+
+               <div class="alert alert-block">
+                       <a class="close" data-dismiss="alert">&times;</a>
+                       <h4 class="alert-header">{{ _("Notice") }}</h4>
+                       {{ _("Your search query '<em>%s</em>' did not return any results.") % escape(pattern) }}
+                       {{ _("Use the box below to try again.") }}
+               </div>
+       {% else %}
+               <div class="page-header">
+                       <h1>{{ _("Advanced search") }}</h1>
+               </div>
+
+               <div class="row">
+                       <div class="span8 offset2">
+                               <p>
+                                       {{ _("Type a search pattern into the box below and hit the 'Search' button.") }}
+                               </p>
+                       </div>
+               </div>
+       {% end %}
+
+       <div class="row">
+               <div class="span4 offset4">
+                       <form class="well form-search" method="GET" action="/search">
+                               <input type="text" class="input-medium search-query" name="q" value="{{ escape(pattern) }}">
+                               <button type="submit" class="btn">{{ _("Search") }}</button>
+                       </form>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span6">
+                       <h2>{{ _("Search pattern syntax") }}</h2>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span6">
+                       <div class="well">
+                               <h3>{{ _("Package names and descriptions") }}</h3>
+                               <p>
+                                       {{ _("If you type a package name to the search box you will get a link to the package.") }}
+                                       {{ _("The search is performed in case insensitive mode.") }}
+                               </p>
+                       </div>
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span6">
+                       <div class="well">
+                               <h3>{{ _("UUIDs") }}</h3>
+                               <p>
+                                       {{ _("If you type a UUID to the search box, you will be directed to the job, build or package it belongs to.") }}
+                                       {{ _("This is a handy feature if you have a UUID and search for the corresponding package or build.") }}
+                               </p>
+
+                               <h4>{{ _("Examples") }}</h4>
+                               <ul>
+                                       <li>fde110ca-f7d9-4ae5-a6c4-1e465a05662a</li>
+                               </ul>
+                       </div>
+               </div>
+
+               <div class="span6">
+                       <div class="well">
+                               <h3>{{ _("Files") }}</h3>
+                               <p>
+                                       {{ _("You may also search for file names.") }}
+                                       {{ _("You will get a list of packages that contain the file.") }}
+                                       {{ _("The search pattern must start with a slash that it will be recognized as a file.") }}
+                               </p>
+
+                               <h4>{{ _("Examples") }}</h4>
+                               <ul>
+                                       <li>/bin/bash</li>
+                                       <li>/usr/bin/gcc</li>
+                               </ul>
+                       </div>
+               </div>
+       </div>
 {% end block %}
index 93687b47c5727f76d3eabcdb192be6eb81a1115b..5329ef648f7699d0e49131c3ed02aef9a31500f6 100644 (file)
 {% extends "base.html" %}
 
-{% block title %}{{ _("Search results for '%s'") % escape(query) }}{% end block %}
+{% block title %}{{ _("Search results for '%s'") % escape(pattern) }}{% end block %}
 
 {% block body %}
-       <h1>{{ _("Search results for '%s'") % escape(query) }}</h1>
-       <p>
-               {{ _("These are the results you searched for.") }}
-       </p>
-
-       <table class="form2">
-               {% for pkg in pkgs %}
-                       <tr>
-                               <td class="col1">
-                                       <a href="/package/{{ pkg.name }}">{{ pkg.name }}</a>
-                               </td>
-                               <td class="col2">{{ pkg.summary }}</td>
-                       </tr>
-               {% end %}
-       </table>
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/search">{{ _("Search results") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/search?q={{ escape(pattern) }}">{{ escape(pattern) }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Search results for '%s'") % escape(pattern) }}
+               </h1>
+       </div>
+
+       {% if not pkgs and not files and not users %}
+               <div class="alert alert-block">
+                       <h4 class="alert-heading">{{ _("No results found.") }}</h4>
+                       <a href="/search">
+                               {{ _("Visit the advanced search page to find about how to define your query.") }}
+                       </a>
+               </div>
+       {% end %}
+
+       <div class="row">
+               <div class="span4 offset4">
+                       <form class="well form-search" method="GET" action="/search">
+                               <input type="text" class="input-medium search-query" name="q" value="{{ escape(pattern) }}">
+                               <button type="submit" class="btn">{{ _("Search") }}</button>
+                       </form>
+               </div>
+       </div>
+
+       {% if pkgs %}
+               <div class="row">
+                       <div class="span10 offset1">
+                               <h2>
+                                       {{ _("Packages") }}
+                                       <small>({{ len(pkgs) }})</small>
+                               </h2>
+
+                               <table class="table table-striped">
+                                       <tbody>
+                                               {% for pkg in pkgs %}
+                                                       <tr>
+                                                               <td>
+                                                                       <a href="/package/{{ escape(pkg.name) }}">
+                                                                               {{ escape(pkg.name) }}
+                                                                       </a>
+                                                               </td>
+                                                               <td>{{ escape(pkg.summary) }}</td>
+                                                       </tr>
+                                               {% end %}
+                                       </tbody>
+                               </table>
+                       </div>
+               </div>
+       {% end %}
+
+       {% if files %}
+               <div class="row">
+                       <div class="span10 offset1">
+                               <h2>
+                                       {{ _("Files") }}
+                                       <small>({{ len(files) }})</small>
+                               </h2>
+
+                               <p>
+                                       {{ _("%s was found in the following package.", "%s was found in the following packages.", len(files)) % escape(pattern) }}
+                               </p>
+
+                               <table class="table table-striped">
+                                       <tbody>
+                                               {% for pkg, file in files %}
+                                                       <tr>
+                                                               <td>
+                                                                       <a href="/package/{{ pkg.uuid }}">{{ pkg.friendly_name }}</a>
+                                                               </td>
+                                                               <td>
+                                                                       <pre>{{ format_filemode(file.type, file.mode) }} {{ file.user }}:{{ file.group }} {% if file.size %}{{ format_size(file.size) }}{% else %}-{% end %} {{ escape(file.name) }}</pre>
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+                                       </tbody>
+                               </table>
+                       </div>
+               </div>
+       {% end %}
+
+       {% if users %}
+               <div class="row">
+                       <div class="span10 offset1">
+                               <h2>
+                                       {{ _("Users") }}
+                                       <small>({{ len(users) }})</small>
+                               </h2>
+
+                               <table class="table table-striped">
+                                       <tbody>
+                                               {% for user in users %}
+                                                       <tr>
+                                                               <td>
+                                                                       <a href="/user/{{ escape(user.name) }}">{{ escape(user.realname) }}
+                                                                               {% if not user.name == user.realname %}
+                                                                                       ({{ escape(user.name) }})
+                                                                               {% end %}
+                                                                       </a>
+                                                               </td>
+                                                               <td>
+                                                                       &nbsp;
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+                                       </tbody>
+                               </table>
+                       </div>
+               </div>
+       {% end %}
 {% end block %}
diff --git a/data/templates/source-detail.html b/data/templates/source-detail.html
deleted file mode 100644 (file)
index 06dae3c..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "base.html" %}
-
-{% block body %}
-       <h1>{{ _("Source") }}: {{ source.name }}</h1>
-
-       <table>
-               <!-- Status -->
-               <tr>
-                       <td>{{ _("Revision") }}</td>
-                       <td>{{ source.revision }}</td>
-               </tr>
-
-               <!-- Branch -->
-               <tr>
-                       <td>{{ _("Branch") }}</td>
-                       <td>
-                               {{ source.branch }}
-                       </td>
-               </tr>
-       </table>
-       <div style="clear: both;">&nbsp;</div>
-
-       <h2>{{ _("Latest builds") }}</h2>
-       {{ modules.BuildTable(source.builds) }}
-{% end block %}
diff --git a/data/templates/statistics-main.html b/data/templates/statistics-main.html
new file mode 100644 (file)
index 0000000..f9d5c9e
--- /dev/null
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Statistics") }}{% end block %}
+
+{% block body %}
+       <h1>{{ _("Statistics") }}</h1>
+       <p>
+               {{ _("On this page, you will find a lot of information bundled in graphs and figures.") }}
+               {{ _("They give a very quick overview about what is going on in the build service.") }}
+       </p>
+
+       <h2>{{ _("Builds") }}</h2>
+       <ul>
+               <li>
+                       {{ _("The average build time is %.1f minutes.") % (jobs_avg_build_time / 60) }}
+               </li>
+               <li>
+                       {{ _("There %(builds_count)s builds containing %(jobs_count_all)s jobs.") % globals() }}
+               </li>
+       </ul>
+
+       <h2>{{ _("Users") }}</h2>
+       <p>
+               {{ _("There is currently one user account.", "There are currently %s user accounts.", users_count) % users_count }}
+       </p>
+
+       <div style="clear: both;">&nbsp;</div>
+{% end block %}
diff --git a/data/templates/updates-index.html b/data/templates/updates-index.html
new file mode 100644 (file)
index 0000000..b2396cf
--- /dev/null
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Updates") }}{% end block %}
+
+{% block body %}
+       {{ _("Coming soon...") }}
+{% end block %}
diff --git a/data/templates/uploads-list.html b/data/templates/uploads-list.html
new file mode 100644 (file)
index 0000000..840a466
--- /dev/null
@@ -0,0 +1,93 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Uploads") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/uploads">{{ _("Uploads") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>
+                       {{ _("Uploads") }}
+                       <small>({{ len(uploads) }})</small>
+               </h1>
+       </div>
+
+       <div class="row">
+               <div class="span12">
+                       <p>
+                               {{ _("On this page, you will see all running uploads.") }}
+                       </p>
+               </div>
+       </div>
+
+       {% if uploads %}
+               <div class="row">
+                       <div class="span6 offset3">
+                               <table class="table table-striped">
+                                       <thead>
+                                               <tr>
+                                                       <th>{{ _("Filename") }}</th>
+                                                       <th>{{ _("Owner") }}</th>
+                                                       <th>{{ _("Filesize") }}</th>
+                                                       <th>{{ _("Time running") }}</th>
+                                               </tr>
+                                       </thead>
+                                       <tbody>
+                                               {% for upload in uploads %}
+                                                       <tr>
+                                                               <td>{{ escape(upload.filename) }}</td>
+                                                               <td>
+                                                                       {% if upload.builder %}
+                                                                               <a href="/builder/{{ escape(upload.builder.name) }}">
+                                                                                       {{ escape(upload.builder.name) }}
+                                                                               </a>
+                                                                       {% elif upload.user %}
+                                                                               <a href="/user/{{ escape(upload.user.name) }}">
+                                                                                       {{ escape(upload.user.realname) }}
+                                                                               </a>
+                                                                       {% else %}
+                                                                               {{ _("No owner.") }}
+                                                                       {% end %}
+                                                               </td>
+                                                               <td>
+                                                                       {{ format_size(upload.size) }}
+                                                               </td>
+                                                               <td>
+                                                                       {% if upload.time_running %}
+                                                                               {{ friendly_time(upload.time_running) }} /
+                                                                               {{ format_size(upload.speed) }}b/s
+                                                                       {% else %}
+                                                                               {{ _("N/A") }}
+                                                                       {% end %}
+                                                               </td>
+                                                       </tr>
+                                                       <tr>
+                                                               <td colspan="3">
+                                                                       <div class="progress progress-striped active">
+                                                                               <div class="bar" style="width: {{ "%d" % (upload.progress * 100) }}%;"></div>
+                                                                       </div>
+                                                               </td>
+                                                               <td>
+                                                                       {{ "%.2f%%" % (upload.progress * 100) }}
+                                                               </td>
+                                                       </tr>
+                                               {% end %}
+                                       </tbody>
+                               </table>
+                       </div>
+               </div>
+       {% else %}
+               <div class="alert alert-info alert-block">
+                       <h4 class="alert-heading">{{ _("I'm sorry!") }}</h4>
+                       {{ _("There are currently no uploads running.") }}
+               </div>
+       {% end %}
+{% end block %}
diff --git a/data/templates/user-forgot-password.html b/data/templates/user-forgot-password.html
new file mode 100644 (file)
index 0000000..eb62efb
--- /dev/null
@@ -0,0 +1,55 @@
+{% extends "base-form2.html" %}
+
+{% block title %}{{ _("Forgot password") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/password-recovery">{{ _("Forgot password") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Forgot password") }}</h1>
+       </div>
+
+       <!-- XXX --->
+       <div class="alert alert-warning">
+               {{ _("Work in progress!") }}
+       </div>
+
+       <div class="row">
+               <div class="span6">
+                       <p>
+                               {{ _("You have forgotten you password, eh? Shame on you.") }}
+                               {{ _("However, we allow to re-activate your account.") }}
+                       </p>
+                       <p>
+                               {{ _("You need to enter your username below.") }}
+                               {{ _("After that, you will receive an email with intructions how to go on.") }}
+                       </p>
+                       <hr>
+
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+
+                               <fieldset>
+                                       <div class="control-group">
+                                               <label class="control-label" for="name">{{ _("Your username") }}</label>
+                                               <div class="controls">
+                                                       <input type="text" class="input-xlarge" id="name" name="name" />
+                                               </div>
+                                       </div>
+
+                                       <div class="form-actions">
+                                               <button type="submit" class="btn btn-primary">{{ _("Submit") }}</button>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </div>
+{% end block %}
diff --git a/data/templates/user-impersonation.html b/data/templates/user-impersonation.html
new file mode 100644 (file)
index 0000000..13f4a18
--- /dev/null
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Impersonate user %s") % escape(user.realname) }}{% end block %}
+
+{% block body %}
+       <div class="page-header">
+               <h1>{{ _("User impersonation") }}: {{ escape(user.realname) }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span2 offset2">
+                       <img src="{{ user.gravatar_icon() }}" alt="{{ user.name }}" />
+               </div>
+
+               <div class="span6">
+                       <p>
+                               {{ _("When impersonating another user, every action you perform will be taking place as if you had logged in as the user whom will be impersonating.") }}
+                       </p>
+
+                       <div class="alert alert-danger">
+                               <h4 class="alert-heading">{{ _("Use with caution!") }}</h4>
+                               {{ _("This is a very powerful feature. You should be very careful while using it.") }}
+                       </div>
+
+                       <form method="POST" action="">
+                               {{ xsrf_form_html() }}
+                               <input type="hidden" name="user" value="{{ escape(user.name) }}" />
+                               <input class="btn btn-danger pull-right" type="submit" value="{{ _("Impersonate %s") % escape(user.realname) }}" />
+                       </form>
+               </div>
+       </div>
+{% end block %}
index 3a98e72f3afafcae338f2c9a7569a26ecf1971bf..a8de0b9aa485401f82fec1656857ed19d0b561e3 100644 (file)
@@ -1,30 +1,44 @@
 {% extends "base.html" %}
 
+{% block title %}{{ _("User list") }}{% end block %}
+
 {% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/users">{{ _("Users") }}</a>
+               </li>
+       </ul>
+
        <h1>{{ _("Users") }}</h1>
        <p>
                {{ _("On this page you can see a list of all users that are known to the system.") }}
        </p>
 
        {% if admins %}
-               <h2>{{ _("Administrators") }}</h2>
+               <h2>
+                       {{ _("Developers") }}
+                       <span>({{ len(admins) }})</span>
+               </h2>
                {{ modules.UsersTable(admins) }}
        {% end %}
 
        {% if testers %}
-               <h2>{{ _("Testers") }}</h2>
+               <h2>
+                       {{ _("Testers") }}
+                       <span>({{ len(testers) }})</span>
+               </h2>
                {{ modules.UsersTable(testers) }}
        {% end %}
 
        {% if users %}
-               <h2>{{ _("Users") }}</h2>
+               <h2>
+                       {{ _("Users") }}
+                       <span>({{ len(admins) }})</span>
+               </h2>
                {{ modules.UsersTable(users) }}
        {% end %}
 {% end block %}
-
-{% block sidebar %}
-       <h1>{{ _("Actions") }}</h1>
-       <ul>
-               <li><a href="/users/comments">{{ _("Latest comments") }}</a></li>
-       </ul>
-{% end block %}
diff --git a/data/templates/user-profile-builds.html b/data/templates/user-profile-builds.html
new file mode 100644 (file)
index 0000000..7739250
--- /dev/null
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+
+{% block body %}
+       <h1>{{ _("Builds by %s") % escape(user.realname) }}</h1>
+
+       {% if builds %}
+
+               {% for order, items in sorted(builds.items()) %}
+                       {{ order }}
+
+                       {{ modules.BuildTable(items) }}
+               {% end %}
+
+       {% else %}
+
+               {{ _("No builds found matching your search criteria.") }}
+
+       {% end %}
+{% end block %}
+
index 432f6da3098e00b5e4dfd99f2d39d4f4b5036c1f..7d9c8167e866c4ee5e273dd2db6c290b16de664d 100644 (file)
                                </td>
                                <td class="col3">{{ _("Auto-detect will use the language transmitted by your browser.") }}</td>
                        </tr>
+                       <tr>
+                               <td class="col1">{{ _("Timezone") }}:</td>
+                               <td class="col2">
+                                       <select name="timezone">
+                                               {% for tz in supported_timezones %}
+                                                       <option value="{{ tz }}" {% if user.timezone.zone == tz %}selected{% end %}>{{ tz }}</option>
+                                               {% end %}
+                                       </select>
+                               </td>
+                               <td class="col3">{{ _("Auto-detect will use the language transmitted by your browser.") }}</td>
+                       </tr>
 
        {% if current_user.is_admin() and not current_user == user %}
                </table>
diff --git a/data/templates/user-profile-passwd-ok.html b/data/templates/user-profile-passwd-ok.html
new file mode 100644 (file)
index 0000000..aa34c1a
--- /dev/null
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Password changed") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/users">{{ _("Users") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/user/{{ escape(user.name) }}">{{ escape(user.realname) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/user/{{ escape(user.name) }}/passwd">{{ _("Change password") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       {{ _("Done!") }}
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Password changed") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       <div class="well">
+                               {% if current_user == user %}
+                                       <p>
+                                               {{ _("Your password has successfully been changed.") }}
+                                       </p>
+                               {% else %}
+                                       <p>
+                                               {{ _("The password of %s has successfully been changed.") % escape(user.realname) }}
+                                       </p>
+                               {% end %}
+
+                               <div class="pull-right">
+                                       <a class="btn btn-primary" href="/user/{{ escape(user.name) }}">{{ _("Ok") }}</a>
+                               </div>
+                       </div>
+               </div>
+       </div>
+{% end %}
diff --git a/data/templates/user-profile-passwd.html b/data/templates/user-profile-passwd.html
new file mode 100644 (file)
index 0000000..9e861bf
--- /dev/null
@@ -0,0 +1,102 @@
+{% extends "base.html" %}
+
+{% block title %}{{ _("Change password") }}{% end block %}
+
+{% block body %}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/users">{{ _("Users") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/user/{{ escape(user.name) }}">{{ escape(user.realname) }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/user/{{ escape(user.name) }}/passwd">{{ _("Change password") }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("Change password") }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       {% if user == current_user %}
+                               <p>                                     
+                                       {{ _("You are going to change your password.") }}
+                               </p>
+                               <p>
+                                       {{ _("To do so, you need to enter your current password and the new password twice.") }}
+                               </p>
+                       {% else %}
+                               <p>
+                                       {{ _("In this dialog, you may change the password of %s.") % escape(user.realname) }}
+                               </p>
+                       {% end %}
+                       <hr />
+               </div>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset3">
+                       {% if error_msg %}
+                               <div class="alert alert-block alert-important">
+                                       <a class="close" data-dismiss="alert">&times;</a>
+                                       <strong>{{ _("Oops!") }}</strong> {{ escape(error_msg) }}
+                               </div>
+                       {% end %}
+
+                       <form class="form-horizontal" method="POST" action="">
+                               {{ xsrf_form_html() }}
+
+                               <fieldset>
+                                       {% if user == current_user %}
+                                               <div class="control-group">
+                                                       <label class="control-label">{{ _("Old password") }}</label>
+                                                       <div class="controls">
+                                                               <input type="password" class="input-xlarge" name="pass0" />
+
+                                                               <p class="help-block">
+                                                                       {{ _("Please provide your old password.") }}
+                                                               </p>
+                                                       </div>
+                                               </div>
+                                       {% end %}
+
+                                       <div class="control-group">
+                                               <label class="control-label">{{ _("New password") }}</label>
+                                               <div class="controls">
+                                                       <input type="password" class="input-xlarge" name="pass1" />
+
+                                                       <p class="help-block">
+                                                               {{ _("Choose a new password. Make sure that it is as strong as possible.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="control-group">
+                                               <label class="control-label">{{ _("Confirm") }}</label>
+                                               <div class="controls">
+                                                       <input type="password" class="input-xlarge" name="pass2" />
+
+                                                       <p class="help-block">
+                                                               {{ _("Confirm the new password.") }}
+                                                       </p>
+                                               </div>
+                                       </div>
+
+                                       <div class="form-actions">
+                                               <button type="submit" class="btn btn-primary">{{ _("Change password") }}</button>
+                                               <a class="btn" href="/user/{{ escape(user.name) }}">{{ _("Cancel") }}</a>
+                                       </div>
+                               </fieldset>
+                       </form>
+               </div>
+       </div>
+{% end %}
index dc87da84e3f4f010c8ee10ddcc9bf6134db53fde..091a4f188fb88d01f4d3156352e0a65cca099710 100644 (file)
 {% extends "base.html" %}
 
+{% block title %}{{ escape(user.realname) }}{% end block %}
+
 {% block body %}
-       <img class="avatar" src="{{ user.gravatar_icon() }}" alt="{{ user.name }}" />
-       <h1>{{ _("User") }}: {{ escape(user.realname) }}</h1>
-
-       <table class="form form2">
-               <tr>
-                       <td class="col1">{{ _("Username") }}</td>
-                       <td class="col2">{{ escape(user.name) }}</td>
-               </tr>
-               <tr>
-                       <td class="col1">{{ _("Email") }}</td>
-                       <td class="col2">
-                               <a href="mailto:{{ escape(user.email) }}">{{ escape(user.email) }}</a>
-                       </td>
-               </tr>
-               <tr>
-                       <td class="col1">{{ _("State") }}</td>
-                       <td class="col2">
-                               {% if user.is_admin() %}
-                                       {{ _("Admin") }}
-                               {% elif user.is_tester() %}
-                                       {{ _("Tester") }}
+       <ul class="breadcrumb">
+               <li>
+                       <a href="/">{{ _("Home") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li>
+                       <a href="/users">{{ _("Users") }}</a>
+                       <span class="divider">/</span>
+               </li>
+               <li class="active">
+                       <a href="/user/{{ escape(user.name) }}">{{ escape(user.realname) }}</a>
+               </li>
+       </ul>
+
+       <div class="page-header">
+               <h1>{{ _("User") }}: {{ escape(user.realname) }}</h1>
+       </div>
+
+       <div class="row">
+               <div class="span6 offset2">
+                       <table class="table">
+                               <tbody>
+                                       <tr>
+                                               <td>{{ _("Username") }}</td>
+                                               <td>{{ escape(user.name) }}</td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("Email") }}</td>
+                                               <td>
+                                                       <a href="mailto:{{ escape(user.email) }}">{{ escape(user.email) }}</a>
+                                               </td>
+                                       </tr>
+                                       <tr>
+                                               <td>{{ _("State") }}</td>
+                                               <td>
+                                                       {% if user.is_admin() %}
+                                                               {{ _("Admin") }}
+                                                       {% elif user.is_tester() %}
+                                                               {{ _("Tester") }}
+                                                       {% else %}
+                                                               {{ _("User") }}
+                                                       {% end %}
+                                               </td>
+                                       </tr>
+
+                                       {% if current_user == user or current_user.is_admin() %}
+                                               <tr>
+                                                       <td>{{ _("Registered") }}</td>
+                                                       <td>
+                                                               {{ format_date(user.registered, full_format=True) }}
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+
+                                       {% if current_user == user or current_user.is_admin() %}
+                                               <tr>
+                                                       <td colspan="2">
+                                                               <div class="btn-group pull-right">
+                                                                       <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
+                                                                               {{ _("Action") }}
+                                                                               <span class="caret"></span>
+                                                                       </a>
+                                                                       <ul class="dropdown-menu">
+                                                                               <li>
+                                                                                       <a href="/user/{{ user.name }}/edit">
+                                                                                               <i class="icon-edit"></i>
+                                                                                               {{ _("Edit profile") }}
+                                                                                       </a>
+                                                                               </li>
+                                                                               <li>
+                                                                                       <a href="/user/{{ user.name }}/passwd">
+                                                                                               <i class="icon-lock"></i>
+                                                                                               {{ _("Change password") }}
+                                                                                       </a>
+                                                                               </li>
+
+                                                                               <li class="divider"></li>
+                                                                               <li>
+                                                                                       <a href="/user/{{ user.name }}/delete">
+                                                                                               <i class="icon-trash"></i>
+                                                                                               {{ _("Delete account") }}
+                                                                                       </a>
+                                                                               </li>
+
+                                                                               {% if not current_user == user and current_user.is_admin() %}
+                                                                                       <li class="divider"></li>
+                                                                                       <li>
+                                                                                               <a href="/user/impersonate?user={{ escape(user.name) }}">{{ _("Impersonate user") }}</a>
+                                                                                       </li>
+                                                                               {% end %}
+                                                                       </ul>
+                                                               </div>
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+                               </tbody>
+                       </table>
+               </div>
+
+               <div class="span4">
+                       <img src="{{ user.gravatar_icon(200) }}" alt="{{ user.name }}" />
+               </div>
+       </div>
+
+       {% if current_user == user or current_user.is_admin() %}
+               <div class="row">
+                       <div class="span6 offset2">
+                               <h2>{{ _("Permissions") }}</h2>
+
+                               {% if user.is_admin() %}                        
+                                       <p>
+                                               {{ _("This user has administration rights.") }}
+                                               <i class="icon-star"></i>
+                                       </p>
                                {% else %}
-                                       {{ _("User") }}
+                                       <table class="table table-striped">
+                                               <tr>
+                                                       <td>
+                                                               {{ _("User is allowed to create scratch builds?") }}
+                                                       </td>
+                                                       <td>
+                                                               {% if user.has_perm("create_scratch_builds") %}
+                                                                       {{ _("Yes") }}
+                                                               {% else %}
+                                                                       {{ _("No") }}
+                                                               {% end %}
+                                                       </td>
+                                               </tr>
+                                       </table>
                                {% end %}
-                       </td>
-               </tr>
-               {% if current_user == user or current_user.is_admin() %}
-                       <tr>
-                               <td class="col1">{{ _("Registered") }}</td>
-                               <td class="col2">{{ locale.format_date(user.registered, full_format=True) }}</td>
-                       </tr>
-               {% end %}
-       </table>
-       <div style="clear: both;">&nbsp;</div>
-
-       <h2>{{ _("Log") }}</h2>
-       {{ modules.LogTable(user.log) }}
-
-       <h2>{{ _("Comments written by %s") % user.realname }}</h2>
-       {{ modules.CommentsTable(user.comments, show_package=True, show_user=False) }}
-{% end block %}
+                       </div>
+               </div>
+       {% end %}
 
-{% block sidebar %}
-       <h1>{{ _("Actions") }}</h1>
-       <ul>
-               {% if current_user == user or current_user.is_admin() %}
-                       <li>
-                               <a href="/user/edit/{{ user.name }}">{{ _("Account settings") }}</a>
-                       </li>
-                       <li>
-                               <a href="/user/delete/{{ user.name }}">{{ _("Delete account") }}</a>
-                       </li>
-               {% end %}
-       </ul>
+       <!-- <h2>{{ _("Links") }}</h2>
+       <ul class="user-links">
+               <li>
+                       <a href="#">{{ _("View all comments written %s.") % escape(user.realname) }}</a>
+               </li>
+               <li>
+                       <a href="#">{{ _("View all builds %s is linked to.") % escape(user.realname) }}</a>
+               </li>
+       </ul> -->
+
+       <!-- <h2>{{ _("Log") }}</h2>
+       {{ modules.LogTable(user.log) }} -->
+
+       <!-- <h2>{{ _("Comments written by %s") % user.realname }}</h2>
+       {..{ modules.CommentsTable(user.comments, show_package=True, show_user=False) }..} -->
 {% end block %}
similarity index 67%
rename from master/__init__.py
rename to hub/__init__.py
index 063146fb782f65766e5e98babebc92f068390a58..946fe47587e30c271d181e46d75d5dc068face5c 100644 (file)
@@ -8,19 +8,20 @@ import tornado.options
 import tornado.web
 
 import backend
-
-from handlers import *
+import handlers
 
 BASEDIR = os.path.join(os.path.dirname(__file__), "..", "data")
 
 # Enable logging
 tornado.options.parse_command_line()
 
-class MasterApplication(tornado.web.Application):
+class Application(tornado.web.Application):
        def __init__(self):
+               self.__pakfire = None
+
                settings = dict(
-                       debug = True,
-                       gzip = False,
+                       debug = False,
+                       gzip  = True,
                )
 
                # Load translations.
@@ -29,15 +30,31 @@ class MasterApplication(tornado.web.Application):
 
                tornado.web.Application.__init__(self, **settings)
 
-               self.add_handlers(r".*", [
+               self.add_handlers(r"pakfirehub.ipfire.org", [
+                       # Redirect strayed users.
+                       (r"/", handlers.RedirectHandler),
+
                        # API
-                       (r"/api/builder/([A-Za-z0-9\-\.]+)/(\w+)", RPCBuilderHandler),
+                       (r"/builder", handlers.BuilderHandler),
+                       (r"/user",    handlers.UserHandler),
                ])
 
-               self.pakfire = backend.Pakfire()
+               # This is the deprecated version. It will be removed some time.
+               self.add_handlers(r"pakfire.ipfire.org", [
+                       # API
+                       (r"/pakfirehub/builder", handlers.BuilderHandler),
+                       (r"/pakfirehub/user",    handlers.UserHandler),
+               ])
 
                logging.info("Successfully initialied application")
 
+       @property
+       def pakfire(self):
+               if self.__pakfire is None:
+                       self.__pakfire = backend.Pakfire()
+
+               return self.__pakfire
+
        def __del__(self):
                logging.info("Shutting down application")
 
@@ -52,9 +69,6 @@ class MasterApplication(tornado.web.Application):
        def run(self, port=81):
                logging.debug("Going to background")
 
-               # All requests should be done after 30 seconds or they will be killed.
-               self.ioloop.set_blocking_log_threshold(30)
-
                http_server = tornado.httpserver.HTTPServer(self, xheaders=True)
 
                # If we are not running in debug mode, we can actually run multiple
@@ -65,6 +79,9 @@ class MasterApplication(tornado.web.Application):
                else:
                        http_server.listen(port)
 
+               # All requests should be done after 30 seconds or they will be killed.
+               self.ioloop.set_blocking_log_threshold(30)
+
                self.ioloop.start()
 
        def reload(self):
diff --git a/hub/handlers.py b/hub/handlers.py
new file mode 100644 (file)
index 0000000..af14ad3
--- /dev/null
@@ -0,0 +1,789 @@
+#!/usr/bin/python
+
+import base64
+import hashlib
+import logging
+import os
+import tornado.web
+import uuid
+import xmlrpclib
+
+import backend.builds
+from backend.builders import Builder
+from backend.builds import Build
+from backend.packages import Package
+from backend.uploads import Upload
+from backend.users import User
+
+class BaseHandler(tornado.web.RequestHandler):
+       """
+               Handler class that provides very basic things we will need.
+       """
+       @property
+       def pakfire(self):
+               """
+                       Reference to the Pakfire object.
+               """
+               return self.application.pakfire
+
+       @property
+       def remote_address(self):
+               """
+                       Returns the IP address the request came from.
+               """
+               remote_ips = self.request.remote_ip.split(", ")
+
+               return remote_ips[-1]
+
+
+class RedirectHandler(BaseHandler):
+       """
+               This handler redirects from the hub to the main website.
+       """
+       def get(self):
+               url = self.pakfire.settings.get("baseurl", None)
+
+               # If there was no URL in the database, we cannot do anything.
+               if not url:
+                       raise tornado.web.HTTPError(404)
+
+               self.redirect(url)
+
+# From: http://blog.joshmarshall.org/2009/10/its-a-twister-now-with-more-xml/
+#
+# This is just a very simple implementation from the website above, because
+# I badly want to run this software out of the box on any distribution.
+#
+
+def private(func):
+       # Decorator to make a method, well, private.
+       class PrivateMethod(object):
+               def __init__(self):
+                       self.private = True
+
+               __call__ = func
+
+       return PrivateMethod()
+
+
+class XMLRPCHandler(BaseHandler):
+       """
+               Subclass this to add methods -- you can treat them
+               just like normal methods, this handles the XML formatting.
+       """
+       def post(self):
+               """
+                       Later we'll make this compatible with "dot" calls like:
+                       server.namespace.method()
+                       If you implement this, make sure you do something proper
+                       with the Exceptions, i.e. follow the XMLRPC spec.
+               """
+               try:
+                       params, method_name = xmlrpclib.loads(self.request.body)
+               except:
+                       # Bad request formatting, bad.
+                       raise tornado.web.HTTPError(400)
+
+               if method_name in dir(tornado.web.RequestHandler):
+                       # Pre-existing, not an implemented attribute
+                       raise AttributeError('%s is not implemented.' % method_name)
+
+               try:
+                       method = getattr(self, method_name)
+               except:
+                       # Attribute doesn't exist
+                       print self
+                       raise AttributeError('%s is not a valid method.' % method_name)
+
+               if not callable(method):
+                       # Not callable, so not a method
+                       raise Exception('Attribute %s is not a method.' % method_name)
+
+               if method_name.startswith('_') or \
+                               ('private' in dir(method) and method.private is True):
+                       # No, no. That's private.
+                       raise Exception('Private function %s called.' % method_name)
+
+               response = method(*params)
+               response_xml = xmlrpclib.dumps((response,), methodresponse=True,
+                       allow_none=True)
+
+               self.set_header("Content-Type", "text/xml")
+               self.write(response_xml)
+
+
+class CommonHandler(XMLRPCHandler):
+       """
+               Subclass that provides very basic functions that do not need any
+               kind of authentication and are accessable by any user/builder.
+       """
+
+       def noop(self):
+               """
+                       No operation. Just check if the connection is working.
+               """
+               return True
+
+       def test_code(self, error_code=200):
+               """
+                       For testing a client.
+
+                       This just returns a HTTP response with the given code.
+               """
+               raise tornado.web.HTTPError(error_code)
+
+       def get_my_address(self):
+               """
+                       Return the address of the requesting host.
+
+                       This is to discover it through NAT.
+               """
+               return self.remote_address
+
+       def get_hub_status(self):
+               """
+                       Return some status information about the hub.
+               """
+
+               # Return number of pending and running builds.
+               ret = {
+                       "jobs_pending" : self.pakfire.jobs.count(state="pending"),
+                       "jobs_running" : self.pakfire.jobs.count(state="running"),
+               }
+
+               return ret
+
+
+class AuthHandler(CommonHandler):
+       def _auth(self, name, password):
+               raise NotImplementedError
+
+       def get_current_user(self):
+               """
+                       This handles HTTP Basic authentication.
+               """
+               auth_header = self.request.headers.get("Authorization", None)
+
+               # If no authentication information was provided, we stop here.
+               if not auth_header:
+                       return
+
+               # No basic auth? We cannot handle that.
+               if not auth_header.startswith("Basic "):
+                       raise tornado.web.HTTPError(400, "Can only handle Basic auth.")
+
+               # Decode the authentication information.
+               auth_header = base64.decodestring(auth_header[6:])
+
+               try:
+                       name, password = auth_header.split(":", 1)
+               except:
+                       raise tornado.web.HTTPError(400, "Authorization data was malformed")
+
+               # Authenticate user to the database.
+               return self._auth(name, password)
+
+
+class UserAuthMixin(object):
+       """
+               Mixin to authenticate users.
+       """
+       def _auth(self, username, password):
+               return self.pakfire.users.auth(username, password)
+
+       @property
+       def user(self):
+               """
+                       Alias for "current_user".
+               """
+               return self.current_user
+
+       @property
+       def builder(self):
+               return None
+
+       def check_auth(self):
+               """
+                       Tell the user if he authenticated successfully.
+               """
+               if self.user:
+                       return True
+
+               return False
+
+
+class BuilderAuthMixin(object):
+       """
+               Mixin to authenticate builders.
+       """
+       def _auth(self, hostname, password):
+               return self.pakfire.builders.auth(hostname, password)
+
+       @property
+       def builder(self):
+               """
+                       Alias for "current_user".
+               """
+               return self.current_user
+
+       @property
+       def user(self):
+               return None
+
+
+class CommonAuthHandler(AuthHandler):
+       """
+               Methods that are usable by both, the real users and the builders
+               but they require an authentication.
+       """
+       @tornado.web.authenticated
+       def build_create(self, upload_id, distro_ident, arches):
+               ## Check if the user has permission to create a build.
+               # Builders do have the permission to create all kinds of builds.
+               if isinstance(self.current_user, Builder):
+                       type = "release"
+                       check_for_duplicates = True
+               #
+               # Users only have the permission to create scratch builds.
+               elif isinstance(self.current_user, User) and \
+                               self.current_user.has_perm("create_scratch_builds"):
+                       type = "scratch"
+                       check_for_duplicates = False
+               #
+               # In all other cases, it is not allowed to proceed.
+               else:
+                       raise tornado.web.HTTPError(403, "Not allowed to create a build.")
+
+               # Get previously uploaded file to create this build from.
+               upload = self.pakfire.uploads.get_by_uuid(upload_id)
+               if not upload:
+                       raise tornado.web.HTTPError(400, "Upload does not exist: %s" % upload_id)
+
+               # Check if the uploaded file belongs to this user/builder.
+               if self.user and not upload.user == self.user:
+                       raise tornado.web.HTTPError(400, "Upload does not belong to this user.")
+
+               elif self.builder and not upload.builder == self.builder:
+                       raise tornado.web.HTTPError(400, "Upload does not belong to this builder.")
+
+               # Get distribution this package should be built for.
+               distro = self.pakfire.distros.get_by_ident(distro_ident)
+               if not distro:
+                       distro = self.pakfire.distros.get_default()
+
+               # Open the package that was uploaded earlier and add it to
+               # the database. Create a new build object from the uploaded package.
+               ret = backend.builds.import_from_package(self.pakfire, upload.path,
+                       distro=distro, type=type, arches=arches, owner=self.current_user,
+                       check_for_duplicates=check_for_duplicates)
+
+               if not ret:
+                       raise tornado.web.HTTPError(500, "Could not create build from package.")
+
+               # Creating the build will move the file to the build directory,
+               # so we can safely remove the uploaded file.
+               upload.remove()
+
+               # Return a bunch of information about the build back to the user.
+               pkg, build = ret
+
+               return build.info
+
+       # Upload processing.
+
+       @tornado.web.authenticated
+       def upload_create(self, filename, size, hash):
+               """
+                       Create a new upload object in the database and return a unique ID
+                       to the uploader.
+               """
+               upload = Upload.create(self.pakfire, filename, size, hash,
+                       user=self.user, builder=self.builder)
+
+               return upload.uuid
+
+       @tornado.web.authenticated
+       def upload_chunk(self, upload_id, data):
+               upload = self.pakfire.uploads.get_by_uuid(upload_id)
+               if not upload:
+                       raise tornado.web.HTTPError(404, "Invalid upload id.")
+
+               if not upload.builder == self.builder:
+                       raise tornado.web.HTTPError(403, "Uploading an other host's file.")
+
+               upload.append(data.data)
+
+       @tornado.web.authenticated
+       def upload_finished(self, upload_id):
+               upload = self.pakfire.uploads.get_by_uuid(upload_id)
+               if not upload:
+                       raise tornado.web.HTTPError(404, "Invalid upload id.")
+
+               if not upload.builder == self.builder:
+                       raise tornado.web.HTTPError(403, "Uploading an other host's file.")
+
+               # Validate the uploaded data to its hash.
+               ret = upload.validate()
+
+               # If the validation was successfull, we mark the upload
+               # as finished and send True to the client.
+               if ret:
+                       upload.finished()
+                       return True
+
+               # In case the download was corrupted or incomplete, we delete it
+               # and tell the client to start over.
+               upload.remove()
+               return False
+
+       @tornado.web.authenticated
+       def upload_remove(self, upload_id):
+               upload = self.pakfire.uploads.get_by_uuid(upload_id)
+               if not upload:
+                       raise tornado.web.HTTPError(404, "Invalid upload id.")
+
+               if not upload.builder == self.builder:
+                       raise tornado.web.HTTPError(403, "Removing an other host's file.")
+
+               # Remove the upload from the database and trash the data.
+               upload.remove()
+
+
+class UserHandler(UserAuthMixin, CommonAuthHandler):
+       """
+               Subclass with methods that are only accessable by users.
+       """
+       @tornado.web.authenticated
+       def get_user_profile(self):
+               """
+                       Send a bunch of account information to the user.
+               """
+               user = self.current_user
+
+               ret = {
+                       "name"       : user.name,
+                       "realname"   : user.realname,
+                       "role"       : user.state,
+                       "email"      : user.email,
+                       "registered" : user.registered,
+               }
+
+               return ret
+
+       @tornado.web.authenticated
+       def get_builds(self, type=None, limit=10, offset=0):
+               if not type in (None, "scratch", "release"):
+                       return
+
+               builds = self.pakfire.builds.get_by_user_iter(self.current_user, type=type)
+
+               try:
+                       counter = limit + offset
+               except ValueError:
+                       return []
+
+               ret = []
+               for build in builds:
+                       build = self.get_build(build.id)
+
+                       ret.append(build)
+
+                       counter -= 1
+                       if counter <= 0:
+                               break
+
+               return ret
+
+       @tornado.web.authenticated
+       def get_build(self, build_id):
+               # Check for empty input.
+               if not build_id:
+                       return None
+
+               build = self.pakfire.builds.get_by_uuid(build_id)
+               if not build:
+                       return {}
+
+               ret = {
+                       # Identity information.
+                       "uuid"         : build.uuid,
+                       "type"         : build.type,
+                       "state"        : build.state, # XXX do we actually use this?
+
+                       "name"         : build.name,
+                       "sup_arches"   : build.supported_arches,
+                       "jobs"         : [self.get_job(j.uuid) for j in build.jobs],
+
+                       "severity"     : build.severity,
+                       "priority"     : build.priority,
+
+                       # The source package of this build.
+                       "pkg_id"       : build.pkg.uuid,
+
+                       "distro"       : build.distro.id,
+                       "repo"         : None,
+
+                       "time_created" : build.created,
+                       "score"        : build.credits,
+               }
+
+               # If the build is in a repository, update that bit.
+               if build.repo:
+                       ret["repo"] = build.repo.id
+
+               return ret
+
+       @tornado.web.authenticated
+       def get_latest_jobs(self):
+               jobs = []
+
+               for job in self.pakfire.jobs.get_latest():
+                       job = self.get_job(job.uuid)
+                       if job:
+                               jobs.append(job)
+
+               return jobs
+
+       @tornado.web.authenticated
+       def get_active_jobs(self, host_id=None):
+               jobs = []
+
+               for job in self.pakfire.jobs.get_active(host_id=host_id):
+                       job = self.get_job(job.uuid)
+                       if job:
+                               jobs.append(job)
+
+               return jobs
+
+       @tornado.web.authenticated
+       def get_job(self, job_id):
+               job = self.pakfire.jobs.get_by_uuid(job_id)
+               if not job:
+                       return
+
+               # XXX check if user is allowed to view this job.
+
+               ret = {
+                       # Identity information.
+                       "uuid"          : job.uuid,
+                       "type"          : job.type,
+
+                       # Name, state, architecture.
+                       "name"          : job.name,
+                       "state"         : job.state,
+                       "arch"          : job.arch.name,
+
+                       # Information about the build this job lives in.
+                       "build_id"      : job.build.uuid,
+
+                       # The package that is built in this job.
+                       "pkg_id"        : job.pkg.uuid,
+                       "packages"      : [self.get_package(p.uuid) for p in job.packages],
+
+                       # The builder that builds this job.
+                       "builder_id"    : job.builder_id,
+
+                       # Time information.
+                       "duration"      : job.duration,
+                       "time_created"  : job.time_created,
+                       "time_started"  : job.time_started,
+                       "time_finished" : job.time_finished,
+               }
+
+               return ret
+
+       @tornado.web.authenticated
+       def get_builders(self):
+               builders = []
+
+               for builder in self.pakfire.builders.get_all():
+                       builder = self.get_builder(builder.id)
+                       if builder:
+                               builders.append(builder)
+
+               return builders
+
+       @tornado.web.authenticated
+       def get_builder(self, builder_id):
+               builder = self.pakfire.builders.get_by_id(builder_id)
+               if not builder:
+                       return
+
+               ret = {
+                       "name"          : builder.name,
+                       "description"   : builder.description,
+                       "state"         : builder.state,
+
+                       "arches"        : [a.name for a in builder.arches],
+                       "disabled"      : builder.disabled,
+
+                       "cpu_model"     : builder.cpu_model,
+                       "cpu_count"     : builder.cpu_count,
+                       "memory"        : builder.memory / 1024,
+
+                       "active_jobs"   : [j.uuid for j in builder.get_active_jobs()],
+               }
+
+               return ret
+
+       @tornado.web.authenticated
+       def get_package(self, pkg_id):
+               pkg = self.pakfire.packages.get_by_uuid(pkg_id)
+               if not pkg:
+                       return
+
+               ret = {
+                       "uuid"             : pkg.uuid,
+                       "name"             : pkg.name,
+                       "epoch"            : pkg.epoch,
+                       "version"          : pkg.version,
+                       "release"          : pkg.release,
+                       "arch"             : pkg.arch.name,
+                       "supported_arches" : pkg.supported_arches,
+                       "type"             : pkg.type,
+                       "friendly_name"    : pkg.friendly_name,
+                       "friendly_version" : pkg.friendly_version,
+                       "groups"           : pkg.groups,
+                       "license"          : pkg.license,
+                       "url"              : pkg.url,
+                       "summary"          : pkg.summary,
+                       "description"      : pkg.description,
+
+                       "size"             : pkg.size,
+                       "filesize"         : pkg.filesize,
+                       "hash_sha512"      : pkg.hash_sha512,
+
+                       # Dependencies.
+                       "prerequires"      : pkg.prerequires,
+                       "requires"         : pkg.requires,
+                       "provides"         : pkg.provides,
+                       "obsoletes"        : pkg.obsoletes,
+                       "conflicts"        : pkg.conflicts,
+
+                       # Build infos.
+                       "build_id"         : pkg.build_id,
+                       "build_host"       : pkg.build_host,
+                       "build_time"       : pkg.build_time,
+               }
+
+               if isinstance(pkg.maintainer, User):
+                       ret["maintainer"] = "%s <%s>" % (pkg.maintainer.realname, pkg.maintainer.email)
+               elif pkg.maintainer:
+                       ret["maintainer"] = pkg.maintainer
+
+               if pkg.distro:
+                       ret["distro_id"] = pkg.distro.id
+               else:
+                       ret["distro_id"] = None
+
+               return ret
+
+
+class BuilderHandler(BuilderAuthMixin, CommonAuthHandler):
+       """
+               Subclass with methods that are only accessable by builders.
+       """
+       @tornado.web.authenticated
+       def send_keepalive(self, loadavg, overload, free_space=None):
+               """
+                       The client just says hello and we tell it if we it needs to
+                       send some information about itself.
+               """
+               self.builder.update_keepalive(loadavg, free_space)
+
+               # Pass overload argument.
+               if overload in (True, False):
+                       self.builder.update_overload(overload)
+
+               # Tell the client if it should send an update of its infos.
+               return self.builder.needs_update()
+
+       @tornado.web.authenticated
+       def send_update(self, arches, cpu_model, cpu_count, memory, pakfire_version=None, host_key_id=None):
+               self.builder.update_info(arches, cpu_model, cpu_count, memory * 1024,
+                       pakfire_version=pakfire_version, host_key_id=host_key_id)
+
+       @tornado.web.authenticated
+       def build_get_job(self, arches):
+               # Disabled buildes do not get any jobs.
+               if self.builder.disabled:
+                       logging.debug("Host requested job but is disabled: %s" \
+                               % self.builder.name)
+                       return
+
+               # So do hosts where the metadata is not up to date.
+               #if self.builder.needs_update():
+               #       logging.debug("Host requested job but needs metadata update: %s" \
+               #               % self.builder.name)
+               #       return
+
+               # Check if host has already too many simultaneous jobs.
+               if len(self.builder.get_active_jobs(uploads=False)) >= self.builder.max_jobs:
+                       logging.debug("Host has already too many jobs: %s" % \
+                               self.builder.name)
+                       return
+
+               # Automatically add noarch if not already present.
+               if not "noarch" in arches:
+                       arches.append("noarch")
+
+               # Get all supported architectures.
+               supported_arches = []
+               for arch_name in arches:
+                       arch = self.pakfire.arches.get_by_name(arch_name)
+                       if not arch:
+                               logging.debug("Unsupported architecture: %s" % arch_name)
+                               continue
+
+                       # Skip disabled arches.
+                       if arch in self.builder.disabled_arches:
+                               continue
+
+                       supported_arches.append(arch)
+
+               if not supported_arches:
+                       logging.warning("Host does not support any arches: %s" % \
+                               self.builder.name)
+                       return
+
+               # Get all jobs from the database that can be built by this host.
+               jobs = self.pakfire.jobs.get_next_iter(states=["pending"],
+                       arches=supported_arches)
+
+               job = None
+               for _job in jobs:
+                       # Skip jobs that should not be built here.
+                       if _job.type == "test" and not "test" in self.builder.build_types:
+                               continue
+
+                       if not _job.build.type in self.builder.build_types:
+                               continue
+
+                       job = _job
+                       break
+
+               if not job:
+                       logging.debug("Could not find a buildable job for %s" % self.builder.name)
+                       return
+
+               # We got a buildable job, so let's start...
+               logging.debug("%s is going to build %s" % (self.builder.name, job))
+               build = job.build
+
+               try:
+                       # Set job to dispatching state.
+                       job.state = "dispatching"
+
+                       # Set our build host.
+                       job.builder = self.builder
+
+                       ret = {
+                               "id"                 : job.uuid,
+                               "arch"               : job.arch.name,
+                               "source_url"         : build.source_download,
+                               "source_hash_sha512" : build.source_hash_sha512,
+                               "type"               : job.type,
+                               "config"             : job.get_config(),
+                       }
+
+                       # Send build information to the builder.
+                       return ret
+
+               except:
+                       # If anything went wrong, we reset the state.
+                       job.state = "pending"
+                       raise
+
+       def build_job_update_state(self, job_id, state, message=None):
+               job = self.pakfire.jobs.get_by_uuid(job_id)
+               if not job:
+                       raise tornado.web.HTTPError(404, "Invalid job id.")
+
+               if not job.builder == self.builder:
+                       raise tornado.web.HTTPError(403, "Altering another builder's build.")
+
+               # Save information to database.
+               job.state = state
+               job.update_message(message)
+
+               return True
+
+       def build_job_add_file(self, job_id, upload_id, type):
+               assert type in ("package", "log")
+
+               # Fetch job we are working on and check if it is actually ours.
+               job = self.pakfire.jobs.get_by_uuid(job_id)
+               if not job:
+                       raise tornado.web.HTTPError(404, "Invalid job id.")
+
+               if not job.builder == self.builder:
+                       raise tornado.web.HTTPError(403, "Altering another builder's job.")
+
+               # Fetch uploaded file object and check we uploaded it ourself.
+               upload = self.pakfire.uploads.get_by_uuid(upload_id)
+               if not upload:
+                       raise tornado.web.HTTPError(404, "Invalid upload id.")
+
+               if not upload.builder == self.builder:
+                       raise tornado.web.HTTPError(403, "Using an other host's file.")
+
+               # Remove all files that have to be deleted, first.
+               self.pakfire.cleanup_files()
+
+               try:
+                       job.add_file(upload.path)
+
+               finally:
+                       # Finally, remove the uploaded file.
+                       upload.remove()
+
+               return True
+
+       def build_job_crashed(self, job_id, exitcode):
+               job = self.pakfire.jobs.get_by_uuid(job_id)
+               if not job:
+                       raise tornado.web.HTTPError(404, "Invalid job id.")
+
+               if not job.builder == self.builder:
+                       raise tornado.web.HTTPError(403, "Altering another builder's build.")
+
+               # Set build into aborted state.
+               job.state = "aborted"
+
+               # Set aborted state.
+               job.aborted_state = exitcode
+
+       def build_jobs_aborted(self, job_ids):
+               """
+                       Returns all aborted job ids from the input list.
+               """
+               aborted_jobs = []
+
+               for job_id in job_ids:
+                       job = self.pakfire.jobs.get_by_uuid(job_id)
+                       if not job:
+                               logging.debug("Unknown job id: %s" % job_id)
+                               continue
+
+                       # Check if we own this job.
+                       if not job.builder == self.builder:
+                               logging.debug("Job %s belongs to another builder." % job_id)
+                               continue
+
+                       if job.state == "aborted":
+                               aborted_jobs.append(job.uuid)
+
+               return aborted_jobs
+
+       def build_upload_buildroot(self, job_id, pkgs):
+               """
+                       Saves the buildroot the builder sends.
+               """
+               job = self.pakfire.jobs.get_by_uuid(job_id)
+               if not job:
+                       raise tornado.web.HTTPError(404, "Invalid job id.")
+
+               if not job.builder == self.builder:
+                       raise tornado.web.HTTPError(403, "Altering another builder's build.")
+
+               job.save_buildroot(pkgs)
diff --git a/master/handlers.py b/master/handlers.py
deleted file mode 100644 (file)
index e8a1fcb..0000000
+++ /dev/null
@@ -1,316 +0,0 @@
-#!/usr/bin/python
-
-import hashlib
-import logging
-import os
-import tornado.web
-import uuid
-import xmlrpclib
-
-from backend.build import BinaryBuild, SourceBuild
-from backend.packages import Package
-
-class BaseHandler(tornado.web.RequestHandler):
-       @property
-       def pakfire(self):
-               return self.application.pakfire
-
-       # XXX should not be needed
-       #@property
-       #def db(self):
-       #       return self.application.pakfire.db
-
-
-# From: http://blog.joshmarshall.org/2009/10/its-a-twister-now-with-more-xml/
-#
-# This is just a very simple implementation from the website above, because
-# I badly want to run this software out of the box on any distribution.
-#
-
-def private(func):
-       # Decorator to make a method, well, private.
-       class PrivateMethod(object):
-               def __init__(self):
-                       self.private = True
-
-               __call__ = func
-
-       return PrivateMethod()
-
-
-class XMLRPCHandler(BaseHandler):
-       """
-               Subclass this to add methods -- you can treat them
-               just like normal methods, this handles the XML formatting.
-       """
-       def post(self):
-               """
-                       Later we'll make this compatible with "dot" calls like:
-                       server.namespace.method()
-                       If you implement this, make sure you do something proper
-                       with the Exceptions, i.e. follow the XMLRPC spec.
-               """
-               try:
-                       params, method_name = xmlrpclib.loads(self.request.body)
-               except:
-                       # Bad request formatting, bad.
-                       raise tornado.web.HTTPError(400)
-
-               if method_name in dir(tornado.web.RequestHandler):
-                       # Pre-existing, not an implemented attribute
-                       raise AttributeError('%s is not implemented.' % method_name)
-
-               try:
-                       method = getattr(self, method_name)
-               except:
-                       # Attribute doesn't exist
-                       raise AttributeError('%s is not a valid method.' % method_name)
-
-               if not callable(method):
-                       # Not callable, so not a method
-                       raise Exception('Attribute %s is not a method.' % method_name)
-
-               if method_name.startswith('_') or \
-                               ('private' in dir(method) and method.private is True):
-                       # No, no. That's private.
-                       raise Exception('Private function %s called.' % method_name)
-
-               response = method(*params)
-               response_xml = xmlrpclib.dumps((response,), methodresponse=True,
-                       allow_none=True)
-
-               self.set_header("Content-Type", "text/xml")
-               self.write(response_xml)
-
-
-class AuthXMLRPCHandler(XMLRPCHandler):
-       """
-               This handler forces the host to authenticate against the server.
-
-               All methods of this class can be sure that they receive 100% okay data.
-       """
-       def post(self, hostname, passphrase):
-               # Get the builder from the database.
-               self.builder = self.pakfire.builders.get_by_name(hostname)
-               if not self.builder:
-                       raise tornado.web.HTTPError(403)
-
-               # Check if the passphrase matches and return 403 Forbidden if
-               # the authentication data is invalid.
-               if not self.builder.validate_passphrase(passphrase):
-                       raise tornado.web.HTTPError(403)
-
-               # Parse the actual request.
-               XMLRPCHandler.post(self)
-
-
-class RPCBaseHandler(AuthXMLRPCHandler):
-       @staticmethod
-       def chunkPath(id):
-               return os.path.join("/var/tmp/pakfire-upload-%s" % id)
-
-       def get_upload_cookie(self, filename, size, hash):
-               """
-                       Create a new upload object in the database and return a unique ID
-                       to the uploader.
-               """
-               upload = self.pakfire.uploads.new(self.builder, filename, size, hash)
-
-               return upload.uuid
-
-       def upload_chunk(self, upload_id, data):
-               upload = self.pakfire.uploads.get_by_uuid(upload_id)
-               if not upload:
-                       raise tornado.web.HTTPError(404, "Invalid upload id.")
-
-               if not upload.builder == self.builder:
-                       raise tornado.web.HTTPError(403, "Uploading an other hosts file.")
-
-               upload.append(data.data)
-
-       def finish_upload(self, upload_id, build_id):
-               upload = self.pakfire.uploads.get_by_uuid(upload_id)
-               if not upload:
-                       raise tornado.web.HTTPError(404, "Invalid upload id.")
-
-               # Get the corresponding build (needed for arch, etc.)
-               build = self.pakfire.builds.get_by_uuid(build_id)
-               if not build:
-                       raise tornado.web.HTTPError(400, "Invalid build id.")
-
-               # Validate the uploaded data to its hash.
-               ret = upload.validate()
-
-               # If the hash does not match, we delete the upload.
-               if not ret:
-                       upload.remove()
-                       return ret
-
-               # Save the file to its designated place.
-               upload.commit(build)
-
-               # Send the validation result to the uploader.
-               return ret
-
-       def chunk_upload(self, id, hash, data):
-               if not id:
-                       id = "%s" % uuid.uuid4()
-
-               # Get the filename of the upload.
-               filename = self.chunkPath(id)
-
-               # Extract data.
-               data = data.data
-
-               # Check the data integrity.
-               if not hash == hashlib.sha1(data).hexdigest():
-                       raise Exception, "Chunk was corrupted"
-
-               # Write the data to file.
-               f = open(filename, "a")
-               f.write(data)
-               f.close()
-
-               # Return the ID to add more chunks to the data.
-               return id
-
-       def package_add_file(self, pkg_id, file_id, info):
-               pkg = self.pakfire.packages.get_by_id(pkg_id)
-
-               filename = self.chunkPath(file_id)
-               if not os.path.exists(filename):
-                       raise Exception, "Chunk file not found"
-
-               return pkg.add_file(filename, info)
-
-       def package_add(self, info):
-               pkg = self.pakfire.packages.get_by_tuple(info.get("name"), info.get("epoch"),
-                       info.get("version"), info.get("release"))
-
-               if pkg:
-                       logging.debug("Package does already exist: %s" % pkg)
-                       return pkg.id
-
-               pkg = Package.new(self.pakfire, info)
-
-               return pkg.id
-
-       def build_add_log(self, build_id, file_id):
-               build = self.pakfire.builds.get_by_uuid(build_id)
-
-               filename = self.chunkPath(file_id)
-               if not os.path.exists(filename):
-                       raise Exception, "Chunk file not found"
-
-               build.add_log(filename)
-
-               return True
-
-
-class RPCBuilderHandler(RPCBaseHandler):
-       def update_host_info(self, loadavg, cpu_model, memory, arches):
-               """
-                       Receive detailed host information and store it to the database.
-               """
-               self.builder.update_info(loadavg, cpu_model, memory, arches)
-
-       def update_build_state(self, build_id, state, message):
-               build = self.pakfire.builds.get_by_uuid(build_id)
-               if not build:
-                       return
-
-               # Save information to database.
-               build.state, build.message = state, message
-
-               return True
-
-       def build_job(self, type=None):
-               if self.builder.disabled:
-                       logging.warning("Disabled builder wants to get a job: %s" % \
-                               self.builder.hostname)
-                       return
-
-               # XXX need to handle type here
-
-               # Check if host has already enough running build jobs.
-               if len(self.builder.active_builds) >= self.builder.max_jobs:
-                       return
-
-               # Determine what kind of builds the host should get.
-               build = None
-               if self.builder.build_src:
-                       # Source builds are preferred over binary builds if the host does
-                       # support this.
-
-                       # If there is not already a source job running on this host, we
-                       # can grab a new one.
-                       if not "source" in [b.type for b in self.builder.active_builds]:
-                               build = self.pakfire.builds.get_next(type="source", limit=1)
-
-               if not build and self.builder.build_bin:
-                       # If the host does not support source builds or there are no source
-                       # builds to do, we try to grab a binary build in a supported arch.
-                       build = self.pakfire.builds.get_next(type="binary", limit=1,
-                               arches=self.builder.arches)
-
-               # If there is no build that we can do, we can skip the rest.
-               if not build:
-                       return
-
-               try:
-                       # Set build to be dispatching that it won't be taken by another
-                       # host.
-                       build.state = "dispatching"
-
-                       # Assign the build job to the host that requested this.
-                       build.host = self.builder.hostname
-
-                       if build.type == "source":
-                               # The source build job build job is immediately changed to running
-                               # state.
-                               return {
-                                       "type"      : "source",
-                                       "id"        : build.uuid,
-                                       "revision"  : build.revision,
-                                       "source"    : build.source.info,
-                               }
-
-                       elif build.type == "binary":
-                               # Get source package.
-                               source = build.pkg.sourcefile
-                               assert source
-
-                               return {
-                                       "type"      : "binary",
-                                       "arch"      : build.arch,
-                                       "id"        : build.uuid,
-                                       "pkg_id"    : build.pkg_id,
-                                       "source_id" : source.source.id,
-                                       "name"      : source.name,
-                                       "download"  : source.download,
-                                       "hash1"     : source.hash1,
-                               }
-               except:
-                       # If there has been any error, we reset the build.
-                       build.state = "pending"
-
-       def get_repos(self, limit=None):
-               repos = self.pakfire.repos.get_needs_update(limit=limit)
-
-               # XXX disabled for testing
-               #for repo in repos:
-               #       repo.needs_update = False
-
-               return [r.info for r in repos]
-
-       def get_repo_packages(self, repo_id):
-               repo = self.pakfire.repos.get_by_id(repo_id)
-
-               if not repo:
-                       return
-
-               pkgs = []
-               for pkg in repo.get_packages():
-                       pkgs += [f.abspath for f in pkg.packagefiles]
-
-               return pkgs
diff --git a/pakfire-hub b/pakfire-hub
new file mode 100644 (file)
index 0000000..21d2836
--- /dev/null
@@ -0,0 +1,7 @@
+#!/usr/bin/python
+
+import hub
+
+app = hub.Application()
+app.run()
+
diff --git a/pakfire-master b/pakfire-master
deleted file mode 100644 (file)
index 07fca3b..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/python
-
-from master import MasterApplication
-
-app = MasterApplication()
-
-app.run()
index 093e11e98b6164679be6514fd838940efd982459..1c09dd43bd6e089330fb55312b6ad7487811b41b 100644 (file)
@@ -17,26 +17,49 @@ tornado.options.parse_command_line()
 
 class Application(tornado.web.Application):
        def __init__(self):
+               self.__pakfire = None
+
                settings = dict(
-                       cookie_secret = "12345",
                        debug = True,
-                       gzip = True,
+                       gzip  = False,
                        login_url = "/login",
                        template_path = os.path.join(BASEDIR, "templates"),
                        ui_modules = {
-                               "BuildLog"        : BuildLogModule,
-                               "BuildOffset"     : BuildOffsetModule,
-                               "BuildTable"      : BuildTableModule,
-                               "CommentsTable"   : CommentsTableModule,
-                               "FilesTable"      : FilesTableModule,
-                               "LogTable"        : LogTableModule,
-                               "PackageTable"    : PackageTableModule,
-                               "PackageTable2"   : PackageTable2Module,
-                               "PackageFilesTable" : PackageFilesTableModule,
-                               "RepositoryTable" : RepositoryTableModule,
-                               "RepoActionsTable": RepoActionsTableModule,
-                               "SourceTable"     : SourceTableModule,
-                               "UsersTable"      : UsersTableModule,
+                               "Text"               : TextModule,
+                               "Modal"              : ModalModule,
+
+                               "Footer"             : FooterModule,
+
+                               # Logging
+                               "Log"                : LogModule,
+                               "LogEntry"           : LogEntryModule,
+                               "LogEntryComment"    : LogEntryCommentModule,
+
+                               "BuildHeadline"      : BuildHeadlineModule,
+                               "BuildStateWarnings" : BuildStateWarningsModule,
+
+                               "BugsTable"          : BugsTableModule,
+                               "BuildLog"           : BuildLogModule,
+                               "BuildOffset"        : BuildOffsetModule,
+                               "BuildTable"         : BuildTableModule,
+                               "CommitsTable"       : CommitsTableModule,
+                               "JobsTable"          : JobsTableModule,
+                               "JobsList"           : JobsListModule,
+                               "CommentsTable"      : CommentsTableModule,
+                               "FilesTable"         : FilesTableModule,
+                               "LogTable"           : LogTableModule,
+                               "LogFilesTable"      : LogFilesTableModule,
+                               "Maintainer"         : MaintainerModule,
+                               "PackagesTable"      : PackagesTableModule,
+                               "PackageTable2"      : PackageTable2Module,
+                               "PackageHeader"      : PackageHeaderModule,
+                               "PackageFilesTable"  : PackageFilesTableModule,
+                               "RepositoryTable"    : RepositoryTableModule,
+                               "RepoActionsTable"   : RepoActionsTableModule,
+                               "SourceTable"        : SourceTableModule,
+                               "UpdatesTable"       : UpdatesTableModule,
+                               "UsersTable"         : UsersTableModule,
+                               "WatchersSidebarTable" : WatchersSidebarTableModule,
                        },
                        xsrf_cookies = True,
                )
@@ -58,71 +81,146 @@ class Application(tornado.web.Application):
                        # Entry site that lead the user to index
                        (r"/", IndexHandler),
 
+                       # Advanced options for logged in users.
+                       (r"/advanced", AdvancedHandler),
+
                        # Handle all the users logins/logouts/registers and stuff.
                        (r"/login", LoginHandler),
                        (r"/logout", LogoutHandler),
                        (r"/register", RegisterHandler),
+                       (r"/password-recovery", PasswordRecoveryHandler),
+
+                       # User profiles
                        (r"/users", UsersHandler),
                        (r"/users/comments", UsersCommentsHandler),
-                       (r"/user/delete/(\w+)", UserDeleteHandler),
-                       (r"/user/edit/(\w+)", UserEditHandler),
+                       (r"/user/impersonate", UserImpersonateHandler),
+                       (r"/user/(\w+)/passwd", UserPasswdHandler),
+                       (r"/user/(\w+)/delete", UserDeleteHandler),
+                       (r"/user/(\w+)/edit", UserEditHandler),
+                       (r"/user/(\w+)/activate", ActivationHandler),
                        (r"/user/(\w+)", UserHandler),
-                       (r"/user/(\w+)/activate/(\w+)", ActivationHandler),
                        (r"/profile", UserHandler),
+                       (r"/profile/builds", UsersBuildsHandler),
 
                        # Packages
                        (r"/packages", PackageListHandler),
-                       (r"/package/([\w\-\+]+)/([\d]+)/([\w\.\-]+)/([\w\.]+)", PackageDetailHandler),
+                       (r"/package/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})", PackageDetailHandler),
+                       (r"/package/([\w\-\+]+)/properties", PackagePropertiesHandler),
                        (r"/package/([\w\-\+]+)", PackageNameHandler),
 
                        # Files
                        (r"/file/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})", FileDetailHandler),
 
                        # Builds
-                       (r"/builds", BuildListHandler),
+                       (r"/builds", BuildsHandler),
                        (r"/builds/filter", BuildFilterHandler),
+                       (r"/builds/queue", BuildQueueHandler),
                        (r"/build/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})", BuildDetailHandler),
-                       (r"/build/priority/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})", BuildPriorityHandler),
-                       (r"/build/schedule/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})", BuildScheduleHandler),
+                       (r"/build/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/bugs", BuildBugsHandler),
+                       (r"/build/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/manage", BuildManageHandler),
+                       (r"/build/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/comment", BuildDetailCommentHandler),
+                       (r"/build/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/priority", BuildPriorityHandler),
+                       (r"/build/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/state", BuildStateHandler),
+                       (r"/build/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/watch", BuildWatchersAddHandler),
+                       (r"/build/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/watchers", BuildWatchersHandler),
+                       (r"/build/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/delete", BuildDeleteHandler),
+
+                       # Jobs
+                       (r"/job/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})", JobDetailHandler),
+                       (r"/job/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/abort", JobAbortHandler),
+                       (r"/job/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/buildroot", JobBuildrootHandler),
+                       (r"/job/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/schedule", JobScheduleHandler),
 
                        # Builders
                        (r"/builders", BuilderListHandler),
                        (r"/builder/new", BuilderNewHandler),
-                       (r"/builder/delete/([A-Za-z0-9\-\.]+)", BuilderDeleteHandler),
-                       (r"/builder/edit/([A-Za-z0-9\-\.]+)", BuilderEditHandler),
-                       (r"/builder/renew/([A-Za-z0-9\-\.]+)", BuilderRenewPassphraseHandler),
+                       (r"/builder/([A-Za-z0-9\-\.]+)/enable", BuilderEnableHander),
+                       (r"/builder/([A-Za-z0-9\-\.]+)/disable", BuilderDisableHander),
+                       (r"/builder/([A-Za-z0-9\-\.]+)/delete", BuilderDeleteHandler),
+                       (r"/builder/([A-Za-z0-9\-\.]+)/edit", BuilderEditHandler),
+                       (r"/builder/([A-Za-z0-9\-\.]+)/renew", BuilderRenewPassphraseHandler),
                        (r"/builder/([A-Za-z0-9\-\.]+)", BuilderDetailHandler),
 
-                       # Sources
-                       (r"/sources", SourceListHandler),
-                       (r"/source/([0-9]+)", SourceDetailHandler),
-
                        # Distributions
-                       (r"/distributions", DistributionListHandler),
+                       (r"/distros", DistributionListHandler),
+                       (r"/distro/([A-Za-z0-9\-\.]+)", DistributionDetailHandler),
+
+                       # XXX THOSE URLS ARE DEPRECATED
                        (r"/distribution/delete/([A-Za-z0-9\-\.]+)", DistributionDetailHandler),
                        (r"/distribution/edit/([A-Za-z0-9\-\.]+)", DistributionEditHandler),
-                       (r"/distribution/([A-Za-z0-9\-\.]+)", DistributionDetailHandler),
-                       (r"/distribution/([A-Za-z0-9\-\.]+)/repository/([A-Za-z0-9\-\.]+)", RepositoryDetailHandler),
+
+                       (r"/distro/([A-Za-z0-9\-\.]+)/repo/([A-Za-z0-9\-]+)",
+                               RepositoryDetailHandler),
+                       (r"/distro/([A-Za-z0-9\-\.]+)/repo/([A-Za-z0-9\-]+)\.repo",
+                               RepositoryConfHandler),
+                       (r"/distro/([A-Za-z0-9\-\.]+)/repo/([A-Za-z0-9\-]+)/mirrorlist",
+                               RepositoryMirrorlistHandler),
+                       (r"/distro/([A-Za-z0-9\-\.]+)/repo/([A-Za-z0-9\-]+)/edit",
+                               RepositoryEditHandler),
+
+                       (r"/distro/([A-Za-z0-9\-\.]+)/source/([A-Za-z0-9\-\.]+)",
+                               DistroSourceDetailHandler),
+                       (r"/distro/([A-Za-z0-9\-\.]+)/source/([A-Za-z0-9\-\.]+)/commits",
+                               DistroSourceCommitsHandler),
+                       (r"/distro/([A-Za-z0-9\-\.]+)/source/([A-Za-z0-9\-\.]+)/([\w]{40})",
+                               DistroSourceCommitDetailHandler),
+                       (r"/distro/([A-Za-z0-9\-\.]+)/source/([A-Za-z0-9\-\.]+)/([\w]{40})/reset",
+                               DistroSourceCommitResetHandler),
+
+                       (r"/distro/([A-Za-z0-9\-\.]+)/update/create",
+                               DistroUpdateCreateHandler),
+                       (r"/distro/([A-Za-z0-9\-\.]+)/update/(\d+)/(\d+)",
+                               DistroUpdateDetailHandler),
+
+                       # Updates
+                       (r"/updates", UpdatesHandler),
+
+                       # Mirrors
+                       (r"/mirrors", MirrorListHandler),
+                       (r"/mirror/new", MirrorNewHandler),
+                       (r"/mirror/([A-Za-z0-9\-\.]+)/delete", MirrorDeleteHandler),
+                       (r"/mirror/([A-Za-z0-9\-\.]+)/edit", MirrorEditHandler),
+                       (r"/mirror/([A-Za-z0-9\-\.]+)", MirrorDetailHandler),
+
+                       # Key management
+                       (r"/keys", KeysListHandler),
+                       (r"/key/import", KeysImportHandler),
+                       (r"/key/([A-Z0-9]+)", KeysDownloadHandler),
+                       (r"/key/([A-Z0-9]+)/delete", KeysDeleteHandler),
+
+                       # Statistics
+                       (r"/statistics", StatisticsMainHandler),
 
                        # Documents
                        (r"/documents", DocsIndexHandler),
                        (r"/documents/builds", DocsBuildsHandler),
                        (r"/documents/users", DocsUsersHandler),
+                       (r"/documents/what-is-the-pakfire-build-service", DocsWhatsthisHandler),
 
                        # Search
                        (r"/search", SearchHandler),
 
-                       # API
-                       (r"/api/action/(\w+)", RepoActionHandler),
+                       # Uploads
+                       (r"/uploads", UploadsHandler),
 
                        # Log
                        (r"/log", LogHandler),
-               ] + static_handlers)
 
-               self.pakfire = backend.Pakfire()
+               ] + static_handlers + [
+
+                       # Everything else is catched by the 404 handler.
+                       (r"/.*", Error404Handler),
+               ])
 
                logging.info("Successfully initialied application")
 
+       @property
+       def pakfire(self):
+               if self.__pakfire is None:
+                       self.__pakfire = backend.Pakfire()
+
+               return self.__pakfire
+
        def __del__(self):
                logging.info("Shutting down application")
 
@@ -137,9 +235,6 @@ class Application(tornado.web.Application):
        def run(self, port=80):
                logging.debug("Going to background")
 
-               # All requests should be done after 30 seconds or they will be killed.
-               self.ioloop.set_blocking_log_threshold(30)
-
                http_server = tornado.httpserver.HTTPServer(self, xheaders=True)
 
                # If we are not running in debug mode, we can actually run multiple
@@ -150,6 +245,9 @@ class Application(tornado.web.Application):
                else:
                        http_server.listen(port)
 
+               # All requests should be done after 60 seconds or they will be killed.
+               self.ioloop.set_blocking_log_threshold(60)
+
                self.ioloop.start()
 
        def reload(self):
index d0828c3c734d0db5a905cb80a11e5d33a1cab6a2..8fe48a181884d91f085c24b4843e0fd443becf47 100644 (file)
@@ -4,190 +4,82 @@ import tornado.web
 
 from handlers_auth import *
 from handlers_base import *
+from handlers_builds import *
 from handlers_builders import *
+from handlers_distro import *
+from handlers_jobs import *
+from handlers_keys import *
+from handlers_mirrors import *
+from handlers_packages import *
 from handlers_search import *
+from handlers_updates import *
 from handlers_users import *
 
 class IndexHandler(BaseHandler):
        def get(self):
-               active_builds = self.pakfire.builds.get_active()
-               latest_builds = self.pakfire.builds.get_latest(limit=10)
-               next_builds   = self.pakfire.builds.get_next(limit=10)
+               kwargs = {
+                       "active_jobs" : self.pakfire.jobs.get_active(),
+                       "latest_jobs" : self.pakfire.jobs.get_latest(limit=10),
 
-               # Get counters
-               counter_pending = self.pakfire.builds.count(state="pending")
+                       "job_queue" : self.pakfire.jobs.count("pending"),
+               }
 
-               average_build_time = self.pakfire.builds.average_build_time()
+               # Updates
+               updates = []
+               active = True
+               for type in ("stable", "unstable", "testing"):
+                       u = self.pakfire.updates.get_latest(type=type)
+                       if u:
+                               updates.append((type, u, active))
+                               active = False
 
-               self.render("index.html", latest_builds=latest_builds,
-                       active_builds=active_builds, next_builds=next_builds,
-                       counter_pending=counter_pending, average_build_time=average_build_time)
+               kwargs["updates"] = updates
 
+               self.render("index.html", **kwargs)
 
-class PackageIDDetailHandler(BaseHandler):
-       def get(self, id):
-               package = self.packages.get_by_id(id)
-               if not package:
-                       return tornado.web.HTTPError(404, "Package not found: %s" % id)
 
-               self.render("package-detail.html", package=package)
-
-
-class PackageListHandler(BaseHandler):
+class Error404Handler(BaseHandler):
        def get(self):
-               packages = {}
-
-               # Sort all packages in an array like "<first char>" --> [packages, ...]
-               # to print them in a table for each letter of the alphabet.
-               for pkg in self.pakfire.packages.get_all_names():
-                       c = pkg[0].lower()
-
-                       if not packages.has_key(c):
-                               packages[c] = []
-
-                       packages[c].append(pkg)
-
-               self.render("package-list.html", packages=packages)
-
-
-class PackageNameHandler(BaseHandler):
-       def get(self, package):
-               packages = self.pakfire.packages.get_by_name(package)
-
-               if not packages:
-                       raise tornado.web.HTTPError(404, "Package '%s' was not found")
-
-               # Take info from the most recent package.
-               pkg = packages[0]
-
-               self.render("package-detail-list.html", pkg=pkg, packages=packages)
-
-
-class PackageDetailHandler(BaseHandler):
-       def get(self, name, epoch, version, release):
-               pkg = self.pakfire.packages.get_by_tuple(name, epoch, version, release)
-               pkg.update()
-
-               self.render("package-detail.html", pkg=pkg)
-
-       @tornado.web.authenticated
-       def post(self, name, epoch, version, release):
-               pkg = self.pakfire.packages.get_by_tuple(name, epoch, version, release)
-
-               action = self.get_argument("action", None)
-
-               if action == "comment":
-                       vote = self.get_argument("vote", None)
-                       if not self.current_user.is_tester() and \
-                                       not self.current_user.is_admin():
-                               vote = None
-
-                       pkg.comment(self.current_user.id, self.get_argument("text"),
-                               vote or "none")
-
-               self.render("package-detail.html", pkg=pkg)
-
-
-class BuildDetailHandler(BaseHandler):
-       def get(self, uuid):
-               build = self.pakfire.builds.get_by_uuid(uuid)
-
-               if not build:
-                       raise tornado.web.HTTPError(404, "Build not found")
+               raise tornado.web.HTTPError(404)
 
-               self.render("build-detail.html", build=build)
 
+class AdvancedHandler(BaseHandler):
+       def get(self):
+               self.render("advanced.html")
 
-class BuildPriorityHandler(BaseHandler):
-       @tornado.web.authenticated
-       def get(self, uuid):
-               build = self.pakfire.builds.get_by_uuid(uuid)
-
-               if not build:
-                       raise tornado.web.HTTPError(404, "Build not found")
-
-               self.render("build-priority.html", build=build)
-
-       @tornado.web.authenticated
-       def post(self, uuid):
-               build = self.pakfire.builds.get_by_uuid(uuid)
-
-               if not build:
-                       raise tornado.web.HTTPError(404, "Build not found")
-
-               # Get the priority from the request data and convert it to an integer.
-               # If that cannot be done, we default to zero.
-               prio = self.get_argument("priority")
-               try:
-                       prio = int(prio)
-               except TypeError:
-                       prio = 0
-
-               # Check if the value is in a valid range.
-               if not prio in (-2, -1, 0, 1, 2):
-                       prio = 0
-
-               # Save priority.
-               build.priority = prio
 
-               self.redirect("/build/%s" % build.uuid)
+class StatisticsMainHandler(BaseHandler):
+       def get(self):
+               args = {}
 
+               # Build statistics.
+               args.update({
+                       "builds_count" : self.pakfire.builds.count(),
+               })
 
-class BuildScheduleHandler(BaseHandler):
-       allowed_types = ("test", "rebuild",)
+               # Job statistics.
+               args.update({
+                       "jobs_count_all"      : self.pakfire.jobs.count(),
+                       "jobs_avg_build_time" : self.pakfire.jobs.get_average_build_time(),
+               })
 
-       @tornado.web.authenticated
-       def get(self, uuid):
-               type = self.get_argument("type")
-               assert type in self.allowed_types
+               # User statistics.
+               args.update({
+                       "users_count" : self.pakfire.users.count(),
+               })
 
-               build = self.pakfire.builds.get_by_uuid(uuid)
-               if not build:
-                       raise tornado.web.HTTPError(404, "Build not found")
+               self.render("statistics-main.html", **args)
 
-               self.render("build-schedule-%s.html" % type, type=type, build=build)
 
+class UploadsHandler(BaseHandler):
        @tornado.web.authenticated
-       def post(self, uuid):
-               type = self.get_argument("type")
-               assert type in self.allowed_types
-
-               build = self.pakfire.builds.get_by_uuid(uuid)
-               if not build:
-                       raise tornado.web.HTTPError(404, "Build not found")
-
-               # Get the start offset.
-               offset = self.get_argument("offset", 0)
-               try:
-                       offset = int(offset)
-               except TypeError:
-                       offset = 0
-
-               # Submit the build.
-               if type == "test":
-                       build.schedule_test(offset)
-               elif type == "rebuild":
-                       build.schedule_rebuild(offset)
-
-               self.redirect("/build/%s" % build.uuid)
-
-
-class BuildListHandler(BaseHandler):
        def get(self):
-               builder = self.get_argument("builder", None)
-               state = self.get_argument("state", None)
-
-               builds = self.pakfire.builds.get_latest(state=state, builder=builder,
-                       limit=25)
-
-               self.render("build-list.html", builds=builds)
+               if not self.current_user.is_admin():
+                       raise tornado.web.HTTPError(403)
 
+               uploads = self.pakfire.uploads.get_all()
 
-class BuildFilterHandler(BaseHandler):
-       def get(self):
-               builders = self.pakfire.builders.get_all()
-
-               self.render("build-filter.html", builders=builders)
+               self.render("uploads-list.html", uploads=uploads)
 
 
 class DocsIndexHandler(BaseHandler):
@@ -205,18 +97,9 @@ class DocsUsersHandler(BaseHandler):
                self.render("docs-users.html")
 
 
-class SourceListHandler(BaseHandler):
+class DocsWhatsthisHandler(BaseHandler):
        def get(self):
-               sources = self.pakfire.sources.get_all()
-
-               self.render("source-list.html", sources=sources)
-
-
-class SourceDetailHandler(BaseHandler):
-       def get(self, id):
-               source = self.pakfire.sources.get_by_id(id)
-
-               self.render("source-detail.html", source=source)
+               self.render("docs-whatsthis.html")
 
 
 class FileDetailHandler(BaseHandler):
@@ -229,63 +112,82 @@ class FileDetailHandler(BaseHandler):
                self.render("file-detail.html", pkg=pkg, file=file)
 
 
-class DistributionListHandler(BaseHandler):
+class LogHandler(BaseHandler):
        def get(self):
-               distros = self.pakfire.distros.get_all()
-
-               self.render("distro-list.html", distros=distros)
+               self.render("log.html", log=self.pakfire.log)
 
 
-class DistributionDetailHandler(BaseHandler):
-       def get(self, name):
-               distro = self.pakfire.distros.get_by_name(name)
+class RepositoryDetailHandler(BaseHandler):
+       def get(self, distro, repo):
+               distro = self.pakfire.distros.get_by_name(distro)
                if not distro:
-                       raise tornado.web.HTTPError(404, "Distro not found")
+                       raise tornado.web.HTTPError(404)
 
-               self.render("distro-detail.html", distro=distro)
+               repo = distro.get_repo(repo)
+               if not repo:
+                       raise tornado.web.HTTPError(404)
 
+               limit = self.get_argument("limit", 50)
+               try:
+                       limit = int(limit)
+               except ValueError:
+                       limit = None
 
-class DistributionEditHandler(BaseHandler):
-       def prepare(self):
-               self.arches = self.pakfire.builders.get_all_arches()
-               self.sources = self.pakfire.sources.get_all()
+               offset = self.get_argument("offset", 0)
+               try:
+                       offset = int(offset)
+               except ValueError:
+                       offset = None
 
-       @tornado.web.authenticated
-       def get(self, name):
-               distro = self.pakfire.distros.get_by_name(name)
-               if not distro:
-                       raise tornado.web.HTTPError(404, "Distro not found")
+               builds = repo.get_builds(limit=limit, offset=offset)
+               unpushed_builds = repo.get_unpushed_builds()
+               obsolete_builds = repo.get_obsolete_builds()
+
+               # Get the build times of this repository.
+               build_times = repo.get_build_times()
+
+               self.render("repository-detail.html", distro=distro, repo=repo,
+                       builds=builds, unpushed_builds=unpushed_builds,
+                       obsolete_builds=obsolete_builds, build_times=build_times)
 
-               self.render("distro-edit.html", distro=distro, arches=self.arches,
-                       sources=self.sources)
 
+class RepositoryEditHandler(BaseHandler):
        @tornado.web.authenticated
-       def post(self, name):
-               distro = self.pakfire.distros.get_by_name(name)
+       def get(self, distro, repo):
+               distro = self.pakfire.distros.get_by_name(distro)
                if not distro:
-                       raise tornado.web.HTTPError(404, "Distro not found")
+                       raise tornado.web.HTTPError(404)
 
-               name = self.get_argument("name", distro.name)
-               vendor = self.get_argument("vendor", distro.vendor)
-               slogan = self.get_argument("slogan", distro.slogan)
-               arches = self.get_argument("arches", distro.arches)
-               sources = self.get_argument("sources", distro.sources)
+               repo = distro.get_repo(repo)
+               if not repo:
+                       raise tornado.web.HTTPError(404)
 
-               distro.set("name", name)
-               distro.set("vendor", vendor)
-               distro.set("slogan", slogan)
-               distro.set("arches", arches)
-               distro.set("sources", sources)
+               # XXX check if user has permissions to do this
 
-               self.redirect("/distribution/%s" % distro.sname)
+               self.render("repository-edit.html", distro=distro, repo=repo)
 
 
-class LogHandler(BaseHandler):
-       def get(self):
-               self.render("log.html", log=self.pakfire.log)
+class RepositoryConfHandler(BaseHandler):
+       def get(self, distro, repo):
+               distro = self.pakfire.distros.get_by_name(distro)
+               if not distro:
+                       raise tornado.web.HTTPError(404)
 
+               repo = distro.get_repo(repo)
+               if not repo:
+                       raise tornado.web.HTTPError(404)
 
-class RepositoryDetailHandler(BaseHandler):
+               # This is a plaintext file.
+               self.set_header("Content-Type", "text/plain")
+
+               # Write the header.
+               self.write("# Downloaded from the pakfire build service on %s.\n\n" \
+                       % datetime.datetime.utcnow())
+               self.write(repo.get_conf())
+               self.finish()
+
+
+class RepositoryMirrorlistHandler(BaseHandler):
        def get(self, distro, repo):
                distro = self.pakfire.distros.get_by_name(distro)
                if not distro:
@@ -295,7 +197,81 @@ class RepositoryDetailHandler(BaseHandler):
                if not repo:
                        raise tornado.web.HTTPError(404)
 
-               self.render("repository-detail.html", distro=distro, repo=repo)
+               # This is a plaintext file.
+               self.set_header("Content-Type", "text/plain")
+
+               arch = self.get_argument("arch", None)
+               arch = self.pakfire.arches.get_by_name(arch)
+
+               if not arch:
+                       raise tornado.web.HTTPError(400, "You must specify the architecture.")
+
+               ret = {
+                       "type"    : "mirrorlist",
+                       "version" : 1,
+               }
+
+               # A list with mirrors that are sent to the user.
+               mirrors = []
+
+               # Only search for mirrors on repositories that are supposed to be found
+               # on mirror servers.
+
+               if repo.mirrored:
+                       # See how many mirrors we can max. find.
+                       num_mirrors = self.mirrors.count(status="enabled")
+                       assert num_mirrors > 0
+
+                       # Create a list with all mirrors that is up to 50 mirrors long.
+                       # First add all preferred mirrors and then fill the rest up
+                       # with other mirrors.
+                       if num_mirrors >= 10:
+                               MAX_MIRRORS = 10
+                       else:
+                               MAX_MIRRORS = num_mirrors
+
+
+                       for mirror in self.mirrors.get_for_location(self.remote_address):
+                               mirrors.append({
+                                       "url"       : "/".join((mirror.url, distro.identifier, repo.identifier, arch.name)),
+                                       "location"  : mirror.country_code,
+                                       "preferred" : 1,
+                               })
+
+                       while MAX_MIRRORS - len(mirrors) > 0:
+                               mirror = self.mirrors.get_random(limit=1)[0]
+
+                               mirror = {
+                                       "url"       : "/".join((mirror.url, distro.identifier, repo.identifier, arch.name)),
+                                       "location"  : mirror.country_code,
+                                       "preferred" : 0,
+                               }
+
+                               if not mirror in mirrors:
+                                       mirrors.append(mirror)
+
+               else:
+                       repo_baseurl = self.pakfire.settings.get("repository_baseurl")
+                       if repo_baseurl.endswith("/"):
+                               repo_baseurl = repo_baseurl[:-1]
+
+                       for mirror in self.mirrors.get_all():
+                               print mirror.url, repo_baseurl
+                               if not mirror.url == repo_baseurl:
+                                       continue
+
+                               mirror = {
+                                       "url"       : "/".join((mirror.url, distro.identifier, repo.identifier, arch.name)),
+                                       "location"  : mirror.country_code,
+                                       "preferred" : 0,
+                               }
+
+                               mirrors.append(mirror)
+                               break
+
+               ret["mirrors"] = mirrors
+               self.write(ret)
+               
 
 
 class RepoActionHandler(BaseHandler):
index d1d31931823f2cb5d7c259ff04c33f38251a9acc..b3b06d8ae12613e2be9b6fa927ac789e292ee828 100644 (file)
@@ -2,10 +2,19 @@
 
 import tornado.web
 
+import backend.sessions
+import backend.users
+
 from handlers_base import *
 
 class LoginHandler(BaseHandler):
        def get(self):
+               # If the user is already logged in, we just send him back
+               # to the start page.
+               if self.current_user:
+                       self.redirect("/")
+                       return
+
                self.render("login.html", failed=False)
 
        def post(self):
@@ -15,8 +24,11 @@ class LoginHandler(BaseHandler):
                user = self.pakfire.users.auth(name, passphrase)
 
                if user:
+                       # Create a new session for the user.
+                       session = backend.sessions.Session.create(self.pakfire, user)
+
                        # Set a cookie and update the current user.
-                       self.set_secure_cookie("user", user.name)
+                       self.set_cookie("session_id", session.id)
                        self._current_user = user
 
                        # If there is "next" given, we redirect the user accordingly.
@@ -36,6 +48,12 @@ class LoginHandler(BaseHandler):
 
 class RegisterHandler(BaseHandler):
        def get(self):
+               # If the user is already logged in, we just send him back
+               # to the start page.
+               if self.current_user:
+                       self.redirect("/")
+                       return
+
                self.render("register.html")
 
        def post(self):
@@ -64,10 +82,12 @@ class RegisterHandler(BaseHandler):
                # Check if the passphrase is okay.
                if not pass1:
                        msgs.append(_("No password provided."))
-               elif not len(pass1) >= 8:
-                       msgs.append(_("Password has less than 8 characters."))
                elif not pass1 == pass2:
                        msgs.append(_("Passwords do not match."))
+               else:
+                       accepted, score = backend.users.check_password_strength(pass1)
+                       if not accepted:
+                               msgs.append(_("Your password is too weak."))
 
                if msgs:
                        self.render("register-fail.html", messages=msgs)
@@ -82,18 +102,28 @@ class RegisterHandler(BaseHandler):
 
 
 class ActivationHandler(BaseHandler):
-       def get(self, _user, code):
+       def get(self, _user):
                user = self.pakfire.users.get_by_name(_user)
                if not user:
                        raise tornado.web.HTTPError(404)
 
+               code = self.get_argument("code")
+
                # Check if the activation code matches and then activate the account.
                if user.activation_code == code:
                        user.activate()
 
-                       # Automatically login the user.
-                       self.set_secure_cookie("user", user.name)
-                       self._current_user = user
+                       # If an admin activated another account, he impersonates it.
+                       if self.current_user and self.current_user.is_admin():
+                               self.session.start_impersonation(user)
+
+                       else:
+                               # Automatically login the user.
+                               session = backend.sessions.Session.create(self.pakfire, user)
+
+                               # Set a cookie and update the current user.
+                               self.set_cookie("session_id", session.id)
+                               self._current_user = user
 
                        self.render("register-activation-success.html", user=user)
                        return
@@ -102,12 +132,28 @@ class ActivationHandler(BaseHandler):
                self.render("register-activation-fail.html")
 
 
+class PasswordRecoveryHandler(BaseHandler):
+       def get(self):
+               return self.render("user-forgot-password.html")
+
+       def post(self):
+               username = self.get_argument("name", None)
+
+               if not username:
+                       return self.get()
+
+               # XXX TODO
+
+
 class LogoutHandler(BaseHandler):
        @tornado.web.authenticated
        def get(self):
                # Remove the cookie, that identifies the user.
-               self.clear_cookie("user")
-               del self._current_user
+               self.clear_cookie("session_id")
+
+               # Destroy the user's session.
+               self.session.destroy()
+               self._current_user = None
 
                # Show a message to the user.
                self.render("logout.html")
index bccdefa6e238925c7e6f3ca9f701cb4f39e3c671..1091efb09750910dc7eae1dacb204dcea57843b6 100644 (file)
@@ -1,20 +1,44 @@
 #!/usr/bin/python
 
+import pakfire
+
+import datetime
 import httplib
+import pytz
 import time
 import tornado.locale
 import tornado.web
+import traceback
 
 import backend
 import backend.misc
+import backend.sessions
 
 class BaseHandler(tornado.web.RequestHandler):
+       @property
+       def cache(self):
+               return self.pakfire.cache
+
        def get_current_user(self):
-               user = self.get_secure_cookie("user")
-               if not user:
+               session_id = self.get_cookie("session_id")
+               if not session_id:
+                       return
+
+               try:
+                       self.session = backend.sessions.Session(self.pakfire, session_id)
+               except:
                        return
 
-               return self.pakfire.users.get_by_name(user)
+               # Update the session lifetime.
+               # XXX refresh cookie, too
+               self.session.refresh()
+
+               # If the session impersonated a user, we return that one.
+               if self.session.impersonated_user:
+                       return self.session.impersonated_user
+
+               # By default, we return the user of this session.
+               return self.session.user
 
        def get_user_locale(self):
                DEFAULT_LOCALE = tornado.locale.get("en_US")
@@ -43,15 +67,56 @@ class BaseHandler(tornado.web.RequestHandler):
                # If no one of the cases above worked we use our default locale
                return DEFAULT_LOCALE
 
+       def random_slogan(self):
+               slogan = self.pakfire.db.get("SELECT message FROM slogans ORDER BY RAND() LIMIT 1")
+               if slogan:
+                       return slogan.message
+
+       @property
+       def remote_address(self):
+               """
+                       Returns the IP address the request came from.
+               """
+               remote_ips = self.request.remote_ip.split(", ")
+
+               return remote_ips[-1]
+
+       @property
+       def timezone(self):
+               if self.current_user:
+                       return self.current_user.timezone
+
+               return pytz.utc
+
+       def format_date(self, date, relative=True, shorter=False,
+                       full_format=False):
+               # XXX not very precise but working for now.
+               gmt_offset = self.timezone.utcoffset(date).total_seconds() / -60
+
+               return self.locale.format_date(date, gmt_offset=gmt_offset,
+                       relative=relative, shorter=shorter, full_format=full_format)
+
        @property
        def render_args(self):
-               return {
-                       "hostname" : self.request.host,
-                       "friendly_size" : backend.misc.friendly_size,
-                       "lang" : self.locale.code[:2],
-                       "year" : time.strftime("%Y"),
+               ret = {
+                       "bugtracker"      : self.pakfire.bugzilla,
+                       "hostname"        : self.request.host,
+                       "format_date"     : self.format_date,
+                       "format_size"     : backend.misc.format_size,
+                       "friendly_time"   : backend.misc.friendly_time,
+                       "format_email"    : backend.misc.format_email,
+                       "format_filemode" : backend.misc.format_filemode,
+                       "lang"            : self.locale.code[:2],
+                       "pakfire_version" : pakfire.__version__,
+                       "random_slogan"   : self.random_slogan,
+                       "year"            : time.strftime("%Y"),
                }
 
+               # Add session.
+               ret["session"] = getattr(self, "session", None)
+
+               return ret
+
        def render(self, *args, **kwargs):
                kwargs.update(self.render_args)
                tornado.web.RequestHandler.render(self, *args, **kwargs)
@@ -60,18 +125,44 @@ class BaseHandler(tornado.web.RequestHandler):
                kwargs.update(self.render_args)
                return tornado.web.RequestHandler.render_string(self, *args, **kwargs)
 
-       def get_error_html(self, status_code, **kwargs):
-               if status_code in (404, 500):
-                       render_args = ({
-                               "code"      : status_code,
-                               "exception" : kwargs.get("exception", None),
-                               "message"   : httplib.responses[status_code],
-                       })
-                       return self.render_string("error-%s.html" % status_code, **render_args)
-               else:
-                       return tornado.web.RequestHandler.get_error_html(self, status_code, **kwargs)
+       def get_error_html(self, status_code, exception=None, **kwargs):
+               error_document = "error.html"
+
+               kwargs.update({
+                       "code"      : status_code,
+                       "message"   : httplib.responses[status_code],
+               })
+
+               if status_code in (403, 404):
+                       error_document = "error-%s.html" % status_code
+
+               # Collect more information about the exception if possible.
+               if exception:
+                       exception = traceback.format_exc()
+
+               return self.render_string(error_document, exception=exception, **kwargs)
 
        @property
        def pakfire(self):
                return self.application.pakfire
 
+       @property
+       def arches(self):
+               return self.pakfire.arches
+
+       @property
+       def mirrors(self):
+               return self.pakfire.mirrors
+
+       @property
+       def public(self):
+               """
+                       Indicates what level of public/non-public things a user
+                       may see.
+               """
+               if self.current_user and self.current_user.is_admin():
+                       public = None
+               else:
+                       public = True
+
+               return public
index de9bd765e9b675341b45ba8df5b577584dcbbcf4..aa9ba6d558d27ab877ba80c776e6836623566ab8 100644 (file)
@@ -1,5 +1,7 @@
 #!/usr/bin/python
 
+import tornado.web
+
 import backend
 
 from handlers_base import *
@@ -7,8 +9,11 @@ from handlers_base import *
 class BuilderListHandler(BaseHandler):
        def get(self):
                builders = self.pakfire.builders.get_all()
+               load = self.pakfire.builders.get_load()
+
+               log = self.pakfire.builders.get_history(limit=10)
 
-               self.render("builder-list.html", builders=builders)
+               self.render("builder-list.html", builders=builders, load=load, log=log)
 
 
 class BuilderDetailHandler(BaseHandler):
@@ -17,18 +22,36 @@ class BuilderDetailHandler(BaseHandler):
 
                self.render("builder-detail.html", builder=builder)
 
+       @tornado.web.authenticated
+       def post(self, hostname):
+               if not self.current_user.has_perm("maintain_mirrors"):
+                       raise tornado.web.HTTPError(403, "User is not allowed to do this.")
+
+               builder = self.pakfire.builders.get_by_name(hostname)
+
+               description = self.get_argument("description", "")
+               builder.update_description(description)
+
+               self.redirect("/builder/%s" % builder.hostname)
+
 
 class BuilderNewHandler(BaseHandler):
        def get(self):
                self.render("builder-new.html")
 
+       @tornado.web.authenticated
        def post(self):
+               if not self.current_user.has_perm("maintain_builders"):
+                       raise tornado.web.HTTPError(403)
+
                name = self.get_argument("name")
 
                # Create a new builder.
-               builder = backend.builders.Builder.new(self.pakfire, name)
+               builder, passphrase = \
+                       backend.builders.Builder.create(self.pakfire, name, user=self.current_user)
 
-               self.render("builder-pass.html", action="new", builder=builder)
+               self.render("builder-pass.html", action="new", builder=builder,
+                       passphrase=passphrase)
 
 
 class BuilderEditHandler(BaseHandler):
@@ -44,12 +67,16 @@ class BuilderEditHandler(BaseHandler):
        def post(self, hostname):
                builder = self.pakfire.builders.get_by_name(hostname)
                if not builder:
-                       raise tornado.web.HTTPError(404, "Builder not found")
+                       raise tornado.web.HTTPError(404, "Builder not found: %s" % hostname)
+
+               # Check for sufficient right to edit things.
+               if not self.current_user.has_perm("maintain_builders"):
+                       raise tornado.web.HTTPError(403)
 
-               builder.enabled = self.get_argument("enabled", False)
-               builder.build_src = self.get_argument("build_src", False)
-               builder.build_bin = self.get_argument("build_bin", False)
-               builder.build_test = self.get_argument("build_test", False)
+               builder.enabled       = self.get_argument("enabled", False)
+               builder.build_release = self.get_argument("build_release", False)
+               builder.build_scratch = self.get_argument("build_scratch", False)
+               builder.build_test    = self.get_argument("build_test", False)
 
                # Save max_jobs.
                max_jobs = self.get_argument("max_jobs", builder.max_jobs)
@@ -58,30 +85,75 @@ class BuilderEditHandler(BaseHandler):
                except TypeError:
                        max_jobs = 1
 
-               if not max_jobs in (1, 2, 3, 4, 5, 6, 7, 8,):
+               if not max_jobs in range(1, 100):
                        max_jobs = 1
                builder.max_jobs = max_jobs
 
+
+               for arch in builder.get_arches():
+                       builder.set_arch_status(arch, False)
+
+               for arch in self.get_arguments("arches", []):
+                       arch = self.pakfire.arches.get_by_name(arch)
+                       if not arch:
+                               continue
+
+                       builder.set_arch_status(arch, True)
+
                self.redirect("/builder/%s" % builder.hostname)
 
 
 class BuilderRenewPassphraseHandler(BaseHandler):
+       @tornado.web.authenticated
        def get(self, name):
                builder = self.pakfire.builders.get_by_name(name)
 
-               builder.regenerate_passphrase()
+               passphrase = builder.regenerate_passphrase()
 
-               self.render("builder-pass.html", action="update", builder=builder)
+               self.render("builder-pass.html", action="update", builder=builder,
+                       passphrase=passphrase)
 
 
 class BuilderDeleteHandler(BaseHandler):
+       @tornado.web.authenticated
        def get(self, name):
                builder = self.pakfire.builders.get_by_name(name)
+               if not builder:
+                       raise tornado.web.HTTPError(404, "Builder not found: %s" % name)
+
+               # Check for sufficient right to delete this builder.
+               if not self.current_user.has_perm("maintain_builders"):
+                       raise tornado.web.HTTPError(403)
 
                confirmed = self.get_argument("confirmed", None)        
                if confirmed:
-                       builder.delete()
+                       builder.set_status("deleted", user=self.current_user)
+
                        self.redirect("/builders")
                        return
 
                self.render("builder-delete.html", builder=builder)
+
+
+class BuilderStatusChangeHandler(BaseHandler):
+       new_status = None
+
+       @tornado.web.authenticated
+       def get(self, hostname):
+               builder = self.pakfire.builders.get_by_name(hostname)
+               if not builder:
+                       raise tornado.web.HTTPError(404, "Builder not found: %s" % hostname)
+
+               # Check for sufficient right to edit things.
+               if self.current_user.has_perm("maintain_builders"):
+                       builder.set_status(self.status, user=self.current_user)
+
+               self.redirect("/builder/%s" % builder.name)
+
+
+class BuilderEnableHander(BuilderStatusChangeHandler):
+       status = "enabled"
+
+
+class BuilderDisableHander(BuilderStatusChangeHandler):
+       status = "disabled"
diff --git a/web/handlers_builds.py b/web/handlers_builds.py
new file mode 100644 (file)
index 0000000..7b96949
--- /dev/null
@@ -0,0 +1,359 @@
+#!/usr/bin/python
+
+import tornado.web
+
+from handlers_base import BaseHandler
+
+class BuildsHandler(BaseHandler):
+       def get(self):
+               builds = self.pakfire.builds.get_all(limit=50)
+
+               self.render("build-index.html", builds=builds)
+
+
+class BuildBaseHandler(BaseHandler):
+       def get_build(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+               if not build:
+                       raise tornado.web.HTTPError(404, "No such build: %s" % uuid)
+
+               return build
+
+
+class BuildDetailHandler(BuildBaseHandler):
+       def get(self, uuid):
+               build = self.get_build(uuid)
+
+               # Cache the log.
+               log = build.get_log()
+
+               if build.repo:
+                       next_repo = build.repo.next()
+               else:
+                       next_repo = None
+
+               # Bugs.
+               bugs = build.get_bugs()
+
+               self.render("build-detail.html", build=build, log=log, pkg=build.pkg,
+                       distro=build.distro, bugs=bugs, repo=build.repo, next_repo=next_repo)
+
+
+class BuildDeleteHandler(BuildBaseHandler):
+       @tornado.web.authenticated
+       def get(self, uuid):
+               build = self.get_build(uuid)
+
+               # Check if the user has got sufficient rights to do this modification.
+               if not build.has_perm(self.current_user):
+                       raise tornado.web.HTTPError(403)
+
+               # Check if the user confirmed the action.
+               confirmed = self.get_argument("confirmed", None)
+               if confirmed:
+                       # Save the name of the package to redirect the user
+                       # to the other packages of that name.
+                       package_name = build.pkg.name
+
+                       # Delete the build and everything that comes with it.
+                       build.delete()
+
+                       return self.redirect("/package/%s" % package_name)
+
+               self.render("build-delete.html", build=build)
+
+
+class BuildBugsHandler(BaseHandler):
+       @tornado.web.authenticated
+       def get(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+               if not build:
+                       raise tornado.web.HTTPError(404, "No such build: %s" % uuid)
+
+               # Check if the user has got the right to alter this build.
+               if not build.has_perm(self.current_user):
+                       raise tornado.web.HTTPError(403)
+
+               # Bugs.
+               fixed_bugs = build.get_bugs()
+               open_bugs = []
+
+               for bug in self.pakfire.bugzilla.get_bugs_from_component(build.pkg.name):
+                       if bug in fixed_bugs:
+                               continue
+
+                       open_bugs.append(bug)
+
+               self.render("build-bugs.html", build=build, pkg=build.pkg,
+                       fixed_bugs=fixed_bugs, open_bugs=open_bugs)
+
+       @tornado.web.authenticated
+       def post(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+               if not build:
+                       raise tornado.web.HTTPError(404, "No such build: %s" % uuid)
+
+               # Check if the user has got the right to alter this build.
+               if not build.has_perm(self.current_user):
+                       raise tornado.web.HTTPError(403)
+
+               action = self.get_argument("action", None)
+               bugid  = self.get_argument("bugid")
+
+               # Convert the bug id to integer.
+               try:
+                       bugid = int(bugid)
+               except ValueError:
+                       raise tornado.web.HTTPError(400, "Bad bug id given: %s" % bugid)
+
+               if action == "add":
+                       # Add bug to the build.
+                       build.add_bug(bugid, user=self.current_user)
+
+               elif action == "remove":
+                       # Remove bug from the build.
+                       build.rem_bug(bugid, user=self.current_user)
+
+               else:
+                       raise tornado.web.HTTPError(400, "Unhandled action: %s" % action)
+
+               self.redirect("/build/%s/bugs" % build.uuid)
+
+
+class BuildStateHandler(BaseHandler):
+       def get(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+               if not build:
+                       raise tornado.web.HTTPError(404, "No such build: %s" % uuid)
+
+               self.render("build-state.html", build=build)
+
+       @tornado.web.authenticated
+       def post(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+               if not build:
+                       raise tornado.web.HTTPError(404, "No such build: %s" % uuid)
+
+               # Check if user has the right to perform this action.
+               if not build.has_perm(self.current_user):
+                       raise tornado.web.HTTPError(403, "User is not allowed to perform this action")
+
+               # Check if given state is valid.
+               state = self.get_argument("state", None)
+               if not state in ("broken", "unbreak", "obsolete"):
+                       raise tornado.web.HTTPError(400, "Invalid argument given: %s" % state)
+
+               # XXX this is not quite accurate
+               if state == "unbreak":
+                       state = "stable"
+
+               rem_from_repo = self.get_argument("rem_from_repo", False)
+               if rem_from_repo == "on":
+                       rem_from_repo = True
+
+               # Perform the state change.
+               build.update_state(state, user=self.current_user, remove=rem_from_repo)
+
+               self.redirect("/build/%s" % build.uuid)
+
+
+class BuildQueueHandler(BaseHandler):
+       def get(self):
+               jobs = self.pakfire.jobs.get_next(limit=1000, states=["pending",])
+
+               self.render("build-queue.html", jobs=jobs)
+
+
+class BuildDetailCommentHandler(BaseHandler):
+       @tornado.web.authenticated
+       def post(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+
+               if not build:
+                       raise tornado.web.HTTPError(404, "Build not found")
+
+               vote = self.get_argument("vote", "none")
+
+               if vote == "up":
+                       vote = 1
+               elif vote == "down":
+                       vote = -1
+               else:
+                       vote = 0
+
+               text = self.get_argument("text", "")
+
+               # Add a new comment to the build.
+               if text or vote:
+                       build.add_comment(self.current_user, text, vote)
+
+               # Redirect to the build detail page.
+               self.redirect("/build/%s" % build.uuid)
+
+
+class BuildManageHandler(BaseHandler):
+       @tornado.web.authenticated
+       def get(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+               if not build:
+                       raise tornado.web.HTTPError(404, "Build not found: %s" % uuid)
+
+               mode = "user"
+               if self.current_user.is_admin():
+                       mode = self.get_argument("mode", "user")
+
+               # Get the next repo.
+               if build.repo:
+                       next_repo = build.repo.next()
+               else:
+                       next_repo = build.distro.first_repo
+
+               self.render("build-manage.html", mode=mode, build=build,
+                       distro=build.distro, repo=build.repo, next_repo=next_repo)
+
+       @tornado.web.authenticated
+       def post(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+               if not build:
+                       raise tornado.web.HTTPError(404, "Build not found: %s" % uuid)
+
+               # check for sufficient permissions
+               if not build.has_perm(self.current_user):
+                       raise tornado.web.HTTPError(403)
+
+               action = self.get_argument("action")
+               assert action in ("push", "unpush")
+
+               current_repo = build.repo
+
+               if action == "unpush":
+                       current_repo.rem_build(build, user=self.current_user)
+
+               elif action == "push":
+                       repo_name = self.get_argument("repo")
+                       next_repo = build.distro.get_repo(repo_name)
+
+                       if not next_repo:
+                               raise tornado.web.HTTPError(404, "No such repository: %s" % next_repo)
+
+                       if not self.current_user.is_admin():
+                               if not distro.repo.next() == next_repo:
+                                       raise tornado.web.HTTPError(403)
+
+                       if current_repo:
+                               current_repo.move_build(build, next_repo, user=self.current_user)
+                       else:
+                               next_repo.add_build(build, user=self.current_user)
+
+               self.redirect("/build/%s" % build.uuid)
+
+
+class BuildPriorityHandler(BaseHandler):
+       @tornado.web.authenticated
+       def get(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+
+               if not build:
+                       raise tornado.web.HTTPError(404, "Build not found")
+
+               self.render("build-priority.html", build=build)
+
+       @tornado.web.authenticated
+       def post(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+
+               if not build:
+                       raise tornado.web.HTTPError(404, "Build not found")
+
+               # Get the priority from the request data and convert it to an integer.
+               # If that cannot be done, we default to zero.
+               prio = self.get_argument("priority")
+               try:
+                       prio = int(prio)
+               except TypeError:
+                       prio = 0
+
+               # Check if the value is in a valid range.
+               if not prio in (-2, -1, 0, 1, 2):
+                       prio = 0
+
+               # Save priority.
+               build.priority = prio
+
+               self.redirect("/build/%s" % build.uuid)
+
+
+class BuildWatchersHandler(BaseHandler):
+       def get(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+
+               if not build:
+                       raise tornado.web.HTTPError(404, "Build not found")
+
+               # Get a list of all watchers and sort them by their realname.
+               watchers = build.get_watchers()
+               watchers.sort(key=lambda watcher: watcher.realname)
+
+               self.render("builds-watchers-list.html", build=build, watchers=watchers)
+
+
+class BuildWatchersAddHandler(BaseHandler):
+       @tornado.web.authenticated
+       def get(self, uuid, error_msg=None):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+
+               if not build:
+                       raise tornado.web.HTTPError(404, "Build not found")
+
+               # Get a list of all users that are currently watching this build.
+               watchers = build.get_watchers()
+
+               self.render("builds-watchers-add.html", error_msg=error_msg,
+                       build=build, users=self.pakfire.users.get_all(), watchers=watchers)
+
+       @tornado.web.authenticated
+       def post(self, uuid):
+               build = self.pakfire.builds.get_by_uuid(uuid)
+
+               if not build:
+                       raise tornado.web.HTTPError(404, "Build not found")
+
+               # Get the user id of the new watcher.
+               user_id = self.current_user.id
+
+               if self.current_user.is_admin():
+                       user_id = self.get_argument("user_id", self.current_user.id)
+               assert user_id
+
+               user = self.pakfire.users.get_by_id(user_id)
+               if not user:
+                       _ = self.locale.translate
+                       error_msg = _("User not found.")
+
+                       return self.get(uuid, error_msg=error_msg)
+
+               # Actually add the user to the list of watchers.
+               build.add_watcher(user)
+
+               # Send user back to the build detail page.
+               self.redirect("/build/%s" % build.uuid)
+
+
+class BuildListHandler(BaseHandler):
+       def get(self):
+               builder = self.get_argument("builder", None)
+               state = self.get_argument("state", None)
+
+               builds = self.pakfire.builds.get_latest(state=state, builder=builder,
+                       limit=25)
+
+               self.render("build-list.html", builds=builds)
+
+
+class BuildFilterHandler(BaseHandler):
+       def get(self):
+               builders = self.pakfire.builders.get_all()
+               distros  = self.pakfire.distros.get_all()
+
+               self.render("build-filter.html", builders=builders, distros=distros)
+
diff --git a/web/handlers_distro.py b/web/handlers_distro.py
new file mode 100644 (file)
index 0000000..04bacc9
--- /dev/null
@@ -0,0 +1,218 @@
+#!/usr/bin/python
+
+from handlers_base import *
+
+
+class DistributionListHandler(BaseHandler):
+       def get(self):
+               distros = self.pakfire.distros.get_all()
+
+               self.render("distro-list.html", distros=distros)
+
+
+class DistributionDetailHandler(BaseHandler):
+       def get(self, name):
+               distro = self.pakfire.distros.get_by_name(name)
+               if not distro:
+                       raise tornado.web.HTTPError(404, "Distro not found")
+
+               self.render("distro-detail.html", distro=distro)
+
+
+class DistributionEditHandler(BaseHandler):
+       def prepare(self):
+               self.sources = self.pakfire.sources.get_all()
+
+       @tornado.web.authenticated
+       def get(self, name):
+               distro = self.pakfire.distros.get_by_name(name)
+               if not distro:
+                       raise tornado.web.HTTPError(404, "Distro not found")
+
+               self.render("distro-edit.html", distro=distro,
+                       arches=self.arches.get_all(), sources=self.sources)
+
+       @tornado.web.authenticated
+       def post(self, name):
+               distro = self.pakfire.distros.get_by_name(name)
+               if not distro:
+                       raise tornado.web.HTTPError(404, "Distro not found")
+
+               name = self.get_argument("name", distro.name)
+               vendor = self.get_argument("vendor", distro.vendor)
+               contact = self.get_argument("contact", "")
+               slogan = self.get_argument("slogan", distro.slogan)
+               tag = self.get_argument("tag", "")
+
+               distro.set("name", name)
+               distro.set("vendor", vendor)
+               distro.set("slogan", slogan)
+
+               # Update the contact email address.
+               distro.contact = contact
+
+               # Update the tag.
+               distro.tag = tag
+
+               # Update architectures.
+               arches = []
+               for arch in self.get_arguments("arches", []):
+                       try:
+                               arch_id = int(arch)
+                       except ValueError:
+                               continue
+
+                       if not self.arches.exists(arch_id):
+                               continue
+
+                       arch = self.arches.get_by_id(arch_id)
+                       arches.append(arch)
+
+               distro.arches = arches
+
+               self.redirect("/distribution/%s" % distro.sname)
+
+
+class DistroSourceDetailHandler(BaseHandler):
+       def get(self, distro_ident, source_ident):
+               distro = self.pakfire.distros.get_by_name(distro_ident)
+               if not distro:
+                       raise tornado.web.HTTPError(404, "Distro not found")
+
+               source = distro.get_source(source_ident)
+               if not source:
+                       raise tornado.web.HTTPError(404, "Source '%s' not found in distro '%s'" \
+                               % (source_ident, distro.name))
+
+               # Get the latest commits.
+               commits = source.get_commits(limit=5)
+
+               self.render("distro-source-detail.html", distro=distro, source=source,
+                       commits=commits)
+
+
+class DistroSourceCommitsHandler(BaseHandler):
+       def get(self, distro_ident, source_ident):
+               distro = self.pakfire.distros.get_by_name(distro_ident)
+               if not distro:
+                       raise tornado.web.HTTPError(404, "Distro not found")
+
+               source = distro.get_source(source_ident)
+               if not source:
+                       raise tornado.web.HTTPError(404, "Source '%s' not found in distro '%s'" \
+                               % (source_ident, distro.name))
+
+               offset = self.get_argument("offset", 0)
+               try:
+                       offset = int(offset)
+               except ValueError:
+                       offset = 0
+
+               limit  = self.get_argument("limit", 50)
+               try:
+                       limit = int(limit)
+               except ValueError:
+                       limit = 50
+
+               commits = source.get_commits(limit=limit, offset=offset)
+
+               self.render("distro-source-commits.html", distro=distro, source=source,
+                       commits=commits, limit=limit, offset=offset, number=50)
+
+
+class DistroSourceCommitDetailHandler(BaseHandler):
+       def get(self, distro_ident, source_ident, commit_ident):
+               distro = self.pakfire.distros.get_by_name(distro_ident)
+               if not distro:
+                       raise tornado.web.HTTPError(404, "Distribution '%s' not found" % distro_ident)
+
+               source = distro.get_source(source_ident)
+               if not source:
+                       raise tornado.web.HTTPError(404, "Source '%s' not found in distro '%s'" \
+                               % (source_ident, distro.name))
+
+               commit = source.get_commit(commit_ident)
+               if not commit:
+                       raise tornado.web.HTTPError(404, "Commit '%s' not found in source '%s'" \
+                               % (commit_ident, source.name))
+
+               self.render("distro-source-commit-detail.html", distro=distro,
+                       source=source, commit=commit)
+
+
+class DistroSourceCommitResetHandler(BaseHandler):
+       @tornado.web.authenticated
+       def get(self, distro_ident, source_ident, commit_ident):
+               distro = self.pakfire.distros.get_by_name(distro_ident)
+               if not distro:
+                       raise tornado.web.HTTPError(404, "Distribution '%s' not found" % distro_ident)
+
+               source = distro.get_source(source_ident)
+               if not source:
+                       raise tornado.web.HTTPError(404, "Source '%s' not found in distro '%s'" \
+                               % (source_ident, distro.name))
+
+               commit = source.get_commit(commit_ident)
+               if not commit:
+                       raise tornado.web.HTTPError(404, "Commit '%s' not found in source '%s'" \
+                               % (commit_ident, source.name))
+
+               if not self.current_user.is_admin():
+                       raise tornado.web.HTTPError(403)
+
+               confirmed = self.get_argument("confirmed", None)
+               if confirmed:
+                       commit.reset()
+
+                       self.redirect("/distro/%s/source/%s/%s" % \
+                               (distro.identifier, source.identifier, commit.revision))
+                       return
+
+               self.render("distro-source-commit-reset.html", distro=distro,
+                       source=source, commit=commit)
+
+
+class DistroUpdateCreateHandler(BaseHandler):
+       def get(self, distro_ident):
+               distro = self.pakfire.distros.get_by_name(distro_ident)
+               if not distro:
+                       raise tornado.web.HTTPError(404, "Distribution '%s' not found" % distro_ident)
+
+               # Get all preset builds.
+               builds = []
+               for build in self.get_arguments("builds", []):
+                       build = self.pakfire.builds.get_by_uuid(build)
+                       builds.append(build)
+
+               builds.sort()
+
+               self.render("distro-update-edit.html", update=None,
+                       distro=distro, builds=builds)
+
+
+class DistroUpdateDetailHandler(BaseHandler):
+       def get(self, distro_ident, year, num):
+               distro = self.pakfire.distros.get_by_name(distro_ident)
+               if not distro:
+                       raise tornado.web.HTTPError(404, "Distribution '%s' not found" % distro_ident)
+
+               update = distro.get_update(year, num)
+               if not update:
+                       raise tornado.web.HTTPError(404, "Update cannot be found: %s %s" % (year, num))
+
+               self.render("distro-update-detail.html", distro=distro,
+                       update=update, repo=update.repo, user=update.user)
+
+# XXX currently unused
+class SourceListHandler(BaseHandler):
+       def get(self):
+               sources = self.pakfire.sources.get_all()
+
+               self.render("source-list.html", sources=sources)
+
+
+class SourceDetailHandler(BaseHandler):
+       def get(self, id):
+               source = self.pakfire.sources.get_by_id(id)
+
+               self.render("source-detail.html", source=source)
diff --git a/web/handlers_jobs.py b/web/handlers_jobs.py
new file mode 100644 (file)
index 0000000..09dfe2e
--- /dev/null
@@ -0,0 +1,111 @@
+#!/usr/bin/python
+
+import tornado.web
+
+from handlers_base import BaseHandler
+
+
+class JobDetailHandler(BaseHandler):
+       def get(self, uuid):
+               job = self.pakfire.jobs.get_by_uuid(uuid)
+               if not job:
+                       raise tornado.web.HTTPError(404, "No such job: %s" % job)
+
+               # Cache the log.
+               log = job.get_log()
+
+               self.render("jobs-detail.html", job=job, build=job.build, log=log)
+
+
+class JobBuildrootHandler(BaseHandler):
+       def get_job(self, uuid):
+               job = self.pakfire.jobs.get_by_uuid(uuid)
+               if not job:
+                       raise tornado.web.HTTPError(404, "Job not found: %s" % uuid)
+
+               return job
+
+       def get(self, uuid):
+               job = self.get_job(uuid)
+
+               tries = self.get_argument("tries", None)
+               buildroot = job.get_buildroot(tries)
+
+               self.render("jobs-buildroot.html", job=job, buildroot=buildroot)
+
+
+class JobScheduleHandler(BaseHandler):
+       allowed_types = ("test", "rebuild",)
+
+       @tornado.web.authenticated
+       def get(self, uuid):
+               type = self.get_argument("type")
+               assert type in self.allowed_types
+
+               job = self.pakfire.jobs.get_by_uuid(uuid)
+               if not job:
+                       raise tornado.web.HTTPError(404, "Job not found: %s" % uuid)
+
+               self.render("job-schedule-%s.html" % type, type=type, job=job, build=job.build)
+
+       @tornado.web.authenticated
+       def post(self, uuid):
+               type = self.get_argument("type")
+               assert type in self.allowed_types
+
+               job = self.pakfire.jobs.get_by_uuid(uuid)
+               if not job:
+                       raise tornado.web.HTTPError(404, "Job not found: %s" % uuid)
+
+               # Get the start offset.
+               offset = self.get_argument("offset", 0)
+               try:
+                       offset = int(offset)
+               except TypeError:
+                       offset = 0
+
+               # Submit the build.
+               if type == "test":
+                       job = job.schedule_test(offset)
+
+               elif type == "rebuild":
+                       job.schedule_rebuild(offset)
+
+               self.redirect("/job/%s" % job.uuid)
+
+
+class JobAbortHandler(BaseHandler):
+       def get_job(self, uuid):
+               job = self.pakfire.jobs.get_by_uuid(uuid)
+               if not job:
+                       raise tornado.web.HTTPError(404, "Job not found: %s" % uuid)
+
+               return job
+
+       @tornado.web.authenticated
+       def get(self, uuid):
+               job = self.get_job(uuid)
+
+               # XXX Check if user has the right to manage the job.
+
+               self.render("jobs-abort.html", job=job)
+
+       @tornado.web.authenticated
+       def post(self, uuid):
+               job = self.get_job(uuid)
+
+               # XXX Check if user has the right to manage the job.
+
+               # Only running builds can be set to aborted state.
+               if not job.state == "running":
+                       # XXX send the user a nicer error message.
+                       self.redirect("/job/%s" % job.uuid)
+                       return
+
+               # Set the job into aborted state.
+               job.state = "aborted"
+
+               # 0 means the job was aborted by the user.
+               job.aborted_state = 0
+
+               self.redirect("/job/%s" % job.uuid)
diff --git a/web/handlers_keys.py b/web/handlers_keys.py
new file mode 100644 (file)
index 0000000..e3f40d1
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/python
+
+import tornado.web
+
+from handlers_base import BaseHandler
+
+class KeysActionHandler(BaseHandler):
+       def prepare(self):
+               if not self.current_user.has_perm("manage_keys"):
+                       raise tornado.web.HTTPError(403)
+
+
+class KeysImportHandler(KeysActionHandler):
+       @tornado.web.authenticated
+       def get(self):
+               self.render("keys-import.html")
+
+       @tornado.web.authenticated
+       def post(self):
+               data = self.get_argument("data")
+
+               key = self.pakfire.keys.create(data)
+               assert key
+
+               self.redirect("/keys")
+
+
+class KeysDeleteHandler(KeysActionHandler):
+       @tornado.web.authenticated
+       def get(self, fingerprint):
+               key = self.pakfire.keys.get_by_fpr(fingerprint)
+               if not key:
+                       raise tornado.web.HTTPError(404, "Could not find key: %s" % fingerprint)
+
+               confirmed = self.get_argument("confirmed", False)
+               if confirmed:
+                       key.delete()
+
+                       return self.redirect("/keys")
+
+               self.render("keys-delete.html", key=key)
+
+
+class KeysListHandler(BaseHandler):
+       def get(self):
+               keys = self.pakfire.keys.get_all()
+
+               self.render("keys-list.html", keys=keys)
+
+
+class KeysDownloadHandler(BaseHandler):
+       def get(self, fingerprint):
+               key = self.pakfire.keys.get_by_fpr(fingerprint)
+               if not key:
+                       raise tornado.web.HTTPError(404, "Could not find key: %s" % fingerprint)
+
+               # Send the key data.
+               self.set_header("Content-Type", "text/plain")
+               self.write(key.key)
diff --git a/web/handlers_mirrors.py b/web/handlers_mirrors.py
new file mode 100644 (file)
index 0000000..5a81da1
--- /dev/null
@@ -0,0 +1,135 @@
+#!/usr/bin/python
+
+import tornado.web
+
+import backend
+
+from handlers_base import BaseHandler
+
+class MirrorListHandler(BaseHandler):
+       def get(self):
+               mirrors = self.pakfire.mirrors.get_all()
+               mirrors_nearby = self.pakfire.mirrors.get_for_location(self.remote_address)
+
+               mirrors_worldwide = []
+               for mirror in mirrors:
+                       if mirror in mirrors_nearby:
+                               continue
+
+                       mirrors_worldwide.append(mirror)
+
+               kwargs = {
+                       "mirrors" : mirrors,
+                       "mirrors_nearby" : mirrors_nearby,
+                       "mirrors_worldwide" : mirrors_worldwide,
+               }
+
+               # Get recent log messages.
+               kwargs["log"] = self.pakfire.mirrors.get_history(limit=10)
+
+               self.render("mirrors-list.html", **kwargs)
+
+
+class MirrorDetailHandler(BaseHandler):
+       def get(self, hostname):
+               mirror = self.pakfire.mirrors.get_by_hostname(hostname)
+               if not mirror:
+                       raise tornado.web.HTTPError(404, "Could not find mirror: %s" % hostname)
+
+               self.render("mirrors-detail.html", mirror=mirror)
+
+
+class MirrorActionHandler(BaseHandler):
+       """
+               A handler that makes sure if the user has got sufficent rights to
+               do actions.
+       """
+       def prepare(self):
+               # Check if the user has sufficient rights to create a new mirror.
+               if not self.current_user.has_perm("manage_mirrors"):
+                       raise tornado.web.HTTPError(403)
+
+
+class MirrorNewHandler(MirrorActionHandler):
+       @tornado.web.authenticated
+       def get(self, hostname="", path="", hostname_missing=False, path_invalid=False):
+               self.render("mirrors-new.html", _hostname=hostname, path=path,
+                       hostname_missing=hostname_missing, path_invalid=path_invalid)
+
+       @tornado.web.authenticated
+       def post(self):
+               errors = {}
+
+               hostname = self.get_argument("name", None)
+               if not hostname:
+                       errors["hostname_missing"] = True
+
+               path = self.get_argument("path", "")
+               if path is None:
+                       errors["path_invalid"] = True
+
+               if errors:
+                       errors.update({
+                               "hostname" : hostname,
+                               "path" : path,
+                       })
+                       return self.get(**errors)
+
+               print hostname, path
+
+               mirror = backend.mirrors.Mirror.create(self.pakfire, hostname, path,
+                       user=self.current_user)
+               assert mirror
+
+               self.redirect("/mirror/%s" % mirror.hostname)
+
+
+class MirrorEditHandler(MirrorActionHandler):
+       @tornado.web.authenticated
+       def get(self, hostname):
+               mirror = self.pakfire.mirrors.get_by_hostname(hostname)
+               if not mirror:
+                       raise tornado.web.HTTPError(404, "Could not find mirror: %s" % hostname)
+
+               self.render("mirrors-edit.html", mirror=mirror)
+
+       @tornado.web.authenticated
+       def post(self, hostname):
+               mirror = self.pakfire.mirrors.get_by_hostname(hostname)
+               if not mirror:
+                       raise tornado.web.HTTPError(404, "Could not find mirror: %s" % hostname)
+
+               hostname = self.get_argument("name")
+               path     = self.get_argument("path", "")
+               owner    = self.get_argument("owner", None)
+               contact  = self.get_argument("contact", None)
+               enabled  = self.get_argument("enabled", None)
+
+               if enabled:
+                       mirror.set_status("enabled")
+               else:
+                       mirror.set_status("disabled")
+
+               mirror.hostname = hostname
+               mirror.path     = path
+               mirror.owner    = owner
+               mirror.contact  = contact
+
+               self.redirect("/mirror/%s" % mirror.hostname)
+
+
+class MirrorDeleteHandler(MirrorActionHandler):
+       @tornado.web.authenticated
+       def get(self, hostname):
+               mirror = self.pakfire.mirrors.get_by_hostname(hostname)
+               if not mirror:
+                       raise tornado.web.HTTPError(404, "Could not find mirror: %s" % hostname)
+
+               confirmed = self.get_argument("confirmed", None)        
+               if confirmed:
+                       mirror.set_status("deleted", user=self.current_user)
+
+                       self.redirect("/mirrors")
+                       return
+
+               self.render("mirrors-delete.html", mirror=mirror)
diff --git a/web/handlers_packages.py b/web/handlers_packages.py
new file mode 100644 (file)
index 0000000..857647c
--- /dev/null
@@ -0,0 +1,154 @@
+#!/usr/bin/python
+
+import tornado.web
+
+from handlers_base import BaseHandler
+
+class PackageIDDetailHandler(BaseHandler):
+       def get(self, id):
+               package = self.packages.get_by_id(id)
+               if not package:
+                       return tornado.web.HTTPError(404, "Package not found: %s" % id)
+
+               self.render("package-detail.html", package=package)
+
+
+class PackageListHandler(BaseHandler):
+       def get(self):
+               packages = {}
+
+               show = self.get_argument("show", None)
+               if show == "all":
+                       states = None
+               elif show == "obsoletes":
+                       states = ["obsolete"]
+               elif show == "broken":
+                       states = ["broken"]
+               else:
+                       states = ["building", "stable", "testing"]
+
+               # Get all packages that fulfill the required parameters.
+               pkgs = self.pakfire.packages.get_all_names(public=self.public,
+                       user=self.current_user, states=states)
+
+               # Sort all packages in an array like "<first char>" --> [packages, ...]
+               # to print them in a table for each letter of the alphabet.
+               for pkg in pkgs:
+                       c = pkg[0][0].lower()
+
+                       if not packages.has_key(c):
+                               packages[c] = []
+
+                       packages[c].append(pkg)
+
+               self.render("packages-list.html", packages=packages)
+
+
+class PackageNameHandler(BaseHandler):
+       def get(self, name):
+               builds = {
+                       "release" : [],
+                       "scratch" : [],
+               }
+
+               query = self.pakfire.builds.get_by_name(name, public=self.public,
+                       user=self.current_user)
+
+               if not query:
+                       raise tornado.web.HTTPError(404, "Package '%s' was not found" % name)
+
+               for build in query:
+                       try:
+                               builds[build.type].append(build)
+                       except KeyError:
+                               logging.warning("Unknown build type: %s" % build.type)
+
+               latest_build = None
+               for type in builds.keys():
+                       # Take info from the most recent package.
+                       if builds[type]:
+                               latest_build = builds[type][-1]
+                               break
+
+               assert latest_build
+
+               # Move the latest builds to the top.
+               for type in builds.keys():
+                       builds[type].reverse()
+
+               # Get the average build times of this package.
+               build_times = self.pakfire.packages.get_avg_build_times(name)
+
+               # Get the latest bugs from bugzilla.
+               bugs = self.pakfire.bugzilla.get_bugs_from_component(name)
+
+               kwargs = {
+                       "release_builds" : builds["release"],
+                       "scratch_builds" : builds["scratch"],
+               }
+
+               self.render("package-detail-list.html", builds=builds,
+                       latest_build=latest_build, pkg=latest_build.pkg,
+                       build_times=build_times, bugs=bugs, **kwargs)
+
+
+class PackageDetailHandler(BaseHandler):
+       def get(self, uuid):
+               pkg = self.pakfire.packages.get_by_uuid(uuid)
+               if not pkg:
+                       raise tornado.web.HTTPError(404, "Package not found: %s" % uuid)
+
+               self.render("package-detail.html", pkg=pkg)
+
+       @tornado.web.authenticated
+       def post(self, name, epoch, version, release):
+               pkg = self.pakfire.packages.get_by_tuple(name, epoch, version, release)
+
+               action = self.get_argument("action", None)
+
+               if action == "comment":
+                       vote = self.get_argument("vote", None)
+                       if not self.current_user.is_tester() and \
+                                       not self.current_user.is_admin():
+                               vote = None
+
+                       pkg.comment(self.current_user.id, self.get_argument("text"),
+                               vote or "none")
+
+               self.render("package-detail.html", pkg=pkg)
+
+
+class PackagePropertiesHandler(BaseHandler):
+       @tornado.web.authenticated
+       def get(self, name):
+               build = self.pakfire.builds.get_latest_by_name(name, public=self.public)
+
+               if not build:
+                       raise tornado.web.HTTPError(404, "Package '%s' was not found" % name)
+
+               # Check if the user has sufficient permissions.
+               if not build.has_perm(self.current_user):
+                       raise tornado.web.HTTPError(403, "User %s is not allowed to manage build %s" \
+                               % (self.current_user, build))
+
+               self.render("package-properties.html", build=build,
+                       pkg=build.pkg, properties=build.pkg.properties)
+
+       @tornado.web.authenticated
+       def post(self, name):
+               build = self.pakfire.builds.get_latest_by_name(name, public=self.public)
+
+               if not build:
+                       raise tornado.web.HTTPError(404, "Package '%s' was not found" % name)
+
+               # Check if the user has sufficient permissions.
+               if not build.has_perm(self.current_user):
+                       raise tornado.web.HTTPError(403, "User %s is not allowed to manage build %s" \
+                               % (self.current_user, build))
+
+               critical_path = self.get_argument("critical_path", False)
+               if critical_path:
+                       critical_path = True
+               else:
+                       critical_path = False
+               build.pkg.update_property("critical_path", critical_path)
index 6c830e64dab3a6512adda5ff7c865f481fea2ff1..dd9eca6a09908fc2f0d5263688db2718b6a6a860 100644 (file)
@@ -1,14 +1,62 @@
 #!/usr/bin/python
 
+import re
+
 from handlers_base import *
 
 class SearchHandler(BaseHandler):
        def get(self):
-               query = self.get_argument("q", "")
-               if not query:
-                       self.render("search-form.html")
+               pattern = self.get_argument("q", "")
+               if not pattern:
+                       self.render("search-form.html", pattern="")
                        return
 
-               pkgs = self.pakfire.packages.search(query)
+               # Check if the given search pattern is a UUID.
+               if re.match(r"^([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})$", pattern):
+                       # Search for a matching object and redirect to it.
+
+                       # Search in packages.
+                       pkg = self.pakfire.packages.get_by_uuid(pattern)
+                       if pkg:
+                               self.redirect("/package/%s" % pkg.uuid)
+                               return
+
+                       # Search in builds.
+                       build = self.pakfire.builds.get_by_uuid(pattern)
+                       if build:
+                               self.redirect("/build/%s" % build.uuid)
+                               return
+
+                       # Search in jobs.
+                       job = self.pakfire.jobs.get_by_uuid(pattern)
+                       if job:
+                               self.redirect("/job/%s" % job.uuid)
+                               return
+
+               pkgs = files = users = []
+
+               if pattern.startswith("/"):
+                       # Do a file search.
+                       files = self.pakfire.packages.search_by_filename(pattern, limit=50)
+
+               else:
+                       # Make fulltext search in the packages.
+                       pkgs = self.pakfire.packages.search(pattern, limit=50)
+
+                       # Search for users.
+                       users = self.pakfire.users.search(pattern, limit=50)
+
+               if len(pkgs) == 1 and not files and not users:
+                       pkg = pkgs[0]
+
+                       self.redirect("/package/%s" % pkg.name)
+                       return
+
+               # If we have results, we show them.
+               if pkgs or files or users:
+                       self.render("search-results.html", pattern=pattern,
+                               pkgs=pkgs, files=files, users=users)
+                       return
 
-               self.render("search-results.html", query=query, pkgs=pkgs)
+               # If there were no results, we show the advanced search site.
+               self.render("search-form.html", pattern=pattern)
diff --git a/web/handlers_updates.py b/web/handlers_updates.py
new file mode 100644 (file)
index 0000000..a1028f9
--- /dev/null
@@ -0,0 +1,9 @@
+#!/usr/bin/python
+
+import tornado.web
+
+from handlers_base import BaseHandler
+
+class UpdatesHandler(BaseHandler):
+       def get(self):
+               self.render("updates-index.html")
index c45379b79770493e604cbdca2d03319aa86f1549..f190b6df9201e0927f4dcc3c7a288ebf101cafe2 100644 (file)
@@ -1,5 +1,8 @@
 #!/usr/bin/python
 
+import datetime
+import pytz
+
 import tornado.locale
 import tornado.web
 
@@ -15,12 +18,52 @@ class UserHandler(BaseHandler):
                        if not user:
                                raise tornado.web.HTTPError(404, "User does not exist: %s" % name)
 
-               self.render("user-profile.html", user=user)
+               log = user.get_history(limit=10)
+               comments = user.get_comments(limit=5)
 
+               self.render("user-profile.html", user=user, log=log, comments=comments)
 
-class UserDeleteHandler(BaseHandler):
+
+class UserImpersonateHandler(BaseHandler):
        @tornado.web.authenticated
-       def get(self, name):
+       def get(self):
+               action = self.get_argument("action", "start")
+
+               if action == "stop":
+                       self.session.stop_impersonation()
+                       self.redirect("/")
+                       return
+
+               # You must be an admin to do this.
+               if not self.current_user.is_admin():
+                       raise tornado.web.HTTPError(403, "You are not allowed to do this.")
+
+               username = self.get_argument("user", "")
+               user = self.pakfire.users.get_by_name(username)
+               if not user:
+                       raise tornado.web.HTTPError(404, "User not found: %s" % username)
+
+               self.render("user-impersonation.html", user=user)
+
+       @tornado.web.authenticated
+       def post(self):
+               # You must be an admin to do this.
+               if not self.current_user.is_admin():
+                       raise tornado.web.HTTPError(403, "You are not allowed to do this.")
+
+               username = self.get_argument("user")
+               user = self.pakfire.users.get_by_name(username)
+               if not user:
+                       raise tornado.web.HTTPError(404, "User does not exist: %s" % username)
+
+               self.session.start_impersonation(user)
+
+               # Redirect to start page.
+               self.redirect("/")
+
+
+class UserActionHandler(BaseHandler):
+       def get_user(self, name):
                user = self.pakfire.users.get_by_name(name)
                if not user:
                        raise tornado.web.HTTPError(404)
@@ -28,6 +71,14 @@ class UserDeleteHandler(BaseHandler):
                if not self.current_user == user and not self.current_user.is_admin():
                        raise tornado.web.HTTPError(403)
 
+               return user
+
+
+class UserDeleteHandler(BaseHandler):
+       @tornado.web.authenticated
+       def get(self, name):
+               user = self.get_user(name)
+
                confirmed = self.get_argument("confirmed", None)
                if confirmed:
                        user.delete()
@@ -40,6 +91,55 @@ class UserDeleteHandler(BaseHandler):
                self.render("user-delete.html", user=user)
 
 
+class UserPasswdHandler(UserActionHandler):
+       @tornado.web.authenticated
+       def get(self, name, error_msg=None):
+               user = self.get_user(name)
+
+               self.render("user-profile-passwd.html", user=user,
+                       error_msg=error_msg)
+
+       @tornado.web.authenticated
+       def post(self, name):
+               _ = self.locale.translate
+
+               # Fetch the user.
+               user = self.get_user(name)              
+
+               # If the user who wants to change the password is not an admin,
+               # he needs to provide the old password.
+               if not self.current_user.is_admin() or self.current_user == user:
+                       pass0 = self.get_argument("pass0", None)
+                       if not pass0:
+                               return self.get(name, error_msg=_("You need to enter you current password."))
+
+                       if not self.current_user.check_password(pass0):
+                               return self.get(name, error_msg=_("The provided account password is wrong."))
+
+               pass1 = self.get_argument("pass1", "")
+               pass2 = self.get_argument("pass2", "")
+
+               error_msg = None
+
+               # The password must at least have 8 characters.
+               if not pass1 == pass2:
+                       error_msg = _("The given passwords do not match.")
+               elif len(pass1) == 0:
+                       error_msg = _("The password was blank.")
+               else:
+                       accepted, score = backend.users.check_password_strength(pass1)
+                       if not accepted:
+                               error_msg = _("The given password is too weak.")
+
+               if error_msg:
+                       return self.get(name, error_msg=error_msg)
+
+               # Update the password.
+               user.set_passphrase(pass1)
+
+               self.render("user-profile-passwd-ok.html", user=user)
+
+
 class UserEditHandler(BaseHandler):
        def prepare(self):
                # Make list of all supported locales.
@@ -56,6 +156,7 @@ class UserEditHandler(BaseHandler):
                        raise tornado.web.HTTPError(403)
 
                self.render("user-profile-edit.html", user=user,
+                       supported_timezones=pytz.common_timezones,
                        supported_locales=self.supported_locales)
 
        @tornado.web.authenticated
@@ -108,6 +209,10 @@ class UserEditHandler(BaseHandler):
                        user.passphrase = pass1
                user.state = state
 
+               # Get the timezone settings.
+               tz = self.get_argument("timezone", None)
+               user.timezone = tz
+
                if not user.activated:
                        self.render("user-profile-need-activation.html", user=user)
                        return
@@ -139,3 +244,66 @@ class UsersCommentsHandler(BaseHandler):
 
                self.render("user-comments.html", comments=comments)
 
+
+class UsersBuildsHandler(BaseHandler):
+       def __chunk_by_name(self, _builds):
+               builds = {}
+
+               for build in _builds:
+                       i = build.pkg.name[0]
+                       try:
+                               builds[i].append(build)
+                       except KeyError:
+                               builds[i] = [build,]
+               
+               return builds
+               #return [v for k,v in sorted(builds.items())]
+
+       def __chunk_by_date(self, _builds):
+               # XXX dummy function
+               builds = {
+                       datetime.datetime.utcnow() : _builds,
+               }
+
+               return builds
+
+               for build in _builds:
+                       builds.append([build,])
+
+               return builds
+
+       def get(self, name=None):
+               if name:
+                       user = self.pakfire.users.get_by_name(name)
+                       if not user:
+                               raise tornado.web.HTTPError(404, "User not found: %s" % name)
+               else:
+                       user = self.current_user
+
+               # By default users see only public builds.
+               # Admins are allowed to see all builds.
+               public = True
+               if self.current_user and self.current_user.is_admin():
+                       public = None
+
+               # Select the type of the builds that are shown.
+               # None for all.
+               type = self.get_argument("type", None)
+
+               # Select how to order the results. The default is by date.
+               order_by = self.get_argument("order_by", "date")
+               if not order_by in ("date", "name"):
+                       order_by = "date"
+
+               # Get a list of the builds this user has built.
+               builds = self.pakfire.builds.get_by_user_iter(user, type=type,
+                       public=public, order_by=order_by)
+
+               if builds:
+                       # Chunk the list for a better presentation.
+                       if order_by == "date":
+                               builds = self.__chunk_by_date(builds)
+                       elif order_by == "name":
+                               builds = self.__chunk_by_name(builds)
+
+               self.render("user-profile-builds.html", user=user, builds=builds)
index ca3d0f0a72c551c899f22f1494a96bff4dbbce0e..1550318094cf6b618d4f0c18c29eb8a13ffcc7ac 100644 (file)
@@ -1,6 +1,11 @@
+#!/usr/bin/python
 
+import re
+import string
+import tornado.escape
 import tornado.web
 
+import backend.users
 from backend.constants import *
 
 class UIModule(tornado.web.UIModule):
@@ -8,11 +13,86 @@ class UIModule(tornado.web.UIModule):
        def pakfire(self):
                return self.handler.application.pakfire
 
+       @property
+       def settings(self):
+               return self.pakfire.settings
+
+
+class TextModule(UIModule):
+       def render(self, text, pre=True):
+               link = """<a href="%s" target="_blank">%s</a>"""
+
+               bz_url = self.settings.get("bugzilla_url", "")
+               bz_pattern = re.compile(r"(bug\s?|#)(\d+)")
+               bz_repl = link % (bz_url % { "bugid" : r"\2" }, r"\1\2")
+
+               cve_url = self.settings.get("cve_url", "")
+               cve_pattern = re.compile(r"(CVE)(\s|\-)(\d{4}\-\d{4})")
+               cve_repl = link % (cve_url % r"\3", r"\1\2\3")
+
+               o = []
+               for p in text.splitlines():
+                       # Escape the text and create make urls clickable.
+                       p = tornado.escape.xhtml_escape(p)
+                       p = tornado.escape.linkify(p, shorten=True,
+                               extra_params='target="_blank"')
+
+                       # Search for bug ids that need to be linked to bugzilla.
+                       if bz_url:
+                               p = re.sub(bz_pattern, bz_repl, p, re.I|re.U)
+
+                       # Search for CVE numbers and create hyperlinks.
+                       if cve_url:
+                               p = re.sub(cve_pattern, cve_repl, p, re.I|re.U)
+
+                       o.append(p)
+
+               o = "\n".join(o)
+
+               if pre:
+                       return "<pre>%s</pre>" % o
+
+               return o.replace("\n", "<br />")
+
+
+class ModalModule(UIModule):
+       def render(self, what, **kwargs):
+               what = "modules/modal-%s.html" % what
 
-class PackageTableModule(UIModule):
-       def render(self, letter, packages):
-               return self.render_string("modules/package-table.html",
-                       letter=letter, packages=packages)
+               return self.render_string(what, **kwargs)
+
+
+class BuildHeadlineModule(UIModule):
+       def render(self, prefix, build, short=False, shorter=False):
+               if shorter:
+                       short = True
+
+               return self.render_string("modules/build-headline.html",
+                       prefix=prefix, build=build, pkg=build.pkg, short=short, shorter=shorter)
+
+
+class BugsTableModule(UIModule):
+       def render(self, pkg, bugs):
+               return self.render_string("modules/bugs-table.html",
+                       pkg=pkg, bugs=bugs)
+
+
+class CommitsTableModule(UIModule):
+       def render(self, distro, source, commits, full_format=True):
+               return self.render_string("modules/commits-table.html",
+                       distro=distro, source=source, commits=commits,
+                       full_format=full_format)
+
+
+class FooterModule(UIModule):
+       def render(self):
+               return self.render_string("modules/footer.html")
+
+
+class PackagesTableModule(UIModule):
+       def render(self, job, packages):
+               return self.render_string("modules/packages-table.html", job=job,
+                       packages=packages)
 
 
 class PackageTable2Module(UIModule):
@@ -26,14 +106,56 @@ class FilesTableModule(UIModule):
                return self.render_string("modules/files-table.html", files=files)
 
 
+class LogFilesTableModule(UIModule):
+       def render(self, job, files):
+               return self.render_string("modules/log-files-table.html", job=job,
+                       files=files)
+
+
+class PackageHeaderModule(UIModule):
+       def render(self, pkg):
+               return self.render_string("modules/package-header.html", pkg=pkg)
+
+
 class PackageFilesTableModule(UIModule):
-       def render(self, files):
-               return self.render_string("modules/package-files-table.html", files=files)
+       def render(self, pkg, filelist):
+               return self.render_string("modules/packages-files-table.html",
+                       pkg=pkg, filelist=filelist)
 
 
 class BuildTableModule(UIModule):
-       def render(self, builds):
-               return self.render_string("modules/build-table.html", builds=builds)
+       def render(self, builds, **kwargs):
+               settings = dict(
+                       show_user = False,
+                       show_repo = False,
+                       show_repo_time = False,
+                       show_can_move_forward = False,
+                       show_when = True,
+               )
+               settings.update(kwargs)
+
+               return self.render_string("modules/build-table.html",
+                       builds=builds, **settings)
+
+
+class BuildStateWarningsModule(UIModule):
+       def render(self, build):
+               return self.render_string("modules/build-state-warnings.html", build=build)
+
+
+class JobsTableModule(UIModule):
+       def render(self, build, jobs=None, type="release"):
+               if jobs is None:
+                       jobs = build.jobs
+
+               return self.render_string("modules/jobs-table.html", build=build,
+                       jobs=jobs, type=type)
+
+
+class JobsListModule(UIModule):
+       def render(self, jobs, show_builder=False):
+               return self.render_string("modules/jobs-list.html", jobs=jobs,
+                       show_builder=show_builder)
 
 
 class RepositoryTableModule(UIModule):
@@ -74,6 +196,35 @@ class CommentsTableModule(UIModule):
                        comments=comments, show_package=show_package, show_user=show_user)
 
 
+class LogModule(UIModule):
+       def render(self, entries, **args):
+               return self.render_string("modules/log.html",
+                       entries=entries, args=args)
+
+
+class LogEntryModule(UIModule):
+       def render(self, entry, **args):
+               return self.render_string("modules/log-entry.html",
+                       entry=entry, **args)
+
+
+class LogEntryCommentModule(LogEntryModule):
+       def render(self, entry, **args):
+               return self.render_string("modules/log-entry-comment.html",
+                       entry=entry, **args)
+
+
+class MaintainerModule(UIModule):
+       def render(self, maintainer):
+               if isinstance(maintainer, backend.users.User):
+                       type = "user"
+               else:
+                       type = "string"
+
+               return self.render_string("modules/maintainer.html",
+                       type=type, maintainer=maintainer)
+
+
 class BuildLogModule(UIModule):
        # XXX deprecated
        def render(self, messages):
@@ -123,3 +274,20 @@ class RepoActionsTableModule(UIModule):
 
                return self.render_string("modules/repo-actions-table.html",
                        repo=repo, actions=actions)
+
+
+class UpdatesTableModule(UIModule):
+       def render(self, updates):
+               return self.render_string("modules/updates-table.html", updates=updates)
+
+
+class WatchersSidebarTableModule(UIModule):
+       def css_files(self):
+               return "css/watchers-sidebar-table.css"
+
+       def render(self, build, watchers, limit=5):
+               # Sort the watchers by their realname.
+               watchers.sort(key=lambda watcher: watcher.realname)
+
+               return self.render_string("modules/watchers-sidebar-table.html",
+                       build=build, watchers=watchers, limit=limit)