From 9137135a5912aa6ae71da8b0ca59d6fd29b949e8 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Sat, 8 Oct 2011 11:40:47 +0200 Subject: [PATCH] Initial import. --- .gitignore | 2 + Makefile | 24 + backend/__init__.py | 67 + backend/base.py | 19 + backend/build.py | 553 ++++++ backend/builders.py | 210 +++ backend/constants.py | 65 + backend/database.py | 54 + backend/distribution.py | 104 ++ backend/managers.py | 160 ++ backend/messages.py | 122 ++ backend/misc.py | 42 + backend/packages.py | 539 ++++++ backend/repository.py | 287 +++ backend/settings.py | 49 + backend/sources.py | 171 ++ backend/uploads.py | 148 ++ backend/users.py | 232 +++ data/static/css/style.css | 604 ++++++ data/static/favicon.ico | Bin 0 -> 4286 bytes .../images/icons/build-dependency_error.png | Bin 0 -> 2085 bytes .../static/images/icons/build-dispatching.png | Bin 0 -> 4839 bytes data/static/images/icons/build-failed.png | Bin 0 -> 4681 bytes data/static/images/icons/build-finished.png | Bin 0 -> 4434 bytes data/static/images/icons/build-pending.png | Bin 0 -> 5305 bytes data/static/images/icons/build-running.png | Bin 0 -> 4611 bytes data/static/images/icons/build-unknown.png | Bin 0 -> 2727 bytes data/static/images/icons/build-uploading.png | Bin 0 -> 5109 bytes data/static/images/icons/build-waiting.png | Bin 0 -> 1324 bytes data/static/images/icons/builder-disabled.png | Bin 0 -> 4347 bytes data/static/images/icons/slave-offline.png | 1 + data/static/images/icons/slave-online.png | 1 + data/static/images/img01.jpg | Bin 0 -> 1287 bytes data/static/images/img02.jpg | Bin 0 -> 391 bytes data/static/images/img03.jpg | Bin 0 -> 6321 bytes data/static/images/img04.jpg | Bin 0 -> 355 bytes data/static/images/ipfire_tux_128x128.png | Bin 0 -> 23763 bytes data/static/images/readme.txt | 3 + data/static/js/jquery-1.6.min.js | 16 + data/static/js/pbs.js | 42 + data/static/robots.txt | 3 + data/templates/base.html | 109 ++ data/templates/build-detail.html | 151 ++ data/templates/build-filter.html | 55 + data/templates/build-list.html | 15 + data/templates/build-priority.html | 41 + data/templates/build-schedule-rebuild.html | 15 + data/templates/build-schedule-test.html | 20 + data/templates/builder-delete.html | 16 + data/templates/builder-detail.html | 98 + data/templates/builder-edit.html | 80 + data/templates/builder-list.html | 29 + data/templates/builder-new.html | 27 + data/templates/builder-pass.html | 22 + data/templates/distro-detail.html | 28 + data/templates/distro-edit.html | 81 + data/templates/distro-list.html | 21 + data/templates/docs-base.html | 14 + data/templates/docs-build.html | 28 + data/templates/docs-index.html | 27 + data/templates/docs-users.html | 41 + data/templates/error-404.html | 5 + data/templates/error-500.html | 9 + data/templates/error.html | 1 + data/templates/file-detail.html | 120 ++ data/templates/index.html | 35 + data/templates/log.html | 7 + data/templates/login-successful.html | 16 + data/templates/login.html | 36 + data/templates/logout.html | 14 + data/templates/modules/build-log.html | 13 + data/templates/modules/build-offset.html | 25 + data/templates/modules/build-table.html | 20 + data/templates/modules/comments-table.html | 21 + data/templates/modules/files-table.html | 14 + data/templates/modules/log-table.html | 28 + .../modules/package-files-table.html | 16 + .../modules/package-table-detail.html | 7 + data/templates/modules/package-table.html | 14 + .../templates/modules/repo-actions-table.html | 45 + data/templates/modules/repository-table.html | 8 + data/templates/modules/source-table.html | 7 + data/templates/modules/user-table.html | 5 + data/templates/package-detail-list.html | 15 + data/templates/package-detail.html | 111 ++ data/templates/package-list.html | 23 + data/templates/register-activation-fail.html | 10 + .../register-activation-success.html | 9 + data/templates/register-fail.html | 19 + data/templates/register-success.html | 9 + data/templates/register.html | 63 + data/templates/repository-detail.html | 31 + data/templates/search-form.html | 11 + data/templates/search-results.html | 21 + data/templates/source-detail.html | 25 + data/templates/source-list.html | 21 + data/templates/user-comments.html | 14 + data/templates/user-delete.html | 20 + data/templates/user-list.html | 30 + data/templates/user-profile-edit-fail.html | 19 + data/templates/user-profile-edit.html | 97 + .../user-profile-need-activation.html | 10 + data/templates/user-profile.html | 58 + .../translations/de_DE/LC_MESSAGES/pakfire.mo | Bin 0 -> 29731 bytes .../translations/de_DE/LC_MESSAGES/pakfire.po | 1627 +++++++++++++++++ .../de_DE/LC_MESSAGES/pakfire.po.sic | 1090 +++++++++++ data/translations/pakfire.pot | 1571 ++++++++++++++++ master/__init__.py | 71 + master/handlers.py | 316 ++++ pakfire-manager | 50 + pakfire-master | 7 + pakfire-web | 31 + web/__init__.py | 156 ++ web/handlers.py | 316 ++++ web/handlers_auth.py | 113 ++ web/handlers_base.py | 77 + web/handlers_builders.py | 87 + web/handlers_search.py | 14 + web/handlers_users.py | 141 ++ web/ui_modules.py | 125 ++ 120 files changed, 11310 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 backend/__init__.py create mode 100644 backend/base.py create mode 100644 backend/build.py create mode 100644 backend/builders.py create mode 100644 backend/constants.py create mode 100644 backend/database.py create mode 100644 backend/distribution.py create mode 100644 backend/managers.py create mode 100644 backend/messages.py create mode 100644 backend/misc.py create mode 100644 backend/packages.py create mode 100644 backend/repository.py create mode 100644 backend/settings.py create mode 100644 backend/sources.py create mode 100644 backend/uploads.py create mode 100644 backend/users.py create mode 100644 data/static/css/style.css create mode 100644 data/static/favicon.ico create mode 100644 data/static/images/icons/build-dependency_error.png create mode 100644 data/static/images/icons/build-dispatching.png create mode 100644 data/static/images/icons/build-failed.png create mode 100644 data/static/images/icons/build-finished.png create mode 100644 data/static/images/icons/build-pending.png create mode 100644 data/static/images/icons/build-running.png create mode 100644 data/static/images/icons/build-unknown.png create mode 100644 data/static/images/icons/build-uploading.png create mode 100644 data/static/images/icons/build-waiting.png create mode 100644 data/static/images/icons/builder-disabled.png create mode 120000 data/static/images/icons/slave-offline.png create mode 120000 data/static/images/icons/slave-online.png create mode 100644 data/static/images/img01.jpg create mode 100644 data/static/images/img02.jpg create mode 100644 data/static/images/img03.jpg create mode 100644 data/static/images/img04.jpg create mode 100644 data/static/images/ipfire_tux_128x128.png create mode 100644 data/static/images/readme.txt create mode 100644 data/static/js/jquery-1.6.min.js create mode 100644 data/static/js/pbs.js create mode 100644 data/static/robots.txt create mode 100644 data/templates/base.html create mode 100644 data/templates/build-detail.html create mode 100644 data/templates/build-filter.html create mode 100644 data/templates/build-list.html create mode 100644 data/templates/build-priority.html create mode 100644 data/templates/build-schedule-rebuild.html create mode 100644 data/templates/build-schedule-test.html create mode 100644 data/templates/builder-delete.html create mode 100644 data/templates/builder-detail.html create mode 100644 data/templates/builder-edit.html create mode 100644 data/templates/builder-list.html create mode 100644 data/templates/builder-new.html create mode 100644 data/templates/builder-pass.html create mode 100644 data/templates/distro-detail.html create mode 100644 data/templates/distro-edit.html create mode 100644 data/templates/distro-list.html create mode 100644 data/templates/docs-base.html create mode 100644 data/templates/docs-build.html create mode 100644 data/templates/docs-index.html create mode 100644 data/templates/docs-users.html create mode 100644 data/templates/error-404.html create mode 100644 data/templates/error-500.html create mode 100644 data/templates/error.html create mode 100644 data/templates/file-detail.html create mode 100644 data/templates/index.html create mode 100644 data/templates/log.html create mode 100644 data/templates/login-successful.html create mode 100644 data/templates/login.html create mode 100644 data/templates/logout.html create mode 100644 data/templates/modules/build-log.html create mode 100644 data/templates/modules/build-offset.html create mode 100644 data/templates/modules/build-table.html create mode 100644 data/templates/modules/comments-table.html create mode 100644 data/templates/modules/files-table.html create mode 100644 data/templates/modules/log-table.html create mode 100644 data/templates/modules/package-files-table.html create mode 100644 data/templates/modules/package-table-detail.html create mode 100644 data/templates/modules/package-table.html create mode 100644 data/templates/modules/repo-actions-table.html create mode 100644 data/templates/modules/repository-table.html create mode 100644 data/templates/modules/source-table.html create mode 100644 data/templates/modules/user-table.html create mode 100644 data/templates/package-detail-list.html create mode 100644 data/templates/package-detail.html create mode 100644 data/templates/package-list.html create mode 100644 data/templates/register-activation-fail.html create mode 100644 data/templates/register-activation-success.html create mode 100644 data/templates/register-fail.html create mode 100644 data/templates/register-success.html create mode 100644 data/templates/register.html create mode 100644 data/templates/repository-detail.html create mode 100644 data/templates/search-form.html create mode 100644 data/templates/search-results.html create mode 100644 data/templates/source-detail.html create mode 100644 data/templates/source-list.html create mode 100644 data/templates/user-comments.html create mode 100644 data/templates/user-delete.html create mode 100644 data/templates/user-list.html create mode 100644 data/templates/user-profile-edit-fail.html create mode 100644 data/templates/user-profile-edit.html create mode 100644 data/templates/user-profile-need-activation.html create mode 100644 data/templates/user-profile.html create mode 100644 data/translations/de_DE/LC_MESSAGES/pakfire.mo create mode 100644 data/translations/de_DE/LC_MESSAGES/pakfire.po create mode 100644 data/translations/de_DE/LC_MESSAGES/pakfire.po.sic create mode 100644 data/translations/pakfire.pot create mode 100644 master/__init__.py create mode 100644 master/handlers.py create mode 100644 pakfire-manager create mode 100644 pakfire-master create mode 100644 pakfire-web create mode 100644 web/__init__.py create mode 100644 web/handlers.py create mode 100644 web/handlers_auth.py create mode 100644 web/handlers_base.py create mode 100644 web/handlers_builders.py create mode 100644 web/handlers_search.py create mode 100644 web/handlers_users.py create mode 100644 web/ui_modules.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49cbba5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.py[co] +.mo diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b975ccd --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ + +PO_PATH = data/translations +POTFILE = $(PO_PATH)/pakfire.pot +POFILES = $(wildcard $(PO_PATH)/*/LC_MESSAGES/*.po) + +ALL_FILES = $(shell find . /usr/lib*/python*/site-packages/tornado -name "*.py" -or -name "*.html") + +.PHONY: pot +pot: $(POTFILE) + +$(POTFILE): $(ALL_FILES) + xgettext --language=Python --from-code=UTF-8 --keyword=_:1,2 --keyword=N_ -d pakfire -o $(POTFILE) \ + $(ALL_FILES) + +.PHONY: po +po: $(POTFILE) $(patsubst %.po, %.mo, $(POFILES)) + +%.po: $(POTFILE) + # Merge the POTFILE. + msgmerge $@ $(POTFILE) -o $@ + +%.mo: %.po + # Compile the translations. + msgfmt $< -o $@ diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..45c331d --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1,67 @@ +#!/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) diff --git a/backend/base.py b/backend/base.py new file mode 100644 index 0000000..7aeafcf --- /dev/null +++ b/backend/base.py @@ -0,0 +1,19 @@ +#!/usr/bin/python + + +class Object(object): + """ + Main object where all other objects inherit from. + + This is used to access the global instance of Pakfire + and hold the database connection. + """ + def __init__(self, pakfire): + self.pakfire = pakfire + + # Shortcut to the database. + self.db = self.pakfire.db + + # Shortcut to settings. + if hasattr(self.pakfire, "settings"): + self.settings = self.pakfire.settings diff --git a/backend/build.py b/backend/build.py new file mode 100644 index 0000000..80f6c1e --- /dev/null +++ b/backend/build.py @@ -0,0 +1,553 @@ +#!/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 diff --git a/backend/builders.py b/backend/builders.py new file mode 100644 index 0000000..4f25075 --- /dev/null +++ b/backend/builders.py @@ -0,0 +1,210 @@ +#!/usr/bin/python + +import datetime +import hashlib +import logging +import random +import string +import time + +import base + +class Builders(base.Object): + def get_all(self): + builders = self.db.query("SELECT id FROM builders WHERE deleted = 'N' ORDER BY name") + + return [Builder(self.pakfire, b.id) for b in builders] + + def get_by_id(self, id): + if not id: + return + + return Builder(self.pakfire, id) + + def get_by_name(self, name): + builder = self.db.get("SELECT id FROM builders WHERE name = %s LIMIT 1", name) + + if builder: + return Builder(self.pakfire, builder.id) + + def get_all_arches(self): + arches = set() + + for result in self.db.query("SELECT DISTINCT arches FROM builders"): + if not result.arches: + continue + + _arches = result.arches.split() + + for arch in _arches: + arches.add(arch) + + return sorted(arches) + + +class Builder(base.Object): + def __init__(self, pakfire, id): + base.Object.__init__(self, pakfire) + + self.id = id + + self.data = self.db.get("SELECT * FROM builders WHERE id = %s", self.id) + + def __cmp__(self, other): + return cmp(self.id, other.id) + + @classmethod + def new(cls, pakfire, name): + id = pakfire.db.execute("INSERT INTO builders(name) VALUES(%s)", name) + + builder = cls(pakfire, id) + builder.regenerate_passphrase() + + return builder + + 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)) + + self.set("passphrase", passphrase) + + def validate_passphrase(self, passphrase): + return self.passphrase == passphrase + + 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 get_enabled(self): + return not self.disabled + + def set_enabled(self, value): + if value: + value = "N" + else: + value = "Y" + + self.set("disabled", value) + + enabled = property(get_enabled, set_enabled) + + @property + def disabled(self): + return self.data.disabled == "Y" + + @property + def arches(self): + arches = ["noarch",] + + if self.build_src: + arches.append("src") + + if self.data.arches: + arches += self.data.arches.split() + + return sorted(arches) + + def get_build_src(self): + return self.data.build_src == "Y" + + def set_build_src(self, value): + if value: + value = "Y" + else: + value = "N" + + self.set("build_src", value) + + build_src = property(get_build_src, set_build_src) + + def get_build_bin(self): + return self.data.build_bin == "Y" + + def set_build_bin(self, value): + if value: + value = "Y" + else: + value = "N" + + self.set("build_bin", value) + + build_bin = property(get_build_bin, set_build_bin) + + def get_build_test(self): + return self.data.build_test == "Y" + + def set_build_test(self, value): + if value: + value = "Y" + else: + value = "N" + + self.set("build_test", value) + + build_test = property(get_build_test, set_build_test) + + def get_max_jobs(self): + return self.data.max_jobs + + def set_max_jobs(self, value): + self.set("max_jobs", value) + + max_jobs = property(get_max_jobs, set_max_jobs) + + @property + def name(self): + return self.data.name + + @property + def hostname(self): + return self.name + + @property + def passphrase(self): + return self.data.passphrase + + @property + def loadavg(self): + if not self.status == "ONLINE": + return 0 + + return self.data.loadavg + + @property + def cpu_model(self): + return self.data.cpu_model + + @property + def memory(self): + return self.data.memory * 1024 + + @property + def status(self): + if self.disabled: + return "DISABLED" + + threshhold = datetime.datetime.utcnow() - datetime.timedelta(minutes=6) + + if self.data.updated < threshhold: + return "OFFLINE" + + return "ONLINE" + + @property + def builds(self): + return self.pakfire.builds.get_by_host(self.id) + + @property + def active_builds(self): + return self.pakfire.builds.get_active(host_id=self.id) diff --git a/backend/constants.py b/backend/constants.py new file mode 100644 index 0000000..8b5afa9 --- /dev/null +++ b/backend/constants.py @@ -0,0 +1,65 @@ +#!/usr/bin/python + +N_ = lambda x: x + +# 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"), +} + +UPLOADS_DIR = "/var/tmp/pakfire/uploads" + +MSG_BUILD_FAILED_SUBJECT = N_("[%(build_name)s] Build job failed.") +MSG_BUILD_FAILED = N_("""\ +The build job "%(build_name)s" has failed. + +This could have a couple of reasons and needs to be investigated by you. + +Here is more information about the incident: + + Build name: %(build_name)s + Build host: %(build_host)s + +Click on this link to get all details about the build: + http://pakfire.ipfire.org/build/%(build_uuid)s + +Sincerely, + The Pakfire Build Service""") + + +MSG_BUILD_FINISHED_SUBJECT = N_("[%(build_name)s] Build job finished.") +MSG_BUILD_FINISHED = N_("""\ +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 + +Sincerely, + The Pakfire Build Service""") diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..463674e --- /dev/null +++ b/backend/database.py @@ -0,0 +1,54 @@ +#!/usr/bin/python + +import logging +import tornado.database + +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)) + + return tornado.database.Connection._execute(self, cursor, query, parameters) + diff --git a/backend/distribution.py b/backend/distribution.py new file mode 100644 index 0000000..4e6f229 --- /dev/null +++ b/backend/distribution.py @@ -0,0 +1,104 @@ +#!/usr/bin/python + +import base +from repository import Repository + +class Distributions(base.Object): + def get_all(self): + distros = self.db.query("SELECT id FROM distributions ORDER BY name") + + return [Distribution(self.pakfire, d.id) for d in distros] + + def get_by_id(self, id): + distro = self.db.get("SELECT id FROM distributions WHERE id = %s LIMIT 1", id) + + if distro: + return Distribution(self.pakfire, distro.id) + + def get_by_name(self, name): + distro = self.db.get("SELECT id FROM distributions WHERE sname = %s LIMIT 1", name) + + if distro: + return Distribution(self.pakfire, distro.id) + + +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) + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.name) + + def set(self, key, value): + self.db.execute("UPDATE distributions SET %s = %%s WHERE id = %%s" % key, + value, self.id) + self._data[key] = value + + @property + def info(self): + return { + "name" : self.name, + "sname" : self.sname, + "slogan" : self.slogan, + "arches" : self.arches, + "vendor" : self.vendor, + "description" : self.description, + } + + @property + def name(self): + return self._data.name + + @property + def sname(self): + return self._data.sname + + @property + def slogan(self): + return self._data.slogan + + @property + def arches(self): + arches = self._data.arches.split() + + return sorted(arches) + + @property + def vendor(self): + return self._data.vendor + + @property + def description(self): + return self._data.description or "" + + @property + def repositories(self): + repos = self.db.query("SELECT id FROM repositories WHERE distro_id = %s", self.id) + + return sorted([Repository(self.pakfire, r.id) for r in 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) + + @property + def comprehensive_repositories(self): + return [r for r in self.repositories if r.comprehensive] + + def add_repository(self, name, description): + return Repository.new(self.pakfire, self, name, description) + + @property + def sources(self): + return self.pakfire.sources.get_by_distro(self) + + @property + def log(self): + return self.db.query("SELECT * FROM log WHERE distro_id = %s ORDER BY time DESC", self.id) + diff --git a/backend/managers.py b/backend/managers.py new file mode 100644 index 0000000..7153e2a --- /dev/null +++ b/backend/managers.py @@ -0,0 +1,160 @@ +#!/usr/bin/python + +import logging +import tornado.ioloop + +import base + +managers = [] + +class Manager(base.Object): + def __init__(self, pakfire): + base.Object.__init__(self, pakfire) + + self.pc = tornado.ioloop.PeriodicCallback(self, self.timeout * 1000) + + logging.info("%s was initialized." % self.__class__.__name__) + + self() + + def __call__(self): + logging.info("%s main method was called." % self.__class__.__name__) + + timeout = self.do() + + if timeout is None: + timeout = self.timeout + + # Update callback_time. + self.pc.callback_time = timeout * 1000 + logging.debug("Next call will be in ~%.2f seconds." % timeout) + + @property + def timeout(self): + """ + Return a new callback timeout in seconds. + """ + raise NotImplementedError + + def do(self): + raise NotImplementedError + + +class MessagesManager(Manager): + @property + def messages(self): + """ + Shortcut to messages object. + """ + return self.pakfire.messages + + @property + def timeout(self): + # If we have messages, we should run as soon as possible. + if self.messages.count: + return 0 + + # Otherwise we sleep for "mesages_interval" + return self.settings.get_int("messages_interval", 10) + + def do(self): + logging.info("Sending a bunch of messages.") + + # Send up to 10 messages and return. + self.messages.send_messages(limit=10) + + +managers.append(MessagesManager) + + +class SourceManager(Manager): + @property + def sources(self): + return self.pakfire.sources + + @property + def timeout(self): + return self.settings.get_int("source_update_interval", 60) + + def do(self): + for source in self.sources.get_all(): + # 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 we have cloned a new repository, we exit to not get over + # the time treshold. + return 0 + + # Otherwise we just fetch updates. + else: + source.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 + + +managers.append(SourceManager) + + +class BuildsManager(Manager): + @property + def timeout(self): + 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() + + +managers.append(BuildsManager) + + +class UploadsManager(Manager): + @property + def timeout(self): + return self.settings.get_int("uploads_remove_interval", 3600) + + def do(self): + self.pakfire.uploads.cleanup() + + +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) diff --git a/backend/messages.py b/backend/messages.py new file mode 100644 index 0000000..c37a076 --- /dev/null +++ b/backend/messages.py @@ -0,0 +1,122 @@ +#!/usr/bin/python + +import logging +import smtplib +import subprocess +import tornado.locale + +from email.mime.text import MIMEText + +import base + +class Messages(base.Object): + def add(self, to, subject, text, frm=None): + subject = "%s %s" % (self.pakfire.settings.get("email_subject_prefix"), subject) + + # Get default sender from the settings. + if not frm: + frm = self.pakfire.settings.get("email_from") + + self.db.execute("INSERT INTO user_messages(frm, `to`, subject, text)" + " VALUES(%s, %s, %s, %s)", frm, to, subject, text) + + def get_all(self, limit=None): + query = "SELECT * FROM user_messages ORDER BY time_added ASC" + if limit: + query += " LIMIT %d" % limit + + return self.db.query(query) + + @property + def count(self): + ret = self.db.get("SELECT COUNT(*) as count FROM user_messages") + + return ret.count + + def delete(self, id): + self.db.execute("DELETE FROM user_messages WHERE id = %s", id) + + def send_to_all(self, recipients, subject, body, format=None): + """ + Sends an email to all recipients and does the translation. + """ + if not format: + format = {} + + for recipient in recipients: + if not recipient: + logging.warning("Ignoring empty recipient.") + continue + + # We try to get more information about the user from the database + # like the locale. + user = self.pakfire.users.get_by_email(recipient) + if user: + # Get locale that the user prefers. + locale = tornado.locale.get(user.locale) + else: + # Get the default locale. + locale = tornado.locale.get() + + # Translate the message. + _subject = locale.translate(subject) % format + _body = locale.translate(body) % format + + # If we know the real name of the user we add the realname to + # the recipient field. + if user: + recipient = "%s <%s>" % (user.realname, user.email) + + # 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: + logging.warning("Dropping message with empty recipient.") + return + + logging.debug("Sending mail to %s: %s" % (msg.to, msg.subject)) + + # Preparing mail content. + mail = MIMEText(msg.text.encode("latin-1")) + mail["From"] = msg.frm.encode("latin-1") + mail["To"] = msg.to.encode("latin-1") + mail["Subject"] = msg.subject.encode("latin-1") + #mail["Content-type"] = "text/plain; charset=utf-8" + + #smtp = smtplib.SMTP("localhost") + #smtp.sendmail(msg.frm, msg.to.split(", "), mail.as_string()) + #smtp.quit() + + # We use sendmail here to workaround problems with the mailserver + # communication. + # So, just call /usr/lib/sendmail, pipe the message in and see + # what sendmail tells us in return. + sendmail = ["/usr/lib/sendmail", "-t"] + p = subprocess.Popen(sendmail, bufsize=0, close_fds=True, + stdin=subprocess.PIPE, stdout=subprocess.PIPE) + + stdout, stderr = p.communicate(mail.as_string()) + + # Wait until sendmail has finished. + p.wait() + + if p.returncode: + raise Exception, "Could not send mail: %s" % stderr diff --git a/backend/misc.py b/backend/misc.py new file mode 100644 index 0000000..b88cb18 --- /dev/null +++ b/backend/misc.py @@ -0,0 +1,42 @@ +#!/usr/bin/python + +from __future__ import division + +import hashlib +import tarfile + +def friendly_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]) + +def calc_hash1(filename): + f = open(filename) + + h = hashlib.sha1() + while True: + buf = f.read(1024) + if not buf: + break + + h.update(buf) + + f.close() + + return h.hexdigest() + + +def guess_filetype(filename): + # XXX very cheap check. Need to do better here. + if tarfile.is_tarfile(filename): + return "pkg" + + elif filename.endswith(".log"): + return "log" + + return "unknown" diff --git a/backend/packages.py b/backend/packages.py new file mode 100644 index 0000000..f749167 --- /dev/null +++ b/backend/packages.py @@ -0,0 +1,539 @@ +#!/usr/bin/python + +import os +import shutil +import time +import urlparse +import uuid + +import base +import build + +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) + + return [Package(self.pakfire, pkg.id) for pkg in pkgs] + + find_by_name = get_by_name + + 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 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) + + return pkgs + + 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) + + if pkg: + return Package(self.pakfire, pkg.id) + + 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) + + if not file: + return None, None + + pkg = Package(self.pakfire, file.pkg_id) + + if file.type == SourcePackageFile.type: + file = SourcePackageFile(self.pakfire, file.id) + + elif file.type == BinaryPackageFile.type: + file = BinaryPackageFile(self.pakfire, file.id) + + return pkg, file + + def get_comments(self, limit=50): + comments = self.db.query("""SELECT * FROM package_comments + ORDER BY time DESC LIMIT %s""", limit) + + return comments + + +class Package(base.Object): + """ + This class represents a package (like source package) that is passed to + the buildsystem. + + New objects of this are created by the new() method. + """ + + def __init__(self, pakfire, id): + base.Object.__init__(self, pakfire) + self.id = id + + self._data = self.db.get("SELECT * FROM packages WHERE id = %s", self.id) + + def __cmp__(self, other): + # Order alphabetically. + return cmp(self.friendly_name, other.friendly_name) + + @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) + + if pkg: + return pkg + + 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) + + pkg = cls(pakfire, id) + pkg.add_file(file, build) + + # Create all needed build jobs. + pkg.create_builds() + + return pkg + + @property + def name(self): + return self._data.get("name") + + @property + def epoch(self): + return self._data.get("epoch") + + @property + def version(self): + return self._data.get("version") + + @property + def release(self): + return self._data.get("release") + + @property + def friendly_name(self): + return "%s-%s" % (self.name, self.friendly_version) + + @property + def friendly_version(self): + s = "%s-%s" % (self.version, self.release) + + if self.epoch: + s = "%d:%s" % (self.epoch, s) + + return s + + @property + def distro(self): + return self.source.distro + + def get_state(self): + return self._data.get("state") + + def set_state(self, state): + self.db.execute("UPDATE packages SET state = %s WHERE id = %s", state, self.id) + self._data["state"] = state + + if state == "finished": + # Add package to all comprehensive repositories. + for repo in self.distro.comprehensive_repositories: + self.add_to_repository(repo) + + state = property(get_state, set_state) + + @property + def summary(self): + return self._data.get("summary") + + @property + def description(self): + return self._data.get("description") + + @property + def groups(self): + return self._data.get("groups") + + @property + def url(self): + return self._data.get("url") + + @property + def maintainer(self): + return self._data.get("maintainer") + + @property + def license(self): + return self._data.get("license") + + @property + def source(self): + return self.source_build.source + + def get_files(self, type=None): + files = [] + + query = "SELECT id, type FROM package_files WHERE pkg_id = %s" + if type: + query += " AND type = '%s'" % type + + 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 + + @property + def packagefiles(self): + return [f for f in self.get_files() if isinstance(f, PackageFile)] + + @property + def logfiles(self): + return [f for f in self.get_files() if isinstance(f, LogFile)] + + @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] + + @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) + + 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 + + os.unlink(abspath) + + # Save the data to a file. + dirname = os.path.dirname(abspath) + if not os.path.exists(dirname): + os.makedirs(dirname) + + # Copy file to target directory. + shutil.copy(pkg.filename, abspath) + + 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) + + # 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]) + + return id + + 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) + + # Save the data to a file. + dirname = os.path.dirname(abspath) + if not os.path.exists(dirname): + os.makedirs(dirname) + + # Move file to target directory. + shutil.copy(filename, abspath) + + 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 supported_arches(self): + supported = self._data.get("supported_arches") or "" + supported = supported.split() + + arches = [] + + if "all" in supported: + # Inherit all supported architectures from the distribution. + arches += self.distro.arches + supported.remove("all") + + excludes = [a[1:] for a in supported if a.startswith("-")] + + if excludes: + _arches = [] + + for arch in arches: + if arch in excludes: + continue + + _arches.append(arch) + + arches = _arches + + includes = [a[1:] for a in supported if not a.startswith("-")] + + # Add explicitely included architectures. + arches += includes + + return arches + + def create_builds(self): + builds = [] + + for arch in self.supported_arches: + b = build.BinaryBuild.new(self.pakfire, self, arch) + + builds.append(b) + + return builds + + @property + def builds(self): + if not hasattr(self, "_builds"): + self._builds = self.pakfire.builds.get_by_pkgid(self.id) + + if self.source_build: + self._builds.append(self.source_build) + + return self._builds + + @property + def source_build(self): + return self.pakfire.builds.get_by_id(self._data.source_build) + + 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 + + self.state = "finished" + + @property + def repositories(self): + repos = self.db.query("SELECT repo_id FROM repository_packages WHERE pkg_id = %s", self.id) + + return [self.pakfire.repos.get_by_id(r.id) for r in repos] + + def add_to_repository(self, repo): + #repo.add_package(self) + repo.register_action("add", self.id) + + @property + def comments(self): + comments = self.db.query("""SELECT * FROM package_comments + WHERE pkg_id = %s ORDER BY time DESC""", self.id) + + 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) + + vote2credit = { "up" : 1, "down" : -1, "none" : 0, } + try: + self.credits = self.credits + vote2credit[vote] + + except KeyError: + pass + + return id + + def get_credits(self): + return self._data.credits + + def set_credits(self, credits): + self.db.execute("UPDATE packages SET credits = %s WHERE id = %s", + credits, self.id) + self._data["credits"] = credits + + credits = property(get_credits, set_credits) + + def get_actions(self): + return self.pakfire.repos.get_actions_by_pkgid(self.id) + + +class File(base.Object): + type = None + + 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") + + if path.startswith("/"): + path = path[1:] + + return path + + @property + def abspath(self): + return os.path.join(self.source.targetpath, self.path) + + @property + def download(self): + path = self.abspath.split("/") + + while path: + if path[0] == "packages": + break + path = path[1:] + + return "%s/%s" % (self.pakfire.settings.get("baseurl"), "/".join(path)) + + @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") + + @property + def uuid(self): + return self._data.get("uuid") + + @property + def summary(self): + return self._data.get("summary") + + @property + def description(self): + return self._data.get("description") + + @property + def license(self): + return self._data.get("license") + + @property + def size(self): + return self._data.get("size") + + @property + def url(self): + return self._data.get("url") + + @property + def maintainer(self): + return self._data.get("maintainer") + + @property + def build_id(self): + return self._data.get("build_id") + + @property + def build_host(self): + return self._data.get("build_host") + + @property + def build_time(self): + return self._data.get("build_time") + + @property + def build_date(self): + return time.strftime("%a, %d %b %Y %H:%M:%S +0000", + time.gmtime(self.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() + + return sorted(obsoletes) + + @property + def conflicts(self): + conflicts = self._data.get("conflicts").split() + + return sorted(conflicts) + + +class BinaryPackageFile(PackageFile): + type = "binary" + + +class SourcePackageFile(PackageFile): + type = "source" + + +class LogFile(File): + type = "log" + diff --git a/backend/repository.py b/backend/repository.py new file mode 100644 index 0000000..71a4a4e --- /dev/null +++ b/backend/repository.py @@ -0,0 +1,287 @@ +#!/usr/bin/python + +import base +import packages + +class Repositories(base.Object): + def get_all(self): + repos = self.db.query("SELECT id FROM repositories") + + return [Repository(self.pakfire, r.id) for r in repos] + + def get_by_id(self, repo_id): + repo = self.db.get("SELECT id FROM repositories WHERE id = %s", repo_id) + + if repo: + return Repository(self.pakfire, repo.id) + + def get_needs_update(self, limit=None): + query = "SELECT id FROM repositories WHERE needs_update = 'Y'" + query += " ORDER BY last_update ASC" + + # Append limit if any + if limit: + query += " LIMIT %d" % limit + + repos = self.db.query(query) + + 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 + + repo = self.get_by_id(action.repo_id) + + return RepoAction(self.pakfire, repo, action.id) + + def get_actions_by_pkgid(self, pkgid): + actions = self.db.query("SELECT id FROM repository_actions WHERE pkg_id = %s", pkg_id) + + return [RepoAction(self.pakfire, self, a.id) for a in actions] + + +class Repository(base.Object): + def __init__(self, pakfire, id): + base.Object.__init__(self, pakfire) + self.id = id + + self.data = self.db.get("SELECT * FROM repositories WHERE id = %s", self.id) + + def __cmp__(self, other): + if not self.upstream_id or not other.upstream_id: + return 0 + + if self.id == other.upstream_id: + return -1 + + elif self.upstream_id == other.id: + return 1 + + def next(self): + repo = self.db.get("SELECT id FROM repositories WHERE upstream = %s" + " LIMIT 1", self.id) + + if repo: + return self.pakfire.repos.get_by_id(repo.id) + + def last(self): + if self.upstream_id: + return self.pakfire.repos.get_by_id(self.upstream_id) + + @property + def upstream(self): + return self.last() + + @property + def is_upstream(self): + return not self.upstream + + @classmethod + def new(cls, pakfire, distro, name, description): + id = pakfire.db.execute("INSERT INTO repositories(distro_id, name, description)" + " VALUES(%s, %s, %s)", distro.id, name, description) + + return cls(pakfire, id) + + @property + def distro(self): + return self.pakfire.distros.get_by_id(self.data.distro_id) + + @property + def info(self): + return { + "id" : self.id, + "distro" : self.distro.info, + "name" : self.name, + "arches" : self.arches, + } + + @property + def comprehensive(self): + return not self.upstream_id + + @property + def name(self): + return self.data.name + + @property + def description(self): + return self.data.description + + @property + def upstream_id(self): + return self.data.upstream + + @property + def arches(self): + return self.distro.arches + + def get_needs_update(self): + return self.data.needs_update + + def set_needs_update(self, val): + if val: + val = "Y" + else: + val = "N" + + self.db.execute("UPDATE repositories SET needs_update = %s WHERE id = %s", + val, self.id) + + needs_update = property(get_needs_update, set_needs_update) + + @property + def credits_needed(self): + return self.data.credits_needed + + 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) + + # Mark, that the repository was altered and needs an update. + self.needs_update = True + + return id + + def get_packages(self, _query=None): + query = "SELECT pkg_id FROM repository_packages WHERE repo_id = %s" + if _query: + query = "%s %s" % (query, _query) + + pkgs = self.db.query(query, self.id) + + return sorted([packages.Package(self.pakfire, p.pkg_id) for p in pkgs]) + + @property + def packages(self): + return self.get_packages() + + @property + def waiting_packages(self): + return self.get_packages("AND state = 'waiting'") + + @property + def pushed_packages(self): + return self.get_packages("AND state = 'pushed'") + + @property + def log(self): + return self.db.query("SELECT * FROM log WHERE repo_id = %s ORDER BY time DESC", self.id) + + def register_action(self, action, pkg_id, old_pkg_id=None): + assert action in ("add", "remove",) + assert pkg_id is not None + + id = self.db.execute("INSERT INTO repository_actions(action, repo_id, pkg_id)" + " VALUES(%s, %s, %s)", action, self.id, pkg_id) + + # On upstream repositories, all actions are done immediately. + if self.is_upstream: + action = RepoAction(self.pakfire, self, id) + action.run() + + return id + + def has_actions(self): + actions = self.db.get("SELECT COUNT(*) as c FROM repository_actions" + " WHERE repo_id = %s", self.id) + + if actions.c: + return True + + return False + + def get_actions(self): + actions = self.db.query("SELECT id FROM repository_actions" + " WHERE repo_id = %s ORDER BY time_added ASC", self.id) + + return [RepoAction(self.pakfire, self, a.id) for a in actions] + + +class RepoAction(base.Object): + def __init__(self, pakfire, repo, id): + base.Object.__init__(self, pakfire) + + self.repo = repo + self.id = id + + 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 + + @property + def action(self): + return self.data.action + + @property + def pkg_id(self): + return self.data.pkg_id + + @property + def pkg(self): + return self.pakfire.packages.get_by_id(self.pkg_id) + + @property + def credits_needed(self): + return self.repo.credits_needed - self.pkg.credits + + @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 + + # Admins are always allowed to do all actions. + if who.is_admin(): + return True + + # If the maintainer matches, he is also allowed. + if who.email == self.pkg.maintainer_email: + return True + + # Everybody else is denied. + return False + + def is_doable(self): + return self.credits_needed == 0 + + def run(self, who=None): + if who and not self.have_permission(who): + raise Exception, "Insufficient permissions" + + if self.action == "add": + self.repo.add_package(self.pkg_id) + + 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) + + 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) + + # Delete ourself. + self.delete(who) diff --git a/backend/settings.py b/backend/settings.py new file mode 100644 index 0000000..150f56d --- /dev/null +++ b/backend/settings.py @@ -0,0 +1,49 @@ +#!/usr/bin/python + +import base + +class Settings(base.Object): + def query(self, key): + return self.db.get("SELECT * FROM settings WHERE k = %s", key) + + def get(self, key, default=None): + result = self.query(key) + if not result: + return default + + return "%s" % result.v + + def get_id(self, key): + return self.query(key)["id"] + + def get_int(self, key, default=None): + value = self.get(key, default) + + if value is None: + return None + + return int(value) + + def get_float(self, key, default=None): + value = self.get(key, default) + + if value is None: + return None + + return float(value) + + def set(self, key, value): + id = self.get(key) + + if not id: + self.db.execute("INSERT INTO settings(k, v) VALUES(%s, %s)", key, value) + else: + self.db.execute("UPDATE settings SET v = %s WHERE id = %s", value, id) + + def get_all(self): + attrs = {} + + for s in self.db.query("SELECT * FROM settings"): + attrs[s.k] = s.v + + return attrs diff --git a/backend/sources.py b/backend/sources.py new file mode 100644 index 0000000..063c997 --- /dev/null +++ b/backend/sources.py @@ -0,0 +1,171 @@ +#!/usr/bin/python + +import datetime +import logging +import os +import subprocess + +import base +import build + +class Sources(base.Object): + def get_all(self): + sources = self.db.query("SELECT id FROM sources ORDER BY id") + + 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) + + def get_by_distro(self, distro): + sources = self.db.query("SELECT id FROM sources WHERE distro_id = %s", distro.id) + + return [Source(self.pakfire, s.id) for s in sources] + + def update_revision(self, source_id, revision): + query = "UPDATE sources SET revision = %s WHERE id = %s" + + return self.db.execute(query, revision, source_id) + + +class Source(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) + + def __cmp__(self, other): + return cmp(self.id, other.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 _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]) + + def _git_rev_list(self, revision=None): + if not revision: + revision = self.revision + + command = "rev-list %s..origin/%s" % (revision, self.branch) + + # Get all merge commits. + merges = self._git("%s --merges" % command) + merges = merges.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 is_cloned(self): + """ + Say if the repository is already cloned. + """ + return os.path.exists(self.path) + + def clone(self): + if self.is_cloned(): + return + + if not os.path.exists(dirname): + os.makedirs(dirname) + + 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." + + self._git("fetch") + + def import_revisions(self): + # Get all pending revisions. + revisions = self._git_rev_list() + + for revision, merge in revisions: + # If the revision is not a merge, we do import it. + if not merge: + self._import_revision(revision) + + # Save revision in database. + self.db.execute("UPDATE sources SET revision = %s WHERE id = %s", + revision, self.id) + self._data["revision"] = revision + + def _import_revision(self, revision): + logging.debug("Going to import revision: %s" % revision) + + 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 source build object. + build.SourceBuild.new(self.pakfire, self.id, revision, rev_author, + rev_committer, rev_subject, rev_body, rev_date) + + @property + def name(self): + return self._data.name + + @property + def url(self): + return self._data.url + + @property + def path(self): + return self._data.path + + @property + def targetpath(self): + return self._data.targetpath + + @property + def revision(self): + return self._data.revision + + @property + def branch(self): + return self._data.branch + + @property + def builds(self): + return self.pakfire.builds.get_by_source(self.id) + + @property + def distro(self): + return self.pakfire.distros.get_by_id(self._data.distro_id) diff --git a/backend/uploads.py b/backend/uploads.py new file mode 100644 index 0000000..eb31b2b --- /dev/null +++ b/backend/uploads.py @@ -0,0 +1,148 @@ +#!/usr/bin/python + +import hashlib +import logging +import os +import pakfire.packages +import uuid + +import base +import misc +import packages + +from constants import * + +class Uploads(base.Object): + def get_by_uuid(self, _uuid): + upload = self.db.get("SELECT id FROM uploads WHERE uuid = %s", _uuid) + + 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") + + return [Upload(self.pakfire, u.id) for u in uploads] + + def cleanup(self): + for upload in self.get_all(): + upload.cleanup() + + +class Upload(base.Object): + def __init__(self, pakfire, id): + base.Object.__init__(self, pakfire) + + self.id = id + 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() + + id = pakfire.db.execute("INSERT INTO uploads(uuid, builder, filename, size, hash)" + " VALUES(%s, %s, %s, %s, %s)", _uuid, builder.id, filename, size, hash) + + upload = cls(pakfire, id) + + # Create space to where we save the data. + dirname = os.path.dirname(upload.path) + if not os.path.exists(dirname): + os.makedirs(dirname) + + # Create empty file. + f = open(upload.path, "w") + f.close() + + return upload + + @property + def uuid(self): + return self.data.uuid + + @property + def hash(self): + return self.data.hash + + @property + def filename(self): + return self.data.filename + + @property + def path(self): + return os.path.join(UPLOADS_DIR, self.uuid, self.filename) + + @property + def builder(self): + return self.pakfire.builders.get_by_id(self.data.builder) + + def append(self, data): + logging.debug("Writing %s bytes to %s" % (len(data), self.path)) + + f = open(self.path, "ab") + f.write(data) + f.close() + + def validate(self): + # Calculate a hash to validate the upload. + hash = misc.calc_hash1(self.path) + + ret = self.hash == hash + + if not ret: + logging.error("Hash did not match: %s != %s" % (self.hash, hash)) + + return ret + + def remove(self): + # Remove the uploaded data. + if os.path.exists(self.path): + os.unlink(self.path) + + # 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() + + def cleanup(self): + # Get the seconds since we are running. + try: + time_running = datetime.datetime.utcnow() - self.time_start + time_running = time_running.total_seconds() + except: + time_running = 0 + + # Remove uploads that are older than 24 hours. + if time_running >= 3600 * 24: + self.remove() diff --git a/backend/users.py b/backend/users.py new file mode 100644 index 0000000..0dbe246 --- /dev/null +++ b/backend/users.py @@ -0,0 +1,232 @@ +#!/usr/bin/python + +import hashlib +import logging +import random +import string +import urllib + +import tornado.locale + +import base + +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): + return + + user = self.db.get("""SELECT id FROM users WHERE name = %s + AND passphrase = SHA1(%s) AND activated = 'Y' AND deleted = 'N'""", + name, passphrase) + + if user: + return User(self.pakfire, user.id) + + def register(self, name, passphrase, email, realname, locale=None): + return User.new(self.pakfire, name, passphrase, email, realname, locale) + + def name_is_used(self, name): + users = self.db.query("SELECT id FROM users WHERE name = %s", name) + + if users: + return True + + return False + + def email_is_used(self, email): + users = self.db.query("SELECT id FROM users WHERE email = %s", email) + + if users: + return True + + return False + + def get_all(self): + users = self.db.query("""SELECT id FROM users WHERE activated = 'Y' AND + deleted = 'N' ORDER BY realname, name""") + + 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) + + def get_by_name(self, name): + user = self.db.get("SELECT id FROM users WHERE name = %s LIMIT 1", name) + + if user: + 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) + + if user: + return User(self.pakfire, 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) + + def __cmp__(self, other): + return cmp(self.id, other.id) + + @classmethod + def new(cls, pakfire, name, passphrase, email, realname, locale=None): + + id = pakfire.db.execute("""INSERT INTO users(name, passphrase, email, realname) + VALUES(%s, SHA1(%s), %s, %s)""", name, passphrase, email, realname) + + user = cls(pakfire, id) + + # If we have a guessed locale, we save it (for sending emails). + if locale: + user.locale = locale + + user.send_activation_mail() + + return user + + def delete(self): + self.db.execute("UPDATE users SET deleted = 'Y' WHERE id = %s", self.id) + + def activate(self): + self.db.execute("UPDATE users SET activated = 'Y' WHERE id = %s", self.id) + + 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) + + passphrase = property(lambda x: None, set_passphrase) + + @property + def activation_code(self): + return self.data.activation_code + + def get_realname(self): + if not self.data.realname: + return self.name + + return self.data.realname + + def set_realname(self, realname): + self.db.execute("UPDATE users SET realname = %s WHERE id = %s", + realname, self.id) + self.data["realname"] = realname + + realname = property(get_realname, set_realname) + + @property + def name(self): + return self.data.name + + def get_email(self): + return self.data.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.data.update({ + "email" : email, + "activated" : "N", + }) + + # Inform the user, that he or she has to re-activate the account. + self.send_activation_mail() + + email = property(get_email, set_email) + + def get_state(self): + return self.data.state + + def set_state(self, state): + self.db.execute("UPDATE users SET state = %s WHERE id = %s", state, + self.id) + self.data["state"] = state + + state = property(get_state, set_state) + + def get_locale(self): + return self.data.locale or "" + + def set_locale(self, locale): + self.db.execute("UPDATE users SET locale = %s WHERE id = %s", locale, + self.id) + self.data["locale"] = locale + + locale = property(get_locale, set_locale) + + @property + def activated(self): + return self.data.activated == "Y" + + @property + def registered(self): + return self.data.registered + + def gravatar_icon(self, size=128): + # construct the url + gravatar_url = "http://www.gravatar.com/avatar/" + \ + hashlib.md5(self.email.lower()).hexdigest() + "?" + gravatar_url += urllib.urlencode({'d': "mm", 's': str(size)}) + + return gravatar_url + + def is_admin(self): + return self.state == "admin" + + def is_tester(self): + return self.state == "tester" + + def send_activation_mail(self): + logging.debug("Sending activation mail to %s" % self.email) + + # Generate a random activation code. + source = string.ascii_letters + string.digits + self.data["activation_code"] = "".join(random.sample(source * 20, 20)) + self.db.execute("UPDATE users SET activation_code = %s WHERE id = %s", + self.activation_code, self.id) + + # Get the saved locale from the user. + locale = tornado.locale.get(self.locale) + _ = locale.translate + + subject = _("Account Activation") + + message = _("You, or somebody using you email address, has registered an account on the Pakfire Build Service.") + 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 += "\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) + + return comments + + @property + def log(self): + log = self.db.query("SELECT * FROM log WHERE user_id = %s ORDER BY time DESC", + self.id) + + return log diff --git a/data/static/css/style.css b/data/static/css/style.css new file mode 100644 index 0000000..855ce34 --- /dev/null +++ b/data/static/css/style.css @@ -0,0 +1,604 @@ + +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; + 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%; +} diff --git a/data/static/favicon.ico b/data/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..565ba7bd6b7546f86104540ad6e9b468202ea4ef GIT binary patch literal 4286 zc-pm=30PF+8pns!vg<7rMrO{Ljn(wxf~Hp|ui(;c(L_r#+$vMAqFI{PB`wKPAw@|P zM9mpNaRYIoz!FkX)QciuifppY#yShL;w=AO2fNRGdLD10_0IE~bI$jD=l#Fm`z`0= za6IVe>&vchpJv#V3OOeZ&)MVMHLn*R%QV+@^i4{at zYO*{!{^+Ugn=;NOMQ8k&v?VP&G2!Tmc=>_Ut*LTZw7bng*-H022V@b44#+|e%4H0v zleRZ4ecX)bYqv+eQ{osiy~H(UW(med6l2WBas+3Y;FoVlAGsdE`?TnHKnLmhR*cnXMSSPzTXo zD|#O=qxWGmd}pc<^e;8MB3r>bV~1a!3n7s@45qe2-`65|sUF--Z3w(#KPDfvus$E zY{K+Z6Goi2LX>mQF1TPfiZ0q?h;E&UL6~Wa5@)sB`~TREnP;qcCEW~3j21!3tr)(h z4$^5=kj}0qst~fa0Wa)nL-;8xKBjg9DQ021rCo5+Y7}JJ?g_JPmxO29*-qsk)f~w` zVlf2EYq2c76tgd>G2^lZ6F#rPxcAEN%Di&CI=>9BEiOmoDJ2%=H{jhfYRr{Yz%Q{8 zynO}}KgHa}95T1FM=hD`F^hL+^nH>{k-Y61>(ouBa3a4D8>-Y;b*Ub6zbr$M-z7L#LJuqelj6}d)yddz^R z(*^{uZNbKKCP>$7F>YNezAgr~#>Rm=au)>i8?JFjM@I`s&QJ&JD%MJJO_Ltl|02h9 zzkFMZ|4{=17Vk$u*hZ^hSbKGYQkrGjd=YFJ^Fpvgn3Ci951wC?kNjC+;7CZ zlUAC8R%}ls3uxd5-fHs0;q;UfQ} zb>glWi7EAXWnB|0%?|vpEnBt>AJ|JqiXa|hI5N2}3wuo5eGxBOQ-EiKKh?ZtfwzJa;m zkJEw4XunCio5Z)A{U2o=NNzg)iCp%Oc@=MhrdZ@z2Oe*Z+wc1I>#$#*32~?j+%PRh zysyXV-9{7^-o=jiZ`^I3?coE7APa)$)nNK5?(vfhE8X+!j;qTf=_?T|OfbI6uyr4> zUW!`L^L6m{{s038_QTDaH&Igcx%xGBp$0kL2)yRiffV`sM|9ZIogJx~z&=;o#Cq04=&5k^^jwNr~c+W*V1-#Bb2c>k|mPXncI?qBxIfte;{9E3DVB-t`WFr^?0kipB14Wl(C= zXl*khZhJh24I7T|@X08nJ+Gm;1udk%bLTeFUk(A6^nDdckE6{DRCZ(fE5j*89jS8BkW;mTrtX{nuQmGVYb8=8wRfWvVOaun`gYnx1Hn`Ej3{&MX zFRJ`Itv^WVK`}hV_E8LBMfD2STM41y90t8O9vR2eU^1DIlamX7|3Cx;^o2wcfDzJA z1o;O+8a@wx(sJ-V(bh0gx;L4@O25Y)114D6pAAq{Qx3qg^qrq}7~>~S#+cFLXrB3E z`ph}l5}SbK|Nab%BR|IH8)7gqx&*!Zs=(i%uN4(I0ys}#Oo(z8#b9P7^aI?McB1>VBuTHY>0EgM{q-V-%0?^wOdf4|AqpUzl)*6Sgh*8(o}+2?Hy_*DV)Kzz;7D8A{Ga-Psg+k<$Ea_}-UQqA&rwT1AeX_}eg3J9}o4oQW}A-Usn@DdD8 z_O#VS&&}fNjtt3d=ULk4M{xcCBmEvU4|>pA^bp9+-6*$-ys2s@6Rm^zw$rUoXG^D$ z&ghad*9FSCn6#G=|KR+=M&FLEq`8JUVz%&Nboaz1&U^F>aiIhBT?J{@5dS0si0A%r z{Zc1GT%a6Z7vi8L0E`t zV*a0`{}eHa81mry6~QG2J;?j}#LvW5qRXEYgU5Z}#8Bde-_$)#yiN4&j87Mb6J?_# zhl6l+;Q8Ph_A6=*XHYGb|B9aKZ5?${4vq)tT{kW_9>1XraQ>MNYQqRg2R1pK)^zv< D{@WKG literal 0 Hc-jL100001 diff --git a/data/static/images/icons/build-dependency_error.png b/data/static/images/icons/build-dependency_error.png new file mode 100644 index 0000000000000000000000000000000000000000..2230c05f1cbb3a15355bb970d7c3ad6dbf07c8be GIT binary patch literal 2085 zc-oy+c~nzp77t)%P+3|O8y7SU3doX|eUpSnk_c)r6Qe{n3CV*5l9!k~B!FNA+*k^t zj9{m9lnA31q!a-w3Pn+o5g2r&AShT{s>mjSqD`3>2ke|VJ^s<}ocF%(-uL_6d%wG! zcUTm(!G6KA1t=8CJ}`hMMs^R&WBVuMo&9T_2HBRvd?~zD8x1GO^dO3>&_;pSK#eR0 z6oWEF;;t6Z7lpEkR!O9=RJfTX*J|)G3kGl0=unpB>u1!-uY2o1~ecn!x}Ye$iOnPac}Likh4V% z;IMBYa2y-=-=L&I5!PR;2eDK2z-_i9jR(1QI|b`4H(WGKobX zU}qN&@upWqv&1~XtS>}jArXRQMjy!F zI%k0g8svJF4pwO)tVNbZY2#rw4mWoNjZP?hXBaZfCJIRzV3g?qBAx(fG{79%0E@xs z_oEGxL>&l-L4!74FGuPT?fO5i-cPjvk!V<3^eUt%GBr;tkJo?@9LQtikS}F$G+{K)|CBi97*QK;!WlBr=sx zW^hR~${aTkGQcuO4$g5^zj1l*<+A+spbXaPC0ea|t^-6$Evz*twK}Z-Rw`B~ldB+$ zY$?xdq@Z548&nANS`GGXep#yD@lWP62uv!0!sn96mU}b&i4=c61<`T^3D0 zNX`Hx7~ub!Wln{3pyl|k^%3FS@PH69F?wV)k{2HKM4{$w3*>Pn#-3+|8uiv~m>V;- z3E}tezx=xAa-?m3-m^tn|ESMs&e&11dC*Ns`ioPIv-ocr{qE`Wza5jMHezfqW!Ntv zbzm?8r>+xC`Ks)oZ%pJ5ej$kFhj&#>Rh)kTUhXoCy2*wj3cH0bPr6T+(Fc9$=E=n3 ziH5m*`P>YlBrKN{R}LTI}=k0B`sB)JY6 z-*93ta_TVsBd+1@8?$@l$vcx*F7GPXH;Ddayv*@OZC0@9!}ozm8%=>j6-Hxc5m>&1L=|@uc}9q z5_`u<%Cq-z&5yyOlocbV)@D|zZaW`p-p}l}UJF!S4_Ry7^V~WhBnMuyUm815Xv{ou zxYeeA^3D|O*6QdxHvM;zh&dEX?YB2_^+Rn7Bf~>${m=jV8HE(P_`@H>o~n)))tD+x zPATQe@G9I=le9ZUH)5PLPdJnW-I;DV8GcA{dkoE7P2TlYxre*`&8u{-`llwq_CS^48!*zk$>#Vk5~rwQ$$&i;DE(SO-hnW}TWz9_=2Igjl+CiK284jC8b zJ9~t1*ETk{#Ba|_(oP*s+u%`=_vo>4CWMq;T%7VmBEVo4zUF!r6h4|>v1@>0$I5|j z?29;fv#wA!l^By&2(2ll kf%X#XNGn}I&R#SI)gag&p7T7o!1Ch`rO2X`A%ivR!s literal 0 Hc-jL100001 diff --git a/data/static/images/icons/build-dispatching.png b/data/static/images/icons/build-dispatching.png new file mode 100644 index 0000000000000000000000000000000000000000..14efdaf3944f70c8291854742cd121b158492387 GIT binary patch literal 4839 zc-jHy5*Y1?P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000OQNkl0LBLR}iJ$m@ z#+bN7qlrPm7y=SMkO8R@WlvAfw^V!%V?G>`s!0+*S}YY` zsuZW6P3;1H_WTfK5f*t*C2(k5=Jb0vGjV%2G>wAT{}@~!e30O+x8Ed=>b~=tJGb2% zh7lrys(>mAs49x0BAgyN&B>D|zEPYSduDLB^mP>-5K&Yf$;QUqxy%hzOyxbRQky(ckW)y`zKmYnquVRv9e`B7$?4i!NTy z@?}eJjdKT{-#0k@~V*}IMowW*JfR7T!BAR6jnfYpzr)f%(q*#R4 z-tA*rax86aV&DG544pVi`OH!Jj&$?hp(DgjFu?S5nX))muU&q3YVB4xN~!e!*h)?(OCi>+;;Np#x{3QmyjV!9iaA*D=~UR#8knlZ7%( z%}pjBM|U9X7JTjg4=+h8s+rqsP6uYo4?sj2n@)JA`#3js)wAuUmE`j|^7%YVTbj9d z`?YLb)5OTg1S(3UT*X?4b>@<)Pw}E)%xpkWi^n20K!J*6!A#~_)l3Rw49-}bb6nNA zi01kUZo6&;)fD0|q<3JF?%r`)n)4h!F^;QmCGjcV3yC*a%w`d}#|5_82IW0Q+@2(j z$-6Rv7i2~PG;1=(m@NOAS&RWSkfh4(n=c`7hDsu+g0Yr{e9SYi^k8#MENN*Z9|jbr zs$d`uEtjrey`fw#J$3l-z*ztOt+z%t<=Ry{_}uy@*zk7B!0H_EL+nfy%pL>QSisCS zpfnZARi8;^45Ef*OB#9Li<@c81?1`*7@jDTrk*tQj82uwEofk@;HSom!RJ?1Cij13 z%}@AJ=M&^zg$6B=H>HdL@rsCIFo0qVSZlG?qM{gUvfqs2T!c@mR0=~>jA2cygJS!3{^+aiufN%~=17xsN~#Mam0-$~&hr2@=bIV&4aR2L zTVugw&|H`!txR(KrH3gFbQ45*Ft92zT;safJ8?HrU5tn@HC1MV*=4qGe(Sz~;DM^y zWWbSBr%OZQ$G>e;`NK?K2-N%8VIno>TVt`tB1SVa1|ieK$9VtM$64H%qZ~IgG%}2} z0b>>}3-<6{zhScdvsmj0`~S}7_SeivA#6zrW{uU^bzM_iu<`e&Pd&Y{9P7hCDX7ck z(7BPDF<{JGHQ*36^zV6+wH?c7T6P%+4*i;U51k+iBS5jn)1jx?)w77i}D2;r7;O&=p1J4G}J^L5#y6es?;~E3Tn7JuvK$0@n7g^BI z%9)ehv@LCA%a$#KQG~St)`eIjT%C>}Hlnp|D`H}-br|a~HoySw%a$-Yc#I`Yi|Gxc z)d0bc7yfE*ym3o?eSJM)7^0dPud%TaV+_^?q}38sMmVy+kI`vm?b@}pwQs`v1dPQP zQ|lXXIsS2+=n zb1u8og-CUZk%3-T-|{sUjU8t}Q-H6|4J%PWC2P4N9`PwYO%b2q(}XmwVqL`P$wC~y zcH|euM*Vqeb;Zn}vOwNB*M9#G?)&A#4?S{=)_Q}monfowDZRq6R8wc2YdNHyR&46&qtol4`cPsRDwbHd9oxxiaT&zz_t1 z4t3UbmJ4T0Jj(2-o*T{3V6AOabgp*hHA!o<^vN!^-F8j;PutesOz*)r5T9a<#W_P9 zTEf80UOWHY`wEv|yR~q|t@m7g=GdD@f+%MF?cXJs2S9*H5t;m`?Zqq9MXvYKjzuh4 zc@d52Qq=pirrm5uKj)F8RqFGRwyfJazOZvEGpl!Spic0Q7@V z%{(NMR%mW*=Esjbd#tAFAzYRSC$y- zaAAmbwpIc5oTNV}g(0A->ZS^X2Ih^v3jrX2R1&61D}+HnbI0|d>AAe0qk^eb5KQ(D z`M#6A#gD^)07YQQ`=`cwIBWQy z|FJ*>#MV$BS{BaCuK)5s1=JYI6;D5q0-kw`?*&z1jWxsn;HWD99sqAzNy)o=Jp%v$ N002ovPDHLkV1gPkGXwwt literal 0 Hc-jL100001 diff --git a/data/static/images/icons/build-failed.png b/data/static/images/icons/build-failed.png new file mode 100644 index 0000000000000000000000000000000000000000..494d66236fa33d0fa37e4b629ac533e59110f625 GIT binary patch literal 4681 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 literal 0 Hc-jL100001 diff --git a/data/static/images/icons/build-unknown.png b/data/static/images/icons/build-unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..b16d02067bdadba1579a7369d5dc17c76b30ca53 GIT binary patch literal 2727 zc-oy-dpMN&9-opyXdM)a%np@unRjL|VzcrAHI4dZ#a3gUh3**umQU=Y3z!viDSW6f?8D9ijTH9S^A{ILmk|IK(aIU}wIo0?x62axT zAa^^^0GfyhMQ|zcVkj`)m(GrlWIJ(?t~(IhWdxN0J|tx!WPF}LLXf#2|FBC?otM-o zB;pTh-V~fTSuowaW zAU-do%A1%IMhF1OpM9w$7i5G~Dk7jzv9Ymsu~<8yI2?s`a&lU-fx+0S5Vn#yfs`e) z6-X?WEr5`OE#``(T%iE5B(p+=(NY&Aa`_5;5smhzVS(gxqEsnE$yg#3+73YR`KV>I zL>d5v{XJSjj}t+t07xQ?7PD3L2($PvSAS1k0;-}R_=&lyqOf?NkR8p31X3#Kf>eF6 z<8V0yH0Xc^-Q95*)pEq6(N1W~1xdfsZVo8N!x=_el?g0M?p;Rb|5Q-4Q zKnDbk#pVi@Nbg zN3R&(JbIAvLdQ?jBXvY`9otYvQL%2F)ltMrPsV%1tt|3IIMK5CWz~IM9@E&sK)-hY zu5VzVlpnt4PxVQ^Y!B*Mr@}*%cT|1#s4J!)J3BF3m@qlP|C-U2RW-t07@bk+5{Ks( z7Uw(YvI0-P>CTa@<_egRbLhvn?Zx}E$KnVttX^nd{NjG9vBwiPrUw(mYai@w^?dgp zmJ`AGMsIlks{=(7=gLdDq^vQ6sIs%W!mD{P`^*bQevTRM-)(i*;B3P4YGbB6s>Gu9 z#b9uP<4)pUxlQYrkE2Tct0_c%hm!G;9{#zyAAU$_`ZV!iuKA5AHerl9IKaA!#HvuI+{sH#%tslV)7hD)Mw5oUFn9_VE4#-7CU@Ip=lSKx2Z$`8aKN zThtH%B$%|eoLF@v4BYpy!m4B*x}fi>vk1H zecD~GX1ScBB|6cAsNa%A;k$Wgd0CnsaFIW%Vb}nr54TvINsrc$;6053ZqG9wG}Y%8 z1>@>iIo$OMqZOUndK4kM#mBNyDPz#HudJFv>*W{@_MOuAi(0GrX!4HQ4>BKiCED(G zT*twhTZVpUSU9(V`!p}e3T6&ct9(BF-TgQ;vlcxoH(3$)0`;d#lz~ZtxHQ!4iX7HQI+JJIZFSzYI z(q&Kms~tYsN&@R2=u^zpg0g*nH6uvvRegCK1DqzG*vXfjP-xxeZl5XhUh@9M#A#*) z6Puz&3csQ~#4EKE7AH)RS{aqsKaD zttjzYT~qZfx#LI5GNXP8l=PkbB!82*{>!7!a?AjyotG%0`{-Qj%#9tL5>du@in3KS znwH!gT~fq80_;_sAvHVRz=(RUT4(s1cI`4OKT$p;-#WKZ+gp2Qy5w+&|CJ1jd%}8n zCFI-b6*Xe$-g)+V@SNt6(N=f2ZQ~CQm`8-WQL+Hk-MHJ=jmj z+f3E-Yc9*!f~s|#+3$bMzX+SUXf54xhJMdbF-+!pfV|!JR#xksnhs<>kkK}2lzRqBS-)`9W#v^(6+hUK{>%KWUyVCozi&80t0X@bOwan7%M@t+H zfl1-7+9rc1kItDu<&!r%jy}&+2$`)@-RqlrPA4jxjMQaO(pQHZ`xnyfJIYoXo8pJ9 zTV)XTi?2V(HOz9KY0Mdgsn-sUi31NEnuqaWoZjz>E;F~S%`wwa9xxo}X&-HQ`QU1g z+U5vcP|jIbaFAqh-!#pOkbi1LbU~Ta)%LIr!4qx8yTBmrLsIhtf49ecK&kIAp0G3$ zJTuuiGCB4=qi5HZUk^qkrCT+fZ=P;2h;j|1FFag{TGb(Rcp~KSU}^G_>Ee!IY@1S} zyTP_)6}Tx1#x`C0b)=Gf K!3vKM`M&`FZH?Ce literal 0 Hc-jL100001 diff --git a/data/static/images/icons/build-uploading.png b/data/static/images/icons/build-uploading.png new file mode 100644 index 0000000000000000000000000000000000000000..c2cf1bd0e05e9e11e6d4173457a3d91d2edf9632 GIT binary patch literal 5109 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 XPMA5Kqfh4(00000NkvXXu0mjfl(T>+ literal 0 Hc-jL100001 diff --git a/data/static/images/icons/build-waiting.png b/data/static/images/icons/build-waiting.png new file mode 100644 index 0000000000000000000000000000000000000000..6b4a87f773e3de5fbfbb77e0cc0b9eb156f6d503 GIT binary patch literal 1324 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 literal 0 Hc-jL100001 diff --git a/data/static/images/icons/slave-offline.png b/data/static/images/icons/slave-offline.png new file mode 120000 index 0000000..a5b4f8f --- /dev/null +++ b/data/static/images/icons/slave-offline.png @@ -0,0 +1 @@ +build-failed.png \ No newline at end of file diff --git a/data/static/images/icons/slave-online.png b/data/static/images/icons/slave-online.png new file mode 120000 index 0000000..ef2847d --- /dev/null +++ b/data/static/images/icons/slave-online.png @@ -0,0 +1 @@ +build-finished.png \ No newline at end of file diff --git a/data/static/images/img01.jpg b/data/static/images/img01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e830ed235cf4d31a5c3aac5d2bafc33b6a02565d GIT binary patch literal 1287 zc-s4a<0R}-1rgsc-%#4BzOoEKef{g!lB|#*6SI;qo0vpk@T7$oFaN*Azyov#lOVGogFVB?f4}zr zR$w3jSigRnfA~McoT}&FtzP-J8nu$1grwyv1(X@$K<1Dx*LAXLv5R z^Y8J6b&7ZI_FMk=WfM}?e(A=y*MDUvS3X}0b|>@ZIxpK#|0dP={bPL_{-C?lW=i$b zzbZfG{^7p0{veO%lg0P-U#cH@{~+#a9Y>PCs@%_dPdoK`rf;i1I17D!W%Gl9;Q}Q5 z9T*t+_xO6qz>ppT1Jf?oQ;(cIb{Z-!iJ39slAe#px|u<2GXtbj9Wtjf9bjP4U1q{I zv8tUqb#RSg{F#?7PBVo*#cwC@%4!FQ(L01E9LrV=cG@W;+-?~)h1TV zgfg`kD=;wqadp@+xnl*7=aCRLl}B34LYJ4{{>zd1?LUJ9g4iz@zz##aGOiAAWKt-~ z07n;(gfPMo(=U+Y8NvQA`G}-w-;4`q6)sD0IW991Y4*v`T@6ow=CUhytl)DO43Ghd>n}4gt~5E;^eMBK^~q%^?$d5F zF2wm7F4!|G*%2&$ahZt`%yRuDG3!)Jx+e=sxt{FgReExy>*Z91%&CmaLVyvYk2l;O zl>BbI{022txn=UL*0UGd!G9J0jBfY?C-D8+A+aUA<5xL_eW6_5 n7$W*W7sT-bePhW7j2@ui)(1d89$aQ34wB?^?qHw-_<0R}-11{nq>W=25_A}^ z0R{$;RR}poCT12^26jOdV&50iV9+i-eTZkW(3;GEXZKbAfBZA zdL_q{hCJ|0>r^6%dTg|OvLhSk|?H(zc_C@&CrHHTgD)Wj2W>NAwW{=Hi) xzTk^b#!jZ55|=Ed7{0sHGMB%9%J}M9mZRF)OG-DCGAAtI{lV10K8yeVO#l~%N#Fng literal 0 Hc-jL100001 diff --git a/data/static/images/img03.jpg b/data/static/images/img03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64995c9f85253b072e7ed63507a6be348e5c56bf GIT binary patch literal 6321 zc-rlido+~m9>?FAF)p<$qv>Xc6x*djE{QOugh7)bBA0SpLQ=zIWSA{%HDba{E|EqV zx0pp+8U|52Vi>nEl~V3SiH2=3-Z`Va&OWVm{yBe~wa+^1_&(qFS0P!aPaTNcz;79-hNC5!gE z@~5CH7Zmz!`a>`%Od`o7wIUe&n`Blb`==m(q~I_ZTm~)$|E}_HO8f*s$N~niUKj)c zKoJla0wR6}6ieD(=>Yhr;LbL^5jOvl16rB>|J-x%9yi8C-1zXpDQNm@m~5C95T zdSB!F_(<8+lDpL2f2RE$#eoDFX1aS2%ZZG1x}MtQ=Z_TXa1jA31CM}}_vVh~3R5ZHqP zx#71*9ySz{5K}JUETYDD&nx9F+X*SShXrke7TFI`1G)!@O@#RqpT10ZKZ~N}>Uhh| z^0L60$fsi)n`;sSNLX;!n+G3pLJZ>pcRr~@MemMz@4=NVl}7{YGMb`|{7G!Diwp5H zQ}kEwdK<3I?>6eNmCj(0|Dr72 z*fZkku-igKL&Op}%zU5Uqi13udDrMn$dMAhOa>vb+DJFJj~Q*HUP?RRL3PB6Sg*$>d?hea9uVDRckQim?_c>nX!#;vZ)pJ8eFa}H4TglSX0Y+*Y>_6 zk0L6b7Vxa9mb^lC26|5k2}DvNr)7IXBbymT+{vCy(}f ziZK}D#M^ClPn#Q6?TW(c`M2A|fVNAz;QqFx456{6pVnqld)B<>DDir>D(S|!+l?lh zQO(TfwboBz$04)QXL#D~$UEVn!`C3@hFR=plq9Paf;)0UY>#f0m-y%FdT7~n=&HZBVnj!hVwf0^15vk* zk6^$)`>kT&F=?t==(!vc0=^XkToULa2JG&@<$!;Y!4`!F0VEKWqX7=me@7)Hej!OM zvBz89e*sbLW8oC8DC?IqrD6cV$2*glU|x4h)g3}@hWCNItOKpCsf_iw0E^un+jb}W zZi}5)L%rpO$py3PPiwatL~2*NPT+#Vm53C(0M3AmKF-Y+8Q4Xbj(_yAy)vimLK!jI zxy*+&R#iaM82vacT+j&%v^K|~HhtZ&&3rFcdw``hWT!#8jl}wc>Qo)vA>655MNum5gegsIZi04Wdt;21`w-wNIOZm+P67LC`A(rF(=VPE-Hp%ww|p+UVTT+vr+0$r#kw1-&} zk9=seed!hmbhnw7yU-pptiT8}mco^XQw6u{ZKDW7{5_3Lkt9vO`u(k?m1&$h;~YDl z|DxeY;As?9^-6|DxP5MKi3cNzLar*tBqO_~olCfnmXh|C)ibx&S7l31*8br^Z%X3r zidq5qE2(a$7X!7}!`i!0f4nuAJ+QYR1<58&;qRvU+zF|`yPQf_rc2qz%4+^>e+~E_ z9FRDZwJ8Ct>U80fY1_bCWNT$kq7iy*{p2m3{27O#NWGipMfb~-;m#iz35xm!MGR`U zMd4*-RE!i+V@jwH$$waf z^mwdWn4sfdxWA2$y%^pa>QrsDc5JOD<2X~l)ljRGlx@~-rsWuYC8F>cO^VI9dLmw3 z?yFD;TF5?{3_dBU{X|i07A#oqE60Na?WmJGmRqd5oR>Wsco^;nqmG9DSA`SzRai3l zg{2vb#S6>QEdElI;j(kw{Ek`YIh+_ci9+4wn~H%@|Gbi$F(b<4JLTRJh2yh|Xas)3e3$t!a$KN2R4`sOBY?w{!mE}P`<2pKNopXQezT8*#9SL6TPI7a;H?=AvL%K!iX literal 0 Hc-jL100001 diff --git a/data/static/images/img04.jpg b/data/static/images/img04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f5bc7cdf29ea490ad91e8d682acbced57504864c GIT binary patch literal 355 zc-s4aDnSU`Z86%5!xfCCN~A-Y(ZnOWIcSy+)|hy?!+ zFbHxm*fK~kGYT>=2{JMZGX6ip5Dan{0}5d0LKk6R5CDp>qlmDCTw$HcCN7Af^#3ge z9%e?M4a|ZJ_6(PyqM9|`)e{@fMrs{+5`CwKFY9C&yVAWU3B_Sv8)6+`TI>Jc1ON%> BE#LqE literal 0 Hc-jL100001 diff --git a/data/static/images/ipfire_tux_128x128.png b/data/static/images/ipfire_tux_128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..eb7819ed2eca8edeb87a91a13c9d94e06dd672c4 GIT binary patch literal 23763 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").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d===""){ci||(ci=c.createElement("iframe"),ci.frameBorder=ci.width=ci.height=0),c.body.appendChild(ci);if(!cj||!ci.createElement)cj=(ci.contentWindow||ci.contentDocument).document,cj.write("");b=cj.createElement(a),cj.body.appendChild(b),d=f.css(b,"display"),c.body.removeChild(ci)}ch[a]=d}return ch[a]}function cs(a,b){var c={};f.each(cn.concat.apply([],cn.slice(0,b)),function(){c[this]=a});return c}function cr(){co=b}function cq(){setTimeout(cr,0);return co=f.now()}function cg(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cf(){try{return new a.XMLHttpRequest}catch(b){}}function b_(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=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;ic)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){name="data-"+c.replace(j,"$1-$2").toLowerCase(),d=a.getAttribute(name);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(e){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function H(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(H,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=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=d.userAgent,x,y,z,A=Object.prototype.toString,B=Object.prototype.hasOwnProperty,C=Array.prototype.push,D=Array.prototype.slice,E=String.prototype.trim,F=Array.prototype.indexOf,G={};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=[null,a,null]:g=i.exec(a);if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.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.6",length:0,size:function(){return this.length},toArray:function(){return D.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)?C.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(),y.done(a);return this},eq:function(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(D.apply(this,arguments),"slice",D.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:C,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(;j0)return;y.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!y){y=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",z,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",z),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&H()}}},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},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):G[A.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!B.call(a,"constructor")&&!B.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||B.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw 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(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},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(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c
a",b=a.getElementsByTagName("*"),d=a.getElementsByTagName("a")[0];if(!b||!b.length||!d)return{};e=c.createElement("select"),f=e.appendChild(c.createElement("option")),g=a.getElementsByTagName("input")[0],i={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.55$/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:g.value==="on",optSelected:f.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},g.checked=!0,i.noCloneChecked=g.cloneNode(!0).checked,e.disabled=!0,i.optDisabled=!f.disabled;try{delete a.test}catch(r){i.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function click(){i.noCloneEvent=!1,a.detachEvent("onclick",click)}),a.cloneNode(!0).fireEvent("onclick")),g=c.createElement("input"),g.value="t",g.setAttribute("type","radio"),i.radioValue=g.value==="t",g.setAttribute("checked","checked"),a.appendChild(g),j=c.createDocumentFragment(),j.appendChild(a.firstChild),i.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",k=c.createElement("body"),l={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"};for(p in l)k.style[p]=l[p];k.appendChild(a),c.documentElement.appendChild(k),i.appendChecked=g.checked,i.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,i.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
",i.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",m=a.getElementsByTagName("td"),q=m[0].offsetHeight===0,m[0].style.display="",m[1].style.display="none",i.reliableHiddenOffsets=q&&m[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(h=c.createElement("div"),h.style.width="0",h.style.marginRight="0",a.appendChild(h),i.reliableMarginRight=(parseInt(c.defaultView.getComputedStyle(h,null).marginRight,10)||0)===0),k.innerHTML="",c.documentElement.removeChild(k);if(a.attachEvent)for(p in{submit:1,change:1,focusin:1})o="on"+p,q=o in a,q||(a.setAttribute(o,"return;"),q=typeof a[o]=="function"),i[p+"Bubbles"]=q;return i}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([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&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[c]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=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=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;return(e.value||"").replace(p,"")}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.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}})}}),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=a.selectedIndex,c=[],d=a.options,e=a.type==="select-one";if(b<0)return null;for(var g=e?b:0,h=e?b+1:d.length;g=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},attrFix:{tabindex:"tabIndex",readonly:"readOnly"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);var h,i,j=g!==1||!f.isXMLDoc(a);c=j&&f.attrFix[c]||c,i=f.attrHooks[c]||(v&&(f.nodeName(a,"form")||u.test(c))?v:b);if(d!==b){if(d===null||d===!1&&!t.test(c)){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;d===!0&&!t.test(c)&&(d=c),a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j)return i.get(a,c);h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))))},attrHooks:{type:{set:function(a,b){if(q.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.getAttribute("value");a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}}},propFix:{},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);c=i&&f.propFix[c]||c,h=f.propHooks[c];return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),f.support.getSetAttribute||(f.attrFix=f.extend(f.attrFix,{"for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder"}),v=f.attrHooks.name=f.attrHooks.value=f.valHooks.button={get:function(a,c){var d;if(c==="value"&&!f.nodeName(a,"button"))return a.getAttribute(c);d=a.getAttributeNode(c);return d&&d.specified?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},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.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)}})),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 w=Object.prototype.hasOwnProperty,x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function J(a){var c=a.target,d,e;if(!!y.test(c.nodeName)&&!c.readOnly){d=f._data(c,"_change_data"),e=I(c),(a.type!=="focusout"||c.type!=="radio")&&f._data(c,"_change_data",e);if(d===b||e===d)return;if(d!=null||e)a.type="change",a.liveFired=b,f.event.trigger(a,arguments[1],c)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.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]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),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]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.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!!k(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){return a.nodeName.toLowerCase()==="input"&&"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 bc[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=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||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=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":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=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=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(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.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:[]}},l.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&&(l.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.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[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}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.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 k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(h=g;h0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d-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||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());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(V(c[0])||V(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),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.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 X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};be.optgroup=be.option,be.tbody=be.tfoot=be.colgroup=be.caption=be.thead,be.th=be.td,f.support.htmlSerialize||(be._default=[1,"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){return this.each(function(){f(this).wrapAll(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(arguments[0]);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(arguments[0]).toArray());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(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!be[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1>");try{for(var c=0,d=this.length;c1&&l0?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=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bh(a,d),e=bi(a),g=bi(d);for(h=0;e[h];++h)bh(e[h],g[h])}if(b){bg(a,d);if(c){e=bi(a),g=bi(d);for(h=0;e[h];++h)bg(e[h],g[h])}}return d},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=[];for(var i=0,j;(j=a[i])!=null;i++){typeof j=="number"&&(j+="");if(!j)continue;if(typeof j=="string")if(!ba.test(j))j=b.createTextNode(j);else{j=j.replace(Z,"<$1>");var k=($.exec(j)||["",""])[1].toLowerCase(),l=be[k]||be._default,m=l[0],n=b.createElement("div");n.innerHTML=l[1]+j+l[2];while(m--)n=n.lastChild;if(!f.support.tbody){var o=_.test(j),p=k==="table"&&!o?n.firstChild&&n.firstChild.childNodes:l[1]===""&&!o?n.childNodes:[];for(var q=p.length-1;q>=0;--q)f.nodeName(p[q],"tbody")&&!p[q].childNodes.length&&p[q].parentNode.removeChild(p[q])}!f.support.leadingWhitespace&&Y.test(j)&&n.insertBefore(b.createTextNode(Y.exec(j)[0]),n.firstChild),j=n.childNodes}var r;if(!f.support.appendChecked)if(j[0]&&typeof (r=j.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bn.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;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bm.test(g)?g.replace(bm,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=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,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.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV;try{bU=e.href}catch(bW){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("
").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.bind(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?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[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","*":"*/*"},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}},ajaxPrefilter:bX(bS),ajaxTransport:bX(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?4:0;var o,r,u,w=l?b$(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b_(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),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._Deferred(),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.done,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)),bY(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),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]!=="*"?", */*; 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=bY(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){status<2?w(-1,z):f.error(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)bZ(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var ca=f.now(),cb=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+ca++}}),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&&(cb.test(b.url)||e&&cb.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(cb,l),b.url===j&&(e&&(k=k.replace(cb,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 cc=a.ActiveXObject?function(){for(var a in ce)ce[a](0,1)}:!1,cd=0,ce;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cf()||cg()}:cf,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,cc&&delete ce[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=++cd,cc&&(ce||(ce={},f(a).unload(cc)),ce[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ch={},ci,cj,ck=/^(?:toggle|show|hide)$/,cl=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cm,cn=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],co,cp=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cs("show",3),a,b,c);for(var g=0,h=this.length;g=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a=f.timers,b=a.length;while(b--)a[b]()||a.splice(b,1);a.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cm),cm=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.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cu=/^t(?:able|d|h)$/i,cv=/^(?: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=cw(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);f.offset.initialize();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.offset.supportsFixedPosition&&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.offset.doesNotAddBorder&&(!f.offset.doesAddBorderForTableAndCells||!cu.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.offset.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.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={initialize:function(){var a=c.body,b=c.createElement("div"),d,e,g,h,i=parseFloat(f.css(a,"marginTop"))||0,j="
";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.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=cv.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&&!cv.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=cw(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=cw(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(){return this[0]?parseFloat(f.css(this[0],d,"padding")):null},f.fn["outer"+c]=function(a){return this[0]?parseFloat(f.css(this[0],d,a?"margin":"border")):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];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["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 h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file diff --git a/data/static/js/pbs.js b/data/static/js/pbs.js new file mode 100644 index 0000000..a294dc1 --- /dev/null +++ b/data/static/js/pbs.js @@ -0,0 +1,42 @@ + +function getCookie(name) { + var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); + return r ? r[1] : undefined; +} + +jQuery.postJSON = function(url, args, callback) { + args._xsrf = getCookie("_xsrf"); + $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST", + success: function(response) { + callback(eval("(" + response + ")")); + }}); +}; + +$(function() { + var $search = $('#search'); + original_val = $search.val(); + + $search.focus(function() { + if($(this).val() === original_val) { + $(this).val(''); + } + }) + + .blur(function() { + if($(this).val() === '') { + $(this).val(original_val); + } + }); +}); + +action_run = function(action_id) { + $.postJSON("/api/action/run", { "id" : action_id }, function() {}); + + $("#action-" + action_id).hide(); +} + +action_remove = function(action_id) { + $.postJSON("/api/action/remove", { "id" : action_id }, function() {}); + + $("#action-" + action_id).hide(); +} diff --git a/data/static/robots.txt b/data/static/robots.txt new file mode 100644 index 0000000..6ffbc30 --- /dev/null +++ b/data/static/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Disallow: / + diff --git a/data/templates/base.html b/data/templates/base.html new file mode 100644 index 0000000..df80523 --- /dev/null +++ b/data/templates/base.html @@ -0,0 +1,109 @@ + + + + {{ hostname }} - {% block title %}{{ _("No title given") }}{% end block %} + + + + + + + + + + +
+ + + +
+
+
+
+ {% block body %}EMPTY BODY{% end block %} +
+ +
 
+
+
+
+
+ +
+
+

{{ _("About Pakfire") }}

+

+ {{ _("Pakfire is the buildsystem that is used to build the IPFire Linux firewall distribution.") }} + {{ _("It also installs and updates packages on the IPFire systems.") }} +

+
+
+

{{ _("Documentation") }}

+ +
+
+ + + diff --git a/data/templates/build-detail.html b/data/templates/build-detail.html new file mode 100644 index 0000000..7ab1fa6 --- /dev/null +++ b/data/templates/build-detail.html @@ -0,0 +1,151 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Build") }}: {{ build.name }}{% end block %} + +{% block body %} + {% if build.type == "binary" %} +

{{ _("Build") }}: {{ build.name }}

+ {% elif build.type == "source" %} +

{{ _("Build") }}: {{ build.name }}

+ {% end %} + + + + + + + + + + + + {% if build.type == "binary" %} + + + + + + + + + + + + + {% end %} + + + + {% if build.host %} + + {% else %} + + {% end %} + + + + + +
{{ _("ID") }}{{ build.uuid }}
{{ _("State") }}{{ build.state }}
{{ _("Package") }} + {{ build.pkg.friendly_name }} +
{{ _("Source build") }} + {{ build.source_build.name }} +
{{ _("Architecture") }}{{ build.arch }}
{{ _("Host") }} + {{ build.host.name }} + {{ _("No host assigned, yet.") }}
+ {{ _("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 %} +
+
 
+ + {% if build.type == "source" %} +

{{ _("Commit") }}: {{ escape(build.commit_subject) }}

+ + {% if build.commit_body %} + + + + {% end %} + + + + + + + + + + + + +
+ {{ escape(build.commit_body) }} +
{{ _("Author") }}{{ escape(build.commit_author) }}
{{ _("Committer") }}{{ escape(build.commit_committer) }}
{{ _("Date") }}{{ locale.format_date(build.commit_date or 0, full_format=True) }}
+
 
+ {% end %} + +

{{ _("Time") }}

+ + + + + + + + + + + + + + + + {% if build.duration %} + + + + + {% end %} +
{{ _("Job added") }}{{ build.time_added }}
{{ _("Job started") }}{{ build.time_started or _("Not started, yet.") }}
{{ _("Job finished") }}{{ build.time_finished or _("Not finished, yet.") }}
{{ _("Duration") }}{{ build.duration }}
+
 
+ + {% if build.packagefiles %} +

{{ _("Package files") }}

+ {{ modules.FilesTable(build.packagefiles) }} + {% end %} + + {% if build.logfiles %} +

{{ _("Logfiles") }}

+ {{ modules.FilesTable(build.logfiles) }} + {% end %} + +

{{ _("Log") }}

+ {{ modules.LogTable(build.log) }} +{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end %} diff --git a/data/templates/build-filter.html b/data/templates/build-filter.html new file mode 100644 index 0000000..de4f677 --- /dev/null +++ b/data/templates/build-filter.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Build job list") }}{% end block %} + +{% block body %} +

{{ _("Filter builds") }}

+
+ + + + + + + + + + + + + + +
{{ _("State") }} + + + {{ _("Only show builds with given state.") }} +
{{ _("Build host") }} + + + {{ _("Display only builds by selected host.") }} +
+ +
+
+{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/templates/build-list.html b/data/templates/build-list.html new file mode 100644 index 0000000..de639c3 --- /dev/null +++ b/data/templates/build-list.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Build job list") }}{% end block %} + +{% block body %} +

{{ _("Build job list") }}

+ {{ modules.BuildTable(builds) }} +{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/templates/build-priority.html b/data/templates/build-priority.html new file mode 100644 index 0000000..4f60ee3 --- /dev/null +++ b/data/templates/build-priority.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Edit build priority") }}{% end block %} + +{% block body %} +

{{ _("Edit build priority") }}: {{ build.name }}

+
+ {{ xsrf_form_html() }} + + + + + + + + + + + + +
{{ _("Priority") }} + + + {{ _("Set the priority of the build process.") }} +
+

+ {{ _("Beware") }}: + {{ _("Shuffeling build jobs can cause problems with the dependency solving.") }} + {{ _("Don't do this if you are not totally sure you won't break anything.") }} +

+
+ +
+
+{% end block %} diff --git a/data/templates/build-schedule-rebuild.html b/data/templates/build-schedule-rebuild.html new file mode 100644 index 0000000..0277f4e --- /dev/null +++ b/data/templates/build-schedule-rebuild.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Schedule rebuild for %s") % build.name }}{% end block %} + +{% block body %} +

{{ _("Schedule rebuild for %s") % build.name }}

+

+ {{ _("At this place, you can submit failed build jobs to be built again.") }} +

+

+ {{ _("The build job will be started when a build slot is available but not before the given time.") }} +

+ + {{ modules.BuildOffset() }} +{% end block %} diff --git a/data/templates/build-schedule-test.html b/data/templates/build-schedule-test.html new file mode 100644 index 0000000..6ccf0c1 --- /dev/null +++ b/data/templates/build-schedule-test.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Schedule test build for %s") % build.name }}{% end block %} + +{% block body %} +

{{ _("Schedule test build for %s") % build.name }}

+

+ {{ _("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.") }} +

+

+ {{ _("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.") }} +

+

+ {{ _("The build job will be started when a build slot is available but not before the given time.") }} +

+ + {{ modules.BuildOffset() }} +{% end block %} diff --git a/data/templates/builder-delete.html b/data/templates/builder-delete.html new file mode 100644 index 0000000..1812587 --- /dev/null +++ b/data/templates/builder-delete.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Delete builder %s") % builder.name }}{% end block %} + +{% block body %} +

{{ _("Builder") }}: {{ builder.name }}

+ +

+ {{ _("You are going to delete the build host %s.") % builder.name }} +

+ +

+ {{ _("Delete %s") % builder.name }} + {{ _("Back") }} +

+{% end block %} diff --git a/data/templates/builder-detail.html b/data/templates/builder-detail.html new file mode 100644 index 0000000..fa6594a --- /dev/null +++ b/data/templates/builder-detail.html @@ -0,0 +1,98 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Builder") }}: {{ builder.name }}{% end block %} + +{% block body %} +

{{ _("Builder") }}: {{ builder.name }}

+ + + + + + + + + + + + + + + + + + +
{{ _("Status") }}{{ builder.status }}
{{ _("Load average") }} + {{ builder.loadavg }} +
{{ _("Supported architectures") }} + {{ locale.list(builder.arches) or _("Unknown") }} +
+
 
+ +

{{ _("Configuration") }}

+ + + + + + + + + + + + + + + + + +
{{ _("Builds source packages") }} + {% if builder.build_src %}{{ _("Yes") }}{% else %}{{ _("No") }}{% end %} +
{{ _("Builds binary packages") }} + {% if builder.build_bin %}{{ _("Yes") }}{% else %}{{ _("No") }}{% end %} +
{{ _("Runs tests") }} + {% if builder.build_test %}{{ _("Yes") }}{% else %}{{ _("No") }}{% end %} +
{{ _("Parallel build jobs") }}{{ _("One job only.", "Up to %(num)s jobs.", builder.max_jobs) % { "num" : builder.max_jobs } }}
+
 
+ +

{{ _("Host information") }}

+ + + + + + + + + +
{{ _("CPU model") }}{{ builder.cpu_model or _("Unknown") }}
{{ _("Memory") }}{{ friendly_size(builder.memory) }}
+
 
+ + {% if builder.active_builds %} +

{{ _("Currently running builds on this host") }}

+ {{ modules.BuildTable(builder.active_builds) }} + {% end %} +{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/templates/builder-edit.html b/data/templates/builder-edit.html new file mode 100644 index 0000000..78b675b --- /dev/null +++ b/data/templates/builder-edit.html @@ -0,0 +1,80 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Edit builder %s") % builder.hostname }}{% end block %} + +{% block body %} +

{{ _("Edit builder %s") % builder.hostname }}

+ +
+ {{ xsrf_form_html() }} + + + + + + + + + + + +
{{ _("Name") }} + {{ builder.hostname }} + + {{ _("The hostname cannot be changed.") }} +
{{ _("Enabled") }} + + + {{ _("The builder must be enabled in order to process build jobs.") }} +
+
 
+ +

{{ _("Build job settings") }}

+

+ {{ _("These settings do only take effect if the builder is enabled.") }} +

+ + + + + + + + + + + + + + + + + + + + + + + + +
{{ _("Authorized to build source packages") }} + + + {{ _("Only a few build servers are allowed to build source packages.") }} +
{{ _("Authorized to build binary packages") }} + +  
{{ _("Authorized to build test packages") }} + +  
{{ _("Maximum number of parallel build jobs") }} + + + {{ _("This is the number of build jobs that are started in parallel.") }} +
+ +
+
+{% end block %} diff --git a/data/templates/builder-list.html b/data/templates/builder-list.html new file mode 100644 index 0000000..8211d0e --- /dev/null +++ b/data/templates/builder-list.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Build servers") }}{% end block %} + +{% block body %} +

{{ _("Build servers") }}

+ +

+ {{ _("Builders are those, that do all the hard work.") }} + {{ _("Build jobs are scheduled to these hosts that they process and send back the result.") }} +

+ + +{% end block %} + +{% if current_user.is_admin() %} + {% block sidebar %} +

{{ _("Actions") }}

+ + {% end block %} +{% end %} diff --git a/data/templates/builder-new.html b/data/templates/builder-new.html new file mode 100644 index 0000000..e3f23b8 --- /dev/null +++ b/data/templates/builder-new.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Create new builder") }}{% end block %} + +{% block body %} +

{{ _("Create a new builder") }}

+ +
+ {{ xsrf_form_html() }} + + + + + + + + + +
{{ _("Name") }} + + + {{ _("Must be the canonical hostname of the machine.") }} +
+ +
+
+{% end block %} diff --git a/data/templates/builder-pass.html b/data/templates/builder-pass.html new file mode 100644 index 0000000..5215876 --- /dev/null +++ b/data/templates/builder-pass.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Builder") }}: {{ builder.name }}

+ +

+ {% if action == "new" %} + {{ _("The new host %s has been successfully created.") % builder.name }} + {% elif action == "update" %} + {{ _("The passphrase for %s has been regenerated.") % builder.name }} + {% end %} + + {{ _("For authorization to the Pakfire Master Server there is a passphrase required which must be configured to the host.") }} + {{ _("This passphrase is:") }} +

+ +

{{ builder.passphrase }}

+ +

+ {{ _("Next") }} +

+{% end block %} diff --git a/data/templates/distro-detail.html b/data/templates/distro-detail.html new file mode 100644 index 0000000..18ef0af --- /dev/null +++ b/data/templates/distro-detail.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Distribution") }}: {{ distro.name }}

+ +

{{ distro.description }}

+ +

+ {{ _("This distribution is available for %s and maintained by %s.") % (locale.list(distro.arches), distro.vendor) }} +

+ +

{{ _("Binary repositories") }}

+ {{ modules.RepositoryTable(distro, distro.repositories) }} + +

{{ _("Sources") }}

+ {{ modules.SourceTable(distro, distro.sources) }} + +

{{ _("Log") }}

+ {{ modules.LogTable(distro.log) }} +{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/templates/distro-edit.html b/data/templates/distro-edit.html new file mode 100644 index 0000000..4119f0d --- /dev/null +++ b/data/templates/distro-edit.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Edit distribution %s") % distro.name }}{% end block %} + +{% block body %} +

{{ _("Edit distribution %s") % distro.name }}

+ +
+ {{ xsrf_form_html() }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ _("Name") }} + + + {{ _("The fancy name of the distribution.") }} +
{{ _("Identifier") }} + {{ distro.sname }} + + {{ _("Cannot be changed.") }} +
{{ _("Vendor") }} + + + {{ _("From whom is the distribution from?") }} +
{{ _("Slogan") }} + + + {{ _("A short sentence that characterizes the distribution.") }} +
{{ _("Architectures") }} + + + {{ _("For which architectures should the distribution be built?") }} +
{{ _("Sources") }} + + + {{ _("Which sources should be imported to the distribution?") }} +
+ +
+
 
+
+{% end block %} diff --git a/data/templates/distro-list.html b/data/templates/distro-list.html new file mode 100644 index 0000000..9d53143 --- /dev/null +++ b/data/templates/distro-list.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Distributions") }}{% end block %} + +{% block body %} +

{{ _("Distributions") }}

+
    + {% for distro in distros %} +
  • + {{ distro.name }} - {{ distro.slogan }} +
  • + {% end %} +
+{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/templates/docs-base.html b/data/templates/docs-base.html new file mode 100644 index 0000000..f7a0dd6 --- /dev/null +++ b/data/templates/docs-base.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block sidebar %} + +
 
+ +

{{ _("Topics") }}

+ +{% end block %} diff --git a/data/templates/docs-build.html b/data/templates/docs-build.html new file mode 100644 index 0000000..6cdfcac --- /dev/null +++ b/data/templates/docs-build.html @@ -0,0 +1,28 @@ +{% extends "docs-base.html" %} + +{% block title %}{{ _("Legend of the build states") }}{% end block %} + +{% block body %} +

{{ _("Legend of the build states") }}

+

+ {{ _("Every build that is done by the Pakfire Build Service has to go through several states:") }} +

+

+ {{ _("After checking out the source from the source repository a source package is created and submitted to the build server.") }} + {{ _("Starting from inserting a source file to the build service, there are binary build jobs created for every supported architecture.") }} +

+

+ {{ _("These get assigned to a build host which has to compile or assemble the package and return it back to the build server.") }} + {{ _("In the table below, there are all states that a build job goes through:") }} +

+ +{% end %} diff --git a/data/templates/docs-index.html b/data/templates/docs-index.html new file mode 100644 index 0000000..e2553ce --- /dev/null +++ b/data/templates/docs-index.html @@ -0,0 +1,27 @@ +{% extends "docs-base.html" %} + +{% block title %}{{ _("Legend of the build states") }}{% end block %} + +{% block body %} +

{{ _("Documents") }}

+

+ {{ _("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.") }} +

+ +

{{ _("Documents for testers") }}

+ + +

{{ _("Documents for developers") }}

+ + +

+ {{ _("Technical documentation is available on the wiki:") }} + {{ _("Technical documentation") }}. +

+{% end %} diff --git a/data/templates/docs-users.html b/data/templates/docs-users.html new file mode 100644 index 0000000..1fafd1f --- /dev/null +++ b/data/templates/docs-users.html @@ -0,0 +1,41 @@ +{% extends "docs-base.html" %} + +{% block title %}{{ _("Legend of the build states") }}{% end block %} + +{% block body %} +

{{ _("Users") }}

+

+ {{ _("All users can join the Pakfire Build Service and are separated into three groups:") }} +

+ +

{{ _("Developers") }}

+

+ {{ _("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.") }} +

+

+ {{ _("Guidelines for developers") }} +

+ +

{{ _("Testers") }}

+

+ {{ _("Testers are like users but have the right to vote on packages, which is used to figure out the quality of the package.") }} + {{ _("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.") }} +

+

+ {{ _("Guidelines for testers") }} +

+ +

{{ _("Users") }}

+

+ {{ _("Everybody can join the Pakfire Build Service by registering an account.") }} + {{ _("After a successful activation you are able to leave comments on packages and give feedback to the developers about its status.") }} +

+ + {% if not current_user %} +

+ {{ _("Register") }} +

+ {% end %} +{% end %} diff --git a/data/templates/error-404.html b/data/templates/error-404.html new file mode 100644 index 0000000..d9d75a3 --- /dev/null +++ b/data/templates/error-404.html @@ -0,0 +1,5 @@ +{% extends "error.html" %} + +{% block body %} + 404 +{% end block %} diff --git a/data/templates/error-500.html b/data/templates/error-500.html new file mode 100644 index 0000000..01f85d2 --- /dev/null +++ b/data/templates/error-500.html @@ -0,0 +1,9 @@ +{% extends "error.html" %} + +{% block body %} + 500 + + {% if exception %} +
{{ exception }}
+ {% end %} +{% end block %} diff --git a/data/templates/error.html b/data/templates/error.html new file mode 100644 index 0000000..94d9808 --- /dev/null +++ b/data/templates/error.html @@ -0,0 +1 @@ +{% extends "base.html" %} diff --git a/data/templates/file-detail.html b/data/templates/file-detail.html new file mode 100644 index 0000000..9326ec3 --- /dev/null +++ b/data/templates/file-detail.html @@ -0,0 +1,120 @@ +{% extends "base.html" %} + +{% block body %} +

+ {{ _("File") }}: + {{ file.name }} +

+ +

{{ file.summary or pkg.summary }}

+ + + + + + + + + + + + + + + + + + + + + + + + + {% if file.maintainer %} + + + + + {% end %} + + + + + + + + + + + + {% for i in file.provides %} + + + + + {% end %} + + + + + + + {% for i in file.requires %} + + + + + {% end %} + + {% for i in file.obsoletes %} + + + + + {% end %} + + {% for i in file.conflicts %} + + + + + {% end %} +
{{ _("Package") }} + {{ pkg.friendly_name }} +
{{ _("Description") }}
{{ file.description or pkg.description }}
{{ _("URL") }} + {{ file.url or pkg.url }} +
{{ _("License") }}{{ file.license or pkg.license}}
{{ _("Maintainer") }}{{ escape(file.maintainer or pkg.maintainer) }}
{{ _("Size") }}{{ friendly_size(file.size) }}
{{ _("Hash") }}{{ file.hash1 }}
{{ _("Provides") }}{{ i }}
{{ _("Requires") }}{{ "
".join(file.requires) }}
{{ _("Requires") }}{{ i }}
{{ _("Obsoletes") }}{{ i }}
{{ _("Conflicts") }}{{ i }}
+
 
+ +

{{ _("Build information") }}

+ + + + + + + + + + + + + +
{{ _("ID") }} + {{ file.build_id }} +
{{ _("Host") }} + {{ file.build_host }} +
{{ _("Time") }}{{ file.build_date }}
+
 
+ +

{{ _("Files") }}

+ {{ modules.PackageFilesTable(file.filelist) }} +{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/templates/index.html b/data/templates/index.html new file mode 100644 index 0000000..07d46d4 --- /dev/null +++ b/data/templates/index.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Welcome to the Pakfire Build Service") }}{% end block %} + +{% block body %} +

{{ _("Welcome to the Pakfire Build Service") }}

+

+ {{ _("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.") }} + {{ _("Learn more...") }} +

+ + {% if active_builds %} +

{{ _("Ongoing builds") }}

+ {{ modules.BuildTable(active_builds) }} + {% end %} + + {% if next_builds %} +

{{ _("Queued builds") }}

+ {{ modules.BuildTable(next_builds) }} + {% end %} + +

{{ _("Lately updated builds") }}

+ {{ modules.BuildTable(latest_builds) }} + +

{{ _("Statistics") }}

+
    +
  • + {{ _("There is currently one pending build job.", "There are currently %(num)s pending build jobs.", counter_pending) % { "num" : counter_pending } }} +
  • +
  • + {{ _("Average build time is %.1f minutes.") % (average_build_time / 60) }} +
  • +
+{% end %} diff --git a/data/templates/log.html b/data/templates/log.html new file mode 100644 index 0000000..e684a60 --- /dev/null +++ b/data/templates/log.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Log") }}

+ + {{ modules.LogTable(log) }} +{% end %} diff --git a/data/templates/login-successful.html b/data/templates/login-successful.html new file mode 100644 index 0000000..be35756 --- /dev/null +++ b/data/templates/login-successful.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Login successful") }}

+

+ {{ _("Welcome, %s.") % escape(user.realname) }} +

+ +

+ {{ _("Your login to the Pakfire Build Server was successful.") }} +

+ +

+ {{ _("Go on") }} +

+{% end %} diff --git a/data/templates/login.html b/data/templates/login.html new file mode 100644 index 0000000..47e73b9 --- /dev/null +++ b/data/templates/login.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Login") }}

+ + {% if failed %} +

+ {{ _("Username and/or password was wrong. Login failed.") }} +

+ {% end %} + +

+ {{ _("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.") }} + {{ _("Register a new account.") }} +

+ +
+ {{ xsrf_form_html() }} + + + + + + + + + + + + +
{{ _("Username") }}
{{ _("Password") }}
+ +
+
+{% end %} diff --git a/data/templates/logout.html b/data/templates/logout.html new file mode 100644 index 0000000..7b8563b --- /dev/null +++ b/data/templates/logout.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Logout successful") }}

+ +

+ {{ _("You have successfully logged out from the Pakfire Build Server.") }} + {{ _("Have a nice day!") }} +

+ +

+ {{ _("Go on") }} +

+{% end %} diff --git a/data/templates/modules/build-log.html b/data/templates/modules/build-log.html new file mode 100644 index 0000000..89e5bd9 --- /dev/null +++ b/data/templates/modules/build-log.html @@ -0,0 +1,13 @@ + + + {% for message in messages %} + + + + + {% end %} +
+ {{ message.time }} + + {{ message.message }} +
diff --git a/data/templates/modules/build-offset.html b/data/templates/modules/build-offset.html new file mode 100644 index 0000000..7f8568f --- /dev/null +++ b/data/templates/modules/build-offset.html @@ -0,0 +1,25 @@ +
+ {{ xsrf_form_html() }} + + + + + + + + + +
{{ _("Start time") }} + + + {{ _("Set the time after which the build job starts.") }} +
+ +
+
diff --git a/data/templates/modules/build-table.html b/data/templates/modules/build-table.html new file mode 100644 index 0000000..d1e936c --- /dev/null +++ b/data/templates/modules/build-table.html @@ -0,0 +1,20 @@ +
    + {% if builds %} + {% for build in builds %} +
  • + {% if build.type == "binary" %} + {{ build.pkg.friendly_name }}.{{ build.arch }} + {% elif build.type == "source" %} + {{ build.name }} + {% else %} + {{ _("Unknown build type.") }} + {% end %} +
  • + {% end %} + {% else %} +
  • There are no builds to display.
  • + {% end %} +
+
 
diff --git a/data/templates/modules/comments-table.html b/data/templates/modules/comments-table.html new file mode 100644 index 0000000..590769f --- /dev/null +++ b/data/templates/modules/comments-table.html @@ -0,0 +1,21 @@ +
+ {% if comments %} + {% for comment in comments %} +
+

{{ escape(comment.text) }}

+ + {% if show_package %} + {{ _("on %s") % comment.pkg.friendly_name }} - + {% end %} + {% if show_user %} + {{ _("by %s") % escape(comment.user.realname) }} - + {% end %} + {{ locale.format_date(comment.time) }} + +
+ {% end %} + {% else %} +

{{ _("No comments so far.") }}

+ {% end %} +
+
 
diff --git a/data/templates/modules/files-table.html b/data/templates/modules/files-table.html new file mode 100644 index 0000000..722b723 --- /dev/null +++ b/data/templates/modules/files-table.html @@ -0,0 +1,14 @@ + + {% for file in files %} + + + + + {% end %} +
+ {{ _("Download") }} + {% if not file.type == "log" %} + | {{ _("Info") }} + {% end %} + {{ file.name }}
+
 
diff --git a/data/templates/modules/log-table.html b/data/templates/modules/log-table.html new file mode 100644 index 0000000..547b7e9 --- /dev/null +++ b/data/templates/modules/log-table.html @@ -0,0 +1,28 @@ + + {% if messages %} + {% for message in messages %} + + + + + + {% end %} + {% else %} + + + + {% end %} +
+ {{ locale.format_date(message.time, relative=False, full_format=True) }} + + {{ escape(message.message) }} +
+ {{ _("No log entries, yet.") }} +
+
 
diff --git a/data/templates/modules/package-files-table.html b/data/templates/modules/package-files-table.html new file mode 100644 index 0000000..d8987a5 --- /dev/null +++ b/data/templates/modules/package-files-table.html @@ -0,0 +1,16 @@ + + {% for file in files %} + + + + + + {% end %} +
+ {{ file.name }} + + {{ friendly_size(file.size) }} + + {{ file.hash1 }} +
+
 
diff --git a/data/templates/modules/package-table-detail.html b/data/templates/modules/package-table-detail.html new file mode 100644 index 0000000..318e80d --- /dev/null +++ b/data/templates/modules/package-table-detail.html @@ -0,0 +1,7 @@ + diff --git a/data/templates/modules/package-table.html b/data/templates/modules/package-table.html new file mode 100644 index 0000000..d3e8dc3 --- /dev/null +++ b/data/templates/modules/package-table.html @@ -0,0 +1,14 @@ + + +

{{ letter.upper() }}

+ + + {% for pkg in packages %} + + + + {% end %} +
+ {{ pkg }} +
+
 
diff --git a/data/templates/modules/repo-actions-table.html b/data/templates/modules/repo-actions-table.html new file mode 100644 index 0000000..900b2db --- /dev/null +++ b/data/templates/modules/repo-actions-table.html @@ -0,0 +1,45 @@ +
+ {% for action in actions %} +
+ {{ action.pkg.friendly_name }} + {{ _("added %s") % locale.format_date(action.time_added) }} + + {% if current_user and action.have_permission(current_user) %} +

+ {% if action.is_doable() %} + {{ _("Run") }} + {% end %} + {{ _("Remove action") }} +

+ {% end %} + +
+

+ {% if action.credits_needed %} + {{ _("%(credits)s more credit needed.", "%(credits)s more credits needed.", action.credits_needed) % { "credits" : action.credits_needed } }} + {% else %} + {{ _("No more credits needed.") }} + {% end %} +
+ {% if action.pkg.maintainer %} + {{ _("Maintainer: %s") % action.pkg.maintainer }} +
+ {% end %} + {{ _("Go to package description") }} +
+

+
+ + +
+ {% end %} +
+
 
diff --git a/data/templates/modules/repository-table.html b/data/templates/modules/repository-table.html new file mode 100644 index 0000000..12416ab --- /dev/null +++ b/data/templates/modules/repository-table.html @@ -0,0 +1,8 @@ + +
 
diff --git a/data/templates/modules/source-table.html b/data/templates/modules/source-table.html new file mode 100644 index 0000000..fd73e10 --- /dev/null +++ b/data/templates/modules/source-table.html @@ -0,0 +1,7 @@ + diff --git a/data/templates/modules/user-table.html b/data/templates/modules/user-table.html new file mode 100644 index 0000000..9ff2287 --- /dev/null +++ b/data/templates/modules/user-table.html @@ -0,0 +1,5 @@ + diff --git a/data/templates/package-detail-list.html b/data/templates/package-detail-list.html new file mode 100644 index 0000000..f3ea1f3 --- /dev/null +++ b/data/templates/package-detail-list.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Package") }} {{ pkg.name }}{% end block %} + +{% block body %} +

{{ _("Package") }}: {{ pkg.name }}

+ +

+ {{ pkg.summary }} +

+

+ {{ _("There is one version of %(pkg)s.", "There are %(num)s different versions of %(pkg)s.", len(packages)) % { "num" : len(packages), "pkg" : pkg.name, } }} +

+ {{ modules.PackageTable2(packages) }} +{% end block %} diff --git a/data/templates/package-detail.html b/data/templates/package-detail.html new file mode 100644 index 0000000..e218bd9 --- /dev/null +++ b/data/templates/package-detail.html @@ -0,0 +1,111 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Package") }}: {{ pkg.friendly_name }}{% end block %} + +{% block body %} +

{{ _("Package") }}: {{ pkg.name }}-{{ pkg.friendly_version }}

+ +

{{ pkg.description }}

+ + + + + + + + + + + + {% if pkg.maintainer %} + + + + + {% end %} + + + + + +
{{ _("URL") }} + {{ pkg.url }} +
{{ _("License") }}{{ pkg.license }}
{{ _("Maintainer") }}{{ escape(pkg.maintainer) }}
{{ _("Supported architectures") }}{{ locale.list(pkg.supported_arches) }}
+
 
+ +

{{ _("Comments") }}

+

+ {{ _("This package got a total credit count of %s credits.") % pkg.credits }} +

+ + {% if current_user %} +

+ {{ _("Add comment") }} + +

+
+

{{ _("Add comment") }}

+
+ {{ xsrf_form_html() }} + + + + + + + + + +
+ +
+ {% if current_user.is_tester() or current_user.is_admin() %} + {{ _("Vote") }}: + {{ _("Not tested") }} + {{ _("Works for me") }} + {{ _("Doesn't work for me") }} + {% end %} + + +
+
+
+ {% else %} +

{{ _("You must be logged in to comment.") }}

+ {% end %} + {{ modules.CommentsTable(pkg.comments) }} + + {% if pkg.builds %} +

{{ _("Build jobs") }}

+ {{ modules.BuildTable(pkg.builds) }} + {% end %} + + {% if pkg.packagefiles %} +

{{ _("Package files") }}

+ {{ modules.FilesTable(pkg.packagefiles) }} + {% end %} + + {% if pkg.logfiles %} +

{{ _("Logfiles") }}

+ {{ modules.FilesTable(pkg.logfiles) }} + {% end %} + +

{{ _("Log") }}

+ {{ modules.LogTable(pkg.log) }} +{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/templates/package-list.html b/data/templates/package-list.html new file mode 100644 index 0000000..0f20e72 --- /dev/null +++ b/data/templates/package-list.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Package list") }}{% end block %} + +{% block body %} +

{{ _("Package list") }}

+

+ {{ _("This is an alphabetically ordered list of all packages in the distribution.") }} + {{ _("Click on a link to see further information about the package.") }} +

+ +
    +
  • {{ _("Quick selection:") }}
  • + {% for letter in sorted(packages.keys()) %} +
  • {{ letter.upper() }}
  • + {% end %} +
+
 
+ + {% for letter, pkgs in sorted(packages.items()) %} + {{ modules.PackageTable(letter, pkgs) }} + {% end %} +{% end block %} diff --git a/data/templates/register-activation-fail.html b/data/templates/register-activation-fail.html new file mode 100644 index 0000000..eb6c52e --- /dev/null +++ b/data/templates/register-activation-fail.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Activation failed") }}

+

+ {{ _("We are sorry.") }} + {{ _("The activation of your account has failed.") }} + {{ _("Possibly the registration code is wrong or your registration timed out.") }} +

+{% end %} diff --git a/data/templates/register-activation-success.html b/data/templates/register-activation-success.html new file mode 100644 index 0000000..0043c68 --- /dev/null +++ b/data/templates/register-activation-success.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Activation successful") }}

+

+ {{ _("Your account has been activated, %s.") % user.realname }} + {{ _("Have fun!") }} +

+{% end %} diff --git a/data/templates/register-fail.html b/data/templates/register-fail.html new file mode 100644 index 0000000..af204af --- /dev/null +++ b/data/templates/register-fail.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Registration failed") }}

+

+ {{ _("We are sorry.") }} + {{ _("We could not create your requested account.") }} +

+ +
    + {% for msg in messages %} +
  • {{ msg }}
  • + {% end %} +
+ +

+ {{ _("Use the back button on your web browser to go back to the previous page and correct your submission.") }} +

+{% end %} diff --git a/data/templates/register-success.html b/data/templates/register-success.html new file mode 100644 index 0000000..d5d2623 --- /dev/null +++ b/data/templates/register-success.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Registration successful") }}

+

+ {{ _("Your new account has been created, %s.") % user.realname }} + {{ _("To complete the activation, follow the instructions that were sent to you in an activation email.") }} +

+{% end %} diff --git a/data/templates/register.html b/data/templates/register.html new file mode 100644 index 0000000..da75c30 --- /dev/null +++ b/data/templates/register.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Register new account") }}

+ +
+ {{ xsrf_form_html() }} + + + + + + + + + + + + + + + + +
{{ _("Name") }}: + + + {{ _("Must be a unique name you login with.") }} +
{{ _("Email") }}: + + + {{ _("Type your email address.") }} +
{{ _("Real name (optional)") }}: + + + {{ _("Type you firstname and your lastname here.") }} +
+ +

{{ _("Account security") }}

+ + + + + + + + + + + + + + +
{{ _("Password") }}: + + + {{ _("The password is used to secure the login and must be at least 8 characters.") }} +
{{ _("Confirm") }}: + +  
+ +
+
+{% end block %} diff --git a/data/templates/repository-detail.html b/data/templates/repository-detail.html new file mode 100644 index 0000000..abca9b4 --- /dev/null +++ b/data/templates/repository-detail.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block body %} +

+ {{ _("Repository") }}: {{ repo.name }} - + {{ _("from") }} {{ distro.name }} +

+ +

{{ repo.description }}

+ +

+ {{ _("This repository contains %s packages and is available for %s.") % (len(repo.packages), locale.list(repo.arches)) }} +

+ + {% if repo.has_actions() %} +

{{ _("Pending actions") }}

+ {{ modules.RepoActionsTable(repo) }} + {% end %} + +

{{ _("Waiting packages") }}

+

+ {{ _("These packages are waiting to be published.") }} +

+ {{ modules.PackageTable2(repo.waiting_packages) }} + +

{{ _("Pushed packages") }}

+ {{ modules.PackageTable2(repo.pushed_packages) }} + +

{{ _("Log") }}

+ {{ modules.LogTable(repo.log) }} +{% end block %} diff --git a/data/templates/search-form.html b/data/templates/search-form.html new file mode 100644 index 0000000..63eb95b --- /dev/null +++ b/data/templates/search-form.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Advanced search") }}{% end block %} + +{% block body %} +

{{ _("Advanced search") }}

+ +

+ XXX TO BE DONE +

+{% end block %} diff --git a/data/templates/search-results.html b/data/templates/search-results.html new file mode 100644 index 0000000..93687b4 --- /dev/null +++ b/data/templates/search-results.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Search results for '%s'") % escape(query) }}{% end block %} + +{% block body %} +

{{ _("Search results for '%s'") % escape(query) }}

+

+ {{ _("These are the results you searched for.") }} +

+ + + {% for pkg in pkgs %} + + + + + {% end %} +
+ {{ pkg.name }} + {{ pkg.summary }}
+{% end block %} diff --git a/data/templates/source-detail.html b/data/templates/source-detail.html new file mode 100644 index 0000000..06dae3c --- /dev/null +++ b/data/templates/source-detail.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Source") }}: {{ source.name }}

+ + + + + + + + + + + + + +
{{ _("Revision") }}{{ source.revision }}
{{ _("Branch") }} + {{ source.branch }} +
+
 
+ +

{{ _("Latest builds") }}

+ {{ modules.BuildTable(source.builds) }} +{% end block %} diff --git a/data/templates/source-list.html b/data/templates/source-list.html new file mode 100644 index 0000000..69bda37 --- /dev/null +++ b/data/templates/source-list.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Sources repositories") }}{% end block %} + +{% block body %} +

{{ _("Source repositories") }}

+ + + +{% end block %} diff --git a/data/templates/user-comments.html b/data/templates/user-comments.html new file mode 100644 index 0000000..846825e --- /dev/null +++ b/data/templates/user-comments.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Latest user comments") }}

+ + {{ modules.CommentsTable(comments, show_package=True, show_user=True) }} +{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/templates/user-delete.html b/data/templates/user-delete.html new file mode 100644 index 0000000..926a6fc --- /dev/null +++ b/data/templates/user-delete.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Delete user %s") % user.realname }}

+ +

+ {% if current_user == user %} + {{ _("Do you really want to delete your own account?") }} + {{ _("You won't be able to login and use this service any more.") }} + {% else %} + {{ _("Do you really want to delete the user %s?") % user.realname }} + {% end %} +

+ +

+ {{ _("Delete %s") % user.realname }} + | + {{ _("Back") }} +

+{% end block %} diff --git a/data/templates/user-list.html b/data/templates/user-list.html new file mode 100644 index 0000000..3a98e72 --- /dev/null +++ b/data/templates/user-list.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Users") }}

+

+ {{ _("On this page you can see a list of all users that are known to the system.") }} +

+ + {% if admins %} +

{{ _("Administrators") }}

+ {{ modules.UsersTable(admins) }} + {% end %} + + {% if testers %} +

{{ _("Testers") }}

+ {{ modules.UsersTable(testers) }} + {% end %} + + {% if users %} +

{{ _("Users") }}

+ {{ modules.UsersTable(users) }} + {% end %} +{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/templates/user-profile-edit-fail.html b/data/templates/user-profile-edit-fail.html new file mode 100644 index 0000000..d1ec50f --- /dev/null +++ b/data/templates/user-profile-edit-fail.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Edit failed") }}

+

+ {{ _("We are sorry.") }} + {{ _("The user profile cannot be saved.") }} +

+ +
    + {% for msg in messages %} +
  • {{ msg }}
  • + {% end %} +
+ +

+ {{ _("Use the back button on your web browser to go back to the previous page and correct your submission.") }} +

+{% end %} diff --git a/data/templates/user-profile-edit.html b/data/templates/user-profile-edit.html new file mode 100644 index 0000000..432f6da --- /dev/null +++ b/data/templates/user-profile-edit.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Edit user %s") % user.realname }}

+ +
+ {{ xsrf_form_html() }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if current_user.is_admin() and not current_user == user %} +
{{ _("Username") }}:{{ user.name }}{{ _("Cannot be changed.") }}
{{ _("Email") }}: + + + {{ _("If the email address is changed, your account will be disabled until you reconfirm the new email address.") }} +
{{ _("Real name (optional)") }}: + + + {{ _("Your real name is used to identify you by others.") }} +
{{ _("Password") }}: + + + {{ _("The password is used to secure the login and must be at least 8 characters.") }} +
{{ _("Confirm") }}: + + + {{ _("Leave the password fields empty to keep the current password.") }} +
{{ _("Preferred language") }}: + + {{ _("Auto-detect will use the language transmitted by your browser.") }}
+ +

{{ _("Admin actions") }}

+ + + + + + + {% end %} + + + +
{{ _("State") }} + + + {{ _("Define the permissions of the user.") }} +
+ +
+
+{% end block %} diff --git a/data/templates/user-profile-need-activation.html b/data/templates/user-profile-need-activation.html new file mode 100644 index 0000000..bcf792a --- /dev/null +++ b/data/templates/user-profile-need-activation.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block body %} +

{{ _("Edit successful") }}

+

+ {{ _("The user profile was successfully altered.") }} + {{ _("But as you have changed the email address, you need to re-activate the account.") }} + {{ _("Have a look at you mailbox - you already do know what to do.") }} +

+{% end %} diff --git a/data/templates/user-profile.html b/data/templates/user-profile.html new file mode 100644 index 0000000..dc87da8 --- /dev/null +++ b/data/templates/user-profile.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block body %} + {{ user.name }} +

{{ _("User") }}: {{ escape(user.realname) }}

+ + + + + + + + + + + + + + + {% if current_user == user or current_user.is_admin() %} + + + + + {% end %} +
{{ _("Username") }}{{ escape(user.name) }}
{{ _("Email") }} + {{ escape(user.email) }} +
{{ _("State") }} + {% if user.is_admin() %} + {{ _("Admin") }} + {% elif user.is_tester() %} + {{ _("Tester") }} + {% else %} + {{ _("User") }} + {% end %} +
{{ _("Registered") }}{{ locale.format_date(user.registered, full_format=True) }}
+
 
+ +

{{ _("Log") }}

+ {{ modules.LogTable(user.log) }} + +

{{ _("Comments written by %s") % user.realname }}

+ {{ modules.CommentsTable(user.comments, show_package=True, show_user=False) }} +{% end block %} + +{% block sidebar %} +

{{ _("Actions") }}

+ +{% end block %} diff --git a/data/translations/de_DE/LC_MESSAGES/pakfire.mo b/data/translations/de_DE/LC_MESSAGES/pakfire.mo new file mode 100644 index 0000000000000000000000000000000000000000..3c448408754ca6d6ee32a5f35b814ea4ab4c9bae GIT binary patch literal 29731 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

m) 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 literal 0 Hc-jL100001 diff --git a/data/translations/de_DE/LC_MESSAGES/pakfire.po b/data/translations/de_DE/LC_MESSAGES/pakfire.po new file mode 100644 index 0000000..a8f525d --- /dev/null +++ b/data/translations/de_DE/LC_MESSAGES/pakfire.po @@ -0,0 +1,1627 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Pakfire Build Service\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-05-15 22:25+0200\n" +"PO-Revision-Date: 2011-05-15 22:56+0100\n" +"Last-Translator: Stefan Schantl \n" +"Language-Team: German \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Poedit-Language: German\n" +"X-Poedit-Country: GERMANY\n" +"X-Poedit-SourceCharset: utf-8\n" + +#: data/templates/build-detail.html:3 +#: data/templates/build-detail.html:7 +#: data/templates/build-detail.html:9 +#: data/templates/modules/log-table.html:10 +msgid "Build" +msgstr "Build" + +#: data/templates/build-detail.html:14 +#: data/templates/file-detail.html:98 +msgid "ID" +msgstr "ID" + +#: data/templates/build-detail.html:18 +#: data/templates/build-filter.html:10 +#: data/templates/user-profile.html:19 +#: data/templates/user-profile-edit.html:71 +msgid "State" +msgstr "Status" + +#: data/templates/build-detail.html:24 +#: data/templates/file-detail.html:14 +#: data/templates/package-detail-list.html:3 +#: data/templates/package-detail-list.html:6 +#: data/templates/package-detail.html:3 +#: data/templates/package-detail.html:6 +#: data/templates/modules/log-table.html:12 +msgid "Package" +msgstr "Paket" + +#: data/templates/build-detail.html:30 +msgid "Architecture" +msgstr "Architektur" + +#: data/templates/build-detail.html:36 +#: data/templates/file-detail.html:104 +msgid "Host" +msgstr "Host" + +#: data/templates/build-detail.html:42 +msgid "No host assigned, yet." +msgstr "Noch kein Host zugewiesen." + +#: data/templates/build-detail.html:47 +#: data/templates/build-priority.html:11 +msgid "Priority" +msgstr "Priorität" + +#: data/templates/build-detail.html:51 +#: data/templates/build-priority.html:14 +msgid "Very high" +msgstr "Sehr Hoch" + +#: data/templates/build-detail.html:53 +#: data/templates/build-priority.html:15 +msgid "High" +msgstr "Hoch" + +#: data/templates/build-detail.html:55 +#: data/templates/build-priority.html:16 +msgid "Medium" +msgstr "Normal" + +#: data/templates/build-detail.html:57 +#: data/templates/build-priority.html:17 +msgid "Low" +msgstr "Niedrig" + +#: data/templates/build-detail.html:59 +#: data/templates/build-priority.html:18 +msgid "Very low" +msgstr "Sehr Niedrig" + +#: data/templates/build-detail.html:67 +msgid "Commit" +msgstr "Commit" + +#: data/templates/build-detail.html:77 +msgid "Author" +msgstr "Autor" + +#: data/templates/build-detail.html:81 +msgid "Committer" +msgstr "Committer" + +#: data/templates/build-detail.html:85 +msgid "Date" +msgstr "Datum" + +#: data/templates/build-detail.html:92 +#: data/templates/file-detail.html:110 +msgid "Time" +msgstr "Zeit" + +#: data/templates/build-detail.html:96 +msgid "Job added" +msgstr "Job hinzugefügt" + +#: data/templates/build-detail.html:100 +msgid "Job started" +msgstr "Job gestartet" + +#: data/templates/build-detail.html:101 +msgid "Not started, yet." +msgstr "Noch nicht gestartet." + +#: data/templates/build-detail.html:104 +msgid "Job finished" +msgstr "Job beendet" + +#: data/templates/build-detail.html:105 +msgid "Not finished, yet." +msgstr "Noch nicht beendet." + +#: data/templates/build-detail.html:110 +msgid "Duration" +msgstr "Dauer" + +#: data/templates/build-detail.html:118 +#: data/templates/file-detail.html:115 +msgid "Files" +msgstr "Dateien" + +#: data/templates/build-detail.html:122 +#: data/templates/package-detail.html:95 +#: data/templates/distro-detail.html:18 +#: data/templates/user-profile.html:39 +#: data/templates/repository-detail.html:24 +#: data/templates/log.html:4 +#: data/templates/base.html:51 +msgid "Log" +msgstr "Log" + +#: data/templates/build-detail.html:127 +#: data/templates/file-detail.html:120 +#: data/templates/package-detail.html:100 +#: data/templates/user-list.html:26 +#: data/templates/build-list.html:11 +#: data/templates/builder-list.html:24 +#: data/templates/build-filter.html:51 +#: data/templates/user-comments.html:10 +#: data/templates/user-profile.html:47 +#: data/templates/distro-list.html:17 +#: data/templates/builder-detail.html:75 +msgid "Actions" +msgstr "Aktionen" + +#: data/templates/build-detail.html:130 +msgid "Re-submit build" +msgstr "Build neustarten" + +#: data/templates/build-detail.html:131 +msgid "Mark as permanently failed" +msgstr "Als defekt markieren" + +#: data/templates/build-detail.html:135 +msgid "Schedule test build" +msgstr "Test-Build planen" + +#: data/templates/build-detail.html:138 +msgid "Modify priority" +msgstr "Priorität ändern" + +#: data/templates/file-detail.html:5 +msgid "File" +msgstr "Datei" + +#: data/templates/file-detail.html:22 +msgid "Description" +msgstr "Beschreibung" + +#: data/templates/file-detail.html:30 +#: data/templates/package-detail.html:13 +msgid "URL" +msgstr "URL" + +#: data/templates/file-detail.html:38 +#: data/templates/package-detail.html:21 +msgid "License" +msgstr "Lizenz" + +#: data/templates/file-detail.html:45 +#: data/templates/package-detail.html:28 +msgid "Maintainer" +msgstr "Maintainer" + +#: data/templates/file-detail.html:52 +msgid "Size" +msgstr "Größe" + +#: data/templates/file-detail.html:58 +msgid "Hash" +msgstr "Prüfsumme" + +#: data/templates/file-detail.html:65 +msgid "Provides" +msgstr "bietet" + +#: data/templates/file-detail.html:73 +msgid "Requires" +msgstr "benötigt" + +#: data/templates/file-detail.html:81 +msgid "Obsoletes" +msgstr "technisch überholt" + +#: data/templates/file-detail.html:89 +msgid "Conflicts" +msgstr "Konflikte" + +#: data/templates/file-detail.html:95 +msgid "Build information" +msgstr "Build-Informationen" + +#: data/templates/file-detail.html:122 +msgid "Download file" +msgstr "Datei herunterladen" + +#: data/templates/source-detail.html:4 +msgid "Source" +msgstr "Source" + +#: data/templates/source-detail.html:9 +msgid "Revision" +msgstr "Revision" + +#: data/templates/source-detail.html:15 +msgid "Branch" +msgstr "Branch" + +#: data/templates/source-detail.html:23 +#: data/templates/index.html:16 +msgid "Latest builds" +msgstr "Letzte Builds" + +#: data/templates/package-detail-list.html:12 +#, python-format +msgid "There is one version of %(pkg)s." +msgid_plural "There are %(num)s different versions of %(pkg)s." +msgstr[0] "Es existiert eine Version von %(pkg)s." +msgstr[1] "Es existieren %(num)s verschiedene Versionen von %(pkg)s." + +#: data/templates/package-detail.html:35 +msgid "Comments" +msgstr "Kommentare" + +#: data/templates/package-detail.html:37 +#, python-format +msgid "This package got a total credit count of %s credits." +msgstr "Dieses Paket hat insgesamt %s credits." + +#: data/templates/package-detail.html:42 +#: data/templates/package-detail.html:55 +msgid "Add comment" +msgstr "Kommentieren" + +#: data/templates/package-detail.html:67 +msgid "Vote" +msgstr "Bewertung" + +#: data/templates/package-detail.html:68 +msgid "None" +msgstr "Keine" + +#: data/templates/package-detail.html:69 +msgid "Approve" +msgstr "akzeptieren" + +#: data/templates/package-detail.html:70 +msgid "Disapprove" +msgstr "ablehnen" + +#: data/templates/package-detail.html:81 +msgid "You must be logged in to comment." +msgstr "Sie müssen eingeloggt sein um zu kommentieren." + +#: data/templates/package-detail.html:86 +msgid "Package files" +msgstr "Paketdateien" + +#: data/templates/package-detail.html:91 +#: data/templates/base.html:40 +msgid "Build jobs" +msgstr "Build-Jobs" + +#: data/templates/package-detail.html:102 +msgid "Package is broken" +msgstr "Das Paket ist defekt" + +#: data/templates/user-list.html:4 +#: data/templates/user-list.html:20 +#: data/templates/docs-users.html:6 +#: data/templates/docs-users.html:30 +#: data/templates/docs-index.html:14 +#: data/templates/docs-index.html:20 +#: data/templates/base.html:47 +#: data/templates/docs-base.html:12 +msgid "Users" +msgstr "Benutzer" + +#: data/templates/user-list.html:6 +msgid "On this page you can see a list of all users that are known to the system." +msgstr "Auf dieser Seite sehen Sie einer Übersicht aller bekannten Benutzer." + +#: data/templates/user-list.html:10 +msgid "Administrators" +msgstr "Administratoren" + +#: data/templates/user-list.html:15 +#: data/templates/docs-users.html:20 +msgid "Testers" +msgstr "Tester" + +#: data/templates/user-list.html:28 +msgid "Latest comments" +msgstr "Letzte Kommentare" + +#: data/templates/package-list.html:3 +#: data/templates/package-list.html:6 +msgid "Package list" +msgstr "Paketliste" + +#: data/templates/package-list.html:8 +msgid "This is an alphabetically ordered list of all packages in the distribution." +msgstr "Hier sehen Sie eine alphabetisch sortierte Liste aller Packete der Distribution." + +#: data/templates/package-list.html:9 +msgid "Click on a link to see further information about the package." +msgstr "Klicken Sie auf den Link um mehr Details über das Paket zu sehen." + +#: data/templates/package-list.html:13 +msgid "Quick selection:" +msgstr "Schnellauswahl:" + +#: data/templates/user-profile-need-activation.html:4 +msgid "Edit successful" +msgstr "Bearbeitung erfolgreich" + +#: data/templates/user-profile-need-activation.html:6 +msgid "The user profile was successfully altered." +msgstr "Das Benutzerprofil wurde erfolgreich geändert." + +#: data/templates/user-profile-need-activation.html:7 +msgid "But as you have changed the email address, you need to re-activate the account." +msgstr "Durch die Änderung der E-Mail-Adresse muss der Benutzeraccount neu aktiviert werden." + +#: data/templates/user-profile-need-activation.html:8 +msgid "Have a look at you mailbox - you already do know what to do." +msgstr "Prüfen Sie Ihre Mailbox - Sie wissen ja wie's geht." + +#: data/templates/distro-detail.html:4 +msgid "Distribution" +msgstr "Distrubution" + +#: data/templates/distro-detail.html:9 +#, python-format +msgid "This distribution is available for %s and maintained by %s." +msgstr "Diese Distribution ist verfügbar für %s und wird gepflegt von %s." + +#: data/templates/distro-detail.html:12 +msgid "Binary repositories" +msgstr "Binärrepositories" + +#: data/templates/distro-detail.html:15 +msgid "Sources" +msgstr "Sourcen" + +#: data/templates/index.html:3 +msgid "Welcome to the Pakfire Build Service" +msgstr "Willkommen beim Pakfire Build Service" + +#: data/templates/index.html:6 +msgid "Summary" +msgstr "Zusammenfassung" + +#: data/templates/index.html:8 +msgid "Welcome to the Pakfire Build Server." +msgstr "Willkommen beim Pakfire Build Server." + +#: data/templates/index.html:12 +msgid "Ongoing builds" +msgstr "Laufende Builds" + +#: data/templates/index.html:19 +msgid "Statistics" +msgstr "Statistiken" + +#: data/templates/index.html:22 +#, python-format +msgid "There is currently one pending build job." +msgid_plural "There are currently %(num)s pending build jobs." +msgstr[0] "Ein ausstehender Build Job." +msgstr[1] "%(num)s ausstehende Build Jobs." + +#: data/templates/index.html:25 +#, python-format +msgid "Average build time is %.1f minutes." +msgstr "Die durchschnittliche Buildzeit beträgt %.1f Minuten." + +#: data/templates/build-priority.html:3 +#: data/templates/build-priority.html:6 +msgid "Edit build priority" +msgstr "Build Priorität ändern" + +#: data/templates/build-priority.html:22 +msgid "Set the priority of the build process." +msgstr "Die Priorität des Build Prozesses einstellen." + +#: data/templates/build-priority.html:28 +msgid "Beware" +msgstr "Vorsicht" + +#: data/templates/build-priority.html:29 +msgid "Shuffeling build jobs can cause problems with the dependency solving." +msgstr "Eine Veränderung der Reihenfolge kann Probleme mit eventuellen Abhängigkeiten bewirken." + +#: data/templates/build-priority.html:30 +msgid "Don't do this if you are not totally sure you won't break anything." +msgstr "Verändern Sie nichts, sofern Sie sich nicht absolut im Klaren sind was Sie tun." + +#: data/templates/build-list.html:3 +#: data/templates/build-list.html:6 +#: data/templates/build-filter.html:3 +msgid "Build job list" +msgstr "Build-Job-Liste" + +#: data/templates/build-list.html:13 +#: data/templates/build-filter.html:6 +msgid "Filter builds" +msgstr "Builds filtern" + +#: data/templates/user-delete.html:4 +#, python-format +msgid "Delete user %s" +msgstr "User %s löschen" + +#: data/templates/user-delete.html:8 +msgid "Do you really want to delete your own account?" +msgstr "Möchten Sie wirklich Ihren eigenen Account löschen?" + +#: data/templates/user-delete.html:9 +msgid "You won't be able to login and use this service any more." +msgstr "Sie werden sich nicht mehr einloggen oder den Dienst verwenden können." + +#: data/templates/user-delete.html:11 +#, python-format +msgid "Do you really want to delete the user %s?" +msgstr "Möchten Sie den Benutzer %s wirklich löschen?" + +#: data/templates/user-delete.html:16 +#: data/templates/builder-delete.html:13 +#, python-format +msgid "Delete %s" +msgstr "%s löschen" + +#: data/templates/user-delete.html:18 +#: data/templates/builder-delete.html:14 +msgid "Back" +msgstr "Zurück" + +#: data/templates/logout.html:4 +msgid "Logout successful" +msgstr "Logout erfolgreich" + +#: data/templates/logout.html:7 +msgid "You have successfully logged out from the Pakfire Build Server." +msgstr "Sie haben sich erfolgreich vom Pakfire Build Server ausgeloggt." + +#: data/templates/logout.html:8 +msgid "Have a nice day!" +msgstr "Einen schönen Tag noch!" + +#: data/templates/logout.html:12 +#: data/templates/login-successful.html:14 +msgid "Go on" +msgstr "Weiter" + +#: data/templates/register-fail.html:4 +msgid "Registration failed" +msgstr "Registrierung fehlgeschlagen" + +#: data/templates/register-fail.html:6 +#: data/templates/register-activation-fail.html:6 +#: data/templates/user-profile-edit-fail.html:6 +msgid "We are sorry." +msgstr "Es tut uns Leid." + +#: data/templates/register-fail.html:7 +msgid "We could not create your requested account." +msgstr "Der gewünschte Account konnte nicht erstellt werden." + +#: data/templates/register-fail.html:17 +#: data/templates/user-profile-edit-fail.html:17 +msgid "Use the back button on your web browser to go back to the previous page and correct your submission." +msgstr "Bitte verwenden Sie die \"Zurück\" Funktion Ihres Browsers und überprüfen Sie Ihre Eingaben." + +#: data/templates/builder-list.html:3 +#: data/templates/builder-list.html:6 +#: data/templates/base.html:43 +msgid "Build servers" +msgstr "Buildserver" + +#: data/templates/builder-list.html:9 +msgid "Builders are those, that do all the hard work." +msgstr "Die Builder verrichten all die harte Arbeit." + +#: data/templates/builder-list.html:10 +msgid "Build jobs are scheduled to these hosts that they process and send back the result." +msgstr "Build Aufträge werden einzelnen Hosts zugewiesen, diese werden abgearbeitet und das Resultat zurück gesendet." + +#: data/templates/builder-list.html:26 +#: data/templates/builder-new.html:3 +msgid "Create new builder" +msgstr "Neuen Builder anlegen" + +#: data/templates/register-activation-fail.html:4 +msgid "Activation failed" +msgstr "Aktivierung fehlgeschlagen" + +#: data/templates/register-activation-fail.html:7 +msgid "The activation of your account has failed." +msgstr "Die Aktivierung Ihres Accounts schlug fehl." + +#: data/templates/register-activation-fail.html:8 +msgid "Possibly the registration code is wrong or your registration timed out." +msgstr "Möglicherweise ist der Registrierungscode falsch oder abgelaufen." + +#: data/templates/docs-users.html:3 +#: data/templates/docs-index.html:3 +#: data/templates/docs-build.html:3 +#: data/templates/docs-build.html:6 +msgid "Legend of the build states" +msgstr "Legende der Buildstatus" + +#: data/templates/docs-users.html:8 +msgid "All users can join the Pakfire Build Service and are separated into three groups:" +msgstr "Jeder kann sich am Pakfire Build Service anmelden und wird einer der folgenden drei Gruppen zugewiesen:" + +#: data/templates/docs-users.html:11 +msgid "Developers" +msgstr "Entwickler" + +#: data/templates/docs-users.html:13 +msgid "Developers manage this build service and have access to all parts of it." +msgstr "Die Entwickler verwalten den Build Service und haben Zugriff auf all seine Bereichen." + +#: data/templates/docs-users.html:14 +msgid "They are responsible to keep the system running and able to push package updates to the repostories." +msgstr "Sie sind verantwortlich für ein korrektes Arbeiten des Systems und dürfen Paket Updates in den einzelnen Repositorien veröffentlichen." + +#: data/templates/docs-users.html:17 +msgid "Guidelines for developers" +msgstr "Leitfaden für Entwickler" + +#: data/templates/docs-users.html:22 +msgid "Testers are like users but have the right to vote on packages, which is used to figure out the quality of the package." +msgstr "Tester werden wie Benutzer behandelt, haben jedoch die Berechtigung für Pakete abzustimmen um deren Qualität zu bestimmen." + +#: data/templates/docs-users.html:23 +msgid "Everyone can become a tester after he or she has proven to know the IPFire system very well." +msgstr "Um Tester zu werden, sollte man sich mit IPFire gut auskennen. Dies ist für jeden nach einer kurzen Überprüfung möglich. " + +#: data/templates/docs-users.html:24 +msgid "On these people depends a very huge amount of the quality of the distribution that is made out of the feedback they give." +msgstr "Diese Personen tragen durch ihr Feedback eine große Mitverantwortung an der Qualität der Distribution." + +#: data/templates/docs-users.html:27 +msgid "Guidelines for testers" +msgstr "Leitfaden für Tester" + +#: data/templates/docs-users.html:32 +msgid "Everybody can join the Pakfire Build Service by registering an account." +msgstr "Jeder kann sich am Pakfire Build Service nach einer Registrierung anmelden." + +#: data/templates/docs-users.html:33 +msgid "After a successful activation you are able to leave comments on packages and give feedback to the developers about its status." +msgstr "Nach einer erfolgten Aktivierung sollten Sie in der Lage sein, Pakete zu bewerten bzw. Kommentare abzugeben, um die Entwickler über den Paket Status zu informieren." + +#: data/templates/docs-users.html:38 +#: data/templates/base.html:22 +msgid "Register" +msgstr "Registrieren" + +#: data/templates/register-activation-success.html:4 +msgid "Activation successful" +msgstr "Aktivierung erfolgreich" + +#: data/templates/register-activation-success.html:6 +#, python-format +msgid "Your account has been activated, %s." +msgstr "Ihr Account wurde aktiviert, %s." + +#: data/templates/register-activation-success.html:7 +msgid "Have fun!" +msgstr "Viel Spaß!" + +#: data/templates/builder-new.html:6 +msgid "Create a new builder" +msgstr "Einen neuen Builder anlegen" + +#: data/templates/builder-new.html:12 +#: data/templates/builder-edit.html:12 +#: data/templates/register.html:10 +msgid "Name" +msgstr "Name" + +#: data/templates/builder-new.html:17 +msgid "Must be the canonical hostname of the machine." +msgstr "Der kanonische Hostname des Systems." + +#: data/templates/build-filter.html:13 +msgid "All" +msgstr "Alle" + +#: data/templates/build-filter.html:14 +msgid "Running" +msgstr "Laufend" + +#: data/templates/build-filter.html:15 +msgid "Pending" +msgstr "In der Warteschlange" + +#: data/templates/build-filter.html:16 +msgid "Finished" +msgstr "Beendet" + +#: data/templates/build-filter.html:17 +msgid "Failed" +msgstr "Fehlgeschlgen" + +#: data/templates/build-filter.html:18 +#, fuzzy +msgid "Permanently failed" +msgstr "Als defekt markieren" + +#: data/templates/build-filter.html:19 +msgid "Dispatching" +msgstr "Vorbereitung" + +#: data/templates/build-filter.html:20 +msgid "Uploading" +msgstr "Hochladen" + +#: data/templates/build-filter.html:24 +msgid "Only show builds with given state." +msgstr "Nur Builds mit diesem Status anzeigen." + +#: data/templates/build-filter.html:28 +msgid "Build host" +msgstr "Buildhost" + +#: data/templates/build-filter.html:31 +msgid "Any" +msgstr "Alle" + +#: data/templates/build-filter.html:38 +msgid "Display only builds by selected host." +msgstr "Nur Builds vom ausgewähltem Host anzeigen." + +#: data/templates/build-filter.html:53 +msgid "Show all builds" +msgstr "Alle Builds anzeigen" + +#: data/templates/docs-index.html:6 +msgid "Documents" +msgstr "Dokumente" + +#: data/templates/docs-index.html:8 +msgid "This is a collection of documents that should be read by everybody who is using this system." +msgstr "Hier handelt es sich um eine Auswahl von Dokumenten, die von jedem der dieses System verwendet auch gelesen werden sollten." + +#: data/templates/docs-index.html:9 +msgid "To make this easy for you, the documents are grouped into two parts." +msgstr "Um Ihnen das Lesen zu erleichtern, wurden die Dokumente in zwei Gruppen eingeteilt." + +#: data/templates/docs-index.html:12 +msgid "Documents for testers" +msgstr "Dokumente für Tester" + +#: data/templates/docs-index.html:17 +msgid "Documents for developers" +msgstr "Dokumente für Entwickler" + +#: data/templates/docs-index.html:19 +#: data/templates/docs-base.html:11 +msgid "Builds" +msgstr "Builds" + +#: data/templates/docs-index.html:24 +msgid "Technical documentation is available on the wiki:" +msgstr "Technische Dokumentation findet sich im Wiki:" + +#: data/templates/docs-index.html:25 +msgid "Technical documentation" +msgstr "Technische Dokumentation" + +#: data/templates/builder-edit.html:3 +#: data/templates/builder-edit.html:6 +#, python-format +msgid "Edit builder %s" +msgstr "Builder %s bearbeiten" + +#: data/templates/builder-edit.html:17 +msgid "The hostname cannot be changed." +msgstr "Der Hostname kann nicht geändert werden." + +#: data/templates/builder-edit.html:21 +msgid "Enabled" +msgstr "Aktiviert" + +#: data/templates/builder-edit.html:26 +msgid "The builder must be enabled in order to process build jobs." +msgstr "Der Buildserver muss aktiviert sein um Buildjobs zu erhalten." + +#: data/templates/builder-edit.html:32 +msgid "Build job settings" +msgstr "Buildjob-Einstellungen" + +#: data/templates/builder-edit.html:34 +msgid "These settings do only take effect if the builder is enabled." +msgstr "Diese Einstellungen haben nur eine Auswirkung wenn der Server aktiviert ist." + +#: data/templates/builder-edit.html:38 +msgid "Authorized to build source packages" +msgstr "Autorisiert um Sourcepakete zu bauen." + +#: data/templates/builder-edit.html:43 +msgid "Only a few build servers are allowed to build source packages." +msgstr "Nur eine begrenzte Anzahl an Buildserver sind berechtigt Sourcepakete zu bauen." + +#: data/templates/builder-edit.html:47 +msgid "Authorized to build binary packages" +msgstr "Autorisiert um Binärpakete zu bauen." + +#: data/templates/builder-edit.html:54 +msgid "Authorized to build test packages" +msgstr "Autorisiert um Testpakete zu bauen." + +#: data/templates/builder-delete.html:3 +#, python-format +msgid "Delete builder %s" +msgstr "Builder %s löschen" + +#: data/templates/builder-delete.html:6 +#: data/templates/builder-detail.html:3 +#: data/templates/builder-detail.html:6 +#: data/templates/builder-pass.html:4 +msgid "Builder" +msgstr "Builder" + +#: data/templates/builder-delete.html:9 +#, python-format +msgid "You are going to delete the build host %s." +msgstr "Sie sind im Begriff den build host %s zu löschen." + +#: data/templates/user-comments.html:4 +msgid "Latest user comments" +msgstr "Letzte Kommentare" + +#: data/templates/user-comments.html:12 +msgid "Show all users" +msgstr "Alle Benutzer anzeigen" + +#: data/templates/user-profile.html:5 +#: data/templates/user-profile.html:26 +#: data/templates/user-profile-edit.html:74 +msgid "User" +msgstr "Benutzer" + +#: data/templates/user-profile.html:9 +#: data/templates/login.html:22 +#: data/templates/user-profile-edit.html:10 +msgid "Username" +msgstr "Benutzername" + +#: data/templates/user-profile.html:13 +#: data/templates/register.html:19 +#: data/templates/user-profile-edit.html:15 +msgid "Email" +msgstr "Email" + +#: data/templates/user-profile.html:22 +#: data/templates/user-profile-edit.html:81 +msgid "Admin" +msgstr "Admin" + +#: data/templates/user-profile.html:24 +#: data/templates/user-profile-edit.html:77 +msgid "Tester" +msgstr "Tester" + +#: data/templates/user-profile.html:32 +msgid "Registered" +msgstr "Registriert" + +#: data/templates/user-profile.html:42 +#, python-format +msgid "Comments written by %s" +msgstr "Kommentare von %s" + +#: data/templates/user-profile.html:51 +msgid "Account settings" +msgstr "Accounteinstellungen" + +#: data/templates/user-profile.html:54 +msgid "Delete account" +msgstr "Account löschen" + +#: data/templates/repository-detail.html:5 +msgid "Repository" +msgstr "Repositorium" + +#: data/templates/repository-detail.html:6 +msgid "from" +msgstr "von" + +#: data/templates/repository-detail.html:12 +#, python-format +msgid "This repository contains %s packages and is available for %s." +msgstr "Das Repositurium enthält %s Pakete und ist verfügbar für %s." + +#: data/templates/repository-detail.html:15 +msgid "Waiting packages" +msgstr "Packete in der Warteschlange" + +#: data/templates/repository-detail.html:17 +msgid "These packages are waiting to be published." +msgstr "Pakete die noch nicht veröffentlicht wurden." + +#: data/templates/repository-detail.html:21 +msgid "Pushed packages" +msgstr "Veröffentlichte Pakete" + +#: data/templates/register-success.html:4 +msgid "Registration successful" +msgstr "Registrierung erfolgreich" + +#: data/templates/register-success.html:6 +#, python-format +msgid "Your new account has been created, %s." +msgstr "Ihre Account wurde erstellt, %s." + +#: data/templates/register-success.html:7 +msgid "To complete the activation, follow the instructions that were sent to you in an activation email." +msgstr "Um die Aktivierung abzuschließen, folgen Sie bitte den Instruktionen der E-Mail." + +#: data/templates/login.html:4 +#: data/templates/base.html:21 +msgid "Login" +msgstr "Login" + +#: data/templates/login.html:8 +msgid "Username and/or password was wrong. Login failed." +msgstr "Falscher Benutzername und/oder Passwort. Login fehlgeschlagen." + +#: data/templates/login.html:13 +msgid "Please type your username and your password to the form to log in." +msgstr "Bitte tippen Sie ihren Benutzernamen und Ihr Passwort in die vorgesehen Felder." + +#: data/templates/login.html:14 +msgid "If you have no account, yet you can create a new one." +msgstr "Falls Sie noch keinen Account besitzen, können Sie einen erstellen." + +#: data/templates/login.html:15 +msgid "Register a new account." +msgstr "Einen neuen Account erstellen." + +#: data/templates/login.html:26 +#: data/templates/register.html:41 +#: data/templates/user-profile-edit.html:33 +msgid "Password" +msgstr "Passwort" + +#: data/templates/register.html:4 +msgid "Register new account" +msgstr "Einen neuen Account erstellen." + +#: data/templates/register.html:15 +msgid "Must be a unique name you login with." +msgstr "Einmaliger Name für den Login." + +#: data/templates/register.html:24 +msgid "Type your email address." +msgstr "Ihre E-Mail-Adresse." + +#: data/templates/register.html:28 +#: data/templates/user-profile-edit.html:24 +msgid "Real name (optional)" +msgstr "Wahrer Name (optional)" + +#: data/templates/register.html:33 +msgid "Type you firstname and your lastname here." +msgstr "Tippen Sie hier Ihren Vor- und Nachnamen ein." + +#: data/templates/register.html:38 +msgid "Account security" +msgstr "Account-Sicherheit" + +#: data/templates/register.html:46 +#: data/templates/user-profile-edit.html:38 +msgid "The password is used to secure the login and must be at least 8 characters." +msgstr "Das Passwort wird für einen sicheren Login benötigt und muss 8 Zeichen lang sein." + +#: data/templates/register.html:50 +#: data/templates/user-profile-edit.html:42 +msgid "Confirm" +msgstr "Bestätigen" + +#: data/templates/login-successful.html:4 +msgid "Login successful" +msgstr "Login erfolgreich" + +#: data/templates/login-successful.html:6 +#, python-format +msgid "Welcome, %s." +msgstr "Willkommen, %s." + +#: data/templates/login-successful.html:10 +msgid "Your login to the Pakfire Build Server was successful." +msgstr "Ihr Login am Pakfire Build Server war erfolgreich." + +#: data/templates/user-profile-edit-fail.html:4 +msgid "Edit failed" +msgstr "Bearbeitung fehlgeschlagen" + +#: data/templates/user-profile-edit-fail.html:7 +msgid "The user profile cannot be saved." +msgstr "Das Benutzerprofil kann nicht gespeichert werden." + +#: data/templates/modules/files-table.html:8 +msgid "Info" +msgstr "Info" + +#: data/templates/modules/files-table.html:9 +msgid "Download" +msgstr "Herunterladen" + +#: data/templates/modules/comments-table.html:8 +#, python-format +msgid "on %s" +msgstr "zu %s" + +#: data/templates/modules/comments-table.html:11 +#, python-format +msgid "by %s" +msgstr "von %s" + +#: data/templates/modules/comments-table.html:18 +msgid "No comments so far." +msgstr "Bisher keine Kommentare." + +#: data/templates/modules/log-table.html:23 +msgid "No log entries, yet." +msgstr "Noch keine Logeinträge." + +#: data/templates/source-list.html:3 +msgid "Sources repositories" +msgstr "Quellrepositorien" + +#: data/templates/source-list.html:6 +msgid "Source repositories" +msgstr "Quell-Repositories" + +#: data/templates/source-list.html:17 +msgid "Add source repository" +msgstr "Quellrepository hinzufügen" + +#: data/templates/source-list.html:18 +msgid "Blah 123" +msgstr "Blah 123" + +#: data/templates/distro-list.html:3 +#: data/templates/distro-list.html:6 +#: data/templates/base.html:37 +msgid "Distributions" +msgstr "Distributionen" + +#: data/templates/distro-list.html:19 +msgid "Add distribution" +msgstr "Distribution hinzufügen" + +#: data/templates/user-profile-edit.html:4 +#, python-format +msgid "Edit user %s" +msgstr "Benutzer %s bearbeiten" + +#: data/templates/user-profile-edit.html:12 +msgid "Cannot be changed." +msgstr "Kann nicht geändert werden." + +#: data/templates/user-profile-edit.html:20 +msgid "If the email address is changed, your account will be disabled until you reconfirm the new email address." +msgstr "Nach einer Änderung der E-Mail-Adresse, wird der Account temporär deaktiviert, bis die neue Adresse bestätigt wurde." + +#: data/templates/user-profile-edit.html:29 +msgid "Your real name is used to identify you by others." +msgstr "" + +#: data/templates/user-profile-edit.html:47 +msgid "Leave the password fields empty to keep the current password." +msgstr "Lassen Sie die Passwort Felder leer um das aktuelle Passwort zu behalten." + +#: data/templates/user-profile-edit.html:51 +msgid "Preferred language" +msgstr "Bevorzugte Sprache" + +#: data/templates/user-profile-edit.html:54 +msgid "Auto-detect" +msgstr "Automatisch erkennen" + +#: data/templates/user-profile-edit.html:62 +msgid "Auto-detect will use the language transmitted by your browser." +msgstr "\"Auto-detect\" verwendet die vom Browser übertragene Spracheinstellung." + +#: data/templates/user-profile-edit.html:68 +msgid "Admin actions" +msgstr "Administrator-Aktionen" + +#: data/templates/user-profile-edit.html:86 +msgid "Define the permissions of the user." +msgstr "Rechte des Users festlegen." + +#: data/templates/base.html:4 +msgid "No title given" +msgstr "Kein Titel angegeben" + +#: data/templates/base.html:19 +msgid "Logout" +msgstr "Ausloggen" + +#: data/templates/base.html:26 +#, python-format +msgid "A service by the %s." +msgstr "Ein Service von %s." + +#: data/templates/base.html:31 +msgid "Index" +msgstr "Startseite" + +#: data/templates/base.html:34 +msgid "Packages" +msgstr "Pakete" + +#: data/templates/base.html:84 +msgid "About Pakfire" +msgstr "Über Pakfire" + +#: data/templates/base.html:86 +msgid "Pakfire is the buildsystem that is used to build the IPFire Linux firewall distribution." +msgstr "Pakfire ist das Buildsystem um die Linux Firewall Distribution IPFire zu bauen." + +#: data/templates/base.html:87 +msgid "It also installs and updates packages on the IPFire systems." +msgstr "Es wird auch für die Installation und zur Aktualisierung von Paketen auf IPFire Systemen verwendet." + +#: data/templates/base.html:91 +msgid "Documentation" +msgstr "Dokumentation" + +#: data/templates/base.html:94 +msgid "Documentation index" +msgstr "Dokumentationsübersicht" + +#: data/templates/base.html:100 +msgid "All rights reserved." +msgstr "Alle Rechte vorbehalten." + +#: data/templates/docs-build.html:8 +msgid "Every build that is done by the Pakfire Build Service has to go through several states:" +msgstr "Jeder einzelne Build des Pakfire Build Services durchläuft mehrere Einzelschritte:" + +#: data/templates/docs-build.html:11 +msgid "After checking out the source from the source repository a source package is created and submitted to the build server." +msgstr "Nach dem Dowload der letzen Änderungen und des Source Codes, wird ein Sourcepaket generiert und an den Pakfire Build Server übertragen." + +#: data/templates/docs-build.html:12 +msgid "Starting from inserting a source file to the build service, there are binary build jobs created for every supported architecture." +msgstr "Nach dem Erhalt der Sorucepakete, erstellt der Buildservice einzelne Build Aufträge für die unterstützten Architekturen." + +#: data/templates/docs-build.html:15 +msgid "These get assigned to a build host which has to compile or assemble the package and return it back to the build server." +msgstr "Diese werden auf die einzelnen Buildhosts verteilt, welche den Code compilieren und das fertige Paket an den Pakfire Build Server zurückschicken." + +#: data/templates/docs-build.html:16 +msgid "In the table below, there are all states that a build job goes through:" +msgstr "In der folgenden Tabelle ist der jeweilige Status, den der Build durchläuft, aufgeführt:" + +#: data/templates/docs-build.html:19 +msgid "Build is running" +msgstr "Build läuft" + +#: data/templates/docs-build.html:20 +msgid "Build has failed" +msgstr "Build ist fehlgeschlagen" + +#: data/templates/docs-build.html:21 +msgid "Build is waiting to be processed" +msgstr "Der Build wartet darauf bearbeitet zu werden" + +#: data/templates/docs-build.html:22 +msgid "There was a dependency error when the package was built" +msgstr "Es gab einen Fehler bei der Auflösung der Abhängigkeiten." + +#: data/templates/docs-build.html:23 +msgid "Build is waiting for source to go to pending state" +msgstr "Der Build wartet auf den Quellcode um in den nächsten Status überzugehen" + +#: data/templates/docs-build.html:24 +msgid "Files of this build are transferred to the build server" +msgstr "Dateien des Builds wurden hochgeladen" + +#: data/templates/docs-build.html:25 +msgid "Files are being uploaded to the service" +msgstr "Dateien werden gerade hochgeladen" + +#: data/templates/docs-build.html:26 +msgid "Build has an unknown state" +msgstr "Der Build hat einen unbekannten Status" + +#: data/templates/builder-detail.html:10 +msgid "Status" +msgstr "Status" + +#: data/templates/builder-detail.html:16 +msgid "Load average" +msgstr "Durchschnittliche Last" + +#: data/templates/builder-detail.html:24 +msgid "Supported architectures" +msgstr "Unterstützte Architekturen" + +#: data/templates/builder-detail.html:26 +#: data/templates/builder-detail.html:59 +msgid "Unknown" +msgstr "Unbekannt" + +#: data/templates/builder-detail.html:32 +msgid "Configuration" +msgstr "Konfiguration" + +#: data/templates/builder-detail.html:35 +msgid "Builds source packages" +msgstr "Baut Sourcepakete" + +#: data/templates/builder-detail.html:37 +#: data/templates/builder-detail.html:43 +#: data/templates/builder-detail.html:49 +msgid "Yes" +msgstr "Ja" + +#: data/templates/builder-detail.html:37 +#: data/templates/builder-detail.html:43 +#: data/templates/builder-detail.html:49 +msgid "No" +msgstr "Nein" + +#: data/templates/builder-detail.html:41 +msgid "Builds binary packages" +msgstr "Baut Binärpakete" + +#: data/templates/builder-detail.html:47 +msgid "Runs tests" +msgstr "Bearbeitet Tests" + +#: data/templates/builder-detail.html:55 +msgid "Host information" +msgstr "Host-Informationen" + +#: data/templates/builder-detail.html:58 +msgid "CPU model" +msgstr "CPU-Modell" + +#: data/templates/builder-detail.html:62 +msgid "Memory" +msgstr "Arbeitsspeicher" + +#: data/templates/builder-detail.html:69 +msgid "Currently running builds on this host" +msgstr "Aktuell laufende Jobs auf diesem Host" + +#: data/templates/builder-detail.html:78 +msgid "Show all build jobs" +msgstr "Alle Buildjobs anzeigen" + +#: data/templates/builder-detail.html:82 +msgid "Edit builder" +msgstr "Builder bearbeiten" + +#: data/templates/builder-detail.html:86 +msgid "Renew passphrase" +msgstr "Passphrase erneuern" + +#: data/templates/builder-detail.html:90 +msgid "Delete builder" +msgstr "Builder löschen" + +#: data/templates/builder-pass.html:8 +#, python-format +msgid "The new host %s has been successfully created." +msgstr "Der neue Host %s wurde erfolgreich erstellt." + +#: data/templates/builder-pass.html:10 +#, python-format +msgid "The passphrase for %s has been regenerated." +msgstr "Die Passphrase für %s wurde erneut generiert." + +#: data/templates/builder-pass.html:13 +msgid "For authorization to the Pakfire Master Server there is a passphrase required which must be configured to the host." +msgstr "Eine erforderliche Passphrase für das System, um sich am Pakfire Master Server anzumelden." + +#: data/templates/builder-pass.html:14 +msgid "This passphrase is:" +msgstr "Die Passphrase lautet:" + +#: data/templates/builder-pass.html:20 +msgid "Next" +msgstr "Weiter" + +#: data/templates/build-schedule.html:6 +#, python-format +msgid "Schedule test build for %s" +msgstr "Einen Testbuild für %s planen" + +#: data/templates/build-schedule.html:8 +msgid "A test build is used to check if a package builds with the current package set." +msgstr "Testbuilds dienen um festzustellen, ob ein Paket mit den aktuellen Einstellungen korrekt baut." + +#: data/templates/build-schedule.html:9 +msgid "In this way, developers are able to find quality issues fast and without actively searching for them." +msgstr "Mit dieser Methode sind die Entwickler in der Lage Qualitätsmängel schnell und ohne aufwändiger Suche zu finden." + +#: data/templates/build-schedule.html:12 +msgid "As this build platform only has a limited amount of performance, test builds only have a very less priority." +msgstr "Da die verfügbaren Ressourcen dieser Plattform begrenzt sind, werden Testbuilds mit einer geringeren Priorität eingereiht." + +#: data/templates/build-schedule.html:13 +msgid "However, you can manually request to run a test." +msgstr "Es ist auch möglich, manuell einen Test durchführen zu lassen." + +#: data/templates/build-schedule.html:16 +msgid "The build job will be started when a build slot is available but not before the given time." +msgstr "Der Buildjob wird gestartet sobald ein Server zur Verfügung steht, jedoch niemals vor der angegebenen Zeit." + +#: data/templates/build-schedule.html:23 +msgid "Start time" +msgstr "Startzeit" + +#: data/templates/build-schedule.html:26 +msgid "As soon as possible" +msgstr "Sobald als möglich" + +#: data/templates/build-schedule.html:27 +msgid "After 5 minutes" +msgstr "In 5 Minuten" + +#: data/templates/build-schedule.html:28 +msgid "After 15 minutes" +msgstr "In 15 Minuten" + +#: data/templates/build-schedule.html:29 +msgid "After one hour" +msgstr "In einer Stunde" + +#: data/templates/build-schedule.html:30 +msgid "After one day" +msgstr "In einem Tag" + +#: data/templates/build-schedule.html:34 +msgid "Set the time after which the build job starts." +msgstr "Zeitpunkt des Buildjobs einstellen." + +#: data/templates/docs-base.html:5 +msgid "All Documents" +msgstr "Alle Dokumente" + +#: data/templates/docs-base.html:9 +msgid "Topics" +msgstr "Themen" + +#: web/handlers_auth.py:53 +msgid "No username provided." +msgstr "Kein Benutzername angegeben." + +#: web/handlers_auth.py:55 +msgid "The given username is already taken." +msgstr "Der gewählte Benutzername ist bereits belegt." + +#: web/handlers_auth.py:58 +#: web/handlers_users.py:85 +msgid "No email address provided." +msgstr "Keine Email-Adresse angegeben." + +#: web/handlers_auth.py:60 +#: web/handlers_users.py:87 +msgid "Email address is invalid." +msgstr "Die Email-Adresse ist ungültig." + +#: web/handlers_auth.py:62 +msgid "The given email address is already used for another account." +msgstr "Die eingegebene Email-Adresse wird bereits für einen anderen Account verwendet." + +#: web/handlers_auth.py:66 +msgid "No password provided." +msgstr "Kein Passwort angegeben." + +#: web/handlers_auth.py:68 +#: web/handlers_users.py:91 +msgid "Password has less than 8 characters." +msgstr "Das Passwort hat weniger als 8 Zeichen." + +#: web/handlers_auth.py:70 +#: web/handlers_users.py:93 +msgid "Passwords do not match." +msgstr "Passwörter stimmen nicht überein." + +#: web/handlers_users.py:97 +msgid "The choosen locale is invalid." +msgstr "Die gewählte Sprache ist ungültig." + +#: backend/users.py:207 +msgid "Account Activation" +msgstr "Account aktivieren" + +#: backend/users.py:209 +msgid "You, or somebody using you email address, has registered an account on the Pakfire Build Service." +msgstr "Sie, oder jemand, der Ihre E-Mail-Adresse benutzt, hat einen Account im Pakfire Build Service registriert." + +#: backend/users.py:211 +msgid "To activate your account, please click on the link below." +msgstr "Um Ihren Account zu aktivieren, klicken Sie auf den Link." + +#: backend/constants.py:22 +msgid "Build job created" +msgstr "Buildjob erstellt" + +#: backend/constants.py:23 +msgid "Build job is now pending" +msgstr "Build ist noch ausstehend" + +#: backend/constants.py:24 +msgid "Build job is dispatching" +msgstr "Build wird herruntergeladen" + +#: backend/constants.py:25 +msgid "Build job is running" +msgstr "Build läuft" + +#: backend/constants.py:26 +msgid "Build job has failed" +msgstr "Build ist fehlgeschlagen" + +#: backend/constants.py:27 +msgid "Build job has permanently failed" +msgstr "Build ist defekt." + +#: backend/constants.py:28 +msgid "Build job has dependency errors" +msgstr "Der Buildjob hat Abhängigkeitsprobleme" + +#: backend/constants.py:29 +msgid "Build job is waiting for the source package" +msgstr "Der Build wartet auf den Quellcode" + +#: backend/constants.py:30 +msgid "Build job is finished" +msgstr "Build erfolgreich" + +#: backend/constants.py:31 +msgid "Build job has an unknown state" +msgstr "Build hat einen unbekannten Status" + +#: backend/constants.py:32 +msgid "Build job is uploading" +msgstr "Build wird hochgeladen." + +#: backend/constants.py:37 +#, python-format +msgid "Build job failed: %(build_name)s" +msgstr "Build Job %(build_name)s fehlgeschlagen" + +#: backend/constants.py:38 +#, python-format +msgid "" +"The build job \"%(build_name)s\" has failed.\n" +"\n" +"This could have a couple of reasons and needs to be investigated by you.\n" +"\n" +"Here is more information about the incident:\n" +"\n" +" Build name: %(build_name)s\n" +" Build host: %(build_host)s\n" +"\n" +"Click on this link to get all details about the build:\n" +" http://pakfire.ipfire.org/build/%(build_uuid)s\n" +"\n" +"Sincerely,\n" +" The Pakfire Build Service" +msgstr "" +"Der Build von \"%(build_name)s\" ist fehlgeschlagen.\n" +"\n" +"Dies kann aus mehreren Gründen passiert sein und muss von Ihnen begutachtet werden.\n" +"\n" +"Mehr Informationen zum Vorgang:\n" +"\n" +" Build Name: %(build_name)s\n" +" Build Host: %(build_host)s\n" +"\n" +"Klicken Sie auf den folgenden Link um alle verfügbaren Informationen zum Build zu erhalten:\n" +" http://pakfire.ipfire.org/build/%(build_uuid)s\n" +"\n" +"Mit freundlichen Grüßen,\n" +" The Pakfire Build Service" + +#: backend/constants.py:55 +#, python-format +msgid "Build job finished: %(build_name)s" +msgstr "Build Job %(build_name)s erfolgreich" + +#: backend/constants.py:56 +#, python-format +msgid "" +"The build job \"%(build_name)s\" has finished.\n" +"\n" +"If you are the maintainer, it is up to you to push it to one or more repositories.\n" +"\n" +"Click on this link to get all details about the build:\n" +" http://pakfire.ipfire.org/build/%(build_uuid)s\n" +"\n" +"Sincerely,\n" +" The Pakfire Build Service" +msgstr "" +"Der Build von \"%(build_name)s\" wurde erfolgreich beendet.\n" +"\n" +"Wenn Sie der Maintainer des Paketes sind, obliegt es Ihnen dieses in einem oder mehreren Repositoren zu veröffentlichen.\n" +"\n" +"Klicken Sie auf den folgenden Link um alle verfügbaren Informationen zum Build zu erhalten:\n" +" http://pakfire.ipfire.org/build/%(build_uuid)s\n" +"\n" +"Mit freundlichen Grüßen,\n" +" The Pakfire Build Service" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "January" +msgstr "Januar" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "February" +msgstr "Februar" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "March" +msgstr "März" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "April" +msgstr "April" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "May" +msgstr "Mai" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "June" +msgstr "Juni" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "July" +msgstr "Juli" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "August" +msgstr "August" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "September" +msgstr "September" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "October" +msgstr "Oktober" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "November" +msgstr "November" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "December" +msgstr "Dezember" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Monday" +msgstr "Montag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Tuesday" +msgstr "Dienstag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Wednesday" +msgstr "Mittwoch" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Thursday" +msgstr "Donnerstag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:230 +msgid "Friday" +msgstr "Freitag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:230 +msgid "Saturday" +msgstr "Samstag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:230 +msgid "Sunday" +msgstr "Sonntag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:274 +#, python-format +msgid "1 second ago" +msgid_plural "%(seconds)d seconds ago" +msgstr[0] "vor einer Sekunde" +msgstr[1] "vor %(seconds)d Sekunden" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:279 +#, python-format +msgid "1 minute ago" +msgid_plural "%(minutes)d minutes ago" +msgstr[0] "vor einer Minute" +msgstr[1] "vor %(minutes)d Minuten" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:283 +#, python-format +msgid "1 hour ago" +msgid_plural "%(hours)d hours ago" +msgstr[0] "vor einer Stunde" +msgstr[1] "vor %(hours)d Stunden" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:287 +#, python-format +msgid "%(time)s" +msgstr "%(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:290 +msgid "yesterday" +msgstr "gestern" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:291 +#, python-format +msgid "yesterday at %(time)s" +msgstr "gestern um %(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:293 +#, python-format +msgid "%(weekday)s" +msgstr "%(weekday)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:294 +#, python-format +msgid "%(weekday)s at %(time)s" +msgstr "%(weekday)s um %(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:296 +#: /usr/lib/python2.7/site-packages/tornado/locale.py:338 +#, python-format +msgid "%(month_name)s %(day)s" +msgstr "%(month_name)s %(day)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:297 +#, python-format +msgid "%(month_name)s %(day)s at %(time)s" +msgstr "%(month_name)s %(day)s um %(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:300 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s" +msgstr "%(month_name)s %(day)s, %(year)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:301 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s at %(time)s" +msgstr "%(month_name)s %(day)s, %(year)s um %(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:332 +#, python-format +msgid "%(weekday)s, %(month_name)s %(day)s" +msgstr "%(weekday)s, %(month_name)s %(day)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:353 +#, python-format +msgid "%(commas)s and %(last)s" +msgstr "%(commas)s und %(last)s" + +#, fuzzy +#~ msgid "" +#~ "A build job that was submitted to the Pakfire Build Service has failed." +#~ msgstr "Willkommen beim Pakfire Build Service" + +#~ msgid "Show more builds." +#~ msgstr "Mehr Builds anzeigen." + +#~ msgid "Disable builder" +#~ msgstr "Builder deaktivieren" + +#~ msgid "Information" +#~ msgstr "Informationen" diff --git a/data/translations/de_DE/LC_MESSAGES/pakfire.po.sic b/data/translations/de_DE/LC_MESSAGES/pakfire.po.sic new file mode 100644 index 0000000..631652d --- /dev/null +++ b/data/translations/de_DE/LC_MESSAGES/pakfire.po.sic @@ -0,0 +1,1090 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Pakfire Build Service\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-05-07 16:21+0200\n" +"PO-Revision-Date: 2011-05-07 18:02+0100\n" +"Last-Translator: Stefan Schantl \n" +"Language-Team: German \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Poedit-Language: German\n" +"X-Poedit-Country: GERMANY\n" +"X-Poedit-SourceCharset: utf-8\n" + +#: data/templates/build-detail.html:5 +#: data/templates/build-detail.html:7 +#: data/templates/modules/log-table.html:10 +msgid "Build" +msgstr "Build" + +#: data/templates/build-detail.html:15 +#: data/templates/build-filter.html:10 +#: data/templates/user-profile.html:19 +#: data/templates/user-profile-edit.html:61 +msgid "State" +msgstr "Status" + +#: data/templates/build-detail.html:22 +#: data/templates/file-detail.html:17 +#: data/templates/package-detail-list.html:3 +#: data/templates/package-detail-list.html:6 +#: data/templates/package-detail.html:4 +#: data/templates/modules/log-table.html:12 +msgid "Package" +msgstr "Paket" + +#: data/templates/build-detail.html:30 +msgid "Architecture" +msgstr "Architektur" + +#: data/templates/build-detail.html:37 +#: data/templates/file-detail.html:106 +msgid "Host" +msgstr "Host" + +#: data/templates/build-detail.html:43 +msgid "No host assigned, yet." +msgstr "Noch kein Host zugewiesen." + +#: data/templates/build-detail.html:48 +#: data/templates/file-detail.html:112 +msgid "Time" +msgstr "Zeit" + +#: data/templates/build-detail.html:52 +msgid "Job added" +msgstr "Job hinzugefügt" + +#: data/templates/build-detail.html:56 +msgid "Job started" +msgstr "Job gestartet" + +#: data/templates/build-detail.html:57 +msgid "Not started, yet." +msgstr "Noch nicht gestartet." + +#: data/templates/build-detail.html:60 +msgid "Job finished" +msgstr "Job beendet" + +#: data/templates/build-detail.html:61 +msgid "Not finished, yet." +msgstr "Noch nicht beendet." + +#: data/templates/build-detail.html:66 +msgid "Duration" +msgstr "Dauer" + +#: data/templates/build-detail.html:73 +#: data/templates/file-detail.html:117 +msgid "Files" +msgstr "Dateien" + +#: data/templates/build-detail.html:77 +#: data/templates/package-detail.html:91 +#: data/templates/distro-detail.html:18 +#: data/templates/user-profile.html:39 +#: data/templates/repository-detail.html:24 +#: data/templates/log.html:4 +#: data/templates/base.html:51 +msgid "Log" +msgstr "Log" + +#: data/templates/build-detail.html:82 +#: data/templates/package-detail.html:96 +#: data/templates/user-list.html:26 +#: data/templates/build-list.html:11 +#: data/templates/builder-list.html:24 +#: data/templates/build-filter.html:49 +#: data/templates/user-comments.html:10 +#: data/templates/user-profile.html:47 +#: data/templates/distro-list.html:17 +#: data/templates/builder-detail.html:57 +msgid "Actions" +msgstr "Aktionen" + +#: data/templates/build-detail.html:85 +msgid "Re-submit build" +msgstr "Build neustarten" + +#: data/templates/build-detail.html:86 +msgid "Mark as permanently failed" +msgstr "Als defekt markieren" + +#: data/templates/build-detail.html:88 +msgid "Schedule test build" +msgstr "Test-Build planen" + +#: data/templates/file-detail.html:7 +#: data/templates/modules/files-table.html:10 +msgid "Download" +msgstr "Herunterladen" + +#: data/templates/file-detail.html:10 +msgid "Information" +msgstr "Informationen" + +#: data/templates/file-detail.html:25 +msgid "Description" +msgstr "Beschreibung" + +#: data/templates/file-detail.html:33 +#: data/templates/package-detail.html:11 +msgid "URL" +msgstr "URL" + +#: data/templates/file-detail.html:39 +#: data/templates/package-detail.html:17 +msgid "License" +msgstr "Lizenz" + +#: data/templates/file-detail.html:46 +#: data/templates/package-detail.html:24 +msgid "Maintainer" +msgstr "Maintainer" + +#: data/templates/file-detail.html:53 +msgid "Size" +msgstr "Größe" + +#: data/templates/file-detail.html:59 +msgid "Hash" +msgstr "Prüfsumme" + +#: data/templates/file-detail.html:66 +msgid "Provides" +msgstr "bietet" + +#: data/templates/file-detail.html:74 +msgid "Requires" +msgstr "benötigt" + +#: data/templates/file-detail.html:82 +msgid "Obsoletes" +msgstr "technisch überholt" + +#: data/templates/file-detail.html:90 +msgid "Conflicts" +msgstr "Konflikte" + +#: data/templates/file-detail.html:96 +msgid "Build information" +msgstr "Build-Informationen" + +#: data/templates/file-detail.html:100 +msgid "ID" +msgstr "ID" + +#: data/templates/source-detail.html:4 +msgid "Source" +msgstr "Source" + +#: data/templates/source-detail.html:9 +msgid "Revision" +msgstr "Revision" + +#: data/templates/source-detail.html:15 +msgid "Branch" +msgstr "Branch" + +#: data/templates/source-detail.html:23 +#: data/templates/index.html:14 +#: data/templates/builder-detail.html:49 +msgid "Latest builds" +msgstr "Letzte Builds" + +#: data/templates/package-detail-list.html:12 +#, python-format +msgid "There is one version of %(pkg)s." +msgid_plural "There are %(num)s different versions of %(pkg)s." +msgstr[0] "Es existiert eine Version von %(pkg)s." +msgstr[1] "Es existieren %(num)s verschiedene Versionen von %(pkg)s." + +#: data/templates/package-detail.html:31 +msgid "Comments" +msgstr "Kommentare" + +#: data/templates/package-detail.html:33 +#, python-format +msgid "This package got a total credit count of %s credits." +msgstr "Dieses Paket hat insgesamt %s credits." + +#: data/templates/package-detail.html:38 +#: data/templates/package-detail.html:51 +msgid "Add comment" +msgstr "Kommentieren" + +#: data/templates/package-detail.html:63 +msgid "Vote" +msgstr "Bewertung" + +#: data/templates/package-detail.html:64 +msgid "None" +msgstr "Keine" + +#: data/templates/package-detail.html:65 +msgid "Approve" +msgstr "akzeptieren" + +#: data/templates/package-detail.html:66 +msgid "Disapprove" +msgstr "ablehnen" + +#: data/templates/package-detail.html:77 +msgid "You must be logged in to comment." +msgstr "Sie müssen eingeloggt sein um zu kommentieren." + +#: data/templates/package-detail.html:82 +msgid "Package files" +msgstr "Paketdateien" + +#: data/templates/package-detail.html:87 +#: data/templates/base.html:40 +msgid "Build jobs" +msgstr "Build-Jobs" + +#: data/templates/package-detail.html:98 +msgid "Package is broken" +msgstr "Das Paket ist defekt" + +#: data/templates/user-list.html:4 +#: data/templates/user-list.html:20 +#: data/templates/base.html:47 +msgid "Users" +msgstr "Benutzer" + +#: data/templates/user-list.html:6 +msgid "On this page you can see a list of all users that are known to the system." +msgstr "Auf dieser Seite sehen Sie einer Übersicht aller bekannten Benutzer." + +#: data/templates/user-list.html:10 +msgid "Administrators" +msgstr "Administratoren" + +#: data/templates/user-list.html:15 +msgid "Testers" +msgstr "Tester" + +#: data/templates/user-list.html:28 +msgid "Latest comments" +msgstr "Letzte Kommentare" + +#: data/templates/package-list.html:3 +#: data/templates/package-list.html:6 +msgid "Package list" +msgstr "Paketliste" + +#: data/templates/package-list.html:8 +msgid "This is an alphabetically ordered list of all packages in the distribution." +msgstr "Hier sehen Sie eine alphabetisch sortierte Liste aller Packete der Distribution." + +#: data/templates/package-list.html:9 +msgid "Click on a link to see further information about the package." +msgstr "Klicken Sie auf den Link um mehr Details über das Paket zu sehen." + +#: data/templates/package-list.html:13 +msgid "Quick selection:" +msgstr "Schnellauswahl:" + +#: data/templates/user-profile-need-activation.html:4 +msgid "Edit successful" +msgstr "Bearbeitung erfolgreich" + +#: data/templates/user-profile-need-activation.html:6 +msgid "The user profile was successfully altered." +msgstr "Das Benutzerprofil wurde erfolgreich geändert." + +#: data/templates/user-profile-need-activation.html:7 +msgid "But as you have changed the email address, you need to re-activate the account." +msgstr "Durch die Änderung der E-Mail-Adresse muss der Benutzeraccount neu aktiviert werden." + +#: data/templates/user-profile-need-activation.html:8 +msgid "Have a look at you mailbox - you already do know what to do." +msgstr "Prüfen Sie Ihre Mailbox - Sie wissen ja wie's geht." + +#: data/templates/distro-detail.html:4 +msgid "Distribution" +msgstr "Distrubution" + +#: data/templates/distro-detail.html:9 +#, python-format +msgid "This distribution is available for %s and maintained by %s." +msgstr "Diese Distribution ist verfügbar für %s und wird gepflegt von %s." + +#: data/templates/distro-detail.html:12 +msgid "Binary repositories" +msgstr "Binärrepositories" + +#: data/templates/distro-detail.html:15 +msgid "Sources" +msgstr "Sourcen" + +#: data/templates/index.html:4 +msgid "Summary" +msgstr "Zusammenfassung" + +#: data/templates/index.html:6 +msgid "Welcome to the Pakfire Build Server." +msgstr "Willkommen beim Pakfire Build Server." + +#: data/templates/index.html:10 +msgid "Ongoing builds" +msgstr "Laufende Builds" + +#: data/templates/index.html:17 +msgid "Show more builds." +msgstr "Mehr Builds anzeigen." + +#: data/templates/build-list.html:3 +#: data/templates/build-list.html:6 +#: data/templates/build-filter.html:3 +msgid "Build job list" +msgstr "Build-Job-Liste" + +#: data/templates/build-list.html:13 +#: data/templates/build-filter.html:6 +msgid "Filter builds" +msgstr "Builds filtern" + +#: data/templates/user-delete.html:4 +#, python-format +msgid "Delete user %s" +msgstr "User %s löschen" + +#: data/templates/user-delete.html:8 +msgid "Do you really want to delete your own account?" +msgstr "Möchten Sie wirklich Ihren eigenen Account löschen?" + +#: data/templates/user-delete.html:9 +msgid "You won't be able to login and use this service any more." +msgstr "Sie werden sich nicht mehr einloggen oder den Dienst verwenden können." + +#: data/templates/user-delete.html:11 +#, python-format +msgid "Do you really want to delete the user %s?" +msgstr "Möchten Sie den Benutzer %s wirklich löschen?" + +#: data/templates/user-delete.html:16 +#: data/templates/builder-delete.html:11 +#, python-format +msgid "Delete %s" +msgstr "%s löschen" + +#: data/templates/user-delete.html:18 +#: data/templates/builder-delete.html:12 +msgid "Back" +msgstr "Zurück" + +#: data/templates/logout.html:4 +msgid "Logout successful" +msgstr "Logout erfolgreich" + +#: data/templates/logout.html:7 +msgid "You have successfully logged out from the Pakfire Build Server." +msgstr "Sie haben sich erfolgreich vom Pakfire Build Server ausgeloggt." + +#: data/templates/logout.html:8 +msgid "Have a nice day!" +msgstr "Einen schönen Tag noch!" + +#: data/templates/logout.html:12 +#: data/templates/login-successful.html:14 +msgid "Go on" +msgstr "Weiter" + +#: data/templates/register-fail.html:4 +msgid "Registration failed" +msgstr "Registrierung fehlgeschlagen" + +#: data/templates/register-fail.html:6 +#: data/templates/register-activation-fail.html:6 +#: data/templates/user-profile-edit-fail.html:6 +msgid "We are sorry." +msgstr "Es tut uns Leid." + +#: data/templates/register-fail.html:7 +msgid "We could not create your requested account." +msgstr "Der gewünschte Account konnte nicht erstellt werden." + +#: data/templates/register-fail.html:17 +#: data/templates/user-profile-edit-fail.html:17 +msgid "Use the back button on your web browser to go back to the previous page and correct your submission." +msgstr "" + +#: data/templates/builder-list.html:3 +#: data/templates/builder-list.html:6 +#: data/templates/base.html:43 +msgid "Build servers" +msgstr "Buildserver" + +#: data/templates/builder-list.html:9 +msgid "Builders are those, that do all the hard work." +msgstr "Die Builder verrichten all die harte Arbeit." + +#: data/templates/builder-list.html:10 +msgid "Build jobs are scheduled to these hosts that they process and send back the result." +msgstr "" + +#: data/templates/builder-list.html:26 +msgid "Create new builder" +msgstr "Neuen Builder anlegen" + +#: data/templates/register-activation-fail.html:4 +msgid "Activation failed" +msgstr "Aktivierung fehlgeschlagen" + +#: data/templates/register-activation-fail.html:7 +msgid "The activation of your account has failed." +msgstr "Die Aktivierung Ihres Accounts schlug fehl." + +#: data/templates/register-activation-fail.html:8 +msgid "Possibly the registration code is wrong or your registration timed out." +msgstr "" + +#: data/templates/register-activation-success.html:4 +msgid "Activation successful" +msgstr "Aktivierung erfolgreich" + +#: data/templates/register-activation-success.html:6 +#, python-format +msgid "Your account has been activated, %s." +msgstr "Ihr Account wurde aktiviert, %s." + +#: data/templates/register-activation-success.html:7 +msgid "Have fun!" +msgstr "Viel Spaß!" + +#: data/templates/builder-new.html:4 +msgid "Create a new builder" +msgstr "Einen neuen Builder anlegen" + +#: data/templates/builder-new.html:10 +#: data/templates/register.html:10 +msgid "Name" +msgstr "Name" + +#: data/templates/builder-new.html:15 +msgid "Must be the canonical hostname of the machine." +msgstr "" + +#: data/templates/build-filter.html:13 +msgid "All" +msgstr "Alle" + +#: data/templates/build-filter.html:14 +msgid "Running" +msgstr "" + +#: data/templates/build-filter.html:15 +msgid "Pending" +msgstr "" + +#: data/templates/build-filter.html:16 +msgid "Finished" +msgstr "" + +#: data/templates/build-filter.html:17 +msgid "Dispatching" +msgstr "" + +#: data/templates/build-filter.html:18 +msgid "Uploading" +msgstr "" + +#: data/templates/build-filter.html:22 +msgid "Only show builds with given state." +msgstr "Nur Builds mit diesem Status anzeigen." + +#: data/templates/build-filter.html:26 +msgid "Build host" +msgstr "Buildhost" + +#: data/templates/build-filter.html:29 +msgid "Any" +msgstr "Alle" + +#: data/templates/build-filter.html:36 +msgid "Display only builds by selected host." +msgstr "Nur Builds vom ausgewähltem Host anzeigen." + +#: data/templates/build-filter.html:51 +msgid "Show all builds" +msgstr "Alle Builds anzeigen" + +#: data/templates/builder-delete.html:4 +#: data/templates/builder-detail.html:4 +#: data/templates/builder-pass.html:4 +msgid "Builder" +msgstr "Builder" + +#: data/templates/builder-delete.html:7 +#, python-format +msgid "You are going to delete the build host %s." +msgstr "" + +#: data/templates/user-comments.html:4 +msgid "Latest user comments" +msgstr "Letzte Kommentare" + +#: data/templates/user-comments.html:12 +msgid "Show all users" +msgstr "Alle Benutzer anzeigen" + +#: data/templates/user-profile.html:5 +#: data/templates/user-profile.html:26 +#: data/templates/user-profile-edit.html:64 +msgid "User" +msgstr "Benutzer" + +#: data/templates/user-profile.html:9 +#: data/templates/login.html:22 +#: data/templates/user-profile-edit.html:10 +msgid "Username" +msgstr "Benutzername" + +#: data/templates/user-profile.html:13 +#: data/templates/register.html:19 +#: data/templates/user-profile-edit.html:15 +msgid "Email" +msgstr "Email" + +#: data/templates/user-profile.html:22 +#: data/templates/user-profile-edit.html:71 +msgid "Admin" +msgstr "Admin" + +#: data/templates/user-profile.html:24 +#: data/templates/user-profile-edit.html:67 +msgid "Tester" +msgstr "Tester" + +#: data/templates/user-profile.html:32 +msgid "Registered" +msgstr "Registriert" + +#: data/templates/user-profile.html:42 +#, python-format +msgid "Comments written by %s" +msgstr "Kommentare von %s" + +#: data/templates/user-profile.html:51 +msgid "Account settings" +msgstr "Accounteinstellungen" + +#: data/templates/user-profile.html:54 +msgid "Delete account" +msgstr "Account löschen" + +#: data/templates/repository-detail.html:5 +msgid "Repository" +msgstr "Repositorium" + +#: data/templates/repository-detail.html:6 +msgid "from" +msgstr "von" + +#: data/templates/repository-detail.html:12 +#, python-format +msgid "This repository contains %s packages and is available for %s." +msgstr "" + +#: data/templates/repository-detail.html:15 +msgid "Waiting packages" +msgstr "" + +#: data/templates/repository-detail.html:17 +msgid "These packages are waiting to be published." +msgstr "" + +#: data/templates/repository-detail.html:21 +msgid "Pushed packages" +msgstr "" + +#: data/templates/register-success.html:4 +msgid "Registration successful" +msgstr "" + +#: data/templates/register-success.html:6 +#, python-format +msgid "Your new account has been created, %s." +msgstr "" + +#: data/templates/register-success.html:7 +msgid "To complete the activation, follow the instructions that were sent to you in an activation email." +msgstr "" + +#: data/templates/login.html:4 +#: data/templates/base.html:21 +msgid "Login" +msgstr "Login" + +#: data/templates/login.html:8 +msgid "Username and/or password was wrong. Login failed." +msgstr "" + +#: data/templates/login.html:13 +msgid "Please type your username and your password to the form to log in." +msgstr "" + +#: data/templates/login.html:14 +msgid "If you have no account, yet you can create a new one." +msgstr "" + +#: data/templates/login.html:15 +msgid "Register a new account." +msgstr "" + +#: data/templates/login.html:26 +#: data/templates/register.html:41 +#: data/templates/user-profile-edit.html:33 +msgid "Password" +msgstr "Passwort" + +#: data/templates/register.html:4 +msgid "Register new account" +msgstr "" + +#: data/templates/register.html:15 +msgid "Must be a unique name you login with." +msgstr "" + +#: data/templates/register.html:24 +msgid "Type your email address." +msgstr "" + +#: data/templates/register.html:28 +#: data/templates/user-profile-edit.html:24 +msgid "Real name (optional)" +msgstr "Wahrer Name (optional)" + +#: data/templates/register.html:33 +#: data/templates/user-profile-edit.html:29 +msgid "Your real name is used to identify you by others." +msgstr "" + +#: data/templates/register.html:38 +msgid "Account security" +msgstr "Account-Sicherheit" + +#: data/templates/register.html:46 +#: data/templates/user-profile-edit.html:38 +msgid "The password is used to secure the login and must be at least 8 characters." +msgstr "" + +#: data/templates/register.html:50 +#: data/templates/user-profile-edit.html:42 +msgid "Confirm" +msgstr "Bestätigen" + +#: data/templates/login-successful.html:4 +msgid "Login successful" +msgstr "Login erfolgreich" + +#: data/templates/login-successful.html:6 +#, python-format +msgid "Welcome, %s." +msgstr "Willkommen, %s." + +#: data/templates/login-successful.html:10 +msgid "Your login to the Pakfire Build Server was successful." +msgstr "" + +#: data/templates/user-profile-edit-fail.html:4 +msgid "Edit failed" +msgstr "Bearbeitung fehlgeschlagen" + +#: data/templates/user-profile-edit-fail.html:7 +msgid "The user profile cannot be saved." +msgstr "Das Benutzerprofil kann nicht gespeichert werden." + +#: data/templates/modules/files-table.html:9 +msgid "Info" +msgstr "Info" + +#: data/templates/modules/comments-table.html:8 +#, python-format +msgid "on %s" +msgstr "zu %s" + +#: data/templates/modules/comments-table.html:11 +#, python-format +msgid "by %s" +msgstr "von %s" + +#: data/templates/modules/comments-table.html:18 +msgid "No comments so far." +msgstr "Bisher keine Kommentare." + +#: data/templates/modules/log-table.html:23 +msgid "No log entries, yet." +msgstr "Noch keine Logeinträge." + +#: data/templates/source-list.html:3 +msgid "Sources repositories" +msgstr "Quellrepositorien" + +#: data/templates/source-list.html:6 +msgid "Source repositories" +msgstr "Quell-Repositories" + +#: data/templates/source-list.html:17 +msgid "Add source repository" +msgstr "Quellrepository hinzufügen" + +#: data/templates/source-list.html:18 +#, fuzzy +msgid "Blah 123" +msgstr "Blah 123" + +#: data/templates/distro-list.html:3 +#: data/templates/distro-list.html:6 +#: data/templates/base.html:37 +msgid "Distributions" +msgstr "Distributionen" + +#: data/templates/distro-list.html:19 +msgid "Add distribution" +msgstr "Distribution hinzufügen" + +#: data/templates/user-profile-edit.html:4 +#, python-format +msgid "Edit user %s" +msgstr "Benutzer %s bearbeiten" + +#: data/templates/user-profile-edit.html:12 +msgid "Cannot be changed." +msgstr "Kann nicht geändert werden." + +#: data/templates/user-profile-edit.html:20 +msgid "If the email address is changed, your account will be disabled until you reconfirm the new email address." +msgstr "" + +#: data/templates/user-profile-edit.html:47 +msgid "Leave the password fields empty to keep the current password." +msgstr "" + +#: data/templates/user-profile-edit.html:58 +msgid "Admin actions" +msgstr "Administrator-Aktionen" + +#: data/templates/user-profile-edit.html:76 +msgid "Define the permissions of the user." +msgstr "Rechte des Users festlegen." + +#: data/templates/base.html:4 +msgid "No title given" +msgstr "Kein Titel angegeben" + +#: data/templates/base.html:19 +msgid "Logout" +msgstr "Ausloggen" + +#: data/templates/base.html:22 +msgid "Register" +msgstr "Registrieren" + +#: data/templates/base.html:26 +#, python-format +msgid "A service by the %s." +msgstr "Ein Service von %s." + +#: data/templates/base.html:31 +msgid "Index" +msgstr "Startseite" + +#: data/templates/base.html:34 +msgid "Packages" +msgstr "Pakete" + +#: data/templates/base.html:84 +msgid "About Pakfire" +msgstr "Über Pakfire" + +#: data/templates/base.html:86 +msgid "Pakfire is the buildsystem that is used to build the IPFire Linux firewall distribution." +msgstr "" + +#: data/templates/base.html:87 +msgid "It also installs and updates packages on the IPFire systems." +msgstr "" + +#: data/templates/base.html:91 +msgid "Documentation" +msgstr "Dokumentation" + +#: data/templates/base.html:94 +msgid "Documentation index" +msgstr "Dokumentationsübersicht" + +#: data/templates/builder-detail.html:8 +msgid "Status" +msgstr "Status" + +#: data/templates/builder-detail.html:14 +msgid "Load average" +msgstr "Durchschnittliche Last" + +#: data/templates/builder-detail.html:22 +msgid "Supported architectures" +msgstr "Unterstützte Architekturen" + +#: data/templates/builder-detail.html:24 +#: data/templates/builder-detail.html:34 +msgid "Unknown" +msgstr "Unbekannt" + +#: data/templates/builder-detail.html:30 +msgid "Host information" +msgstr "Host-Informationen" + +#: data/templates/builder-detail.html:33 +msgid "CPU model" +msgstr "CPU-Modell" + +#: data/templates/builder-detail.html:37 +msgid "Memory" +msgstr "Arbeitsspeicher" + +#: data/templates/builder-detail.html:44 +msgid "Currently running builds on this host" +msgstr "Aktuell laufende Jobs auf diesem Host" + +#: data/templates/builder-detail.html:60 +msgid "Edit builder" +msgstr "Builder bearbeiten" + +#: data/templates/builder-detail.html:64 +msgid "Disable builder" +msgstr "Builder deaktivieren" + +#: data/templates/builder-detail.html:68 +msgid "Enable builder" +msgstr "Builder aktivieren" + +#: data/templates/builder-detail.html:71 +msgid "Renew passphrase" +msgstr "Passphrase erneuern" + +#: data/templates/builder-detail.html:75 +msgid "Delete builder" +msgstr "Builder löschen" + +#: data/templates/builder-pass.html:8 +#, python-format +msgid "The new host %s has been successfully created." +msgstr "" + +#: data/templates/builder-pass.html:10 +#, python-format +msgid "The passphrase for %s has been regenerated." +msgstr "" + +#: data/templates/builder-pass.html:13 +msgid "For authorization to the Pakfire Master Server there is a passphrase required which must be configured to the host." +msgstr "" + +#: data/templates/builder-pass.html:14 +msgid "This passphrase is:" +msgstr "Die Passphrase lautet:" + +#: data/templates/builder-pass.html:20 +msgid "Next" +msgstr "Weiter" + +#: web/handlers_auth.py:53 +msgid "No username provided." +msgstr "Kein Benutzername angegeben." + +#: web/handlers_auth.py:55 +msgid "The given username is already taken." +msgstr "Der gewählte Benutzername ist bereits belegt." + +#: web/handlers_auth.py:58 +#: web/handlers_users.py:76 +msgid "No email address provided." +msgstr "Keine Email-Adresse angegeben." + +#: web/handlers_auth.py:60 +#: web/handlers_users.py:78 +msgid "Email address is invalid." +msgstr "Die Email-Adresse ist ungültig." + +#: web/handlers_auth.py:62 +msgid "The given email address is already used for another account." +msgstr "Die eingegebene Email-Adresse wird bereits für einen anderen Account verwendet." + +#: web/handlers_auth.py:66 +msgid "No password provided." +msgstr "Kein Passwort angegeben." + +#: web/handlers_auth.py:68 +#: web/handlers_users.py:82 +msgid "Password has less than 8 characters." +msgstr "Das Passwort hat weniger als 8 Zeichen." + +#: web/handlers_auth.py:70 +#: web/handlers_users.py:84 +msgid "Passwords do not match." +msgstr "Passwörter stimmen nicht überein." + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "January" +msgstr "Januar" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "February" +msgstr "Februar" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "March" +msgstr "März" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "April" +msgstr "April" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "May" +msgstr "Mai" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "June" +msgstr "Juni" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "July" +msgstr "Juli" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "August" +msgstr "August" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "September" +msgstr "September" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "October" +msgstr "Oktober" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "November" +msgstr "November" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "December" +msgstr "Dezember" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Monday" +msgstr "Montag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Tuesday" +msgstr "Dienstag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Wednesday" +msgstr "Mittwoch" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Thursday" +msgstr "Donnerstag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:230 +msgid "Friday" +msgstr "Freitag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:230 +msgid "Saturday" +msgstr "Samstag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:230 +msgid "Sunday" +msgstr "Sonntag" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:274 +#, python-format +msgid "1 second ago" +msgid_plural "%(seconds)d seconds ago" +msgstr[0] "vor einer Sekunde" +msgstr[1] "vor %(seconds)d Sekunden" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:279 +#, python-format +msgid "1 minute ago" +msgid_plural "%(minutes)d minutes ago" +msgstr[0] "vor einer Minute" +msgstr[1] "vor %(minutes)d Minuten" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:283 +#, python-format +msgid "1 hour ago" +msgid_plural "%(hours)d hours ago" +msgstr[0] "vor einer Stunde" +msgstr[1] "vor %(hours)d Stunden" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:287 +#, python-format +msgid "%(time)s" +msgstr "%(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:290 +msgid "yesterday" +msgstr "gestern" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:291 +#, python-format +msgid "yesterday at %(time)s" +msgstr "gestern um %(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:293 +#, python-format +msgid "%(weekday)s" +msgstr "%(weekday)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:294 +#, python-format +msgid "%(weekday)s at %(time)s" +msgstr "%(weekday)s um %(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:296 +#: /usr/lib/python2.7/site-packages/tornado/locale.py:338 +#, python-format +msgid "%(month_name)s %(day)s" +msgstr "%(month_name)s %(day)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:297 +#, python-format +msgid "%(month_name)s %(day)s at %(time)s" +msgstr "%(month_name)s %(day)s um %(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:300 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s" +msgstr "%(month_name)s %(day)s, %(year)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:301 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s at %(time)s" +msgstr "%(month_name)s %(day)s, %(year)s um %(time)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:332 +#, python-format +msgid "%(weekday)s, %(month_name)s %(day)s" +msgstr "%(weekday)s, %(month_name)s %(day)s" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:353 +#, python-format +msgid "%(commas)s and %(last)s" +msgstr "%(commas)s und %(last)s" + diff --git a/data/translations/pakfire.pot b/data/translations/pakfire.pot new file mode 100644 index 0000000..082d5ff --- /dev/null +++ b/data/translations/pakfire.pot @@ -0,0 +1,1571 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-05-15 22:58+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: data/templates/build-detail.html:3 data/templates/build-detail.html:7 +#: data/templates/build-detail.html:9 data/templates/modules/log-table.html:10 +msgid "Build" +msgstr "" + +#: data/templates/build-detail.html:14 data/templates/file-detail.html:98 +msgid "ID" +msgstr "" + +#: data/templates/build-detail.html:18 data/templates/build-filter.html:10 +#: data/templates/user-profile.html:19 +#: data/templates/user-profile-edit.html:71 +msgid "State" +msgstr "" + +#: data/templates/build-detail.html:24 data/templates/file-detail.html:14 +#: data/templates/package-detail-list.html:3 +#: data/templates/package-detail-list.html:6 +#: data/templates/package-detail.html:3 data/templates/package-detail.html:6 +#: data/templates/modules/log-table.html:12 +msgid "Package" +msgstr "" + +#: data/templates/build-detail.html:30 +msgid "Architecture" +msgstr "" + +#: data/templates/build-detail.html:36 data/templates/file-detail.html:104 +msgid "Host" +msgstr "" + +#: data/templates/build-detail.html:42 +msgid "No host assigned, yet." +msgstr "" + +#: data/templates/build-detail.html:47 data/templates/build-priority.html:11 +msgid "Priority" +msgstr "" + +#: data/templates/build-detail.html:51 data/templates/build-priority.html:14 +msgid "Very high" +msgstr "" + +#: data/templates/build-detail.html:53 data/templates/build-priority.html:15 +msgid "High" +msgstr "" + +#: data/templates/build-detail.html:55 data/templates/build-priority.html:16 +msgid "Medium" +msgstr "" + +#: data/templates/build-detail.html:57 data/templates/build-priority.html:17 +msgid "Low" +msgstr "" + +#: data/templates/build-detail.html:59 data/templates/build-priority.html:18 +msgid "Very low" +msgstr "" + +#: data/templates/build-detail.html:67 +msgid "Commit" +msgstr "" + +#: data/templates/build-detail.html:77 +msgid "Author" +msgstr "" + +#: data/templates/build-detail.html:81 +msgid "Committer" +msgstr "" + +#: data/templates/build-detail.html:85 +msgid "Date" +msgstr "" + +#: data/templates/build-detail.html:92 data/templates/file-detail.html:110 +msgid "Time" +msgstr "" + +#: data/templates/build-detail.html:96 +msgid "Job added" +msgstr "" + +#: data/templates/build-detail.html:100 +msgid "Job started" +msgstr "" + +#: data/templates/build-detail.html:101 +msgid "Not started, yet." +msgstr "" + +#: data/templates/build-detail.html:104 +msgid "Job finished" +msgstr "" + +#: data/templates/build-detail.html:105 +msgid "Not finished, yet." +msgstr "" + +#: data/templates/build-detail.html:110 +msgid "Duration" +msgstr "" + +#: data/templates/build-detail.html:118 data/templates/file-detail.html:115 +msgid "Files" +msgstr "" + +#: data/templates/build-detail.html:122 data/templates/package-detail.html:95 +#: data/templates/distro-detail.html:18 data/templates/user-profile.html:39 +#: data/templates/repository-detail.html:24 data/templates/log.html:4 +#: data/templates/base.html:51 +msgid "Log" +msgstr "" + +#: data/templates/build-detail.html:127 data/templates/file-detail.html:120 +#: data/templates/package-detail.html:100 data/templates/user-list.html:26 +#: data/templates/build-list.html:11 data/templates/builder-list.html:24 +#: data/templates/build-filter.html:51 data/templates/user-comments.html:10 +#: data/templates/user-profile.html:47 data/templates/distro-list.html:17 +#: data/templates/builder-detail.html:75 +msgid "Actions" +msgstr "" + +#: data/templates/build-detail.html:130 +msgid "Re-submit build" +msgstr "" + +#: data/templates/build-detail.html:131 +msgid "Mark as permanently failed" +msgstr "" + +#: data/templates/build-detail.html:135 +msgid "Schedule test build" +msgstr "" + +#: data/templates/build-detail.html:138 +msgid "Modify priority" +msgstr "" + +#: data/templates/file-detail.html:5 +msgid "File" +msgstr "" + +#: data/templates/file-detail.html:22 +msgid "Description" +msgstr "" + +#: data/templates/file-detail.html:30 data/templates/package-detail.html:13 +msgid "URL" +msgstr "" + +#: data/templates/file-detail.html:38 data/templates/package-detail.html:21 +msgid "License" +msgstr "" + +#: data/templates/file-detail.html:45 data/templates/package-detail.html:28 +msgid "Maintainer" +msgstr "" + +#: data/templates/file-detail.html:52 +msgid "Size" +msgstr "" + +#: data/templates/file-detail.html:58 +msgid "Hash" +msgstr "" + +#: data/templates/file-detail.html:65 +msgid "Provides" +msgstr "" + +#: data/templates/file-detail.html:73 +msgid "Requires" +msgstr "" + +#: data/templates/file-detail.html:81 +msgid "Obsoletes" +msgstr "" + +#: data/templates/file-detail.html:89 +msgid "Conflicts" +msgstr "" + +#: data/templates/file-detail.html:95 +msgid "Build information" +msgstr "" + +#: data/templates/file-detail.html:122 +msgid "Download file" +msgstr "" + +#: data/templates/source-detail.html:4 +msgid "Source" +msgstr "" + +#: data/templates/source-detail.html:9 +msgid "Revision" +msgstr "" + +#: data/templates/source-detail.html:15 +msgid "Branch" +msgstr "" + +#: data/templates/source-detail.html:23 data/templates/index.html:16 +msgid "Latest builds" +msgstr "" + +#: data/templates/package-detail-list.html:12 +#, python-format +msgid "There is one version of %(pkg)s." +msgid_plural "There are %(num)s different versions of %(pkg)s." +msgstr[0] "" +msgstr[1] "" + +#: data/templates/package-detail.html:35 +msgid "Comments" +msgstr "" + +#: data/templates/package-detail.html:37 +#, python-format +msgid "This package got a total credit count of %s credits." +msgstr "" + +#: data/templates/package-detail.html:42 data/templates/package-detail.html:55 +msgid "Add comment" +msgstr "" + +#: data/templates/package-detail.html:67 +msgid "Vote" +msgstr "" + +#: data/templates/package-detail.html:68 +msgid "None" +msgstr "" + +#: data/templates/package-detail.html:69 +msgid "Approve" +msgstr "" + +#: data/templates/package-detail.html:70 +msgid "Disapprove" +msgstr "" + +#: data/templates/package-detail.html:81 +msgid "You must be logged in to comment." +msgstr "" + +#: data/templates/package-detail.html:86 +msgid "Package files" +msgstr "" + +#: data/templates/package-detail.html:91 data/templates/base.html:40 +msgid "Build jobs" +msgstr "" + +#: data/templates/package-detail.html:102 +msgid "Package is broken" +msgstr "" + +#: data/templates/user-list.html:4 data/templates/user-list.html:20 +#: data/templates/docs-users.html:6 data/templates/docs-users.html:30 +#: data/templates/docs-index.html:14 data/templates/docs-index.html:20 +#: data/templates/base.html:47 data/templates/docs-base.html:12 +msgid "Users" +msgstr "" + +#: data/templates/user-list.html:6 +msgid "" +"On this page you can see a list of all users that are known to the system." +msgstr "" + +#: data/templates/user-list.html:10 +msgid "Administrators" +msgstr "" + +#: data/templates/user-list.html:15 data/templates/docs-users.html:20 +msgid "Testers" +msgstr "" + +#: data/templates/user-list.html:28 +msgid "Latest comments" +msgstr "" + +#: data/templates/package-list.html:3 data/templates/package-list.html:6 +msgid "Package list" +msgstr "" + +#: data/templates/package-list.html:8 +msgid "" +"This is an alphabetically ordered list of all packages in the distribution." +msgstr "" + +#: data/templates/package-list.html:9 +msgid "Click on a link to see further information about the package." +msgstr "" + +#: data/templates/package-list.html:13 +msgid "Quick selection:" +msgstr "" + +#: data/templates/user-profile-need-activation.html:4 +msgid "Edit successful" +msgstr "" + +#: data/templates/user-profile-need-activation.html:6 +msgid "The user profile was successfully altered." +msgstr "" + +#: data/templates/user-profile-need-activation.html:7 +msgid "" +"But as you have changed the email address, you need to re-activate the " +"account." +msgstr "" + +#: data/templates/user-profile-need-activation.html:8 +msgid "Have a look at you mailbox - you already do know what to do." +msgstr "" + +#: data/templates/distro-detail.html:4 +msgid "Distribution" +msgstr "" + +#: data/templates/distro-detail.html:9 +#, python-format +msgid "This distribution is available for %s and maintained by %s." +msgstr "" + +#: data/templates/distro-detail.html:12 +msgid "Binary repositories" +msgstr "" + +#: data/templates/distro-detail.html:15 +msgid "Sources" +msgstr "" + +#: data/templates/index.html:3 +msgid "Welcome to the Pakfire Build Service" +msgstr "" + +#: data/templates/index.html:6 +msgid "Summary" +msgstr "" + +#: data/templates/index.html:8 +msgid "Welcome to the Pakfire Build Server." +msgstr "" + +#: data/templates/index.html:12 +msgid "Ongoing builds" +msgstr "" + +#: data/templates/index.html:19 +msgid "Statistics" +msgstr "" + +#: data/templates/index.html:22 +#, python-format +msgid "There is currently one pending build job." +msgid_plural "There are currently %(num)s pending build jobs." +msgstr[0] "" +msgstr[1] "" + +#: data/templates/index.html:25 +#, python-format +msgid "Average build time is %.1f minutes." +msgstr "" + +#: data/templates/build-priority.html:3 data/templates/build-priority.html:6 +msgid "Edit build priority" +msgstr "" + +#: data/templates/build-priority.html:22 +msgid "Set the priority of the build process." +msgstr "" + +#: data/templates/build-priority.html:28 +msgid "Beware" +msgstr "" + +#: data/templates/build-priority.html:29 +msgid "Shuffeling build jobs can cause problems with the dependency solving." +msgstr "" + +#: data/templates/build-priority.html:30 +msgid "Don't do this if you are not totally sure you won't break anything." +msgstr "" + +#: data/templates/build-list.html:3 data/templates/build-list.html:6 +#: data/templates/build-filter.html:3 +msgid "Build job list" +msgstr "" + +#: data/templates/build-list.html:13 data/templates/build-filter.html:6 +msgid "Filter builds" +msgstr "" + +#: data/templates/user-delete.html:4 +#, python-format +msgid "Delete user %s" +msgstr "" + +#: data/templates/user-delete.html:8 +msgid "Do you really want to delete your own account?" +msgstr "" + +#: data/templates/user-delete.html:9 +msgid "You won't be able to login and use this service any more." +msgstr "" + +#: data/templates/user-delete.html:11 +#, python-format +msgid "Do you really want to delete the user %s?" +msgstr "" + +#: data/templates/user-delete.html:16 data/templates/builder-delete.html:13 +#, python-format +msgid "Delete %s" +msgstr "" + +#: data/templates/user-delete.html:18 data/templates/builder-delete.html:14 +msgid "Back" +msgstr "" + +#: data/templates/logout.html:4 +msgid "Logout successful" +msgstr "" + +#: data/templates/logout.html:7 +msgid "You have successfully logged out from the Pakfire Build Server." +msgstr "" + +#: data/templates/logout.html:8 +msgid "Have a nice day!" +msgstr "" + +#: data/templates/logout.html:12 data/templates/login-successful.html:14 +msgid "Go on" +msgstr "" + +#: data/templates/register-fail.html:4 +msgid "Registration failed" +msgstr "" + +#: data/templates/register-fail.html:6 +#: data/templates/register-activation-fail.html:6 +#: data/templates/user-profile-edit-fail.html:6 +msgid "We are sorry." +msgstr "" + +#: data/templates/register-fail.html:7 +msgid "We could not create your requested account." +msgstr "" + +#: data/templates/register-fail.html:17 +#: data/templates/user-profile-edit-fail.html:17 +msgid "" +"Use the back button on your web browser to go back to the previous page and " +"correct your submission." +msgstr "" + +#: data/templates/builder-list.html:3 data/templates/builder-list.html:6 +#: data/templates/base.html:43 +msgid "Build servers" +msgstr "" + +#: data/templates/builder-list.html:9 +msgid "Builders are those, that do all the hard work." +msgstr "" + +#: data/templates/builder-list.html:10 +msgid "" +"Build jobs are scheduled to these hosts that they process and send back the " +"result." +msgstr "" + +#: data/templates/builder-list.html:26 data/templates/builder-new.html:3 +msgid "Create new builder" +msgstr "" + +#: data/templates/register-activation-fail.html:4 +msgid "Activation failed" +msgstr "" + +#: data/templates/register-activation-fail.html:7 +msgid "The activation of your account has failed." +msgstr "" + +#: data/templates/register-activation-fail.html:8 +msgid "Possibly the registration code is wrong or your registration timed out." +msgstr "" + +#: data/templates/docs-users.html:3 data/templates/docs-index.html:3 +#: data/templates/docs-build.html:3 data/templates/docs-build.html:6 +msgid "Legend of the build states" +msgstr "" + +#: data/templates/docs-users.html:8 +msgid "" +"All users can join the Pakfire Build Service and are separated into three " +"groups:" +msgstr "" + +#: data/templates/docs-users.html:11 +msgid "Developers" +msgstr "" + +#: data/templates/docs-users.html:13 +msgid "" +"Developers manage this build service and have access to all parts of it." +msgstr "" + +#: data/templates/docs-users.html:14 +msgid "" +"They are responsible to keep the system running and able to push package " +"updates to the repostories." +msgstr "" + +#: data/templates/docs-users.html:17 +msgid "Guidelines for developers" +msgstr "" + +#: data/templates/docs-users.html:22 +msgid "" +"Testers are like users but have the right to vote on packages, which is used " +"to figure out the quality of the package." +msgstr "" + +#: data/templates/docs-users.html:23 +msgid "" +"Everyone can become a tester after he or she has proven to know the IPFire " +"system very well." +msgstr "" + +#: data/templates/docs-users.html:24 +msgid "" +"On these people depends a very huge amount of the quality of the " +"distribution that is made out of the feedback they give." +msgstr "" + +#: data/templates/docs-users.html:27 +msgid "Guidelines for testers" +msgstr "" + +#: data/templates/docs-users.html:32 +msgid "Everybody can join the Pakfire Build Service by registering an account." +msgstr "" + +#: data/templates/docs-users.html:33 +msgid "" +"After a successful activation you are able to leave comments on packages and " +"give feedback to the developers about its status." +msgstr "" + +#: data/templates/docs-users.html:38 data/templates/base.html:22 +msgid "Register" +msgstr "" + +#: data/templates/register-activation-success.html:4 +msgid "Activation successful" +msgstr "" + +#: data/templates/register-activation-success.html:6 +#, python-format +msgid "Your account has been activated, %s." +msgstr "" + +#: data/templates/register-activation-success.html:7 +msgid "Have fun!" +msgstr "" + +#: data/templates/builder-new.html:6 +msgid "Create a new builder" +msgstr "" + +#: data/templates/builder-new.html:12 data/templates/builder-edit.html:12 +#: data/templates/register.html:10 +msgid "Name" +msgstr "" + +#: data/templates/builder-new.html:17 +msgid "Must be the canonical hostname of the machine." +msgstr "" + +#: data/templates/build-filter.html:13 +msgid "All" +msgstr "" + +#: data/templates/build-filter.html:14 +msgid "Running" +msgstr "" + +#: data/templates/build-filter.html:15 +msgid "Pending" +msgstr "" + +#: data/templates/build-filter.html:16 +msgid "Finished" +msgstr "" + +#: data/templates/build-filter.html:17 +msgid "Failed" +msgstr "" + +#: data/templates/build-filter.html:18 +msgid "Permanently failed" +msgstr "" + +#: data/templates/build-filter.html:19 +msgid "Dispatching" +msgstr "" + +#: data/templates/build-filter.html:20 +msgid "Uploading" +msgstr "" + +#: data/templates/build-filter.html:24 +msgid "Only show builds with given state." +msgstr "" + +#: data/templates/build-filter.html:28 +msgid "Build host" +msgstr "" + +#: data/templates/build-filter.html:31 +msgid "Any" +msgstr "" + +#: data/templates/build-filter.html:38 +msgid "Display only builds by selected host." +msgstr "" + +#: data/templates/build-filter.html:53 +msgid "Show all builds" +msgstr "" + +#: data/templates/docs-index.html:6 +msgid "Documents" +msgstr "" + +#: data/templates/docs-index.html:8 +msgid "" +"This is a collection of documents that should be read by everybody who is " +"using this system." +msgstr "" + +#: data/templates/docs-index.html:9 +msgid "To make this easy for you, the documents are grouped into two parts." +msgstr "" + +#: data/templates/docs-index.html:12 +msgid "Documents for testers" +msgstr "" + +#: data/templates/docs-index.html:17 +msgid "Documents for developers" +msgstr "" + +#: data/templates/docs-index.html:19 data/templates/docs-base.html:11 +msgid "Builds" +msgstr "" + +#: data/templates/docs-index.html:24 +msgid "Technical documentation is available on the wiki:" +msgstr "" + +#: data/templates/docs-index.html:25 +msgid "Technical documentation" +msgstr "" + +#: data/templates/builder-edit.html:3 data/templates/builder-edit.html:6 +#, python-format +msgid "Edit builder %s" +msgstr "" + +#: data/templates/builder-edit.html:17 +msgid "The hostname cannot be changed." +msgstr "" + +#: data/templates/builder-edit.html:21 +msgid "Enabled" +msgstr "" + +#: data/templates/builder-edit.html:26 +msgid "The builder must be enabled in order to process build jobs." +msgstr "" + +#: data/templates/builder-edit.html:32 +msgid "Build job settings" +msgstr "" + +#: data/templates/builder-edit.html:34 +msgid "These settings do only take effect if the builder is enabled." +msgstr "" + +#: data/templates/builder-edit.html:38 +msgid "Authorized to build source packages" +msgstr "" + +#: data/templates/builder-edit.html:43 +msgid "Only a few build servers are allowed to build source packages." +msgstr "" + +#: data/templates/builder-edit.html:47 +msgid "Authorized to build binary packages" +msgstr "" + +#: data/templates/builder-edit.html:54 +msgid "Authorized to build test packages" +msgstr "" + +#: data/templates/builder-delete.html:3 +#, python-format +msgid "Delete builder %s" +msgstr "" + +#: data/templates/builder-delete.html:6 data/templates/builder-detail.html:3 +#: data/templates/builder-detail.html:6 data/templates/builder-pass.html:4 +msgid "Builder" +msgstr "" + +#: data/templates/builder-delete.html:9 +#, python-format +msgid "You are going to delete the build host %s." +msgstr "" + +#: data/templates/user-comments.html:4 +msgid "Latest user comments" +msgstr "" + +#: data/templates/user-comments.html:12 +msgid "Show all users" +msgstr "" + +#: data/templates/user-profile.html:5 data/templates/user-profile.html:26 +#: data/templates/user-profile-edit.html:74 +msgid "User" +msgstr "" + +#: data/templates/user-profile.html:9 data/templates/login.html:22 +#: data/templates/user-profile-edit.html:10 +msgid "Username" +msgstr "" + +#: data/templates/user-profile.html:13 data/templates/register.html:19 +#: data/templates/user-profile-edit.html:15 +msgid "Email" +msgstr "" + +#: data/templates/user-profile.html:22 +#: data/templates/user-profile-edit.html:81 +msgid "Admin" +msgstr "" + +#: data/templates/user-profile.html:24 +#: data/templates/user-profile-edit.html:77 +msgid "Tester" +msgstr "" + +#: data/templates/user-profile.html:32 +msgid "Registered" +msgstr "" + +#: data/templates/user-profile.html:42 +#, python-format +msgid "Comments written by %s" +msgstr "" + +#: data/templates/user-profile.html:51 +msgid "Account settings" +msgstr "" + +#: data/templates/user-profile.html:54 +msgid "Delete account" +msgstr "" + +#: data/templates/repository-detail.html:5 +msgid "Repository" +msgstr "" + +#: data/templates/repository-detail.html:6 +msgid "from" +msgstr "" + +#: data/templates/repository-detail.html:12 +#, python-format +msgid "This repository contains %s packages and is available for %s." +msgstr "" + +#: data/templates/repository-detail.html:15 +msgid "Waiting packages" +msgstr "" + +#: data/templates/repository-detail.html:17 +msgid "These packages are waiting to be published." +msgstr "" + +#: data/templates/repository-detail.html:21 +msgid "Pushed packages" +msgstr "" + +#: data/templates/register-success.html:4 +msgid "Registration successful" +msgstr "" + +#: data/templates/register-success.html:6 +#, python-format +msgid "Your new account has been created, %s." +msgstr "" + +#: data/templates/register-success.html:7 +msgid "" +"To complete the activation, follow the instructions that were sent to you in " +"an activation email." +msgstr "" + +#: data/templates/login.html:4 data/templates/base.html:21 +msgid "Login" +msgstr "" + +#: data/templates/login.html:8 +msgid "Username and/or password was wrong. Login failed." +msgstr "" + +#: data/templates/login.html:13 +msgid "Please type your username and your password to the form to log in." +msgstr "" + +#: data/templates/login.html:14 +msgid "If you have no account, yet you can create a new one." +msgstr "" + +#: data/templates/login.html:15 +msgid "Register a new account." +msgstr "" + +#: data/templates/login.html:26 data/templates/register.html:41 +#: data/templates/user-profile-edit.html:33 +msgid "Password" +msgstr "" + +#: data/templates/register.html:4 +msgid "Register new account" +msgstr "" + +#: data/templates/register.html:15 +msgid "Must be a unique name you login with." +msgstr "" + +#: data/templates/register.html:24 +msgid "Type your email address." +msgstr "" + +#: data/templates/register.html:28 data/templates/user-profile-edit.html:24 +msgid "Real name (optional)" +msgstr "" + +#: data/templates/register.html:33 +msgid "Type you firstname and your lastname here." +msgstr "" + +#: data/templates/register.html:38 +msgid "Account security" +msgstr "" + +#: data/templates/register.html:46 data/templates/user-profile-edit.html:38 +msgid "" +"The password is used to secure the login and must be at least 8 characters." +msgstr "" + +#: data/templates/register.html:50 data/templates/user-profile-edit.html:42 +msgid "Confirm" +msgstr "" + +#: data/templates/login-successful.html:4 +msgid "Login successful" +msgstr "" + +#: data/templates/login-successful.html:6 +#, python-format +msgid "Welcome, %s." +msgstr "" + +#: data/templates/login-successful.html:10 +msgid "Your login to the Pakfire Build Server was successful." +msgstr "" + +#: data/templates/user-profile-edit-fail.html:4 +msgid "Edit failed" +msgstr "" + +#: data/templates/user-profile-edit-fail.html:7 +msgid "The user profile cannot be saved." +msgstr "" + +#: data/templates/modules/files-table.html:8 +msgid "Info" +msgstr "" + +#: data/templates/modules/files-table.html:9 +msgid "Download" +msgstr "" + +#: data/templates/modules/comments-table.html:8 +#, python-format +msgid "on %s" +msgstr "" + +#: data/templates/modules/comments-table.html:11 +#, python-format +msgid "by %s" +msgstr "" + +#: data/templates/modules/comments-table.html:18 +msgid "No comments so far." +msgstr "" + +#: data/templates/modules/log-table.html:23 +msgid "No log entries, yet." +msgstr "" + +#: data/templates/source-list.html:3 +msgid "Sources repositories" +msgstr "" + +#: data/templates/source-list.html:6 +msgid "Source repositories" +msgstr "" + +#: data/templates/source-list.html:17 +msgid "Add source repository" +msgstr "" + +#: data/templates/source-list.html:18 +msgid "Blah 123" +msgstr "" + +#: data/templates/distro-list.html:3 data/templates/distro-list.html:6 +#: data/templates/base.html:37 +msgid "Distributions" +msgstr "" + +#: data/templates/distro-list.html:19 +msgid "Add distribution" +msgstr "" + +#: data/templates/user-profile-edit.html:4 +#, python-format +msgid "Edit user %s" +msgstr "" + +#: data/templates/user-profile-edit.html:12 +msgid "Cannot be changed." +msgstr "" + +#: data/templates/user-profile-edit.html:20 +msgid "" +"If the email address is changed, your account will be disabled until you " +"reconfirm the new email address." +msgstr "" + +#: data/templates/user-profile-edit.html:29 +msgid "Your real name is used to identify you by others." +msgstr "" + +#: data/templates/user-profile-edit.html:47 +msgid "Leave the password fields empty to keep the current password." +msgstr "" + +#: data/templates/user-profile-edit.html:51 +msgid "Preferred language" +msgstr "" + +#: data/templates/user-profile-edit.html:54 +msgid "Auto-detect" +msgstr "" + +#: data/templates/user-profile-edit.html:62 +msgid "Auto-detect will use the language transmitted by your browser." +msgstr "" + +#: data/templates/user-profile-edit.html:68 +msgid "Admin actions" +msgstr "" + +#: data/templates/user-profile-edit.html:86 +msgid "Define the permissions of the user." +msgstr "" + +#: data/templates/base.html:4 +msgid "No title given" +msgstr "" + +#: data/templates/base.html:19 +msgid "Logout" +msgstr "" + +#: data/templates/base.html:26 +#, python-format +msgid "A service by the %s." +msgstr "" + +#: data/templates/base.html:31 +msgid "Index" +msgstr "" + +#: data/templates/base.html:34 +msgid "Packages" +msgstr "" + +#: data/templates/base.html:84 +msgid "About Pakfire" +msgstr "" + +#: data/templates/base.html:86 +msgid "" +"Pakfire is the buildsystem that is used to build the IPFire Linux firewall " +"distribution." +msgstr "" + +#: data/templates/base.html:87 +msgid "It also installs and updates packages on the IPFire systems." +msgstr "" + +#: data/templates/base.html:91 +msgid "Documentation" +msgstr "" + +#: data/templates/base.html:94 +msgid "Documentation index" +msgstr "" + +#: data/templates/base.html:100 +msgid "All rights reserved." +msgstr "" + +#: data/templates/docs-build.html:8 +msgid "" +"Every build that is done by the Pakfire Build Service has to go through " +"several states:" +msgstr "" + +#: data/templates/docs-build.html:11 +msgid "" +"After checking out the source from the source repository a source package is " +"created and submitted to the build server." +msgstr "" + +#: data/templates/docs-build.html:12 +msgid "" +"Starting from inserting a source file to the build service, there are binary " +"build jobs created for every supported architecture." +msgstr "" + +#: data/templates/docs-build.html:15 +msgid "" +"These get assigned to a build host which has to compile or assemble the " +"package and return it back to the build server." +msgstr "" + +#: data/templates/docs-build.html:16 +msgid "In the table below, there are all states that a build job goes through:" +msgstr "" + +#: data/templates/docs-build.html:19 +msgid "Build is running" +msgstr "" + +#: data/templates/docs-build.html:20 +msgid "Build has failed" +msgstr "" + +#: data/templates/docs-build.html:21 +msgid "Build is waiting to be processed" +msgstr "" + +#: data/templates/docs-build.html:22 +msgid "There was a dependency error when the package was built" +msgstr "" + +#: data/templates/docs-build.html:23 +msgid "Build is waiting for source to go to pending state" +msgstr "" + +#: data/templates/docs-build.html:24 +msgid "Files of this build are transferred to the build server" +msgstr "" + +#: data/templates/docs-build.html:25 +msgid "Files are being uploaded to the service" +msgstr "" + +#: data/templates/docs-build.html:26 +msgid "Build has an unknown state" +msgstr "" + +#: data/templates/builder-detail.html:10 +msgid "Status" +msgstr "" + +#: data/templates/builder-detail.html:16 +msgid "Load average" +msgstr "" + +#: data/templates/builder-detail.html:24 +msgid "Supported architectures" +msgstr "" + +#: data/templates/builder-detail.html:26 data/templates/builder-detail.html:59 +msgid "Unknown" +msgstr "" + +#: data/templates/builder-detail.html:32 +msgid "Configuration" +msgstr "" + +#: data/templates/builder-detail.html:35 +msgid "Builds source packages" +msgstr "" + +#: data/templates/builder-detail.html:37 data/templates/builder-detail.html:43 +#: data/templates/builder-detail.html:49 +msgid "Yes" +msgstr "" + +#: data/templates/builder-detail.html:37 data/templates/builder-detail.html:43 +#: data/templates/builder-detail.html:49 +msgid "No" +msgstr "" + +#: data/templates/builder-detail.html:41 +msgid "Builds binary packages" +msgstr "" + +#: data/templates/builder-detail.html:47 +msgid "Runs tests" +msgstr "" + +#: data/templates/builder-detail.html:55 +msgid "Host information" +msgstr "" + +#: data/templates/builder-detail.html:58 +msgid "CPU model" +msgstr "" + +#: data/templates/builder-detail.html:62 +msgid "Memory" +msgstr "" + +#: data/templates/builder-detail.html:69 +msgid "Currently running builds on this host" +msgstr "" + +#: data/templates/builder-detail.html:78 +msgid "Show all build jobs" +msgstr "" + +#: data/templates/builder-detail.html:82 +msgid "Edit builder" +msgstr "" + +#: data/templates/builder-detail.html:86 +msgid "Renew passphrase" +msgstr "" + +#: data/templates/builder-detail.html:90 +msgid "Delete builder" +msgstr "" + +#: data/templates/builder-pass.html:8 +#, python-format +msgid "The new host %s has been successfully created." +msgstr "" + +#: data/templates/builder-pass.html:10 +#, python-format +msgid "The passphrase for %s has been regenerated." +msgstr "" + +#: data/templates/builder-pass.html:13 +msgid "" +"For authorization to the Pakfire Master Server there is a passphrase " +"required which must be configured to the host." +msgstr "" + +#: data/templates/builder-pass.html:14 +msgid "This passphrase is:" +msgstr "" + +#: data/templates/builder-pass.html:20 +msgid "Next" +msgstr "" + +#: data/templates/build-schedule.html:3 data/templates/build-schedule.html:6 +#, python-format +msgid "Schedule test build for %s" +msgstr "" + +#: data/templates/build-schedule.html:8 +msgid "" +"A test build is used to check if a package builds with the current package " +"set." +msgstr "" + +#: data/templates/build-schedule.html:9 +msgid "" +"In this way, developers are able to find quality issues fast and without " +"actively searching for them." +msgstr "" + +#: data/templates/build-schedule.html:12 +msgid "" +"As this build platform only has a limited amount of performance, test builds " +"only have a very less priority." +msgstr "" + +#: data/templates/build-schedule.html:13 +msgid "However, you can manually request to run a test." +msgstr "" + +#: data/templates/build-schedule.html:16 +msgid "" +"The build job will be started when a build slot is available but not before " +"the given time." +msgstr "" + +#: data/templates/build-schedule.html:23 +msgid "Start time" +msgstr "" + +#: data/templates/build-schedule.html:26 +msgid "As soon as possible" +msgstr "" + +#: data/templates/build-schedule.html:27 +msgid "After 5 minutes" +msgstr "" + +#: data/templates/build-schedule.html:28 +msgid "After 15 minutes" +msgstr "" + +#: data/templates/build-schedule.html:29 +msgid "After one hour" +msgstr "" + +#: data/templates/build-schedule.html:30 +msgid "After one day" +msgstr "" + +#: data/templates/build-schedule.html:34 +msgid "Set the time after which the build job starts." +msgstr "" + +#: data/templates/docs-base.html:5 +msgid "All Documents" +msgstr "" + +#: data/templates/docs-base.html:9 +msgid "Topics" +msgstr "" + +#: web/handlers_auth.py:53 +msgid "No username provided." +msgstr "" + +#: web/handlers_auth.py:55 +msgid "The given username is already taken." +msgstr "" + +#: web/handlers_auth.py:58 web/handlers_users.py:85 +msgid "No email address provided." +msgstr "" + +#: web/handlers_auth.py:60 web/handlers_users.py:87 +msgid "Email address is invalid." +msgstr "" + +#: web/handlers_auth.py:62 +msgid "The given email address is already used for another account." +msgstr "" + +#: web/handlers_auth.py:66 +msgid "No password provided." +msgstr "" + +#: web/handlers_auth.py:68 web/handlers_users.py:91 +msgid "Password has less than 8 characters." +msgstr "" + +#: web/handlers_auth.py:70 web/handlers_users.py:93 +msgid "Passwords do not match." +msgstr "" + +#: web/handlers_users.py:97 +msgid "The choosen locale is invalid." +msgstr "" + +#: backend/users.py:207 +msgid "Account Activation" +msgstr "" + +#: backend/users.py:209 +msgid "" +"You, or somebody using you email address, has registered an account on the " +"Pakfire Build Service." +msgstr "" + +#: backend/users.py:211 +msgid "To activate your account, please click on the link below." +msgstr "" + +#: backend/constants.py:22 +msgid "Build job created" +msgstr "" + +#: backend/constants.py:23 +msgid "Build job is now pending" +msgstr "" + +#: backend/constants.py:24 +msgid "Build job is dispatching" +msgstr "" + +#: backend/constants.py:25 +msgid "Build job is running" +msgstr "" + +#: backend/constants.py:26 +msgid "Build job has failed" +msgstr "" + +#: backend/constants.py:27 +msgid "Build job has permanently failed" +msgstr "" + +#: backend/constants.py:28 +msgid "Build job has dependency errors" +msgstr "" + +#: backend/constants.py:29 +msgid "Build job is waiting for the source package" +msgstr "" + +#: backend/constants.py:30 +msgid "Build job is finished" +msgstr "" + +#: backend/constants.py:31 +msgid "Build job has an unknown state" +msgstr "" + +#: backend/constants.py:32 +msgid "Build job is uploading" +msgstr "" + +#: backend/constants.py:37 +#, python-format +msgid "Build job failed: %(build_name)s" +msgstr "" + +#: backend/constants.py:38 +#, python-format +msgid "" +"The build job \"%(build_name)s\" has failed.\n" +"\n" +"This could have a couple of reasons and needs to be investigated by you.\n" +"\n" +"Here is more information about the incident:\n" +"\n" +" Build name: %(build_name)s\n" +" Build host: %(build_host)s\n" +"\n" +"Click on this link to get all details about the build:\n" +" http://pakfire.ipfire.org/build/%(build_uuid)s\n" +"\n" +"Sincerely,\n" +" The Pakfire Build Service" +msgstr "" + +#: backend/constants.py:55 +#, python-format +msgid "Build job finished: %(build_name)s" +msgstr "" + +#: backend/constants.py:56 +#, python-format +msgid "" +"The build job \"%(build_name)s\" has finished.\n" +"\n" +"If you are the maintainer, it is up to you to push it to one or more " +"repositories.\n" +"\n" +"Click on this link to get all details about the build:\n" +" http://pakfire.ipfire.org/build/%(build_uuid)s\n" +"\n" +"Sincerely,\n" +" The Pakfire Build Service" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "January" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "February" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "March" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:225 +msgid "April" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "May" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "June" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "July" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:226 +msgid "August" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "September" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "October" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "November" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:227 +msgid "December" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Monday" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Tuesday" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Wednesday" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:229 +msgid "Thursday" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:230 +msgid "Friday" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:230 +msgid "Saturday" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:230 +msgid "Sunday" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:274 +#, python-format +msgid "1 second ago" +msgid_plural "%(seconds)d seconds ago" +msgstr[0] "" +msgstr[1] "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:279 +#, python-format +msgid "1 minute ago" +msgid_plural "%(minutes)d minutes ago" +msgstr[0] "" +msgstr[1] "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:283 +#, python-format +msgid "1 hour ago" +msgid_plural "%(hours)d hours ago" +msgstr[0] "" +msgstr[1] "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:287 +#, python-format +msgid "%(time)s" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:290 +msgid "yesterday" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:291 +#, python-format +msgid "yesterday at %(time)s" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:293 +#, python-format +msgid "%(weekday)s" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:294 +#, python-format +msgid "%(weekday)s at %(time)s" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:296 +#: /usr/lib/python2.7/site-packages/tornado/locale.py:338 +#, python-format +msgid "%(month_name)s %(day)s" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:297 +#, python-format +msgid "%(month_name)s %(day)s at %(time)s" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:300 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:301 +#, python-format +msgid "%(month_name)s %(day)s, %(year)s at %(time)s" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:332 +#, python-format +msgid "%(weekday)s, %(month_name)s %(day)s" +msgstr "" + +#: /usr/lib/python2.7/site-packages/tornado/locale.py:353 +#, python-format +msgid "%(commas)s and %(last)s" +msgstr "" diff --git a/master/__init__.py b/master/__init__.py new file mode 100644 index 0000000..063146f --- /dev/null +++ b/master/__init__.py @@ -0,0 +1,71 @@ +#!/usr/bin/python + +import logging +import os.path +import tornado.httpserver +import tornado.locale +import tornado.options +import tornado.web + +import backend + +from handlers import * + +BASEDIR = os.path.join(os.path.dirname(__file__), "..", "data") + +# Enable logging +tornado.options.parse_command_line() + +class MasterApplication(tornado.web.Application): + def __init__(self): + settings = dict( + debug = True, + gzip = False, + ) + + # Load translations. + tornado.locale.load_gettext_translations( + os.path.join(BASEDIR, "translations"), "pakfire") + + tornado.web.Application.__init__(self, **settings) + + self.add_handlers(r".*", [ + # API + (r"/api/builder/([A-Za-z0-9\-\.]+)/(\w+)", RPCBuilderHandler), + ]) + + self.pakfire = backend.Pakfire() + + logging.info("Successfully initialied application") + + def __del__(self): + logging.info("Shutting down application") + + @property + def ioloop(self): + return tornado.ioloop.IOLoop.instance() + + def shutdown(self, *args): + logging.debug("Caught shutdown signal") + self.ioloop.stop() + + 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 + # frontends to get best performance out of our service. + if not self.settings["debug"]: + http_server.bind(port) + http_server.start(num_processes=4) + else: + http_server.listen(port) + + self.ioloop.start() + + def reload(self): + logging.debug("Caught reload signal") diff --git a/master/handlers.py b/master/handlers.py new file mode 100644 index 0000000..e8a1fcb --- /dev/null +++ b/master/handlers.py @@ -0,0 +1,316 @@ +#!/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-manager b/pakfire-manager new file mode 100644 index 0000000..4a6bd89 --- /dev/null +++ b/pakfire-manager @@ -0,0 +1,50 @@ +#!/usr/bin/python + +import logging +import time +import tornado.ioloop +import tornado.options + +import backend +from backend.managers import * + +tornado.options.parse_command_line() + +class Daemon(object): + def __init__(self): + self._managers = [] + + self.ioloop.set_blocking_log_threshold(300) + + self.pakfire = backend.Pakfire() + + @property + def ioloop(self): + return tornado.ioloop.IOLoop.instance() + + def add(self, manager_cls): + logging.info("Registering new manager: %s" % manager_cls.__name__) + manager = manager_cls(self.pakfire) + self._managers.append(manager) + + def run(self): + """ + Main loop. + """ + for manager in self._managers: + manager.pc.start() + + self.ioloop.start() + + def shutdown(self): + self.ioloop.stop() + + + + +if __name__ == "__main__": + d = Daemon() + for manager in managers: + d.add(manager) + + d.run() diff --git a/pakfire-master b/pakfire-master new file mode 100644 index 0000000..07fca3b --- /dev/null +++ b/pakfire-master @@ -0,0 +1,7 @@ +#!/usr/bin/python + +from master import MasterApplication + +app = MasterApplication() + +app.run() diff --git a/pakfire-web b/pakfire-web new file mode 100644 index 0000000..feea458 --- /dev/null +++ b/pakfire-web @@ -0,0 +1,31 @@ +#!/usr/bin/python + +import daemon +import os +import signal +import sys + +import tornado.options + +from web import Application + + +#tornado.options.parse_command_line() + +if __name__ == "__main__": + app = Application() + + context = daemon.DaemonContext( + working_directory=os.getcwd(), +# stdout=sys.stdout, stderr=sys.stderr, # XXX causes errors... + ) + + context.signal_map = { + signal.SIGHUP : app.reload, + signal.SIGTERM : app.shutdown, + } + +# with context: +# app.run() + + app.run() diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..093e11e --- /dev/null +++ b/web/__init__.py @@ -0,0 +1,156 @@ +#!/usr/bin/python + +import logging +import os.path +import tornado.httpserver +import tornado.locale +import tornado.options +import tornado.web + +from handlers import * +from ui_modules import * + +BASEDIR = os.path.join(os.path.dirname(__file__), "..", "data") + +# Enable logging +tornado.options.parse_command_line() + +class Application(tornado.web.Application): + def __init__(self): + settings = dict( + cookie_secret = "12345", + debug = True, + gzip = True, + 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, + }, + xsrf_cookies = True, + ) + + # Load translations. + tornado.locale.load_gettext_translations( + os.path.join(BASEDIR, "translations"), "pakfire") + + tornado.web.Application.__init__(self, **settings) + + self.settings["static_path"] = static_path = os.path.join(BASEDIR, "static") + static_handlers = [ + (r"/static/(.*)", tornado.web.StaticFileHandler, dict(path = static_path)), + (r"/(favicon\.ico)", tornado.web.StaticFileHandler, dict(path = static_path)), + (r"/(robots\.txt)", tornado.web.StaticFileHandler, dict(path = static_path)), + ] + + self.add_handlers(r".*", [ + # Entry site that lead the user to index + (r"/", IndexHandler), + + # Handle all the users logins/logouts/registers and stuff. + (r"/login", LoginHandler), + (r"/logout", LogoutHandler), + (r"/register", RegisterHandler), + (r"/users", UsersHandler), + (r"/users/comments", UsersCommentsHandler), + (r"/user/delete/(\w+)", UserDeleteHandler), + (r"/user/edit/(\w+)", UserEditHandler), + (r"/user/(\w+)", UserHandler), + (r"/user/(\w+)/activate/(\w+)", ActivationHandler), + (r"/profile", UserHandler), + + # Packages + (r"/packages", PackageListHandler), + (r"/package/([\w\-\+]+)/([\d]+)/([\w\.\-]+)/([\w\.]+)", PackageDetailHandler), + (r"/package/([\w\-\+]+)", PackageNameHandler), + + # Files + (r"/file/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})", FileDetailHandler), + + # Builds + (r"/builds", BuildListHandler), + (r"/builds/filter", BuildFilterHandler), + (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), + + # 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\-\.]+)", BuilderDetailHandler), + + # Sources + (r"/sources", SourceListHandler), + (r"/source/([0-9]+)", SourceDetailHandler), + + # Distributions + (r"/distributions", DistributionListHandler), + (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), + + # Documents + (r"/documents", DocsIndexHandler), + (r"/documents/builds", DocsBuildsHandler), + (r"/documents/users", DocsUsersHandler), + + # Search + (r"/search", SearchHandler), + + # API + (r"/api/action/(\w+)", RepoActionHandler), + + # Log + (r"/log", LogHandler), + ] + static_handlers) + + self.pakfire = backend.Pakfire() + + logging.info("Successfully initialied application") + + def __del__(self): + logging.info("Shutting down application") + + @property + def ioloop(self): + return tornado.ioloop.IOLoop.instance() + + def shutdown(self, *args): + logging.debug("Caught shutdown signal") + self.ioloop.stop() + + 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 + # frontends to get best performance out of our service. + if not self.settings["debug"]: + http_server.bind(port) + http_server.start(num_processes=4) + else: + http_server.listen(port) + + self.ioloop.start() + + def reload(self): + logging.debug("Caught reload signal") diff --git a/web/handlers.py b/web/handlers.py new file mode 100644 index 0000000..d0828c3 --- /dev/null +++ b/web/handlers.py @@ -0,0 +1,316 @@ +#!/usr/bin/python + +import tornado.web + +from handlers_auth import * +from handlers_base import * +from handlers_builders import * +from handlers_search 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) + + # Get counters + counter_pending = self.pakfire.builds.count(state="pending") + + average_build_time = self.pakfire.builds.average_build_time() + + 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) + + +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 = {} + + # Sort all packages in an array like "" --> [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") + + self.render("build-detail.html", build=build) + + +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 BuildScheduleHandler(BaseHandler): + allowed_types = ("test", "rebuild",) + + @tornado.web.authenticated + def get(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") + + self.render("build-schedule-%s.html" % type, type=type, build=build) + + @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) + + +class BuildFilterHandler(BaseHandler): + def get(self): + builders = self.pakfire.builders.get_all() + + self.render("build-filter.html", builders=builders) + + +class DocsIndexHandler(BaseHandler): + def get(self): + self.render("docs-index.html") + + +class DocsBuildsHandler(BaseHandler): + def get(self): + self.render("docs-build.html") + + +class DocsUsersHandler(BaseHandler): + def get(self): + self.render("docs-users.html") + + +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) + + +class FileDetailHandler(BaseHandler): + def get(self, uuid): + pkg, file = self.pakfire.packages.get_with_file_by_uuid(uuid) + + if not file: + raise tornado.web.HTTPError(404, "File not found") + + self.render("file-detail.html", pkg=pkg, file=file) + + +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.arches = self.pakfire.builders.get_all_arches() + 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, + 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) + slogan = self.get_argument("slogan", distro.slogan) + arches = self.get_argument("arches", distro.arches) + sources = self.get_argument("sources", distro.sources) + + distro.set("name", name) + distro.set("vendor", vendor) + distro.set("slogan", slogan) + distro.set("arches", arches) + distro.set("sources", sources) + + self.redirect("/distribution/%s" % distro.sname) + + +class LogHandler(BaseHandler): + def get(self): + self.render("log.html", log=self.pakfire.log) + + +class RepositoryDetailHandler(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) + + self.render("repository-detail.html", distro=distro, repo=repo) + + +class RepoActionHandler(BaseHandler): + @tornado.web.authenticated + def post(self, type): + assert type in ("run", "remove") + + action_id = self.get_argument("id") + + action = self.pakfire.repos.get_action_by_id(action_id) + if not action: + raise tornado.web.HTTPError(400) + + if type == "run": + action.run(self.current_user) + + elif type == "remove": + action.delete(self.current_user) diff --git a/web/handlers_auth.py b/web/handlers_auth.py new file mode 100644 index 0000000..d1d3193 --- /dev/null +++ b/web/handlers_auth.py @@ -0,0 +1,113 @@ +#!/usr/bin/python + +import tornado.web + +from handlers_base import * + +class LoginHandler(BaseHandler): + def get(self): + self.render("login.html", failed=False) + + def post(self): + name = self.get_argument("name", None) + passphrase = self.get_argument("pass", None) + + user = self.pakfire.users.auth(name, passphrase) + + if user: + # Set a cookie and update the current user. + self.set_secure_cookie("user", user.name) + self._current_user = user + + # If there is "next" given, we redirect the user accordingly. + # Otherwise we show a nice welcome message and tell the user, that + # the login was successful. + next = self.get_argument("next", None) + + if next: + self.redirect(next) + else: + self.render("login-successful.html", user=user) + + else: + # If the login failed we return an error message. + self.render("login.html", failed=True) + + +class RegisterHandler(BaseHandler): + def get(self): + self.render("register.html") + + def post(self): + _ = self.locale.translate + msgs = [] + + # Read all information from the request. + name = self.get_argument("name", None) + email = self.get_argument("email", None) + realname = self.get_argument("realname", None) + pass1 = self.get_argument("pass1", None) + pass2 = self.get_argument("pass2", None) + + if not name: + msgs.append(_("No username provided.")) + elif self.pakfire.users.name_is_used(name): + msgs.append(_("The given username is already taken.")) + + if not email: + msgs.append(_("No email address provided.")) + elif not "@" in email: + msgs.append(_("Email address is invalid.")) + elif self.pakfire.users.email_is_used(email): + msgs.append(_("The given email address is already used for another account.")) + + # 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.")) + + if msgs: + self.render("register-fail.html", messages=msgs) + return + + # All provided data seems okay. + # Register the new user to the database. + user = self.pakfire.users.register(name, pass1, email, realname, + self.locale.code) + + self.render("register-success.html", user=user) + + +class ActivationHandler(BaseHandler): + def get(self, _user, code): + user = self.pakfire.users.get_by_name(_user) + if not user: + raise tornado.web.HTTPError(404) + + # 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 + + self.render("register-activation-success.html", user=user) + return + + # Otherwise, show an error message. + self.render("register-activation-fail.html") + + +class LogoutHandler(BaseHandler): + @tornado.web.authenticated + def get(self): + # Remove the cookie, that identifies the user. + self.clear_cookie("user") + del self._current_user + + # Show a message to the user. + self.render("logout.html") diff --git a/web/handlers_base.py b/web/handlers_base.py new file mode 100644 index 0000000..bccdefa --- /dev/null +++ b/web/handlers_base.py @@ -0,0 +1,77 @@ +#!/usr/bin/python + +import httplib +import time +import tornado.locale +import tornado.web + +import backend +import backend.misc + +class BaseHandler(tornado.web.RequestHandler): + def get_current_user(self): + user = self.get_secure_cookie("user") + if not user: + return + + return self.pakfire.users.get_by_name(user) + + def get_user_locale(self): + DEFAULT_LOCALE = tornado.locale.get("en_US") + ALLOWED_LOCALES = \ + [tornado.locale.get(l) for l in tornado.locale.get_supported_locales(None)] + + # One can append "?locale=de" to mostly and URI on the site and + # another output that guessed. + locale = self.get_argument("locale", None) + if locale: + locale = tornado.locale.get(locale) + for locale in ALLOWED_LOCALES: + return locale + + # Get the locale from the user settings. + if self.current_user and self.current_user.locale: + locale = tornado.locale.get(self.current_user.locale) + if locale in ALLOWED_LOCALES: + return locale + + # If no locale was provided we guess what the browser sends us + locale = self.get_browser_locale() + if locale in ALLOWED_LOCALES: + return locale + + # If no one of the cases above worked we use our default locale + return DEFAULT_LOCALE + + @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"), + } + + def render(self, *args, **kwargs): + kwargs.update(self.render_args) + tornado.web.RequestHandler.render(self, *args, **kwargs) + + def render_string(self, *args, **kwargs): + 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) + + @property + def pakfire(self): + return self.application.pakfire + diff --git a/web/handlers_builders.py b/web/handlers_builders.py new file mode 100644 index 0000000..de9bd76 --- /dev/null +++ b/web/handlers_builders.py @@ -0,0 +1,87 @@ +#!/usr/bin/python + +import backend + +from handlers_base import * + +class BuilderListHandler(BaseHandler): + def get(self): + builders = self.pakfire.builders.get_all() + + self.render("builder-list.html", builders=builders) + + +class BuilderDetailHandler(BaseHandler): + def get(self, hostname): + builder = self.pakfire.builders.get_by_name(hostname) + + self.render("builder-detail.html", builder=builder) + + +class BuilderNewHandler(BaseHandler): + def get(self): + self.render("builder-new.html") + + def post(self): + name = self.get_argument("name") + + # Create a new builder. + builder = backend.builders.Builder.new(self.pakfire, name) + + self.render("builder-pass.html", action="new", builder=builder) + + +class BuilderEditHandler(BaseHandler): + @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") + + self.render("builder-edit.html", builder=builder) + + @tornado.web.authenticated + def post(self, hostname): + builder = self.pakfire.builders.get_by_name(hostname) + if not builder: + raise tornado.web.HTTPError(404, "Builder not found") + + 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) + + # Save max_jobs. + max_jobs = self.get_argument("max_jobs", builder.max_jobs) + try: + max_jobs = int(max_jobs) + except TypeError: + max_jobs = 1 + + if not max_jobs in (1, 2, 3, 4, 5, 6, 7, 8,): + max_jobs = 1 + builder.max_jobs = max_jobs + + self.redirect("/builder/%s" % builder.hostname) + + +class BuilderRenewPassphraseHandler(BaseHandler): + def get(self, name): + builder = self.pakfire.builders.get_by_name(name) + + builder.regenerate_passphrase() + + self.render("builder-pass.html", action="update", builder=builder) + + +class BuilderDeleteHandler(BaseHandler): + def get(self, name): + builder = self.pakfire.builders.get_by_name(name) + + confirmed = self.get_argument("confirmed", None) + if confirmed: + builder.delete() + self.redirect("/builders") + return + + self.render("builder-delete.html", builder=builder) diff --git a/web/handlers_search.py b/web/handlers_search.py new file mode 100644 index 0000000..6c830e6 --- /dev/null +++ b/web/handlers_search.py @@ -0,0 +1,14 @@ +#!/usr/bin/python + +from handlers_base import * + +class SearchHandler(BaseHandler): + def get(self): + query = self.get_argument("q", "") + if not query: + self.render("search-form.html") + return + + pkgs = self.pakfire.packages.search(query) + + self.render("search-results.html", query=query, pkgs=pkgs) diff --git a/web/handlers_users.py b/web/handlers_users.py new file mode 100644 index 0000000..c45379b --- /dev/null +++ b/web/handlers_users.py @@ -0,0 +1,141 @@ +#!/usr/bin/python + +import tornado.locale +import tornado.web + +from handlers_base import * + +class UserHandler(BaseHandler): + @tornado.web.authenticated + def get(self, name=None): + user = self.current_user + + if name: + user = self.pakfire.users.get_by_name(name) + if not user: + raise tornado.web.HTTPError(404, "User does not exist: %s" % name) + + self.render("user-profile.html", user=user) + + +class UserDeleteHandler(BaseHandler): + @tornado.web.authenticated + def get(self, name): + user = self.pakfire.users.get_by_name(name) + if not user: + raise tornado.web.HTTPError(404) + + if not self.current_user == user and not self.current_user.is_admin(): + raise tornado.web.HTTPError(403) + + confirmed = self.get_argument("confirmed", None) + if confirmed: + user.delete() + + if self.current_user == user: + self.redirect("/logout") + else: + self.redirect("/users") + + self.render("user-delete.html", user=user) + + +class UserEditHandler(BaseHandler): + def prepare(self): + # Make list of all supported locales. + self.supported_locales = \ + [tornado.locale.get(l) for l in tornado.locale.get_supported_locales(None)] + + @tornado.web.authenticated + def get(self, name): + user = self.pakfire.users.get_by_name(name) + if not user: + raise tornado.web.HTTPError(404) + + if not self.current_user == user and not self.current_user.is_admin(): + raise tornado.web.HTTPError(403) + + self.render("user-profile-edit.html", user=user, + supported_locales=self.supported_locales) + + @tornado.web.authenticated + def post(self, name): + _ = self.locale.translate + + user = self.pakfire.users.get_by_name(name) + if not user: + raise tornado.web.HTTPError(404) + + email = self.get_argument("email", user.email) + realname = self.get_argument("realname", user.realname) + pass1 = self.get_argument("pass1", None) + pass2 = self.get_argument("pass2", None) + locale = self.get_argument("locale", "") + + # Only an admin can alter the state of a user. + if self.current_user.is_admin(): + state = self.get_argument("state", user.state) + else: + state = user.state + + # Collect error messages + msgs = [] + + if not email: + msgs.append(_("No email address provided.")) + elif not "@" in email: + msgs.append(_("Email address is invalid.")) + + # Check if the passphrase is okay. + if pass1 and not len(pass1) >= 8: + msgs.append(_("Password has less than 8 characters.")) + elif not pass1 == pass2: + msgs.append(_("Passwords do not match.")) + + # Check if locale is valid. + if locale and not locale in [l.code for l in self.supported_locales]: + msg.append(_("The choosen locale is invalid.")) + + if msgs: + self.render("user-profile-edit-fail.html", messages=msgs) + return + + # Everything is okay, we can save the new settings. + user.locale = locale + user.email = email + user.realname = realname + if pass1: + user.passphrase = pass1 + user.state = state + + if not user.activated: + self.render("user-profile-need-activation.html", user=user) + return + + self.redirect("/user/%s" % user.name) + + +class UsersHandler(BaseHandler): + @tornado.web.authenticated + def get(self): + admins, testers, users = [], [], [] + + for user in self.pakfire.users.get_all(): + if user.is_admin(): + admins.append(user) + elif user.is_tester(): + testers.append(user) + else: + users.append(user) + + self.render("user-list.html", admins=admins, testers=testers, + users=users) + + +class UsersCommentsHandler(BaseHandler): + @tornado.web.authenticated + def get(self): + comments = self.pakfire.packages.get_comments(limit=20) + + self.render("user-comments.html", comments=comments) + diff --git a/web/ui_modules.py b/web/ui_modules.py new file mode 100644 index 0000000..ca3d0f0 --- /dev/null +++ b/web/ui_modules.py @@ -0,0 +1,125 @@ + +import tornado.web + +from backend.constants import * + +class UIModule(tornado.web.UIModule): + @property + def pakfire(self): + return self.handler.application.pakfire + + +class PackageTableModule(UIModule): + def render(self, letter, packages): + return self.render_string("modules/package-table.html", + letter=letter, packages=packages) + + +class PackageTable2Module(UIModule): + def render(self, packages): + return self.render_string("modules/package-table-detail.html", + packages=packages) + + +class FilesTableModule(UIModule): + def render(self, files): + return self.render_string("modules/files-table.html", files=files) + + +class PackageFilesTableModule(UIModule): + def render(self, files): + return self.render_string("modules/package-files-table.html", files=files) + + +class BuildTableModule(UIModule): + def render(self, builds): + return self.render_string("modules/build-table.html", builds=builds) + + +class RepositoryTableModule(UIModule): + def render(self, distro, repos): + return self.render_string("modules/repository-table.html", + distro=distro, repos=repos) + + +class SourceTableModule(UIModule): + def render(self, distro, sources): + return self.render_string("modules/source-table.html", + distro=distro, sources=sources) + + +class CommentsTableModule(UIModule): + def render(self, comments, show_package=False, show_user=True): + pkgs, users = {}, {} + for comment in comments: + if show_package: + try: + pkg = pkgs[comment.pkg_id] + except KeyError: + pkg = pkgs[comment.pkg_id] = \ + self.pakfire.packages.get_by_id(comment.pkg_id) + + comment["pkg"] = pkg + + if show_user: + try: + user = users[comment.user_id] + except KeyError: + user = users[comment.user_id] = \ + self.pakfire.users.get_by_id(comment.user_id) + + comment["user"] = user + + return self.render_string("modules/comments-table.html", + comments=comments, show_package=show_package, show_user=show_user) + + +class BuildLogModule(UIModule): + # XXX deprecated + def render(self, messages): + _ = self.locale.translate + + for message in messages: + try: + msg = LOG2MSG[message.message] + message["message"] = _(msg) + except KeyError: + pass + + return self.render_string("modules/build-log.html", messages=messages) + + +class LogTableModule(UIModule): + def render(self, messages, links=["pkg",]): + for message in messages: + try: + message["message"] = LOG2MSG[message.message] + except KeyError: + pass + + if message.build_id: + message["build"] = self.pakfire.builds.get_by_id(message.build_id) + + elif message.pkg_id: + message["pkg"] = self.pakfire.packages.get_by_id(message.pkg_id) + + return self.render_string("modules/log-table.html", + messages=messages, links=links) + + +class UsersTableModule(UIModule): + def render(self, users): + return self.render_string("modules/user-table.html", users=users) + + +class BuildOffsetModule(UIModule): + def render(self): + return self.render_string("modules/build-offset.html") + + +class RepoActionsTableModule(UIModule): + def render(self, repo): + actions = repo.get_actions() + + return self.render_string("modules/repo-actions-table.html", + repo=repo, actions=actions) -- 2.39.5