diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..ec28e4479 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,28 @@ + + +Copyright (c) 2009, Friedrich Lindenberg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* The names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..f125015a1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include adhocracy/config/deployment.ini_tmpl +recursive-include adhocracy/public * +recursive-include adhocracy/templates * diff --git a/README.txt b/README.txt new file mode 100644 index 000000000..24171807d --- /dev/null +++ b/README.txt @@ -0,0 +1,34 @@ + +Adhocracy Liquid Democracy Implementation +========================================= + + + +Memcache +====================== + +Adhocracy uses Memcache to cache results such as rendered pages +and results from time-intensive computations. To use memcache, +uncomment the memcache config line in your .ini file and point it +to a running instance of memcache. + +If no memcache is configured or available, Adhocracy should still +function, but displaying motions that have a lot of votes can take +a long time. + +Installation and Setup +====================== + +Install ``adhocracy`` using easy_install:: + + easy_install adhocracy + +Make a config file as follows:: + + paster make-config adhocracy config.ini + +Tweak the config file as appropriate and then setup the application:: + + paster setup-app config.ini + +Then you are ready to go. diff --git a/adhoc.wsgi b/adhoc.wsgi new file mode 100644 index 000000000..ba3bf140b --- /dev/null +++ b/adhoc.wsgi @@ -0,0 +1,7 @@ +import os, sys +sys.path.append('/home/fl/web/adhocracy.cc') +os.environ['PYTHON_EGG_CACHE'] = '/home/fl/web/adhocracy.cc/data/eggs' + +from paste.deploy import loadapp + +application = loadapp('config:/home/fl/web/adhocracy.cc/deploy.ini') diff --git a/adhocracy.egg-info/PKG-INFO b/adhocracy.egg-info/PKG-INFO new file mode 100644 index 000000000..fc76ae75a --- /dev/null +++ b/adhocracy.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: adhocracy +Version: 0.1dev-r0 +Summary: Community decision process manager +Home-page: http://www.adhocracy.de +Author: Friedrich Lindenberg +Author-email: friedrich@adhocracy.de +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/adhocracy.egg-info/SOURCES.txt b/adhocracy.egg-info/SOURCES.txt new file mode 100644 index 000000000..417bba095 --- /dev/null +++ b/adhocracy.egg-info/SOURCES.txt @@ -0,0 +1,203 @@ +MANIFEST.in +README.txt +adhoc.wsgi +adhocracy_mate.tmproj +development.ini +ez_setup.py +setup.cfg +setup.py +test.ini +adhocracy/__init__.py +adhocracy/websetup.py +adhocracy.egg-info/PKG-INFO +adhocracy.egg-info/SOURCES.txt +adhocracy.egg-info/dependency_links.txt +adhocracy.egg-info/entry_points.txt +adhocracy.egg-info/not-zip-safe +adhocracy.egg-info/paster_plugins.txt +adhocracy.egg-info/requires.txt +adhocracy.egg-info/top_level.txt +adhocracy/config/__init__.py +adhocracy/config/deployment.ini_tmpl +adhocracy/config/environment.py +adhocracy/config/middleware.py +adhocracy/config/routing.py +adhocracy/contrib/__init__.py +adhocracy/contrib/markdown2.py +adhocracy/controllers/__init__.py +adhocracy/controllers/account.py +adhocracy/controllers/admin.py +adhocracy/controllers/category.py +adhocracy/controllers/comment.py +adhocracy/controllers/delegation.py +adhocracy/controllers/editor.py +adhocracy/controllers/error.py +adhocracy/controllers/event.py +adhocracy/controllers/instance.py +adhocracy/controllers/motion.py +adhocracy/controllers/page.py +adhocracy/controllers/root.py +adhocracy/controllers/search.py +adhocracy/controllers/vote.py +adhocracy/lib/__init__.py +adhocracy/lib/app_globals.py +adhocracy/lib/auth.py +adhocracy/lib/base.py +adhocracy/lib/browser.py +adhocracy/lib/commenting.py +adhocracy/lib/decision.py +adhocracy/lib/helpers.py +adhocracy/lib/i18n.py +adhocracy/lib/install.py +adhocracy/lib/modtoken.py +adhocracy/lib/social.py +adhocracy/lib/state.py +adhocracy/lib/version.py +adhocracy/lib/voting.py +adhocracy/lib/cache/__init__.py +adhocracy/lib/cache/invalidate.py +adhocracy/lib/cache/util.py +adhocracy/lib/event/__init__.py +adhocracy/lib/event/event.py +adhocracy/lib/event/formatting.py +adhocracy/lib/event/panel.py +adhocracy/lib/event/query.py +adhocracy/lib/event/stats.py +adhocracy/lib/event/types.py +adhocracy/lib/event/util.py +adhocracy/lib/instance/__init__.py +adhocracy/lib/instance/browser.py +adhocracy/lib/instance/discriminator.py +adhocracy/lib/search/__init__.py +adhocracy/lib/search/index.py +adhocracy/lib/search/motion.py +adhocracy/lib/text/__init__.py +adhocracy/lib/text/diff.py +adhocracy/model/__init__.py +adhocracy/model/annotation.py +adhocracy/model/category.py +adhocracy/model/comment.py +adhocracy/model/delegateable.py +adhocracy/model/delegation.py +adhocracy/model/filter.py +adhocracy/model/forms.py +adhocracy/model/group.py +adhocracy/model/hooks.py +adhocracy/model/instance.py +adhocracy/model/membership.py +adhocracy/model/meta.py +adhocracy/model/motion.py +adhocracy/model/permission.py +adhocracy/model/revision.py +adhocracy/model/transition.py +adhocracy/model/user.py +adhocracy/model/vote.py +adhocracy/public/.DS_Store +adhocracy/public/favicon.ico +adhocracy/public/robots.txt +adhocracy/public/img/.DS_Store +adhocracy/public/img/about.gif +adhocracy/public/img/adhocracy_button.png +adhocracy/public/img/adhocracy_large.png +adhocracy/public/img/header_bg.png +adhocracy/public/img/logo.png +adhocracy/public/img/page_bg.png +adhocracy/public/img/welcome_logo.png +adhocracy/public/img/icons/.DS_Store +adhocracy/public/img/icons/abstain.png +adhocracy/public/img/icons/alt_nay.png +adhocracy/public/img/icons/aye.png +adhocracy/public/img/icons/cancel.png +adhocracy/public/img/icons/category_create.png +adhocracy/public/img/icons/delete.png +adhocracy/public/img/icons/diff.png +adhocracy/public/img/icons/edit.png +adhocracy/public/img/icons/history.png +adhocracy/public/img/icons/manage.png +adhocracy/public/img/icons/motion_back.png +adhocracy/public/img/icons/motion_create.png +adhocracy/public/img/icons/motions.png +adhocracy/public/img/icons/nay.png +adhocracy/public/img/icons/save.png +adhocracy/public/img/icons/search.png +adhocracy/public/img/icons/settings.png +adhocracy/public/img/icons/votes.png +adhocracy/public/js/.DS_Store +adhocracy/public/js/adhocracy.js +adhocracy/public/js/jquery.autocomplete.min.js +adhocracy/public/style/.DS_Store +adhocracy/public/style/base.css +adhocracy/public/style/iphone.css +adhocracy/templates/components.html +adhocracy/templates/index.html +adhocracy/templates/sitemap.xml +adhocracy/templates/template.html +adhocracy/templates/template_browse.html +adhocracy/templates/template_doc.html +adhocracy/templates/template_entity.html +adhocracy/templates/account/login.html +adhocracy/templates/account/login_form.html +adhocracy/templates/account/manage.html +adhocracy/templates/account/motions.html +adhocracy/templates/account/parts.html +adhocracy/templates/account/register_form.html +adhocracy/templates/account/settings.html +adhocracy/templates/account/view.html +adhocracy/templates/account/votes.html +adhocracy/templates/admin/members.html +adhocracy/templates/admin/permissions.html +adhocracy/templates/category/create.html +adhocracy/templates/category/edit.html +adhocracy/templates/category/tree.html +adhocracy/templates/category/view.html +adhocracy/templates/comment/inline.html +adhocracy/templates/comment/listing.html +adhocracy/templates/comment/parts.html +adhocracy/templates/delegation/create.html +adhocracy/templates/delegation/graph.dot +adhocracy/templates/delegation/review.html +adhocracy/templates/error/http.html +adhocracy/templates/event/all.html +adhocracy/templates/event/panel.html +adhocracy/templates/instance/browse.html +adhocracy/templates/instance/create.html +adhocracy/templates/instance/edit.html +adhocracy/templates/instance/news.html +adhocracy/templates/motion/browse.html +adhocracy/templates/motion/create.html +adhocracy/templates/motion/diff.html +adhocracy/templates/motion/edit.html +adhocracy/templates/motion/history.html +adhocracy/templates/motion/parts.html +adhocracy/templates/motion/release.html +adhocracy/templates/motion/view.html +adhocracy/templates/motion/votes.html +adhocracy/templates/search/results.html +adhocracy/templates/static/about.html +adhocracy/templates/static/faq.html +adhocracy/templates/static/imprint.html +adhocracy/templates/static/privacy.html +adhocracy/templates/vote/browse.html +adhocracy/tests/__init__.py +adhocracy/tests/functional/__init__.py +adhocracy/tests/functional/test_account.py +adhocracy/tests/functional/test_admin.py +adhocracy/tests/functional/test_category.py +adhocracy/tests/functional/test_comment.py +adhocracy/tests/functional/test_delegation.py +adhocracy/tests/functional/test_editor.py +adhocracy/tests/functional/test_event.py +adhocracy/tests/functional/test_instance.py +adhocracy/tests/functional/test_motion.py +adhocracy/tests/functional/test_root.py +adhocracy/tests/functional/test_search.py +adhocracy/tests/functional/test_vote.py +adhocracy/tests/lib/__init__.py +adhocracy/tests/lib/test_event.py +adhocracy/tests/model/__init__.py +adhocracy/tests/model/test_user.py +docs/index.txt +graphics/BONSAI_TREES_Ligustrum.psd +graphics/button.psd +graphics/gradient.psd +graphics/green_top.psd \ No newline at end of file diff --git a/adhocracy.egg-info/dependency_links.txt b/adhocracy.egg-info/dependency_links.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/adhocracy.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/adhocracy.egg-info/entry_points.txt b/adhocracy.egg-info/entry_points.txt new file mode 100644 index 000000000..62b13dfc1 --- /dev/null +++ b/adhocracy.egg-info/entry_points.txt @@ -0,0 +1,7 @@ + + [paste.app_factory] + main = adhocracy.config.middleware:make_app + + [paste.app_install] + main = pylons.util:PylonsInstaller + \ No newline at end of file diff --git a/adhocracy.egg-info/not-zip-safe b/adhocracy.egg-info/not-zip-safe new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/adhocracy.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/adhocracy.egg-info/paster_plugins.txt b/adhocracy.egg-info/paster_plugins.txt new file mode 100644 index 000000000..c24c7feaa --- /dev/null +++ b/adhocracy.egg-info/paster_plugins.txt @@ -0,0 +1,2 @@ +PasteScript +Pylons diff --git a/adhocracy.egg-info/requires.txt b/adhocracy.egg-info/requires.txt new file mode 100644 index 000000000..5c1ee6dfa --- /dev/null +++ b/adhocracy.egg-info/requires.txt @@ -0,0 +1,9 @@ +Pylons>=0.9.7 +SQLAlchemy>=0.5 +FormEncode>=1.2.2 +repoze.who>=1.0.15 +repoze.what>=1.0.8 +repoze.who.plugins.sa>=1.0rc2 +repoze.what-pylons>=1.0 +repoze.what.plugins.sql>=1.0rc1 +repoze.who-friendlyform>=1.0b3 \ No newline at end of file diff --git a/adhocracy.egg-info/top_level.txt b/adhocracy.egg-info/top_level.txt new file mode 100644 index 000000000..7d9a39256 --- /dev/null +++ b/adhocracy.egg-info/top_level.txt @@ -0,0 +1 @@ +adhocracy diff --git a/adhocracy/__init__.py b/adhocracy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adhocracy/config/__init__.py b/adhocracy/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adhocracy/config/deployment.ini_tmpl b/adhocracy/config/deployment.ini_tmpl new file mode 100644 index 000000000..0d8f12156 --- /dev/null +++ b/adhocracy/config/deployment.ini_tmpl @@ -0,0 +1,63 @@ +# +# adhocracy - Pylons configuration +# +# The %(here)s variable will be replaced with the parent directory of this file +# +[DEFAULT] +debug = true +email_to = you@yourdomain.com +smtp_server = localhost +error_email_from = paste@localhost + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 5000 + +[app:main] +use = egg:adhocracy +full_stack = true +static_files = true + +cache_dir = %(here)s/data +beaker.session.key = adhocracy +beaker.session.secret = ${app_instance_secret} +app_instance_uuid = ${app_instance_uuid} + +# If you'd like to fine-tune the individual locations of the cache data dirs +# for the Cache data, or the Session saves, un-comment the desired settings +# here: +#beaker.cache.data_dir = %(here)s/data/cache +#beaker.session.data_dir = %(here)s/data/sessions + +# SQLAlchemy database URL +sqlalchemy.url = sqlite:///production.db + +# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* +# Debug mode will enable the interactive debugging tool, allowing ANYONE to +# execute malicious code after an exception is raised. +set debug = false + + +# Logging configuration +[loggers] +keys = root + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s diff --git a/adhocracy/config/environment.py b/adhocracy/config/environment.py new file mode 100644 index 000000000..f838bb37d --- /dev/null +++ b/adhocracy/config/environment.py @@ -0,0 +1,45 @@ +"""Pylons environment configuration""" +import os + +from mako.lookup import TemplateLookup +from pylons import config +from pylons.error import handle_mako_error +from sqlalchemy import engine_from_config + +import adhocracy.lib.app_globals as app_globals +import adhocracy.lib.helpers +from adhocracy.config.routing import make_map +from adhocracy.model import init_model + +def load_environment(global_conf, app_conf): + """Configure the Pylons environment via the ``pylons.config`` + object + """ + # Pylons paths + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + paths = dict(root=root, + controllers=os.path.join(root, 'controllers'), + static_files=os.path.join(root, 'public'), + templates=[os.path.join(root, 'templates')]) + + # Initialize config with the basic options + config.init_app(global_conf, app_conf, package='adhocracy', paths=paths) + + config['routes.map'] = make_map() + config['pylons.app_globals'] = app_globals.Globals() + config['pylons.h'] = adhocracy.lib.helpers + + # Create the Mako TemplateLookup, with the default auto-escaping + config['pylons.app_globals'].mako_lookup = TemplateLookup( + directories=paths['templates'], + error_handler=handle_mako_error, + module_directory=os.path.join(app_conf['cache_dir'], 'templates'), + input_encoding='utf-8', default_filters=['escape'], + imports=['from webhelpers.html import escape']) + + # Setup the SQLAlchemy database engine + engine = engine_from_config(config, 'sqlalchemy.') + init_model(engine) + + # CONFIGURATION OPTIONS HERE (note: all config options will override + # any Pylons config options) diff --git a/adhocracy/config/middleware.py b/adhocracy/config/middleware.py new file mode 100644 index 000000000..d295ebeee --- /dev/null +++ b/adhocracy/config/middleware.py @@ -0,0 +1,77 @@ +"""Pylons middleware initialization""" +from beaker.middleware import CacheMiddleware, SessionMiddleware +from paste.cascade import Cascade +from paste.registry import RegistryManager +from paste.urlparser import StaticURLParser +from paste.deploy.converters import asbool +from pylons import config +from pylons.middleware import ErrorHandler, StatusCodeRedirect +from pylons.wsgiapp import PylonsApp +from routes.middleware import RoutesMiddleware +from paste.debug.profile import make_profile_middleware + + +from adhocracy.lib.authentication import setup_auth +from adhocracy.lib.instance import setup_discriminator +from adhocracy.config.environment import load_environment + +def make_app(global_conf, full_stack=True, static_files=True, **app_conf): + """Create a Pylons WSGI application and return it + + ``global_conf`` + The inherited configuration for this application. Normally from + the [DEFAULT] section of the Paste ini file. + + ``full_stack`` + Whether this application provides a full WSGI stack (by default, + meaning it handles its own exceptions and errors). Disable + full_stack when this application is "managed" by another WSGI + middleware. + + ``static_files`` + Whether this application serves its own static files; disable + when another web server is responsible for serving them. + + ``app_conf`` + The application's local configuration. Normally specified in + the [app:] section of the Paste ini file (where + defaults to main). + + """ + # Configure the Pylons environment + load_environment(global_conf, app_conf) + + # The Pylons WSGI app + app = PylonsApp() + + # Routing/Session/Cache Middleware + app = RoutesMiddleware(app, config['routes.map']) + app = SessionMiddleware(app, config) + app = CacheMiddleware(app, config) + + #app = make_profile_middleware(app, config) + + # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) + app = setup_auth(app, config) + app = setup_discriminator(app, config) + + if asbool(full_stack): + # Handle Python exceptions + app = ErrorHandler(app, global_conf, **config['pylons.errorware']) + + # Display error documents for 401, 403, 404 status codes (and + # 500 when debug is disabled) + if asbool(config['debug']): + app = StatusCodeRedirect(app) + else: + app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) + + # Establish the Registry for this application + app = RegistryManager(app) + + if asbool(static_files): + # Serve static files + static_app = StaticURLParser(config['pylons.paths']['static_files']) + app = Cascade([static_app, app]) + + return app diff --git a/adhocracy/config/routing.py b/adhocracy/config/routing.py new file mode 100644 index 000000000..c522a6958 --- /dev/null +++ b/adhocracy/config/routing.py @@ -0,0 +1,98 @@ +"""Routes configuration + +The more specific and detailed routes should be defined first so they +may take precedent over the more generic routes. For more information +refer to the routes manual at http://routes.groovie.org/docs/ +""" +from pylons import config +from routes import Mapper + +def make_map(): + """Create, configure and return the routes Mapper""" + map = Mapper(directory=config['pylons.paths']['controllers'], + always_scan=config['debug']) + map.minimization = False + + # The ErrorController route (handles 404/500 error pages); it should + # likely stay at the top, ensuring it can always be resolved + map.connect('/error/{action}', controller='error') + map.connect('/error/{action}/{id}', controller='error') + + # CUSTOM ROUTES HERE + map.connect('/', controller='root', action='index') + map.connect('/register', controller='user', action='create') + map.connect('/login', controller='user', action='login') + map.connect('/logout', controller='user', action='logout') + + map.connect('/users', controller='user', action='index') + map.connect('/user/post_logout', controller='user', action='post_logout') + map.connect('/user/post_login', controller='user', action='post_login') + map.connect('/user/perform_login', controller='user', action='perform_login') + map.connect('/user/reset/{id}', controller='user', action='reset_code') + map.connect('/user/reset', controller='user', action='reset') + map.connect('/user/complete', controller='user', action='autocomplete') + map.connect('/user/edit/{id}', controller='user', action='edit') + map.connect('/user/{id}/votes', controller='user', action='votes') + map.connect('/user/{id}/delegations', controller='user', action='delegations') + map.connect('/user/{id}/motions', controller='user', action='motions') + map.connect('/user/{id}/manage', controller='user', action='manage') + map.connect('/user/{id}.{format}', controller='user', action='view') + map.connect('/user/{id}', controller='user', action='view', format='html') + + map.connect('/issue/create', controller='issue', action='create') + map.connect('/issue/{action}/{id}', controller='issue') + map.connect('/issue/{id}.{format}', controller='issue', action='view') + map.connect('/issue/{id}', controller='issue', action='view', format='html') + + map.connect('/motions', controller='motion', action='index') + map.connect('/motion/create', controller='motion', action='create') + map.connect('/motion/{id}/votes', controller='motion', action='votes') + map.connect('/motion/{id}/begin_poll', controller='motion', action='begin_poll') + map.connect('/motion/{id}/end_poll', controller='motion', action='end_poll') + map.connect('/motion/{action}/{id}', controller='motion') + map.connect('/motion/{id}.{format}', controller='motion', action='view') + map.connect('/motion/{id}', controller='motion', action='view', format='html') + + map.connect('/comment/create', controller='comment', action='create') + map.connect('/comment/edit/{id}', controller='comment', action='edit') + map.connect('/comment/delete/{id}', controller='comment', action='delete') + map.connect('/comment/{id}/history', controller='comment', action='history') + map.connect('/comment/{id}/revert', controller='comment', action='revert') + map.connect('/comment/r/{id}', controller='comment', action='redirect') + map.connect('/comment/{id}', controller='comment', action='view') + + map.connect('/karma/give', controller='karma', action='give', format='html') + map.connect('/karma/give.json', controller='karma', action='give', format='json') + + map.connect('/category/create', controller='category', action='create') + map.connect('/category/{action}/{id}', controller='category') + map.connect('/category/{id}.{format}', controller='category', action='view') + map.connect('/category/{id}', controller='category', action='view', format='html') + + map.connect('/delegation/create', controller='delegation', action='create') + map.connect('/delegation/graph.dot', controller='delegation', action='graph') + map.connect('/delegation/revoke/{id}', controller='delegation', action='revoke') + map.connect('/delegation/user/{id}', controller='delegation', action='user') + map.connect('/delegation/{id}', controller='delegation', action='review') + + map.connect('/d/{id}', controller='root', action='dispatch_delegateable') + map.connect('/sitemap.xml', controller='root', action='sitemap_xml') + map.connect('/feed.rss', controller='root', action='index', format='rss') + + map.connect('/search', controller='search', action='query') + + map.connect('/adhocracies', controller='instance', action='index') + map.connect('/instance/create', controller='instance', action='create') + map.connect('/instance/{key}/join', controller='instance', action='join') + map.connect('/instance/{key}/leave', controller='instance', action='leave') + map.connect('/instance/logo/{key}.png', controller='instance', action='logo') + map.connect('/instance/{action}/{key}', controller='instance') + map.connect('/instance/{key}.rss', controller='instance', action='view', format='rss') + map.connect('/instance/{key}', controller='instance', action='view', format='html') + + map.connect('/page/{page_name}.html', controller='page', action='serve') + + map.connect('/{controller}/{action}') + map.connect('/{controller}/{action}/{id}') + + return map diff --git a/adhocracy/contrib/__init__.py b/adhocracy/contrib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adhocracy/contrib/markdown2.py b/adhocracy/contrib/markdown2.py new file mode 100644 index 000000000..d72f414e3 --- /dev/null +++ b/adhocracy/contrib/markdown2.py @@ -0,0 +1,1877 @@ +#!/usr/bin/env python +# Copyright (c) 2007-2008 ActiveState Corp. +# License: MIT (http://www.opensource.org/licenses/mit-license.php) + +r"""A fast and complete Python implementation of Markdown. + +[from http://daringfireball.net/projects/markdown/] +> Markdown is a text-to-HTML filter; it translates an easy-to-read / +> easy-to-write structured text format into HTML. Markdown's text +> format is most similar to that of plain text email, and supports +> features such as headers, *emphasis*, code blocks, blockquotes, and +> links. +> +> Markdown's syntax is designed not as a generic markup language, but +> specifically to serve as a front-end to (X)HTML. You can use span-level +> HTML tags anywhere in a Markdown document, and you can use block level +> HTML tags (like
and as well). + +Module usage: + + >>> import markdown2 + >>> markdown2.markdown("*boo!*") # or use `html = markdown_path(PATH)` + u'

boo!

\n' + + >>> markdowner = Markdown() + >>> markdowner.convert("*boo!*") + u'

boo!

\n' + >>> markdowner.convert("**boom!**") + u'

boom!

\n' + +This implementation of Markdown implements the full "core" syntax plus a +number of extras (e.g., code syntax coloring, footnotes) as described on +. +""" + +cmdln_desc = """A fast and complete Python implementation of Markdown, a +text-to-HTML conversion tool for web writers. +""" + +# Dev Notes: +# - There is already a Python markdown processor +# (http://www.freewisdom.org/projects/python-markdown/). +# - Python's regex syntax doesn't have '\z', so I'm using '\Z'. I'm +# not yet sure if there implications with this. Compare 'pydoc sre' +# and 'perldoc perlre'. + +__version_info__ = (1, 0, 1, 13) # first three nums match Markdown.pl +__version__ = '1.0.1.13' +__author__ = "Trent Mick" + +import os +import sys +from pprint import pprint +import re +import logging +try: + from hashlib import md5 +except ImportError: + from md5 import md5 +import optparse +from random import random +import codecs + + + +#---- Python version compat + +if sys.version_info[:2] < (2,4): + from sets import Set as set + def reversed(sequence): + for i in sequence[::-1]: + yield i + def _unicode_decode(s, encoding, errors='xmlcharrefreplace'): + return unicode(s, encoding, errors) +else: + def _unicode_decode(s, encoding, errors='strict'): + return s.decode(encoding, errors) + + +#---- globals + +DEBUG = False +log = logging.getLogger("markdown") + +DEFAULT_TAB_WIDTH = 4 + +# Table of hash values for escaped characters: +def _escape_hash(s): + # Lame attempt to avoid possible collision with someone actually + # using the MD5 hexdigest of one of these chars in there text. + # Other ideas: random.random(), uuid.uuid() + #return md5(s).hexdigest() # Markdown.pl effectively does this. + return 'md5-'+md5(s).hexdigest() +g_escape_table = dict([(ch, _escape_hash(ch)) + for ch in '\\`*_{}[]()>#+-.!']) + + + +#---- exceptions + +class MarkdownError(Exception): + pass + + + +#---- public api + +def markdown_path(path, encoding="utf-8", + html4tags=False, tab_width=DEFAULT_TAB_WIDTH, + safe_mode=None, extras=None, link_patterns=None, + use_file_vars=False): + text = codecs.open(path, 'r', encoding).read() + return Markdown(html4tags=html4tags, tab_width=tab_width, + safe_mode=safe_mode, extras=extras, + link_patterns=link_patterns, + use_file_vars=use_file_vars).convert(text) + +def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH, + safe_mode=None, extras=None, link_patterns=None, + use_file_vars=False): + return Markdown(html4tags=html4tags, tab_width=tab_width, + safe_mode=safe_mode, extras=extras, + link_patterns=link_patterns, + use_file_vars=use_file_vars).convert(text) + +class Markdown(object): + # The dict of "extras" to enable in processing -- a mapping of + # extra name to argument for the extra. Most extras do not have an + # argument, in which case the value is None. + # + # This can be set via (a) subclassing and (b) the constructor + # "extras" argument. + extras = None + + urls = None + titles = None + html_blocks = None + html_spans = None + html_removed_text = "[HTML_REMOVED]" # for compat with markdown.py + + # Used to track when we're inside an ordered or unordered list + # (see _ProcessListItems() for details): + list_level = 0 + + _ws_only_line_re = re.compile(r"^[ \t]+$", re.M) + + def __init__(self, html4tags=False, tab_width=4, safe_mode=None, + extras=None, link_patterns=None, use_file_vars=False): + if html4tags: + self.empty_element_suffix = ">" + else: + self.empty_element_suffix = " />" + self.tab_width = tab_width + + # For compatibility with earlier markdown2.py and with + # markdown.py's safe_mode being a boolean, + # safe_mode == True -> "replace" + if safe_mode is True: + self.safe_mode = "replace" + else: + self.safe_mode = safe_mode + + if self.extras is None: + self.extras = {} + elif not isinstance(self.extras, dict): + self.extras = dict([(e, None) for e in self.extras]) + if extras: + if not isinstance(extras, dict): + extras = dict([(e, None) for e in extras]) + self.extras.update(extras) + assert isinstance(self.extras, dict) + self._instance_extras = self.extras.copy() + self.link_patterns = link_patterns + self.use_file_vars = use_file_vars + self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M) + + def reset(self): + self.urls = {} + self.titles = {} + self.html_blocks = {} + self.html_spans = {} + self.list_level = 0 + self.extras = self._instance_extras.copy() + if "footnotes" in self.extras: + self.footnotes = {} + self.footnote_ids = [] + + def convert(self, text): + """Convert the given text.""" + # Main function. The order in which other subs are called here is + # essential. Link and image substitutions need to happen before + # _EscapeSpecialChars(), so that any *'s or _'s in the + # and tags get encoded. + + # Clear the global hashes. If we don't clear these, you get conflicts + # from other articles when generating a page which contains more than + # one article (e.g. an index page that shows the N most recent + # articles): + self.reset() + + if not isinstance(text, unicode): + #TODO: perhaps shouldn't presume UTF-8 for string input? + text = unicode(text, 'utf-8') + + if self.use_file_vars: + # Look for emacs-style file variable hints. + emacs_vars = self._get_emacs_vars(text) + if "markdown-extras" in emacs_vars: + splitter = re.compile("[ ,]+") + for e in splitter.split(emacs_vars["markdown-extras"]): + if '=' in e: + ename, earg = e.split('=', 1) + try: + earg = int(earg) + except ValueError: + pass + else: + ename, earg = e, None + self.extras[ename] = earg + + # Standardize line endings: + text = re.sub("\r\n|\r", "\n", text) + + # Make sure $text ends with a couple of newlines: + text += "\n\n" + + # Convert all tabs to spaces. + text = self._detab(text) + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ \t]*\n+/ . + text = self._ws_only_line_re.sub("", text) + + if self.safe_mode: + text = self._hash_html_spans(text) + + # Turn block-level HTML blocks into hash entries + text = self._hash_html_blocks(text, raw=True) + + # Strip link definitions, store in hashes. + if "footnotes" in self.extras: + # Must do footnotes first because an unlucky footnote defn + # looks like a link defn: + # [^4]: this "looks like a link defn" + text = self._strip_footnote_definitions(text) + text = self._strip_link_definitions(text) + + text = self._run_block_gamut(text) + + text = self._unescape_special_chars(text) + + if "footnotes" in self.extras: + text = self._add_footnotes(text) + + if self.safe_mode: + text = self._unhash_html_spans(text) + + text += "\n" + return text + + _emacs_oneliner_vars_pat = re.compile(r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE) + # This regular expression is intended to match blocks like this: + # PREFIX Local Variables: SUFFIX + # PREFIX mode: Tcl SUFFIX + # PREFIX End: SUFFIX + # Some notes: + # - "[ \t]" is used instead of "\s" to specifically exclude newlines + # - "(\r\n|\n|\r)" is used instead of "$" because the sre engine does + # not like anything other than Unix-style line terminators. + _emacs_local_vars_pat = re.compile(r"""^ + (?P(?:[^\r\n|\n|\r])*?) + [\ \t]*Local\ Variables:[\ \t]* + (?P.*?)(?:\r\n|\n|\r) + (?P.*?\1End:) + """, re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE) + + def _get_emacs_vars(self, text): + """Return a dictionary of emacs-style local variables. + + Parsing is done loosely according to this spec (and according to + some in-practice deviations from this): + http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables + """ + emacs_vars = {} + SIZE = pow(2, 13) # 8kB + + # Search near the start for a '-*-'-style one-liner of variables. + head = text[:SIZE] + if "-*-" in head: + match = self._emacs_oneliner_vars_pat.search(head) + if match: + emacs_vars_str = match.group(1) + assert '\n' not in emacs_vars_str + emacs_var_strs = [s.strip() for s in emacs_vars_str.split(';') + if s.strip()] + if len(emacs_var_strs) == 1 and ':' not in emacs_var_strs[0]: + # While not in the spec, this form is allowed by emacs: + # -*- Tcl -*- + # where the implied "variable" is "mode". This form + # is only allowed if there are no other variables. + emacs_vars["mode"] = emacs_var_strs[0].strip() + else: + for emacs_var_str in emacs_var_strs: + try: + variable, value = emacs_var_str.strip().split(':', 1) + except ValueError: + log.debug("emacs variables error: malformed -*- " + "line: %r", emacs_var_str) + continue + # Lowercase the variable name because Emacs allows "Mode" + # or "mode" or "MoDe", etc. + emacs_vars[variable.lower()] = value.strip() + + tail = text[-SIZE:] + if "Local Variables" in tail: + match = self._emacs_local_vars_pat.search(tail) + if match: + prefix = match.group("prefix") + suffix = match.group("suffix") + lines = match.group("content").splitlines(0) + #print "prefix=%r, suffix=%r, content=%r, lines: %s"\ + # % (prefix, suffix, match.group("content"), lines) + + # Validate the Local Variables block: proper prefix and suffix + # usage. + for i, line in enumerate(lines): + if not line.startswith(prefix): + log.debug("emacs variables error: line '%s' " + "does not use proper prefix '%s'" + % (line, prefix)) + return {} + # Don't validate suffix on last line. Emacs doesn't care, + # neither should we. + if i != len(lines)-1 and not line.endswith(suffix): + log.debug("emacs variables error: line '%s' " + "does not use proper suffix '%s'" + % (line, suffix)) + return {} + + # Parse out one emacs var per line. + continued_for = None + for line in lines[:-1]: # no var on the last line ("PREFIX End:") + if prefix: line = line[len(prefix):] # strip prefix + if suffix: line = line[:-len(suffix)] # strip suffix + line = line.strip() + if continued_for: + variable = continued_for + if line.endswith('\\'): + line = line[:-1].rstrip() + else: + continued_for = None + emacs_vars[variable] += ' ' + line + else: + try: + variable, value = line.split(':', 1) + except ValueError: + log.debug("local variables error: missing colon " + "in local variables entry: '%s'" % line) + continue + # Do NOT lowercase the variable name, because Emacs only + # allows "mode" (and not "Mode", "MoDe", etc.) in this block. + value = value.strip() + if value.endswith('\\'): + value = value[:-1].rstrip() + continued_for = variable + else: + continued_for = None + emacs_vars[variable] = value + + # Unquote values. + for var, val in emacs_vars.items(): + if len(val) > 1 and (val.startswith('"') and val.endswith('"') + or val.startswith('"') and val.endswith('"')): + emacs_vars[var] = val[1:-1] + + return emacs_vars + + # Cribbed from a post by Bart Lateur: + # + _detab_re = re.compile(r'(.*?)\t', re.M) + def _detab_sub(self, match): + g1 = match.group(1) + return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width)) + def _detab(self, text): + r"""Remove (leading?) tabs from a file. + + >>> m = Markdown() + >>> m._detab("\tfoo") + ' foo' + >>> m._detab(" \tfoo") + ' foo' + >>> m._detab("\t foo") + ' foo' + >>> m._detab(" foo") + ' foo' + >>> m._detab(" foo\n\tbar\tblam") + ' foo\n bar blam' + """ + if '\t' not in text: + return text + return self._detab_re.subn(self._detab_sub, text)[0] + + _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del' + _strict_tag_block_re = re.compile(r""" + ( # save in \1 + ^ # start of line (with re.M) + <(%s) # start tag = \2 + \b # word break + (.*\n)*? # any number of lines, minimally matching + # the matching end tag + [ \t]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + ) + """ % _block_tags_a, + re.X | re.M) + + _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math' + _liberal_tag_block_re = re.compile(r""" + ( # save in \1 + ^ # start of line (with re.M) + <(%s) # start tag = \2 + \b # word break + (.*\n)*? # any number of lines, minimally matching + .* # the matching end tag + [ \t]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + ) + """ % _block_tags_b, + re.X | re.M) + + def _hash_html_block_sub(self, match, raw=False): + html = match.group(1) + if raw and self.safe_mode: + html = self._sanitize_html(html) + key = _hash_text(html) + self.html_blocks[key] = html + return "\n\n" + key + "\n\n" + + def _hash_html_blocks(self, text, raw=False): + """Hashify HTML blocks + + We only want to do this for block-level HTML tags, such as headers, + lists, and tables. That's because we still want to wrap

s around + "paragraphs" that are wrapped in non-block-level tags, such as anchors, + phrase emphasis, and spans. The list of tags we're looking for is + hard-coded. + + @param raw {boolean} indicates if these are raw HTML blocks in + the original source. It makes a difference in "safe" mode. + """ + if '<' not in text: + return text + + # Pass `raw` value into our calls to self._hash_html_block_sub. + hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw) + + # First, look for nested blocks, e.g.: + #

+ #
+ # tags for inner block must be indented. + #
+ #
+ # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `
` and stop at the first `
`. + text = self._strict_tag_block_re.sub(hash_html_block_sub, text) + + # Now match more liberally, simply from `\n` to `\n` + text = self._liberal_tag_block_re.sub(hash_html_block_sub, text) + + # Special case just for
. It was easier to make a special + # case than to make the other regex more complicated. + if "", start_idx) + 3 + except ValueError, ex: + break + + # Start position for next comment block search. + start = end_idx + + # Validate whitespace before comment. + if start_idx: + # - Up to `tab_width - 1` spaces before start_idx. + for i in range(self.tab_width - 1): + if text[start_idx - 1] != ' ': + break + start_idx -= 1 + if start_idx == 0: + break + # - Must be preceded by 2 newlines or hit the start of + # the document. + if start_idx == 0: + pass + elif start_idx == 1 and text[0] == '\n': + start_idx = 0 # to match minute detail of Markdown.pl regex + elif text[start_idx-2:start_idx] == '\n\n': + pass + else: + break + + # Validate whitespace after comment. + # - Any number of spaces and tabs. + while end_idx < len(text): + if text[end_idx] not in ' \t': + break + end_idx += 1 + # - Must be following by 2 newlines or hit end of text. + if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'): + continue + + # Escape and hash (must match `_hash_html_block_sub`). + html = text[start_idx:end_idx] + if raw and self.safe_mode: + html = self._sanitize_html(html) + key = _hash_text(html) + self.html_blocks[key] = html + text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:] + + if "xml" in self.extras: + # Treat XML processing instructions and namespaced one-liner + # tags as if they were block HTML tags. E.g., if standalone + # (i.e. are their own paragraph), the following do not get + # wrapped in a

tag: + # + # + # + _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width) + text = _xml_oneliner_re.sub(hash_html_block_sub, text) + + return text + + def _strip_link_definitions(self, text): + # Strips link definitions from text, stores the URLs and titles in + # hash references. + less_than_tab = self.tab_width - 1 + + # Link defs are in the form: + # [id]: url "optional title" + _link_def_re = re.compile(r""" + ^[ ]{0,%d}\[(.+)\]: # id = \1 + [ \t]* + \n? # maybe *one* newline + [ \t]* + ? # url = \2 + [ \t]* + (?: + \n? # maybe one newline + [ \t]* + (?<=\s) # lookbehind for whitespace + ['"(] + ([^\n]*) # title = \3 + ['")] + [ \t]* + )? # title is optional + (?:\n+|\Z) + """ % less_than_tab, re.X | re.M | re.U) + return _link_def_re.sub(self._extract_link_def_sub, text) + + def _extract_link_def_sub(self, match): + id, url, title = match.groups() + key = id.lower() # Link IDs are case-insensitive + self.urls[key] = self._encode_amps_and_angles(url) + if title: + self.titles[key] = title.replace('"', '"') + return "" + + def _extract_footnote_def_sub(self, match): + id, text = match.groups() + text = _dedent(text, skip_first_line=not text.startswith('\n')).strip() + normed_id = re.sub(r'\W', '-', id) + # Ensure footnote text ends with a couple newlines (for some + # block gamut matches). + self.footnotes[normed_id] = text + "\n\n" + return "" + + def _strip_footnote_definitions(self, text): + """A footnote definition looks like this: + + [^note-id]: Text of the note. + + May include one or more indented paragraphs. + + Where, + - The 'note-id' can be pretty much anything, though typically it + is the number of the footnote. + - The first paragraph may start on the next line, like so: + + [^note-id]: + Text of the note. + """ + less_than_tab = self.tab_width - 1 + footnote_def_re = re.compile(r''' + ^[ ]{0,%d}\[\^(.+)\]: # id = \1 + [ \t]* + ( # footnote text = \2 + # First line need not start with the spaces. + (?:\s*.*\n+) + (?: + (?:[ ]{%d} | \t) # Subsequent lines must be indented. + .*\n+ + )* + ) + # Lookahead for non-space at line-start, or end of doc. + (?:(?=^[ ]{0,%d}\S)|\Z) + ''' % (less_than_tab, self.tab_width, self.tab_width), + re.X | re.M) + return footnote_def_re.sub(self._extract_footnote_def_sub, text) + + + _hr_res = [ + re.compile(r"^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$", re.M), + re.compile(r"^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$", re.M), + re.compile(r"^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$", re.M), + ] + + def _run_block_gamut(self, text): + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + + text = self._do_headers(text) + + # Do Horizontal Rules: + hr = "\n tags around block-level tags. + text = self._hash_html_blocks(text) + + text = self._form_paragraphs(text) + + return text + + def _pyshell_block_sub(self, match): + lines = match.group(0).splitlines(0) + _dedentlines(lines) + indent = ' ' * self.tab_width + s = ('\n' # separate from possible cuddled paragraph + + indent + ('\n'+indent).join(lines) + + '\n\n') + return s + + def _prepare_pyshell_blocks(self, text): + """Ensure that Python interactive shell sessions are put in + code blocks -- even if not properly indented. + """ + if ">>>" not in text: + return text + + less_than_tab = self.tab_width - 1 + _pyshell_block_re = re.compile(r""" + ^([ ]{0,%d})>>>[ ].*\n # first line + ^(\1.*\S+.*\n)* # any number of subsequent lines + ^\n # ends with a blank line + """ % less_than_tab, re.M | re.X) + + return _pyshell_block_re.sub(self._pyshell_block_sub, text) + + def _run_span_gamut(self, text): + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + + text = self._do_code_spans(text) + + text = self._escape_special_chars(text) + + # Process anchor and image tags. + text = self._do_links(text) + + # Make links out of things like `` + # Must come after _do_links(), because you can use < and > + # delimiters in inline links like [this](). + text = self._do_auto_links(text) + + if "link-patterns" in self.extras: + text = self._do_link_patterns(text) + + text = self._encode_amps_and_angles(text) + + text = self._do_italics_and_bold(text) + + # Do hard breaks: + text = re.sub(r" {2,}\n", " + | + # auto-link (e.g., ) + <\w+[^>]*> + | + # comment + | + <\?.*?\?> # processing instruction + ) + """, re.X) + + def _escape_special_chars(self, text): + # Python markdown note: the HTML tokenization here differs from + # that in Markdown.pl, hence the behaviour for subtle cases can + # differ (I believe the tokenizer here does a better job because + # it isn't susceptible to unmatched '<' and '>' in HTML tags). + # Note, however, that '>' is not allowed in an auto-link URL + # here. + escaped = [] + is_html_markup = False + for token in self._sorta_html_tokenize_re.split(text): + if is_html_markup: + # Within tags/HTML-comments/auto-links, encode * and _ + # so they don't conflict with their use in Markdown for + # italics and strong. We're replacing each such + # character with its corresponding MD5 checksum value; + # this is likely overkill, but it should prevent us from + # colliding with the escape values by accident. + escaped.append(token.replace('*', g_escape_table['*']) + .replace('_', g_escape_table['_'])) + else: + escaped.append(self._encode_backslash_escapes(token)) + is_html_markup = not is_html_markup + return ''.join(escaped) + + def _hash_html_spans(self, text): + # Used for safe_mode. + + def _is_auto_link(s): + if ':' in s and self._auto_link_re.match(s): + return True + elif '@' in s and self._auto_email_link_re.match(s): + return True + return False + + tokens = [] + is_html_markup = False + for token in self._sorta_html_tokenize_re.split(text): + if is_html_markup and not _is_auto_link(token): + sanitized = self._sanitize_html(token) + key = _hash_text(sanitized) + self.html_spans[key] = sanitized + tokens.append(key) + else: + tokens.append(token) + is_html_markup = not is_html_markup + return ''.join(tokens) + + def _unhash_html_spans(self, text): + for key, sanitized in self.html_spans.items(): + text = text.replace(key, sanitized) + return text + + def _sanitize_html(self, s): + if self.safe_mode == "replace": + return self.html_removed_text + elif self.safe_mode == "escape": + replacements = [ + ('&', '&'), + ('<', '<'), + ('>', '>'), + ] + for before, after in replacements: + s = s.replace(before, after) + return s + else: + raise MarkdownError("invalid value for 'safe_mode': %r (must be " + "'escape' or 'replace')" % self.safe_mode) + + _tail_of_inline_link_re = re.compile(r''' + # Match tail of: [text](/url/) or [text](/url/ "title") + \( # literal paren + [ \t]* + (?P # \1 + <.*?> + | + .*? + ) + [ \t]* + ( # \2 + (['"]) # quote char = \3 + (?P.*?) + \3 # matching quote + )? # title is optional + \) + ''', re.X | re.S) + _tail_of_reference_link_re = re.compile(r''' + # Match tail of: [text][id] + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + \[ + (?P<id>.*?) + \] + ''', re.X | re.S) + + def _do_links(self, text): + """Turn Markdown link shortcuts into XHTML <a> and <img> tags. + + This is a combination of Markdown.pl's _DoAnchors() and + _DoImages(). They are done together because that simplified the + approach. It was necessary to use a different approach than + Markdown.pl because of the lack of atomic matching support in + Python's regex engine used in $g_nested_brackets. + """ + MAX_LINK_TEXT_SENTINEL = 3000 # markdown2 issue 24 + + # `anchor_allowed_pos` is used to support img links inside + # anchors, but not anchors inside anchors. An anchor's start + # pos must be `>= anchor_allowed_pos`. + anchor_allowed_pos = 0 + + curr_pos = 0 + while True: # Handle the next link. + # The next '[' is the start of: + # - an inline anchor: [text](url "title") + # - a reference anchor: [text][id] + # - an inline img: ![text](url "title") + # - a reference img: ![text][id] + # - a footnote ref: [^id] + # (Only if 'footnotes' extra enabled) + # - a footnote defn: [^id]: ... + # (Only if 'footnotes' extra enabled) These have already + # been stripped in _strip_footnote_definitions() so no + # need to watch for them. + # - a link definition: [id]: url "title" + # These have already been stripped in + # _strip_link_definitions() so no need to watch for them. + # - not markup: [...anything else... + try: + start_idx = text.index('[', curr_pos) + except ValueError: + break + text_length = len(text) + + # Find the matching closing ']'. + # Markdown.pl allows *matching* brackets in link text so we + # will here too. Markdown.pl *doesn't* currently allow + # matching brackets in img alt text -- we'll differ in that + # regard. + bracket_depth = 0 + for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL, + text_length)): + ch = text[p] + if ch == ']': + bracket_depth -= 1 + if bracket_depth < 0: + break + elif ch == '[': + bracket_depth += 1 + else: + # Closing bracket not found within sentinel length. + # This isn't markup. + curr_pos = start_idx + 1 + continue + link_text = text[start_idx+1:p] + + # Possibly a footnote ref? + if "footnotes" in self.extras and link_text.startswith("^"): + normed_id = re.sub(r'\W', '-', link_text[1:]) + if normed_id in self.footnotes: + self.footnote_ids.append(normed_id) + result = '<sup class="footnote-ref" id="fnref-%s">' \ + '<a href="#fn-%s">%s</a></sup>' \ + % (normed_id, normed_id, len(self.footnote_ids)) + text = text[:start_idx] + result + text[p+1:] + else: + # This id isn't defined, leave the markup alone. + curr_pos = p+1 + continue + + # Now determine what this is by the remainder. + p += 1 + if p == text_length: + return text + + # Inline anchor or img? + if text[p] == '(': # attempt at perf improvement + match = self._tail_of_inline_link_re.match(text, p) + if match: + # Handle an inline anchor or img. + is_img = start_idx > 0 and text[start_idx-1] == "!" + if is_img: + start_idx -= 1 + + url, title = match.group("url"), match.group("title") + if url and url[0] == '<': + url = url[1:-1] # '<url>' -> 'url' + # We've got to encode these to avoid conflicting + # with italics/bold. + url = url.replace('*', g_escape_table['*']) \ + .replace('_', g_escape_table['_']) + if title: + title_str = ' title="%s"' \ + % title.replace('*', g_escape_table['*']) \ + .replace('_', g_escape_table['_']) \ + .replace('"', '"') + else: + title_str = '' + if is_img: + result = '<img src="%s" alt="%s"%s%s' \ + % (url, link_text.replace('"', '"'), + title_str, self.empty_element_suffix) + curr_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + elif start_idx >= anchor_allowed_pos: + result_head = '<a href="%s"%s>' % (url, title_str) + result = '%s%s</a>' % (result_head, link_text) + # <img> allowed from curr_pos on, <a> from + # anchor_allowed_pos on. + curr_pos = start_idx + len(result_head) + anchor_allowed_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + else: + # Anchor not allowed here. + curr_pos = start_idx + 1 + continue + + # Reference anchor or img? + else: + match = self._tail_of_reference_link_re.match(text, p) + if match: + # Handle a reference-style anchor or img. + is_img = start_idx > 0 and text[start_idx-1] == "!" + if is_img: + start_idx -= 1 + link_id = match.group("id").lower() + if not link_id: + link_id = link_text.lower() # for links like [this][] + if link_id in self.urls: + url = self.urls[link_id] + # We've got to encode these to avoid conflicting + # with italics/bold. + url = url.replace('*', g_escape_table['*']) \ + .replace('_', g_escape_table['_']) + title = self.titles.get(link_id) + if title: + title = title.replace('*', g_escape_table['*']) \ + .replace('_', g_escape_table['_']) + title_str = ' title="%s"' % title + else: + title_str = '' + if is_img: + result = '<img src="%s" alt="%s"%s%s' \ + % (url, link_text.replace('"', '"'), + title_str, self.empty_element_suffix) + curr_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + elif start_idx >= anchor_allowed_pos: + result = '<a href="%s"%s>%s</a>' \ + % (url, title_str, link_text) + result_head = '<a href="%s"%s>' % (url, title_str) + result = '%s%s</a>' % (result_head, link_text) + # <img> allowed from curr_pos on, <a> from + # anchor_allowed_pos on. + curr_pos = start_idx + len(result_head) + anchor_allowed_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + else: + # Anchor not allowed here. + curr_pos = start_idx + 1 + else: + # This id isn't defined, leave the markup alone. + curr_pos = match.end() + continue + + # Otherwise, it isn't markup. + curr_pos = start_idx + 1 + + return text + + + _setext_h_re = re.compile(r'^(.+)[ \t]*\n(=+|-+)[ \t]*\n+', re.M) + def _setext_h_sub(self, match): + n = {"=": 1, "-": 2}[match.group(2)[0]] + demote_headers = self.extras.get("demote-headers") + if demote_headers: + n = min(n + demote_headers, 6) + return "<h%d>%s</h%d>\n\n" \ + % (n, self._run_span_gamut(match.group(1)), n) + + _atx_h_re = re.compile(r''' + ^(\#{1,6}) # \1 = string of #'s + [ \t]* + (.+?) # \2 = Header text + [ \t]* + (?<!\\) # ensure not an escaped trailing '#' + \#* # optional closing #'s (not counted) + \n+ + ''', re.X | re.M) + def _atx_h_sub(self, match): + n = len(match.group(1)) + demote_headers = self.extras.get("demote-headers") + if demote_headers: + n = min(n + demote_headers, 6) + return "<h%d>%s</h%d>\n\n" \ + % (n, self._run_span_gamut(match.group(2)), n) + + def _do_headers(self, text): + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + text = self._setext_h_re.sub(self._setext_h_sub, text) + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + text = self._atx_h_re.sub(self._atx_h_sub, text) + + return text + + + _marker_ul_chars = '*+-' + _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars + _marker_ul = '(?:[%s])' % _marker_ul_chars + _marker_ol = r'(?:\d+\.)' + + def _list_sub(self, match): + lst = match.group(1) + lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol" + result = self._process_list_items(lst) + if self.list_level: + return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type) + else: + return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type) + + def _do_lists(self, text): + # Form HTML ordered (numbered) and unordered (bulleted) lists. + + for marker_pat in (self._marker_ul, self._marker_ol): + # Re-usable pattern to match any entire ul or ol list: + less_than_tab = self.tab_width - 1 + whole_list = r''' + ( # \1 = whole list + ( # \2 + [ ]{0,%d} + (%s) # \3 = first list item marker + [ \t]+ + ) + (?:.+?) + ( # \4 + \Z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ \t]* + %s[ \t]+ + ) + ) + ) + ''' % (less_than_tab, marker_pat, marker_pat) + + # We use a different prefix before nested lists than top-level lists. + # See extended comment in _process_list_items(). + # + # Note: There's a bit of duplication here. My original implementation + # created a scalar regex pattern as the conditional result of the test on + # $g_list_level, and then only ran the $text =~ s{...}{...}egmx + # substitution once, using the scalar as the pattern. This worked, + # everywhere except when running under MT on my hosting account at Pair + # Networks. There, this caused all rebuilds to be killed by the reaper (or + # perhaps they crashed, but that seems incredibly unlikely given that the + # same script on the same server ran fine *except* under MT. I've spent + # more time trying to figure out why this is happening than I'd like to + # admit. My only guess, backed up by the fact that this workaround works, + # is that Perl optimizes the substition when it can figure out that the + # pattern will never change, and when this optimization isn't on, we run + # afoul of the reaper. Thus, the slightly redundant code to that uses two + # static s/// patterns rather than one conditional pattern. + + if self.list_level: + sub_list_re = re.compile("^"+whole_list, re.X | re.M | re.S) + text = sub_list_re.sub(self._list_sub, text) + else: + list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list, + re.X | re.M | re.S) + text = list_re.sub(self._list_sub, text) + + return text + + _list_item_re = re.compile(r''' + (\n)? # leading line = \1 + (^[ \t]*) # leading whitespace = \2 + (%s) [ \t]+ # list marker = \3 + ((?:.+?) # list item text = \4 + (\n{1,2})) # eols = \5 + (?= \n* (\Z | \2 (%s) [ \t]+)) + ''' % (_marker_any, _marker_any), + re.M | re.X | re.S) + + _last_li_endswith_two_eols = False + def _list_item_sub(self, match): + item = match.group(4) + leading_line = match.group(1) + leading_space = match.group(2) + if leading_line or "\n\n" in item or self._last_li_endswith_two_eols: + item = self._run_block_gamut(self._outdent(item)) + else: + # Recursion for sub-lists: + item = self._do_lists(self._outdent(item)) + if item.endswith('\n'): + item = item[:-1] + item = self._run_span_gamut(item) + self._last_li_endswith_two_eols = (len(match.group(5)) == 2) + return "<li>%s</li>\n" % item + + def _process_list_items(self, list_str): + # Process the contents of a single ordered or unordered list, + # splitting it into individual list items. + + # The $g_list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + self.list_level += 1 + self._last_li_endswith_two_eols = False + list_str = list_str.rstrip('\n') + '\n' + list_str = self._list_item_re.sub(self._list_item_sub, list_str) + self.list_level -= 1 + return list_str + + def _get_pygments_lexer(self, lexer_name): + try: + from pygments import lexers, util + except ImportError: + return None + try: + return lexers.get_lexer_by_name(lexer_name) + except util.ClassNotFound: + return None + + def _color_with_pygments(self, codeblock, lexer, **formatter_opts): + import pygments + import pygments.formatters + + class HtmlCodeFormatter(pygments.formatters.HtmlFormatter): + def _wrap_code(self, inner): + """A function for use in a Pygments Formatter which + wraps in <code> tags. + """ + yield 0, "<code>" + for tup in inner: + yield tup + yield 0, "</code>" + + def wrap(self, source, outfile): + """Return the source with a code, pre, and div.""" + return self._wrap_div(self._wrap_pre(self._wrap_code(source))) + + formatter = HtmlCodeFormatter(cssclass="codehilite", **formatter_opts) + return pygments.highlight(codeblock, lexer, formatter) + + def _code_block_sub(self, match): + codeblock = match.group(1) + codeblock = self._outdent(codeblock) + codeblock = self._detab(codeblock) + codeblock = codeblock.lstrip('\n') # trim leading newlines + codeblock = codeblock.rstrip() # trim trailing whitespace + + if "code-color" in self.extras and codeblock.startswith(":::"): + lexer_name, rest = codeblock.split('\n', 1) + lexer_name = lexer_name[3:].strip() + lexer = self._get_pygments_lexer(lexer_name) + codeblock = rest.lstrip("\n") # Remove lexer declaration line. + if lexer: + formatter_opts = self.extras['code-color'] or {} + colored = self._color_with_pygments(codeblock, lexer, + **formatter_opts) + return "\n\n%s\n\n" % colored + + codeblock = self._encode_code(codeblock) + return "\n\n<pre><code>%s\n</code></pre>\n\n" % codeblock + + def _do_code_blocks(self, text): + """Process Markdown `<pre><code>` blocks.""" + code_block_re = re.compile(r''' + (?:\n\n|\A) + ( # $1 = the code block -- one or more lines, starting with a space/tab + (?: + (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc + ''' % (self.tab_width, self.tab_width), + re.M | re.X) + + return code_block_re.sub(self._code_block_sub, text) + + + # Rules for a code span: + # - backslash escapes are not interpreted in a code span + # - to include one or or a run of more backticks the delimiters must + # be a longer run of backticks + # - cannot start or end a code span with a backtick; pad with a + # space and that space will be removed in the emitted HTML + # See `test/tm-cases/escapes.text` for a number of edge-case + # examples. + _code_span_re = re.compile(r''' + (?<!\\) + (`+) # \1 = Opening run of ` + (?!`) # See Note A test/tm-cases/escapes.text + (.+?) # \2 = The code block + (?<!`) + \1 # Matching closer + (?!`) + ''', re.X | re.S) + + def _code_span_sub(self, match): + c = match.group(2).strip(" \t") + c = self._encode_code(c) + return "<code>%s</code>" % c + + def _do_code_spans(self, text): + # * Backtick quotes are used for <code></code> spans. + # + # * You can use multiple backticks as the delimiters if you want to + # include literal backticks in the code span. So, this input: + # + # Just type ``foo `bar` baz`` at the prompt. + # + # Will translate to: + # + # <p>Just type <code>foo `bar` baz</code> at the prompt.</p> + # + # There's no arbitrary limit to the number of backticks you + # can use as delimters. If you need three consecutive backticks + # in your code, use four for delimiters, etc. + # + # * You can use spaces to get literal backticks at the edges: + # + # ... type `` `bar` `` ... + # + # Turns to: + # + # ... type <code>`bar`</code> ... + return self._code_span_re.sub(self._code_span_sub, text) + + def _encode_code(self, text): + """Encode/escape certain characters inside Markdown code runs. + The point is that in code, these characters are literals, + and lose their special Markdown meanings. + """ + replacements = [ + # Encode all ampersands; HTML entities are not + # entities within a Markdown code span. + ('&', '&'), + # Do the angle bracket song and dance: + ('<', '<'), + ('>', '>'), + # Now, escape characters that are magic in Markdown: + ('*', g_escape_table['*']), + ('_', g_escape_table['_']), + ('{', g_escape_table['{']), + ('}', g_escape_table['}']), + ('[', g_escape_table['[']), + (']', g_escape_table[']']), + ('\\', g_escape_table['\\']), + ] + for before, after in replacements: + text = text.replace(before, after) + return text + + _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S) + _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S) + _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S) + _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S) + def _do_italics_and_bold(self, text): + # <strong> must go first: + if "code-friendly" in self.extras: + text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text) + text = self._code_friendly_em_re.sub(r"<em>\1</em>", text) + else: + text = self._strong_re.sub(r"<strong>\2</strong>", text) + text = self._em_re.sub(r"<em>\2</em>", text) + return text + + + _block_quote_re = re.compile(r''' + ( # Wrap whole match in \1 + ( + ^[ \t]*>[ \t]? # '>' at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + ''', re.M | re.X) + _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M); + + _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S) + def _dedent_two_spaces_sub(self, match): + return re.sub(r'(?m)^ ', '', match.group(1)) + + def _block_quote_sub(self, match): + bq = match.group(1) + bq = self._bq_one_level_re.sub('', bq) # trim one level of quoting + bq = self._ws_only_line_re.sub('', bq) # trim whitespace-only lines + bq = self._run_block_gamut(bq) # recurse + + bq = re.sub('(?m)^', ' ', bq) + # These leading spaces screw with <pre> content, so we need to fix that: + bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq) + + return "<blockquote>\n%s\n</blockquote>\n\n" % bq + + def _do_block_quotes(self, text): + if '>' not in text: + return text + return self._block_quote_re.sub(self._block_quote_sub, text) + + def _form_paragraphs(self, text): + # Strip leading and trailing lines: + text = text.strip('\n') + + # Wrap <p> tags. + grafs = re.split(r"\n{2,}", text) + for i, graf in enumerate(grafs): + if graf in self.html_blocks: + # Unhashify HTML blocks + grafs[i] = self.html_blocks[graf] + else: + # Wrap <p> tags. + graf = self._run_span_gamut(graf) + grafs[i] = "<p>" + graf.lstrip(" \t") + "</p>" + + return "\n\n".join(grafs) + + def _add_footnotes(self, text): + if self.footnotes: + footer = [ + '<div class="footnotes">', + '<hr' + self.empty_element_suffix, + '<ol>', + ] + for i, id in enumerate(self.footnote_ids): + if i != 0: + footer.append('') + footer.append('<li id="fn-%s">' % id) + footer.append(self._run_block_gamut(self.footnotes[id])) + backlink = ('<a href="#fnref-%s" ' + 'class="footnoteBackLink" ' + 'title="Jump back to footnote %d in the text.">' + '↩</a>' % (id, i+1)) + if footer[-1].endswith("</p>"): + footer[-1] = footer[-1][:-len("</p>")] \ + + ' ' + backlink + "</p>" + else: + footer.append("\n<p>%s</p>" % backlink) + footer.append('</li>') + footer.append('</ol>') + footer.append('</div>') + return text + '\n\n' + '\n'.join(footer) + else: + return text + + # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + # http://bumppo.net/projects/amputator/ + _ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)') + _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I) + _naked_gt_re = re.compile(r'''(?<![a-z?!/'"-])>''', re.I) + + def _encode_amps_and_angles(self, text): + # Smart processing for ampersands and angle brackets that need + # to be encoded. + text = self._ampersand_re.sub('&', text) + + # Encode naked <'s + text = self._naked_lt_re.sub('<', text) + + # Encode naked >'s + # Note: Other markdown implementations (e.g. Markdown.pl, PHP + # Markdown) don't do this. + text = self._naked_gt_re.sub('>', text) + return text + + def _encode_backslash_escapes(self, text): + for ch, escape in g_escape_table.items(): + text = text.replace("\\"+ch, escape) + return text + + _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I) + def _auto_link_sub(self, match): + g1 = match.group(1) + return '<a href="%s">%s</a>' % (g1, g1) + + _auto_email_link_re = re.compile(r""" + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-\w]+(\.[-\w]+)*\.[a-z]+ + ) + > + """, re.I | re.X | re.U) + def _auto_email_link_sub(self, match): + return self._encode_email_address( + self._unescape_special_chars(match.group(1))) + + def _do_auto_links(self, text): + text = self._auto_link_re.sub(self._auto_link_sub, text) + text = self._auto_email_link_re.sub(self._auto_email_link_sub, text) + return text + + def _encode_email_address(self, addr): + # Input: an email address, e.g. "foo@example.com" + # + # Output: the email address as a mailto link, with each character + # of the address encoded as either a decimal or hex entity, in + # the hopes of foiling most address harvesting spam bots. E.g.: + # + # <a href="mailto:foo@e + # xample.com">foo + # @example.com</a> + # + # Based on a filter by Matthew Wickline, posted to the BBEdit-Talk + # mailing list: <http://tinyurl.com/yu7ue> + chars = [_xml_encode_email_char_at_random(ch) + for ch in "mailto:" + addr] + # Strip the mailto: from the visible part. + addr = '<a href="%s">%s</a>' \ + % (''.join(chars), ''.join(chars[7:])) + return addr + + def _do_link_patterns(self, text): + """Caveat emptor: there isn't much guarding against link + patterns being formed inside other standard Markdown links, e.g. + inside a [link def][like this]. + + Dev Notes: *Could* consider prefixing regexes with a negative + lookbehind assertion to attempt to guard against this. + """ + link_from_hash = {} + for regex, repl in self.link_patterns: + replacements = [] + for match in regex.finditer(text): + if hasattr(repl, "__call__"): + href = repl(match) + else: + href = match.expand(repl) + replacements.append((match.span(), href)) + for (start, end), href in reversed(replacements): + escaped_href = ( + href.replace('"', '"') # b/c of attr quote + # To avoid markdown <em> and <strong>: + .replace('*', g_escape_table['*']) + .replace('_', g_escape_table['_'])) + link = '<a href="%s">%s</a>' % (escaped_href, text[start:end]) + hash = md5(link).hexdigest() + link_from_hash[hash] = link + text = text[:start] + hash + text[end:] + for hash, link in link_from_hash.items(): + text = text.replace(hash, link) + return text + + def _unescape_special_chars(self, text): + # Swap back in all the special characters we've hidden. + for ch, hash in g_escape_table.items(): + text = text.replace(hash, ch) + return text + + def _outdent(self, text): + # Remove one level of line-leading tabs or spaces + return self._outdent_re.sub('', text) + + +class MarkdownWithExtras(Markdown): + """A markdowner class that enables most extras: + + - footnotes + - code-color (only has effect if 'pygments' Python module on path) + + These are not included: + - pyshell (specific to Python-related documenting) + - code-friendly (because it *disables* part of the syntax) + - link-patterns (because you need to specify some actual + link-patterns anyway) + """ + extras = ["footnotes", "code-color"] + + +#---- internal support functions + +# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549 +def _curry(*args, **kwargs): + function, args = args[0], args[1:] + def result(*rest, **kwrest): + combined = kwargs.copy() + combined.update(kwrest) + return function(*args + rest, **combined) + return result + +# Recipe: regex_from_encoded_pattern (1.0) +def _regex_from_encoded_pattern(s): + """'foo' -> re.compile(re.escape('foo')) + '/foo/' -> re.compile('foo') + '/foo/i' -> re.compile('foo', re.I) + """ + if s.startswith('/') and s.rfind('/') != 0: + # Parse it: /PATTERN/FLAGS + idx = s.rfind('/') + pattern, flags_str = s[1:idx], s[idx+1:] + flag_from_char = { + "i": re.IGNORECASE, + "l": re.LOCALE, + "s": re.DOTALL, + "m": re.MULTILINE, + "u": re.UNICODE, + } + flags = 0 + for char in flags_str: + try: + flags |= flag_from_char[char] + except KeyError: + raise ValueError("unsupported regex flag: '%s' in '%s' " + "(must be one of '%s')" + % (char, s, ''.join(flag_from_char.keys()))) + return re.compile(s[1:idx], flags) + else: # not an encoded regex + return re.compile(re.escape(s)) + +# Recipe: dedent (0.1.2) +def _dedentlines(lines, tabsize=8, skip_first_line=False): + """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines + + "lines" is a list of lines to dedent. + "tabsize" is the tab width to use for indent width calculations. + "skip_first_line" is a boolean indicating if the first line should + be skipped for calculating the indent width and for dedenting. + This is sometimes useful for docstrings and similar. + + Same as dedent() except operates on a sequence of lines. Note: the + lines list is modified **in-place**. + """ + DEBUG = False + if DEBUG: + print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\ + % (tabsize, skip_first_line) + indents = [] + margin = None + for i, line in enumerate(lines): + if i == 0 and skip_first_line: continue + indent = 0 + for ch in line: + if ch == ' ': + indent += 1 + elif ch == '\t': + indent += tabsize - (indent % tabsize) + elif ch in '\r\n': + continue # skip all-whitespace lines + else: + break + else: + continue # skip all-whitespace lines + if DEBUG: print "dedent: indent=%d: %r" % (indent, line) + if margin is None: + margin = indent + else: + margin = min(margin, indent) + if DEBUG: print "dedent: margin=%r" % margin + + if margin is not None and margin > 0: + for i, line in enumerate(lines): + if i == 0 and skip_first_line: continue + removed = 0 + for j, ch in enumerate(line): + if ch == ' ': + removed += 1 + elif ch == '\t': + removed += tabsize - (removed % tabsize) + elif ch in '\r\n': + if DEBUG: print "dedent: %r: EOL -> strip up to EOL" % line + lines[i] = lines[i][j:] + break + else: + raise ValueError("unexpected non-whitespace char %r in " + "line %r while removing %d-space margin" + % (ch, line, margin)) + if DEBUG: + print "dedent: %r: %r -> removed %d/%d"\ + % (line, ch, removed, margin) + if removed == margin: + lines[i] = lines[i][j+1:] + break + elif removed > margin: + lines[i] = ' '*(removed-margin) + lines[i][j+1:] + break + else: + if removed: + lines[i] = lines[i][removed:] + return lines + +def _dedent(text, tabsize=8, skip_first_line=False): + """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text + + "text" is the text to dedent. + "tabsize" is the tab width to use for indent width calculations. + "skip_first_line" is a boolean indicating if the first line should + be skipped for calculating the indent width and for dedenting. + This is sometimes useful for docstrings and similar. + + textwrap.dedent(s), but don't expand tabs to spaces + """ + lines = text.splitlines(1) + _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line) + return ''.join(lines) + + +class _memoized(object): + """Decorator that caches a function's return value each time it is called. + If called later with the same arguments, the cached value is returned, and + not re-evaluated. + + http://wiki.python.org/moin/PythonDecoratorLibrary + """ + def __init__(self, func): + self.func = func + self.cache = {} + def __call__(self, *args): + try: + return self.cache[args] + except KeyError: + self.cache[args] = value = self.func(*args) + return value + except TypeError: + # uncachable -- for instance, passing a list as an argument. + # Better to not cache than to blow up entirely. + return self.func(*args) + def __repr__(self): + """Return the function's docstring.""" + return self.func.__doc__ + + +def _xml_oneliner_re_from_tab_width(tab_width): + """Standalone XML processing instruction regex.""" + return re.compile(r""" + (?: + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + [ ]{0,%d} + (?: + <\?\w+\b\s+.*?\?> # XML processing instruction + | + <\w+:\w+\b\s+.*?/> # namespaced single tag + ) + [ \t]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + ) + """ % (tab_width - 1), re.X) +_xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width) + +def _hr_tag_re_from_tab_width(tab_width): + return re.compile(r""" + (?: + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in \1 + [ ]{0,%d} + <(hr) # start tag = \2 + \b # word break + ([^<>])*? # + /?> # the matching end tag + [ \t]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + ) + """ % (tab_width - 1), re.X) +_hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width) + + +def _xml_encode_email_char_at_random(ch): + r = random() + # Roughly 10% raw, 45% hex, 45% dec. + # '@' *must* be encoded. I [John Gruber] insist. + # Issue 26: '_' must be encoded. + if r > 0.9 and ch not in "@_": + return ch + elif r < 0.45: + # The [1:] is to drop leading '0': 0x63 -> x63 + return '&#%s;' % hex(ord(ch))[1:] + else: + return '&#%s;' % ord(ch) + +def _hash_text(text): + return 'md5:'+md5(text.encode("utf-8")).hexdigest() + + +#---- mainline + +class _NoReflowFormatter(optparse.IndentedHelpFormatter): + """An optparse formatter that does NOT reflow the description.""" + def format_description(self, description): + return description or "" + +def _test(): + import doctest + doctest.testmod() + +def main(argv=None): + if argv is None: + argv = sys.argv + if not logging.root.handlers: + logging.basicConfig() + + usage = "usage: %prog [PATHS...]" + version = "%prog "+__version__ + parser = optparse.OptionParser(prog="markdown2", usage=usage, + version=version, description=cmdln_desc, + formatter=_NoReflowFormatter()) + parser.add_option("-v", "--verbose", dest="log_level", + action="store_const", const=logging.DEBUG, + help="more verbose output") + parser.add_option("--encoding", + help="specify encoding of text content") + parser.add_option("--html4tags", action="store_true", default=False, + help="use HTML 4 style for empty element tags") + parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode", + help="sanitize literal HTML: 'escape' escapes " + "HTML meta chars, 'replace' replaces with an " + "[HTML_REMOVED] note") + parser.add_option("-x", "--extras", action="append", + help="Turn on specific extra features (not part of " + "the core Markdown spec). Supported values: " + "'code-friendly' disables _/__ for emphasis; " + "'code-color' adds code-block syntax coloring; " + "'link-patterns' adds auto-linking based on patterns; " + "'footnotes' adds the footnotes syntax;" + "'xml' passes one-liner processing instructions and namespaced XML tags;" + "'pyshell' to put unindented Python interactive shell sessions in a <code> block.") + parser.add_option("--use-file-vars", + help="Look for and use Emacs-style 'markdown-extras' " + "file var to turn on extras. See " + "<http://code.google.com/p/python-markdown2/wiki/Extras>.") + parser.add_option("--link-patterns-file", + help="path to a link pattern file") + parser.add_option("--self-test", action="store_true", + help="run internal self-tests (some doctests)") + parser.add_option("--compare", action="store_true", + help="run against Markdown.pl as well (for testing)") + parser.set_defaults(log_level=logging.INFO, compare=False, + encoding="utf-8", safe_mode=None, use_file_vars=False) + opts, paths = parser.parse_args() + log.setLevel(opts.log_level) + + if opts.self_test: + return _test() + + if opts.extras: + extras = {} + for s in opts.extras: + splitter = re.compile("[,;: ]+") + for e in splitter.split(s): + if '=' in e: + ename, earg = e.split('=', 1) + try: + earg = int(earg) + except ValueError: + pass + else: + ename, earg = e, None + extras[ename] = earg + else: + extras = None + + if opts.link_patterns_file: + link_patterns = [] + f = open(opts.link_patterns_file) + try: + for i, line in enumerate(f.readlines()): + if not line.strip(): continue + if line.lstrip().startswith("#"): continue + try: + pat, href = line.rstrip().rsplit(None, 1) + except ValueError: + raise MarkdownError("%s:%d: invalid link pattern line: %r" + % (opts.link_patterns_file, i+1, line)) + link_patterns.append( + (_regex_from_encoded_pattern(pat), href)) + finally: + f.close() + else: + link_patterns = None + + from os.path import join, dirname, abspath, exists + markdown_pl = join(dirname(dirname(abspath(__file__))), "test", + "Markdown.pl") + for path in paths: + if opts.compare: + print "==== Markdown.pl ====" + perl_cmd = 'perl %s "%s"' % (markdown_pl, path) + o = os.popen(perl_cmd) + perl_html = o.read() + o.close() + sys.stdout.write(perl_html) + print "==== markdown2.py ====" + html = markdown_path(path, encoding=opts.encoding, + html4tags=opts.html4tags, + safe_mode=opts.safe_mode, + extras=extras, link_patterns=link_patterns, + use_file_vars=opts.use_file_vars) + sys.stdout.write( + html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) + if opts.compare: + test_dir = join(dirname(dirname(abspath(__file__))), "test") + if exists(join(test_dir, "test_markdown2.py")): + sys.path.insert(0, test_dir) + from test_markdown2 import norm_html_from_html + norm_html = norm_html_from_html(html) + norm_perl_html = norm_html_from_html(perl_html) + else: + norm_html = html + norm_perl_html = perl_html + print "==== match? %r ====" % (norm_perl_html == norm_html) + + +if __name__ == "__main__": + sys.exit( main(sys.argv) ) + diff --git a/adhocracy/controllers/__init__.py b/adhocracy/controllers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adhocracy/controllers/admin.py b/adhocracy/controllers/admin.py new file mode 100644 index 000000000..fc7c59341 --- /dev/null +++ b/adhocracy/controllers/admin.py @@ -0,0 +1,100 @@ +from datetime import datetime + +from pylons.i18n import _ + +from adhocracy.lib.base import * + +from adhocracy.model.forms import AdminUpdateMembershipForm, AdminForceLeaveForm + +log = logging.getLogger(__name__) + +class AdminController(BaseController): + + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("global.admin")) + def permissions(self): + if request.method == "POST": + groups = model.Group.all() + for permission in model.Permission.all(): + for group in groups: + t = request.params.get("%s-%s" % ( + group.code, permission.permission_name)) + if t and permission not in group.permissions: + group.permissions.append(permission) + elif not t and permission in group.permissions: + group.permissions.remove(permission) + for group in groups: + model.meta.Session.add(group) + model.meta.Session.commit() + return render("/admin/permissions.html") + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("instance.admin")) + def members(self): + return render("/admin/members.html") + + @RequireInstance + @RequireInternalRequest() + @ActionProtector(has_permission("instance.admin")) + @validate(schema=AdminUpdateMembershipForm(), form="members", post_only=False, on_get=True) + def update_membership(self): + user = self.form_result.get("user") + to_group = self.form_result.get("to_group") + if not to_group.code in [model.Group.CODE_OBSERVER, + model.Group.CODE_VOTER, + model.Group.CODE_SUPERVISOR]: + h.flash("Cannot make %(user)s a member of %(group)s" % { + 'user': user.name, + 'group': group.group_name}) + return render("/admin/members.html") + + had_vote = True if user.has_permission("vote.cast") else False + + for membership in user.memberships: + if not membership.expire_time and membership.instance == c.instance: + membership.group = to_group + model.meta.Session.add(membership) + model.meta.Session.commit() + + event.emit(event.T_INSTANCE_MEMBERSHIP_UPDATE, + {'group': to_group.code, 'instance': c.instance}, + user, scopes=[c.instance], topics=[c.page_instance, user]) + + if had_vote and not user.has_permission("vote.cast"): + # user has lost voting privileges + democracy.DelegationNode.detach(user, c.instance) + + redirect_to("/admin/members#u_%s" % str(user.user_name)) + h.flash(_("%(user)s is not a member of %(instance)s") % { + 'user': user.name, + 'instance': c.instance.label}) + return render("/admin/members.html") + + @RequireInstance + @RequireInternalRequest() + @ActionProtector(has_permission("instance.admin")) + @validate(schema=AdminForceLeaveForm(), form="members", post_only=False, on_get=True) + def force_leave(self): + user = self.form_result.get("user") + for membership in user.memberships: + if not membership.expire_time and membership.instance == c.instance: + membership.expire_time = datetime.now() + model.meta.Session.add(membership) + model.meta.Session.commit() + + democracy.DelegationNode.detach(user, c.instance) + + event.emit(event.T_INSTANCE_FORCE_LEAVE, + {'instance': c.instance, 'user': c.user}, + user, scopes=[c.instance], + topics=[c.page_instance, user, c.user]) + + h.flash(_("%(user)s was removed from %(instance)s") % { + 'user': user.name, + 'instance': c.instance.label}) + return render("/admin/members.html") + h.flash(_("%(user)s isn't a member of %(instance)s") % { + 'user': user.name, + 'instance': c.instance.label}) + return render("/admin/members.html") \ No newline at end of file diff --git a/adhocracy/controllers/category.py b/adhocracy/controllers/category.py new file mode 100644 index 000000000..31bba682f --- /dev/null +++ b/adhocracy/controllers/category.py @@ -0,0 +1,141 @@ +from datetime import datetime + +from pylons.i18n import _ + +import adhocracy.lib.text as text +from adhocracy.lib.base import * +from adhocracy.model.forms import CategoryCreateForm, CategoryEditForm + +log = logging.getLogger(__name__) + +class CategoryController(BaseController): + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("category.create")) + @validate(schema=CategoryCreateForm(), form="create", post_only=True) + def create(self): + auth.require_delegateable_perm(None, 'category.create') + if request.method == "POST": + category = model.Category(c.instance, + self.form_result.get("label"), + c.user) + category.parents.append(self.form_result.get("categories")) + if self.form_result.get("description"): + category.description = text.cleanup(self.form_result.get("description")) + model.meta.Session.add(category) + model.meta.Session.commit() + model.meta.Session.refresh(category) + + event.emit(event.T_CATEGORY_CREATE, + {'category': category, 'parent': self.form_result.get("categories")}, + c.user, scopes=[c.instance], topics = [category, c.instance]) + + redirect_to("/category/%s" % str(category.id)) + return render("/category/create.html") + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("category.edit")) + @validate(schema=CategoryEditForm(), form="edit", post_only=True) + def edit(self, id): + c.category = model.Category.find(id) + if not c.category: + abort(404, _("No category with ID '%s' exists.") % id) + auth.require_delegateable_perm(c.category, 'category.edit') + if request.method == "POST": + c.category.label = self.form_result.get("label") + if self.form_result.get("description"): + c.category.description = text.cleanup(self.form_result.get("description")) + if not c.category.id == c.instance.root: + parent = self.form_result.get("categories") + if not c.category.is_super(parent): + c.category.parents = [parent] + model.meta.Session.add(c.category) + model.meta.Session.commit() + + event.emit(event.T_CATEGORY_EDIT, {'category': c.category}, + c.user, scopes=[c.instance], topics = [c.category]) + + return redirect_to('/category/%s' % str(c.category.id)) + return render("/category/edit.html") + + @RequireInstance + @ActionProtector(has_permission("category.view")) + def view(self, id, format='html'): + c.category = model.Category.find(id) + if not c.category: + abort(404, _("No category with ID '%s' exists.") % id) + + if c.category.instance.root == c.category: + redirect_to("/instance/%s" % str(c.category.instance.key)) + + if c.category.description: + h.add_meta("description", + h.text.truncate(text.meta_escape(c.category.description), + length=200, whole_word=True)) + h.add_meta("dc.title", text.meta_escape(c.category.label, markdown=False)) + h.add_meta("dc.date", c.category.create_time.strftime("%Y-%m-%d")) + + c.tile = tiles.category.CategoryTile(c.category) + if c.tile.is_root: + c.instance_tile = tiles.instance.InstanceTile(c.instance) + + issues = c.category.search_children(recurse=True, cls=model.Issue) + + if format == 'rss': + events = event.q.run(event.q._or(map(event.q.topic, issues))) + return event.rss_feed(events, _("Category: %s") % c.category.label, + h.instance_url(c.instance, path="/category/%s" % c.category.id), + c.category.description if c.category.description else "") + + h.add_rss(_("Category: %s") % c.category.label, + h.instance_url(c.instance, "/category/%s.rss" % c.category.id)) + + c.issues_pager = NamedPager('issues', issues, tiles.issue.row, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest, + _("activity"): sorting.issue_activity, + _("name"): sorting.delegateable_label}, + default_sort=sorting.issue_activity) + + c.subcats_pager = NamedPager('categories', c.tile.categories, tiles.category.list_item, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest, + _("activity"): sorting.category_activity, + _("name"): sorting.delegateable_label}, + default_sort=sorting.category_activity) + + return render("category/view.html") + + @RequireInstance + @RequireInternalRequest() + @ActionProtector(has_permission("category.delete")) + def delete(self, id): + if id == c.instance.root.id: + abort(500, _("Deleting the root category isn't possible.")) + category = model.Category.find(id) + if not category: + abort(404, _("No category with ID '%(id)s' exists.") % {'id': id}) + auth.require_delegateable_perm(category, 'category.delete') + + parent = c.instance.root + if len(category.parents): + parent = category.parents[0] + for child in category.children: + if category in child.parents: + child.parents.remove(category) + if not parent in child.parents: + child.parents.append(parent) + model.meta.Session.add(child) + category.delete_time = datetime.now() + + h.flash(_("Category '%(category)s' has been deleted.") % {'category': category.label}) + + model.meta.Session.add(category) + model.meta.Session.commit() + + event.emit(event.T_CATEGORY_DELETE, {'category': category}, + c.user, scopes=[c.instance], topics=[parent, c.instance, category]) + + redirect_to("/category/%s" % str(parent.id)) diff --git a/adhocracy/controllers/comment.py b/adhocracy/controllers/comment.py new file mode 100644 index 000000000..e74fa4414 --- /dev/null +++ b/adhocracy/controllers/comment.py @@ -0,0 +1,169 @@ +from datetime import datetime + +from pylons.i18n import _ + +from adhocracy.lib.base import * +import adhocracy.lib.text as text +import adhocracy.model.forms as forms + +log = logging.getLogger(__name__) + +class CommentCreateForm(formencode.Schema): + allow_extra_fields = True + text = validators.String(max=20000, min=4, not_empty=True) + canonical = validators.Bool(if_empty=0, if_missing=0, not_empty=False) + topic = forms.ValidDelegateable() + reply = forms.ValidComment(if_empty=None) + +class CommentEditForm(formencode.Schema): + allow_extra_fields = True + text = validators.String(max=20000, min=4, not_empty=True) + +class CommentRevertForm(formencode.Schema): + allow_extra_fields = True + to = forms.ValidRevision() + +class CommentController(BaseController): + + def _comment_anchor(self, comment): + + # for canonical comment discussion, return to the discussion page, + # not the main topic page! + parent = comment + while parent.reply: + parent = parent.reply + if parent.canonical and parent != comment: + return "/comment/%s#c%s" % (str(parent.id), comment.id) + + if isinstance(comment.topic, model.Issue): + return "/issue/%s#c%s" % (str(comment.topic.id), comment.id) + elif isinstance(comment.topic, model.Motion): + return "/motion/%s#c%s" % (str(comment.topic.id), comment.id) + else: + abort(500, _("Unsupported topic type.")) + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("comment.create")) + @validate(schema=CommentCreateForm(), form="create", post_only=True) + def create(self): + if request.method == "POST": + topic = self.form_result.get('topic') + auth.require_delegateable_perm(topic, 'comment.create') + comment = model.Comment(topic, c.user) + _text = text.cleanup(self.form_result.get('text')) + comment.latest = model.Revision(comment, c.user, _text) + + comment.canonical = self.form_result.get('canonical', 0) + if comment.canonical: + if not isinstance(topic, model.Motion): + comment.canonical = 0 + else: + auth.require_motion_perm(topic, 'comment.create') + + if self.form_result.get('reply'): + comment.reply = self.form_result.get('reply') + + model.meta.Session.add(comment) + model.meta.Session.commit() + model.meta.Session.refresh(comment) + + event.emit(event.T_COMMENT_CREATE, {'comment': comment, 'delegateable': topic}, + c.user, scopes=[c.instance], topics=[topic, comment]) + + redirect_to(self._comment_anchor(comment)) + return render('/comment/create.html') + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("comment.edit")) + @validate(schema=CommentEditForm(), form="edit", post_only=True) + def edit(self, id): + c.comment = model.Comment.find(id) + if not c.comment: + abort(404, _("No comment with ID %s exists") % id) + auth.require_comment_perm(c.comment, 'comment.delete') + if request.method == "POST": + _text = text.cleanup(self.form_result.get('text')) + c.comment.latest = model.Revision(c.comment, c.user, _text) + model.meta.Session.add(c.comment.latest) + model.meta.Session.commit() + + event.emit(event.T_COMMENT_EDIT, {'comment': c.comment, + 'delegateable': c.comment.topic}, + c.user, scopes=[c.instance], topics=[c.comment.topic, c.comment]) + + redirect_to(self._comment_anchor(c.comment)) + return render('/comment/edit.html') + + @ActionProtector(has_permission("comment.view")) + def redirect(self, id): + c.comment = model.Comment.find(id, instance_filter=False) + if not c.comment: + abort(404, _("No comment with ID %s exists") % id) + redirect_to(h.instance_url(c.comment.topic.instance, + path=self._comment_anchor(c.comment))) + + @RequireInstance + @ActionProtector(has_permission("comment.view")) + def view(self, id): + c.comment = model.Comment.find(id) + if not c.comment: + abort(404, _("No comment with ID %s exists") % id) + + return render('/comment/view.html') + + @RequireInstance + @RequireInternalRequest() + @ActionProtector(has_permission("comment.delete")) + def delete(self, id): + c.comment = model.Comment.find(id) + if not c.comment: + abort(404, _("No comment with ID %s exists") % id) + auth.require_comment_perm(c.comment, 'comment.delete') + c.comment.delete_time = datetime.now() + model.meta.Session.add(c.comment) + model.meta.Session.commit() + + event.emit(event.T_COMMENT_DELETE, {'comment': c.comment, + 'delegateable': c.comment.topic}, + c.user, scopes=[c.instance], topics=[c.comment.topic, c.comment]) + + redirect_to(self._comment_anchor(c.comment)) + + @RequireInstance + @ActionProtector(has_permission("comment.view")) + def history(self, id): + c.comment = model.Comment.find(id) + if not c.comment: + abort(404, _("No comment with ID %s exists") % id) + + + c.revisions_pager = NamedPager('revisions', c.comment.revisions, tiles.revision.row, count=10, #list_item, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest}, + default_sort=sorting.entity_newest) + return render('/comment/history.html') + + @RequireInstance + @RequireInternalRequest() + @ActionProtector(has_permission("comment.edit")) + @validate(schema=CommentRevertForm(), form="history", post_only=False, on_get=True) + def revert(self, id): + c.comment = model.Comment.find(id) + if not c.comment: + abort(404, _("No comment with ID %s exists") % id) + auth.require_comment_perm(c.comment, 'comment.delete') + revision = self.form_result.get('to') + if revision.comment != c.comment: + abort(500, _("You're trying to revert to a revision which is not part of this comments history")) + c.comment.latest = model.Revision(c.comment, c.user, revision.text) + model.meta.Session.add(c.comment.latest) + model.meta.Session.commit() + + + event.emit(event.T_COMMENT_EDIT, {'comment': c.comment, + 'delegateable': c.comment.topic}, + c.user, scopes=[c.instance], topics=[c.comment.topic, c.comment]) + + redirect_to(self._comment_anchor(c.comment)) diff --git a/adhocracy/controllers/delegation.py b/adhocracy/controllers/delegation.py new file mode 100644 index 000000000..c55ba5c19 --- /dev/null +++ b/adhocracy/controllers/delegation.py @@ -0,0 +1,93 @@ +from datetime import datetime + +from pylons.i18n import _ +import formencode.validators + +from adhocracy.lib.base import * +from repoze.what.plugins.pylonshq import ActionProtector +from adhocracy.model.forms import DelegationCreateForm + +log = logging.getLogger(__name__) + +class DelegationController(BaseController): + + @RequireInstance + @RequireInternalRequest(methods=["POST"]) + @ActionProtector(has_permission("vote.cast")) + def create(self): + id = request.params.get("scope", c.instance.root.id) + c.scope = model.Delegateable.find(id) + if not c.scope: + abort(404, _("No motion or category with ID '%(id)s' exists") % {'id':id}) + errors = {} + if request.method == "POST": + try: + self.form_result = DelegationCreateForm().to_python(request.params) + agent = self.form_result.get("agent") + if agent and agent != c.user: + delegation = model.Delegation(c.user, agent, c.scope) + #voting.replay_delegation(delegation) + model.meta.Session.add(delegation) + model.meta.Session.commit() + + log.debug("Replaying the vote for Delegation: %s" % delegation) + democracy.Decision.replay_decisions(delegation) + + event.emit(event.T_DELEGATION_CREATE, {'scope': c.scope, 'agent': agent}, + c.user, scopes=[c.instance], topics=[c.scope, agent]) + + redirect_to("/d/%s" % str(c.scope.id)) + except formencode.validators.Invalid, error: + errors = error.error_dict + #pass + return htmlfill.render(render("delegation/create.html"), + defaults=request.params, errors=errors) + + @RequireInstance + @RequireInternalRequest() + @ActionProtector(has_permission("vote.cast")) + def revoke(self, id): + delegation = model.Delegation.find(id) + if not delegation: + abort(404, _("Couldn't find delegation %(id)s") % {'id': id}) + if not delegation.principal == c.user: + abort(403, _("Cannot access delegation %(id)s") % {'id': id}) + delegation.revoke_time = datetime.now() + + event.emit(event.T_DELEGATION_REVOKE, + {'scope': delegation.scope, 'agent': delegation.agent}, c.user, + topics=[delegation.scope, delegation.agent]) + + model.meta.Session.add(delegation) + model.meta.Session.commit() + h.flash(_("The delegation is now revoked.")) + redirect_to("/d/%s" % str(delegation.scope.id)) + + @ActionProtector(has_permission("delegation.view")) + def review(self, id): + c.delegation = model.Delegation.find(id) + if not c.delegation: + abort(404, _("Couldn't find delegation %(id)s") % {'id': id}) + c.scope = c.delegation.scope + + decisions = democracy.Decision.for_user(c.delegation.principal, c.instance) + decisions = filter(lambda d: c.delegation in d.delegations, decisions) + + c.decisions_pager = NamedPager('decisions', decisions, tiles.decision.user_row, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest}, + default_sort=sorting.entity_newest) + + return render("delegation/review.html") + + @RequireInstance + @ActionProtector(has_permission("user.view")) + def graph(self): + c.users = model.meta.Session.query(model.User).all() + c.delegations = model.meta.Session.query(model.Delegation).all() + response.content_type = "text/plain" + return render("/delegation/graph.dot") + + + + \ No newline at end of file diff --git a/adhocracy/controllers/error.py b/adhocracy/controllers/error.py new file mode 100644 index 000000000..753a7e0e9 --- /dev/null +++ b/adhocracy/controllers/error.py @@ -0,0 +1,55 @@ +import cgi +import re + +from pylons import request, response, session, tmpl_context as c +from pylons.i18n import _ +from adhocracy.lib.base import BaseController, render + +from paste.urlparser import PkgResourcesParser +from pylons import request +from pylons.controllers.util import forward +from pylons.middleware import error_document_template +from webhelpers.html.builder import literal + +from adhocracy.lib.base import BaseController + +BODY_RE = re.compile("<br \/><br \/>.*<!--(.*)-->.*<\/body", re.S) + +class ErrorController(BaseController): + + """Generates error documents as and when they are required. + + The ErrorDocuments middleware forwards to ErrorController when error + related status codes are returned from the application. + + This behaviour can be altered by changing the parameters to the + ErrorDocuments middleware in your config/middleware.py file. + + """ + + def document(self): + resp = request.environ.get('pylons.original_response') + + # YOU DO NOT SEE THIS. IF YOU DO, ITS NOT WHAT IT LOOKS LIKE + # I DID NOT HAVE REGEX RELATIONS WITH THAT HTML PAGE + for match in BODY_RE.finditer(resp.body): + c.error_message = match.group(1) + c.error_code = cgi.escape(request.GET.get('code', str(resp.status_int))) + + response.status = resp.status + return render("/error/http.html") + + def img(self, id): + """Serve Pylons' stock images""" + return self._serve_file('/'.join(['media/img', id])) + + def style(self, id): + """Serve Pylons' stock stylesheets""" + return self._serve_file('/'.join(['media/style', id])) + + def _serve_file(self, path): + """Call Paste's FileApp (a WSGI application) to serve the file + at the specified path + """ + request.environ['PATH_INFO'] = '/%s' % path + return forward(PkgResourcesParser('pylons', 'pylons')) diff --git a/adhocracy/controllers/event.py b/adhocracy/controllers/event.py new file mode 100644 index 000000000..58ff8a83e --- /dev/null +++ b/adhocracy/controllers/event.py @@ -0,0 +1,17 @@ +import lucene + +from pylons.i18n import _ + +from adhocracy.lib.base import * +import adhocracy.lib.helpers as h +from adhocracy.model.forms import EditorAddForm, EditorRemoveForm + +log = logging.getLogger(__name__) + +class EventController(BaseController): + + @ActionProtector(has_permission("global.admin")) + def all(self): + events = event.q.run("+type:event") + c.event_pager = NamedPager('events', events, tiles.event.list_item, count=50) + return render('/event/all.html') \ No newline at end of file diff --git a/adhocracy/controllers/instance.py b/adhocracy/controllers/instance.py new file mode 100644 index 000000000..4cce75e42 --- /dev/null +++ b/adhocracy/controllers/instance.py @@ -0,0 +1,227 @@ +from datetime import datetime +import os.path +import StringIO + +from pylons.i18n import _ + +import Image + +from adhocracy.lib.base import * +import adhocracy.lib.text as text +import adhocracy.model.forms as forms + +import adhocracy.lib.instance as libinstance + +log = logging.getLogger(__name__) + +class InstanceCreateForm(formencode.Schema): + allow_extra_fields = True + key = formencode.All(validators.String(min=4, max=20), + forms.UniqueInstanceKey()) + label = validators.String(min=4, max=254, not_empty=True) + description = validators.String(max=100000, if_empty=None, not_empty=False) + +class InstanceEditForm(formencode.Schema): + allow_extra_fields = True + label = validators.String(min=4, max=254, not_empty=True) + description = validators.String(max=100000, if_empty=None, not_empty=False) + activation_delay = validators.Int(not_empty=True) + required_majority = validators.Number(not_empty=True) + default_group = forms.ValidGroup(not_empty=True) + +class InstanceController(BaseController): + + def __init__(self): + self.LOGO = Image.open(os.path.join(config['pylons.paths']['static_files'], + 'img', 'header_logo.png')) + self.PATH = os.path.join(config['cache.dir'], 'img', '%(key)s.png') + + def _find_key(self, key): + c.page_instance = model.Instance.find(key) + if not c.page_instance: + abort(404, _("No such adhocracy exists: %(key)s") % {'key': key}) + + @ActionProtector(has_permission("instance.index")) + def index(self): + + h.add_meta("description", _("An index of adhocracies run at adhocracy.cc. " + + "Select which ones you would like to join and participate in!")) + + instances = model.Instance.all() + c.instances_pager = NamedPager('instances', instances, tiles.instance.row, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest, + _("activity"): sorting.instance_activity, + _("name"): sorting.delegateable_label}, + default_sort=sorting.instance_activity) + + @memoize('instance-index') + def cached(user, p): + return render("/instance/index.html") + return cached(c.user, request.params) + + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("instance.create")) + @validate(schema=InstanceCreateForm(), form="create", post_only=True) + def create(self): + if request.method == "POST": + inst = libinstance.create(self.form_result.get('key'), + self.form_result.get('label'), + c.user) + inst.description = self.form_result.get('description') + model.meta.Session.refresh(inst) + + event.emit(event.T_INSTANCE_CREATE, {'instance': inst.key}, + c.user, scopes=[inst], topics=[inst]) + + redirect_to(h.instance_url(inst)) + return render("/instance/create.html") + + @ActionProtector(has_permission("instance.view")) + def view(self, key, format='html'): + self._find_key(key) + + issues = c.page_instance.root.search_children(recurse=True, cls=model.Issue) + + if format == 'rss': + query = event.q._or(event.q.scope(c.page_instance), event.q.topic(c.page_instance)) + events = event.q.run(query) + return event.rss_feed(events, _('%s News' % c.page_instance.label), + h.instance_url(c.page_instance), + _("News from the %s Adhocracy") % c.page_instance.label) + + #c.events_pager = NamedPager('events', events, tiles.event.list_item, count=20) + + c.tile = tiles.instance.InstanceTile(c.page_instance) + + c.issues_pager = NamedPager('issues', issues, tiles.issue.row, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest, + _("activity"): sorting.issue_activity, + _("name"): sorting.delegateable_label}, + default_sort=sorting.issue_activity) + + c.subcats_pager = NamedPager('categories', c.tile.categories, tiles.category.list_item, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest, + _("activity"): sorting.category_activity, + _("name"): sorting.delegateable_label}, + default_sort=sorting.category_activity) + + return render("/instance/view.html") + + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("instance.admin")) + @validate(schema=InstanceEditForm(), form="edit", post_only=True) + def edit(self, key): + self._find_key(key) + if request.method == "POST": + c.page_instance.description = text.cleanup(self.form_result.get('description')) + c.page_instance.label = self.form_result.get('label') + c.page_instance.required_majority = self.form_result.get('required_majority') + c.page_instance.activation_delay = self.form_result.get('activation_delay') + if self.form_result.get('default_group').code in model.Group.INSTANCE_GROUPS: + c.page_instance.default_group = self.form_result.get('default_group') + + try: + logo = request.POST.get('logo') + if logo.file: + logo_image = Image.open(logo.file) + logo_image.thumbnail(self.LOGO.size, Image.ANTIALIAS) + logo_image.save(self.PATH % {'key': c.page_instance.key}) + except Exception, e: + log.debug(e) + + model.meta.Session.add(c.page_instance) + model.meta.Session.commit() + + event.emit(event.T_INSTANCE_EDIT, {'instance': c.page_instance}, + c.user, scopes=[c.page_instance], topics=[c.page_instance]) + + #h.flash("%s has been updated." % c.page_instance.label) + redirect_to(h.instance_url(c.page_instance)) + c._Group = model.Group + return htmlfill.render(render("/instance/edit.html"), + defaults={ + 'label': c.page_instance.label, + 'description': c.page_instance.description, + 'required_majority': c.page_instance.required_majority, + 'activation_delay': c.page_instance.activation_delay, + 'default_group': c.page_instance.default_group.code if \ + c.page_instance.default_group else \ + model.Group.INSTANCE_DEFAULT + }) + + @ActionProtector(has_permission("instance.index")) + def logo(self, key): + #self._logo_setup() + instance = model.Instance.find(key) + response.content_type = "application/png" + if instance: + instance_path = self.PATH % {'key': instance.key} + if os.path.exists(instance_path): + return open(instance_path, 'rb').read() + sio = StringIO.StringIO() + self.LOGO.save(sio, 'PNG') + return sio.getvalue() + + + @RequireInternalRequest() + @ActionProtector(has_permission("instance.delete")) + def delete(self, key): + self._find_key(key) + abort(500, _("Deleting an instance is not currently implemented")) + + @RequireInternalRequest() + @ActionProtector(has_permission("instance.join")) + def join(self, key): + self._find_key(key) + if c.page_instance in c.user.instances: + h.flash(_("You're already a member in %(instance)s.") % { + 'instance': c.page_instance.label}) + redirect_to('/adhocracies') + + grp = c.page_instance.default_group + if not grp: + grp = model.Group.by_code(model.Group.INSTANCE_DEFAULT) + membership = model.Membership(c.user, c.page_instance, grp) + model.meta.Session.add(membership) + model.meta.Session.commit() + + event.emit(event.T_INSTANCE_JOIN, {'instance': c.page_instance}, + c.user, scopes=[c.page_instance], topics=[c.page_instance]) + + h.flash(_("Welcome to %(instance)s") % { + 'instance': c.page_instance.label}) + return redirect_to(h.instance_url(c.page_instance)) + + @RequireInternalRequest() + @ActionProtector(has_permission("instance.leave")) + def leave(self, key): + self._find_key(key) + if not c.page_instance in c.user.instances: + h.flash(_("You're not a member of %(instance)s.") % { + 'instance': c.page_instance.label}) + elif c.user == c.page_instance.creator: + h.flash(_("You're the founder of %s, cannot leave.") % { + 'instance': c.page_instance.label}) + else: + t = datetime.now() + + for membership in c.user.memberships: + if membership.expire_time: + continue + if membership.instance == c.page_instance: + membership.expire_time = t + model.meta.Session.add(membership) + + democracy.DelegationNode.detach(c.user, c.page_instance) + + event.emit(event.T_INSTANCE_LEAVE, + {'instance': c.page_instance.key}, + c.user, scopes=[c.page_instance], + topics=[c.page_instance]) + model.meta.Session.commit() + redirect_to('/adhocracies') + + diff --git a/adhocracy/controllers/issue.py b/adhocracy/controllers/issue.py new file mode 100644 index 000000000..28f550174 --- /dev/null +++ b/adhocracy/controllers/issue.py @@ -0,0 +1,143 @@ +import logging +from datetime import datetime + +from pylons.i18n import _ + +from adhocracy.lib.base import * +from adhocracy.lib.base import BaseController, render +import adhocracy.model.forms as forms +import adhocracy.lib.text as text + +log = logging.getLogger(__name__) + +class IssueCreateForm(formencode.Schema): + allow_extra_fields = True + label = validators.String(max=255, min=4, not_empty=True) + text = validators.String(max=10000, not_empty=False, if_empty="") + categories = formencode.Any(formencode.foreach.ForEach(forms.ValidCategory(), convert_to_list=True)) + +class IssueEditForm(formencode.Schema): + allow_extra_fields = True + label = validators.String(max=255, min=4, not_empty=True) + categories = formencode.Any(formencode.foreach.ForEach(forms.ValidCategory(), convert_to_list=True)) + +class IssueController(BaseController): + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("issue.create")) + @validate(schema=IssueCreateForm(), form="create", post_only=True) + def create(self): + auth.require_delegateable_perm(None, 'issue.create') + if request.method == "POST": + issue = model.Issue(c.instance, self.form_result.get('label'), c.user) + issue.parents = self.form_result.get('categories') + comment = model.Comment(issue, c.user) + rev = model.Revision(comment, c.user, + text.cleanup(self.form_result.get("text"))) + comment.latest = rev + model.meta.Session.add(issue) + model.meta.Session.add(comment) + model.meta.Session.add(rev) + model.meta.Session.commit() + issue.comment = comment + model.meta.Session.add(issue) + model.meta.Session.commit() + model.meta.Session.refresh(rev) + + event.emit(event.T_ISSUE_CREATE, {'issue': issue}, c.user, + scopes=[c.instance], topics=[issue, c.instance]) + + redirect_to('/issue/%s' % str(issue.id)) + return render("/issue/create.html") + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("issue.edit")) + @validate(schema=IssueEditForm(), form="edit", post_only=True) + def edit(self, id): + c.issue = model.Issue.find(id) + if not c.issue: + abort(404, _("No issue with ID %s exists." % id)) + auth.require_delegateable_perm(c.issue, 'issue.edit') + if request.method == "POST": + c.issue.label = self.form_result.get('label') + c.issue.parents = self.form_result.get('categories') + model.meta.Session.add(c.issue) + model.meta.Session.commit() + model.meta.Session.refresh(c.issue) + + event.emit(event.T_ISSUE_EDIT, {'issue': c.issue}, c.user, + scopes=[c.instance], topics=[c.issue, c.instance]) + + redirect_to('/issue/%s' % str(c.issue.id)) + return render("/issue/edit.html") + + @RequireInstance + @ActionProtector(has_permission("issue.view")) + def view(self, id, format="html"): + c.issue = model.Issue.find(id) + if not c.issue: + abort(404, _("No issue with ID %s exists." % id)) + + h.add_meta("dc.title", text.meta_escape(c.issue.label, markdown=False)) + h.add_meta("dc.date", c.issue.create_time.strftime("%Y-%m-%d")) + h.add_meta("dc.author", text.meta_escape(c.issue.creator.name, markdown=False)) + + if format == 'rss': + events = event.q.run(event.q._or(event.q.topic(c.issue), "foo:schnasel", + *map(event.q.topic, c.issue.motions))) + + return event.rss_feed(events, _("Issue: %s") % c.issue.label, + h.instance_url(c.instance, path="/issue/%s" % str(c.issue.id)), + description=_("Activity on the %s issue") % c.issue.label) + + h.add_rss(_("Issue: %(issue)s") % {'issue': c.issue.label}, + h.instance_url(c.instance, "/issue/%s.rss" % c.issue.id)) + + c.tile = tiles.issue.IssueTile(c.issue) + c.motions_pager = NamedPager('motions', c.issue.motions, tiles.motion.row, count=4, #list_item, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest, + _("activity"): sorting.motion_activity, + _("name"): sorting.delegateable_label}, + default_sort=sorting.motion_activity) + + #c.events_pager = NamedPager('events', events, tiles.event.list_item, count=5) + + return render("/issue/view.html") + + @RequireInstance + @RequireInternalRequest() + @ActionProtector(has_permission("issue.delete")) + def delete(self, id): + c.issue = model.Issue.find(id) + if not c.issue: + abort(404, _("No issue with ID %s exists." % id)) + auth.require_delegateable_perm(c.issue, 'issue.delete') + parent = c.issue.parents[0] + + for motion in c.issue.motions: + try: + result = democracy.Result(motion) + if not result.is_motion_mutable: + h.flash(_("The issue %(issue)s cannot be deleted, because the contained " + + "motion %(motion)s is polling.") % {'issue': c.issue.label, 'motion': motion.label}) + redirect_to('/issue/%s' % str(c.issue.id)) + except democracy.NoPollException: + pass + motion.delete_time = datetime.now() + model.meta.Session.add(motion) + + h.flash(_("Issue '%(issue)s' has been deleted.") % {'issue': c.issue.label}) + + c.issue.delete_time = datetime.now() + model.meta.Session.add(c.issue) + model.meta.Session.commit() + + event.emit(event.T_ISSUE_DELETE, {'issue': c.issue}, + c.user, scopes=[c.instance], topics=[c.issue, c.instance] + c.issue.parents) + + redirect_to('/category/%s' % str(parent.id)) + + diff --git a/adhocracy/controllers/karma.py b/adhocracy/controllers/karma.py new file mode 100644 index 000000000..e44bf840b --- /dev/null +++ b/adhocracy/controllers/karma.py @@ -0,0 +1,45 @@ +import logging +import simplejson +from datetime import datetime + +from pylons.i18n import _ + +from adhocracy.lib.base import * +from adhocracy.lib.karma import * +import adhocracy.model.forms as forms + +log = logging.getLogger(__name__) + +class KarmaGiveForm(formencode.Schema): + allow_extra_fields = True + comment = forms.ValidComment(not_empty=True) + value = validators.Int(min=-1, max=1, not_empty=True) + +class KarmaController(BaseController): + + @RequireInstance + @ActionProtector(has_permission("karma.give")) + @validate(schema=KarmaGiveForm(), post_only=False, on_get=True) + def give(self, format="html"): + comment = self.form_result.get('comment') + auth.require_comment_perm(comment, 'karma.give') + value = self.form_result.get('value') + if not value in [1, -1]: + h.flash(_("Invalid karma value. Karma is either positive or negative!")) + redirect_to("/comment/r/%s" % comment.id) + + if not c.user == comment.creator: + karma = position(comment, c.user) + if karma: + karma.value = value + karma.create_time = datetime.now() + else: + karma = model.Karma(value, c.user, comment.creator, comment) + + model.meta.Session.add(karma) + model.meta.Session.commit() + + if format == 'json': + return simplejson.dumps({'score': comment_score(comment)}) + + redirect_to("/comment/r/%s" % comment.id) diff --git a/adhocracy/controllers/motion.py b/adhocracy/controllers/motion.py new file mode 100644 index 000000000..2ca3f32ef --- /dev/null +++ b/adhocracy/controllers/motion.py @@ -0,0 +1,309 @@ +import cgi +from datetime import datetime + +from pylons.i18n import _ + +from adhocracy.lib.base import * +import adhocracy.lib.text as text +import adhocracy.model.forms as forms +from adhocracy.lib.tiles.motion_tiles import MotionTile + +log = logging.getLogger(__name__) + +class MotionCreateForm(formencode.Schema): + allow_extra_fields = True + label = validators.String(max=255, min=4, not_empty=True) + text = validators.String(max=10000, min=4, not_empty=True) + #issue = forms.ValidIssue() + +class MotionEditForm(formencode.Schema): + allow_extra_fields = True + label = validators.String(max=255, min=4, not_empty=True) + issue = forms.ValidIssue(not_empty=True) + +class MotionDecisionsFilterForm(formencode.Schema): + allow_extra_fields = True + result = validators.Int(not_empty=False, if_empty=None, min=-1, max=1) + + +class MotionController(BaseController): + + @RequireInstance + @ActionProtector(has_permission("motion.view")) + def index(self): + scored = democracy.Result.critical_motions(c.instance) + urgency_sort = sorting.dict_value_sorter(scored) + + c.motions_pager = NamedPager('motions', scored.keys(), tiles.motion.detail_row, count=4, #list_item, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest, + _("activity"): sorting.motion_activity, + _("urgency"): urgency_sort, + _("name"): sorting.delegateable_label}, + default_sort=urgency_sort) + return render("/motion/index.html") + + def _parse_relations(self, motion=None): + types_val = formencode.ForEach(validators.OneOf(['a', 'd', 'n'], not_empty=True), + convert_to_list=True) + motions_val = formencode.ForEach(forms.ValidMotion(if_empty=None, if_invalid=None), + convert_to_list=True) + + print "REL_MOTIONS", request.params.getall('rel_motion') + + types = types_val.to_python(request.params.getall('rel_type')) + motions = motions_val.to_python(request.params.getall('rel_motion')) + if len(types) != len(motions): + raise formencode.Invalid("", type, None, + error_dict={'rel_error': _("Input error while applying relations.")}) + + print "MOTIONS ", motions + + c.relations = dict() + for type, other in zip(types, motions): + if not other: + continue + if (motion and other == motion) and type in ['a', 'd']: + raise formencode.Invalid("", type, None, + error_dict={ + 'rel_error': _("A motion cannot have a relation with itself.")}) + if other in c.relations.keys() and c.relations[other] != type: + raise formencode.Invalid("", type, None, + error_dict={ + 'rel_error': _("A motion can either contradict " + + "or require another, not both.")}) + c.relations[other] = type + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("motion.create")) + #@validate(schema=MotionCreateForm(), form="create", post_only=True) + def create(self): + auth.require_motion_perm(None, 'comment.create') + try: + c.issue = forms.ValidIssue(not_empty=True).to_python(request.params.get('issue')) + except formencode.Invalid: + h.flash(_("Cannot identify the parent issue.")) + redirect_to("/") + c.canonicals = ["", ""] + c.relations = dict(map(lambda m: (m, 'a'), c.issue.motions)) + c.motions = model.Motion.all(instance=c.instance) + + if request.method == "POST": + try: + # if the remaining validation fails, we'll still want this to execute + canonicals_val = formencode.ForEach(validators.String(), + convert_to_list=True) + c.canonicals = filter(lambda p: p != None and len(p), + canonicals_val.to_python(request.params.getall('canonicals'))) + + self._parse_relations() + form_result = MotionCreateForm().to_python(request.params) + motion = model.Motion(c.instance, + form_result.get("label"), + c.user) + motion.issue = c.issue + comment = model.Comment(motion, c.user) + rev = model.Revision(comment, c.user, + text.cleanup(form_result.get("text"))) + comment.latest = rev + model.meta.Session.add(motion) + model.meta.Session.add(comment) + model.meta.Session.add(rev) + + for c_text in c.canonicals: + canonical = model.Comment(motion, c.user) + canonical.canonical = True + c_rev = model.Revision(canonical, c.user, + text.cleanup(c_text)) + canonical.latest = c_rev + model.meta.Session.add(canonical) + model.meta.Session.add(c_rev) + + for r_motion, type in c.relations.items(): + if type=='a': + alternative = model.Alternative(motion, r_motion) + model.meta.Session.add(alternative) + elif type=='d': + dependency = model.Dependency(motion, r_motion) + model.meta.Session.add(dependency) + + model.meta.Session.commit() + motion.comment = comment + model.meta.Session.add(motion) + model.meta.Session.commit() + model.meta.Session.refresh(rev) + + event.emit(event.T_MOTION_CREATE, {'motion': motion}, + c.user, scopes=[c.instance], topics=[motion, motion.issue, c.instance]) + + redirect_to("/motion/%s" % str(motion.id)) + except formencode.Invalid, error: + defaults = dict(request.params) + del defaults['canonicals'] + del defaults['rel_type'] + del defaults['rel_motion'] + + if len(c.canonicals) < 2: + c.canonicals += [""] * (2 - len(c.canonicals)) + + page = render("/motion/create.html") + return formencode.htmlfill.render(page, + defaults=defaults, + errors=error.error_dict, + force_defaults=False) + return render("/motion/create.html") + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("motion.edit")) + #@validate(schema=MotionEditForm(), form="edit", post_only=True) + def edit(self, id): + self._parse_motion_id(id) + auth.require_motion_perm(c.motion, 'comment.edit') + c.issues = model.Issue.all(instance=c.instance) + c.motions = model.Motion.all(instance=c.instance) + c.relations = dict() + for dependency in c.motion.dependencies: + if not dependency.delete_time: + print "DEP", dependency + c.relations[dependency.requirement] = 'd' + for ra in c.motion.right_alternatives: + if not ra.delete_time: + c.relations[ra.left] = 'a' + for la in c.motion.left_alternatives: + if not la.delete_time: + c.relations[la.right] = 'a' + for m in c.motion.issue.motions: + if not m in c.relations.keys() and m != c.motion: + c.relations[m] = 'n' + + if request.method == "POST": + try: + self._parse_relations(c.motion) + form_result = MotionEditForm().to_python(request.params) + + c.motion.label = form_result.get("label") + c.motion.issue = form_result.get("issue") + + model.meta.Session.add(c.motion) + + now = datetime.now() + for dependency in c.motion.dependencies: + if dependency.delete_time: + continue + keep = False + for other, type in c.relations.items(): + if other == dependency.requirement and type == 'd': + keep = True + del c.relations[other] + if not keep: + dependency.delete_time = now + model.meta.Session.add(dependency) + + for alternative in c.motion.right_alternatives + \ + c.motion.left_alternatives: + if alternative.delete_time: + continue + keep = False + for other, type in c.relations.items(): + if other == alternative.other(c.motion) and type == 'a': + keep = True + del c.relations[other] + if not keep: + alternative.delete_time = now + model.meta.Session.add(alternative) + + for other, type in c.relations.items(): + if type == 'd': + dependency = model.Dependency(c.motion, other) + model.meta.Session.add(dependency) + elif type == 'a': + alternative = model.Alternative(c.motion, other) + model.meta.Session.add(alternative) + + + model.meta.Session.commit() + + event.emit(event.T_MOTION_EDIT, {'motion': c.motion}, + c.user, scopes=[c.instance], topics=[c.motion, c.motion.issue]) + + return redirect_to("/motion/%s" % str(id)) + except formencode.Invalid, error: + defaults = dict(request.params) + del defaults['rel_type'] + del defaults['rel_motion'] + page = render("/motion/edit.html") + return formencode.htmlfill.render(page, + defaults=defaults, + errors=error.error_dict, + force_defaults=False) + return render("/motion/edit.html") + + @RequireInstance + @ActionProtector(has_permission("motion.view")) + def view(self, id, format='html'): + self._parse_motion_id(id) + + h.add_meta("description", "") + h.add_meta("dc.title", text.meta_escape(c.motion.label, markdown=False)) + h.add_meta("dc.date", c.motion.create_time.strftime("%Y-%m-%d")) + h.add_meta("dc.author", text.meta_escape(c.motion.creator.name, markdown=False)) + + if format == 'rss': + events = event.q.run(event.q.topic(c.motion)) + return event.rss_feed(events, _("Motion: %s") % c.motion.label, + h.instance_url(c.instance, path="/motion/%s" % str(c.motion.id)), + description=_("Activity on the %s motion") % c.motion.label) + + h.add_rss(_("Motion: %(motion)s") % {'motion': c.motion.label}, + h.instance_url(c.instance, "/motion/%s.rss" % c.motion.id)) + + c.tile = tiles.motion.MotionTile(c.motion) + c.issue_tile = tiles.issue.IssueTile(c.motion.issue) + + return render("/motion/view.html") + + @RequireInstance + @RequireInternalRequest() + @ActionProtector(has_permission("motion.delete")) + def delete(self, id): + self._parse_motion_id(id) + auth.require_motion_perm(c.motion, 'comment.delete') + parent = c.instance.root + if len(c.motion.parents): + parent = c.motion.parents[0] + h.flash("Motion %(motion)s has been deleted." % {'motion': c.motion.label}) + + event.emit(event.T_MOTION_DELETE, {'motion': c.motion}, + c.user, scopes=[c.instance], topics=[c.motion, c.motion.issue, c.instance]) + + c.motion.delete_time = datetime.now() + model.meta.Session.add(c.motion) + model.meta.Session.commit() + redirect_to("/category/%s" % str(parent.id)) + + @RequireInstance + @ActionProtector(has_permission("motion.view")) + def votes(self, id): + self._parse_motion_id(id) + filters = dict() + try: + filters = MotionDecisionsFilterForm().to_python(request.params) + except formencode.Invalid: + pass + + if not c.motion.poll: + h.flash(_("%s is not currently in a poll, thus no votes have been counted.")) + redirect_to("/motion/%s" % str(c.motion.id)) + + decisions = democracy.Decision.for_poll(c.motion.poll) + + if filters.get('result'): + decisions = filter(lambda d: d.result==filters.get('result'), decisions) + + c.decisions_pager = NamedPager('decisions', decisions, tiles.decision.motion_row, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest}, + default_sort=sorting.entity_newest) + return render("/motion/votes.html") diff --git a/adhocracy/controllers/page.py b/adhocracy/controllers/page.py new file mode 100644 index 000000000..d309c594b --- /dev/null +++ b/adhocracy/controllers/page.py @@ -0,0 +1,36 @@ +import os, os.path +import re + +from BeautifulSoup import BeautifulSoup + +from pylons.i18n import _, get_lang + +from adhocracy.lib.base import * + +log = logging.getLogger(__name__) + +VALID_PAGE = re.compile("^[a-zA-Z0-9\_\-]*$") +STATIC_PATH = os.path.join(config.get('here'), 'adhocracy', 'templates', 'static') + +class PageController(BaseController): + + def serve(self, page_name): + if not VALID_PAGE.match(page_name): + abort(404, _('The requested page was not found')) + + page_path = os.path.join(STATIC_PATH, "%s.%s.html" % (page_name.lower(), get_lang())) + if not os.path.exists(page_path): + page_path = os.path.join(STATIC_PATH, '%s.html' % page_name.lower()) + if os.path.exists(page_path): + log.debug("Page '%s' needs to be localized to %s" % (page_name.lower(), get_lang())) + else: + abort(404, _('The requested page was not found')) + + page_content = file(page_path, 'r').read() + page_soup = BeautifulSoup(page_content) + + c.page_text = "".join(map(unicode,page_soup.findAll('body', limit=1)[0].contents)) + c.page_title = "".join(map(unicode,page_soup.findAll('title', limit=1)[0].contents)) + + return render('/template_doc.html') + \ No newline at end of file diff --git a/adhocracy/controllers/poll.py b/adhocracy/controllers/poll.py new file mode 100644 index 000000000..12cd6164f --- /dev/null +++ b/adhocracy/controllers/poll.py @@ -0,0 +1,52 @@ +import logging +from datetime import datetime + +from adhocracy.lib.base import * +from adhocracy.lib.tiles.motion_tiles import MotionTile + +log = logging.getLogger(__name__) + +class PollController(BaseController): + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("poll.create")) + def create(self, id): + self._parse_motion_id(id) + + tile = MotionTile(c.motion) + if not tile.can_begin_poll: + abort(403, _("The poll cannot be started either because there are " + + "no provisions or a poll has already started.")) + + if request.method == "POST": + poll = model.Poll(c.motion, c.user) + model.meta.Session.add(poll) + model.meta.Session.commit() + event.emit(event.T_MOTION_STATE_VOTING, {'motion': c.motion}, + c.user, scopes=[c.instance], topics=[c.motion, c.motion.issue, c.instance]) + redirect_to("/motion/%s" % str(c.motion.id)) + return render("/poll/create.html") + + @RequireInstance + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("poll.abort")) + def abort(self, id): + self._parse_motion_id(id) + auth.require_motion_perm(c.motion, 'poll.abort', enforce_immutability=False) + if not c.motion.poll: + h.flash(_("The motion is not undergoing a poll.")) + redirect_to("/motion/%s" % str(c.motion.id)) + + if request.method == "POST": + poll = c.motion.poll + poll.end_time = datetime.now() + poll.end_user = c.user + model.meta.Session.add(poll) + model.meta.Session.commit() + event.emit(event.T_MOTION_STATE_REDRAFT, {'motion': c.motion}, + c.user, scopes=[c.instance], topics=[c.motion, c.motion.issue, c.instance]) + redirect_to("/motion/%s" % str(c.motion.id)) + + return render("/poll/abort.html") + diff --git a/adhocracy/controllers/root.py b/adhocracy/controllers/root.py new file mode 100644 index 000000000..b652b3c3f --- /dev/null +++ b/adhocracy/controllers/root.py @@ -0,0 +1,64 @@ +from pylons.i18n import _ + +from adhocracy.lib.base import * + +log = logging.getLogger(__name__) + +class RootController(BaseController): + + def index(self, format='html'): + if c.instance: + redirect_to('/instance/%s' % str(c.instance.key)) + + if format == 'rss': + query = None + if c.user: + query = event.q._or(event.q.scope(c.user), "scope:wall", + *map(event.q.topic, c.user.instances)) + else: + query = event.q._or(map(event.q.topic, model.Instance.all())) + events = event.q.run(query) + return event.rss_feed(events, _('My Adhocracies'), + h.instance_url(None), + _("Updates from the Adhocracies in which you are a member")) + else: + c.instances = model.Instance.all()[:5] + + return render('index.html') + + #@RequireInstance + def dispatch_delegateable(self, id): + if c.instance: + dgb = model.Delegateable.find(id) + else: + dgb = model.Delegateable.find(id, instance_filter=False) + if not dgb: + abort(404, _("No motion or category with ID %(id)s exists") % {'id': id}) + + id = str(id) + + if isinstance(dgb, model.Category): + redirect_to(h.instance_url(dgb.instance, path="/category/%s" % id)) + + if isinstance(dgb, model.Issue): + redirect_to(h.instance_url(dgb.instance, path="/issue/%s" % id)) + + redirect_to(h.instance_url(dgb.instance, path="/motion/%s" % id)) + + def sitemap_xml(self): + c.delegateables = [] + def add_delegateables(instance): + children = instance.root.search_children(recurse=True) + c.delegateables.extend(children) + + response.content_type = "text/xml" + + if c.instance: + add_delegateables(c.instance) + else: + instances = model.Instance.all() + for instance in instances: + add_delegateables(instance) + + return render("sitemap.xml") + \ No newline at end of file diff --git a/adhocracy/controllers/search.py b/adhocracy/controllers/search.py new file mode 100644 index 000000000..72786674a --- /dev/null +++ b/adhocracy/controllers/search.py @@ -0,0 +1,30 @@ +from lucene import JavaError + +from pylons.i18n import _ + +from adhocracy.lib.base import * +import adhocracy.model as model + +log = logging.getLogger(__name__) + +class SearchQueryForm(formencode.Schema): + allow_extra_fields = True + q = validators.String(max=255, min=1, not_empty=True) + +class SearchController(BaseController): + + @RequireInstance + @ActionProtector(has_permission("motion.view")) + def query(self): + try: + c.query = SearchQueryForm().to_python(request.params).get("q") + c.entities = libsearch.query.run(c.query, instance=c.instance) + c.entities = filter(lambda e: not isinstance(e, model.Comment), c.entities) + c.entities_pager = NamedPager('serp', c.entities, tiles.dispatch_row, q=c.query) + except formencode.Invalid: + h.flash(_("Received no query for search.")) + except JavaError, je: + h.flash("Error: %s" % je.message) + + return formencode.htmlfill.render(render("search/results.html"), + {'q': c.query}) diff --git a/adhocracy/controllers/user.py b/adhocracy/controllers/user.py new file mode 100644 index 000000000..40f90d513 --- /dev/null +++ b/adhocracy/controllers/user.py @@ -0,0 +1,270 @@ +import urllib + +from pylons.i18n import _ +from babel import Locale + +import adhocracy.lib.text as text +from adhocracy.lib.base import * +import adhocracy.model.forms as forms +import adhocracy.lib.text.i18n as i18n +import adhocracy.lib.util as libutil +import adhocracy.lib.mail as libmail + +log = logging.getLogger(__name__) + +class UserCreateForm(formencode.Schema): + allow_extra_fields = True + user_name = formencode.All(validators.PlainText(), + forms.UniqueUsername()) + email = formencode.All(validators.Email(), + forms.UniqueEmail()) + password = validators.String(not_empty=True) + password_confirm = validators.String(not_empty=True) + chained_validators = [validators.FieldsMatch( + 'password', 'password_confirm')] + +class UserEditForm(formencode.Schema): + allow_extra_fields = True + display_name = validators.String(not_empty=False) + email = validators.Email(not_empty=True) + locale = validators.String(not_empty=False) + password = validators.String(not_empty=False) + password_confirm = validators.String(not_empty=False) + chained_validators = [validators.FieldsMatch( + 'password', 'password_confirm')] + bio = validators.String(max=1000, min=0, not_empty=False) + +class UserManageForm(formencode.Schema): + allow_extra_fields = True + group = forms.ValidGroup() + +class UserResetApplyForm(formencode.Schema): + allow_extra_fields = True + email = validators.Email(not_empty=True) + +class UserController(BaseController): + + @ActionProtector(has_permission("user.view")) + def index(self): + c.users = model.User.all(instance_filter=True if c.instance else False) + c.users_pager = NamedPager('users', c.users, tiles.user.row, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest, + _("karma"): sorting.user_karma, + _("activity"): sorting.user_activity, + _("name"): sorting.user_name}, + default_sort=sorting.user_karma) + return render("/user/index.html") + + @RequireInternalRequest(methods=['POST']) + @validate(schema=UserCreateForm(), form="create", post_only=True) + def create(self): + if request.method == "POST": + user = model.User(self.form_result.get("user_name"), + self.form_result.get("email"), + self.form_result.get("password")) + user.locale = c.locale + model.meta.Session.add(user) + + # the plan is to make this configurable via the instance preferences + # screen. + grp = model.Group.by_code(model.Group.CODE_DEFAULT) + membership = model.Membership(user, None, grp) + model.meta.Session.add(membership) + model.meta.Session.commit() + + event.emit(event.T_USER_CREATE, {}, user) + + if c.instance: + session['came_from'] = "/instance/join/%s?%s" % (c.instance.key, h.url_token()) + login_page = render("/user/login.html") + redirect_to("/user/perform_login?%s" % urllib.urlencode({ + 'login': self.form_result.get("user_name"), + 'password': self.form_result.get("password") + })) + return render("/user/login.html") + + @RequireInternalRequest(methods=['POST']) + @ActionProtector(has_permission("user.edit")) + @validate(schema=UserEditForm(), form="edit", post_only=True) + def edit(self, id): + c.page_user = model.User.find(id, instance_filter=False) + if not c.page_user: + abort(404, _("No user named '%s' exists") % id) + if not (c.page_user == c.user or h.has_permission("user.manage")): + abort(403, _("You're not authorized to change %s's settings.") % id) + if request.method == "POST": + if self.form_result.get("password"): + c.page_user.password = self.form_result.get("password") + c.page_user.display_name = self.form_result.get("display_name") + c.page_user.bio = text.cleanup(self.form_result.get("bio")) + c.page_user.email = self.form_result.get("email").lower() + locale = Locale(self.form_result.get("locale")) + if locale and locale in i18n.LOCALES: + c.page_user.locale = locale + model.meta.Session.add(c.page_user) + model.meta.Session.commit() + model.meta.Session.refresh(c.page_user) + if c.page_user == c.user: + event.emit(event.T_USER_EDIT, {}, c.user) + else: + event.emit(event.T_USER_ADMIN_EDIT, {'user': c.page_user}, + c.user, topics=[c.page_user]) + redirect_to("/user/%s" % str(c.page_user.user_name)) + return render("/user/edit.html") + + @validate(schema=UserResetApplyForm(), form="reset", post_only=True) + def reset(self): + if request.method == "POST": + email = self.form_result.get('email').lower() + query = model.meta.Session.query(model.User) + query = query.filter(model.User.email==email) + users = query.all() + if not len(users): + h.flash(_("There is no user registered with that email address.")) + return render("/user/reset_form.html") + c.page_user = users[0] + c.page_user.reset_code = libutil.random_token() + model.meta.Session.add(c.page_user) + model.meta.Session.commit() + url = h.instance_url(None, path="/user/reset/%s?c=%s" % (c.page_user.user_name, c.page_user.reset_code)) + body = _("you have requested that your password for Adhocracy be reset. In order to" + + " confirm the validity of your claim, please open the link below in your" + + " browser:") + "\r\n\r\n " + url + libmail.to_user(c.page_user, _("Reset your password"), body) + return render("/user/reset_pending.html") + return render("/user/reset_form.html") + + def reset_code(self, id): + c.page_user = model.User.find(id, instance_filter=False) + if not c.page_user: + abort(404, _("No user named '%s' exists") % id) + try: + code = request.params.get('c', 'deadbeef') + if c.page_user.reset_code != code: + raise ValueError + + new_password = libutil.random_token() + c.page_user.password = new_password + model.meta.Session.add(c.page_user) + model.meta.Session.commit() + + body = _("your password has been reset. It is now:") + "\r\n\r\n " + new_password + "\r\n\r\n" + body += _("Please login and change the password in your user settings.") + libmail.to_user(c.page_user, _("Your new password"), body) + h.flash(_("Success. You have been sent an email with your new password.")) + except Exception: + h.flash(_("The reset code is invalid. Please repeat the password recovery procedure.")) + redirect_to('/login') + + + @ActionProtector(has_permission("user.view")) + def view(self, id, format='html'): + c.page_user = model.User.find(id, instance_filter=False) + if not c.page_user: + abort(404, _("No user named '%s' exists") % id) + + bio = c.page_user.bio + if not bio: + bio = _("%(user)s is using Adhocracy, a direct democracy decision-making tool.") % {'user': c.page_user.name} + + description = h.text.truncate(text.meta_escape(bio), length=200, whole_word=True) + + h.add_meta("description", description) + h.add_meta("dc.title", text.meta_escape(c.page_user.name)) + h.add_meta("dc.date", c.page_user.access_time.strftime("%Y-%m-%d")) + h.add_meta("dc.author", text.meta_escape(c.page_user.name)) + + h.add_rss(_("%(user)ss Activity") % {'user': c.page_user.name}, + h.instance_url(None, "/user/%s.rss" % c.page_user.user_name)) + + if c.instance and not c.page_user.is_member(c.instance): + h.flash(_("%s is not a member of %s") % (c.page_user.name, c.instance.label)) + + events = event.q.run(event.q._or(event.q.agent(c.page_user), + event.q.topic(c.page_user))) + + c.events_pager = NamedPager('events', events, tiles.event.list_item) + if format == 'rss': + return event.rss_feed(events, "%s Latest Actions" % c.page_user.name, + h.instance_url(None, path='/user/%s' % c.page_user.user_name), + description) + c.tile = tiles.user.UserTile(c.page_user) + + return render("/user/view.html") + + def login(self): + if 'came_from' in request.params: + session['came_from'] = request.params.get('came_from') + session.save() + return render('/user/login.html') + + def perform_login(self): + #print "PERFORM LOGIN ", request.params + pass + + def post_login(self): + if c.user: + if 'came_from' in session: + url = str(session['came_from']) + del session['came_from'] + session.save() + redirect_to(url) + #h.flash("Welcome back, %s" % c.user.user_name) + redirect_to("/") + else: + return formencode.htmlfill.render( + render("/user/login.html"), + errors = {"login": _("Invalid user name or password")}) + + def logout(self): + pass # managed by repoze.who + + def post_logout(self): + #h.flash("Good-bye and thanks for visiting!") + redirect_to("/") + + @ActionProtector(has_permission("user.view")) + def autocomplete(self): + try: + prefix = unicode(request.params['q']) + limit = int(request.params.get('limit', 5)) + users = model.User.complete(prefix, limit) + result = "" + for user in users: + s = user.name + if user.user_name != user.name: + s = "%s (%s)" % (user.user_name, s) + result += "{s: '%s', k: '%s'}" % (s, user.user_name) + return result + except Exception, e: + return "" + + @RequireInstance + @ActionProtector(has_permission("user.view")) + def votes(self, id): + c.page_user = model.User.find(id, instance_filter=False) + if not c.page_user: + abort(404, _("No user named '%s' exists") % id) + + decisions = democracy.Decision.for_user(c.page_user, c.instance) + + c.decisions_pager = NamedPager('decisions', decisions, tiles.decision.user_row, + sorts={_("oldest"): sorting.entity_oldest, + _("newest"): sorting.entity_newest}, + default_sort=sorting.entity_newest) + return render("/user/votes.html") + + @RequireInstance + @ActionProtector(has_permission("delegation.view")) + def delegations(self, id): + c.page_user = model.User.find(id, instance_filter=False) + if not c.page_user: + abort(404, _("No user named '%s' exists") % id) + + cat_id = request.params.get('cat', c.instance.root.id) + c.category = forms.ValidCategory().to_python(cat_id) + c.nodeClass = democracy.DelegationNode + + return render("/user/delegations.html") + \ No newline at end of file diff --git a/adhocracy/controllers/vote.py b/adhocracy/controllers/vote.py new file mode 100644 index 000000000..aa5c95e6b --- /dev/null +++ b/adhocracy/controllers/vote.py @@ -0,0 +1,33 @@ +from pylons.i18n import _ + +from adhocracy.lib.base import * +from adhocracy.model.forms import VoteCastForm + +log = logging.getLogger(__name__) + +class VoteController(BaseController): + + @RequireInstance + @RequireInternalRequest() + @validate(schema=VoteCastForm(), form="cast_error", post_only=False, on_get=True) + def cast(self, id): + motion = model.Motion.find(id) + if not motion: + abort(404, _("No motion with ID %(id)s exists.") % {'id': id}) + if not h.has_permission("vote.cast"): + h.flash(_("You have no voting rights.")) + redirect_to("/motion/%s" % str(id)) + if not motion.poll: + h.flash(_("This motion is not currently being voted on.")) + redirect_to("/motion/%s" % str(id)) + + orientation = self.form_result.get("orientation") + votes = democracy.Decision(c.user, motion.poll).make(orientation) + if len(votes): + event.emit(event.T_VOTE_CAST, {'vote': votes[0], 'motion': motion}, + c.user, scopes=[c.instance], topics=[motion, motion]) + redirect_to("/motion/%s" % str(id)) + + def cast_error(self, id): + h.flash(_("Illegal input for vote cast.")) + redirect_to("/motion/%s" % str(id)) diff --git a/adhocracy/i18n/adhocracy.pot b/adhocracy/i18n/adhocracy.pot new file mode 100644 index 000000000..843bf4f2e --- /dev/null +++ b/adhocracy/i18n/adhocracy.pot @@ -0,0 +1,2030 @@ +# Translations template for adhocracy. +# Copyright (C) 2009 ORGANIZATION +# This file is distributed under the same license as the adhocracy project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2009. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: adhocracy 0.2\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2009-10-29 13:31+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.0dev-r0\n" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:61 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:62 +#: adhocracy/contrib/babel/babel/tests/support.py:62 +#: adhocracy/contrib/babel/babel/tests/support.py:67 +#: adhocracy/contrib/babel/babel/tests/support.py:114 +msgid "foo" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:63 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:64 +msgid "There is" +msgid_plural "There are" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:65 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:66 +msgid "Fizz" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:67 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:68 +msgid "Fuzz" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:69 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:70 +msgid "Fuzzes" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/file1.py:8 +msgid "bar" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/CVS/this_wont_normally_be_here.py:11 +msgid "FooBar" +msgid_plural "FooBars" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/contrib/babel/babel/tests/support.py:78 +#: adhocracy/contrib/babel/babel/tests/support.py:80 +#: adhocracy/contrib/babel/babel/tests/support.py:90 +#: adhocracy/contrib/babel/babel/tests/support.py:92 +#: adhocracy/contrib/babel/babel/tests/support.py:132 +#: adhocracy/contrib/babel/babel/tests/support.py:134 +msgid "foo1" +msgid_plural "foos1" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/controllers/admin.py:69 +#, python-format +msgid "%(user)s is not a member of %(instance)s" +msgstr "" + +#: adhocracy/controllers/admin.py:93 +#, python-format +msgid "%(user)s was removed from %(instance)s" +msgstr "" + +#: adhocracy/controllers/admin.py:97 +#, python-format +msgid "%(user)s isn't a member of %(instance)s" +msgstr "" + +#: adhocracy/controllers/category.py:42 adhocracy/controllers/category.py:66 +#, python-format +msgid "No category with ID '%s' exists." +msgstr "" + +#: adhocracy/controllers/category.py:86 adhocracy/controllers/category.py:90 +#: adhocracy/templates/category/view.html:25 +#, python-format +msgid "Category: %s" +msgstr "" + +#: adhocracy/controllers/category.py:94 adhocracy/controllers/category.py:101 +#: adhocracy/controllers/comment.py:143 adhocracy/controllers/delegation.py:77 +#: adhocracy/controllers/instance.py:52 adhocracy/controllers/instance.py:98 +#: adhocracy/controllers/instance.py:105 adhocracy/controllers/issue.py:99 +#: adhocracy/controllers/motion.py:36 adhocracy/controllers/motion.py:190 +#: adhocracy/controllers/user.py:51 adhocracy/controllers/user.py:253 +msgid "oldest" +msgstr "" + +#: adhocracy/controllers/category.py:95 adhocracy/controllers/category.py:102 +#: adhocracy/controllers/comment.py:144 adhocracy/controllers/delegation.py:78 +#: adhocracy/controllers/instance.py:53 adhocracy/controllers/instance.py:99 +#: adhocracy/controllers/instance.py:106 adhocracy/controllers/issue.py:100 +#: adhocracy/controllers/motion.py:37 adhocracy/controllers/motion.py:191 +#: adhocracy/controllers/user.py:52 adhocracy/controllers/user.py:254 +msgid "newest" +msgstr "" + +#: adhocracy/controllers/category.py:96 adhocracy/controllers/category.py:103 +#: adhocracy/controllers/instance.py:54 adhocracy/controllers/instance.py:100 +#: adhocracy/controllers/instance.py:107 adhocracy/controllers/issue.py:101 +#: adhocracy/controllers/motion.py:38 adhocracy/controllers/user.py:54 +msgid "activity" +msgstr "" + +#: adhocracy/controllers/category.py:97 adhocracy/controllers/category.py:104 +#: adhocracy/controllers/instance.py:55 adhocracy/controllers/instance.py:101 +#: adhocracy/controllers/instance.py:108 adhocracy/controllers/issue.py:102 +#: adhocracy/controllers/motion.py:40 adhocracy/controllers/user.py:55 +msgid "name" +msgstr "" + +#: adhocracy/controllers/category.py:114 +msgid "Deleting the root category isn't possible." +msgstr "" + +#: adhocracy/controllers/category.py:117 +#, python-format +msgid "No category with ID '%(id)s' exists." +msgstr "" + +#: adhocracy/controllers/category.py:130 +#, python-format +msgid "Category '%(category)s' has been deleted." +msgstr "" + +#: adhocracy/controllers/comment.py:43 +msgid "Unsupported topic type." +msgstr "" + +#: adhocracy/controllers/comment.py:84 adhocracy/controllers/comment.py:103 +#: adhocracy/controllers/comment.py:112 adhocracy/controllers/comment.py:122 +#: adhocracy/controllers/comment.py:139 adhocracy/controllers/comment.py:155 +#, python-format +msgid "No comment with ID %s exists" +msgstr "" + +#: adhocracy/controllers/comment.py:159 +msgid "" +"You're trying to revert to a revision which is not part of this comments " +"history" +msgstr "" + +#: adhocracy/controllers/delegation.py:21 +#, python-format +msgid "No motion or category with ID '%(id)s' exists" +msgstr "" + +#: adhocracy/controllers/delegation.py:52 adhocracy/controllers/delegation.py:70 +#, python-format +msgid "Couldn't find delegation %(id)s" +msgstr "" + +#: adhocracy/controllers/delegation.py:54 +#, python-format +msgid "Cannot access delegation %(id)s" +msgstr "" + +#: adhocracy/controllers/delegation.py:63 +msgid "The delegation is now revoked." +msgstr "" + +#: adhocracy/controllers/instance.py:42 +#, python-format +msgid "No such adhocracy exists: %(key)s" +msgstr "" + +#: adhocracy/controllers/instance.py:47 +msgid "" +"An index of adhocracies run at adhocracy.cc. Select which ones you would like" +" to join and participate in!" +msgstr "" + +#: adhocracy/controllers/instance.py:89 +#, python-format +msgid "%s News" +msgstr "" + +#: adhocracy/controllers/instance.py:91 +#, python-format +msgid "News from the %s Adhocracy" +msgstr "" + +#: adhocracy/controllers/instance.py:171 +msgid "Deleting an instance is not currently implemented" +msgstr "" + +#: adhocracy/controllers/instance.py:178 +#, python-format +msgid "You're already a member in %(instance)s." +msgstr "" + +#: adhocracy/controllers/instance.py:192 +#, python-format +msgid "Welcome to %(instance)s" +msgstr "" + +#: adhocracy/controllers/instance.py:201 +#, python-format +msgid "You're not a member of %(instance)s." +msgstr "" + +#: adhocracy/controllers/instance.py:204 +#, python-format +msgid "You're the founder of %s, cannot leave." +msgstr "" + +#: adhocracy/controllers/issue.py:60 adhocracy/controllers/issue.py:80 +#: adhocracy/controllers/issue.py:115 +#, python-format +msgid "No issue with ID %s exists." +msgstr "" + +#: adhocracy/controllers/issue.py:90 adhocracy/templates/issue/view.html:27 +#, python-format +msgid "Issue: %s" +msgstr "" + +#: adhocracy/controllers/issue.py:92 +#, python-format +msgid "Activity on the %s issue" +msgstr "" + +#: adhocracy/controllers/issue.py:94 +#, python-format +msgid "Issue: %(issue)s" +msgstr "" + +#: adhocracy/controllers/issue.py:124 +#, python-format +msgid "" +"The issue %(issue)s cannot be deleted, because the contained motion " +"%(motion)s is polling." +msgstr "" + +#: adhocracy/controllers/issue.py:131 +#, python-format +msgid "Issue '%(issue)s' has been deleted." +msgstr "" + +#: adhocracy/controllers/karma.py:28 +msgid "Invalid karma value. Karma is either positive or negative!" +msgstr "" + +#: adhocracy/controllers/motion.py:39 +msgid "urgency" +msgstr "" + +#: adhocracy/controllers/motion.py:106 +#, python-format +msgid "Motion: %s" +msgstr "" + +#: adhocracy/controllers/motion.py:108 +#, python-format +msgid "Activity on the %s motion" +msgstr "" + +#: adhocracy/controllers/motion.py:110 +#, python-format +msgid "Motion: %(motion)s" +msgstr "" + +#: adhocracy/controllers/motion.py:146 +msgid "" +"The poll cannot be started either because there are no provisions or a poll " +"has already started." +msgstr "" + +#: adhocracy/controllers/motion.py:170 +msgid "The motion is not undergoing a poll." +msgstr "" + +#: adhocracy/controllers/motion.py:194 +#, python-format +msgid "%s is not currently in a poll, thus no votes have been counted." +msgstr "" + +#: adhocracy/controllers/page.py:19 adhocracy/controllers/page.py:27 +msgid "The requested page was not found" +msgstr "" + +#: adhocracy/controllers/root.py:21 adhocracy/lib/base.py:72 +#: adhocracy/templates/index.html:22 +msgid "My Adhocracies" +msgstr "" + +#: adhocracy/controllers/root.py:23 +msgid "Updates from the Adhocracies in which you are a member" +msgstr "" + +#: adhocracy/controllers/root.py:36 +#, python-format +msgid "No motion or category with ID %(id)s exists" +msgstr "" + +#: adhocracy/controllers/search.py:25 +msgid "Received no query for search." +msgstr "" + +#: adhocracy/controllers/user.py:53 +msgid "karma" +msgstr "" + +#: adhocracy/controllers/user.py:93 adhocracy/controllers/user.py:141 +#: adhocracy/controllers/user.py:165 adhocracy/controllers/user.py:248 +#: adhocracy/controllers/user.py:263 +#, python-format +msgid "No user named '%s' exists" +msgstr "" + +#: adhocracy/controllers/user.py:95 +#, python-format +msgid "You're not authorized to change %s's settings." +msgstr "" + +#: adhocracy/controllers/user.py:124 +msgid "There is no user registered with that email address." +msgstr "" + +#: adhocracy/controllers/user.py:131 +msgid "" +"you have requested that your password for Adhocracy be reset. In order to " +"confirm the validity of your claim, please open the link below in your " +"browser:" +msgstr "" + +#: adhocracy/controllers/user.py:134 +msgid "Reset your password" +msgstr "" + +#: adhocracy/controllers/user.py:152 +msgid "your password has been reset. It is now:" +msgstr "" + +#: adhocracy/controllers/user.py:153 +msgid "Please login and change the password in your user settings." +msgstr "" + +#: adhocracy/controllers/user.py:154 +msgid "Your new password" +msgstr "" + +#: adhocracy/controllers/user.py:155 +msgid "Success. You have been sent an email with your new password." +msgstr "" + +#: adhocracy/controllers/user.py:157 +msgid "The reset code is invalid. Please repeat the password recovery procedure." +msgstr "" + +#: adhocracy/controllers/user.py:169 +#, python-format +msgid "%(user)s is using Adhocracy, a direct democracy decision-making tool." +msgstr "" + +#: adhocracy/controllers/user.py:178 +#, python-format +msgid "%(user)ss Activity" +msgstr "" + +#: adhocracy/controllers/user.py:182 +#, python-format +msgid "%s is not a member of %s" +msgstr "" + +#: adhocracy/controllers/user.py:218 +msgid "Invalid user name or password" +msgstr "" + +#: adhocracy/controllers/vote.py:16 adhocracy/lib/base.py:43 +#, python-format +msgid "No motion with ID %(id)s exists." +msgstr "" + +#: adhocracy/controllers/vote.py:18 +msgid "You have no voting rights." +msgstr "" + +#: adhocracy/controllers/vote.py:27 +msgid "This motion is not currently being voted on." +msgstr "" + +#: adhocracy/controllers/vote.py:31 +msgid "Illegal input for vote cast." +msgstr "" + +#: adhocracy/lib/base.py:77 +msgid "" +"A liquid democracy platform for making decisions in distributed, open groups " +"by cooperatively creating proposals and voting on them to establish their " +"support." +msgstr "" + +#: adhocracy/lib/base.py:80 +msgid "" +"adhocracy, direct democracy, liquid democracy, liqd, democracy, wiki, " +"voting,participation, group decisions, decisions, decision-making" +msgstr "" + +#: adhocracy/lib/helpers.py:33 adhocracy/templates/template.html:36 +#: adhocracy/templates/template.html:77 adhocracy/templates/user/parts.html:5 +msgid "Adhocracy" +msgstr "" + +#: adhocracy/lib/helpers.py:51 +msgid "This motion is currently being voted on and cannot be modified." +msgstr "" + +#: adhocracy/lib/helpers.py:103 +msgid "You" +msgstr "" + +#: adhocracy/lib/mail.py:17 +#, python-format +msgid "Hi %s," +msgstr "" + +#: adhocracy/lib/mail.py:19 +msgid "" +"Cheers,\r\n" +"\r\n" +" the Adhocracy Team\r\n" +msgstr "" + +#: adhocracy/lib/xsrf.py:49 +msgid "" +"Action failed. You were probably trying to re-perform an action after using " +"your browser's 'Back' button. This is prohibited for security reasons." +msgstr "" + +#: adhocracy/lib/event/event.py:64 +msgid "(Undefined)" +msgstr "" + +#: adhocracy/lib/event/formatting.py:79 +msgid "voted for" +msgstr "" + +#: adhocracy/lib/event/formatting.py:80 +msgid "abstained on" +msgstr "" + +#: adhocracy/lib/event/formatting.py:81 +msgid "voted against" +msgstr "" + +#: adhocracy/lib/event/formatting.py:89 +msgid "comment" +msgstr "" + +#: adhocracy/lib/event/types.py:44 +msgid "signed up" +msgstr "" + +#: adhocracy/lib/event/types.py:45 +msgid "edited their profile" +msgstr "" + +#: adhocracy/lib/event/types.py:46 +#, python-format +msgid "edited %(user)ss profile" +msgstr "" + +#: adhocracy/lib/event/types.py:47 +#, python-format +msgid "founded the %(instance)s Adhocracy" +msgstr "" + +#: adhocracy/lib/event/types.py:48 +#, python-format +msgid "updated the %(instance)s Adhocracy" +msgstr "" + +#: adhocracy/lib/event/types.py:49 +#, python-format +msgid "deleted the %(instance)s Adhocracy" +msgstr "" + +#: adhocracy/lib/event/types.py:50 +#, python-format +msgid "joined %(instance)s" +msgstr "" + +#: adhocracy/lib/event/types.py:51 +#, python-format +msgid "left %(instance)s" +msgstr "" + +#: adhocracy/lib/event/types.py:52 +#, python-format +msgid "was forced to leave %(instance)s by %(user)s" +msgstr "" + +#: adhocracy/lib/event/types.py:53 +#, python-format +msgid "now is a %(group)s within %(instance)s" +msgstr "" + +#: adhocracy/lib/event/types.py:54 +#, python-format +msgid "created %(issue)s" +msgstr "" + +#: adhocracy/lib/event/types.py:55 +#, python-format +msgid "edited %(issue)s" +msgstr "" + +#: adhocracy/lib/event/types.py:56 +#, python-format +msgid "deleted %(issue)s" +msgstr "" + +#: adhocracy/lib/event/types.py:57 +#, python-format +msgid "created %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:58 +#, python-format +msgid "edited %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:59 +#, python-format +msgid "re-drafted %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:60 +#, python-format +msgid "called a vote on %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:61 +#, python-format +msgid "deleted %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:62 +#, python-format +msgid "named %(user)s as an editor for %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:63 +#, python-format +msgid "removed %(user)s from the editors of %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:64 +#, python-format +msgid "created the category %(category)s in %(parent)s" +msgstr "" + +#: adhocracy/lib/event/types.py:65 +#, python-format +msgid "updated the category %(category)s" +msgstr "" + +#: adhocracy/lib/event/types.py:66 +#, python-format +msgid "deleted the category %(category)s" +msgstr "" + +#: adhocracy/lib/event/types.py:67 +#, python-format +msgid "created a %(comment)s on %(delegateable)s" +msgstr "" + +#: adhocracy/lib/event/types.py:68 +#, python-format +msgid "edited a %(comment)s on %(delegateable)s" +msgstr "" + +#: adhocracy/lib/event/types.py:69 +#, python-format +msgid "deleted a %(comment)s from %(delegateable)s" +msgstr "" + +#: adhocracy/lib/event/types.py:70 +#, python-format +msgid "delegated voting on %(scope)s to %(agent)s" +msgstr "" + +#: adhocracy/lib/event/types.py:71 +#, python-format +msgid "revoked their delegation on %(scope)s to %(agent)s" +msgstr "" + +#: adhocracy/lib/event/types.py:72 +#, python-format +msgid "%(vote)s %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:73 +#, python-format +msgid "test %(test)s" +msgstr "" + +#: adhocracy/lib/instance/__init__.py:13 +msgid "This action is only available in an instance context." +msgstr "" + +#: adhocracy/lib/karma/threshold.py:18 +msgid "create a category" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:19 +msgid "edit this category" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:20 +msgid "delete this category" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:21 +msgid "reply in a comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:22 +msgid "edit this comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:23 +msgid "delete this comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:24 +msgid "rate this comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:25 +msgid "create a motion" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:26 +msgid "edit this motion" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:27 +msgid "call for a vote" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:28 +msgid "cancel a vote" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:29 +msgid "delete a motion" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:30 +msgid "create an issue" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:31 +msgid "edit this issue" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:32 +msgid "delete this issue" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:33 +#, python-format +msgid "You need %s karma to %s" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:34 +msgid "do this" +msgstr "" + +#: adhocracy/lib/text/i18n.py:45 +msgid "Today" +msgstr "" + +#: adhocracy/lib/text/i18n.py:47 +msgid "Yesterday" +msgstr "" + +#: adhocracy/lib/text/i18n.py:61 +#, python-format +msgid "%(ts)s ago" +msgstr "" + +#: adhocracy/model/forms.py:27 +msgid "No username is given" +msgstr "" + +#: adhocracy/model/forms.py:31 +msgid "The username is invalid" +msgstr "" + +#: adhocracy/model/forms.py:35 +msgid "That username already exists" +msgstr "" + +#: adhocracy/model/forms.py:44 +msgid "That email is already registered" +msgstr "" + +#: adhocracy/model/forms.py:52 +msgid "No instance key is given" +msgstr "" + +#: adhocracy/model/forms.py:56 +msgid "The instance key is invalid" +msgstr "" + +#: adhocracy/model/forms.py:60 +msgid "An instance with that key already exists" +msgstr "" + +#: adhocracy/model/forms.py:69 +#, python-format +msgid "No entity with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:105 +#, python-format +msgid "No group with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:114 +#, python-format +msgid "No revision with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:123 +#, python-format +msgid "No comment with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:131 +#, python-format +msgid "'%s' is not a valid motion state." +msgstr "" + +#: adhocracy/model/forms.py:140 +#, python-format +msgid "No user with the user name '%s' exists" +msgstr "" + +#: adhocracy/model/motion.py:38 +msgid "Motion doesn't have a distinct parent issue." +msgstr "" + +#: adhocracy/templates/components.html:4 +msgid "formatting hints" +msgstr "" + +#: adhocracy/templates/components.html:11 +msgid "Save" +msgstr "" + +#: adhocracy/templates/components.html:13 +msgid "or" +msgstr "" + +#: adhocracy/templates/components.html:13 adhocracy/templates/motion/view.html:64 +#: adhocracy/templates/motion/view.html:66 adhocracy/templates/motion/view.html:68 +msgid "cancel" +msgstr "" + +#: adhocracy/templates/components.html:21 adhocracy/templates/index.html:10 +msgid "Say hi" +msgstr "" + +#: adhocracy/templates/components.html:24 +msgid "If you're not registered yet, <a href='/register'>sign up here</a>." +msgstr "" + +#: adhocracy/templates/components.html:33 +msgid "Delegate voting" +msgstr "" + +#: adhocracy/templates/components.html:38 +msgid "You have voted yourself and not delegated voting." +msgstr "" + +#: adhocracy/templates/components.html:39 +msgid "Info..." +msgstr "" + +#: adhocracy/templates/components.html:41 +msgid "You have not delegated voting." +msgstr "" + +#: adhocracy/templates/components.html:42 adhocracy/templates/category/view.html:22 +#: adhocracy/templates/instance/view.html:23 adhocracy/templates/issue/view.html:24 +#: adhocracy/templates/motion/view.html:28 +msgid "delegate" +msgstr "" + +#: adhocracy/templates/components.html:43 +msgid "info..." +msgstr "" + +#: adhocracy/templates/components.html:47 +msgid "By voting yourself, you have overridden:" +msgstr "" + +#: adhocracy/templates/components.html:49 +msgid "You have delegated voting to:" +msgstr "" + +#: adhocracy/templates/components.html:56 +#: adhocracy/templates/decision/tiles.html:50 +msgid "on" +msgstr "" + +#: adhocracy/templates/components.html:57 +#: adhocracy/templates/decision/tiles.html:51 +msgid "review" +msgstr "" + +#: adhocracy/templates/components.html:66 +msgid "You hold an additional vote." +msgstr "" + +#: adhocracy/templates/components.html:68 +#, python-format +msgid "You hold %s additional votes." +msgstr "" + +#: adhocracy/templates/components.html:81 +msgid "What now?" +msgstr "" + +#: adhocracy/templates/components.html:81 +msgid "— using Adhocracy in 3<sup>½</sup> steps:" +msgstr "" + +#: adhocracy/templates/components.html:87 +msgid "Create and discuss issues that need solutions." +msgstr "" + +#: adhocracy/templates/components.html:92 +msgid "Cooperate to develop proposals affecting the issues." +msgstr "" + +#: adhocracy/templates/components.html:96 +msgid "Vote on proposals to collectively make decisions.*" +msgstr "" + +#: adhocracy/templates/components.html:101 +msgid "*Or — if you like — delegate voting in some fields to a peer." +msgstr "" + +#: adhocracy/templates/index.html:3 +msgid "Welcome" +msgstr "" + +#: adhocracy/templates/index.html:5 +msgid "Welcome to Adhocracy" +msgstr "" + +#: adhocracy/templates/index.html:9 +msgid "sign up" +msgstr "" + +#: adhocracy/templates/index.html:15 +msgid "If you're not registered, <a href='/register'>sign up.</a>" +msgstr "" + +#: adhocracy/templates/index.html:20 adhocracy/templates/index.html:36 +#: adhocracy/templates/user/view.html:43 +msgid "more" +msgstr "" + +#: adhocracy/templates/index.html:21 adhocracy/templates/index.html:37 +#: adhocracy/templates/category/view.html:47 +#: adhocracy/templates/category/view.html:49 +#: adhocracy/templates/category/view.html:67 +#: adhocracy/templates/category/view.html:69 +#: adhocracy/templates/instance/index.html:10 +#: adhocracy/templates/instance/view.html:51 +#: adhocracy/templates/instance/view.html:53 +#: adhocracy/templates/instance/view.html:72 +#: adhocracy/templates/instance/view.html:74 adhocracy/templates/issue/view.html:53 +#: adhocracy/templates/issue/view.html:55 adhocracy/templates/motion/view.html:189 +#: adhocracy/templates/motion/view.html:191 +#: adhocracy/templates/motion/view.html:193 +msgid "new" +msgstr "" + +#: adhocracy/templates/index.html:31 +msgid "Join a few Adhocracies to contribute to their policies." +msgstr "" + +#: adhocracy/templates/index.html:38 adhocracy/templates/template.html:114 +#: adhocracy/templates/instance/index.html:3 +#: adhocracy/templates/instance/index.html:12 adhocracy/templates/user/view.html:44 +msgid "Adhocracies" +msgstr "" + +#: adhocracy/templates/index.html:52 +msgid "Adhocracy helps <strong>groups</strong> make <strong>decisions</strong>." +msgstr "" + +#: adhocracy/templates/index.html:55 +msgid "" +"Adhocracy is a platform for virtual direct democracies where you can " +"cooperate to create andselect solutions to your organization's challenges." +msgstr "" + +#: adhocracy/templates/index.html:59 +msgid "What groups?" +msgstr "" + +#: adhocracy/templates/index.html:62 +msgid "" +"Think NGOs, open projects. Distributed, loosely-knit groups in search of a " +"common strategy or groups with complex internal policies like Wikipedia." +msgstr "" + +#: adhocracy/templates/index.html:64 adhocracy/templates/index.html:72 +msgid "Read more..." +msgstr "" + +#: adhocracy/templates/index.html:67 +msgid "What decisions?" +msgstr "" + +#: adhocracy/templates/index.html:70 +msgid "" +"Anything that discusses a method of solving a problem. This could be laws, " +"strategy decisions or even design patterns." +msgstr "" + +#: adhocracy/templates/index.html:76 +msgid "Activity in my Adhocracies" +msgstr "" + +#: adhocracy/templates/pager.html:6 +msgid "sort by" +msgstr "" + +#: adhocracy/templates/pager.html:26 +msgid "previous" +msgstr "" + +#: adhocracy/templates/pager.html:44 +msgid "next" +msgstr "" + +#: adhocracy/templates/template.html:4 +msgid "No Title" +msgstr "" + +#: adhocracy/templates/template.html:55 +msgid "sign in" +msgstr "" + +#: adhocracy/templates/template.html:59 +msgid "settings" +msgstr "" + +#: adhocracy/templates/template.html:60 +msgid "logout" +msgstr "" + +#: adhocracy/templates/template.html:64 +msgid "search" +msgstr "" + +#: adhocracy/templates/template.html:66 +msgid "Go" +msgstr "" + +#: adhocracy/templates/template.html:70 +#, python-format +msgid "Join %s to contribute" +msgstr "" + +#: adhocracy/templates/template.html:83 adhocracy/templates/instance/view.html:6 +msgid "Home" +msgstr "" + +#: adhocracy/templates/template.html:90 +msgid "More Adhocracies..." +msgstr "" + +#: adhocracy/templates/template.html:94 +msgid "Critical" +msgstr "" + +#: adhocracy/templates/template.html:96 +msgid "Review" +msgstr "" + +#: adhocracy/templates/template.html:98 +#: adhocracy/templates/user/delegations.html:11 +msgid "Delegations" +msgstr "" + +#: adhocracy/templates/template.html:103 +msgid "Administration" +msgstr "" + +#: adhocracy/templates/template.html:105 adhocracy/templates/category/edit.html:4 +#: adhocracy/templates/issue/edit.html:4 adhocracy/templates/motion/edit.html:4 +#, python-format +msgid "Edit %s" +msgstr "" + +#: adhocracy/templates/template.html:106 adhocracy/templates/admin/members.html:6 +msgid "Members" +msgstr "" + +#: adhocracy/templates/template.html:108 +msgid "Global Permissions" +msgstr "" + +#: adhocracy/templates/template.html:116 adhocracy/templates/user/index.html:3 +#: adhocracy/templates/user/index.html:6 adhocracy/templates/user/index.html:9 +msgid "Users" +msgstr "" + +#: adhocracy/templates/template.html:159 +msgid "adhocracy · liquid democracy platform" +msgstr "" + +#: adhocracy/templates/template.html:160 +msgid "about" +msgstr "" + +#: adhocracy/templates/template.html:161 +msgid "faq" +msgstr "" + +#: adhocracy/templates/template.html:162 +msgid "development" +msgstr "" + +#: adhocracy/templates/template.html:163 +msgid "imprint/contact" +msgstr "" + +#: adhocracy/templates/admin/members.html:3 +#: adhocracy/templates/admin/members.html:9 +#, python-format +msgid "Members: %s" +msgstr "" + +#: adhocracy/templates/admin/members.html:23 +#, python-format +msgid "is a %s" +msgstr "" + +#: adhocracy/templates/admin/members.html:24 +msgid "force to leave" +msgstr "" + +#: adhocracy/templates/admin/members.html:30 +msgid "move to Group:" +msgstr "" + +#: adhocracy/templates/admin/members.html:33 +msgid "Observer" +msgstr "" + +#: adhocracy/templates/admin/members.html:38 +msgid "Voter" +msgstr "" + +#: adhocracy/templates/admin/members.html:43 +msgid "Supervisor" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:3 +msgid "Admin: Group Permissions" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:6 +msgid "Admin » Group Permissions" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:11 +msgid "Group Permissions" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:14 +msgid "" +"WARNING: This allows you to shut yourself out of your adhocracy. Handle with " +"care!" +msgstr "" + +#: adhocracy/templates/category/create.html:4 +#: adhocracy/templates/category/create.html:7 +#: adhocracy/templates/category/create.html:13 +msgid "New category" +msgstr "" + +#: adhocracy/templates/category/create.html:19 +msgid "Category description:" +msgstr "" + +#: adhocracy/templates/category/edit.html:7 adhocracy/templates/issue/edit.html:7 +#: adhocracy/templates/motion/edit.html:7 adhocracy/templates/user/edit.html:7 +msgid "Edit" +msgstr "" + +#: adhocracy/templates/category/edit.html:13 +msgid "Category title" +msgstr "" + +#: adhocracy/templates/category/edit.html:22 +msgid "Category description" +msgstr "" + +#: adhocracy/templates/category/tiles.html:7 +#: adhocracy/templates/category/tiles.html:25 +#: adhocracy/templates/instance/tiles.html:6 +#: adhocracy/templates/instance/tiles.html:32 adhocracy/templates/user/view.html:32 +#, python-format +msgid "%s issue" +msgid_plural "%s issues" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/category/tiles.html:23 +#: adhocracy/templates/category/view.html:39 +#: adhocracy/templates/comment/revision_tiles.html:7 +#: adhocracy/templates/comment/tiles.html:20 +#: adhocracy/templates/instance/tiles.html:31 +#: adhocracy/templates/issue/tiles.html:15 adhocracy/templates/issue/view.html:42 +#: adhocracy/templates/motion/tiles.html:16 +#: adhocracy/templates/motion/tiles.html:35 adhocracy/templates/motion/view.html:52 +#, python-format +msgid "created %s" +msgstr "" + +#: adhocracy/templates/category/tiles.html:24 +#, python-format +msgid "%s category" +msgid_plural "%s categories" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/category/tree.html:45 +msgid "Categorization" +msgstr "" + +#: adhocracy/templates/category/view.html:10 +#: adhocracy/templates/category/view.html:12 +#: adhocracy/templates/comment/tiles.html:101 +#: adhocracy/templates/issue/view.html:12 adhocracy/templates/issue/view.html:14 +#: adhocracy/templates/motion/view.html:12 adhocracy/templates/motion/view.html:14 +#: adhocracy/templates/motion/view.html:16 +msgid "delete" +msgstr "" + +#: adhocracy/templates/category/view.html:16 +#: adhocracy/templates/category/view.html:18 +#: adhocracy/templates/comment/revision_tiles.html:19 +#: adhocracy/templates/comment/tiles.html:93 +#: adhocracy/templates/comment/tiles.html:95 +#: adhocracy/templates/comment/tiles.html:97 +#: adhocracy/templates/instance/view.html:11 adhocracy/templates/issue/view.html:18 +#: adhocracy/templates/issue/view.html:20 adhocracy/templates/motion/view.html:20 +#: adhocracy/templates/motion/view.html:22 adhocracy/templates/motion/view.html:24 +#: adhocracy/templates/user/view.html:11 +msgid "edit" +msgstr "" + +#: adhocracy/templates/category/view.html:40 +#: adhocracy/templates/comment/tiles.html:118 +msgid "history" +msgstr "" + +#: adhocracy/templates/category/view.html:51 +msgid "Subcategories" +msgstr "" + +#: adhocracy/templates/category/view.html:54 +#: adhocracy/templates/instance/view.html:58 +msgid "Create new categories to further structure the debate." +msgstr "" + +#: adhocracy/templates/category/view.html:71 +#: adhocracy/templates/instance/view.html:76 +msgid "Issues" +msgstr "" + +#: adhocracy/templates/category/view.html:75 +msgid "Create an issue to discuss a new topic within the current category." +msgstr "" + +#: adhocracy/templates/comment/create.html:3 +#: adhocracy/templates/comment/create.html:6 +msgid "New comment" +msgstr "" + +#: adhocracy/templates/comment/create.html:9 +#: adhocracy/templates/comment/tiles.html:13 +#: adhocracy/templates/comment/view.html:2 adhocracy/templates/comment/view.html:5 +msgid "Comment" +msgstr "" + +#: adhocracy/templates/comment/edit.html:3 adhocracy/templates/comment/edit.html:6 +msgid "Edit comment" +msgstr "" + +#: adhocracy/templates/comment/edit.html:9 +#, python-format +msgid "Comment on: %s" +msgstr "" + +#: adhocracy/templates/comment/history.html:2 +#: adhocracy/templates/comment/history.html:5 +#: adhocracy/templates/comment/history.html:8 +msgid "Comment History" +msgstr "" + +#: adhocracy/templates/comment/revision_tiles.html:13 +msgid "revert here" +msgstr "" + +#: adhocracy/templates/comment/revision_tiles.html:17 +msgid "view comment" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:22 +#: adhocracy/templates/comment/tiles.html:113 +#, python-format +msgid "edited %s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:26 +#: adhocracy/templates/motion/tiles.html:23 +msgid "in" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:57 +#: adhocracy/templates/comment/tiles.html:59 +msgid "Sign in to rate comments" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:61 +#: adhocracy/templates/comment/tiles.html:63 +msgid "This is your own comment" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:78 +msgid "discussion" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:87 +#: adhocracy/templates/comment/tiles.html:89 +msgid "reply" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:105 +#, python-format +msgid "deleted %s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:109 +#, python-format +msgid "%s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:115 +#, python-format +msgid "edited %s by %s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:129 +msgid "This comment has been deleted." +msgstr "" + +#: adhocracy/templates/comment/view.html:8 +#, python-format +msgid "Discussion on %s" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:15 +msgid "for" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:17 +msgid "against" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:19 +msgid "abstained" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:21 +msgid "undecided" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:36 +msgid "" +"The user's delegates have voted, but no consensus wasreached among them. The " +"decision is deferred." +msgstr "" + +#: adhocracy/templates/decision/tiles.html:40 +msgid "The decision was made without delegations." +msgstr "" + +#: adhocracy/templates/decision/tiles.html:42 +msgid "The decision was determined as a result of the following delegations:" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:64 +#, python-format +msgid "in a poll %s" +msgstr "" + +#: adhocracy/templates/delegation/create.html:4 +#: adhocracy/templates/delegation/create.html:11 +#, python-format +msgid "Delegate: %s" +msgstr "" + +#: adhocracy/templates/delegation/create.html:7 +#: adhocracy/templates/delegation/review.html:5 +msgid "Delegation" +msgstr "" + +#: adhocracy/templates/delegation/create.html:18 +msgid "This page is obviously a placeholder. Nice, karma-based recs are coming soon." +msgstr "" + +#: adhocracy/templates/delegation/create.html:28 +#, python-format +msgid "Popular delegates for %s" +msgstr "" + +#: adhocracy/templates/delegation/create.html:42 +msgid "Your favourite delegates" +msgstr "" + +#: adhocracy/templates/delegation/create.html:55 +msgid "Delegate to:" +msgstr "" + +#: adhocracy/templates/delegation/review.html:2 +#: adhocracy/templates/delegation/review.html:11 +#, python-format +msgid "Delegation: %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:9 +#: adhocracy/templates/delegation/tiles.html:13 +msgid "revoke" +msgstr "" + +#: adhocracy/templates/delegation/review.html:17 +#: adhocracy/templates/delegation/tiles.html:4 +#, python-format +msgid "from %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:18 +#: adhocracy/templates/delegation/tiles.html:10 +#, python-format +msgid "to %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:22 +#, python-format +msgid "established %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:24 +#, python-format +msgid "revoked %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:30 +msgid "The delegation can be overridden or revoked at any time." +msgstr "" + +#: adhocracy/templates/delegation/review.html:42 +msgid "" +"No decisions have been based on this delegation yet. As soon as this " +"delegation leads to any decisions, they will be listed here." +msgstr "" + +#: adhocracy/templates/delegation/tiles.html:5 +#: adhocracy/templates/delegation/tiles.html:11 +msgid "track record" +msgstr "" + +#: adhocracy/templates/error/http.html:2 adhocracy/templates/error/http.html:4 +#, python-format +msgid "Error %s" +msgstr "" + +#: adhocracy/templates/error/http.html:8 +msgid "" +"If this error continues to occur, please <a href='/page/imprint.html'>notify " +"us</a> with a description of what you were trying to do." +msgstr "" + +#: adhocracy/templates/event/all.html:4 adhocracy/templates/event/all.html:11 +msgid "Whazza" +msgstr "" + +#: adhocracy/templates/event/all.html:7 +msgid "All current events in Adhocracy." +msgstr "" + +#: adhocracy/templates/instance/create.html:3 +#: adhocracy/templates/instance/create.html:6 +#: adhocracy/templates/instance/create.html:12 +msgid "New Adhocracy" +msgstr "" + +#: adhocracy/templates/instance/create.html:18 +msgid "Adhocracy address:" +msgstr "" + +#: adhocracy/templates/instance/create.html:19 +msgid "" +"The address may only contain alpha-numeric characters. Please note that this " +"key cannot be changed after the Adhocracy has been created." +msgstr "" + +#: adhocracy/templates/instance/create.html:22 +#: adhocracy/templates/instance/edit.html:59 +msgid "Description:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:3 +#, python-format +msgid "Manage: %s" +msgstr "" + +#: adhocracy/templates/instance/edit.html:6 +msgid "Manage" +msgstr "" + +#: adhocracy/templates/instance/edit.html:14 +msgid "Adhocracy Name" +msgstr "" + +#: adhocracy/templates/instance/edit.html:19 +#: adhocracy/templates/instance/view.html:34 +msgid "Voting Rules" +msgstr "" + +#: adhocracy/templates/instance/edit.html:20 +msgid "Majority:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:21 +msgid "" +"In order to become active, a motion must reach the given proportion of " +"approval." +msgstr "" + +#: adhocracy/templates/instance/edit.html:23 +msgid "A simple majority (½ of vote)" +msgstr "" + +#: adhocracy/templates/instance/edit.html:24 +msgid "A two-thirds majority" +msgstr "" + +#: adhocracy/templates/instance/edit.html:25 +msgid "In Soviet Russia, motion votes you." +msgstr "" + +#: adhocracy/templates/instance/edit.html:28 +msgid "Delay:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:29 +msgid "" +"Before activating, the defined majority must be continuously held by the " +"moton for the specified interval." +msgstr "" + +#: adhocracy/templates/instance/edit.html:31 +msgid "No delay" +msgstr "" + +#: adhocracy/templates/instance/edit.html:32 +msgid "1 Day" +msgstr "" + +#: adhocracy/templates/instance/edit.html:33 +msgid "2 Days" +msgstr "" + +#: adhocracy/templates/instance/edit.html:34 +msgid "One Week" +msgstr "" + +#: adhocracy/templates/instance/edit.html:35 +msgid "Two Weeks" +msgstr "" + +#: adhocracy/templates/instance/edit.html:36 +msgid "Four Weeks" +msgstr "" + +#: adhocracy/templates/instance/edit.html:39 +msgid "Membership Options" +msgstr "" + +#: adhocracy/templates/instance/edit.html:40 +msgid "Default group:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:41 +msgid "When a new member joins, he or she will be a member of this user group." +msgstr "" + +#: adhocracy/templates/instance/edit.html:51 +msgid "Logo" +msgstr "" + +#: adhocracy/templates/instance/edit.html:52 +msgid "File upload:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:53 +msgid "Select a logo file to appear in the header area of this Adhocracy." +msgstr "" + +#: adhocracy/templates/instance/index.html:16 +msgid "Adhocracies are little democracies that are ran by their community." +msgstr "" + +#: adhocracy/templates/instance/tiles.html:8 +#: adhocracy/templates/instance/tiles.html:34 +#: adhocracy/templates/instance/view.html:14 +#: adhocracy/templates/instance/view.html:17 +msgid "join" +msgstr "" + +#: adhocracy/templates/instance/tiles.html:11 +#: adhocracy/templates/instance/tiles.html:37 +#: adhocracy/templates/instance/view.html:20 +msgid "leave" +msgstr "" + +#: adhocracy/templates/instance/view.html:10 +msgid "members" +msgstr "" + +#: adhocracy/templates/instance/view.html:35 +msgid "Required Majority:" +msgstr "" + +#: adhocracy/templates/instance/view.html:36 +msgid "To become active, a motion must reach the given proportion of approval." +msgstr "" + +#: adhocracy/templates/instance/view.html:38 +msgid "Activation Delay:" +msgstr "" + +#: adhocracy/templates/instance/view.html:39 +msgid "Before becoming active, the majority must be held for the specified interval." +msgstr "" + +#: adhocracy/templates/instance/view.html:45 +msgid "Subscribe to RSS feed:" +msgstr "" + +#: adhocracy/templates/instance/view.html:55 +msgid "Categories" +msgstr "" + +#: adhocracy/templates/instance/view.html:80 +msgid "Create an issue to start debating in this Adhocracy." +msgstr "" + +#: adhocracy/templates/issue/create.html:4 adhocracy/templates/issue/create.html:7 +#: adhocracy/templates/issue/create.html:18 +msgid "New issue" +msgstr "" + +#: adhocracy/templates/issue/create.html:27 +msgid "Issue description:" +msgstr "" + +#: adhocracy/templates/issue/edit.html:13 +msgid "Issue Title" +msgstr "" + +#: adhocracy/templates/issue/tiles.html:16 adhocracy/templates/motion/tiles.html:20 +#: adhocracy/templates/motion/tiles.html:39 adhocracy/templates/user/view.html:34 +#, python-format +msgid "%s comment" +msgid_plural "%s comments" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/issue/tiles.html:17 adhocracy/templates/user/view.html:33 +#, python-format +msgid "%s motion" +msgid_plural "%s motions" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/issue/view.html:35 +#, python-format +msgid "in %s" +msgstr "" + +#: adhocracy/templates/issue/view.html:57 adhocracy/templates/motion/index.html:6 +msgid "Motions" +msgstr "" + +#: adhocracy/templates/issue/view.html:59 +msgid "" +"Motions are <b>proposals</b> that solve some or all of the problems described" +" by this issue. A motion can be <b>discussed and voted</b> upon." +msgstr "" + +#: adhocracy/templates/issue/view.html:66 adhocracy/templates/motion/view.html:209 +msgid "Discussion" +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:6 +msgid "Call a vote" +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:9 +#, python-format +msgid "Call a vote on: %s" +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:17 +msgid "" +"You are about to release this motion and call for a vote. When you do this, " +"you will lose the ability to <strong>change the motion's wording</strong> " +"unless you re-draft it." +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:21 +#: adhocracy/templates/motion/end_poll.html:21 +msgid "Confirm and Proceed" +msgstr "" + +#: adhocracy/templates/motion/create.html:4 +#: adhocracy/templates/motion/create.html:7 +#: adhocracy/templates/motion/create.html:14 +msgid "New motion" +msgstr "" + +#: adhocracy/templates/motion/create.html:20 +msgid "Informal motion description:" +msgstr "" + +#: adhocracy/templates/motion/edit.html:13 +msgid "New Motion" +msgstr "" + +#: adhocracy/templates/motion/end_poll.html:6 +msgid "End a vote" +msgstr "" + +#: adhocracy/templates/motion/end_poll.html:9 +#, python-format +msgid "Cancel vote on: %s" +msgstr "" + +#: adhocracy/templates/motion/end_poll.html:17 +msgid "" +"You are about to re-draft this motion. This means that the motion will become" +" editable again but that all votes that have been cast at this time " +"<strong>will be invalidated</strong>." +msgstr "" + +#: adhocracy/templates/motion/index.html:3 +#, python-format +msgid "Critical Motions in %s" +msgstr "" + +#: adhocracy/templates/motion/index.html:9 +msgid "Critical Motions" +msgstr "" + +#: adhocracy/templates/motion/index.html:14 +msgid "" +"These motions are currently voting and might be nearing a decision. Vote now " +"to make your voice heard." +msgstr "" + +#: adhocracy/templates/motion/index.html:27 +msgid "" +"There are currently no polls open. Call a motion into voting state to begin a" +" poll." +msgstr "" + +#: adhocracy/templates/motion/tiles.html:18 +#: adhocracy/templates/motion/tiles.html:37 +#, python-format +msgid "%s votes cast" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:46 +msgid "Draft" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:48 +msgid "Voting" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:50 +msgid "Activating" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:52 +msgid "Deactivating" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:54 +msgid "Active" +msgstr "" + +#: adhocracy/templates/motion/view.html:32 adhocracy/templates/motion/view.html:34 +#: adhocracy/templates/motion/view.html:36 +msgid "call a vote" +msgstr "" + +#: adhocracy/templates/motion/view.html:34 +msgid "The motion cannot be voted upon since it does not have any provisions yet." +msgstr "" + +#: adhocracy/templates/motion/view.html:66 +msgid "This vote has recieved the required majority and cannot be cancelled" +msgstr "" + +#: adhocracy/templates/motion/view.html:70 +msgid "Vote" +msgstr "" + +#: adhocracy/templates/motion/view.html:83 +msgid "Option" +msgstr "" + +#: adhocracy/templates/motion/view.html:86 +msgid "Delegate Recommendations" +msgstr "" + +#: adhocracy/templates/motion/view.html:89 adhocracy/templates/motion/votes.html:8 +#: adhocracy/templates/user/votes.html:8 +msgid "Votes" +msgstr "" + +#: adhocracy/templates/motion/view.html:90 +msgid "Percent" +msgstr "" + +#: adhocracy/templates/motion/view.html:102 +msgid "Affirm" +msgstr "" + +#: adhocracy/templates/motion/view.html:123 +msgid "Dissent" +msgstr "" + +#: adhocracy/templates/motion/view.html:144 +msgid "Abstain" +msgstr "" + +#: adhocracy/templates/motion/view.html:158 +msgid "vote" +msgstr "" + +#: adhocracy/templates/motion/view.html:163 +#, python-format +msgid "Of the required %s votes, the motion has:" +msgstr "" + +#: adhocracy/templates/motion/view.html:168 +#, python-format +msgid "%d votes" +msgstr "" + +#: adhocracy/templates/motion/view.html:181 +#, python-format +msgid "poll started %s" +msgstr "" + +#: adhocracy/templates/motion/view.html:182 +msgid "help" +msgstr "" + +#: adhocracy/templates/motion/view.html:196 +msgid "Provisions" +msgstr "" + +#: adhocracy/templates/motion/view.html:198 +msgid "" +"<b>Provisions</b> are the body of a motion: together they form the language " +"that will be voted upon. You will need to have at least one clause in order " +"to call for a vote." +msgstr "" + +#: adhocracy/templates/motion/votes.html:5 adhocracy/templates/user/votes.html:5 +#, python-format +msgid "Votes: %s" +msgstr "" + +#: adhocracy/templates/motion/votes.html:12 adhocracy/templates/user/votes.html:14 +msgid "Votes:" +msgstr "" + +#: adhocracy/templates/search/results.html:2 +#: adhocracy/templates/search/results.html:5 +#: adhocracy/templates/search/results.html:12 +msgid "Search" +msgstr "" + +#: adhocracy/templates/search/results.html:10 +#, python-format +msgid "Search for '%s'" +msgstr "" + +#: adhocracy/templates/search/results.html:28 +msgid "" +"No entries could be found that match your criteria. Try a more general search" +" term." +msgstr "" + +#: adhocracy/templates/user/delegations.html:4 +#: adhocracy/templates/user/delegations.html:17 +msgid "My Delegations" +msgstr "" + +#: adhocracy/templates/user/delegations.html:6 +#: adhocracy/templates/user/delegations.html:19 +#, python-format +msgid "Delegations: %s" +msgstr "" + +#: adhocracy/templates/user/delegations.html:25 +msgid "Topic" +msgstr "" + +#: adhocracy/templates/user/delegations.html:26 +msgid "Given" +msgstr "" + +#: adhocracy/templates/user/delegations.html:27 +msgid "Received" +msgstr "" + +#: adhocracy/templates/user/edit.html:4 +#, python-format +msgid "Settings: %s" +msgstr "" + +#: adhocracy/templates/user/edit.html:20 +msgid "User Details" +msgstr "" + +#: adhocracy/templates/user/edit.html:22 +#: adhocracy/templates/user/register_form.html:9 +#: adhocracy/templates/user/reset_form.html:14 +msgid "E-Mail:" +msgstr "" + +#: adhocracy/templates/user/edit.html:24 +msgid "Language:" +msgstr "" + +#: adhocracy/templates/user/edit.html:36 adhocracy/templates/user/login_form.html:5 +#: adhocracy/templates/user/register_form.html:13 +msgid "Password:" +msgstr "" + +#: adhocracy/templates/user/edit.html:38 +msgid "Select a new password or leave the fields blank to keep your old one." +msgstr "" + +#: adhocracy/templates/user/edit.html:42 +#: adhocracy/templates/user/register_form.html:16 +msgid "Password (confirm):" +msgstr "" + +#: adhocracy/templates/user/edit.html:46 +msgid "Configure your user icon at <a href='http://www.gravatar.com'>Gravatar</a>" +msgstr "" + +#: adhocracy/templates/user/edit.html:51 +msgid "Short biography" +msgstr "" + +#: adhocracy/templates/user/edit.html:54 +msgid "" +"A bio will allow others to learn about you and perhaps even get you a few " +"delegations." +msgstr "" + +#: adhocracy/templates/user/index.html:3 adhocracy/templates/user/index.html:9 +#, python-format +msgid "Users in %s" +msgstr "" + +#: adhocracy/templates/user/login.html:2 +msgid "Who are you, then?" +msgstr "" + +#: adhocracy/templates/user/login.html:5 adhocracy/templates/user/login_form.html:8 +msgid "Login" +msgstr "" + +#: adhocracy/templates/user/login.html:6 +msgid "If you already have an account, sign in here." +msgstr "" + +#: adhocracy/templates/user/login.html:10 +msgid "" +"If you have an account but you've lost your password, <a " +"href='/user/reset'>click here</a>." +msgstr "" + +#: adhocracy/templates/user/login.html:15 +#: adhocracy/templates/user/register_form.html:19 +msgid "Register" +msgstr "" + +#: adhocracy/templates/user/login.html:16 +msgid "" +"Creating an account is easy; all you need is a user name, password and email " +"address." +msgstr "" + +#: adhocracy/templates/user/login_form.html:2 +msgid "Login:" +msgstr "" + +#: adhocracy/templates/user/register_form.html:5 +msgid "User name:" +msgstr "" + +#: adhocracy/templates/user/register_form.html:6 +msgid "Can only contain letters and numbers." +msgstr "" + +#: adhocracy/templates/user/register_form.html:10 +msgid "We don't spam." +msgstr "" + +#: adhocracy/templates/user/reset_form.html:2 +#: adhocracy/templates/user/reset_form.html:4 +#: adhocracy/templates/user/reset_pending.html:2 +#: adhocracy/templates/user/reset_pending.html:4 +msgid "Password reset" +msgstr "" + +#: adhocracy/templates/user/reset_form.html:8 +msgid "" +"You will be sent an activation link that, when opened, will cause Adhocracy " +"to email you a new password." +msgstr "" + +#: adhocracy/templates/user/reset_form.html:12 +msgid "In order to retrieve your login, you will have to enter your email adress." +msgstr "" + +#: adhocracy/templates/user/reset_form.html:18 +msgid "Reset" +msgstr "" + +#: adhocracy/templates/user/reset_pending.html:4 +msgid "Confirmation pending" +msgstr "" + +#: adhocracy/templates/user/reset_pending.html:11 +msgid "" +"You have recieved an email containing a link. Please open that link in order " +"to reset you password" +msgstr "" + +#: adhocracy/templates/user/tiles.html:16 +#, python-format +msgid "%s karma" +msgstr "" + +#: adhocracy/templates/user/tiles.html:18 adhocracy/templates/user/view.html:38 +#, python-format +msgid "signed up %s" +msgstr "" + +#: adhocracy/templates/user/view.html:24 +#, python-format +msgid "%s does not have a bio" +msgstr "" + +#: adhocracy/templates/user/view.html:36 +msgid "delegations" +msgstr "" + +#: adhocracy/templates/user/view.html:46 +msgid "is a member in the following adhocracies:" +msgstr "" + +#: adhocracy/templates/user/view.html:56 +msgid "Activity" +msgstr "" + +#: adhocracy/templates/user/votes.html:12 +msgid "Review your voting track" +msgstr "" + diff --git a/adhocracy/i18n/de/LC_MESSAGES/adhocracy.mo b/adhocracy/i18n/de/LC_MESSAGES/adhocracy.mo new file mode 100644 index 000000000..04989ed46 Binary files /dev/null and b/adhocracy/i18n/de/LC_MESSAGES/adhocracy.mo differ diff --git a/adhocracy/i18n/de/LC_MESSAGES/adhocracy.po b/adhocracy/i18n/de/LC_MESSAGES/adhocracy.po new file mode 100644 index 000000000..d24e30384 --- /dev/null +++ b/adhocracy/i18n/de/LC_MESSAGES/adhocracy.po @@ -0,0 +1,2048 @@ +# German translations for adhocracy. +# Copyright (C) 2009 ORGANIZATION +# This file is distributed under the same license as the adhocracy project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: adhocracy 0.2\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2009-10-29 13:31+0100\n" +"PO-Revision-Date: 2009-10-29 13:32+0100\n" +"Last-Translator: Friedrich Lindenberg <friedrich@pudo.org>\n" +"Language-Team: de <team@adhocracy.de>\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.0dev-r0\n" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:61 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:62 +#: adhocracy/contrib/babel/babel/tests/support.py:62 +#: adhocracy/contrib/babel/babel/tests/support.py:67 +#: adhocracy/contrib/babel/babel/tests/support.py:114 +msgid "foo" +msgstr "foo" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:63 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:64 +msgid "There is" +msgid_plural "There are" +msgstr[0] "Es gibt" +msgstr[1] "Es gibt" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:65 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:66 +msgid "Fizz" +msgstr "Fizz" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:67 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:68 +msgid "Fuzz" +msgstr "Fuzz" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:69 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:70 +msgid "Fuzzes" +msgstr "Fuzzes" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/file1.py:8 +msgid "bar" +msgstr "bar" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "foobar" +msgstr[1] "foobars" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/CVS/this_wont_normally_be_here.py:11 +msgid "FooBar" +msgid_plural "FooBars" +msgstr[0] "FooBar" +msgstr[1] "FooBars" + +#: adhocracy/contrib/babel/babel/tests/support.py:78 +#: adhocracy/contrib/babel/babel/tests/support.py:80 +#: adhocracy/contrib/babel/babel/tests/support.py:90 +#: adhocracy/contrib/babel/babel/tests/support.py:92 +#: adhocracy/contrib/babel/babel/tests/support.py:132 +#: adhocracy/contrib/babel/babel/tests/support.py:134 +msgid "foo1" +msgid_plural "foos1" +msgstr[0] "foo1" +msgstr[1] "foos1" + +#: adhocracy/controllers/admin.py:69 +#, python-format +msgid "%(user)s is not a member of %(instance)s" +msgstr "%(user)s ist kein Mitglied von %(instance)s" + +#: adhocracy/controllers/admin.py:93 +#, python-format +msgid "%(user)s was removed from %(instance)s" +msgstr "%(user)s wurde aus %(instance)s entfernt" + +#: adhocracy/controllers/admin.py:97 +#, python-format +msgid "%(user)s isn't a member of %(instance)s" +msgstr "%(user)s ist kein Mitglied von %(instance)s" + +#: adhocracy/controllers/category.py:42 +#: adhocracy/controllers/category.py:66 +#, python-format +msgid "No category with ID '%s' exists." +msgstr "Keine Kategorie mit der ID '%s' existiert." + +#: adhocracy/controllers/category.py:86 +#: adhocracy/controllers/category.py:90 +#: adhocracy/templates/category/view.html:25 +#, python-format +msgid "Category: %s" +msgstr "Kategorie: %s" + +#: adhocracy/controllers/category.py:94 +#: adhocracy/controllers/category.py:101 +#: adhocracy/controllers/comment.py:143 +#: adhocracy/controllers/delegation.py:77 +#: adhocracy/controllers/instance.py:52 +#: adhocracy/controllers/instance.py:98 +#: adhocracy/controllers/instance.py:105 +#: adhocracy/controllers/issue.py:99 +#: adhocracy/controllers/motion.py:36 +#: adhocracy/controllers/motion.py:190 +#: adhocracy/controllers/user.py:51 +#: adhocracy/controllers/user.py:253 +msgid "oldest" +msgstr "älteste" + +#: adhocracy/controllers/category.py:95 +#: adhocracy/controllers/category.py:102 +#: adhocracy/controllers/comment.py:144 +#: adhocracy/controllers/delegation.py:78 +#: adhocracy/controllers/instance.py:53 +#: adhocracy/controllers/instance.py:99 +#: adhocracy/controllers/instance.py:106 +#: adhocracy/controllers/issue.py:100 +#: adhocracy/controllers/motion.py:37 +#: adhocracy/controllers/motion.py:191 +#: adhocracy/controllers/user.py:52 +#: adhocracy/controllers/user.py:254 +msgid "newest" +msgstr "neueste" + +#: adhocracy/controllers/category.py:96 +#: adhocracy/controllers/category.py:103 +#: adhocracy/controllers/instance.py:54 +#: adhocracy/controllers/instance.py:100 +#: adhocracy/controllers/instance.py:107 +#: adhocracy/controllers/issue.py:101 +#: adhocracy/controllers/motion.py:38 +#: adhocracy/controllers/user.py:54 +msgid "activity" +msgstr "aktivität" + +#: adhocracy/controllers/category.py:97 +#: adhocracy/controllers/category.py:104 +#: adhocracy/controllers/instance.py:55 +#: adhocracy/controllers/instance.py:101 +#: adhocracy/controllers/instance.py:108 +#: adhocracy/controllers/issue.py:102 +#: adhocracy/controllers/motion.py:40 +#: adhocracy/controllers/user.py:55 +msgid "name" +msgstr "name" + +#: adhocracy/controllers/category.py:114 +msgid "Deleting the root category isn't possible." +msgstr "Die Hauptkategorie zu löschen ist nicht möglich" + +#: adhocracy/controllers/category.py:117 +#, python-format +msgid "No category with ID '%(id)s' exists." +msgstr "Keine Kategorie mit der ID '%(id)s' vorhanden." + +#: adhocracy/controllers/category.py:130 +#, python-format +msgid "Category '%(category)s' has been deleted." +msgstr "Kategorie '%(category)s' wurde gelöscht." + +#: adhocracy/controllers/comment.py:43 +msgid "Unsupported topic type." +msgstr "Diese Art von Gegenstand wird nicht unterstützt. " + +#: adhocracy/controllers/comment.py:84 +#: adhocracy/controllers/comment.py:103 +#: adhocracy/controllers/comment.py:112 +#: adhocracy/controllers/comment.py:122 +#: adhocracy/controllers/comment.py:139 +#: adhocracy/controllers/comment.py:155 +#, python-format +msgid "No comment with ID %s exists" +msgstr "Kein Kommentar mit der ID %s vorhanden" + +#: adhocracy/controllers/comment.py:159 +msgid "You're trying to revert to a revision which is not part of this comments history" +msgstr "Du versuchst auf eine Version zurückzufallen, die nicht Teil dieses Kommentars ist." + +#: adhocracy/controllers/delegation.py:21 +#, python-format +msgid "No motion or category with ID '%(id)s' exists" +msgstr "Ein Vorschlag mit der ID %(id)s existiert nicht" + +#: adhocracy/controllers/delegation.py:52 +#: adhocracy/controllers/delegation.py:70 +#, python-format +msgid "Couldn't find delegation %(id)s" +msgstr "Die Delegation mit der ID %(id)s konnte nicht gefunden werden" + +#: adhocracy/controllers/delegation.py:54 +#, python-format +msgid "Cannot access delegation %(id)s" +msgstr "Die Delegation %(id)s ist nicht verfügbar" + +#: adhocracy/controllers/delegation.py:63 +msgid "The delegation is now revoked." +msgstr "Die Delegation ist aufgehoben." + +#: adhocracy/controllers/instance.py:42 +#, python-format +msgid "No such adhocracy exists: %(key)s" +msgstr "Die Adhocracy %(key)s existiert nicht" + +#: adhocracy/controllers/instance.py:47 +msgid "An index of adhocracies run at adhocracy.cc. Select which ones you would like to join and participate in!" +msgstr "Eine Liste aller Adhocracies die unter adhocracy.cc angemeldet sind. Wähle einige aus und nimm teil!" + +#: adhocracy/controllers/instance.py:89 +#, python-format +msgid "%s News" +msgstr "Neuigkeiten von %s" + +#: adhocracy/controllers/instance.py:91 +#, python-format +msgid "News from the %s Adhocracy" +msgstr "Neuigkeiten aus der %s-Adhocracy" + +#: adhocracy/controllers/instance.py:171 +msgid "Deleting an instance is not currently implemented" +msgstr "Eine Instanz zu löschen wird gegenwärtig nicht unterstützt" + +#: adhocracy/controllers/instance.py:178 +#, python-format +msgid "You're already a member in %(instance)s." +msgstr "Du bist bereits ein Mitglied von %(instance)s." + +#: adhocracy/controllers/instance.py:192 +#, python-format +msgid "Welcome to %(instance)s" +msgstr "Willkommen in %(instance)s" + +#: adhocracy/controllers/instance.py:201 +#, python-format +msgid "You're not a member of %(instance)s." +msgstr "Du bist kein Mitlied von %(instance)s." + +#: adhocracy/controllers/instance.py:204 +#, python-format +msgid "You're the founder of %s, cannot leave." +msgstr "Als Gründer von %s kannst du die Adhocracy nicht verlassen" + +#: adhocracy/controllers/issue.py:60 +#: adhocracy/controllers/issue.py:80 +#: adhocracy/controllers/issue.py:115 +#, python-format +msgid "No issue with ID %s exists." +msgstr "Ein Thema mit der ID %s existiert nicht" + +#: adhocracy/controllers/issue.py:90 +#: adhocracy/templates/issue/view.html:27 +#, python-format +msgid "Issue: %s" +msgstr "Thema: %s" + +#: adhocracy/controllers/issue.py:92 +#, python-format +msgid "Activity on the %s issue" +msgstr "Aktivität im Thema %s" + +#: adhocracy/controllers/issue.py:94 +#, python-format +msgid "Issue: %(issue)s" +msgstr "Thema: %(issue)s" + +#: adhocracy/controllers/issue.py:124 +#, python-format +msgid "The issue %(issue)s cannot be deleted, because the contained motion %(motion)s is polling." +msgstr "Das Thema %(issue)s kann nicht gelöscht werden, da der Vorschlag %(motion)s gerade abgestimmt wird" + +#: adhocracy/controllers/issue.py:131 +#, python-format +msgid "Issue '%(issue)s' has been deleted." +msgstr "Das Thema %(issue)s wurde gelöscht." + +#: adhocracy/controllers/karma.py:28 +msgid "Invalid karma value. Karma is either positive or negative!" +msgstr "Ungültiger Karma-Wert. Karma ist entweder negativ oder positiv!" + +#: adhocracy/controllers/motion.py:39 +msgid "urgency" +msgstr "dringlichkeit" + +#: adhocracy/controllers/motion.py:106 +#, python-format +msgid "Motion: %s" +msgstr "Vorschlag: %s" + +#: adhocracy/controllers/motion.py:108 +#, python-format +msgid "Activity on the %s motion" +msgstr "Aktivität beim Vorschlag %s" + +#: adhocracy/controllers/motion.py:110 +#, python-format +msgid "Motion: %(motion)s" +msgstr "Vorschlag: %(motion)s" + +#: adhocracy/controllers/motion.py:146 +msgid "The poll cannot be started either because there are no provisions or a poll has already started." +msgstr "Die Abstimmung kann nicht begonnen werden; entweder weil bereits eine Abstimmung stattfindet oder weil es noch keine Absätze gibt." + +#: adhocracy/controllers/motion.py:170 +msgid "The motion is not undergoing a poll." +msgstr "Ãœber diesen Vorschlag wird noch nicht abgestimmt." + +#: adhocracy/controllers/motion.py:194 +#, python-format +msgid "%s is not currently in a poll, thus no votes have been counted." +msgstr "%s steht aktuell nicht zu Abstimmung, es werden daher keine Stimmen gezählt." + +#: adhocracy/controllers/page.py:19 +#: adhocracy/controllers/page.py:27 +msgid "The requested page was not found" +msgstr "Die angeforderte Seite wurde nicht gefunden" + +#: adhocracy/controllers/root.py:21 +#: adhocracy/lib/base.py:72 +#: adhocracy/templates/index.html:22 +msgid "My Adhocracies" +msgstr "Meine Adhocracies" + +#: adhocracy/controllers/root.py:23 +msgid "Updates from the Adhocracies in which you are a member" +msgstr "Aktuelles aus den Adhocracies in denen Du ein Mitglied bist" + +#: adhocracy/controllers/root.py:36 +#, python-format +msgid "No motion or category with ID %(id)s exists" +msgstr "Ein Vorschlag oder eine Kategorie mit der ID %(id)s existiert nicht" + +#: adhocracy/controllers/search.py:25 +msgid "Received no query for search." +msgstr "Bitte gib einen Suchbegriff ein." + +#: adhocracy/controllers/user.py:53 +msgid "karma" +msgstr "Karma" + +#: adhocracy/controllers/user.py:93 +#: adhocracy/controllers/user.py:141 +#: adhocracy/controllers/user.py:165 +#: adhocracy/controllers/user.py:248 +#: adhocracy/controllers/user.py:263 +#, python-format +msgid "No user named '%s' exists" +msgstr "Ein Nutzer mit dem Namen %s existiert nicht" + +#: adhocracy/controllers/user.py:95 +#, python-format +msgid "You're not authorized to change %s's settings." +msgstr "Du bist nicht befugt %ss Einstellungen zu bearbeiten." + +#: adhocracy/controllers/user.py:124 +msgid "There is no user registered with that email address." +msgstr "Unter dieser Email Adresse ist kein Nutzer angemeldet." + +#: adhocracy/controllers/user.py:131 +msgid "you have requested that your password for Adhocracy be reset. In order to confirm the validity of your claim, please open the link below in your browser:" +msgstr "Deine Bitte das Adhocracy Kennwort zurück zu setzen wird bearbeitet. Um den Prozess zu legitimieren und abzuschließen muss der folgende Link in deinem Browser geöffnet werden." + +#: adhocracy/controllers/user.py:134 +msgid "Reset your password" +msgstr "Kennwort zurücksetzen" + +#: adhocracy/controllers/user.py:152 +msgid "your password has been reset. It is now:" +msgstr "Dein Kennwort wurde zurück gesetzt. Es lautet:" + +#: adhocracy/controllers/user.py:153 +msgid "Please login and change the password in your user settings." +msgstr "Bitte logge dich erneut ein und ändere dein Kennwort in den Einstellungen." + +#: adhocracy/controllers/user.py:154 +msgid "Your new password" +msgstr "Dein neues Kennwort" + +#: adhocracy/controllers/user.py:155 +msgid "Success. You have been sent an email with your new password." +msgstr "Geschafft. Das neue Kennwort wurde an deine Email Adresse versandt." + +#: adhocracy/controllers/user.py:157 +msgid "The reset code is invalid. Please repeat the password recovery procedure." +msgstr "Der Code zum zurücksetzen scheint ungültig. Bitte wiederhole die Prozedur zur Widerherstellung deines Kennworts." + +#: adhocracy/controllers/user.py:169 +#, python-format +msgid "%(user)s is using Adhocracy, a direct democracy decision-making tool." +msgstr "%(user)s verwendet Adhocracy ein direktdemokratisches Entscheidungsfindungstool." + +#: adhocracy/controllers/user.py:178 +#, python-format +msgid "%(user)ss Activity" +msgstr "%(user)ss Tätigkeiten" + +#: adhocracy/controllers/user.py:182 +#, python-format +msgid "%s is not a member of %s" +msgstr "%s ist kein Mitglied von %s" + +#: adhocracy/controllers/user.py:218 +msgid "Invalid user name or password" +msgstr "Ungültiger Benutzername oder falsches Passwort" + +#: adhocracy/controllers/vote.py:16 +#: adhocracy/lib/base.py:43 +#, python-format +msgid "No motion with ID %(id)s exists." +msgstr "Ein Vorschlag mit der ID %(id)s existiert nicht." + +#: adhocracy/controllers/vote.py:18 +msgid "You have no voting rights." +msgstr "Du hast kein Wahlrecht." + +#: adhocracy/controllers/vote.py:27 +msgid "This motion is not currently being voted on." +msgstr "Ãœber diesen Vorschlag wird zur Zeit nicht abgestimmt." + +#: adhocracy/controllers/vote.py:31 +msgid "Illegal input for vote cast." +msgstr "Ungültige Eingabe für die Stimmabgabe." + +#: adhocracy/lib/base.py:77 +msgid "A liquid democracy platform for making decisions in distributed, open groups by cooperatively creating proposals and voting on them to establish their support." +msgstr "Eine Liquid Democracy-Plattform auf der verteilte, offene Gruppen kooperativ Vorschläge erarbeiten und abstimmen können." + +#: adhocracy/lib/base.py:80 +msgid "adhocracy, direct democracy, liquid democracy, liqd, democracy, wiki, voting,participation, group decisions, decisions, decision-making" +msgstr "adhocracy, direct democracy, liquid democracy, liqd, democracy, wiki, voting,participation, group decisions, decisions, decision-making" + +#: adhocracy/lib/helpers.py:33 +#: adhocracy/templates/template.html:36 +#: adhocracy/templates/template.html:77 +#: adhocracy/templates/user/parts.html:5 +msgid "Adhocracy" +msgstr "Adhocracy" + +#: adhocracy/lib/helpers.py:51 +msgid "This motion is currently being voted on and cannot be modified." +msgstr "Ãœber diesen Vorschlag wird gerade abgestimmt, er kann daher nicht verändert werden." + +#: adhocracy/lib/helpers.py:103 +msgid "You" +msgstr "Du" + +#: adhocracy/lib/mail.py:17 +#, python-format +msgid "Hi %s," +msgstr "Hallo %s" + +#: adhocracy/lib/mail.py:19 +msgid "" +"Cheers,\r\n" +"\r\n" +" the Adhocracy Team\r\n" +msgstr "Grüße,dein Adhocracy Team" + +#: adhocracy/lib/xsrf.py:49 +msgid "Action failed. You were probably trying to re-perform an action after using your browser's 'Back' button. This is prohibited for security reasons." +msgstr "Aktion gescheitert. Du hast vermutlich versucht eine Handlung nach der Betätigung des \"Zurück\"-Knopfes erneut auszuführen. Dies ist aus Sicherheitsgründen nicht gestattet." + +#: adhocracy/lib/event/event.py:64 +msgid "(Undefined)" +msgstr "(nicht definiert)" + +#: adhocracy/lib/event/formatting.py:79 +msgid "voted for" +msgstr "stimmte für" + +#: adhocracy/lib/event/formatting.py:80 +msgid "abstained on" +msgstr "enthielt sich bei " + +#: adhocracy/lib/event/formatting.py:81 +msgid "voted against" +msgstr "stimmte gegen" + +#: adhocracy/lib/event/formatting.py:89 +msgid "comment" +msgstr "Kommentar" + +#: adhocracy/lib/event/types.py:44 +msgid "signed up" +msgstr "meldete sich an" + +#: adhocracy/lib/event/types.py:45 +msgid "edited their profile" +msgstr "bearbeitete sein Profil" + +#: adhocracy/lib/event/types.py:46 +#, python-format +msgid "edited %(user)ss profile" +msgstr "bearbeitete %(user)ss Profil" + +#: adhocracy/lib/event/types.py:47 +#, python-format +msgid "founded the %(instance)s Adhocracy" +msgstr "gründete die Adhocracy %(instance)s" + +#: adhocracy/lib/event/types.py:48 +#, python-format +msgid "updated the %(instance)s Adhocracy" +msgstr "änderte die Adhocracy %(instance)s" + +#: adhocracy/lib/event/types.py:49 +#, python-format +msgid "deleted the %(instance)s Adhocracy" +msgstr "löschte die Adhocracy %(instance)s" + +#: adhocracy/lib/event/types.py:50 +#, python-format +msgid "joined %(instance)s" +msgstr "trat %(instance)s bei" + +#: adhocracy/lib/event/types.py:51 +#, python-format +msgid "left %(instance)s" +msgstr "verließ %(instance)s" + +#: adhocracy/lib/event/types.py:52 +#, python-format +msgid "was forced to leave %(instance)s by %(user)s" +msgstr "wurde von %(user)s gezwungen, die Adhocracy %(instance)s zu verlassen" + +#: adhocracy/lib/event/types.py:53 +#, python-format +msgid "now is a %(group)s within %(instance)s" +msgstr "ist jetzt ein %(group)s der Adhocracy %(instance)s" + +#: adhocracy/lib/event/types.py:54 +#, python-format +msgid "created %(issue)s" +msgstr "legte %(issue)s an" + +#: adhocracy/lib/event/types.py:55 +#, python-format +msgid "edited %(issue)s" +msgstr "bearbeitete %(issue)s" + +#: adhocracy/lib/event/types.py:56 +#, python-format +msgid "deleted %(issue)s" +msgstr "löschte %(issue)s" + +#: adhocracy/lib/event/types.py:57 +#, python-format +msgid "created %(motion)s" +msgstr "legte %(motion)s an" + +#: adhocracy/lib/event/types.py:58 +#, python-format +msgid "edited %(motion)s" +msgstr "bearbeitete %(motion)s" + +#: adhocracy/lib/event/types.py:59 +#, python-format +msgid "re-drafted %(motion)s" +msgstr "brach die Abstimmung zu %(motion)s ab" + +#: adhocracy/lib/event/types.py:60 +#, python-format +msgid "called a vote on %(motion)s" +msgstr "rief zu einer Abstimmung zu %(motion)s auf" + +#: adhocracy/lib/event/types.py:61 +#, python-format +msgid "deleted %(motion)s" +msgstr "löschte %(motion)s" + +#: adhocracy/lib/event/types.py:62 +#, python-format +msgid "named %(user)s as an editor for %(motion)s" +msgstr "ernannte %(user)s zum Redakteur für %(motion)s" + +#: adhocracy/lib/event/types.py:63 +#, python-format +msgid "removed %(user)s from the editors of %(motion)s" +msgstr "entfernte %(user)s aus der Liste der Redakteure von %(motion)s" + +#: adhocracy/lib/event/types.py:64 +#, python-format +msgid "created the category %(category)s in %(parent)s" +msgstr "legte unter %(parent)s die Kategorie %(category)s an." + +#: adhocracy/lib/event/types.py:65 +#, python-format +msgid "updated the category %(category)s" +msgstr "änderte die Kategorie %(category)s" + +#: adhocracy/lib/event/types.py:66 +#, python-format +msgid "deleted the category %(category)s" +msgstr "löschte die Kategorie %(category)s" + +#: adhocracy/lib/event/types.py:67 +#, python-format +msgid "created a %(comment)s on %(delegateable)s" +msgstr "legte in %(delegateable)s einen %(comment)s an" + +#: adhocracy/lib/event/types.py:68 +#, python-format +msgid "edited a %(comment)s on %(delegateable)s" +msgstr "bearbeitete in %(delegateable)s einen %(comment)s" + +#: adhocracy/lib/event/types.py:69 +#, python-format +msgid "deleted a %(comment)s from %(delegateable)s" +msgstr "löschte aus %(delegateable)s einen %(comment)s" + +#: adhocracy/lib/event/types.py:70 +#, python-format +msgid "delegated voting on %(scope)s to %(agent)s" +msgstr "delegierte sein Wahlrecht in Bezug auf %(scope)s an %(agent)s" + +#: adhocracy/lib/event/types.py:71 +#, python-format +msgid "revoked their delegation on %(scope)s to %(agent)s" +msgstr "wiederrief die Delegation auf %(scope)s an %(agent)s" + +#: adhocracy/lib/event/types.py:72 +#, python-format +msgid "%(vote)s %(motion)s" +msgstr "%(vote)s %(motion)s" + +#: adhocracy/lib/event/types.py:73 +#, python-format +msgid "test %(test)s" +msgstr "test %(test)s" + +#: adhocracy/lib/instance/__init__.py:13 +msgid "This action is only available in an instance context." +msgstr "Die Handlung ist nur in einer Instanz zugänglich." + +#: adhocracy/lib/karma/threshold.py:18 +msgid "create a category" +msgstr "eine Kategorie anzulegen" + +#: adhocracy/lib/karma/threshold.py:19 +msgid "edit this category" +msgstr "diese Kategorie zu bearbeiten" + +#: adhocracy/lib/karma/threshold.py:20 +msgid "delete this category" +msgstr "diese Kategorie zu löschen" + +#: adhocracy/lib/karma/threshold.py:21 +msgid "reply in a comment" +msgstr "in einem Kommtar antworten" + +#: adhocracy/lib/karma/threshold.py:22 +msgid "edit this comment" +msgstr "diesen Kommentar zu bearbeiten" + +#: adhocracy/lib/karma/threshold.py:23 +msgid "delete this comment" +msgstr "diesen Kommentar zu löschen" + +#: adhocracy/lib/karma/threshold.py:24 +msgid "rate this comment" +msgstr "einen Kommentar zu bewerten" + +#: adhocracy/lib/karma/threshold.py:25 +msgid "create a motion" +msgstr "einen Vorschlag anzulegen" + +#: adhocracy/lib/karma/threshold.py:26 +msgid "edit this motion" +msgstr "diesen Vorschlag zu bearbeiten" + +#: adhocracy/lib/karma/threshold.py:27 +msgid "call for a vote" +msgstr "zu einer Abstimmung aufzurufen" + +#: adhocracy/lib/karma/threshold.py:28 +msgid "cancel a vote" +msgstr "eine Abstimmung abzubrechen" + +#: adhocracy/lib/karma/threshold.py:29 +msgid "delete a motion" +msgstr "einen Vorschlag zu löschen" + +#: adhocracy/lib/karma/threshold.py:30 +msgid "create an issue" +msgstr "ein Thema anzulegen" + +#: adhocracy/lib/karma/threshold.py:31 +msgid "edit this issue" +msgstr "dieses Thema zu bearbeiten" + +#: adhocracy/lib/karma/threshold.py:32 +msgid "delete this issue" +msgstr "dieses Thema löschen" + +#: adhocracy/lib/karma/threshold.py:33 +#, python-format +msgid "You need %s karma to %s" +msgstr "Du brauchst %s Karma, um %s" + +#: adhocracy/lib/karma/threshold.py:34 +msgid "do this" +msgstr "dies zu tun" + +#: adhocracy/lib/text/i18n.py:45 +msgid "Today" +msgstr "Heute" + +#: adhocracy/lib/text/i18n.py:47 +msgid "Yesterday" +msgstr "Gestern" + +#: adhocracy/lib/text/i18n.py:61 +#, python-format +msgid "%(ts)s ago" +msgstr "vor %(ts)s" + +#: adhocracy/model/forms.py:27 +msgid "No username is given" +msgstr "Benutzername fehlt" + +#: adhocracy/model/forms.py:31 +msgid "The username is invalid" +msgstr "Benutzername fehlerhaft" + +#: adhocracy/model/forms.py:35 +msgid "That username already exists" +msgstr "Ein Nutzer mit dem Namen %s existiert bereits" + +#: adhocracy/model/forms.py:44 +msgid "That email is already registered" +msgstr "Diese EMail Adresse ist schon registriert worden" + +#: adhocracy/model/forms.py:52 +msgid "No instance key is given" +msgstr "Keine Adhocracy-Kennung angegeben" + +#: adhocracy/model/forms.py:56 +msgid "The instance key is invalid" +msgstr "Dieser Adhocracy-Schlüssel ist ungültig." + +#: adhocracy/model/forms.py:60 +msgid "An instance with that key already exists" +msgstr "Eine Adhocracy mit diesem Schlüssel existiert bereits." + +#: adhocracy/model/forms.py:69 +#, python-format +msgid "No entity with ID '%s' exists" +msgstr "Keine Entität mit der ID %s vorhanden" + +#: adhocracy/model/forms.py:105 +#, python-format +msgid "No group with ID '%s' exists" +msgstr "Keine Gruppe mit der ID '%s' existiert." + +#: adhocracy/model/forms.py:114 +#, python-format +msgid "No revision with ID '%s' exists" +msgstr "Keine Version mit der ID '%s' existiert." + +#: adhocracy/model/forms.py:123 +#, python-format +msgid "No comment with ID '%s' exists" +msgstr "Kein Kommentar mit der ID %s vorhanden" + +#: adhocracy/model/forms.py:131 +#, python-format +msgid "'%s' is not a valid motion state." +msgstr "'%s' ist kein gültiger Zustand für einen Vorschlag." + +#: adhocracy/model/forms.py:140 +#, python-format +msgid "No user with the user name '%s' exists" +msgstr "Es existiert kein Nutzer mit dem Namen %s" + +#: adhocracy/model/motion.py:38 +msgid "Motion doesn't have a distinct parent issue." +msgstr "Der Vorschlag ist keinem bestimmten Thema zugeordnet." + +#: adhocracy/templates/components.html:4 +msgid "formatting hints" +msgstr "Formatierungshilfen" + +#: adhocracy/templates/components.html:11 +msgid "Save" +msgstr "Speichern" + +#: adhocracy/templates/components.html:13 +msgid "or" +msgstr "oder" + +#: adhocracy/templates/components.html:13 +#: adhocracy/templates/motion/view.html:64 +#: adhocracy/templates/motion/view.html:66 +#: adhocracy/templates/motion/view.html:68 +msgid "cancel" +msgstr "Abbrechen" + +#: adhocracy/templates/components.html:21 +#: adhocracy/templates/index.html:10 +msgid "Say hi" +msgstr "Sag hallo" + +#: adhocracy/templates/components.html:24 +msgid "If you're not registered yet, <a href='/register'>sign up here</a>." +msgstr "Wenn Du noch nicht registriert bist, melde dich <a href='/register'>hier an</a>." + +#: adhocracy/templates/components.html:33 +msgid "Delegate voting" +msgstr "Abstimmung delegieren" + +#: adhocracy/templates/components.html:38 +msgid "You have voted yourself and not delegated voting." +msgstr "Du hast selbst abgestimmt und keine Delegation erteilt." + +#: adhocracy/templates/components.html:39 +msgid "Info..." +msgstr "Infos..." + +#: adhocracy/templates/components.html:41 +msgid "You have not delegated voting." +msgstr "Du hast keine Delegation erteilt." + +#: adhocracy/templates/components.html:42 +#: adhocracy/templates/category/view.html:22 +#: adhocracy/templates/instance/view.html:23 +#: adhocracy/templates/issue/view.html:24 +#: adhocracy/templates/motion/view.html:28 +msgid "delegate" +msgstr "delegieren" + +#: adhocracy/templates/components.html:43 +#| msgid "Info..." +msgid "info..." +msgstr "info..." + +#: adhocracy/templates/components.html:47 +msgid "By voting yourself, you have overridden:" +msgstr "Durch Deine Stimme hast Du überstimmt:" + +#: adhocracy/templates/components.html:49 +msgid "You have delegated voting to:" +msgstr "Du hast das Wahlrecht delegiert an:" + +#: adhocracy/templates/components.html:56 +#: adhocracy/templates/decision/tiles.html:50 +msgid "on" +msgstr "auf" + +#: adhocracy/templates/components.html:57 +#: adhocracy/templates/decision/tiles.html:51 +msgid "review" +msgstr "prüfen" + +#: adhocracy/templates/components.html:66 +msgid "You hold an additional vote." +msgstr "Du verfügst über eine zusätzliche Stimme." + +#: adhocracy/templates/components.html:68 +#, python-format +msgid "You hold %s additional votes." +msgstr "Du verfügst über %s zusätzliche Stimmen." + +#: adhocracy/templates/components.html:81 +msgid "What now?" +msgstr "Was nun?" + +#: adhocracy/templates/components.html:81 +msgid "— using Adhocracy in 3<sup>½</sup> steps:" +msgstr "— Adhocracy in 3<sup>½</sup> Schritten:" + +#: adhocracy/templates/components.html:87 +msgid "Create and discuss issues that need solutions." +msgstr "Erstelle und diskutiere Themen die einer Lösung bedürfen." + +#: adhocracy/templates/components.html:92 +msgid "Cooperate to develop proposals affecting the issues." +msgstr "Kooperiere um Lösungsvorschläge zu den Themen zu entwickeln." + +#: adhocracy/templates/components.html:96 +msgid "Vote on proposals to collectively make decisions.*" +msgstr "Stimme über Vorschläge ab um kollektiv Entscheidungen zu fällen.*" + +#: adhocracy/templates/components.html:101 +msgid "*Or — if you like — delegate voting in some fields to a peer." +msgstr "*Wenn Du magst kannst Du das Abstimmen in bestimmen Bereichen auch an andere delegieren." + +#: adhocracy/templates/index.html:3 +msgid "Welcome" +msgstr "Willkommen" + +#: adhocracy/templates/index.html:5 +msgid "Welcome to Adhocracy" +msgstr "Willkommen bei Adhocracy" + +#: adhocracy/templates/index.html:9 +msgid "sign up" +msgstr "registrieren" + +#: adhocracy/templates/index.html:15 +msgid "If you're not registered, <a href='/register'>sign up.</a>" +msgstr "Wenn Du nicht angemeldet bist, <a href='/register'>registriere Dich</a>." + +#: adhocracy/templates/index.html:20 +#: adhocracy/templates/index.html:36 +#: adhocracy/templates/user/view.html:43 +msgid "more" +msgstr "Mehr..." + +#: adhocracy/templates/index.html:21 +#: adhocracy/templates/index.html:37 +#: adhocracy/templates/category/view.html:47 +#: adhocracy/templates/category/view.html:49 +#: adhocracy/templates/category/view.html:67 +#: adhocracy/templates/category/view.html:69 +#: adhocracy/templates/instance/index.html:10 +#: adhocracy/templates/instance/view.html:51 +#: adhocracy/templates/instance/view.html:53 +#: adhocracy/templates/instance/view.html:72 +#: adhocracy/templates/instance/view.html:74 +#: adhocracy/templates/issue/view.html:53 +#: adhocracy/templates/issue/view.html:55 +#: adhocracy/templates/motion/view.html:189 +#: adhocracy/templates/motion/view.html:191 +#: adhocracy/templates/motion/view.html:193 +msgid "new" +msgstr "neu" + +#: adhocracy/templates/index.html:31 +msgid "Join a few Adhocracies to contribute to their policies." +msgstr "Tritt ein paar Adhocracies bei um zu ihren Themen beizutragen." + +#: adhocracy/templates/index.html:38 +#: adhocracy/templates/template.html:114 +#: adhocracy/templates/instance/index.html:3 +#: adhocracy/templates/instance/index.html:12 +#: adhocracy/templates/user/view.html:44 +msgid "Adhocracies" +msgstr "Adhocracies" + +#: adhocracy/templates/index.html:52 +msgid "Adhocracy helps <strong>groups</strong> make <strong>decisions</strong>." +msgstr "Adhocracy hilft <strong>Gruppen</strong>, gute <strong>Entscheidungen</strong> zu treffen." + +#: adhocracy/templates/index.html:55 +msgid "Adhocracy is a platform for virtual direct democracies where you can cooperate to create andselect solutions to your organization's challenges." +msgstr "Adhocracy ist eine Plattform für direkte Demokratie, auf der man gemeinsam Lösungen für die Probleme der eigenen Organisation entwickeln kann. " + +#: adhocracy/templates/index.html:59 +msgid "What groups?" +msgstr "Was für Gruppen?" + +#: adhocracy/templates/index.html:62 +msgid "Think NGOs, open projects. Distributed, loosely-knit groups in search of a common strategy or groups with complex internal policies like Wikipedia." +msgstr "Zum Beispiel NGOs und offene Projekte. Alle verteilten, loose verknüpften Gruppen, die nach gemeinsamen Strategien suchen oder die komplexe interne Regelungen verwalten." + +#: adhocracy/templates/index.html:64 +#: adhocracy/templates/index.html:72 +msgid "Read more..." +msgstr "Mehr lesen..." + +#: adhocracy/templates/index.html:67 +msgid "What decisions?" +msgstr "Was für Entscheidungen?" + +#: adhocracy/templates/index.html:70 +msgid "Anything that discusses a method of solving a problem. This could be laws, strategy decisions or even design patterns." +msgstr "Alles, was Methoden zur Lösung von Problemen beschreibt. Das könnten Richtlinen, Strategien oder sogar Entwurfsmuster sein." + +#: adhocracy/templates/index.html:76 +msgid "Activity in my Adhocracies" +msgstr "Ereignisse in meinen Adhocracies" + +#: adhocracy/templates/pager.html:6 +msgid "sort by" +msgstr "sortierten" + +#: adhocracy/templates/pager.html:26 +msgid "previous" +msgstr "vorherige" + +#: adhocracy/templates/pager.html:44 +msgid "next" +msgstr "nächste" + +#: adhocracy/templates/template.html:4 +msgid "No Title" +msgstr "Keine Bezeichnung" + +#: adhocracy/templates/template.html:55 +msgid "sign in" +msgstr "anmelden" + +#: adhocracy/templates/template.html:59 +msgid "settings" +msgstr "Einstellungen" + +#: adhocracy/templates/template.html:60 +msgid "logout" +msgstr "Abmelden" + +#: adhocracy/templates/template.html:64 +msgid "search" +msgstr "Suchen" + +#: adhocracy/templates/template.html:66 +msgid "Go" +msgstr "Los" + +#: adhocracy/templates/template.html:70 +#, python-format +msgid "Join %s to contribute" +msgstr "Tritt %s bei um beizutragen" + +#: adhocracy/templates/template.html:83 +#: adhocracy/templates/instance/view.html:6 +msgid "Home" +msgstr "Hauptseite" + +#: adhocracy/templates/template.html:90 +msgid "More Adhocracies..." +msgstr "Mehr Adhocracies..." + +#: adhocracy/templates/template.html:94 +msgid "Critical" +msgstr "Dringend" + +#: adhocracy/templates/template.html:96 +msgid "Review" +msgstr "Rückblick" + +#: adhocracy/templates/template.html:98 +#: adhocracy/templates/user/delegations.html:11 +msgid "Delegations" +msgstr "Delegationen" + +#: adhocracy/templates/template.html:103 +msgid "Administration" +msgstr "Verwaltung" + +#: adhocracy/templates/template.html:105 +#: adhocracy/templates/category/edit.html:4 +#: adhocracy/templates/issue/edit.html:4 +#: adhocracy/templates/motion/edit.html:4 +#, python-format +msgid "Edit %s" +msgstr "%s bearbeiten" + +#: adhocracy/templates/template.html:106 +#: adhocracy/templates/admin/members.html:6 +msgid "Members" +msgstr "Mitglieder" + +#: adhocracy/templates/template.html:108 +msgid "Global Permissions" +msgstr "Globale Rechte" + +#: adhocracy/templates/template.html:116 +#: adhocracy/templates/user/index.html:3 +#: adhocracy/templates/user/index.html:6 +#: adhocracy/templates/user/index.html:9 +msgid "Users" +msgstr "Benutzer" + +#: adhocracy/templates/template.html:159 +msgid "adhocracy · liquid democracy platform" +msgstr "adhocracy · plattform für liquid democracy" + +#: adhocracy/templates/template.html:160 +msgid "about" +msgstr "über" + +#: adhocracy/templates/template.html:161 +msgid "faq" +msgstr "faq" + +#: adhocracy/templates/template.html:162 +msgid "development" +msgstr "entwicklung" + +#: adhocracy/templates/template.html:163 +msgid "imprint/contact" +msgstr "impressum/kontakt" + +#: adhocracy/templates/admin/members.html:3 +#: adhocracy/templates/admin/members.html:9 +#, python-format +msgid "Members: %s" +msgstr "Mitglieder: %s" + +#: adhocracy/templates/admin/members.html:23 +#, python-format +msgid "is a %s" +msgstr "ist ein %s" + +#: adhocracy/templates/admin/members.html:24 +msgid "force to leave" +msgstr "zum Verlassen zwingen" + +#: adhocracy/templates/admin/members.html:30 +msgid "move to Group:" +msgstr "in Gruppe verschieben:" + +#: adhocracy/templates/admin/members.html:33 +msgid "Observer" +msgstr "Beobachter" + +#: adhocracy/templates/admin/members.html:38 +msgid "Voter" +msgstr "Wähler" + +#: adhocracy/templates/admin/members.html:43 +msgid "Supervisor" +msgstr "Verwalter" + +#: adhocracy/templates/admin/permissions.html:3 +msgid "Admin: Group Permissions" +msgstr "Verwaltung: Gruppenrechte" + +#: adhocracy/templates/admin/permissions.html:6 +msgid "Admin » Group Permissions" +msgstr "Verwaltung » Gruppenrechte" + +#: adhocracy/templates/admin/permissions.html:11 +msgid "Group Permissions" +msgstr "Gruppenrechte" + +#: adhocracy/templates/admin/permissions.html:14 +msgid "WARNING: This allows you to shut yourself out of your adhocracy. Handle with care!" +msgstr "WARNUNG: Diese Funktion erlaubt es Dir, dich aus dem Adhocracy-System auszuschließen. Sie ist daher nur mit Vorsicht einzusetzen." + +#: adhocracy/templates/category/create.html:4 +#: adhocracy/templates/category/create.html:7 +#: adhocracy/templates/category/create.html:13 +msgid "New category" +msgstr "Neue Kategorie" + +#: adhocracy/templates/category/create.html:19 +msgid "Category description:" +msgstr "Kategorienbeschreibung:" + +#: adhocracy/templates/category/edit.html:7 +#: adhocracy/templates/issue/edit.html:7 +#: adhocracy/templates/motion/edit.html:7 +#: adhocracy/templates/user/edit.html:7 +msgid "Edit" +msgstr "Bearbeiten" + +#: adhocracy/templates/category/edit.html:13 +msgid "Category title" +msgstr "Kategorientitel" + +#: adhocracy/templates/category/edit.html:22 +msgid "Category description" +msgstr "Kategorienbeschreibung" + +#: adhocracy/templates/category/tiles.html:7 +#: adhocracy/templates/category/tiles.html:25 +#: adhocracy/templates/instance/tiles.html:6 +#: adhocracy/templates/instance/tiles.html:32 +#: adhocracy/templates/user/view.html:32 +#, python-format +msgid "%s issue" +msgid_plural "%s issues" +msgstr[0] "%s Thema" +msgstr[1] "%s Themen" + +#: adhocracy/templates/category/tiles.html:23 +#: adhocracy/templates/category/view.html:39 +#: adhocracy/templates/comment/revision_tiles.html:7 +#: adhocracy/templates/comment/tiles.html:20 +#: adhocracy/templates/instance/tiles.html:31 +#: adhocracy/templates/issue/tiles.html:15 +#: adhocracy/templates/issue/view.html:42 +#: adhocracy/templates/motion/tiles.html:16 +#: adhocracy/templates/motion/tiles.html:35 +#: adhocracy/templates/motion/view.html:52 +#, python-format +msgid "created %s" +msgstr "%s angelegt" + +#: adhocracy/templates/category/tiles.html:24 +#, python-format +msgid "%s category" +msgid_plural "%s categories" +msgstr[0] "%s Kategorie" +msgstr[1] "%s Kategorien" + +#: adhocracy/templates/category/tree.html:45 +msgid "Categorization" +msgstr "Kategorisierung" + +#: adhocracy/templates/category/view.html:10 +#: adhocracy/templates/category/view.html:12 +#: adhocracy/templates/comment/tiles.html:101 +#: adhocracy/templates/issue/view.html:12 +#: adhocracy/templates/issue/view.html:14 +#: adhocracy/templates/motion/view.html:12 +#: adhocracy/templates/motion/view.html:14 +#: adhocracy/templates/motion/view.html:16 +msgid "delete" +msgstr "löschen" + +#: adhocracy/templates/category/view.html:16 +#: adhocracy/templates/category/view.html:18 +#: adhocracy/templates/comment/revision_tiles.html:19 +#: adhocracy/templates/comment/tiles.html:93 +#: adhocracy/templates/comment/tiles.html:95 +#: adhocracy/templates/comment/tiles.html:97 +#: adhocracy/templates/instance/view.html:11 +#: adhocracy/templates/issue/view.html:18 +#: adhocracy/templates/issue/view.html:20 +#: adhocracy/templates/motion/view.html:20 +#: adhocracy/templates/motion/view.html:22 +#: adhocracy/templates/motion/view.html:24 +#: adhocracy/templates/user/view.html:11 +msgid "edit" +msgstr "bearbeiten" + +#: adhocracy/templates/category/view.html:40 +#: adhocracy/templates/comment/tiles.html:118 +msgid "history" +msgstr "geschichte" + +#: adhocracy/templates/category/view.html:51 +msgid "Subcategories" +msgstr "Unterkategorien" + +#: adhocracy/templates/category/view.html:54 +#: adhocracy/templates/instance/view.html:58 +msgid "Create new categories to further structure the debate." +msgstr "Lege neue Kategorien an, um die Debatte genauer zu strukturieren. " + +#: adhocracy/templates/category/view.html:71 +#: adhocracy/templates/instance/view.html:76 +msgid "Issues" +msgstr "Themen" + +#: adhocracy/templates/category/view.html:75 +msgid "Create an issue to discuss a new topic within the current category." +msgstr "Lege ein Thema an, um eine neue Debatte in der aktuellen Kategorie zu eröffnen." + +#: adhocracy/templates/comment/create.html:3 +#: adhocracy/templates/comment/create.html:6 +msgid "New comment" +msgstr "Neuer Kommentar" + +#: adhocracy/templates/comment/create.html:9 +#: adhocracy/templates/comment/tiles.html:13 +#: adhocracy/templates/comment/view.html:2 +#: adhocracy/templates/comment/view.html:5 +msgid "Comment" +msgstr "Kommentar" + +#: adhocracy/templates/comment/edit.html:3 +#: adhocracy/templates/comment/edit.html:6 +msgid "Edit comment" +msgstr "Kommentar bearbeiten" + +#: adhocracy/templates/comment/edit.html:9 +#, python-format +msgid "Comment on: %s" +msgstr "Kommentar zu: %s" + +#: adhocracy/templates/comment/history.html:2 +#: adhocracy/templates/comment/history.html:5 +#: adhocracy/templates/comment/history.html:8 +msgid "Comment History" +msgstr "Versionen des Kommentars" + +#: adhocracy/templates/comment/revision_tiles.html:13 +msgid "revert here" +msgstr "hierher zurückfallen" + +#: adhocracy/templates/comment/revision_tiles.html:17 +msgid "view comment" +msgstr "zum Kommentar" + +#: adhocracy/templates/comment/tiles.html:22 +#: adhocracy/templates/comment/tiles.html:113 +#, python-format +msgid "edited %s" +msgstr "%s bearbeitet" + +#: adhocracy/templates/comment/tiles.html:26 +#: adhocracy/templates/motion/tiles.html:23 +msgid "in" +msgstr "in" + +#: adhocracy/templates/comment/tiles.html:57 +#: adhocracy/templates/comment/tiles.html:59 +msgid "Sign in to rate comments" +msgstr "Melde Dich an, um Kommentare zu bewerten" + +#: adhocracy/templates/comment/tiles.html:61 +#: adhocracy/templates/comment/tiles.html:63 +msgid "This is your own comment" +msgstr "Dies ist Dein Kommentar" + +#: adhocracy/templates/comment/tiles.html:78 +msgid "discussion" +msgstr "Diskussion" + +#: adhocracy/templates/comment/tiles.html:87 +#: adhocracy/templates/comment/tiles.html:89 +msgid "reply" +msgstr "antworten" + +#: adhocracy/templates/comment/tiles.html:105 +#, python-format +msgid "deleted %s" +msgstr "%s gelöscht" + +#: adhocracy/templates/comment/tiles.html:109 +#, python-format +msgid "%s" +msgstr "%s" + +#: adhocracy/templates/comment/tiles.html:115 +#, python-format +msgid "edited %s by %s" +msgstr "%s bearbeitet von %s" + +#: adhocracy/templates/comment/tiles.html:129 +msgid "This comment has been deleted." +msgstr "Dieser Kommentar wurde gelöscht." + +#: adhocracy/templates/comment/view.html:8 +#, python-format +msgid "Discussion on %s" +msgstr "Diskussion zu %s" + +#: adhocracy/templates/decision/tiles.html:15 +msgid "for" +msgstr "dafür" + +#: adhocracy/templates/decision/tiles.html:17 +msgid "against" +msgstr "dagegen" + +#: adhocracy/templates/decision/tiles.html:19 +msgid "abstained" +msgstr "enthalten" + +#: adhocracy/templates/decision/tiles.html:21 +msgid "undecided" +msgstr "unentschieden" + +#: adhocracy/templates/decision/tiles.html:36 +msgid "The user's delegates have voted, but no consensus wasreached among them. The decision is deferred." +msgstr "Die Delegaten des Benutzers haben abgestimmt, aber unter ihnen herrscht kein Konsens. Für den Benutzer wurde daher keine Entscheidung gefällt." + +#: adhocracy/templates/decision/tiles.html:40 +msgid "The decision was made without delegations." +msgstr "Diese Entscheidung wurde ohne Delegation getroffen." + +#: adhocracy/templates/decision/tiles.html:42 +msgid "The decision was determined as a result of the following delegations:" +msgstr "Diese Entscheidung wurde auf der Basis der folgenden Delegationen getroffen:" + +#: adhocracy/templates/decision/tiles.html:64 +#, python-format +msgid "in a poll %s" +msgstr "in einer Abstimmung %s" + +#: adhocracy/templates/delegation/create.html:4 +#: adhocracy/templates/delegation/create.html:11 +#, python-format +msgid "Delegate: %s" +msgstr "Delegieren: %s" + +#: adhocracy/templates/delegation/create.html:7 +#: adhocracy/templates/delegation/review.html:5 +msgid "Delegation" +msgstr "Delegation" + +#: adhocracy/templates/delegation/create.html:18 +msgid "This page is obviously a placeholder. Nice, karma-based recs are coming soon." +msgstr "" + +#: adhocracy/templates/delegation/create.html:28 +#, python-format +msgid "Popular delegates for %s" +msgstr "Beliebte Delegaten für %s" + +#: adhocracy/templates/delegation/create.html:42 +msgid "Your favourite delegates" +msgstr "Deine Lieblingsdelegaten" + +#: adhocracy/templates/delegation/create.html:55 +msgid "Delegate to:" +msgstr "Delegieren an: " + +#: adhocracy/templates/delegation/review.html:2 +#: adhocracy/templates/delegation/review.html:11 +#, python-format +msgid "Delegation: %s" +msgstr "Delegation: %s" + +#: adhocracy/templates/delegation/review.html:9 +#: adhocracy/templates/delegation/tiles.html:13 +msgid "revoke" +msgstr "Widerrufen" + +#: adhocracy/templates/delegation/review.html:17 +#: adhocracy/templates/delegation/tiles.html:4 +#, python-format +msgid "from %s" +msgstr "von %s" + +#: adhocracy/templates/delegation/review.html:18 +#: adhocracy/templates/delegation/tiles.html:10 +#, python-format +msgid "to %s" +msgstr "an %s" + +#: adhocracy/templates/delegation/review.html:22 +#, python-format +msgid "established %s" +msgstr "%s angelegt" + +#: adhocracy/templates/delegation/review.html:24 +#, python-format +msgid "revoked %s" +msgstr "%s widerrufen" + +#: adhocracy/templates/delegation/review.html:30 +msgid "The delegation can be overridden or revoked at any time." +msgstr "Die Delegation kann zu jedem Zeitpunkt korrigiert oder widerrufen werden." + +#: adhocracy/templates/delegation/review.html:42 +msgid "No decisions have been based on this delegation yet. As soon as this delegation leads to any decisions, they will be listed here." +msgstr "Es wurden noch keine Entscheidungen auf Basis dieser Delegation getroffen. Sobald Entscheidungen auf der Basis dieser Delegation getroffen wurden, werden sie hier aufgeführt." + +#: adhocracy/templates/delegation/tiles.html:5 +#: adhocracy/templates/delegation/tiles.html:11 +msgid "track record" +msgstr "Protokoll" + +#: adhocracy/templates/error/http.html:2 +#: adhocracy/templates/error/http.html:4 +#, python-format +msgid "Error %s" +msgstr "Fehler %s" + +#: adhocracy/templates/error/http.html:8 +msgid "If this error continues to occur, please <a href='/page/imprint.html'>notify us</a> with a description of what you were trying to do." +msgstr "Wenn dieser Fehler weiterhin auftritt, <a href='/page/imprint.html'>gib uns bitte Bescheid</a> und Beschreibe was Du zu tun versucht hast." + +#: adhocracy/templates/event/all.html:4 +#: adhocracy/templates/event/all.html:11 +msgid "Whazza" +msgstr "Was geht?" + +#: adhocracy/templates/event/all.html:7 +msgid "All current events in Adhocracy." +msgstr "Alle aktuellen Ereignisse in Adhocracy" + +#: adhocracy/templates/instance/create.html:3 +#: adhocracy/templates/instance/create.html:6 +#: adhocracy/templates/instance/create.html:12 +msgid "New Adhocracy" +msgstr "Neue Adhocracy" + +#: adhocracy/templates/instance/create.html:18 +msgid "Adhocracy address:" +msgstr "Adresse der Adhocracy:" + +#: adhocracy/templates/instance/create.html:19 +msgid "The address may only contain alpha-numeric characters. Please note that this key cannot be changed after the Adhocracy has been created." +msgstr "Die Adresse darf nur Zahlen und Buchstaben enthalten. Beachte, dass diese Adresse nach dem Anlegen der Adhocracy nicht mehr geändert werden kann." + +#: adhocracy/templates/instance/create.html:22 +#: adhocracy/templates/instance/edit.html:59 +msgid "Description:" +msgstr "Beschreibung:" + +#: adhocracy/templates/instance/edit.html:3 +#, python-format +msgid "Manage: %s" +msgstr "Verwalten: %s" + +#: adhocracy/templates/instance/edit.html:6 +msgid "Manage" +msgstr "Verwalten" + +#: adhocracy/templates/instance/edit.html:14 +msgid "Adhocracy Name" +msgstr "Name der Adhocracy" + +#: adhocracy/templates/instance/edit.html:19 +#: adhocracy/templates/instance/view.html:34 +msgid "Voting Rules" +msgstr "Wahlregeln" + +#: adhocracy/templates/instance/edit.html:20 +msgid "Majority:" +msgstr "Mehrheit:" + +#: adhocracy/templates/instance/edit.html:21 +msgid "In order to become active, a motion must reach the given proportion of approval." +msgstr "Um gültig zu werden, muss ein Antrag den angegebenen Anteil der Stimmen erhalten." + +#: adhocracy/templates/instance/edit.html:23 +msgid "A simple majority (½ of vote)" +msgstr "Einfache Mehrheit (½ der Stimmen)" + +#: adhocracy/templates/instance/edit.html:24 +msgid "A two-thirds majority" +msgstr "Eine Zwei-Drittel-Mehrheit" + +#: adhocracy/templates/instance/edit.html:25 +msgid "In Soviet Russia, motion votes you." +msgstr "Honecker-Bingo" + +#: adhocracy/templates/instance/edit.html:28 +msgid "Delay:" +msgstr "Verzögerung:" + +#: adhocracy/templates/instance/edit.html:29 +msgid "Before activating, the defined majority must be continuously held by the moton for the specified interval." +msgstr "Bevor ein Vorschlag in Kraft tritt muss er die eingestellte Mehrheit für die angegebene Länge aufrecht erhalten." + +#: adhocracy/templates/instance/edit.html:31 +msgid "No delay" +msgstr "Keine Verzögerung" + +#: adhocracy/templates/instance/edit.html:32 +msgid "1 Day" +msgstr "1 Tag" + +#: adhocracy/templates/instance/edit.html:33 +msgid "2 Days" +msgstr "2 Tage" + +#: adhocracy/templates/instance/edit.html:34 +msgid "One Week" +msgstr "Eine Woche" + +#: adhocracy/templates/instance/edit.html:35 +msgid "Two Weeks" +msgstr "Zwei Wochen" + +#: adhocracy/templates/instance/edit.html:36 +msgid "Four Weeks" +msgstr "Vier Wochen" + +#: adhocracy/templates/instance/edit.html:39 +msgid "Membership Options" +msgstr "Mitgliedseinstellungen" + +#: adhocracy/templates/instance/edit.html:40 +#| msgid "What groups?" +msgid "Default group:" +msgstr "Standardgruppe:" + +#: adhocracy/templates/instance/edit.html:41 +msgid "When a new member joins, he or she will be a member of this user group." +msgstr "Tritt ein neues Mitglied bei, wird er oder sie Teil dieser Benutzergruppe." + +#: adhocracy/templates/instance/edit.html:51 +msgid "Logo" +msgstr "Logo" + +#: adhocracy/templates/instance/edit.html:52 +msgid "File upload:" +msgstr "Datei-Upload:" + +#: adhocracy/templates/instance/edit.html:53 +msgid "Select a logo file to appear in the header area of this Adhocracy." +msgstr "Wähle ein Logo, welches im Kopfbereich dieser Adhocracy erscheinen soll." + +#: adhocracy/templates/instance/index.html:16 +msgid "Adhocracies are little democracies that are ran by their community." +msgstr "Adhocracies sind von ihren eigenen Gemeinschaften verwaltete Kleinstdemokratien." + +#: adhocracy/templates/instance/tiles.html:8 +#: adhocracy/templates/instance/tiles.html:34 +#: adhocracy/templates/instance/view.html:14 +#: adhocracy/templates/instance/view.html:17 +msgid "join" +msgstr "beitreten" + +#: adhocracy/templates/instance/tiles.html:11 +#: adhocracy/templates/instance/tiles.html:37 +#: adhocracy/templates/instance/view.html:20 +msgid "leave" +msgstr "Verlassen" + +#: adhocracy/templates/instance/view.html:10 +msgid "members" +msgstr "Mitglieder" + +#: adhocracy/templates/instance/view.html:35 +msgid "Required Majority:" +msgstr "Erforderliche Mehrheit:" + +#: adhocracy/templates/instance/view.html:36 +msgid "To become active, a motion must reach the given proportion of approval." +msgstr "Um gültig zu werden, muss ein Vorschlag den angegebenen Anteil der Stimmen erhalten." + +#: adhocracy/templates/instance/view.html:38 +msgid "Activation Delay:" +msgstr "Verzögerung:" + +#: adhocracy/templates/instance/view.html:39 +msgid "Before becoming active, the majority must be held for the specified interval." +msgstr "Bevor ein Vorschlag in Kraft tritt muss die Mehrheit für den angegebenen Zeitraum gehalten werden. " + +#: adhocracy/templates/instance/view.html:45 +msgid "Subscribe to RSS feed:" +msgstr "Beim RSS-Feed anmelden:" + +#: adhocracy/templates/instance/view.html:55 +msgid "Categories" +msgstr "Kategorien" + +#: adhocracy/templates/instance/view.html:80 +msgid "Create an issue to start debating in this Adhocracy." +msgstr "Lege ein Thema an um die Diskussion in dieser Adhocracy zu beginnen." + +#: adhocracy/templates/issue/create.html:4 +#: adhocracy/templates/issue/create.html:7 +#: adhocracy/templates/issue/create.html:18 +msgid "New issue" +msgstr "Neues Thema" + +#: adhocracy/templates/issue/create.html:27 +msgid "Issue description:" +msgstr "Themenbeschreibung:" + +#: adhocracy/templates/issue/edit.html:13 +msgid "Issue Title" +msgstr "Titel des Themas" + +#: adhocracy/templates/issue/tiles.html:16 +#: adhocracy/templates/motion/tiles.html:20 +#: adhocracy/templates/motion/tiles.html:39 +#: adhocracy/templates/user/view.html:34 +#, python-format +msgid "%s comment" +msgid_plural "%s comments" +msgstr[0] "%s Kommentar" +msgstr[1] "%s Kommentare" + +#: adhocracy/templates/issue/tiles.html:17 +#: adhocracy/templates/user/view.html:33 +#, python-format +msgid "%s motion" +msgid_plural "%s motions" +msgstr[0] "%s Vorschlag" +msgstr[1] "%s Vorschläge" + +#: adhocracy/templates/issue/view.html:35 +#, python-format +msgid "in %s" +msgstr "in %s" + +#: adhocracy/templates/issue/view.html:57 +#: adhocracy/templates/motion/index.html:6 +msgid "Motions" +msgstr "Vorschläge" + +#: adhocracy/templates/issue/view.html:59 +msgid "Motions are <b>proposals</b> that solve some or all of the problems described by this issue. A motion can be <b>discussed and voted</b> upon." +msgstr "Vorschläge sind Ideen, die einen Teil oder die Gesamheit des im Thema identifizierten Problems lösen. Ein Vorschlag kann diskutiert und abgestimmt werden." + +#: adhocracy/templates/issue/view.html:66 +#: adhocracy/templates/motion/view.html:209 +msgid "Discussion" +msgstr "Diskussion" + +#: adhocracy/templates/motion/begin_poll.html:6 +msgid "Call a vote" +msgstr "Zur Abstimmung aufrufen" + +#: adhocracy/templates/motion/begin_poll.html:9 +#, python-format +msgid "Call a vote on: %s" +msgstr "Abstimmung beginnen: %s" + +#: adhocracy/templates/motion/begin_poll.html:17 +msgid "You are about to release this motion and call for a vote. When you do this, you will lose the ability to <strong>change the motion's wording</strong> unless you re-draft it." +msgstr "Du bist im Begriff diesen Vorschlag eine Abstimmung zu unterwerfen. Wenn Du das tust wirst Du die Möglichkeit verlieren, die <b>Formuliertung des Vorschlags zu ändern</b>, es sei denn du brichst Die Abstimmung ab." + +#: adhocracy/templates/motion/begin_poll.html:21 +#: adhocracy/templates/motion/end_poll.html:21 +msgid "Confirm and Proceed" +msgstr "Bestätigen und Fortsetzen" + +#: adhocracy/templates/motion/create.html:4 +#: adhocracy/templates/motion/create.html:7 +#: adhocracy/templates/motion/create.html:14 +msgid "New motion" +msgstr "Neuer Vorschlag" + +#: adhocracy/templates/motion/create.html:20 +msgid "Informal motion description:" +msgstr "Informelle Beschreibung des Vorschlags:" + +#: adhocracy/templates/motion/edit.html:13 +msgid "New Motion" +msgstr "Neuer Vorschlag" + +#: adhocracy/templates/motion/end_poll.html:6 +msgid "End a vote" +msgstr "Eine Abstimmung abbrechen" + +#: adhocracy/templates/motion/end_poll.html:9 +#, python-format +msgid "Cancel vote on: %s" +msgstr "Abstimmung abbrechen: %s" + +#: adhocracy/templates/motion/end_poll.html:17 +msgid "You are about to re-draft this motion. This means that the motion will become editable again but that all votes that have been cast at this time <strong>will be invalidated</strong>." +msgstr "Du bist im Begriff diesen Vorschlag wieder zum Entwurf zu machen. Das bedeutet, dass der Vorschlag wieder bearbeitet werden kann, jedoch werden alle <b>abgegebenen Stimmen ungültig</b>." + +#: adhocracy/templates/motion/index.html:3 +#, python-format +msgid "Critical Motions in %s" +msgstr "Dringende Vorschläge in %s" + +#: adhocracy/templates/motion/index.html:9 +msgid "Critical Motions" +msgstr "Dringende Vorschläge" + +#: adhocracy/templates/motion/index.html:14 +msgid "These motions are currently voting and might be nearing a decision. Vote now to make your voice heard." +msgstr "Diese Vorschläge stehen aktuell zur Abstimmung und könnten bald entschieden werden. Stimme jetzt ab, damit Du gehört wirst." + +#: adhocracy/templates/motion/index.html:27 +msgid "There are currently no polls open. Call a motion into voting state to begin a poll." +msgstr "Aktuell laufen keine Abstimmungen. Rufe in einem Vorschlag zur Abstimmung auf um abstimmen zu können." + +#: adhocracy/templates/motion/tiles.html:18 +#: adhocracy/templates/motion/tiles.html:37 +#, python-format +msgid "%s votes cast" +msgstr "%s Stimmen abgegeben" + +#: adhocracy/templates/motion/tiles.html:46 +msgid "Draft" +msgstr "Entwurf" + +#: adhocracy/templates/motion/tiles.html:48 +msgid "Voting" +msgstr "Abstimmung" + +#: adhocracy/templates/motion/tiles.html:50 +msgid "Activating" +msgstr "Einführung" + +#: adhocracy/templates/motion/tiles.html:52 +msgid "Deactivating" +msgstr "Läuft aus" + +#: adhocracy/templates/motion/tiles.html:54 +msgid "Active" +msgstr "Gültig" + +#: adhocracy/templates/motion/view.html:32 +#: adhocracy/templates/motion/view.html:34 +#: adhocracy/templates/motion/view.html:36 +msgid "call a vote" +msgstr "Abstimmen" + +#: adhocracy/templates/motion/view.html:34 +msgid "The motion cannot be voted upon since it does not have any provisions yet." +msgstr "Der Vorschlag kann nicht Abgestimmt werden, weil er keine Absätze hat." + +#: adhocracy/templates/motion/view.html:66 +msgid "This vote has recieved the required majority and cannot be cancelled" +msgstr "Diese Abstimmung hat die erforderliche Mehrheit erreicht und kann daher nicht abgebrochen werden" + +#: adhocracy/templates/motion/view.html:70 +msgid "Vote" +msgstr "Abstimmung" + +#: adhocracy/templates/motion/view.html:83 +msgid "Option" +msgstr "Option" + +#: adhocracy/templates/motion/view.html:86 +msgid "Delegate Recommendations" +msgstr "Empfehlungen der Delegaten" + +#: adhocracy/templates/motion/view.html:89 +#: adhocracy/templates/motion/votes.html:8 +#: adhocracy/templates/user/votes.html:8 +msgid "Votes" +msgstr "Stimmen" + +#: adhocracy/templates/motion/view.html:90 +msgid "Percent" +msgstr "Prozent" + +#: adhocracy/templates/motion/view.html:102 +msgid "Affirm" +msgstr "Dafür" + +#: adhocracy/templates/motion/view.html:123 +msgid "Dissent" +msgstr "Dagegen" + +#: adhocracy/templates/motion/view.html:144 +msgid "Abstain" +msgstr "Enthalten" + +#: adhocracy/templates/motion/view.html:158 +msgid "vote" +msgstr "abstimmen" + +#: adhocracy/templates/motion/view.html:163 +#, python-format +msgid "Of the required %s votes, the motion has:" +msgstr "Von den erforderlichen %s Stimmen erreichte der Vorschlag bisher:" + +#: adhocracy/templates/motion/view.html:168 +#, python-format +msgid "%d votes" +msgstr "%d Stimmen" + +#: adhocracy/templates/motion/view.html:181 +#, python-format +msgid "poll started %s" +msgstr "Abstimmung wurde %s gestartet" + +#: adhocracy/templates/motion/view.html:182 +msgid "help" +msgstr "Hilfe" + +#: adhocracy/templates/motion/view.html:196 +msgid "Provisions" +msgstr "Absätze" + +#: adhocracy/templates/motion/view.html:198 +msgid "<b>Provisions</b> are the body of a motion: together they form the language that will be voted upon. You will need to have at least one clause in order to call for a vote." +msgstr "<b>Absätze</b> sind der Kern eines Vorschlags: zusammen bilden sie den Text, über den schließlich abgestimmt wird. Du braucht mindestens einen Absatz um zu einer Abstimmung aufzurufen." + +#: adhocracy/templates/motion/votes.html:5 +#: adhocracy/templates/user/votes.html:5 +#, python-format +msgid "Votes: %s" +msgstr "%s Stimmen abgegeben" + +#: adhocracy/templates/motion/votes.html:12 +#: adhocracy/templates/user/votes.html:14 +msgid "Votes:" +msgstr "Stimmen: " + +#: adhocracy/templates/search/results.html:2 +#: adhocracy/templates/search/results.html:5 +#: adhocracy/templates/search/results.html:12 +msgid "Search" +msgstr "Suchen" + +#: adhocracy/templates/search/results.html:10 +#, python-format +msgid "Search for '%s'" +msgstr "Nach '%s' suchen" + +#: adhocracy/templates/search/results.html:28 +msgid "No entries could be found that match your criteria. Try a more general search term." +msgstr "Kein Eintrag trifft auf die angegebenen Kriterien zu. Versuche einen allgemeineren Suchbegriff." + +#: adhocracy/templates/user/delegations.html:4 +#: adhocracy/templates/user/delegations.html:17 +msgid "My Delegations" +msgstr "Meine Delegationen" + +#: adhocracy/templates/user/delegations.html:6 +#: adhocracy/templates/user/delegations.html:19 +#, python-format +msgid "Delegations: %s" +msgstr "Delegationen: %s" + +#: adhocracy/templates/user/delegations.html:25 +msgid "Topic" +msgstr "Bereich" + +#: adhocracy/templates/user/delegations.html:26 +msgid "Given" +msgstr "Erteilt" + +#: adhocracy/templates/user/delegations.html:27 +msgid "Received" +msgstr "Erhalten" + +#: adhocracy/templates/user/edit.html:4 +#, python-format +msgid "Settings: %s" +msgstr "Einstellungen: %s" + +#: adhocracy/templates/user/edit.html:20 +msgid "User Details" +msgstr "Benutzerdetails" + +#: adhocracy/templates/user/edit.html:22 +#: adhocracy/templates/user/register_form.html:9 +#: adhocracy/templates/user/reset_form.html:14 +msgid "E-Mail:" +msgstr "E-Mail:" + +#: adhocracy/templates/user/edit.html:24 +msgid "Language:" +msgstr "Sprache:" + +#: adhocracy/templates/user/edit.html:36 +#: adhocracy/templates/user/login_form.html:5 +#: adhocracy/templates/user/register_form.html:13 +msgid "Password:" +msgstr "Passwort:" + +#: adhocracy/templates/user/edit.html:38 +msgid "Select a new password or leave the fields blank to keep your old one." +msgstr "Wähle ein neues Passwort oder lass die Felder leer um Dein altes beizubehalten." + +#: adhocracy/templates/user/edit.html:42 +#: adhocracy/templates/user/register_form.html:16 +msgid "Password (confirm):" +msgstr "Passwort (bestätigen):" + +#: adhocracy/templates/user/edit.html:46 +msgid "Configure your user icon at <a href='http://www.gravatar.com'>Gravatar</a>" +msgstr "Lege bei <a href='http://www.gravatar.com'>Gravatar</a> Dein Benutzerbild fest." + +#: adhocracy/templates/user/edit.html:51 +msgid "Short biography" +msgstr "Kurzbiographie" + +#: adhocracy/templates/user/edit.html:54 +msgid "A bio will allow others to learn about you and perhaps even get you a few delegations." +msgstr "Eine Kurzbiographie wird es anderen erlauben mehr über Dich zu erfahren und vielleicht Entscheidungen an Dich zu übertragen." + +#: adhocracy/templates/user/index.html:3 +#: adhocracy/templates/user/index.html:9 +#, python-format +msgid "Users in %s" +msgstr "%ss Benutzer" + +#: adhocracy/templates/user/login.html:2 +msgid "Who are you, then?" +msgstr "Und wer bist Du? " + +#: adhocracy/templates/user/login.html:5 +#: adhocracy/templates/user/login_form.html:8 +msgid "Login" +msgstr "Anmelden" + +#: adhocracy/templates/user/login.html:6 +msgid "If you already have an account, sign in here." +msgstr "Falls Du schon einen Account hast, melde Dich hier an." + +#: adhocracy/templates/user/login.html:10 +msgid "If you have an account but you've lost your password, <a href='/user/reset'>click here</a>." +msgstr "Wenn du schon einen Benutzernamen besitzt, aber dein Kennwort verloren hast, <a href='/user/reset'>klick hier</a>." + +#: adhocracy/templates/user/login.html:15 +#: adhocracy/templates/user/register_form.html:19 +msgid "Register" +msgstr "Registrieren" + +#: adhocracy/templates/user/login.html:16 +msgid "Creating an account is easy; all you need is a user name, password and email address." +msgstr "Ein einen Account anzulegen ist einfach; Du brauchst nur einen Benutzernamen, ein Passwort und eine E-Mail Adresse." + +#: adhocracy/templates/user/login_form.html:2 +msgid "Login:" +msgstr "Login:" + +#: adhocracy/templates/user/register_form.html:5 +msgid "User name:" +msgstr "Nutzername:" + +#: adhocracy/templates/user/register_form.html:6 +msgid "Can only contain letters and numbers." +msgstr "Darf nur Zeichen und Zahlen enthalten." + +#: adhocracy/templates/user/register_form.html:10 +msgid "We don't spam." +msgstr "Wir versenden keine Werbung." + +#: adhocracy/templates/user/reset_form.html:2 +#: adhocracy/templates/user/reset_form.html:4 +#: adhocracy/templates/user/reset_pending.html:2 +#: adhocracy/templates/user/reset_pending.html:4 +msgid "Password reset" +msgstr "Kennwort wurde zurückgesetzt" + +#: adhocracy/templates/user/reset_form.html:8 +msgid "You will be sent an activation link that, when opened, will cause Adhocracy to email you a new password." +msgstr "Dir wird ein Link zugesand der, wenn du ihn öffnest, dazu führt, dass Adhocracy dir ein neues Kennwort zuschickt." + +#: adhocracy/templates/user/reset_form.html:12 +msgid "In order to retrieve your login, you will have to enter your email adress." +msgstr "Um deine Benutzernamen zugesannt zu bekommen muss du deine Email Adresse angeben." + +#: adhocracy/templates/user/reset_form.html:18 +msgid "Reset" +msgstr "Zurücksetzen" + +#: adhocracy/templates/user/reset_pending.html:4 +msgid "Confirmation pending" +msgstr "Bestätigung steht aus" + +#: adhocracy/templates/user/reset_pending.html:11 +msgid "You have recieved an email containing a link. Please open that link in order to reset you password" +msgstr "Du hast eine Email erhalten. Bitte öffne den darin enthaltenen Link um dein Kennwort zurück zu setzen." + +#: adhocracy/templates/user/tiles.html:16 +#, python-format +msgid "%s karma" +msgstr "%s Karma" + +#: adhocracy/templates/user/tiles.html:18 +#: adhocracy/templates/user/view.html:38 +#, python-format +msgid "signed up %s" +msgstr "%s angemeldet" + +#: adhocracy/templates/user/view.html:24 +#, python-format +msgid "%s does not have a bio" +msgstr "%s hat keine Kurzbiographie" + +#: adhocracy/templates/user/view.html:36 +msgid "delegations" +msgstr "Delegationen" + +#: adhocracy/templates/user/view.html:46 +msgid "is a member in the following adhocracies:" +msgstr "ist Mitglied in den folgenden Adhocracies:" + +#: adhocracy/templates/user/view.html:56 +msgid "Activity" +msgstr "Ereignisse" + +#: adhocracy/templates/user/votes.html:12 +msgid "Review your voting track" +msgstr "Kontrolliere Dein Abstimmungsverhalten" + diff --git a/adhocracy/i18n/en/LC_MESSAGES/adhocracy.mo b/adhocracy/i18n/en/LC_MESSAGES/adhocracy.mo new file mode 100644 index 000000000..744de7845 Binary files /dev/null and b/adhocracy/i18n/en/LC_MESSAGES/adhocracy.mo differ diff --git a/adhocracy/i18n/en/LC_MESSAGES/adhocracy.po b/adhocracy/i18n/en/LC_MESSAGES/adhocracy.po new file mode 100644 index 000000000..b0a102373 --- /dev/null +++ b/adhocracy/i18n/en/LC_MESSAGES/adhocracy.po @@ -0,0 +1,2081 @@ +# English translations for adhocracy. +# Copyright (C) 2009 ORGANIZATION +# This file is distributed under the same license as the adhocracy project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: adhocracy 0.1\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2009-10-29 13:31+0100\n" +"PO-Revision-Date: 2009-09-15 16:36+0200\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: en <LL@li.org>\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.0dev-r0\n" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:61 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:62 +#: adhocracy/contrib/babel/babel/tests/support.py:62 +#: adhocracy/contrib/babel/babel/tests/support.py:67 +#: adhocracy/contrib/babel/babel/tests/support.py:114 +msgid "foo" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:63 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:64 +msgid "There is" +msgid_plural "There are" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:65 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:66 +msgid "Fizz" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:67 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:68 +msgid "Fuzz" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:69 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:70 +msgid "Fuzzes" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/file1.py:8 +msgid "bar" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/CVS/this_wont_normally_be_here.py:11 +msgid "FooBar" +msgid_plural "FooBars" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/contrib/babel/babel/tests/support.py:78 +#: adhocracy/contrib/babel/babel/tests/support.py:80 +#: adhocracy/contrib/babel/babel/tests/support.py:90 +#: adhocracy/contrib/babel/babel/tests/support.py:92 +#: adhocracy/contrib/babel/babel/tests/support.py:132 +#: adhocracy/contrib/babel/babel/tests/support.py:134 +msgid "foo1" +msgid_plural "foos1" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/controllers/admin.py:69 +#, python-format +msgid "%(user)s is not a member of %(instance)s" +msgstr "" + +#: adhocracy/controllers/admin.py:93 +#, python-format +msgid "%(user)s was removed from %(instance)s" +msgstr "" + +#: adhocracy/controllers/admin.py:97 +#, python-format +msgid "%(user)s isn't a member of %(instance)s" +msgstr "" + +#: adhocracy/controllers/category.py:42 adhocracy/controllers/category.py:66 +#, python-format +msgid "No category with ID '%s' exists." +msgstr "" + +#: adhocracy/controllers/category.py:86 adhocracy/controllers/category.py:90 +#: adhocracy/templates/category/view.html:25 +#, fuzzy, python-format +msgid "Category: %s" +msgstr "" + +#: adhocracy/controllers/category.py:94 adhocracy/controllers/category.py:101 +#: adhocracy/controllers/comment.py:143 adhocracy/controllers/delegation.py:77 +#: adhocracy/controllers/instance.py:52 adhocracy/controllers/instance.py:98 +#: adhocracy/controllers/instance.py:105 adhocracy/controllers/issue.py:99 +#: adhocracy/controllers/motion.py:36 adhocracy/controllers/motion.py:190 +#: adhocracy/controllers/user.py:51 adhocracy/controllers/user.py:253 +msgid "oldest" +msgstr "" + +#: adhocracy/controllers/category.py:95 adhocracy/controllers/category.py:102 +#: adhocracy/controllers/comment.py:144 adhocracy/controllers/delegation.py:78 +#: adhocracy/controllers/instance.py:53 adhocracy/controllers/instance.py:99 +#: adhocracy/controllers/instance.py:106 adhocracy/controllers/issue.py:100 +#: adhocracy/controllers/motion.py:37 adhocracy/controllers/motion.py:191 +#: adhocracy/controllers/user.py:52 adhocracy/controllers/user.py:254 +msgid "newest" +msgstr "" + +#: adhocracy/controllers/category.py:96 adhocracy/controllers/category.py:103 +#: adhocracy/controllers/instance.py:54 adhocracy/controllers/instance.py:100 +#: adhocracy/controllers/instance.py:107 adhocracy/controllers/issue.py:101 +#: adhocracy/controllers/motion.py:38 adhocracy/controllers/user.py:54 +msgid "activity" +msgstr "" + +#: adhocracy/controllers/category.py:97 adhocracy/controllers/category.py:104 +#: adhocracy/controllers/instance.py:55 adhocracy/controllers/instance.py:101 +#: adhocracy/controllers/instance.py:108 adhocracy/controllers/issue.py:102 +#: adhocracy/controllers/motion.py:40 adhocracy/controllers/user.py:55 +msgid "name" +msgstr "" + +#: adhocracy/controllers/category.py:114 +msgid "Deleting the root category isn't possible." +msgstr "" + +#: adhocracy/controllers/category.py:117 +#, python-format +msgid "No category with ID '%(id)s' exists." +msgstr "" + +#: adhocracy/controllers/category.py:130 +#, python-format +msgid "Category '%(category)s' has been deleted." +msgstr "" + +#: adhocracy/controllers/comment.py:43 +msgid "Unsupported topic type." +msgstr "" + +#: adhocracy/controllers/comment.py:84 adhocracy/controllers/comment.py:103 +#: adhocracy/controllers/comment.py:112 adhocracy/controllers/comment.py:122 +#: adhocracy/controllers/comment.py:139 adhocracy/controllers/comment.py:155 +#, python-format +msgid "No comment with ID %s exists" +msgstr "" + +#: adhocracy/controllers/comment.py:159 +msgid "" +"You're trying to revert to a revision which is not part of this comments " +"history" +msgstr "" + +#: adhocracy/controllers/delegation.py:21 +#, python-format +msgid "No motion or category with ID '%(id)s' exists" +msgstr "" + +#: adhocracy/controllers/delegation.py:52 +#: adhocracy/controllers/delegation.py:70 +#, python-format +msgid "Couldn't find delegation %(id)s" +msgstr "" + +#: adhocracy/controllers/delegation.py:54 +#, python-format +msgid "Cannot access delegation %(id)s" +msgstr "" + +#: adhocracy/controllers/delegation.py:63 +msgid "The delegation is now revoked." +msgstr "" + +#: adhocracy/controllers/instance.py:42 +#, python-format +msgid "No such adhocracy exists: %(key)s" +msgstr "" + +#: adhocracy/controllers/instance.py:47 +msgid "" +"An index of adhocracies run at adhocracy.cc. Select which ones you would " +"like to join and participate in!" +msgstr "" + +#: adhocracy/controllers/instance.py:89 +#, python-format +msgid "%s News" +msgstr "" + +#: adhocracy/controllers/instance.py:91 +#, python-format +msgid "News from the %s Adhocracy" +msgstr "" + +#: adhocracy/controllers/instance.py:171 +msgid "Deleting an instance is not currently implemented" +msgstr "" + +#: adhocracy/controllers/instance.py:178 +#, python-format +msgid "You're already a member in %(instance)s." +msgstr "" + +#: adhocracy/controllers/instance.py:192 +#, python-format +msgid "Welcome to %(instance)s" +msgstr "" + +#: adhocracy/controllers/instance.py:201 +#, python-format +msgid "You're not a member of %(instance)s." +msgstr "" + +#: adhocracy/controllers/instance.py:204 +#, python-format +msgid "You're the founder of %s, cannot leave." +msgstr "" + +#: adhocracy/controllers/issue.py:60 adhocracy/controllers/issue.py:80 +#: adhocracy/controllers/issue.py:115 +#, python-format +msgid "No issue with ID %s exists." +msgstr "" + +#: adhocracy/controllers/issue.py:90 adhocracy/templates/issue/view.html:27 +#, python-format +msgid "Issue: %s" +msgstr "" + +#: adhocracy/controllers/issue.py:92 +#, python-format +msgid "Activity on the %s issue" +msgstr "" + +#: adhocracy/controllers/issue.py:94 +#, python-format +msgid "Issue: %(issue)s" +msgstr "" + +#: adhocracy/controllers/issue.py:124 +#, python-format +msgid "" +"The issue %(issue)s cannot be deleted, because the contained motion " +"%(motion)s is polling." +msgstr "" + +#: adhocracy/controllers/issue.py:131 +#, python-format +msgid "Issue '%(issue)s' has been deleted." +msgstr "" + +#: adhocracy/controllers/karma.py:28 +msgid "Invalid karma value. Karma is either positive or negative!" +msgstr "" + +#: adhocracy/controllers/motion.py:39 +msgid "urgency" +msgstr "" + +#: adhocracy/controllers/motion.py:106 +#, fuzzy, python-format +msgid "Motion: %s" +msgstr "" + +#: adhocracy/controllers/motion.py:108 +#, python-format +msgid "Activity on the %s motion" +msgstr "" + +#: adhocracy/controllers/motion.py:110 +#, python-format +msgid "Motion: %(motion)s" +msgstr "" + +#: adhocracy/controllers/motion.py:146 +msgid "" +"The poll cannot be started either because there are no provisions or a " +"poll has already started." +msgstr "" + +#: adhocracy/controllers/motion.py:170 +msgid "The motion is not undergoing a poll." +msgstr "" + +#: adhocracy/controllers/motion.py:194 +#, python-format +msgid "%s is not currently in a poll, thus no votes have been counted." +msgstr "" + +#: adhocracy/controllers/page.py:19 adhocracy/controllers/page.py:27 +msgid "The requested page was not found" +msgstr "" + +#: adhocracy/controllers/root.py:21 adhocracy/lib/base.py:72 +#: adhocracy/templates/index.html:22 +msgid "My Adhocracies" +msgstr "" + +#: adhocracy/controllers/root.py:23 +msgid "Updates from the Adhocracies in which you are a member" +msgstr "" + +#: adhocracy/controllers/root.py:36 +#, python-format +msgid "No motion or category with ID %(id)s exists" +msgstr "" + +#: adhocracy/controllers/search.py:25 +msgid "Received no query for search." +msgstr "" + +#: adhocracy/controllers/user.py:53 +msgid "karma" +msgstr "" + +#: adhocracy/controllers/user.py:93 adhocracy/controllers/user.py:141 +#: adhocracy/controllers/user.py:165 adhocracy/controllers/user.py:248 +#: adhocracy/controllers/user.py:263 +#, python-format +msgid "No user named '%s' exists" +msgstr "" + +#: adhocracy/controllers/user.py:95 +#, python-format +msgid "You're not authorized to change %s's settings." +msgstr "" + +#: adhocracy/controllers/user.py:124 +msgid "There is no user registered with that email address." +msgstr "" + +#: adhocracy/controllers/user.py:131 +msgid "" +"you have requested that your password for Adhocracy be reset. In order to" +" confirm the validity of your claim, please open the link below in your " +"browser:" +msgstr "" + +#: adhocracy/controllers/user.py:134 +msgid "Reset your password" +msgstr "" + +#: adhocracy/controllers/user.py:152 +msgid "your password has been reset. It is now:" +msgstr "" + +#: adhocracy/controllers/user.py:153 +msgid "Please login and change the password in your user settings." +msgstr "" + +#: adhocracy/controllers/user.py:154 +msgid "Your new password" +msgstr "" + +#: adhocracy/controllers/user.py:155 +msgid "Success. You have been sent an email with your new password." +msgstr "" + +#: adhocracy/controllers/user.py:157 +msgid "The reset code is invalid. Please repeat the password recovery procedure." +msgstr "" + +#: adhocracy/controllers/user.py:169 +#, python-format +msgid "%(user)s is using Adhocracy, a direct democracy decision-making tool." +msgstr "" + +#: adhocracy/controllers/user.py:178 +#, python-format +msgid "%(user)ss Activity" +msgstr "" + +#: adhocracy/controllers/user.py:182 +#, python-format +msgid "%s is not a member of %s" +msgstr "" + +#: adhocracy/controllers/user.py:218 +msgid "Invalid user name or password" +msgstr "" + +#: adhocracy/controllers/vote.py:16 adhocracy/lib/base.py:43 +#, python-format +msgid "No motion with ID %(id)s exists." +msgstr "" + +#: adhocracy/controllers/vote.py:18 +msgid "You have no voting rights." +msgstr "" + +#: adhocracy/controllers/vote.py:27 +msgid "This motion is not currently being voted on." +msgstr "" + +#: adhocracy/controllers/vote.py:31 +msgid "Illegal input for vote cast." +msgstr "" + +#: adhocracy/lib/base.py:77 +msgid "" +"A liquid democracy platform for making decisions in distributed, open " +"groups by cooperatively creating proposals and voting on them to " +"establish their support." +msgstr "" + +#: adhocracy/lib/base.py:80 +msgid "" +"adhocracy, direct democracy, liquid democracy, liqd, democracy, wiki, " +"voting,participation, group decisions, decisions, decision-making" +msgstr "" + +#: adhocracy/lib/helpers.py:33 adhocracy/templates/template.html:36 +#: adhocracy/templates/template.html:77 adhocracy/templates/user/parts.html:5 +msgid "Adhocracy" +msgstr "" + +#: adhocracy/lib/helpers.py:51 +msgid "This motion is currently being voted on and cannot be modified." +msgstr "" + +#: adhocracy/lib/helpers.py:103 +msgid "You" +msgstr "" + +#: adhocracy/lib/mail.py:17 +#, python-format +msgid "Hi %s," +msgstr "" + +#: adhocracy/lib/mail.py:19 +msgid "" +"Cheers,\r\n" +"\r\n" +" the Adhocracy Team\r\n" +msgstr "" + +#: adhocracy/lib/xsrf.py:49 +msgid "" +"Action failed. You were probably trying to re-perform an action after " +"using your browser's 'Back' button. This is prohibited for security " +"reasons." +msgstr "" + +#: adhocracy/lib/event/event.py:64 +msgid "(Undefined)" +msgstr "" + +#: adhocracy/lib/event/formatting.py:79 +msgid "voted for" +msgstr "" + +#: adhocracy/lib/event/formatting.py:80 +msgid "abstained on" +msgstr "" + +#: adhocracy/lib/event/formatting.py:81 +msgid "voted against" +msgstr "" + +#: adhocracy/lib/event/formatting.py:89 +msgid "comment" +msgstr "" + +#: adhocracy/lib/event/types.py:44 +msgid "signed up" +msgstr "" + +#: adhocracy/lib/event/types.py:45 +msgid "edited their profile" +msgstr "" + +#: adhocracy/lib/event/types.py:46 +#, python-format +msgid "edited %(user)ss profile" +msgstr "" + +#: adhocracy/lib/event/types.py:47 +#, python-format +msgid "founded the %(instance)s Adhocracy" +msgstr "" + +#: adhocracy/lib/event/types.py:48 +#, python-format +msgid "updated the %(instance)s Adhocracy" +msgstr "" + +#: adhocracy/lib/event/types.py:49 +#, python-format +msgid "deleted the %(instance)s Adhocracy" +msgstr "" + +#: adhocracy/lib/event/types.py:50 +#, python-format +msgid "joined %(instance)s" +msgstr "" + +#: adhocracy/lib/event/types.py:51 +#, python-format +msgid "left %(instance)s" +msgstr "" + +#: adhocracy/lib/event/types.py:52 +#, python-format +msgid "was forced to leave %(instance)s by %(user)s" +msgstr "" + +#: adhocracy/lib/event/types.py:53 +#, python-format +msgid "now is a %(group)s within %(instance)s" +msgstr "" + +#: adhocracy/lib/event/types.py:54 +#, python-format +msgid "created %(issue)s" +msgstr "" + +#: adhocracy/lib/event/types.py:55 +#, python-format +msgid "edited %(issue)s" +msgstr "" + +#: adhocracy/lib/event/types.py:56 +#, python-format +msgid "deleted %(issue)s" +msgstr "" + +#: adhocracy/lib/event/types.py:57 +#, python-format +msgid "created %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:58 +#, python-format +msgid "edited %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:59 +#, python-format +msgid "re-drafted %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:60 +#, python-format +msgid "called a vote on %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:61 +#, python-format +msgid "deleted %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:62 +#, python-format +msgid "named %(user)s as an editor for %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:63 +#, python-format +msgid "removed %(user)s from the editors of %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:64 +#, python-format +msgid "created the category %(category)s in %(parent)s" +msgstr "" + +#: adhocracy/lib/event/types.py:65 +#, python-format +msgid "updated the category %(category)s" +msgstr "" + +#: adhocracy/lib/event/types.py:66 +#, python-format +msgid "deleted the category %(category)s" +msgstr "" + +#: adhocracy/lib/event/types.py:67 +#, python-format +msgid "created a %(comment)s on %(delegateable)s" +msgstr "" + +#: adhocracy/lib/event/types.py:68 +#, python-format +msgid "edited a %(comment)s on %(delegateable)s" +msgstr "" + +#: adhocracy/lib/event/types.py:69 +#, python-format +msgid "deleted a %(comment)s from %(delegateable)s" +msgstr "" + +#: adhocracy/lib/event/types.py:70 +#, python-format +msgid "delegated voting on %(scope)s to %(agent)s" +msgstr "" + +#: adhocracy/lib/event/types.py:71 +#, python-format +msgid "revoked their delegation on %(scope)s to %(agent)s" +msgstr "" + +#: adhocracy/lib/event/types.py:72 +#, fuzzy, python-format +msgid "%(vote)s %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:73 +#, python-format +msgid "test %(test)s" +msgstr "" + +#: adhocracy/lib/instance/__init__.py:13 +msgid "This action is only available in an instance context." +msgstr "" + +#: adhocracy/lib/karma/threshold.py:18 +#, fuzzy +msgid "create a category" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:19 +#, fuzzy +msgid "edit this category" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:20 +#, fuzzy +msgid "delete this category" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:21 +msgid "reply in a comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:22 +msgid "edit this comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:23 +msgid "delete this comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:24 +msgid "rate this comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:25 +msgid "create a motion" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:26 +#, fuzzy +msgid "edit this motion" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:27 +msgid "call for a vote" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:28 +msgid "cancel a vote" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:29 +msgid "delete a motion" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:30 +msgid "create an issue" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:31 +#, fuzzy +msgid "edit this issue" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:32 +msgid "delete this issue" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:33 +#, python-format +msgid "You need %s karma to %s" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:34 +msgid "do this" +msgstr "" + +#: adhocracy/lib/text/i18n.py:45 +msgid "Today" +msgstr "" + +#: adhocracy/lib/text/i18n.py:47 +msgid "Yesterday" +msgstr "" + +#: adhocracy/lib/text/i18n.py:61 +#, python-format +msgid "%(ts)s ago" +msgstr "" + +#: adhocracy/model/forms.py:27 +msgid "No username is given" +msgstr "" + +#: adhocracy/model/forms.py:31 +msgid "The username is invalid" +msgstr "" + +#: adhocracy/model/forms.py:35 +msgid "That username already exists" +msgstr "" + +#: adhocracy/model/forms.py:44 +msgid "That email is already registered" +msgstr "" + +#: adhocracy/model/forms.py:52 +msgid "No instance key is given" +msgstr "" + +#: adhocracy/model/forms.py:56 +msgid "The instance key is invalid" +msgstr "" + +#: adhocracy/model/forms.py:60 +msgid "An instance with that key already exists" +msgstr "" + +#: adhocracy/model/forms.py:69 +#, python-format +msgid "No entity with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:105 +#, python-format +msgid "No group with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:114 +#, python-format +msgid "No revision with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:123 +#, python-format +msgid "No comment with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:131 +#, python-format +msgid "'%s' is not a valid motion state." +msgstr "" + +#: adhocracy/model/forms.py:140 +#, python-format +msgid "No user with the user name '%s' exists" +msgstr "" + +#: adhocracy/model/motion.py:38 +msgid "Motion doesn't have a distinct parent issue." +msgstr "" + +#: adhocracy/templates/components.html:4 +msgid "formatting hints" +msgstr "" + +#: adhocracy/templates/components.html:11 +msgid "Save" +msgstr "" + +#: adhocracy/templates/components.html:13 +msgid "or" +msgstr "" + +#: adhocracy/templates/components.html:13 +#: adhocracy/templates/motion/view.html:64 +#: adhocracy/templates/motion/view.html:66 +#: adhocracy/templates/motion/view.html:68 +msgid "cancel" +msgstr "" + +#: adhocracy/templates/components.html:21 adhocracy/templates/index.html:10 +msgid "Say hi" +msgstr "" + +#: adhocracy/templates/components.html:24 +msgid "If you're not registered yet, <a href='/register'>sign up here</a>." +msgstr "" + +#: adhocracy/templates/components.html:33 +msgid "Delegate voting" +msgstr "" + +#: adhocracy/templates/components.html:38 +msgid "You have voted yourself and not delegated voting." +msgstr "" + +#: adhocracy/templates/components.html:39 +msgid "Info..." +msgstr "" + +#: adhocracy/templates/components.html:41 +msgid "You have not delegated voting." +msgstr "" + +#: adhocracy/templates/components.html:42 +#: adhocracy/templates/category/view.html:22 +#: adhocracy/templates/instance/view.html:23 +#: adhocracy/templates/issue/view.html:24 +#: adhocracy/templates/motion/view.html:28 +msgid "delegate" +msgstr "" + +#: adhocracy/templates/components.html:43 +msgid "info..." +msgstr "" + +#: adhocracy/templates/components.html:47 +msgid "By voting yourself, you have overridden:" +msgstr "" + +#: adhocracy/templates/components.html:49 +msgid "You have delegated voting to:" +msgstr "" + +#: adhocracy/templates/components.html:56 +#: adhocracy/templates/decision/tiles.html:50 +msgid "on" +msgstr "" + +#: adhocracy/templates/components.html:57 +#: adhocracy/templates/decision/tiles.html:51 +msgid "review" +msgstr "" + +#: adhocracy/templates/components.html:66 +msgid "You hold an additional vote." +msgstr "" + +#: adhocracy/templates/components.html:68 +#, python-format +msgid "You hold %s additional votes." +msgstr "" + +#: adhocracy/templates/components.html:81 +msgid "What now?" +msgstr "" + +#: adhocracy/templates/components.html:81 +msgid "— using Adhocracy in 3<sup>½</sup> steps:" +msgstr "" + +#: adhocracy/templates/components.html:87 +msgid "Create and discuss issues that need solutions." +msgstr "" + +#: adhocracy/templates/components.html:92 +msgid "Cooperate to develop proposals affecting the issues." +msgstr "" + +#: adhocracy/templates/components.html:96 +msgid "Vote on proposals to collectively make decisions.*" +msgstr "" + +#: adhocracy/templates/components.html:101 +msgid "*Or — if you like — delegate voting in some fields to a peer." +msgstr "" + +#: adhocracy/templates/index.html:3 +msgid "Welcome" +msgstr "" + +#: adhocracy/templates/index.html:5 +msgid "Welcome to Adhocracy" +msgstr "" + +#: adhocracy/templates/index.html:9 +msgid "sign up" +msgstr "" + +#: adhocracy/templates/index.html:15 +msgid "If you're not registered, <a href='/register'>sign up.</a>" +msgstr "" + +#: adhocracy/templates/index.html:20 adhocracy/templates/index.html:36 +#: adhocracy/templates/user/view.html:43 +msgid "more" +msgstr "" + +#: adhocracy/templates/index.html:21 adhocracy/templates/index.html:37 +#: adhocracy/templates/category/view.html:47 +#: adhocracy/templates/category/view.html:49 +#: adhocracy/templates/category/view.html:67 +#: adhocracy/templates/category/view.html:69 +#: adhocracy/templates/instance/index.html:10 +#: adhocracy/templates/instance/view.html:51 +#: adhocracy/templates/instance/view.html:53 +#: adhocracy/templates/instance/view.html:72 +#: adhocracy/templates/instance/view.html:74 +#: adhocracy/templates/issue/view.html:53 +#: adhocracy/templates/issue/view.html:55 +#: adhocracy/templates/motion/view.html:189 +#: adhocracy/templates/motion/view.html:191 +#: adhocracy/templates/motion/view.html:193 +msgid "new" +msgstr "" + +#: adhocracy/templates/index.html:31 +msgid "Join a few Adhocracies to contribute to their policies." +msgstr "" + +#: adhocracy/templates/index.html:38 adhocracy/templates/template.html:114 +#: adhocracy/templates/instance/index.html:3 +#: adhocracy/templates/instance/index.html:12 +#: adhocracy/templates/user/view.html:44 +msgid "Adhocracies" +msgstr "" + +#: adhocracy/templates/index.html:52 +msgid "Adhocracy helps <strong>groups</strong> make <strong>decisions</strong>." +msgstr "" + +#: adhocracy/templates/index.html:55 +msgid "" +"Adhocracy is a platform for virtual direct democracies where you can " +"cooperate to create andselect solutions to your organization's " +"challenges." +msgstr "" + +#: adhocracy/templates/index.html:59 +msgid "What groups?" +msgstr "" + +#: adhocracy/templates/index.html:62 +msgid "" +"Think NGOs, open projects. Distributed, loosely-knit groups in search of " +"a common strategy or groups with complex internal policies like " +"Wikipedia." +msgstr "" + +#: adhocracy/templates/index.html:64 adhocracy/templates/index.html:72 +msgid "Read more..." +msgstr "" + +#: adhocracy/templates/index.html:67 +msgid "What decisions?" +msgstr "" + +#: adhocracy/templates/index.html:70 +msgid "" +"Anything that discusses a method of solving a problem. This could be " +"laws, strategy decisions or even design patterns." +msgstr "" + +#: adhocracy/templates/index.html:76 +msgid "Activity in my Adhocracies" +msgstr "" + +#: adhocracy/templates/pager.html:6 +msgid "sort by" +msgstr "" + +#: adhocracy/templates/pager.html:26 +msgid "previous" +msgstr "" + +#: adhocracy/templates/pager.html:44 +msgid "next" +msgstr "" + +#: adhocracy/templates/template.html:4 +msgid "No Title" +msgstr "" + +#: adhocracy/templates/template.html:55 +msgid "sign in" +msgstr "" + +#: adhocracy/templates/template.html:59 +msgid "settings" +msgstr "" + +#: adhocracy/templates/template.html:60 +msgid "logout" +msgstr "" + +#: adhocracy/templates/template.html:64 +msgid "search" +msgstr "" + +#: adhocracy/templates/template.html:66 +msgid "Go" +msgstr "" + +#: adhocracy/templates/template.html:70 +#, python-format +msgid "Join %s to contribute" +msgstr "" + +#: adhocracy/templates/template.html:83 +#: adhocracy/templates/instance/view.html:6 +msgid "Home" +msgstr "" + +#: adhocracy/templates/template.html:90 +msgid "More Adhocracies..." +msgstr "" + +#: adhocracy/templates/template.html:94 +msgid "Critical" +msgstr "" + +#: adhocracy/templates/template.html:96 +msgid "Review" +msgstr "" + +#: adhocracy/templates/template.html:98 +#: adhocracy/templates/user/delegations.html:11 +msgid "Delegations" +msgstr "" + +#: adhocracy/templates/template.html:103 +msgid "Administration" +msgstr "" + +#: adhocracy/templates/template.html:105 +#: adhocracy/templates/category/edit.html:4 +#: adhocracy/templates/issue/edit.html:4 adhocracy/templates/motion/edit.html:4 +#, python-format +msgid "Edit %s" +msgstr "" + +#: adhocracy/templates/template.html:106 +#: adhocracy/templates/admin/members.html:6 +msgid "Members" +msgstr "" + +#: adhocracy/templates/template.html:108 +msgid "Global Permissions" +msgstr "" + +#: adhocracy/templates/template.html:116 adhocracy/templates/user/index.html:3 +#: adhocracy/templates/user/index.html:6 adhocracy/templates/user/index.html:9 +msgid "Users" +msgstr "" + +#: adhocracy/templates/template.html:159 +msgid "adhocracy · liquid democracy platform" +msgstr "" + +#: adhocracy/templates/template.html:160 +msgid "about" +msgstr "" + +#: adhocracy/templates/template.html:161 +msgid "faq" +msgstr "" + +#: adhocracy/templates/template.html:162 +msgid "development" +msgstr "" + +#: adhocracy/templates/template.html:163 +msgid "imprint/contact" +msgstr "" + +#: adhocracy/templates/admin/members.html:3 +#: adhocracy/templates/admin/members.html:9 +#, python-format +msgid "Members: %s" +msgstr "" + +#: adhocracy/templates/admin/members.html:23 +#, python-format +msgid "is a %s" +msgstr "" + +#: adhocracy/templates/admin/members.html:24 +msgid "force to leave" +msgstr "" + +#: adhocracy/templates/admin/members.html:30 +msgid "move to Group:" +msgstr "" + +#: adhocracy/templates/admin/members.html:33 +msgid "Observer" +msgstr "" + +#: adhocracy/templates/admin/members.html:38 +msgid "Voter" +msgstr "" + +#: adhocracy/templates/admin/members.html:43 +msgid "Supervisor" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:3 +msgid "Admin: Group Permissions" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:6 +msgid "Admin » Group Permissions" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:11 +msgid "Group Permissions" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:14 +msgid "" +"WARNING: This allows you to shut yourself out of your adhocracy. Handle " +"with care!" +msgstr "" + +#: adhocracy/templates/category/create.html:4 +#: adhocracy/templates/category/create.html:7 +#: adhocracy/templates/category/create.html:13 +msgid "New category" +msgstr "" + +#: adhocracy/templates/category/create.html:19 +msgid "Category description:" +msgstr "" + +#: adhocracy/templates/category/edit.html:7 +#: adhocracy/templates/issue/edit.html:7 adhocracy/templates/motion/edit.html:7 +#: adhocracy/templates/user/edit.html:7 +msgid "Edit" +msgstr "" + +#: adhocracy/templates/category/edit.html:13 +msgid "Category title" +msgstr "" + +#: adhocracy/templates/category/edit.html:22 +msgid "Category description" +msgstr "" + +#: adhocracy/templates/category/tiles.html:7 +#: adhocracy/templates/category/tiles.html:25 +#: adhocracy/templates/instance/tiles.html:6 +#: adhocracy/templates/instance/tiles.html:32 +#: adhocracy/templates/user/view.html:32 +#, python-format +msgid "%s issue" +msgid_plural "%s issues" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/category/tiles.html:23 +#: adhocracy/templates/category/view.html:39 +#: adhocracy/templates/comment/revision_tiles.html:7 +#: adhocracy/templates/comment/tiles.html:20 +#: adhocracy/templates/instance/tiles.html:31 +#: adhocracy/templates/issue/tiles.html:15 +#: adhocracy/templates/issue/view.html:42 +#: adhocracy/templates/motion/tiles.html:16 +#: adhocracy/templates/motion/tiles.html:35 +#: adhocracy/templates/motion/view.html:52 +#, python-format +msgid "created %s" +msgstr "" + +#: adhocracy/templates/category/tiles.html:24 +#, python-format +msgid "%s category" +msgid_plural "%s categories" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/category/tree.html:45 +msgid "Categorization" +msgstr "" + +#: adhocracy/templates/category/view.html:10 +#: adhocracy/templates/category/view.html:12 +#: adhocracy/templates/comment/tiles.html:101 +#: adhocracy/templates/issue/view.html:12 +#: adhocracy/templates/issue/view.html:14 +#: adhocracy/templates/motion/view.html:12 +#: adhocracy/templates/motion/view.html:14 +#: adhocracy/templates/motion/view.html:16 +msgid "delete" +msgstr "" + +#: adhocracy/templates/category/view.html:16 +#: adhocracy/templates/category/view.html:18 +#: adhocracy/templates/comment/revision_tiles.html:19 +#: adhocracy/templates/comment/tiles.html:93 +#: adhocracy/templates/comment/tiles.html:95 +#: adhocracy/templates/comment/tiles.html:97 +#: adhocracy/templates/instance/view.html:11 +#: adhocracy/templates/issue/view.html:18 +#: adhocracy/templates/issue/view.html:20 +#: adhocracy/templates/motion/view.html:20 +#: adhocracy/templates/motion/view.html:22 +#: adhocracy/templates/motion/view.html:24 +#: adhocracy/templates/user/view.html:11 +msgid "edit" +msgstr "" + +#: adhocracy/templates/category/view.html:40 +#: adhocracy/templates/comment/tiles.html:118 +msgid "history" +msgstr "" + +#: adhocracy/templates/category/view.html:51 +msgid "Subcategories" +msgstr "" + +#: adhocracy/templates/category/view.html:54 +#: adhocracy/templates/instance/view.html:58 +msgid "Create new categories to further structure the debate." +msgstr "" + +#: adhocracy/templates/category/view.html:71 +#: adhocracy/templates/instance/view.html:76 +#, fuzzy +msgid "Issues" +msgstr "" + +#: adhocracy/templates/category/view.html:75 +msgid "Create an issue to discuss a new topic within the current category." +msgstr "" + +#: adhocracy/templates/comment/create.html:3 +#: adhocracy/templates/comment/create.html:6 +msgid "New comment" +msgstr "" + +#: adhocracy/templates/comment/create.html:9 +#: adhocracy/templates/comment/tiles.html:13 +#: adhocracy/templates/comment/view.html:2 +#: adhocracy/templates/comment/view.html:5 +msgid "Comment" +msgstr "" + +#: adhocracy/templates/comment/edit.html:3 +#: adhocracy/templates/comment/edit.html:6 +msgid "Edit comment" +msgstr "" + +#: adhocracy/templates/comment/edit.html:9 +#, python-format +msgid "Comment on: %s" +msgstr "" + +#: adhocracy/templates/comment/history.html:2 +#: adhocracy/templates/comment/history.html:5 +#: adhocracy/templates/comment/history.html:8 +msgid "Comment History" +msgstr "" + +#: adhocracy/templates/comment/revision_tiles.html:13 +msgid "revert here" +msgstr "" + +#: adhocracy/templates/comment/revision_tiles.html:17 +msgid "view comment" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:22 +#: adhocracy/templates/comment/tiles.html:113 +#, python-format +msgid "edited %s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:26 +#: adhocracy/templates/motion/tiles.html:23 +msgid "in" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:57 +#: adhocracy/templates/comment/tiles.html:59 +msgid "Sign in to rate comments" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:61 +#: adhocracy/templates/comment/tiles.html:63 +msgid "This is your own comment" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:78 +msgid "discussion" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:87 +#: adhocracy/templates/comment/tiles.html:89 +msgid "reply" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:105 +#, python-format +msgid "deleted %s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:109 +#, python-format +msgid "%s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:115 +#, python-format +msgid "edited %s by %s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:129 +msgid "This comment has been deleted." +msgstr "" + +#: adhocracy/templates/comment/view.html:8 +#, python-format +msgid "Discussion on %s" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:15 +#, fuzzy +msgid "for" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:17 +msgid "against" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:19 +msgid "abstained" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:21 +msgid "undecided" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:36 +msgid "" +"The user's delegates have voted, but no consensus wasreached among them. " +"The decision is deferred." +msgstr "" + +#: adhocracy/templates/decision/tiles.html:40 +msgid "The decision was made without delegations." +msgstr "" + +#: adhocracy/templates/decision/tiles.html:42 +msgid "The decision was determined as a result of the following delegations:" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:64 +#, python-format +msgid "in a poll %s" +msgstr "" + +#: adhocracy/templates/delegation/create.html:4 +#: adhocracy/templates/delegation/create.html:11 +#, python-format +msgid "Delegate: %s" +msgstr "" + +#: adhocracy/templates/delegation/create.html:7 +#: adhocracy/templates/delegation/review.html:5 +msgid "Delegation" +msgstr "" + +#: adhocracy/templates/delegation/create.html:18 +msgid "" +"This page is obviously a placeholder. Nice, karma-based recs are coming " +"soon." +msgstr "" + +#: adhocracy/templates/delegation/create.html:28 +#, python-format +msgid "Popular delegates for %s" +msgstr "" + +#: adhocracy/templates/delegation/create.html:42 +msgid "Your favourite delegates" +msgstr "" + +#: adhocracy/templates/delegation/create.html:55 +msgid "Delegate to:" +msgstr "" + +#: adhocracy/templates/delegation/review.html:2 +#: adhocracy/templates/delegation/review.html:11 +#, python-format +msgid "Delegation: %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:9 +#: adhocracy/templates/delegation/tiles.html:13 +msgid "revoke" +msgstr "" + +#: adhocracy/templates/delegation/review.html:17 +#: adhocracy/templates/delegation/tiles.html:4 +#, python-format +msgid "from %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:18 +#: adhocracy/templates/delegation/tiles.html:10 +#, python-format +msgid "to %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:22 +#, python-format +msgid "established %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:24 +#, python-format +msgid "revoked %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:30 +msgid "The delegation can be overridden or revoked at any time." +msgstr "" + +#: adhocracy/templates/delegation/review.html:42 +msgid "" +"No decisions have been based on this delegation yet. As soon as this " +"delegation leads to any decisions, they will be listed here." +msgstr "" + +#: adhocracy/templates/delegation/tiles.html:5 +#: adhocracy/templates/delegation/tiles.html:11 +msgid "track record" +msgstr "" + +#: adhocracy/templates/error/http.html:2 adhocracy/templates/error/http.html:4 +#, python-format +msgid "Error %s" +msgstr "" + +#: adhocracy/templates/error/http.html:8 +msgid "" +"If this error continues to occur, please <a " +"href='/page/imprint.html'>notify us</a> with a description of what you " +"were trying to do." +msgstr "" + +#: adhocracy/templates/event/all.html:4 adhocracy/templates/event/all.html:11 +msgid "Whazza" +msgstr "" + +#: adhocracy/templates/event/all.html:7 +msgid "All current events in Adhocracy." +msgstr "" + +#: adhocracy/templates/instance/create.html:3 +#: adhocracy/templates/instance/create.html:6 +#: adhocracy/templates/instance/create.html:12 +msgid "New Adhocracy" +msgstr "" + +#: adhocracy/templates/instance/create.html:18 +msgid "Adhocracy address:" +msgstr "" + +#: adhocracy/templates/instance/create.html:19 +msgid "" +"The address may only contain alpha-numeric characters. Please note that " +"this key cannot be changed after the Adhocracy has been created." +msgstr "" + +#: adhocracy/templates/instance/create.html:22 +#: adhocracy/templates/instance/edit.html:59 +msgid "Description:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:3 +#, python-format +msgid "Manage: %s" +msgstr "" + +#: adhocracy/templates/instance/edit.html:6 +msgid "Manage" +msgstr "" + +#: adhocracy/templates/instance/edit.html:14 +msgid "Adhocracy Name" +msgstr "" + +#: adhocracy/templates/instance/edit.html:19 +#: adhocracy/templates/instance/view.html:34 +msgid "Voting Rules" +msgstr "" + +#: adhocracy/templates/instance/edit.html:20 +msgid "Majority:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:21 +msgid "" +"In order to become active, a motion must reach the given proportion of " +"approval." +msgstr "" + +#: adhocracy/templates/instance/edit.html:23 +msgid "A simple majority (½ of vote)" +msgstr "" + +#: adhocracy/templates/instance/edit.html:24 +msgid "A two-thirds majority" +msgstr "" + +#: adhocracy/templates/instance/edit.html:25 +msgid "In Soviet Russia, motion votes you." +msgstr "" + +#: adhocracy/templates/instance/edit.html:28 +msgid "Delay:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:29 +msgid "" +"Before activating, the defined majority must be continuously held by the " +"moton for the specified interval." +msgstr "" + +#: adhocracy/templates/instance/edit.html:31 +msgid "No delay" +msgstr "" + +#: adhocracy/templates/instance/edit.html:32 +msgid "1 Day" +msgstr "" + +#: adhocracy/templates/instance/edit.html:33 +msgid "2 Days" +msgstr "" + +#: adhocracy/templates/instance/edit.html:34 +msgid "One Week" +msgstr "" + +#: adhocracy/templates/instance/edit.html:35 +msgid "Two Weeks" +msgstr "" + +#: adhocracy/templates/instance/edit.html:36 +msgid "Four Weeks" +msgstr "" + +#: adhocracy/templates/instance/edit.html:39 +msgid "Membership Options" +msgstr "" + +#: adhocracy/templates/instance/edit.html:40 +msgid "Default group:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:41 +msgid "When a new member joins, he or she will be a member of this user group." +msgstr "" + +#: adhocracy/templates/instance/edit.html:51 +msgid "Logo" +msgstr "" + +#: adhocracy/templates/instance/edit.html:52 +msgid "File upload:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:53 +msgid "Select a logo file to appear in the header area of this Adhocracy." +msgstr "" + +#: adhocracy/templates/instance/index.html:16 +msgid "Adhocracies are little democracies that are ran by their community." +msgstr "" + +#: adhocracy/templates/instance/tiles.html:8 +#: adhocracy/templates/instance/tiles.html:34 +#: adhocracy/templates/instance/view.html:14 +#: adhocracy/templates/instance/view.html:17 +msgid "join" +msgstr "" + +#: adhocracy/templates/instance/tiles.html:11 +#: adhocracy/templates/instance/tiles.html:37 +#: adhocracy/templates/instance/view.html:20 +msgid "leave" +msgstr "" + +#: adhocracy/templates/instance/view.html:10 +msgid "members" +msgstr "" + +#: adhocracy/templates/instance/view.html:35 +msgid "Required Majority:" +msgstr "" + +#: adhocracy/templates/instance/view.html:36 +msgid "To become active, a motion must reach the given proportion of approval." +msgstr "" + +#: adhocracy/templates/instance/view.html:38 +msgid "Activation Delay:" +msgstr "" + +#: adhocracy/templates/instance/view.html:39 +msgid "" +"Before becoming active, the majority must be held for the specified " +"interval." +msgstr "" + +#: adhocracy/templates/instance/view.html:45 +msgid "Subscribe to RSS feed:" +msgstr "" + +#: adhocracy/templates/instance/view.html:55 +#, fuzzy +msgid "Categories" +msgstr "" + +#: adhocracy/templates/instance/view.html:80 +msgid "Create an issue to start debating in this Adhocracy." +msgstr "" + +#: adhocracy/templates/issue/create.html:4 +#: adhocracy/templates/issue/create.html:7 +#: adhocracy/templates/issue/create.html:18 +msgid "New issue" +msgstr "" + +#: adhocracy/templates/issue/create.html:27 +msgid "Issue description:" +msgstr "" + +#: adhocracy/templates/issue/edit.html:13 +msgid "Issue Title" +msgstr "" + +#: adhocracy/templates/issue/tiles.html:16 +#: adhocracy/templates/motion/tiles.html:20 +#: adhocracy/templates/motion/tiles.html:39 +#: adhocracy/templates/user/view.html:34 +#, python-format +msgid "%s comment" +msgid_plural "%s comments" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/issue/tiles.html:17 +#: adhocracy/templates/user/view.html:33 +#, python-format +msgid "%s motion" +msgid_plural "%s motions" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/issue/view.html:35 +#, python-format +msgid "in %s" +msgstr "" + +#: adhocracy/templates/issue/view.html:57 +#: adhocracy/templates/motion/index.html:6 +msgid "Motions" +msgstr "" + +#: adhocracy/templates/issue/view.html:59 +msgid "" +"Motions are <b>proposals</b> that solve some or all of the problems " +"described by this issue. A motion can be <b>discussed and voted</b> upon." +msgstr "" + +#: adhocracy/templates/issue/view.html:66 +#: adhocracy/templates/motion/view.html:209 +msgid "Discussion" +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:6 +msgid "Call a vote" +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:9 +#, python-format +msgid "Call a vote on: %s" +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:17 +msgid "" +"You are about to release this motion and call for a vote. When you do " +"this, you will lose the ability to <strong>change the motion's " +"wording</strong> unless you re-draft it." +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:21 +#: adhocracy/templates/motion/end_poll.html:21 +msgid "Confirm and Proceed" +msgstr "" + +#: adhocracy/templates/motion/create.html:4 +#: adhocracy/templates/motion/create.html:7 +#: adhocracy/templates/motion/create.html:14 +msgid "New motion" +msgstr "" + +#: adhocracy/templates/motion/create.html:20 +msgid "Informal motion description:" +msgstr "" + +#: adhocracy/templates/motion/edit.html:13 +msgid "New Motion" +msgstr "" + +#: adhocracy/templates/motion/end_poll.html:6 +msgid "End a vote" +msgstr "" + +#: adhocracy/templates/motion/end_poll.html:9 +#, python-format +msgid "Cancel vote on: %s" +msgstr "" + +#: adhocracy/templates/motion/end_poll.html:17 +msgid "" +"You are about to re-draft this motion. This means that the motion will " +"become editable again but that all votes that have been cast at this time" +" <strong>will be invalidated</strong>." +msgstr "" + +#: adhocracy/templates/motion/index.html:3 +#, python-format +msgid "Critical Motions in %s" +msgstr "" + +#: adhocracy/templates/motion/index.html:9 +msgid "Critical Motions" +msgstr "" + +#: adhocracy/templates/motion/index.html:14 +msgid "" +"These motions are currently voting and might be nearing a decision. Vote " +"now to make your voice heard." +msgstr "" + +#: adhocracy/templates/motion/index.html:27 +msgid "" +"There are currently no polls open. Call a motion into voting state to " +"begin a poll." +msgstr "" + +#: adhocracy/templates/motion/tiles.html:18 +#: adhocracy/templates/motion/tiles.html:37 +#, python-format +msgid "%s votes cast" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:46 +msgid "Draft" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:48 +msgid "Voting" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:50 +msgid "Activating" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:52 +msgid "Deactivating" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:54 +msgid "Active" +msgstr "" + +#: adhocracy/templates/motion/view.html:32 +#: adhocracy/templates/motion/view.html:34 +#: adhocracy/templates/motion/view.html:36 +msgid "call a vote" +msgstr "" + +#: adhocracy/templates/motion/view.html:34 +msgid "The motion cannot be voted upon since it does not have any provisions yet." +msgstr "" + +#: adhocracy/templates/motion/view.html:66 +msgid "This vote has recieved the required majority and cannot be cancelled" +msgstr "" + +#: adhocracy/templates/motion/view.html:70 +msgid "Vote" +msgstr "" + +#: adhocracy/templates/motion/view.html:83 +#, fuzzy +msgid "Option" +msgstr "" + +#: adhocracy/templates/motion/view.html:86 +msgid "Delegate Recommendations" +msgstr "" + +#: adhocracy/templates/motion/view.html:89 +#: adhocracy/templates/motion/votes.html:8 +#: adhocracy/templates/user/votes.html:8 +msgid "Votes" +msgstr "" + +#: adhocracy/templates/motion/view.html:90 +msgid "Percent" +msgstr "" + +#: adhocracy/templates/motion/view.html:102 +msgid "Affirm" +msgstr "" + +#: adhocracy/templates/motion/view.html:123 +msgid "Dissent" +msgstr "" + +#: adhocracy/templates/motion/view.html:144 +msgid "Abstain" +msgstr "" + +#: adhocracy/templates/motion/view.html:158 +msgid "vote" +msgstr "" + +#: adhocracy/templates/motion/view.html:163 +#, python-format +msgid "Of the required %s votes, the motion has:" +msgstr "" + +#: adhocracy/templates/motion/view.html:168 +#, python-format +msgid "%d votes" +msgstr "" + +#: adhocracy/templates/motion/view.html:181 +#, python-format +msgid "poll started %s" +msgstr "" + +#: adhocracy/templates/motion/view.html:182 +msgid "help" +msgstr "" + +#: adhocracy/templates/motion/view.html:196 +msgid "Provisions" +msgstr "" + +#: adhocracy/templates/motion/view.html:198 +msgid "" +"<b>Provisions</b> are the body of a motion: together they form the " +"language that will be voted upon. You will need to have at least one " +"clause in order to call for a vote." +msgstr "" + +#: adhocracy/templates/motion/votes.html:5 +#: adhocracy/templates/user/votes.html:5 +#, python-format +msgid "Votes: %s" +msgstr "" + +#: adhocracy/templates/motion/votes.html:12 +#: adhocracy/templates/user/votes.html:14 +msgid "Votes:" +msgstr "" + +#: adhocracy/templates/search/results.html:2 +#: adhocracy/templates/search/results.html:5 +#: adhocracy/templates/search/results.html:12 +msgid "Search" +msgstr "" + +#: adhocracy/templates/search/results.html:10 +#, python-format +msgid "Search for '%s'" +msgstr "" + +#: adhocracy/templates/search/results.html:28 +msgid "" +"No entries could be found that match your criteria. Try a more general " +"search term." +msgstr "" + +#: adhocracy/templates/user/delegations.html:4 +#: adhocracy/templates/user/delegations.html:17 +msgid "My Delegations" +msgstr "" + +#: adhocracy/templates/user/delegations.html:6 +#: adhocracy/templates/user/delegations.html:19 +#, python-format +msgid "Delegations: %s" +msgstr "" + +#: adhocracy/templates/user/delegations.html:25 +msgid "Topic" +msgstr "" + +#: adhocracy/templates/user/delegations.html:26 +msgid "Given" +msgstr "" + +#: adhocracy/templates/user/delegations.html:27 +msgid "Received" +msgstr "" + +#: adhocracy/templates/user/edit.html:4 +#, python-format +msgid "Settings: %s" +msgstr "" + +#: adhocracy/templates/user/edit.html:20 +msgid "User Details" +msgstr "" + +#: adhocracy/templates/user/edit.html:22 +#: adhocracy/templates/user/register_form.html:9 +#: adhocracy/templates/user/reset_form.html:14 +msgid "E-Mail:" +msgstr "" + +#: adhocracy/templates/user/edit.html:24 +msgid "Language:" +msgstr "" + +#: adhocracy/templates/user/edit.html:36 +#: adhocracy/templates/user/login_form.html:5 +#: adhocracy/templates/user/register_form.html:13 +msgid "Password:" +msgstr "" + +#: adhocracy/templates/user/edit.html:38 +msgid "Select a new password or leave the fields blank to keep your old one." +msgstr "" + +#: adhocracy/templates/user/edit.html:42 +#: adhocracy/templates/user/register_form.html:16 +msgid "Password (confirm):" +msgstr "" + +#: adhocracy/templates/user/edit.html:46 +msgid "Configure your user icon at <a href='http://www.gravatar.com'>Gravatar</a>" +msgstr "" + +#: adhocracy/templates/user/edit.html:51 +msgid "Short biography" +msgstr "" + +#: adhocracy/templates/user/edit.html:54 +msgid "" +"A bio will allow others to learn about you and perhaps even get you a few" +" delegations." +msgstr "" + +#: adhocracy/templates/user/index.html:3 adhocracy/templates/user/index.html:9 +#, python-format +msgid "Users in %s" +msgstr "" + +#: adhocracy/templates/user/login.html:2 +msgid "Who are you, then?" +msgstr "" + +#: adhocracy/templates/user/login.html:5 +#: adhocracy/templates/user/login_form.html:8 +msgid "Login" +msgstr "" + +#: adhocracy/templates/user/login.html:6 +msgid "If you already have an account, sign in here." +msgstr "" + +#: adhocracy/templates/user/login.html:10 +msgid "" +"If you have an account but you've lost your password, <a " +"href='/user/reset'>click here</a>." +msgstr "" + +#: adhocracy/templates/user/login.html:15 +#: adhocracy/templates/user/register_form.html:19 +msgid "Register" +msgstr "" + +#: adhocracy/templates/user/login.html:16 +msgid "" +"Creating an account is easy; all you need is a user name, password and " +"email address." +msgstr "" + +#: adhocracy/templates/user/login_form.html:2 +msgid "Login:" +msgstr "" + +#: adhocracy/templates/user/register_form.html:5 +msgid "User name:" +msgstr "" + +#: adhocracy/templates/user/register_form.html:6 +msgid "Can only contain letters and numbers." +msgstr "" + +#: adhocracy/templates/user/register_form.html:10 +msgid "We don't spam." +msgstr "" + +#: adhocracy/templates/user/reset_form.html:2 +#: adhocracy/templates/user/reset_form.html:4 +#: adhocracy/templates/user/reset_pending.html:2 +#: adhocracy/templates/user/reset_pending.html:4 +msgid "Password reset" +msgstr "" + +#: adhocracy/templates/user/reset_form.html:8 +msgid "" +"You will be sent an activation link that, when opened, will cause " +"Adhocracy to email you a new password." +msgstr "" + +#: adhocracy/templates/user/reset_form.html:12 +msgid "In order to retrieve your login, you will have to enter your email adress." +msgstr "" + +#: adhocracy/templates/user/reset_form.html:18 +msgid "Reset" +msgstr "" + +#: adhocracy/templates/user/reset_pending.html:4 +msgid "Confirmation pending" +msgstr "" + +#: adhocracy/templates/user/reset_pending.html:11 +msgid "" +"You have recieved an email containing a link. Please open that link in " +"order to reset you password" +msgstr "" + +#: adhocracy/templates/user/tiles.html:16 +#, python-format +msgid "%s karma" +msgstr "" + +#: adhocracy/templates/user/tiles.html:18 adhocracy/templates/user/view.html:38 +#, python-format +msgid "signed up %s" +msgstr "" + +#: adhocracy/templates/user/view.html:24 +#, python-format +msgid "%s does not have a bio" +msgstr "" + +#: adhocracy/templates/user/view.html:36 +msgid "delegations" +msgstr "" + +#: adhocracy/templates/user/view.html:46 +msgid "is a member in the following adhocracies:" +msgstr "" + +#: adhocracy/templates/user/view.html:56 +msgid "Activity" +msgstr "" + +#: adhocracy/templates/user/votes.html:12 +msgid "Review your voting track" +msgstr "" + diff --git a/adhocracy/i18n/fr/LC_MESSAGES/adhocracy.mo b/adhocracy/i18n/fr/LC_MESSAGES/adhocracy.mo new file mode 100644 index 000000000..62d8aa234 Binary files /dev/null and b/adhocracy/i18n/fr/LC_MESSAGES/adhocracy.mo differ diff --git a/adhocracy/i18n/fr/LC_MESSAGES/adhocracy.po b/adhocracy/i18n/fr/LC_MESSAGES/adhocracy.po new file mode 100644 index 000000000..14c44b737 --- /dev/null +++ b/adhocracy/i18n/fr/LC_MESSAGES/adhocracy.po @@ -0,0 +1,2081 @@ +# French translations for adhocracy. +# Copyright (C) 2009 ORGANIZATION +# This file is distributed under the same license as the adhocracy project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: adhocracy 0.1\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2009-10-29 13:31+0100\n" +"PO-Revision-Date: 2009-09-15 16:36+0200\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: fr <LL@li.org>\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.0dev-r0\n" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:61 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:62 +#: adhocracy/contrib/babel/babel/tests/support.py:62 +#: adhocracy/contrib/babel/babel/tests/support.py:67 +#: adhocracy/contrib/babel/babel/tests/support.py:114 +msgid "foo" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:63 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:64 +msgid "There is" +msgid_plural "There are" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:65 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:66 +msgid "Fizz" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:67 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:68 +msgid "Fuzz" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:69 +#: adhocracy/contrib/babel/babel/messages/tests/mofile.py:70 +msgid "Fuzzes" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/file1.py:8 +msgid "bar" +msgstr "" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/file2.py:9 +msgid "foobar" +msgid_plural "foobars" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/contrib/babel/babel/messages/tests/data/project/CVS/this_wont_normally_be_here.py:11 +msgid "FooBar" +msgid_plural "FooBars" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/contrib/babel/babel/tests/support.py:78 +#: adhocracy/contrib/babel/babel/tests/support.py:80 +#: adhocracy/contrib/babel/babel/tests/support.py:90 +#: adhocracy/contrib/babel/babel/tests/support.py:92 +#: adhocracy/contrib/babel/babel/tests/support.py:132 +#: adhocracy/contrib/babel/babel/tests/support.py:134 +msgid "foo1" +msgid_plural "foos1" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/controllers/admin.py:69 +#, python-format +msgid "%(user)s is not a member of %(instance)s" +msgstr "" + +#: adhocracy/controllers/admin.py:93 +#, python-format +msgid "%(user)s was removed from %(instance)s" +msgstr "" + +#: adhocracy/controllers/admin.py:97 +#, python-format +msgid "%(user)s isn't a member of %(instance)s" +msgstr "" + +#: adhocracy/controllers/category.py:42 adhocracy/controllers/category.py:66 +#, python-format +msgid "No category with ID '%s' exists." +msgstr "" + +#: adhocracy/controllers/category.py:86 adhocracy/controllers/category.py:90 +#: adhocracy/templates/category/view.html:25 +#, fuzzy, python-format +msgid "Category: %s" +msgstr "" + +#: adhocracy/controllers/category.py:94 adhocracy/controllers/category.py:101 +#: adhocracy/controllers/comment.py:143 adhocracy/controllers/delegation.py:77 +#: adhocracy/controllers/instance.py:52 adhocracy/controllers/instance.py:98 +#: adhocracy/controllers/instance.py:105 adhocracy/controllers/issue.py:99 +#: adhocracy/controllers/motion.py:36 adhocracy/controllers/motion.py:190 +#: adhocracy/controllers/user.py:51 adhocracy/controllers/user.py:253 +msgid "oldest" +msgstr "" + +#: adhocracy/controllers/category.py:95 adhocracy/controllers/category.py:102 +#: adhocracy/controllers/comment.py:144 adhocracy/controllers/delegation.py:78 +#: adhocracy/controllers/instance.py:53 adhocracy/controllers/instance.py:99 +#: adhocracy/controllers/instance.py:106 adhocracy/controllers/issue.py:100 +#: adhocracy/controllers/motion.py:37 adhocracy/controllers/motion.py:191 +#: adhocracy/controllers/user.py:52 adhocracy/controllers/user.py:254 +msgid "newest" +msgstr "" + +#: adhocracy/controllers/category.py:96 adhocracy/controllers/category.py:103 +#: adhocracy/controllers/instance.py:54 adhocracy/controllers/instance.py:100 +#: adhocracy/controllers/instance.py:107 adhocracy/controllers/issue.py:101 +#: adhocracy/controllers/motion.py:38 adhocracy/controllers/user.py:54 +msgid "activity" +msgstr "" + +#: adhocracy/controllers/category.py:97 adhocracy/controllers/category.py:104 +#: adhocracy/controllers/instance.py:55 adhocracy/controllers/instance.py:101 +#: adhocracy/controllers/instance.py:108 adhocracy/controllers/issue.py:102 +#: adhocracy/controllers/motion.py:40 adhocracy/controllers/user.py:55 +msgid "name" +msgstr "" + +#: adhocracy/controllers/category.py:114 +msgid "Deleting the root category isn't possible." +msgstr "" + +#: adhocracy/controllers/category.py:117 +#, python-format +msgid "No category with ID '%(id)s' exists." +msgstr "" + +#: adhocracy/controllers/category.py:130 +#, python-format +msgid "Category '%(category)s' has been deleted." +msgstr "" + +#: adhocracy/controllers/comment.py:43 +msgid "Unsupported topic type." +msgstr "" + +#: adhocracy/controllers/comment.py:84 adhocracy/controllers/comment.py:103 +#: adhocracy/controllers/comment.py:112 adhocracy/controllers/comment.py:122 +#: adhocracy/controllers/comment.py:139 adhocracy/controllers/comment.py:155 +#, python-format +msgid "No comment with ID %s exists" +msgstr "" + +#: adhocracy/controllers/comment.py:159 +msgid "" +"You're trying to revert to a revision which is not part of this comments " +"history" +msgstr "" + +#: adhocracy/controllers/delegation.py:21 +#, python-format +msgid "No motion or category with ID '%(id)s' exists" +msgstr "" + +#: adhocracy/controllers/delegation.py:52 +#: adhocracy/controllers/delegation.py:70 +#, python-format +msgid "Couldn't find delegation %(id)s" +msgstr "" + +#: adhocracy/controllers/delegation.py:54 +#, python-format +msgid "Cannot access delegation %(id)s" +msgstr "" + +#: adhocracy/controllers/delegation.py:63 +msgid "The delegation is now revoked." +msgstr "" + +#: adhocracy/controllers/instance.py:42 +#, python-format +msgid "No such adhocracy exists: %(key)s" +msgstr "" + +#: adhocracy/controllers/instance.py:47 +msgid "" +"An index of adhocracies run at adhocracy.cc. Select which ones you would " +"like to join and participate in!" +msgstr "" + +#: adhocracy/controllers/instance.py:89 +#, python-format +msgid "%s News" +msgstr "" + +#: adhocracy/controllers/instance.py:91 +#, python-format +msgid "News from the %s Adhocracy" +msgstr "" + +#: adhocracy/controllers/instance.py:171 +msgid "Deleting an instance is not currently implemented" +msgstr "" + +#: adhocracy/controllers/instance.py:178 +#, python-format +msgid "You're already a member in %(instance)s." +msgstr "" + +#: adhocracy/controllers/instance.py:192 +#, python-format +msgid "Welcome to %(instance)s" +msgstr "" + +#: adhocracy/controllers/instance.py:201 +#, python-format +msgid "You're not a member of %(instance)s." +msgstr "" + +#: adhocracy/controllers/instance.py:204 +#, python-format +msgid "You're the founder of %s, cannot leave." +msgstr "" + +#: adhocracy/controllers/issue.py:60 adhocracy/controllers/issue.py:80 +#: adhocracy/controllers/issue.py:115 +#, python-format +msgid "No issue with ID %s exists." +msgstr "" + +#: adhocracy/controllers/issue.py:90 adhocracy/templates/issue/view.html:27 +#, python-format +msgid "Issue: %s" +msgstr "" + +#: adhocracy/controllers/issue.py:92 +#, python-format +msgid "Activity on the %s issue" +msgstr "" + +#: adhocracy/controllers/issue.py:94 +#, python-format +msgid "Issue: %(issue)s" +msgstr "" + +#: adhocracy/controllers/issue.py:124 +#, python-format +msgid "" +"The issue %(issue)s cannot be deleted, because the contained motion " +"%(motion)s is polling." +msgstr "" + +#: adhocracy/controllers/issue.py:131 +#, python-format +msgid "Issue '%(issue)s' has been deleted." +msgstr "" + +#: adhocracy/controllers/karma.py:28 +msgid "Invalid karma value. Karma is either positive or negative!" +msgstr "" + +#: adhocracy/controllers/motion.py:39 +msgid "urgency" +msgstr "" + +#: adhocracy/controllers/motion.py:106 +#, fuzzy, python-format +msgid "Motion: %s" +msgstr "" + +#: adhocracy/controllers/motion.py:108 +#, python-format +msgid "Activity on the %s motion" +msgstr "" + +#: adhocracy/controllers/motion.py:110 +#, python-format +msgid "Motion: %(motion)s" +msgstr "" + +#: adhocracy/controllers/motion.py:146 +msgid "" +"The poll cannot be started either because there are no provisions or a " +"poll has already started." +msgstr "" + +#: adhocracy/controllers/motion.py:170 +msgid "The motion is not undergoing a poll." +msgstr "" + +#: adhocracy/controllers/motion.py:194 +#, python-format +msgid "%s is not currently in a poll, thus no votes have been counted." +msgstr "" + +#: adhocracy/controllers/page.py:19 adhocracy/controllers/page.py:27 +msgid "The requested page was not found" +msgstr "" + +#: adhocracy/controllers/root.py:21 adhocracy/lib/base.py:72 +#: adhocracy/templates/index.html:22 +msgid "My Adhocracies" +msgstr "" + +#: adhocracy/controllers/root.py:23 +msgid "Updates from the Adhocracies in which you are a member" +msgstr "" + +#: adhocracy/controllers/root.py:36 +#, python-format +msgid "No motion or category with ID %(id)s exists" +msgstr "" + +#: adhocracy/controllers/search.py:25 +msgid "Received no query for search." +msgstr "" + +#: adhocracy/controllers/user.py:53 +msgid "karma" +msgstr "" + +#: adhocracy/controllers/user.py:93 adhocracy/controllers/user.py:141 +#: adhocracy/controllers/user.py:165 adhocracy/controllers/user.py:248 +#: adhocracy/controllers/user.py:263 +#, python-format +msgid "No user named '%s' exists" +msgstr "" + +#: adhocracy/controllers/user.py:95 +#, python-format +msgid "You're not authorized to change %s's settings." +msgstr "" + +#: adhocracy/controllers/user.py:124 +msgid "There is no user registered with that email address." +msgstr "" + +#: adhocracy/controllers/user.py:131 +msgid "" +"you have requested that your password for Adhocracy be reset. In order to" +" confirm the validity of your claim, please open the link below in your " +"browser:" +msgstr "" + +#: adhocracy/controllers/user.py:134 +msgid "Reset your password" +msgstr "" + +#: adhocracy/controllers/user.py:152 +msgid "your password has been reset. It is now:" +msgstr "" + +#: adhocracy/controllers/user.py:153 +msgid "Please login and change the password in your user settings." +msgstr "" + +#: adhocracy/controllers/user.py:154 +msgid "Your new password" +msgstr "" + +#: adhocracy/controllers/user.py:155 +msgid "Success. You have been sent an email with your new password." +msgstr "" + +#: adhocracy/controllers/user.py:157 +msgid "The reset code is invalid. Please repeat the password recovery procedure." +msgstr "" + +#: adhocracy/controllers/user.py:169 +#, python-format +msgid "%(user)s is using Adhocracy, a direct democracy decision-making tool." +msgstr "" + +#: adhocracy/controllers/user.py:178 +#, python-format +msgid "%(user)ss Activity" +msgstr "" + +#: adhocracy/controllers/user.py:182 +#, python-format +msgid "%s is not a member of %s" +msgstr "" + +#: adhocracy/controllers/user.py:218 +msgid "Invalid user name or password" +msgstr "" + +#: adhocracy/controllers/vote.py:16 adhocracy/lib/base.py:43 +#, python-format +msgid "No motion with ID %(id)s exists." +msgstr "" + +#: adhocracy/controllers/vote.py:18 +msgid "You have no voting rights." +msgstr "" + +#: adhocracy/controllers/vote.py:27 +msgid "This motion is not currently being voted on." +msgstr "" + +#: adhocracy/controllers/vote.py:31 +msgid "Illegal input for vote cast." +msgstr "" + +#: adhocracy/lib/base.py:77 +msgid "" +"A liquid democracy platform for making decisions in distributed, open " +"groups by cooperatively creating proposals and voting on them to " +"establish their support." +msgstr "" + +#: adhocracy/lib/base.py:80 +msgid "" +"adhocracy, direct democracy, liquid democracy, liqd, democracy, wiki, " +"voting,participation, group decisions, decisions, decision-making" +msgstr "" + +#: adhocracy/lib/helpers.py:33 adhocracy/templates/template.html:36 +#: adhocracy/templates/template.html:77 adhocracy/templates/user/parts.html:5 +msgid "Adhocracy" +msgstr "" + +#: adhocracy/lib/helpers.py:51 +msgid "This motion is currently being voted on and cannot be modified." +msgstr "" + +#: adhocracy/lib/helpers.py:103 +msgid "You" +msgstr "" + +#: adhocracy/lib/mail.py:17 +#, python-format +msgid "Hi %s," +msgstr "" + +#: adhocracy/lib/mail.py:19 +msgid "" +"Cheers,\r\n" +"\r\n" +" the Adhocracy Team\r\n" +msgstr "" + +#: adhocracy/lib/xsrf.py:49 +msgid "" +"Action failed. You were probably trying to re-perform an action after " +"using your browser's 'Back' button. This is prohibited for security " +"reasons." +msgstr "" + +#: adhocracy/lib/event/event.py:64 +msgid "(Undefined)" +msgstr "" + +#: adhocracy/lib/event/formatting.py:79 +msgid "voted for" +msgstr "" + +#: adhocracy/lib/event/formatting.py:80 +msgid "abstained on" +msgstr "" + +#: adhocracy/lib/event/formatting.py:81 +msgid "voted against" +msgstr "" + +#: adhocracy/lib/event/formatting.py:89 +msgid "comment" +msgstr "" + +#: adhocracy/lib/event/types.py:44 +msgid "signed up" +msgstr "" + +#: adhocracy/lib/event/types.py:45 +msgid "edited their profile" +msgstr "" + +#: adhocracy/lib/event/types.py:46 +#, python-format +msgid "edited %(user)ss profile" +msgstr "" + +#: adhocracy/lib/event/types.py:47 +#, python-format +msgid "founded the %(instance)s Adhocracy" +msgstr "" + +#: adhocracy/lib/event/types.py:48 +#, python-format +msgid "updated the %(instance)s Adhocracy" +msgstr "" + +#: adhocracy/lib/event/types.py:49 +#, python-format +msgid "deleted the %(instance)s Adhocracy" +msgstr "" + +#: adhocracy/lib/event/types.py:50 +#, python-format +msgid "joined %(instance)s" +msgstr "" + +#: adhocracy/lib/event/types.py:51 +#, python-format +msgid "left %(instance)s" +msgstr "" + +#: adhocracy/lib/event/types.py:52 +#, python-format +msgid "was forced to leave %(instance)s by %(user)s" +msgstr "" + +#: adhocracy/lib/event/types.py:53 +#, python-format +msgid "now is a %(group)s within %(instance)s" +msgstr "" + +#: adhocracy/lib/event/types.py:54 +#, python-format +msgid "created %(issue)s" +msgstr "" + +#: adhocracy/lib/event/types.py:55 +#, python-format +msgid "edited %(issue)s" +msgstr "" + +#: adhocracy/lib/event/types.py:56 +#, python-format +msgid "deleted %(issue)s" +msgstr "" + +#: adhocracy/lib/event/types.py:57 +#, python-format +msgid "created %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:58 +#, python-format +msgid "edited %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:59 +#, python-format +msgid "re-drafted %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:60 +#, python-format +msgid "called a vote on %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:61 +#, python-format +msgid "deleted %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:62 +#, python-format +msgid "named %(user)s as an editor for %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:63 +#, python-format +msgid "removed %(user)s from the editors of %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:64 +#, python-format +msgid "created the category %(category)s in %(parent)s" +msgstr "" + +#: adhocracy/lib/event/types.py:65 +#, python-format +msgid "updated the category %(category)s" +msgstr "" + +#: adhocracy/lib/event/types.py:66 +#, python-format +msgid "deleted the category %(category)s" +msgstr "" + +#: adhocracy/lib/event/types.py:67 +#, python-format +msgid "created a %(comment)s on %(delegateable)s" +msgstr "" + +#: adhocracy/lib/event/types.py:68 +#, python-format +msgid "edited a %(comment)s on %(delegateable)s" +msgstr "" + +#: adhocracy/lib/event/types.py:69 +#, python-format +msgid "deleted a %(comment)s from %(delegateable)s" +msgstr "" + +#: adhocracy/lib/event/types.py:70 +#, python-format +msgid "delegated voting on %(scope)s to %(agent)s" +msgstr "" + +#: adhocracy/lib/event/types.py:71 +#, python-format +msgid "revoked their delegation on %(scope)s to %(agent)s" +msgstr "" + +#: adhocracy/lib/event/types.py:72 +#, fuzzy, python-format +msgid "%(vote)s %(motion)s" +msgstr "" + +#: adhocracy/lib/event/types.py:73 +#, python-format +msgid "test %(test)s" +msgstr "" + +#: adhocracy/lib/instance/__init__.py:13 +msgid "This action is only available in an instance context." +msgstr "" + +#: adhocracy/lib/karma/threshold.py:18 +#, fuzzy +msgid "create a category" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:19 +#, fuzzy +msgid "edit this category" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:20 +#, fuzzy +msgid "delete this category" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:21 +msgid "reply in a comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:22 +msgid "edit this comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:23 +msgid "delete this comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:24 +msgid "rate this comment" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:25 +msgid "create a motion" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:26 +#, fuzzy +msgid "edit this motion" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:27 +msgid "call for a vote" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:28 +msgid "cancel a vote" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:29 +msgid "delete a motion" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:30 +msgid "create an issue" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:31 +#, fuzzy +msgid "edit this issue" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:32 +msgid "delete this issue" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:33 +#, python-format +msgid "You need %s karma to %s" +msgstr "" + +#: adhocracy/lib/karma/threshold.py:34 +msgid "do this" +msgstr "" + +#: adhocracy/lib/text/i18n.py:45 +msgid "Today" +msgstr "" + +#: adhocracy/lib/text/i18n.py:47 +msgid "Yesterday" +msgstr "" + +#: adhocracy/lib/text/i18n.py:61 +#, python-format +msgid "%(ts)s ago" +msgstr "" + +#: adhocracy/model/forms.py:27 +msgid "No username is given" +msgstr "" + +#: adhocracy/model/forms.py:31 +msgid "The username is invalid" +msgstr "" + +#: adhocracy/model/forms.py:35 +msgid "That username already exists" +msgstr "" + +#: adhocracy/model/forms.py:44 +msgid "That email is already registered" +msgstr "" + +#: adhocracy/model/forms.py:52 +msgid "No instance key is given" +msgstr "" + +#: adhocracy/model/forms.py:56 +msgid "The instance key is invalid" +msgstr "" + +#: adhocracy/model/forms.py:60 +msgid "An instance with that key already exists" +msgstr "" + +#: adhocracy/model/forms.py:69 +#, python-format +msgid "No entity with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:105 +#, python-format +msgid "No group with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:114 +#, python-format +msgid "No revision with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:123 +#, python-format +msgid "No comment with ID '%s' exists" +msgstr "" + +#: adhocracy/model/forms.py:131 +#, python-format +msgid "'%s' is not a valid motion state." +msgstr "" + +#: adhocracy/model/forms.py:140 +#, python-format +msgid "No user with the user name '%s' exists" +msgstr "" + +#: adhocracy/model/motion.py:38 +msgid "Motion doesn't have a distinct parent issue." +msgstr "" + +#: adhocracy/templates/components.html:4 +msgid "formatting hints" +msgstr "" + +#: adhocracy/templates/components.html:11 +msgid "Save" +msgstr "" + +#: adhocracy/templates/components.html:13 +msgid "or" +msgstr "" + +#: adhocracy/templates/components.html:13 +#: adhocracy/templates/motion/view.html:64 +#: adhocracy/templates/motion/view.html:66 +#: adhocracy/templates/motion/view.html:68 +msgid "cancel" +msgstr "" + +#: adhocracy/templates/components.html:21 adhocracy/templates/index.html:10 +msgid "Say hi" +msgstr "" + +#: adhocracy/templates/components.html:24 +msgid "If you're not registered yet, <a href='/register'>sign up here</a>." +msgstr "" + +#: adhocracy/templates/components.html:33 +msgid "Delegate voting" +msgstr "" + +#: adhocracy/templates/components.html:38 +msgid "You have voted yourself and not delegated voting." +msgstr "" + +#: adhocracy/templates/components.html:39 +msgid "Info..." +msgstr "" + +#: adhocracy/templates/components.html:41 +msgid "You have not delegated voting." +msgstr "" + +#: adhocracy/templates/components.html:42 +#: adhocracy/templates/category/view.html:22 +#: adhocracy/templates/instance/view.html:23 +#: adhocracy/templates/issue/view.html:24 +#: adhocracy/templates/motion/view.html:28 +msgid "delegate" +msgstr "" + +#: adhocracy/templates/components.html:43 +msgid "info..." +msgstr "" + +#: adhocracy/templates/components.html:47 +msgid "By voting yourself, you have overridden:" +msgstr "" + +#: adhocracy/templates/components.html:49 +msgid "You have delegated voting to:" +msgstr "" + +#: adhocracy/templates/components.html:56 +#: adhocracy/templates/decision/tiles.html:50 +msgid "on" +msgstr "" + +#: adhocracy/templates/components.html:57 +#: adhocracy/templates/decision/tiles.html:51 +msgid "review" +msgstr "" + +#: adhocracy/templates/components.html:66 +msgid "You hold an additional vote." +msgstr "" + +#: adhocracy/templates/components.html:68 +#, python-format +msgid "You hold %s additional votes." +msgstr "" + +#: adhocracy/templates/components.html:81 +msgid "What now?" +msgstr "" + +#: adhocracy/templates/components.html:81 +msgid "— using Adhocracy in 3<sup>½</sup> steps:" +msgstr "" + +#: adhocracy/templates/components.html:87 +msgid "Create and discuss issues that need solutions." +msgstr "" + +#: adhocracy/templates/components.html:92 +msgid "Cooperate to develop proposals affecting the issues." +msgstr "" + +#: adhocracy/templates/components.html:96 +msgid "Vote on proposals to collectively make decisions.*" +msgstr "" + +#: adhocracy/templates/components.html:101 +msgid "*Or — if you like — delegate voting in some fields to a peer." +msgstr "" + +#: adhocracy/templates/index.html:3 +msgid "Welcome" +msgstr "" + +#: adhocracy/templates/index.html:5 +msgid "Welcome to Adhocracy" +msgstr "" + +#: adhocracy/templates/index.html:9 +msgid "sign up" +msgstr "" + +#: adhocracy/templates/index.html:15 +msgid "If you're not registered, <a href='/register'>sign up.</a>" +msgstr "" + +#: adhocracy/templates/index.html:20 adhocracy/templates/index.html:36 +#: adhocracy/templates/user/view.html:43 +msgid "more" +msgstr "" + +#: adhocracy/templates/index.html:21 adhocracy/templates/index.html:37 +#: adhocracy/templates/category/view.html:47 +#: adhocracy/templates/category/view.html:49 +#: adhocracy/templates/category/view.html:67 +#: adhocracy/templates/category/view.html:69 +#: adhocracy/templates/instance/index.html:10 +#: adhocracy/templates/instance/view.html:51 +#: adhocracy/templates/instance/view.html:53 +#: adhocracy/templates/instance/view.html:72 +#: adhocracy/templates/instance/view.html:74 +#: adhocracy/templates/issue/view.html:53 +#: adhocracy/templates/issue/view.html:55 +#: adhocracy/templates/motion/view.html:189 +#: adhocracy/templates/motion/view.html:191 +#: adhocracy/templates/motion/view.html:193 +msgid "new" +msgstr "" + +#: adhocracy/templates/index.html:31 +msgid "Join a few Adhocracies to contribute to their policies." +msgstr "" + +#: adhocracy/templates/index.html:38 adhocracy/templates/template.html:114 +#: adhocracy/templates/instance/index.html:3 +#: adhocracy/templates/instance/index.html:12 +#: adhocracy/templates/user/view.html:44 +msgid "Adhocracies" +msgstr "" + +#: adhocracy/templates/index.html:52 +msgid "Adhocracy helps <strong>groups</strong> make <strong>decisions</strong>." +msgstr "" + +#: adhocracy/templates/index.html:55 +msgid "" +"Adhocracy is a platform for virtual direct democracies where you can " +"cooperate to create andselect solutions to your organization's " +"challenges." +msgstr "" + +#: adhocracy/templates/index.html:59 +msgid "What groups?" +msgstr "" + +#: adhocracy/templates/index.html:62 +msgid "" +"Think NGOs, open projects. Distributed, loosely-knit groups in search of " +"a common strategy or groups with complex internal policies like " +"Wikipedia." +msgstr "" + +#: adhocracy/templates/index.html:64 adhocracy/templates/index.html:72 +msgid "Read more..." +msgstr "" + +#: adhocracy/templates/index.html:67 +msgid "What decisions?" +msgstr "" + +#: adhocracy/templates/index.html:70 +msgid "" +"Anything that discusses a method of solving a problem. This could be " +"laws, strategy decisions or even design patterns." +msgstr "" + +#: adhocracy/templates/index.html:76 +msgid "Activity in my Adhocracies" +msgstr "" + +#: adhocracy/templates/pager.html:6 +msgid "sort by" +msgstr "" + +#: adhocracy/templates/pager.html:26 +msgid "previous" +msgstr "" + +#: adhocracy/templates/pager.html:44 +msgid "next" +msgstr "" + +#: adhocracy/templates/template.html:4 +msgid "No Title" +msgstr "" + +#: adhocracy/templates/template.html:55 +msgid "sign in" +msgstr "" + +#: adhocracy/templates/template.html:59 +msgid "settings" +msgstr "" + +#: adhocracy/templates/template.html:60 +msgid "logout" +msgstr "" + +#: adhocracy/templates/template.html:64 +msgid "search" +msgstr "" + +#: adhocracy/templates/template.html:66 +msgid "Go" +msgstr "" + +#: adhocracy/templates/template.html:70 +#, python-format +msgid "Join %s to contribute" +msgstr "" + +#: adhocracy/templates/template.html:83 +#: adhocracy/templates/instance/view.html:6 +msgid "Home" +msgstr "" + +#: adhocracy/templates/template.html:90 +msgid "More Adhocracies..." +msgstr "" + +#: adhocracy/templates/template.html:94 +msgid "Critical" +msgstr "" + +#: adhocracy/templates/template.html:96 +msgid "Review" +msgstr "" + +#: adhocracy/templates/template.html:98 +#: adhocracy/templates/user/delegations.html:11 +msgid "Delegations" +msgstr "" + +#: adhocracy/templates/template.html:103 +msgid "Administration" +msgstr "" + +#: adhocracy/templates/template.html:105 +#: adhocracy/templates/category/edit.html:4 +#: adhocracy/templates/issue/edit.html:4 adhocracy/templates/motion/edit.html:4 +#, python-format +msgid "Edit %s" +msgstr "" + +#: adhocracy/templates/template.html:106 +#: adhocracy/templates/admin/members.html:6 +msgid "Members" +msgstr "" + +#: adhocracy/templates/template.html:108 +msgid "Global Permissions" +msgstr "" + +#: adhocracy/templates/template.html:116 adhocracy/templates/user/index.html:3 +#: adhocracy/templates/user/index.html:6 adhocracy/templates/user/index.html:9 +msgid "Users" +msgstr "" + +#: adhocracy/templates/template.html:159 +msgid "adhocracy · liquid democracy platform" +msgstr "" + +#: adhocracy/templates/template.html:160 +msgid "about" +msgstr "" + +#: adhocracy/templates/template.html:161 +msgid "faq" +msgstr "" + +#: adhocracy/templates/template.html:162 +msgid "development" +msgstr "" + +#: adhocracy/templates/template.html:163 +msgid "imprint/contact" +msgstr "" + +#: adhocracy/templates/admin/members.html:3 +#: adhocracy/templates/admin/members.html:9 +#, python-format +msgid "Members: %s" +msgstr "" + +#: adhocracy/templates/admin/members.html:23 +#, python-format +msgid "is a %s" +msgstr "" + +#: adhocracy/templates/admin/members.html:24 +msgid "force to leave" +msgstr "" + +#: adhocracy/templates/admin/members.html:30 +msgid "move to Group:" +msgstr "" + +#: adhocracy/templates/admin/members.html:33 +msgid "Observer" +msgstr "" + +#: adhocracy/templates/admin/members.html:38 +msgid "Voter" +msgstr "" + +#: adhocracy/templates/admin/members.html:43 +msgid "Supervisor" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:3 +msgid "Admin: Group Permissions" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:6 +msgid "Admin » Group Permissions" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:11 +msgid "Group Permissions" +msgstr "" + +#: adhocracy/templates/admin/permissions.html:14 +msgid "" +"WARNING: This allows you to shut yourself out of your adhocracy. Handle " +"with care!" +msgstr "" + +#: adhocracy/templates/category/create.html:4 +#: adhocracy/templates/category/create.html:7 +#: adhocracy/templates/category/create.html:13 +msgid "New category" +msgstr "" + +#: adhocracy/templates/category/create.html:19 +msgid "Category description:" +msgstr "" + +#: adhocracy/templates/category/edit.html:7 +#: adhocracy/templates/issue/edit.html:7 adhocracy/templates/motion/edit.html:7 +#: adhocracy/templates/user/edit.html:7 +msgid "Edit" +msgstr "" + +#: adhocracy/templates/category/edit.html:13 +msgid "Category title" +msgstr "" + +#: adhocracy/templates/category/edit.html:22 +msgid "Category description" +msgstr "" + +#: adhocracy/templates/category/tiles.html:7 +#: adhocracy/templates/category/tiles.html:25 +#: adhocracy/templates/instance/tiles.html:6 +#: adhocracy/templates/instance/tiles.html:32 +#: adhocracy/templates/user/view.html:32 +#, python-format +msgid "%s issue" +msgid_plural "%s issues" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/category/tiles.html:23 +#: adhocracy/templates/category/view.html:39 +#: adhocracy/templates/comment/revision_tiles.html:7 +#: adhocracy/templates/comment/tiles.html:20 +#: adhocracy/templates/instance/tiles.html:31 +#: adhocracy/templates/issue/tiles.html:15 +#: adhocracy/templates/issue/view.html:42 +#: adhocracy/templates/motion/tiles.html:16 +#: adhocracy/templates/motion/tiles.html:35 +#: adhocracy/templates/motion/view.html:52 +#, python-format +msgid "created %s" +msgstr "" + +#: adhocracy/templates/category/tiles.html:24 +#, python-format +msgid "%s category" +msgid_plural "%s categories" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/category/tree.html:45 +msgid "Categorization" +msgstr "" + +#: adhocracy/templates/category/view.html:10 +#: adhocracy/templates/category/view.html:12 +#: adhocracy/templates/comment/tiles.html:101 +#: adhocracy/templates/issue/view.html:12 +#: adhocracy/templates/issue/view.html:14 +#: adhocracy/templates/motion/view.html:12 +#: adhocracy/templates/motion/view.html:14 +#: adhocracy/templates/motion/view.html:16 +msgid "delete" +msgstr "" + +#: adhocracy/templates/category/view.html:16 +#: adhocracy/templates/category/view.html:18 +#: adhocracy/templates/comment/revision_tiles.html:19 +#: adhocracy/templates/comment/tiles.html:93 +#: adhocracy/templates/comment/tiles.html:95 +#: adhocracy/templates/comment/tiles.html:97 +#: adhocracy/templates/instance/view.html:11 +#: adhocracy/templates/issue/view.html:18 +#: adhocracy/templates/issue/view.html:20 +#: adhocracy/templates/motion/view.html:20 +#: adhocracy/templates/motion/view.html:22 +#: adhocracy/templates/motion/view.html:24 +#: adhocracy/templates/user/view.html:11 +msgid "edit" +msgstr "" + +#: adhocracy/templates/category/view.html:40 +#: adhocracy/templates/comment/tiles.html:118 +msgid "history" +msgstr "" + +#: adhocracy/templates/category/view.html:51 +msgid "Subcategories" +msgstr "" + +#: adhocracy/templates/category/view.html:54 +#: adhocracy/templates/instance/view.html:58 +msgid "Create new categories to further structure the debate." +msgstr "" + +#: adhocracy/templates/category/view.html:71 +#: adhocracy/templates/instance/view.html:76 +#, fuzzy +msgid "Issues" +msgstr "" + +#: adhocracy/templates/category/view.html:75 +msgid "Create an issue to discuss a new topic within the current category." +msgstr "" + +#: adhocracy/templates/comment/create.html:3 +#: adhocracy/templates/comment/create.html:6 +msgid "New comment" +msgstr "" + +#: adhocracy/templates/comment/create.html:9 +#: adhocracy/templates/comment/tiles.html:13 +#: adhocracy/templates/comment/view.html:2 +#: adhocracy/templates/comment/view.html:5 +msgid "Comment" +msgstr "" + +#: adhocracy/templates/comment/edit.html:3 +#: adhocracy/templates/comment/edit.html:6 +msgid "Edit comment" +msgstr "" + +#: adhocracy/templates/comment/edit.html:9 +#, python-format +msgid "Comment on: %s" +msgstr "" + +#: adhocracy/templates/comment/history.html:2 +#: adhocracy/templates/comment/history.html:5 +#: adhocracy/templates/comment/history.html:8 +msgid "Comment History" +msgstr "" + +#: adhocracy/templates/comment/revision_tiles.html:13 +msgid "revert here" +msgstr "" + +#: adhocracy/templates/comment/revision_tiles.html:17 +msgid "view comment" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:22 +#: adhocracy/templates/comment/tiles.html:113 +#, python-format +msgid "edited %s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:26 +#: adhocracy/templates/motion/tiles.html:23 +msgid "in" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:57 +#: adhocracy/templates/comment/tiles.html:59 +msgid "Sign in to rate comments" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:61 +#: adhocracy/templates/comment/tiles.html:63 +msgid "This is your own comment" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:78 +msgid "discussion" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:87 +#: adhocracy/templates/comment/tiles.html:89 +msgid "reply" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:105 +#, python-format +msgid "deleted %s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:109 +#, python-format +msgid "%s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:115 +#, python-format +msgid "edited %s by %s" +msgstr "" + +#: adhocracy/templates/comment/tiles.html:129 +msgid "This comment has been deleted." +msgstr "" + +#: adhocracy/templates/comment/view.html:8 +#, python-format +msgid "Discussion on %s" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:15 +#, fuzzy +msgid "for" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:17 +msgid "against" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:19 +msgid "abstained" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:21 +msgid "undecided" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:36 +msgid "" +"The user's delegates have voted, but no consensus wasreached among them. " +"The decision is deferred." +msgstr "" + +#: adhocracy/templates/decision/tiles.html:40 +msgid "The decision was made without delegations." +msgstr "" + +#: adhocracy/templates/decision/tiles.html:42 +msgid "The decision was determined as a result of the following delegations:" +msgstr "" + +#: adhocracy/templates/decision/tiles.html:64 +#, python-format +msgid "in a poll %s" +msgstr "" + +#: adhocracy/templates/delegation/create.html:4 +#: adhocracy/templates/delegation/create.html:11 +#, python-format +msgid "Delegate: %s" +msgstr "" + +#: adhocracy/templates/delegation/create.html:7 +#: adhocracy/templates/delegation/review.html:5 +msgid "Delegation" +msgstr "" + +#: adhocracy/templates/delegation/create.html:18 +msgid "" +"This page is obviously a placeholder. Nice, karma-based recs are coming " +"soon." +msgstr "" + +#: adhocracy/templates/delegation/create.html:28 +#, python-format +msgid "Popular delegates for %s" +msgstr "" + +#: adhocracy/templates/delegation/create.html:42 +msgid "Your favourite delegates" +msgstr "" + +#: adhocracy/templates/delegation/create.html:55 +msgid "Delegate to:" +msgstr "" + +#: adhocracy/templates/delegation/review.html:2 +#: adhocracy/templates/delegation/review.html:11 +#, python-format +msgid "Delegation: %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:9 +#: adhocracy/templates/delegation/tiles.html:13 +msgid "revoke" +msgstr "" + +#: adhocracy/templates/delegation/review.html:17 +#: adhocracy/templates/delegation/tiles.html:4 +#, python-format +msgid "from %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:18 +#: adhocracy/templates/delegation/tiles.html:10 +#, python-format +msgid "to %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:22 +#, python-format +msgid "established %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:24 +#, python-format +msgid "revoked %s" +msgstr "" + +#: adhocracy/templates/delegation/review.html:30 +msgid "The delegation can be overridden or revoked at any time." +msgstr "" + +#: adhocracy/templates/delegation/review.html:42 +msgid "" +"No decisions have been based on this delegation yet. As soon as this " +"delegation leads to any decisions, they will be listed here." +msgstr "" + +#: adhocracy/templates/delegation/tiles.html:5 +#: adhocracy/templates/delegation/tiles.html:11 +msgid "track record" +msgstr "" + +#: adhocracy/templates/error/http.html:2 adhocracy/templates/error/http.html:4 +#, python-format +msgid "Error %s" +msgstr "" + +#: adhocracy/templates/error/http.html:8 +msgid "" +"If this error continues to occur, please <a " +"href='/page/imprint.html'>notify us</a> with a description of what you " +"were trying to do." +msgstr "" + +#: adhocracy/templates/event/all.html:4 adhocracy/templates/event/all.html:11 +msgid "Whazza" +msgstr "" + +#: adhocracy/templates/event/all.html:7 +msgid "All current events in Adhocracy." +msgstr "" + +#: adhocracy/templates/instance/create.html:3 +#: adhocracy/templates/instance/create.html:6 +#: adhocracy/templates/instance/create.html:12 +msgid "New Adhocracy" +msgstr "" + +#: adhocracy/templates/instance/create.html:18 +msgid "Adhocracy address:" +msgstr "" + +#: adhocracy/templates/instance/create.html:19 +msgid "" +"The address may only contain alpha-numeric characters. Please note that " +"this key cannot be changed after the Adhocracy has been created." +msgstr "" + +#: adhocracy/templates/instance/create.html:22 +#: adhocracy/templates/instance/edit.html:59 +msgid "Description:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:3 +#, python-format +msgid "Manage: %s" +msgstr "" + +#: adhocracy/templates/instance/edit.html:6 +msgid "Manage" +msgstr "" + +#: adhocracy/templates/instance/edit.html:14 +msgid "Adhocracy Name" +msgstr "" + +#: adhocracy/templates/instance/edit.html:19 +#: adhocracy/templates/instance/view.html:34 +msgid "Voting Rules" +msgstr "" + +#: adhocracy/templates/instance/edit.html:20 +msgid "Majority:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:21 +msgid "" +"In order to become active, a motion must reach the given proportion of " +"approval." +msgstr "" + +#: adhocracy/templates/instance/edit.html:23 +msgid "A simple majority (½ of vote)" +msgstr "" + +#: adhocracy/templates/instance/edit.html:24 +msgid "A two-thirds majority" +msgstr "" + +#: adhocracy/templates/instance/edit.html:25 +msgid "In Soviet Russia, motion votes you." +msgstr "" + +#: adhocracy/templates/instance/edit.html:28 +msgid "Delay:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:29 +msgid "" +"Before activating, the defined majority must be continuously held by the " +"moton for the specified interval." +msgstr "" + +#: adhocracy/templates/instance/edit.html:31 +msgid "No delay" +msgstr "" + +#: adhocracy/templates/instance/edit.html:32 +msgid "1 Day" +msgstr "" + +#: adhocracy/templates/instance/edit.html:33 +msgid "2 Days" +msgstr "" + +#: adhocracy/templates/instance/edit.html:34 +msgid "One Week" +msgstr "" + +#: adhocracy/templates/instance/edit.html:35 +msgid "Two Weeks" +msgstr "" + +#: adhocracy/templates/instance/edit.html:36 +msgid "Four Weeks" +msgstr "" + +#: adhocracy/templates/instance/edit.html:39 +msgid "Membership Options" +msgstr "" + +#: adhocracy/templates/instance/edit.html:40 +msgid "Default group:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:41 +msgid "When a new member joins, he or she will be a member of this user group." +msgstr "" + +#: adhocracy/templates/instance/edit.html:51 +msgid "Logo" +msgstr "" + +#: adhocracy/templates/instance/edit.html:52 +msgid "File upload:" +msgstr "" + +#: adhocracy/templates/instance/edit.html:53 +msgid "Select a logo file to appear in the header area of this Adhocracy." +msgstr "" + +#: adhocracy/templates/instance/index.html:16 +msgid "Adhocracies are little democracies that are ran by their community." +msgstr "" + +#: adhocracy/templates/instance/tiles.html:8 +#: adhocracy/templates/instance/tiles.html:34 +#: adhocracy/templates/instance/view.html:14 +#: adhocracy/templates/instance/view.html:17 +msgid "join" +msgstr "" + +#: adhocracy/templates/instance/tiles.html:11 +#: adhocracy/templates/instance/tiles.html:37 +#: adhocracy/templates/instance/view.html:20 +msgid "leave" +msgstr "" + +#: adhocracy/templates/instance/view.html:10 +msgid "members" +msgstr "" + +#: adhocracy/templates/instance/view.html:35 +msgid "Required Majority:" +msgstr "" + +#: adhocracy/templates/instance/view.html:36 +msgid "To become active, a motion must reach the given proportion of approval." +msgstr "" + +#: adhocracy/templates/instance/view.html:38 +msgid "Activation Delay:" +msgstr "" + +#: adhocracy/templates/instance/view.html:39 +msgid "" +"Before becoming active, the majority must be held for the specified " +"interval." +msgstr "" + +#: adhocracy/templates/instance/view.html:45 +msgid "Subscribe to RSS feed:" +msgstr "" + +#: adhocracy/templates/instance/view.html:55 +#, fuzzy +msgid "Categories" +msgstr "" + +#: adhocracy/templates/instance/view.html:80 +msgid "Create an issue to start debating in this Adhocracy." +msgstr "" + +#: adhocracy/templates/issue/create.html:4 +#: adhocracy/templates/issue/create.html:7 +#: adhocracy/templates/issue/create.html:18 +msgid "New issue" +msgstr "" + +#: adhocracy/templates/issue/create.html:27 +msgid "Issue description:" +msgstr "" + +#: adhocracy/templates/issue/edit.html:13 +msgid "Issue Title" +msgstr "" + +#: adhocracy/templates/issue/tiles.html:16 +#: adhocracy/templates/motion/tiles.html:20 +#: adhocracy/templates/motion/tiles.html:39 +#: adhocracy/templates/user/view.html:34 +#, python-format +msgid "%s comment" +msgid_plural "%s comments" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/issue/tiles.html:17 +#: adhocracy/templates/user/view.html:33 +#, python-format +msgid "%s motion" +msgid_plural "%s motions" +msgstr[0] "" +msgstr[1] "" + +#: adhocracy/templates/issue/view.html:35 +#, python-format +msgid "in %s" +msgstr "" + +#: adhocracy/templates/issue/view.html:57 +#: adhocracy/templates/motion/index.html:6 +msgid "Motions" +msgstr "" + +#: adhocracy/templates/issue/view.html:59 +msgid "" +"Motions are <b>proposals</b> that solve some or all of the problems " +"described by this issue. A motion can be <b>discussed and voted</b> upon." +msgstr "" + +#: adhocracy/templates/issue/view.html:66 +#: adhocracy/templates/motion/view.html:209 +msgid "Discussion" +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:6 +msgid "Call a vote" +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:9 +#, python-format +msgid "Call a vote on: %s" +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:17 +msgid "" +"You are about to release this motion and call for a vote. When you do " +"this, you will lose the ability to <strong>change the motion's " +"wording</strong> unless you re-draft it." +msgstr "" + +#: adhocracy/templates/motion/begin_poll.html:21 +#: adhocracy/templates/motion/end_poll.html:21 +msgid "Confirm and Proceed" +msgstr "" + +#: adhocracy/templates/motion/create.html:4 +#: adhocracy/templates/motion/create.html:7 +#: adhocracy/templates/motion/create.html:14 +msgid "New motion" +msgstr "" + +#: adhocracy/templates/motion/create.html:20 +msgid "Informal motion description:" +msgstr "" + +#: adhocracy/templates/motion/edit.html:13 +msgid "New Motion" +msgstr "" + +#: adhocracy/templates/motion/end_poll.html:6 +msgid "End a vote" +msgstr "" + +#: adhocracy/templates/motion/end_poll.html:9 +#, python-format +msgid "Cancel vote on: %s" +msgstr "" + +#: adhocracy/templates/motion/end_poll.html:17 +msgid "" +"You are about to re-draft this motion. This means that the motion will " +"become editable again but that all votes that have been cast at this time" +" <strong>will be invalidated</strong>." +msgstr "" + +#: adhocracy/templates/motion/index.html:3 +#, python-format +msgid "Critical Motions in %s" +msgstr "" + +#: adhocracy/templates/motion/index.html:9 +msgid "Critical Motions" +msgstr "" + +#: adhocracy/templates/motion/index.html:14 +msgid "" +"These motions are currently voting and might be nearing a decision. Vote " +"now to make your voice heard." +msgstr "" + +#: adhocracy/templates/motion/index.html:27 +msgid "" +"There are currently no polls open. Call a motion into voting state to " +"begin a poll." +msgstr "" + +#: adhocracy/templates/motion/tiles.html:18 +#: adhocracy/templates/motion/tiles.html:37 +#, python-format +msgid "%s votes cast" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:46 +msgid "Draft" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:48 +msgid "Voting" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:50 +msgid "Activating" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:52 +msgid "Deactivating" +msgstr "" + +#: adhocracy/templates/motion/tiles.html:54 +msgid "Active" +msgstr "" + +#: adhocracy/templates/motion/view.html:32 +#: adhocracy/templates/motion/view.html:34 +#: adhocracy/templates/motion/view.html:36 +msgid "call a vote" +msgstr "" + +#: adhocracy/templates/motion/view.html:34 +msgid "The motion cannot be voted upon since it does not have any provisions yet." +msgstr "" + +#: adhocracy/templates/motion/view.html:66 +msgid "This vote has recieved the required majority and cannot be cancelled" +msgstr "" + +#: adhocracy/templates/motion/view.html:70 +msgid "Vote" +msgstr "" + +#: adhocracy/templates/motion/view.html:83 +#, fuzzy +msgid "Option" +msgstr "" + +#: adhocracy/templates/motion/view.html:86 +msgid "Delegate Recommendations" +msgstr "" + +#: adhocracy/templates/motion/view.html:89 +#: adhocracy/templates/motion/votes.html:8 +#: adhocracy/templates/user/votes.html:8 +msgid "Votes" +msgstr "" + +#: adhocracy/templates/motion/view.html:90 +msgid "Percent" +msgstr "" + +#: adhocracy/templates/motion/view.html:102 +msgid "Affirm" +msgstr "" + +#: adhocracy/templates/motion/view.html:123 +msgid "Dissent" +msgstr "" + +#: adhocracy/templates/motion/view.html:144 +msgid "Abstain" +msgstr "" + +#: adhocracy/templates/motion/view.html:158 +msgid "vote" +msgstr "" + +#: adhocracy/templates/motion/view.html:163 +#, python-format +msgid "Of the required %s votes, the motion has:" +msgstr "" + +#: adhocracy/templates/motion/view.html:168 +#, python-format +msgid "%d votes" +msgstr "" + +#: adhocracy/templates/motion/view.html:181 +#, python-format +msgid "poll started %s" +msgstr "" + +#: adhocracy/templates/motion/view.html:182 +msgid "help" +msgstr "" + +#: adhocracy/templates/motion/view.html:196 +msgid "Provisions" +msgstr "" + +#: adhocracy/templates/motion/view.html:198 +msgid "" +"<b>Provisions</b> are the body of a motion: together they form the " +"language that will be voted upon. You will need to have at least one " +"clause in order to call for a vote." +msgstr "" + +#: adhocracy/templates/motion/votes.html:5 +#: adhocracy/templates/user/votes.html:5 +#, python-format +msgid "Votes: %s" +msgstr "" + +#: adhocracy/templates/motion/votes.html:12 +#: adhocracy/templates/user/votes.html:14 +msgid "Votes:" +msgstr "" + +#: adhocracy/templates/search/results.html:2 +#: adhocracy/templates/search/results.html:5 +#: adhocracy/templates/search/results.html:12 +msgid "Search" +msgstr "" + +#: adhocracy/templates/search/results.html:10 +#, python-format +msgid "Search for '%s'" +msgstr "" + +#: adhocracy/templates/search/results.html:28 +msgid "" +"No entries could be found that match your criteria. Try a more general " +"search term." +msgstr "" + +#: adhocracy/templates/user/delegations.html:4 +#: adhocracy/templates/user/delegations.html:17 +msgid "My Delegations" +msgstr "" + +#: adhocracy/templates/user/delegations.html:6 +#: adhocracy/templates/user/delegations.html:19 +#, python-format +msgid "Delegations: %s" +msgstr "" + +#: adhocracy/templates/user/delegations.html:25 +msgid "Topic" +msgstr "" + +#: adhocracy/templates/user/delegations.html:26 +msgid "Given" +msgstr "" + +#: adhocracy/templates/user/delegations.html:27 +msgid "Received" +msgstr "" + +#: adhocracy/templates/user/edit.html:4 +#, python-format +msgid "Settings: %s" +msgstr "" + +#: adhocracy/templates/user/edit.html:20 +msgid "User Details" +msgstr "" + +#: adhocracy/templates/user/edit.html:22 +#: adhocracy/templates/user/register_form.html:9 +#: adhocracy/templates/user/reset_form.html:14 +msgid "E-Mail:" +msgstr "" + +#: adhocracy/templates/user/edit.html:24 +msgid "Language:" +msgstr "" + +#: adhocracy/templates/user/edit.html:36 +#: adhocracy/templates/user/login_form.html:5 +#: adhocracy/templates/user/register_form.html:13 +msgid "Password:" +msgstr "" + +#: adhocracy/templates/user/edit.html:38 +msgid "Select a new password or leave the fields blank to keep your old one." +msgstr "" + +#: adhocracy/templates/user/edit.html:42 +#: adhocracy/templates/user/register_form.html:16 +msgid "Password (confirm):" +msgstr "" + +#: adhocracy/templates/user/edit.html:46 +msgid "Configure your user icon at <a href='http://www.gravatar.com'>Gravatar</a>" +msgstr "" + +#: adhocracy/templates/user/edit.html:51 +msgid "Short biography" +msgstr "" + +#: adhocracy/templates/user/edit.html:54 +msgid "" +"A bio will allow others to learn about you and perhaps even get you a few" +" delegations." +msgstr "" + +#: adhocracy/templates/user/index.html:3 adhocracy/templates/user/index.html:9 +#, python-format +msgid "Users in %s" +msgstr "" + +#: adhocracy/templates/user/login.html:2 +msgid "Who are you, then?" +msgstr "" + +#: adhocracy/templates/user/login.html:5 +#: adhocracy/templates/user/login_form.html:8 +msgid "Login" +msgstr "" + +#: adhocracy/templates/user/login.html:6 +msgid "If you already have an account, sign in here." +msgstr "" + +#: adhocracy/templates/user/login.html:10 +msgid "" +"If you have an account but you've lost your password, <a " +"href='/user/reset'>click here</a>." +msgstr "" + +#: adhocracy/templates/user/login.html:15 +#: adhocracy/templates/user/register_form.html:19 +msgid "Register" +msgstr "" + +#: adhocracy/templates/user/login.html:16 +msgid "" +"Creating an account is easy; all you need is a user name, password and " +"email address." +msgstr "" + +#: adhocracy/templates/user/login_form.html:2 +msgid "Login:" +msgstr "" + +#: adhocracy/templates/user/register_form.html:5 +msgid "User name:" +msgstr "" + +#: adhocracy/templates/user/register_form.html:6 +msgid "Can only contain letters and numbers." +msgstr "" + +#: adhocracy/templates/user/register_form.html:10 +msgid "We don't spam." +msgstr "" + +#: adhocracy/templates/user/reset_form.html:2 +#: adhocracy/templates/user/reset_form.html:4 +#: adhocracy/templates/user/reset_pending.html:2 +#: adhocracy/templates/user/reset_pending.html:4 +msgid "Password reset" +msgstr "" + +#: adhocracy/templates/user/reset_form.html:8 +msgid "" +"You will be sent an activation link that, when opened, will cause " +"Adhocracy to email you a new password." +msgstr "" + +#: adhocracy/templates/user/reset_form.html:12 +msgid "In order to retrieve your login, you will have to enter your email adress." +msgstr "" + +#: adhocracy/templates/user/reset_form.html:18 +msgid "Reset" +msgstr "" + +#: adhocracy/templates/user/reset_pending.html:4 +msgid "Confirmation pending" +msgstr "" + +#: adhocracy/templates/user/reset_pending.html:11 +msgid "" +"You have recieved an email containing a link. Please open that link in " +"order to reset you password" +msgstr "" + +#: adhocracy/templates/user/tiles.html:16 +#, python-format +msgid "%s karma" +msgstr "" + +#: adhocracy/templates/user/tiles.html:18 adhocracy/templates/user/view.html:38 +#, python-format +msgid "signed up %s" +msgstr "" + +#: adhocracy/templates/user/view.html:24 +#, python-format +msgid "%s does not have a bio" +msgstr "" + +#: adhocracy/templates/user/view.html:36 +msgid "delegations" +msgstr "" + +#: adhocracy/templates/user/view.html:46 +msgid "is a member in the following adhocracies:" +msgstr "" + +#: adhocracy/templates/user/view.html:56 +msgid "Activity" +msgstr "" + +#: adhocracy/templates/user/votes.html:12 +msgid "Review your voting track" +msgstr "" + diff --git a/adhocracy/lib/__init__.py b/adhocracy/lib/__init__.py new file mode 100644 index 000000000..a4e65e64a --- /dev/null +++ b/adhocracy/lib/__init__.py @@ -0,0 +1,10 @@ + +import democracy +import text +import search +import event + +from version import get_version + +from social import delegateable_popular_agents +from social import user_popular_agents \ No newline at end of file diff --git a/adhocracy/lib/app_globals.py b/adhocracy/lib/app_globals.py new file mode 100644 index 000000000..409153de2 --- /dev/null +++ b/adhocracy/lib/app_globals.py @@ -0,0 +1,39 @@ +"""The application's Globals object""" +import memcache + +import logging + +from pylons import config + +from adhocracy import model + +import cache +import search + +log = logging.getLogger(__name__) + +class Globals(object): + + """Globals acts as a container for objects available throughout the + life of the application + + """ + + def __init__(self): + """One instance of Globals is created during application + initialization and is available during requests via the + 'app_globals' variable + + """ + if 'memcached.server' in config: + self.cache = memcache.Client([config['memcached.server']]) + log.info("Memcache set up") + self.cache.flush_all() + cache.setup_cache() + else: + log.warn("Skipped memcache, no results caching will take place.") + self.cache = None + + search.setup_search() + + \ No newline at end of file diff --git a/adhocracy/lib/authentication.py b/adhocracy/lib/authentication.py new file mode 100644 index 000000000..4ca84e817 --- /dev/null +++ b/adhocracy/lib/authentication.py @@ -0,0 +1,58 @@ +import logging + +from repoze.who.plugins.basicauth import BasicAuthPlugin +from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin +from repoze.who.plugins.sa import SQLAlchemyAuthenticatorPlugin, \ + SQLAlchemyUserMDPlugin +from repoze.who.plugins.friendlyform import FriendlyFormPlugin + +from repoze.what.middleware import setup_auth as setup_what +from repoze.what.plugins.sql.adapters import SqlGroupsAdapter, SqlPermissionsAdapter + +import adhocracy.model as model +from authorization import InstanceGroupSourceAdapter + +log = logging.getLogger(__name__) + +def setup_auth(app, config): + + groupadapter = InstanceGroupSourceAdapter() + #groupadapter.translations.update({'sections': 'groups'}) + permissionadapter = SqlPermissionsAdapter(model.Permission, + model.Group, + model.meta.Session) + #permissionadapter.translations.update(permission_translations) + + group_adapters = {'sql_auth': groupadapter} + permission_adapters = {'sql_auth': permissionadapter} + + basicauth = BasicAuthPlugin('Adhocracy HTTP Authentication') + auth_tkt = AuthTktCookiePlugin('41d207498d3812741e27c6441760ae494a4f9fbf', cookie_name='adhocracy_login') + + form = FriendlyFormPlugin( + '/login', + '/user/perform_login', + '/user/post_login', + '/logout', + '/user/post_logout', + login_counter_name='_login_tries', + rememberer_name='auth_tkt') + + sqlauth = SQLAlchemyAuthenticatorPlugin(model.User, model.meta.Session) + sql_user_md = SQLAlchemyUserMDPlugin(model.User, model.meta.Session) + + identifiers = [('form', form),('basicauth', basicauth), ('auth_tkt', auth_tkt)] + authenticators = [('sqlauth', sqlauth)] + challengers = [('form', form), ('basicauth', basicauth)] + mdproviders = [('sql_user_md', sql_user_md)] + + log_stream = None + #log_stream = sys.stdout + + return setup_what(app, group_adapters, permission_adapters, + identifiers=identifiers, + authenticators=authenticators, + challengers=challengers, + mdproviders=mdproviders, + log_stream = log_stream, + log_level = logging.DEBUG) \ No newline at end of file diff --git a/adhocracy/lib/authorization.py b/adhocracy/lib/authorization.py new file mode 100644 index 000000000..f8dcd9925 --- /dev/null +++ b/adhocracy/lib/authorization.py @@ -0,0 +1,113 @@ +import logging + +from pylons import config, tmpl_context as c +from pylons.controllers.util import abort, redirect_to +from pylons.i18n import _ + +from repoze.what.predicates import has_permission as what_has_permission +from repoze.what.adapters import BaseSourceAdapter, SourceError +from repoze.what.plugins.sql.adapters import SqlGroupsAdapter + +import adhocracy.model as model +import karma +import democracy +import helpers as h + +log = logging.getLogger(__name__) + + +class InstanceGroupSourceAdapter(SqlGroupsAdapter): + + def __init__(self, *args, **kwargs): + super(InstanceGroupSourceAdapter, self).__init__(model.Group, model.User, model.meta.Session) + self.is_writable = False + + def _get_section_items(self, section): + group = model.Group.by_code(section) + users = [] + for membership in group.memberships: + if not membership.instance: + users.append(membership.user) + elif model.filter.has_instance() and \ + membership.instance == model.filter.get_instance(): + users.append(membership.user) + return set(map(lambda u: u.user_name, users)) + + def _get_item_as_row(self, item_name): + user = model.User.find(item_name, instance_filter=False) + if not user: + raise SourceError("No such user: %s" % item_name) + return user + + +class has_permission(what_has_permission): + """ + This modified version of ``repoze.what``'s ``has_permission`` will apply ``Anonymous`` + rights to any user making requests. This allows to call ``has_permission`` on methods + even when they are not protected, thus making the authorization system more + configurable. + + *WARNING*: This does not include authorizations that are subject to Karma thresholds. + """ + def evaluate(self, environ, credentials): + try: + super(has_permission, self).evaluate(environ, credentials) + except Exception, e: + anonymous = model.Group.by_code(model.Group.CODE_ANONYMOUS) + if anonymous: + for perm in anonymous.permissions: + if perm.permission_name == self.permission_name: + return + raise e + +def on_delegateable(delegateable, permission_name, allow_creator=True): + """ + Check if a user can perform actions protected by the given permission on + the given delegateable. If ``allow_creator`` is set, allow the context user + to perform all actions if she is the creator of ``delegateable`` + """ + if allow_creator and delegateable and c.user and c.user == delegateable.creator: + return True + if c.user and (c.user.has_permission('instance.admin') or c.user.has_permission('global.admin')): + return True + if c.user and c.user.has_permission(permission_name): + if karma.user_score(c.user) >= karma.threshold.limit(permission_name): + return True + return False + +def on_comment(comment, permission_name, allow_creator=True): + """ + Check if a user can perform actions protected by the given permission on + the given comment. Equivalent to ``on_delegateable``. + """ + res = on_delegateable(comment.topic, permission_name, allow_creator=False) + if c.user and c.user == comment.creator and allow_creator: + return True + return res + +def require_delegateable_perm(delegateable, permission_name): + """ If permission is not present, show a warning page. """ + if not on_delegateable(delegateable, permission_name): + h.flash(karma.threshold.message(permission_name)) + if not delegateable: + delegateable = c.instance.root + redirect_to('/d/%s' % str(delegateable.id)) + +def require_motion_perm(motion, permission_name, enforce_immutability=True): + """ If permission is not present, show a warning page. """ + require_delegateable_perm(motion, permission_name) + if motion and not democracy.is_motion_mutable(motion) and enforce_immutability: + h.flash(h.immutable_motion_message()) + redirect_to('/motion/%s' % str(motion.id)) + +def require_comment_perm(comment, permission_name, enforce_immutability=True): + """ If permission is not present, show a warning page. """ + if not on_comment(comment, permission_name): + h.flash(karma.threshold.message(permission_name)) + redirect_to('/comment/r/%s' % comment.id) + if not democracy.is_comment_mutable(comment) and enforce_immutability: + h.flash(h.immutable_motion_message()) + redirect_to('/comment/r/%s' % comment.id) + + + \ No newline at end of file diff --git a/adhocracy/lib/base.py b/adhocracy/lib/base.py new file mode 100644 index 000000000..c5df25e20 --- /dev/null +++ b/adhocracy/lib/base.py @@ -0,0 +1,96 @@ +"""The base Controller API + +Provides the BaseController class for subclassing. +""" +import logging + +from pylons import config +from pylons.controllers import WSGIController +from pylons import request, response, session, tmpl_context as c +from pylons.controllers.util import abort, redirect_to +from pylons.decorators import validate +from pylons.i18n import _, add_fallback, get_lang, set_lang, gettext + +import routes + +import formencode +import formencode.validators as validators +from formencode import htmlfill + +from authorization import has_permission +import authorization as auth +from repoze.what.plugins.pylonshq import ActionProtector + +from cache import memoize +from instance import RequireInstance +from xsrf import RequireInternalRequest +from templating import render, NamedPager +import adhocracy.model as model +import search as libsearch +import helpers as h +import event +import democracy +import tiles +import sorting +import text.i18n as i18n + + +class BaseController(WSGIController): + + def _parse_motion_id(self, id): + c.motion = model.Motion.find(id) + if not c.motion: + abort(404, _("No motion with ID %(id)s exists.") % {'id': id}) + + def __call__(self, environ, start_response): + """Invoke the Controller""" + # WSGIController.__call__ dispatches to the Controller method + # the request is routed to. This routing information is + # available in environ['pylons.routes_dict'] + import adhocracy.lib + c.lib = adhocracy.lib + c.model = model + c.instance = model.filter.get_instance() + + libsearch.attach_thread() + + environ['HTTP_HOST'] = environ['HTTP_HOST_ORIGINAL'] + #print routes.url_for(controller="motion", action="view", id="HFDHJ") + #print routes.request_config().host + + if environ.get('repoze.who.identity'): + c.user = environ.get('repoze.who.identity').get('user') + #model.meta.Session.add(c.user) + else: + c.user = None + + # have to do this with the user in place + i18n.handle_request() + + + if c.user: + h.add_rss(_("My Adhocracies"), h.instance_url(None, '/feed.rss')) + if c.instance: + h.add_rss("%s News" % c.instance.label, + h.instance_url(c.instance, '/instance/%s.rss' % c.instance.key)) + + h.add_meta("description", _("A liquid democracy platform for making decisions in " + + "distributed, open groups by cooperatively creating proposals and voting " + + "on them to establish their support.")) + h.add_meta("keywords", _("adhocracy, direct democracy, liquid democracy, liqd, democracy, wiki, voting," + + "participation, group decisions, decisions, decision-making")) + + try: + return WSGIController.__call__(self, environ, start_response) + finally: + model.meta.Session.remove() + + + + + +def ExpectFormat(formats=['html', 'rss', 'xml', 'json']): + def _parse(f, *a, **kw): + #TODO + return f(*a, **kw) + return decorator(_parse) diff --git a/adhocracy/lib/cache/__init__.py b/adhocracy/lib/cache/__init__.py new file mode 100644 index 000000000..d4887bc93 --- /dev/null +++ b/adhocracy/lib/cache/__init__.py @@ -0,0 +1,22 @@ +import logging + +import adhocracy.model.hooks as hooks +import adhocracy.model as model + +from util import memoize +from invalidate import * + + +log = logging.getLogger(__name__) + +def setup_cache(): + log.info("Setting up memcache-related persistence hooks...") + hooks.patch_default(model.User, invalidate_user) + hooks.patch_default(model.Vote, invalidate_vote) + hooks.patch_default(model.Delegateable, invalidate_delegateable) + hooks.patch_default(model.Delegation, invalidate_delegation) + hooks.patch_default(model.Revision, invalidate_revision) + hooks.patch_default(model.Comment, invalidate_comment) + hooks.patch_default(model.Karma, invalidate_karma) + hooks.patch_default(model.Poll, invalidate_poll) + \ No newline at end of file diff --git a/adhocracy/lib/cache/invalidate.py b/adhocracy/lib/cache/invalidate.py new file mode 100644 index 000000000..d3108d808 --- /dev/null +++ b/adhocracy/lib/cache/invalidate.py @@ -0,0 +1,47 @@ +from util import clear_tag + +def invalidate_user(user): + clear_tag(user) + +def invalidate_delegateable(d): + clear_tag(d) + for p in d.parents: + invalidate_delegateable(p) + +def invalidate_revision(rev): + invalidate_comment(rev.comment) + +def invalidate_karma(karma): + invalidate_comment(karma.comment) + +def invalidate_comment(comment): + clear_tag(comment) + invalidate_delegateable(comment.topic) + +def invalidate_issue(issue): + invalidate_delegateable(issue) + +def invalidate_motion(motion): + invalidate_delegateable(motion) + +def invalidate_category(category): + invalidate_delegateable(category) + +def invalidate_delegation(delegation): + invalidate_user(delegation.principal) + invalidate_user(delegation.agent) + +def invalidate_vote(vote): + clear_tag(vote) + invalidate_user(vote.user) + invalidate_poll(vote.poll) + +def invalidate_poll(poll): + clear_tag(poll) + invalidate_motion(poll.motion) + +def invalidate_instance(instance): + # muharhar cache epic fail + clear_tag(instance) + for d in instance.delegateables: + invalidate_delegateable(d) \ No newline at end of file diff --git a/adhocracy/lib/cache/util.py b/adhocracy/lib/cache/util.py new file mode 100644 index 000000000..c3229b378 --- /dev/null +++ b/adhocracy/lib/cache/util.py @@ -0,0 +1,72 @@ +import logging + +from sqlalchemy.schema import MetaData +from pylons import g, session + +import hashlib + +class NoneResult(object): pass + +log = logging.getLogger(__name__) + +SEP="|" + +cacheTags = {} + +def add_tags(key, tags): + ctags = g.cache.get_multi(tags) + for tag in tags: + if not ctags.get(tag): + ctags[tag] = key + else: + ctags[tag] += SEP + key + g.cache.set_multi(ctags) + +def tag_fn(key, a, kw): + tags = [] + for arg in a: + tags = tags + [arg] + for kword in kw.values(): + tags = tags + [kword] + add_tags(key, map(make_tag, tags)) + +def make_tag(obj): + return make_key("__tag_", [obj], {}) + +def make_key(iden, a, kw=None): + return iden + hashlib.sha1(str(map(str, a)) + str(map(str, kw.items()))).hexdigest() + +def clear_tag(tag): + try: + tag = make_tag(tag) + entities = g.cache.get(tag) + if entities: + g.cache.delete_multi(entities.split(SEP)) + except TypeError, te: + pass + #log.warn(te) + +def memoize(iden, time = 0): + def memoize_fn(fn): + from adhocracy.lib.cache.util import NoneResult + def new_fn(*a, **kw): + if not g.cache: + res = fn(*a, **kw) + else: + key = make_key(iden, a, kw) + res = g.cache.get(key) + if res is None: + res = fn(*a, **kw) + #print "Cache miss", key + if res is None: + res = NoneResult + #print "Cache set:", key + g.cache.set(key, res, time = time) + tag_fn(key, a, kw) + #else: + #print "Cache hit", key + if res == NoneResult: + res = None + return res + return new_fn + return memoize_fn \ No newline at end of file diff --git a/adhocracy/lib/democracy/__init__.py b/adhocracy/lib/democracy/__init__.py new file mode 100644 index 000000000..8de06a529 --- /dev/null +++ b/adhocracy/lib/democracy/__init__.py @@ -0,0 +1,45 @@ +import adhocracy.model as model + +import decision +from decision import Decision + +import delegation_node +from delegation_node import DelegationNode + +#import poll +#from poll import Poll, PollException, NoPollException + +import result +from result import Result + +# TODO: Principals length + +# TODO: Delegation replay + + +def is_motion_mutable(motion): + """ + Find out whether a motion can be modified in its current polling + state. + """ + result = Result(motion) + return not result.polling + +def is_comment_mutable(comment): + """ + Find out whether a comment is a canonical contribution to a motion + that is currently polling. + """ + if not comment.canonical: + return True + if isinstance(comment.topic, model.Motion): + return is_motion_mutable(comment.topic) + return True + +def can_motion_cancel(motion): + """ + Find out whether a motion can be modified in its current polling + state. + """ + result = Result(motion) + return result.can_cancel \ No newline at end of file diff --git a/adhocracy/lib/democracy/conditions.py b/adhocracy/lib/democracy/conditions.py new file mode 100644 index 000000000..fd66aee3b --- /dev/null +++ b/adhocracy/lib/democracy/conditions.py @@ -0,0 +1,104 @@ +from datetime import datetime + +import adhocracy.model as model +from adhocracy.model import Motion +from ..cache import memoize + +from tally import Tally + + +class Condition(object): + + def __init__(self, result): + self.result = result + self._met = None + + def _is_met(self): + if self._met == None: + self._met = self.check([]) + return self._met + + met = property(_is_met) + + +class MajorityCondition(Condition): + + def check_decisions(self, decisions): + return Tally(decisions).rel_for > self.required_majority + + def check(self, decisions): + return True + +class ParticipationCondition(MajorityCondition): + + def check(self, decisions): + return True + + + +class StabilityCondition(MajorityCondition): + + def check(self): + self._stabilizing = False + self._remaining_time = None + now = datetime.now() + activation_time = now - self.result.activation_delay + for vote in reversed(self.result.votes): + decisions = self.result.generate_decisions(at_time=vote.create_time) + if not self.check_tally(Tally(decisions)): + self._remaining_time = self.result.activation_delay \ + - (now - vote.create_time) + break + self._stabilizing = True + if vote.create_time <= activation_time: + return True + return False + + def _activation_delay(self): + return timedelta(days=self.result.motion.instance.activation_delay) + + activation_delay = property(_activation_delay) + + def _is_stabilizing(self): + self.met + return self._stabilizing + + stabilizing = property(_is_stabilizing) + + def _get_remaining_time(self): + self.met + return self._remaining_time + + remaining_time = property(_get_remaining_time) + + +ACTIVATION_CHAIN = [ParticipationCondition, + MajorityCondition, + StabilityCondition] + + +class ChainedCondition(Condition): + + def __init__(self, result, chain=ACTIVATION_CHAIN): + super(ChainedCondition, self).__init__(result) + self._chain = map(lambda c: c(result), chain) + + def __getitem__(self, condition): + for link in self._chain: + if type(link) == condition: + return link + raise IndexError() + + def max(self): + return type(self._chain[-1]) + + def check(self, decisions): + return self.has(self.max(), decisions) + + def has(self, condition, decisions=None): + decisions = decisions if decisions else self.result.decisions + for link in self._chain: + if not link.check(decisions): + return False + if type(link) == condition: # not isinstance + return True diff --git a/adhocracy/lib/democracy/constraints.py b/adhocracy/lib/democracy/constraints.py new file mode 100644 index 000000000..fb2934a95 --- /dev/null +++ b/adhocracy/lib/democracy/constraints.py @@ -0,0 +1,37 @@ +from datetime import datetime + +import adhocracy.model as model +from poll import Poll, NoPollException + +class DependencyConstraint(object): + + def __init__(self, motion, result_class, at_time=None): + if not at_time: + at_time = datetime.now() + self.at_time = at_time + self.motion = motion + self.result_class = result_class + + def _met(self): + for dependency in self.motion.dependencies: + try: + poll = Poll(dependency.requirement, at_time=self.at_time) + result = self.result_class(poll) + print "DEP ", dependency, " RES ", result.state + if not result.state in model.Motion.FULFILLING_STATES: + return False + except NoPollException: + return False + return True + + met = property(_met) + +class AntinomyConstraint(object): + + def __init__(self, motion, result_class, at_time=None): + pass + + def _met(self): + return True + + met = property(_met) \ No newline at end of file diff --git a/adhocracy/lib/democracy/decision.py b/adhocracy/lib/democracy/decision.py new file mode 100644 index 000000000..9772ae4f0 --- /dev/null +++ b/adhocracy/lib/democracy/decision.py @@ -0,0 +1,215 @@ +from datetime import datetime, timedelta +import logging + +from sqlalchemy import and_, or_ +from sqlalchemy.orm import eagerload + +import adhocracy.model as model +from adhocracy.model import Motion, Vote, Poll, User + +from delegation_node import DelegationNode + +log = logging.getLogger(__name__) + +class DecisionException(Exception): + """ A general exception for ``Decision`` errors """ + pass + +class Decision(object): + """ + A decision describes the current or past opinion that a user has + expressed on a given motion. This includes opinions that were determined + by an agent as a result of delegation. + """ + + def __init__(self, user, poll, at_time=None, votes=None): + self.user = user + self.poll = poll + self.at_time = at_time + self.node = DelegationNode(user, poll.motion) + self._votes = votes + + def _get_votes(self): + """ All votes a user has created during the current polling interval. """ + if not self._votes: + q = model.meta.Session.query(Vote) + q = q.filter(Vote.user_id==self.user.id) + q = q.filter(Vote.poll_id==self.poll.id) + q = q.options(eagerload(Vote.delegation)) + if self.at_time: + q = q.filter(Vote.create_time<=self.at_time) + q = q.order_by(Vote.create_time.desc()) + self._votes = q.all() + return self._votes + + votes = property(_get_votes) + + def _relevant_votes(self): + """ + Currently relevant votes for the polling interval. + + **WARNING**: A non-empty list of relevant votes does not always + mean a decision was made. This is not true, for example, when multiple + delegates match a motion and their opinions differ. + + :returns: List of ``Vote`` + """ + relevant = {} + for vote in self.votes: + if not vote.delegation: + return [vote] + if relevant.get(vote.delegation, vote).create_time <= vote.create_time: + relevant[vote.delegation] = vote + use_keys = self.node.filter_delegations(relevant.keys()) + return [v for k, v in relevant.items() if k in use_keys] + + relevant_votes = property(_relevant_votes) + + def _create_time(self): + """ + Utility property to see when this decision became effective. Equals + the latest relevant vote creation date. + + :returns: datetime + """ + return max(map(lambda v: v.create_time, self.relevant_votes)) + + create_time = property(_create_time) + + def _delegations(self): + """ + The set of delegations which have determined this decision, as per + ``relevant_votes``. + + :returns: list of ``Delegation`` + """ + return list(set(map(lambda v: v.delegation, self.relevant_votes))) + + delegations = property(_delegations) + + def _result(self): + """ + The result is an ``orientation`` and reflects the ``User``'s current + decision on the ``Motion``. Values match those in ``Vote``. Given multiple + delegates who have voted on the motion, the current approach is to check + for an unanimous decision and to discard all other constellations. Another + approach would be to require only a certain majority of agents to support an + opinion, thus creating an inner vote. + """ + relevant = self.relevant_votes + orientations = set(map(lambda v: v.orientation, relevant)) + if len(relevant) and len(orientations) == 1: + return orientations.pop() + return None + + result = property(_result) + + def make(self, orientation, _edge=None): + """ + Make a decision on a given motion, i.e. vote. Voting recursively propagates + through the delegation graph to all principals who have assigned voting + power to the ``User``. Each delegated vote will be marked as such by + saving the ``Delegation`` as a part of the ``Vote``. + + :param orientation: orientation of the vote, ``Vote.AYE``, ``Vote.NAY`` + or ``Vote.ABSTAIN`` + :returns: the ``Votes`` that has been cast + """ + + def propagating_vote(user, motion, edge): + vote = Vote(user, self.poll, orientation, delegation=edge) + model.meta.Session.add(vote) + log.debug("Decision was made: %s is voting '%s' on %s (via %s)" % (repr(user), + orientation, repr(self.poll.motion.id), + edge if edge else "self")) + return vote + + votes = self.node.propagate(propagating_vote, _edge=_edge) + model.meta.Session.commit() + return votes + + def made(self): + """ + Determine if a given decision was made by the user, i.e. if the user + or one of his/her agents has voted on the motion. + """ + return not self.result == None + + def self_made(self): + """ + Determine if a given decision was made by the user him-/herself. + This does not consider decisions determined by delegation. + """ + + relevant = self.relevant_votes + return len(relevant) == 1 and relevant[0].delegation == None + + def __repr__(self): + return "<Decision(%s,%s)>" % (self.user.user_name, self.motion.id) +# +# def without_vote(self, vote): +# """ +# Return the same decision given that a certain vote had not been +# cast. +# """ +# if not vote in self.relevant_votes: +# return self +# else: +# votes = self.relevant_votes +# votes.remove(vote) +# return Decision(self.user, self.poll, +# at_time=self.at_time, votes=votes) +# + + @classmethod + def for_user(cls, user, instance, at_time=None): # FUUUBARD + """ + Give a list of all decisions the user made within an instance context. + + :param user: The user for which to list ``Decisions`` + :param instance: an ``Instance`` context. + """ + polls = set([vote.poll for vote in user.votes]) + for poll in polls: + yield cls(user, poll, at_time=at_time) + + @classmethod + def for_poll(cls, poll, at_time=None): + """ + Get all decisions that have been made on a poll. + + :param poll: The poll on which to get decisions. + """ + query = model.meta.Session.query(User) + query = query.distinct().join(Vote) + query = query.filter(Vote.poll_id==poll.id) + if at_time: + query = query.filter(Vote.create_time<=at_time) + return [Decision(u, poll, at_time=at_time) for u in query] + + + @classmethod + def replay_decisions(cls, delegation): + """ + For a new delegation, have the principal reproduce all of the agents + past decisions within the delegation scope. This process is not perfect, + since not the full voting history is reproduced, but only the latest + interim result. The resulting decisions should be the same, though. + + :param delegation: The delegation that is newly created. + """ + for decision in cls.for_user(delegation.agent, delegation.scope.instance, + at_time=delegation.create_time): + log.debug("RP: Decision %s" % decision) + if delegation.is_match(decision.motion): + #log.debug("TIME 1: %s" % delegation.create_time) + #log.debug("TIME 2: %s" % decision.poll.end_time) + if decision.poll.is_running(): #at_time=delegation.create_time): + log.debug("RP: Making %s" % decision) + principal_dec = Decision(delegation.principal, decision.motion) + principal_dec.make(decision.result, _edge=delegation) + + + + + diff --git a/adhocracy/lib/democracy/delegation_node.py b/adhocracy/lib/democracy/delegation_node.py new file mode 100644 index 000000000..dc2f4dd2b --- /dev/null +++ b/adhocracy/lib/democracy/delegation_node.py @@ -0,0 +1,199 @@ +from datetime import datetime +import logging + +from sqlalchemy import or_ + +import adhocracy.model as model +from adhocracy.model import Delegation + +log = logging.getLogger(__name__) + +class DelegationNode(object): + """ + A ``DelegationNode`` describes a part of the voting delegation graph + sorrounding a ``Delegateable`` (i.e. a ``Category``, ``Issue`` or + ``Motion``) and a ``User``. + + **TODO:** Developing a good caching strategy for this class would be + useful in order to cache the delegation graph to memcached. + + :param user: The ``User`` at the center of this ``DelegationNode``. + :param delegateable: A ``Delegateable``. + """ + + def __init__(self, user, delegateable): + self.user = user + self.delegateable = delegateable + + def _query_traverse(self, querymod, recurse, at_time): + if not at_time: + at_time = datetime.now() + query = model.meta.Session.query(Delegation) + query = query.filter(Delegation.scope==self.delegateable) + query = query.filter(Delegation.create_time <= at_time) + query = query.filter(or_(Delegation.revoke_time == None, + Delegation.revoke_time > at_time)) + query = querymod(query) + delegations = query.all() + if recurse: + for parent in self.delegateable.parents: + node = DelegationNode(self.user, parent) + delegations += node._query_traverse(querymod, recurse, at_time) + return delegations + + def inbound(self, recurse=True, at_time=None, filter=True): + """ + Retrieve all inbound delegations (i.e. those that the user has received + from other users in order to vote on their behalf) that apply to the + ``Delegateable``. + + :param recurse: if ``True``, search will include delegations on parent + ``Delegateables`` in breadth-first traversal order. + :param at_time: return the delegation graph at the given time, defaults + to the current time. + :returns: list of ``Delegation`` + """ + delegations = self._query_traverse(lambda q: q.filter(Delegation.agent==self.user), + recurse, at_time) + + if filter: + by_principal = dict() + for delegation in set(delegations): + by_principal[delegation.principal] = by_principal.get(delegation.principal, []) + [delegation] + delegations = [self.filter_delegations(ds)[0] for ds in by_principal.values()] + + return delegations + + def transitive_inbound(self, recurse=True, at_time=None, _path=None): + """ + Retrieve inbound delegations recursing through the delegation graph as well + as through the category tree. + + :param recurse: if ``True``, search will include delegations on parent + ``Delegateables`` in breadth-first traversal order. + :param at_time: return the delegation graph at the given time, defaults + to the current time. + :returns: list of ``Delegation`` + """ + if _path == None: + _path = [] + elif self.user in _path: + return [] + else: + _path.append(self.user) + + delegations = self.inbound(recurse=recurse, at_time=at_time) + for delegation in list(delegations): + ddnode = DelegationNode(delegation.principal, self.delegateable) + delegations += ddnode.transitive_inbound(recurse=recurse, at_time=at_time, + _path=_path) + return delegations + + def outbound(self, recurse=True, at_time=None, filter=True): + """ + Retrieve all outbound delegations (i.e. those that the user has given + to other users in order allow them to vote on his/her behalf) that + apply to the ``Delegateable``. + + :param recurse: if ``True``, search will include delegations on parent + ``Delegateables`` in breadth-first traversal order. + :param at_time: return the delegation graph at the given time, defaults + to the current time. + :returns: list of ``Delegation`` + """ + delegations = self._query_traverse(lambda q: q.filter(Delegation.principal==self.user), + recurse, at_time) + + if filter: + by_agent = dict() + for delegation in set(delegations): + by_agent[delegation.agent] = by_agent.get(delegation.agent, []) + [delegation] + delegations = [self.filter_delegations(ds)[0] for ds in by_agent.values()] + + return delegations + + + def propagate(self, callable, _edge=None, _propagation_path=None): + """ + Propagate a given action along the delegation graph *against* its direction, + i.e. from the agent node towards its principal. This is the natural direction + to propagate actions along this network since it allows principals to reproduce + the actions of their agents. + + Propagation will abort on circular dependencies but has no recursion depth limit. + + :param callable: A callable that is to be called on each node. It must take + three arguments, a ``User``, a ``Delegateable`` and the ``Delegation`` + which served as a transitory edge during the last step of the propagation. + :returns: a list of all results produced by the callable. + """ + if not _propagation_path: + _propagation_path = [self] + elif self in _propagation_path: + return [] + else: + _propagation_path.append(self) + + result = [callable(self.user, self.delegateable, _edge)] + for delegation in self.inbound(): + node = DelegationNode(delegation.principal, self.delegateable) + result += node.propagate(callable, + _edge=delegation, + _propagation_path=_propagation_path) + return result + + @classmethod + def detach(cls, user, instance): + """ + Detach a ``User`` from the delegation graph by destroying any + delegations the user might have issued. This operation in necessary + in cases when the user loses voting privileges. Since authorization + is not a part of the voting logic (it is handled on a higher level), + it is important to avoid delegated voting propagation towards the + user. Otherwise the user would still cast votes when pre-existing + delegations match. + + :param user: The user to be detached. + :param instance: Instance for which to detach the graph. + """ + log.info("Purging delegation graph for %s in %s" % (repr(user), repr(instance))) + + now = datetime.now() + query = model.meta.Session.query(Delegation) + query = query.filter(Delegation.agent==user) + query = query.filter(or_(Delegation.revoke_time == None, + Delegation.revoke_time > now)) + for d in query.all(): + if d.scope.instance == instance: + d.revoke_time = now + model.meta.Session.add(d) + model.meta.Session.commit() + + def __repr__(self): + return "<DelegationNode(%s,%s)>" % (self.user.user_name, + self.delegateable.id) + + def __eq__(self, other): + return self.user == other.user and \ + self.delegateable == other.delegateable + + def __ne__(self, other): + return not self.__eq__(other) + + @classmethod + def filter_delegations(cls, delegations): + """ + Given a set of delegations, remove those that are overriden by others. + A delegation is overridden whenever there is another delegation with a + narrower scope that still applies. + + :param delegations: The list of delegations that are to be filtered. + :returns: A filtered list of delegations. + """ + matches = [d for d in delegations] + for d in delegations: + for m in matches: + if m.scope.is_super(d.scope): + matches.remove(m) + return matches + diff --git a/adhocracy/lib/democracy/result.py b/adhocracy/lib/democracy/result.py new file mode 100644 index 000000000..5b600e0b1 --- /dev/null +++ b/adhocracy/lib/democracy/result.py @@ -0,0 +1,103 @@ +from datetime import datetime, timedelta + +import adhocracy.model as model +from adhocracy.model import Motion, Poll +from ..cache import memoize + +from decision import Decision +import tally +from tally import Tally +from state import State + +class Result(object): + + def __init__(self, motion, poll=None): + self.motion = motion + if not poll and len(self.motion.polls): + for p in self.motion.polls: + if not p.end_time: + poll = p + self.poll = poll + self.state = State(self) + if self.poll: + #self.tally = tally.at(poll, poll.votes[0].create_time) + tallies = tally.interval(poll, datetime.now() - timedelta(hours=1), datetime.now()) + self.tally = tallies[-1] + #print "TALLIES", tallies + #print "TALLY ", self.tally + + polling = property(lambda self: self.poll != None) + + def _can_cancel(self): + print "CAN CANCEL: NOT DONE" + return True + + can_cancel = property(_can_cancel) + + def __repr__(self): + return "<Result(%s)>" % self.poll + + + @classmethod + def average_decisions(cls, instance): + """ + The average number of decisions that a ``Poll`` in the given instance + has. For each motion, this only includes the current poll in order to + not accumulate too much historic data. + + :param instance: the ``Instance`` for which to calculate the average. + """ + @memoize('average_decisions', 84600) + def avg_decisions(instance): + query = model.meta.Session.query(Poll) + query = query.join(Motion).filter(Motion.instance_id==instance.id) + query = query.filter(Poll.end_time==None) + decisions = [] + for poll in query: + result = Result(poll.motion, poll=poll) + if result.is_polling: + poll_decisions = len(result.decisions) + if decisions: + decisions.append(poll_decisions) + return sum(decisions)/float(max(1,len(decisions))) + return avg_decisions(instance) + + @classmethod + def critical_motions(cls, instance): + """ + Returns a list of all motions in the given ``Instance``, as a dict key with + a score describing the distance the ``Motion`` has towards making a state + change. + + :param instance: Instance on which to focus + :returns: A ``dict`` of (``Motion``, score) + """ + @memoize('motion-criticalness') + def motion_criticalness(motion): + result = cls(motion) + if not result.poll: + return None + + score = 1 + + # factor 1: missing votes + score += 1.0/float(max(1, result.required_decisions - len(result.decisions))) + + # factor 2: remaining time, i.e. urgency + #t_remain = min(result.activation_delay, datetime.now() - result.state.begin_time) + #score -= timedelta2seconds(t_remain)/float(timedelta2seconds(result.activation_delay)) + + # factor 3: distance to acceptance majority + maj_dist = abs(result.required_majority - result.rel_for) + score *= 1 - (maj_dist/result.required_majority) + + return score * -1 + + q = model.meta.Session.query(Motion).filter(Motion.instance==instance) + scored = {} + for motion in q.all(): + score = motion_criticalness(motion) + if score: + scored[motion] = score + return scored + diff --git a/adhocracy/lib/democracy/state.py b/adhocracy/lib/democracy/state.py new file mode 100644 index 000000000..9dcda1191 --- /dev/null +++ b/adhocracy/lib/democracy/state.py @@ -0,0 +1,101 @@ +from datetime import datetime + +import adhocracy.model as model +from adhocracy.model import Motion +from ..cache import memoize + +import tally +from tally import Tally + +class State(object): + + def __init__(self, result, at_time=None): + self.result = result + self.motion = result.motion + self.instance = self.motion.instance + + if not at_time: + at_time = datetime.now() + self.at_time = at_time + + if self.result.poll: + self.tally = tally.at(self.result.poll, at_time) + + + def _required_majority(self): + """ Majority that is required for the ``Poll`` to succeed. """ + return self.instance.required_majority + + required_majority = property(_required_majority) + + def check_majority(self, tally): + return tally.rel_for > self.required_majority + + majority = property(lambda self: self.check_majority(self.tally)) + + def _required_participation(self): + """ Minimal participation required for a ``Poll`` to succeed. """ + return max(1, int(self.result.average_decisions(self.motion.instance) \ + * self.required_majority)) + + required_participation = property(_required_participation) + + def check_participation(self, tally): + return len(tally) >= self.required_decisions + + participation = property(lambda self: self.check_participation(self.tally)) + + def _activation_delay(self): + return timedelta(days=self.instance.activation_delay) + + activation_delay = property(_activation_delay) + + def period_votes(self, at_time): + consideration_start = at_time - self.activation_delay + return [v for v in self.result.votes if \ + v.create_time >= consideration_start and \ + v.create_time <= at_time] + + def check_activation_step(self, tally): + # add alternatives, dependencies here + return self.check_majority(tally) and \ + self.check_participation(tally) + + def activation_begin_time(self, tally): + last_vote = None + for vote in reversed(self.period_votes(tally.at_time)): + tally = self.result.tally_at(vote.create_time) + if not self.check_activation_step(tally): + return last_vote.create_time if last_vote else None + last_vote = vote + return self.consideration_start + + def check_activating(self, tally): + begin_time = self.activation_begin_time(tally) + return begin_time and begin_time != self.consideration_start + + activating = property(lambda self: self.check_activating(self.at_time)) + + def deactivation_begin_time(self, tally): + for vote in self.period_votes(tally.at_time): + tally = self.result.tally_at(vote.create_time) + if not (self.check_majority(tally) and \ + self.check_participation(tally)) or \ + self.check_activating(tally): + return vote.create_time + return None + + def check_deactivating(self, tally): + return self.deactivation_begin_time(tally) + + deactivating = property(lambda self: self.check_deactivating(self.tally)) + + def _is_active(self): + if self.majority and self.participation and \ + True: #not self.activating: + return True + if False: #self.deactivating: + return True + return False + + active = property(_is_active) diff --git a/adhocracy/lib/democracy/tally.py b/adhocracy/lib/democracy/tally.py new file mode 100644 index 000000000..0884d132d --- /dev/null +++ b/adhocracy/lib/democracy/tally.py @@ -0,0 +1,105 @@ +import adhocracy.model as model +from adhocracy.model import Vote +from decision import Decision + +from pylons import g + + +def _tally_key(poll, time): + """ Unique ID for memcached. """ + return "tally_" + str(poll.id + hash(time.ctime()) * 13) + +def at(poll, at_time): + """ Generate a tally on poll at the given time. """ + key = _tally_key(poll, at_time) + #print "NEED TALLY" + if g.cache: + tally = g.cache.get(key) + if tally: + return tally + tally = Tally.from_decisions(Decision.for_poll(poll, at_time=at_time), + at_time) + #print "MADE TALLY" + if g.cache: + g.cache.set(key, tally) + return tally + +def interval(poll, min_time=None, max_time=None): + """ Generate a list of tallies for the specified interval. """ + query = model.meta.Session.query(Vote.create_time) + query = query.filter(Vote.poll_id==poll.id) + if min_time: + query = query.filter(Vote.create_time>=min_time) + if max_time: + query = query.filter(Vote.create_time<=max_time) + #query = query.order_by(Vote.create_time.desc()) + tallies = [] + if not g.cache: + tallies = [at(poll, t) for t in query] + else: + times = dict([(t[0], _tally_key(poll, t[0])) for t in query]) + cached = g.cache.get_multi(times.values()) + for time in times.keys(): + tally = cached.get(times[time], None) + if not tally: + tally = at(poll, time) + tallies.append(tally) + return sorted(tallies, key=lambda t: t.at_time) + + +class Tally(object): + + def __init__(self, positions, at_time): + self.at_time = at_time + self.positions = positions + + def _filter_positions(self, position): + return filter(lambda p: p == position, self.positions) + + def _get_num_affirm(self): + """ Number of voters who affirmed the motion. """ + return len(self._filter_positions(Vote.AYE)) + + num_affirm = property(_get_num_affirm) + + def _get_num_dissent(self): + """ Number of voters who dissent on the motion. """ + return len(self._filter_positions(Vote.NAY)) + + num_dissent = property(_get_num_dissent) + + def _get_num_abstain(self): + """ Number of voters who abstained from the motion. """ + return len(self._filter_positions(Vote.ABSTAIN)) + + num_abstain = property(_get_num_abstain) + + def _get_rel_for(self): + """ Fraction of decided voters who are for the motion. """ + if self.num_affirm == 0 and self.num_dissent == 0: + return 0.5 + return self.num_affirm / float(max(1, self.num_affirm + self.num_dissent)) + + rel_for = property(_get_rel_for) + + def _get_rel_against(self): + """ Fraction of decided voters who are against the motion. """ + return 1 - self.rel_for + + rel_against = property(_get_rel_against) + + def __len__(self): + return len(self.positions) + + def __repr__(self): + return "<Tally(%s,%s{+%s:-%s:*%s},%s%% v. %s%%)>" % ( + self.at_time if self.at_time else "now", + len(self), self.num_affirm, + self.num_dissent, self.num_abstain, + int(self.rel_for * 100), int(self.rel_against * 100)) + + @classmethod + def from_decisions(cls, decisions, at_time): + decisions = filter(lambda d: d.made(), decisions) + return cls(map(lambda d: d.result, decisions), at_time) + \ No newline at end of file diff --git a/adhocracy/lib/event/__init__.py b/adhocracy/lib/event/__init__.py new file mode 100644 index 000000000..a79016aac --- /dev/null +++ b/adhocracy/lib/event/__init__.py @@ -0,0 +1,23 @@ +import logging +from datetime import datetime + +from pylons import session, tmpl_context as c + +from event import Event +import util +from util import objtoken, EventException +from types import * +import query as q +from rss import rss_feed + +import adhocracy.model as model + +log = logging.getLogger(__name__) + +def emit(event, data, agent, time=None, scopes=[], topics=[]): + if not time: + time = datetime.now() + e = Event(event, data, agent, time, scopes=scopes, topics=topics) + e.persist() + log.debug("Event %s: %s" % (agent.name, unicode(e))) + return e \ No newline at end of file diff --git a/adhocracy/lib/event/event.py b/adhocracy/lib/event/event.py new file mode 100644 index 000000000..2b596fd53 --- /dev/null +++ b/adhocracy/lib/event/event.py @@ -0,0 +1,167 @@ +from datetime import datetime, date, timedelta +import hashlib, logging + +import simplejson + +from pylons.i18n import _ + +from lucene import Document, Field, BooleanQuery, TermQuery, Term, Hit, BooleanClause + +import adhocracy.model as model +from ..search import index +from ..search import entityrefs +import util, types, formatting + +log = logging.getLogger(__name__) + +class Event(object): + + def __init__(self, event, data, agent, time=None, scopes=[], topics=[]): + self.doc = Document() + self.add_field("type", "event") + if not time: + time = datetime.now() + self.time = time + if not agent: + raise EventException("No agent for event %s" % event) + self.event = event + if not agent: + raise EventException("No agent for event %s" % event) + self.agent = agent + self.scopes = scopes + self.topics = topics + self.data = data + + def add_field(self, name, value, store=True, tokenize=False): + store = store and Field.Store.YES or Field.Store.NO + tokenize = tokenize and Field.Index.TOKENIZED or Field.Index.UN_TOKENIZED + self.doc.add(Field(name, unicode(value), store, tokenize)) + + def _set_data(self, data): + """ + For some known formatting elements, replace them with an ID + key-value pair that can later be used to reproduce the entity + for formatting. + """ + self._data = dict([(k, entityrefs.to_ref(v)) for k, v in data.items()]) + + def _get_data(self): + return self._data + + data = property(_get_data, _set_data) + + def format(self, decoder): + """ + Given a dict of formatting options, load the appropriate + entities from the database and format them with the given + decoder. + """ + def decode_kv(kv): + if not isinstance(kv, basestring): + return kv + entity = entityrefs.to_entity(kv) + if not entity: + return _("(Undefined)") + for cls in formatting.FORMATTERS.keys(): + if isinstance(entity, cls): + return decoder(formatting.FORMATTERS[cls], entity) + return kv + args = dict([(k, decode_kv(v)) for k, v in self.data.items()]) + return types.messages.get(self.event)() % args + + def html(self): + return self.format(lambda formatter, value: formatter.html(value)) + + def __unicode__(self): + return self.format(lambda formatter, value: formatter.unicode(value)) + + def persist(self): + """ + Persist the event to the lucene index. + """ + if self.exists(): + self.delete() + self.add_field("time", self._str_time) + self.add_field("event", self.event) + self.add_field("_event_hash", str(hash(self))) + self.add_field("agent", util.objtoken(self.agent)) + self.add_field("agent_id", self.agent.user_name) + self.add_field("data", simplejson.dumps(self.data), + store=True, tokenize=False) + if not self.agent in self.topics: + self.add_field("topic", util.objtoken(self.agent)) + [self.add_field("scope", util.objtoken(s)) for s in self.scopes] + [self.add_field("topic", util.objtoken(t)) for t in self.topics] + index.write_document(self.doc) + + @classmethod + def restore(cls, doc): + """ + Given a document from the lucene index, restore the Event to its + original state as far as possible. + """ + try: + agent_id = doc.getField("agent_id").stringValue() + agent = model.User.find(agent_id, instance_filter=False) + if not agent: + raise util.EventException("Can't restore motion with non-existing agent: %s" % agent_id) + data_json = doc.getField("data").stringValue() + data = simplejson.loads(data_json) + time_flat = doc.getField("time").stringValue() + time = datetime.strptime(time_flat, formatting.DT_FORMAT) + event = doc.getField("event").stringValue() + return Event(event, data, agent, time) + except ValueError, ve: + raise EventException(ve) + except AttributeError, ae: + raise EventException(ae) + + @classmethod + def by_hash(cls, hash): + """ + Return a given event by its identifying hash value. If there is an error + during restoration, this may raise an EventException. + """ + hquery = TermQuery(util.hash_term(hash)) + hits = index.query(hquery) + if len(hits) > 1: + raise util.EventException("Multiple events exist with hash %s" % hash) + for hit in hits: + hit = Hit.cast_(hit) + doc = hit.getDocument() + return cls.restore(doc) + + def exists(self): + """ + Check if the event is already saved to the lucene index. + """ + return not self.by_hash(hash(self)) == None + + def delete(self): + """ + Delete the event from the lucene index if it exists. If it is not + stored yet, nothing will happen. + """ + index.delete_document(util.hash_term(hash(self))) + + def _get_time(self): + return self._time + + def _set_time(self, time): + self._str_time = time.strftime(formatting.DT_FORMAT) + self._time = time + + time = property(_get_time, _set_time) + create_time = property(_get_time, _set_time) # allow default sorters + + def __repr__(self): + return "Event<%s: %s, %s>" % (repr(self.agent), self.event, self.data) + + def __hash__(self): + hash = hashlib.sha1(str(self.time)) + hash.update(self.event) + hash.update(self.agent.user_name) + return int(hash.hexdigest(), 16) + + def __eq__(self, other): + return hash(self) == hash(other) diff --git a/adhocracy/lib/event/formatting.py b/adhocracy/lib/event/formatting.py new file mode 100644 index 000000000..684b99211 --- /dev/null +++ b/adhocracy/lib/event/formatting.py @@ -0,0 +1,86 @@ +from pylons.i18n import _ + +import adhocracy.model as model +from .. import helpers as h + +DT_FORMAT = "%Y%m%d%H%M%S" + +class ObjectFormatter(object): + + def unicode(self, value): + return value + + def html(self, value): + return value + +class DelegateableFormatter(ObjectFormatter): + + def unicode(self, delegateable): + return delegateable.label + + def html(self, delegateable): + return h.delegateable_link(delegateable) + +class IssueFormatter(DelegateableFormatter): + pass + +class MotionFormatter(DelegateableFormatter): + pass + +class CategoryFormatter(DelegateableFormatter): + pass + +class InstanceFormatter(ObjectFormatter): + + def unicode(self, instance): + return instance.label + + def html(self, instance): + return u"<a class='event_instance' href='%s'>%s</a>" % ( + h.instance_url(instance), + instance.label) + +class UserFormatter(ObjectFormatter): + + def unicode(self, user): + return user.name + + def html(self, user): + return h.user_link(user) + +class GroupFormatter(ObjectFormatter): + + def unicode(self, group): + return group.group_name + + def html(self, group): + return self.unicode(group) + +class VoteFormatter(ObjectFormatter): + + def unicode(self, vote): + return {1: _("voted for"), + 0: _("abstained on"), + -1: _("voted against")}[vote.orientation] + + def html(self, value): + return self.unicode(value) + +class CommentFormatter(ObjectFormatter): + + def unicode(self, comment): + return _("comment") + + def html(self, comment): + return "<a href='/comment/r/%d'>%s</a>" % (comment.id, + self.unicode(comment)) + + +FORMATTERS = {model.Vote: VoteFormatter(), + model.Group: GroupFormatter(), + model.User: UserFormatter(), + model.Instance: InstanceFormatter(), + model.Category: CategoryFormatter(), + model.Motion: MotionFormatter(), + model.Issue: IssueFormatter(), + model.Comment: CommentFormatter()} diff --git a/adhocracy/lib/event/query.py b/adhocracy/lib/event/query.py new file mode 100644 index 000000000..57eb44217 --- /dev/null +++ b/adhocracy/lib/event/query.py @@ -0,0 +1,78 @@ +import logging + +from lucene import BooleanQuery, TermQuery, Term, Hit, BooleanClause, QueryParser + +from event import Event +import util + +log = logging.getLogger(__name__) + +def _and(*pieces): + if len(pieces) == 1: + pieces = pieces[0] + if isinstance(pieces, basestring): + return pieces + if not len(pieces): + return "" + return "(" + " AND ".join(pieces) + ")" + +def _or(*pieces): + if len(pieces) == 1: + pieces = pieces[0] + if isinstance(pieces, basestring): + return pieces + if not len(pieces): + return "" + return "(" + " OR ".join(pieces) + ")" + +def _must(q): + return "+" + q + +def _not(q): + return "-" + q + +def agent(user): + return "agent:%s" % util.objtoken(user) + +def topic(obj): + return "topic:%s" % util.objtoken(obj) + +def scope(obj): + return "scope:%s" % util.objtoken(obj) + +def run(query, sort_time=True, sort_time_desc=True, + from_time=None, to_time=None): + import adhocracy.lib.search.index as index + + try: + bquery = BooleanQuery() + tquery = TermQuery(Term("type", "event")) + bquery.add(BooleanClause(tquery, BooleanClause.Occur.MUST)) + if len(query.strip()): + query = QueryParser("foo", index.get_analyzer()).parse(query) + #log.debug("Compiled query: %s" % query.toString()) + else: + query = TermQuery(Term("schnasel", "0xDEADBEEF")) + bquery.add(BooleanClause(query, BooleanClause.Occur.MUST)) + + log.debug("Event query: %s" % bquery) + + # TODO: run most of this in lucene, not here. + hits = index.query(bquery) + evts = [] + for hit in hits: + hit = Hit.cast_(hit) + evt = Event.restore(hit.getDocument()) + if evt: + evts.append(evt) + if from_time: + evts = [e for e in evts if e.time >= from_time] + if to_time: + evts = [e for e in evts if e.time <= to_time] + if sort_time: + evts = sorted(evts, key=lambda e: e.time, reverse=sort_time_desc) + except TypeError, e: + log.warn(e) + raise e + return [] + return evts \ No newline at end of file diff --git a/adhocracy/lib/event/rss.py b/adhocracy/lib/event/rss.py new file mode 100644 index 000000000..712f7ee0f --- /dev/null +++ b/adhocracy/lib/event/rss.py @@ -0,0 +1,20 @@ +from pylons import response, tmpl_context as c + +from .. import helpers as h +from .. import templating + + +from webhelpers.feedgenerator import Rss201rev2Feed, RssUserland091Feed + +def rss_feed(events, name, link, description): + rss = Rss201rev2Feed(name, link, description) + def event_item(event): + rss.add_item(title="%s %s" % (event.agent.name, unicode(event)), + link=h.instance_url(c.instance), + pubdate=event.time, + description="%s %s" % (h.user_link(event.agent), + event.html()), + author_name=event.agent.name) + response.content_type = 'application/rss+xml' + templating.NamedPager('rss', events, event_item, count=100).here() + return rss.writeString('utf-8') \ No newline at end of file diff --git a/adhocracy/lib/event/stats.py b/adhocracy/lib/event/stats.py new file mode 100644 index 000000000..d24b8fa47 --- /dev/null +++ b/adhocracy/lib/event/stats.py @@ -0,0 +1,68 @@ +import logging +from datetime import datetime + +from adhocracy import model +from ..cache import memoize +from ..util import timedelta2seconds + +import query + +log = logging.getLogger(__name__) + +BASE = datetime(2009, 8, 12, 13, 0, 0) + +def activity(q, from_time=None, to_time=None): + events = query.run(q, from_time=from_time, to_time=to_time) + events = reversed(events) + + if not to_time: + to_time = datetime.now() + if not from_time: + from_time = datetime.min + + events = [e for e in events if e.time >= from_time \ + and e.time < to_time] + + def evt_value(e): + t = max(1, timedelta2seconds(e.time - BASE)) + return 9001.0/float(t) # over 9000 + + act = sum(map(evt_value, events)) + log.debug("Activity %s: %s" % (q, act)) + + return act * -1 + +@memoize('motion_activity') +def motion_activity(motion, from_time=None, to_time=None): + a = activity(query.topic(motion), from_time=from_time, to_time=to_time) + return a + +@memoize('issue_activity') +def issue_activity(issue, from_time=None, to_time=None): + a = activity(query.topic(issue), from_time=from_time, to_time=to_time) + a += sum(map(lambda m: motion_activity(m, from_time=from_time, to_time=to_time), + issue.motions)) + return a + +@memoize('category_activity') +def category_activity(category, from_time=None, to_time=None): + a = activity(query.topic(category), from_time=from_time, + to_time=to_time) + for child in category.children: + if isinstance(child, model.Category): + a += category_activity(child, from_time=from_time, + to_time=to_time) + elif isinstance(child, model.Issue): + a += issue_activity(child, from_time=from_time, + to_time=to_time) + return a + +@memoize('instance_activity') +def instance_activity(instance, from_time=None, to_time=None): + a = activity(query.scope(instance), from_time=from_time, to_time=to_time) + return a + +@memoize('user_activity') +def user_activity(user, from_time=None, to_time=None): + a = activity(query._or(query.topic(user), query.agent(user)), from_time=from_time, to_time=to_time) + return a \ No newline at end of file diff --git a/adhocracy/lib/event/types.py b/adhocracy/lib/event/types.py new file mode 100644 index 000000000..f0eedb818 --- /dev/null +++ b/adhocracy/lib/event/types.py @@ -0,0 +1,74 @@ +from pylons.i18n import _ + +T_USER_CREATE = u"t_account_create" +T_USER_EDIT = u"t_account_edit" +T_USER_ADMIN_EDIT = u"t_account_admin_edit" + +T_INSTANCE_CREATE = u"t_instance_create" +T_INSTANCE_EDIT = u"t_instance_edit" +T_INSTANCE_DELETE = u"t_instance_delete" +T_INSTANCE_JOIN = u"t_instance_join" +T_INSTANCE_LEAVE = u"t_instance_leave" +T_INSTANCE_FORCE_LEAVE = u"t_instance_force_leave" +T_INSTANCE_MEMBERSHIP_UPDATE = u"t_instance_membership_update" + +T_ISSUE_CREATE = u"t_issue_create" +T_ISSUE_EDIT = u"t_issue_edit" +T_ISSUE_DELETE = u"t_issue_delete" + +T_MOTION_CREATE = u"t_motion_create" +T_MOTION_EDIT = u"t_motion_edit" +T_MOTION_STATE_REDRAFT = u"t_motion_state_draft" +T_MOTION_STATE_VOTING = u"t_motion_state_voting" +T_MOTION_DELETE = u"t_motion_delete" + +T_EDITOR_ADD = u"t_editor_add" +T_EDITOR_REMOVE = u"t_editor_remove" + +T_CATEGORY_CREATE = u"t_category_create" +T_CATEGORY_EDIT = u"t_category_edit" +T_CATEGORY_DELETE = u"t_category_delete" + +T_COMMENT_CREATE = u"t_comment_create" +T_COMMENT_EDIT = u"t_comment_edit" +T_COMMENT_DELETE = u"t_comment_delete" + +T_DELEGATION_CREATE = u"t_delegation_create" +T_DELEGATION_REVOKE = u"t_delegation_revoke" + +T_VOTE_CAST = u"t_vote_cast" + +T_TEST = u"t_test" + +messages = { + T_USER_CREATE: lambda: _(u"signed up"), + T_USER_EDIT: lambda: _(u"edited their profile"), + T_USER_ADMIN_EDIT: lambda: _(u"edited %(user)ss profile"), + T_INSTANCE_CREATE: lambda: _(u"founded the %(instance)s Adhocracy"), + T_INSTANCE_EDIT: lambda: _(u"updated the %(instance)s Adhocracy"), + T_INSTANCE_DELETE: lambda: _(u"deleted the %(instance)s Adhocracy"), + T_INSTANCE_JOIN: lambda: _(u"joined %(instance)s"), + T_INSTANCE_LEAVE: lambda: _(u"left %(instance)s"), + T_INSTANCE_FORCE_LEAVE: lambda: _(u"was forced to leave %(instance)s by %(user)s"), + T_INSTANCE_MEMBERSHIP_UPDATE: lambda: _(u"now is a %(group)s within %(instance)s"), + T_ISSUE_CREATE: lambda: _(u"created %(issue)s"), + T_ISSUE_EDIT: lambda: _(u"edited %(issue)s"), + T_ISSUE_DELETE: lambda: _(u"deleted %(issue)s"), + T_MOTION_CREATE: lambda: _(u"created %(motion)s"), + T_MOTION_EDIT: lambda: _(u"edited %(motion)s"), + T_MOTION_STATE_REDRAFT: lambda: _(u"re-drafted %(motion)s"), + T_MOTION_STATE_VOTING: lambda: _(u"called a vote on %(motion)s"), + T_MOTION_DELETE: lambda: _(u"deleted %(motion)s"), + T_EDITOR_ADD: lambda: _(u"named %(user)s as an editor for %(motion)s"), + T_EDITOR_REMOVE: lambda: _(u"removed %(user)s from the editors of %(motion)s"), + T_CATEGORY_CREATE: lambda: _(u"created the category %(category)s in %(parent)s"), + T_CATEGORY_EDIT: lambda: _(u"updated the category %(category)s"), + T_CATEGORY_DELETE: lambda: _(u"deleted the category %(category)s"), + T_COMMENT_CREATE: lambda: _(u"created a %(comment)s on %(delegateable)s"), + T_COMMENT_EDIT: lambda: _(u"edited a %(comment)s on %(delegateable)s"), + T_COMMENT_DELETE: lambda: _(u"deleted a %(comment)s from %(delegateable)s"), + T_DELEGATION_CREATE: lambda: _(u"delegated voting on %(scope)s to %(agent)s"), + T_DELEGATION_REVOKE: lambda: _(u"revoked their delegation on %(scope)s to %(agent)s"), + T_VOTE_CAST: lambda: _(u"%(vote)s %(motion)s"), + T_TEST: lambda: _(u"test %(test)s") + } diff --git a/adhocracy/lib/event/util.py b/adhocracy/lib/event/util.py new file mode 100644 index 000000000..7ffb6a7f6 --- /dev/null +++ b/adhocracy/lib/event/util.py @@ -0,0 +1,42 @@ +from datetime import datetime +import logging + +from lucene import Term + +import adhocracy.model as model + +log = logging.getLogger(__name__) + + +class EventException(Exception): + pass + +class EventFormattingException(EventException): + pass + +def hash_term(hash): + """ + Creates a lucene query term that uniquely searches for this item. + """ + return Term("_event_hash", str(hash)) + +def objtoken(obj): + """ + Encode some known object types for use in event topic, scope, etc. + Maybe this could be replaced by generic hashing? The current method + has the advantage of allowing admins to hand-write queries at some + point. + """ + if isinstance(obj, model.User): + return "user.%s" % obj.user_name.lower() + elif isinstance(obj, model.Category): + return "category.%s" % obj.id.lower() + elif isinstance(obj, model.Motion): + return "motion.%s" % obj.id.lower() + elif isinstance(obj, model.Issue): + return "issue.%s" % obj.id.lower() + elif isinstance(obj, model.Delegation): + return "delegation.%s" % obj.id + elif isinstance(obj, model.Instance): + return "instance.%s" % obj.key.lower() + return str(abs(hash(obj))) \ No newline at end of file diff --git a/adhocracy/lib/helpers.py b/adhocracy/lib/helpers.py new file mode 100644 index 000000000..c47640417 --- /dev/null +++ b/adhocracy/lib/helpers.py @@ -0,0 +1,134 @@ +"""Helper functions + +Consists of functions to typically be used within templates, but also +available to Controllers. This module is available to templates as 'h'. +""" +# Import helpers as desired, or define your own, ie: +#from webhelpers.html.tags import checkbox, password + +import urllib, hashlib, cgi + +from pylons import tmpl_context as c +from pylons import request +from pylons.i18n import add_fallback, get_lang, set_lang, gettext, _ + +import authorization +import karma + +import adhocracy.model as model + +from text.i18n import relative_date, relative_time +from xsrf import url_token, field_token +from karma import user_score as user_karma + +from webhelpers.pylonslib import Flash as _Flash +import webhelpers.text as text +flash = _Flash() + + +def breadcrumbs(delegateable, id=None): + import adhocracy.model as model + + if not delegateable: + return _("Adhocracy") + + if not id: + id = delegateable.id + + if len(delegateable.parents): + link = "<a href='/d/%s'>%s</a>" % (id, text.truncate(delegateable.label, length=30, whole_word=True)) + link = breadcrumbs(delegateable.parents[0]) + " » " + link + else: + link = "<a href='/category/%s'>%s</a>" % (delegateable.instance.root.id, + text.truncate(delegateable.instance.label, length=30, whole_word=True)) + return link + +def has_permission(permission): + p = authorization.has_permission(permission) + return p.is_met(request.environ) + +def immutable_motion_message(): + return _("This motion is currently being voted on and cannot be modified.") + +def user_link(user, size=16, link=None): + if not link: + link = "/user/%s" % user.user_name + return "<a href='%s' class='user_link'><img class='user_icon' src='%s' alt="" /> %s</a><sup>%s</sup>" % ( + instance_url(c.instance, path=link), + gravatar_url(user, size=size), + cgi.escape(user.name), + karma.user_score(user)) + +def delegateable_link(delegateable, icon=True, link=True): + text = "" + if icon: + if isinstance(delegateable, model.Motion): + text = "<img class='user_icon' src='%s/img/icons/motion_16.png' /> " % instance_url(None, path='') + elif isinstance(delegateable, model.Issue): + text = "<img class='user_icon' src='%s/img/icons/issue_16.png' /> " % instance_url(None, path='') + elif isinstance(delegateable, model.Category): + text = "<img class='user_icon' src='%s/img/icons/stack_16.png' /> " % instance_url(None, path='') + text += cgi.escape(delegateable.label) + if link and not delegateable.delete_time: + if isinstance(delegateable, model.Motion): + text = "<a href='%s' class='dgb_link'>%s</a>" % ( + instance_url(delegateable.instance, path='/motion/%s' % delegateable.id), text) + elif isinstance(delegateable, model.Issue): + text = "<a href='%s' class='dgb_link'>%s</a>" % ( + instance_url(delegateable.instance, path='/issue/%s' % delegateable.id), text) + elif isinstance(delegateable, model.Category): + text = "<a href='%s' class='dgb_link'>%s</a>" % ( + instance_url(delegateable.instance, path='/category/%s' % delegateable.id), text) + return text + +def contains_delegations(user, delegateable, recurse=True): + for delegation in user.agencies: + if not delegation.revoke_time and (delegation.scope == delegateable or \ + (delegation.scope.is_sub(delegateable) and recurse)): + return True + for delegation in user.delegated: + if not delegation.revoke_time and (delegation.scope == delegateable or \ + (delegation.scope.is_sub(delegateable) and recurse)): + return True + return False + + + +default_gravatar = "http://www.somewhere.com/homsar.jpg" +def gravatar_url(user, size=32): + # construct the url + gravatar_url = "http://www.gravatar.com/avatar.php?" + gravatar_url += urllib.urlencode({'gravatar_id':hashlib.md5(user.email).hexdigest(), + #'default':default_gravatar, + 'size': str(size)}) + return gravatar_url + +def user_or_you(user): + if user == c.user: + return _("You") + return "<a href='/user/%s'>%s</a>" % (user.user_name, cgi.escape(user.name)) + +def instance_url(instance, path="/"): + subdomain = "" + if instance: # don't ask + subdomain = instance.key + "." + return str("http://%s%s%s" % (subdomain, + request.environ['adhocracy.active.domain'], + path)) + +def add_meta(key, value): + if not c.html_meta: + c.html_meta = dict() + c.html_meta[key] = value + +def add_rss(title, link): + if not c.html_link: + c.html_link = [] + c.html_link.append({'title': title, + 'href': link, + 'rel': 'alternate', + 'type': 'application/rss+xml'}) + +def rss_link(link): + return "<a class='rss_link' href='%s'><img src='/img/rss.png' /></a>" % link + \ No newline at end of file diff --git a/adhocracy/lib/install.py b/adhocracy/lib/install.py new file mode 100644 index 000000000..9990ce16e --- /dev/null +++ b/adhocracy/lib/install.py @@ -0,0 +1,102 @@ +import logging + +import adhocracy.model as model + +log = logging.getLogger(__name__) + + +def mk_group(name, code): + group = model.Group.by_code(unicode(code)) + if not group: + log.debug("Creating group: %s" % name) + group = model.Group(unicode(name), unicode(code)) + model.meta.Session.add(group) + else: + group.group_name = unicode(name) + return group + +def mk_perm(name, *groups): + perm = model.Permission.find(unicode(name)) + if not perm: + log.debug("Creating permission: %s" % name) + perm = model.Permission(unicode(name)) + model.meta.Session.add(perm) + perm.groups = list(groups) + return perm + +def setup_entities(): + #model.meta.Session.begin() + model.meta.Session.commit() + + admin = model.User.find(u"admin") + if not admin: + admin = model.User(u"admin", u"admin@null.naught", u"password") + log.debug("Making admin user..") + model.meta.Session.add(admin) + + model.meta.Session.commit() + + admins = mk_group("Administrator", model.Group.CODE_ADMIN) + supervisor = mk_group("Supervisor", model.Group.CODE_SUPERVISOR) + voter = mk_group("Voter", model.Group.CODE_VOTER) + observer = mk_group("Observer", model.Group.CODE_OBSERVER) + default = mk_group("Default", model.Group.CODE_DEFAULT) + anonymous = mk_group("Anonymous", model.Group.CODE_ANONYMOUS) + + model.meta.Session.commit() + # ADD EACH NEW PERMISSION HERE + + mk_perm("vote.cast", voter) + mk_perm("instance.index", anonymous) + mk_perm("instance.view", anonymous) + mk_perm("instance.create", default) + mk_perm("instance.admin", supervisor) + mk_perm("instance.join", default) + mk_perm("instance.leave", default) + mk_perm("instance.news", anonymous) + mk_perm("instance.delete", admins) + mk_perm("category.create", observer) + mk_perm("category.edit", observer) + mk_perm("category.delete", observer) + mk_perm("category.view", anonymous) + mk_perm("comment.view", anonymous) + mk_perm("comment.create", observer) + mk_perm("comment.edit", observer) + mk_perm("comment.delete", observer) + mk_perm("karma.give", observer) + mk_perm("motion.create", observer) + mk_perm("motion.edit", observer) + mk_perm("motion.delete", observer) + mk_perm("motion.view", anonymous) + mk_perm("poll.create", observer) + mk_perm("poll.abort", observer) + mk_perm("issue.create", observer) + mk_perm("issue.edit", observer) + mk_perm("issue.delete", observer) + mk_perm("issue.view", anonymous) + mk_perm("user.manage", admins) + mk_perm("user.edit", default) + mk_perm("user.view", anonymous) + mk_perm("delegation.view", anonymous) + mk_perm("global.admin", admins) + mk_perm("global.member", admins) + + model.meta.Session.commit() + # END PERMISSIONS LIST + + + observer.permissions = observer.permissions + anonymous.permissions + voter.permissions = voter.permissions + observer.permissions + supervisor.permissions = supervisor.permissions + voter.permissions + admins.permissions = admins.permissions + supervisor.permissions + + a = model.Membership(admin, None, admins, approved=True) + model.meta.Session.add(a) + d = model.Membership(admin, None, default, approved=True) + model.meta.Session.add(d) + + model.meta.Session.commit() + + import instance as libinstance + if not model.Instance.find(u"test"): + libinstance.create(u"test", u"Test Instance", admin) diff --git a/adhocracy/lib/instance/__init__.py b/adhocracy/lib/instance/__init__.py new file mode 100644 index 000000000..a9819a22c --- /dev/null +++ b/adhocracy/lib/instance/__init__.py @@ -0,0 +1,49 @@ +from decorator import decorator + +from pylons import tmpl_context as c +from pylons.controllers.util import abort +from pylons.i18n import _ + +import adhocracy.model as model + +from discriminator import setup_discriminator + +def _RequireInstance(f, *a, **kw): + if not c.instance: + abort(404, _("This action is only available in an instance context.")) + else: + return f(*a, **kw) + +RequireInstance = decorator(_RequireInstance) + +def create(key, label, user): + #model.meta.Session.begin_nested() + + supervisor_grp = model.Group.by_code(model.Group.CODE_SUPERVISOR) + instance = model.Instance(key, label, user) + #instance.default_group = model.Group.by_code(model.Group.INSTANCE_DEFAULT) + membership = model.Membership(user, instance, supervisor_grp, approved=True) + root = model.Category(instance, label, user) + + model.meta.Session.add(instance) + model.meta.Session.commit() + model.meta.Session.add(membership) + model.meta.Session.add(root) + model.meta.Session.commit() + #model.meta.Session.begin_nested() + model.meta.Session.refresh(root) + model.meta.Session.refresh(instance) + instance.root = root + model.meta.Session.add(root) + model.meta.Session.add(instance) + + meta = model.Category(instance, u"Meta", user) + meta.parents.append(root) + + model.meta.Session.add(meta) + + model.meta.Session.commit() + + + return instance + \ No newline at end of file diff --git a/adhocracy/lib/instance/discriminator.py b/adhocracy/lib/instance/discriminator.py new file mode 100644 index 000000000..50910617e --- /dev/null +++ b/adhocracy/lib/instance/discriminator.py @@ -0,0 +1,53 @@ +import logging + +import adhocracy.model as model +from pylons import response + +log = logging.getLogger(__name__) + +class InstanceDiscriminatorMiddleware(object): + + TRUNCATE_PREFIX = "www." + + def __init__(self, app, domains): + self.app = app + self.domains = domains + log.debug("VHosts: %s." % ", ".join(domains)) + + def __call__(self, environ, start_response): + host = environ.get('HTTP_HOST', "") + environ['HTTP_HOST_ORIGINAL'] = host + port = None + if ':' in host: + (host, port) = host.split(':') + if host.startswith(self.TRUNCATE_PREFIX): + host = host[len(self.TRUNCATE_PREFIX):] + + environ['adhocracy.active.domain'] = self.domains[0] + for domain in self.domains: + if host.endswith(domain): + host = host[:len(host)-len(domain)] + if port: + environ['adhocracy.active.domain'] = environ['HTTP_HOST'] = domain + ':' + port + else: + environ['adhocracy.active.domain'] = environ['HTTP_HOST'] = domain + #environ['HTTP_HOST'] = '.' + domain repoze auth tkt cookie hack + break + instance_key = host.strip('. ').lower() + if len(instance_key): + #log.debug("Request instance: %s" % instance_key) + instance = model.Instance.find(instance_key) + if not instance: + log.debug("No such instance: %s, defaulting!" % instance_key) + else: + model.filter.setup_thread(instance) + try: + return self.app(environ, start_response) + finally: + model.filter.setup_thread(None) + + +def setup_discriminator(app, config): + domains = config.get('adhocracy.domains', '') + domains = [d.strip() for d in domains.split(',')] + return InstanceDiscriminatorMiddleware(app, domains) \ No newline at end of file diff --git a/adhocracy/lib/karma/__init__.py b/adhocracy/lib/karma/__init__.py new file mode 100644 index 000000000..30c907026 --- /dev/null +++ b/adhocracy/lib/karma/__init__.py @@ -0,0 +1,21 @@ +from pylons import tmpl_context as c + +from sqlalchemy.orm.exc import NoResultFound + +from adhocracy import model +from ..cache import memoize + +import threshold +from scores import * + +@memoize('user_comment_position') +def position(comment, user): + q = model.meta.Session.query(model.Karma) + q = q.filter(model.Karma.comment==comment) + q = q.filter(model.Karma.donor==user) + try: + return q.one() + except NoResultFound: + return None + except: + return q.all()[0] diff --git a/adhocracy/lib/karma/scores.py b/adhocracy/lib/karma/scores.py new file mode 100644 index 000000000..35d7260dd --- /dev/null +++ b/adhocracy/lib/karma/scores.py @@ -0,0 +1,45 @@ +from pylons import tmpl_context as c + +from adhocracy import model +from ..cache import memoize + +import math + +@memoize('comment_score') +def comment_score(comment, recurse=False): + score = 1 + if recurse: + score += sum(map(lambda c: comment_score(c, recurse=True), comment.replies)) + q = model.meta.Session.query(model.Karma) + q = q.filter(model.Karma.comment==comment) + try: + karmas = q.all() + return score + sum([k.value for k in karmas]) + except: + return score + +def user_score(user): + @memoize('user_instance_score') + def _user_score(user, instance): + q = model.meta.Session.query(model.Karma) + q = q.filter(model.Karma.recipient==user) + q = q.filter(model.Karma.donor!=user) + karmas = q.all() + if instance: + karmas = [k for k in karmas if k.comment.topic.instance == c.instance] + score = 1 + sum([k.value for k in karmas]) + return max(0, score) + return _user_score(user, c.instance) + + +# +# Unadapted Ruby, either find a python lib with p distribution tables or +# hardcode the "power" argument. +# +def wilson_confidence_interval(pos, n, power): + if n == 0: + return 0 + + z = Statistics2.pnormaldist(1-power/2) + phat = 1.0*pos/n + (phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n) \ No newline at end of file diff --git a/adhocracy/lib/karma/threshold.py b/adhocracy/lib/karma/threshold.py new file mode 100644 index 000000000..7639dacb1 --- /dev/null +++ b/adhocracy/lib/karma/threshold.py @@ -0,0 +1,34 @@ +from pylons import config, tmpl_context as c +from pylons.i18n import _ + +import scores + +def limit(permission_name): + cfg = config.get("adhocracy.karma.%s" % permission_name) + if cfg: + return int(cfg) + return 0 + +def has(user, permission_name): + if scores.user_score(user) >= limit(permission_name): + return True + return False + +def message(permission_name): + messages = {'category.create': _("create a category"), + 'category.edit': _("edit this category"), + 'category.delete': _("delete this category"), + 'comment.create': _("reply in a comment"), + 'comment.edit': _("edit this comment"), + 'comment.delete': _("delete this comment"), + 'karma.give': _("rate this comment"), + 'motion.create': _("create a motion"), + 'motion.edit': _("edit this motion"), + 'motion.beginpoll': _("call for a vote"), + 'motion.endpoll': _("cancel a vote"), + 'motion.delete': _("delete a motion"), + 'issue.create': _("create an issue"), + 'issue.edit': _("edit this issue"), + 'issue.delete': _("delete this issue")} + return _("You need %s karma to %s") % (limit(permission_name), + messages.get(permission_name, _("do this"))) \ No newline at end of file diff --git a/adhocracy/lib/mail.py b/adhocracy/lib/mail.py new file mode 100644 index 000000000..5d3a630f2 --- /dev/null +++ b/adhocracy/lib/mail.py @@ -0,0 +1,27 @@ +from datetime import datetime, timedelta +import sha +import smtplib + +from pylons.i18n.translation import * +from pylons import session, config, request + +def to_mail(to_name, to_email, subject, body): + email_from = config['adhocracy.email.from'] + smtp_server = config['smtp_server'] + + header = "From: Adhocracy <%s>\r\n" % email_from \ + + "To: %s <%s>\r\n" % (to_name, to_email) \ + + "Date: %s\r\n" % datetime.utcnow().ctime() \ + + "X-Mailer: adhocracy\r\n" \ + + "Subject: %s\r\n\r\n" % subject + body = _("Hi %s,") % to_name \ + + "\r\n\r\n%s\r\n\r\n" % body \ + + _("Cheers,\r\n\r\n the Adhocracy Team\r\n") + + server = smtplib.SMTP(smtp_server) + #server.set_debuglevel(1) + server.sendmail(email_from, [to_email], header + body) + server.quit() + +def to_user(to_user, subject, body): + return to_mail(to_user.name, to_user.email, subject, body) \ No newline at end of file diff --git a/adhocracy/lib/search/__init__.py b/adhocracy/lib/search/__init__.py new file mode 100644 index 000000000..cc2b90e9c --- /dev/null +++ b/adhocracy/lib/search/__init__.py @@ -0,0 +1,37 @@ +import logging + +from pylons import config + +import lucene + +import adhocracy.model as model +import adhocracy.model.hooks as hooks + +import index +import query +from indexers import * + +DEFAULT_INDEX_DIR = "%(here)s/data/index" + +log = logging.getLogger(__name__) + +def index_dir(): + return config.get("lucene.index.dir", DEFAULT_INDEX_DIR % config) + +def setup_search(): + index.vm = lucene.initVM(lucene.CLASSPATH) + index.store = lucene.FSDirectory.getDirectory(index_dir()) + + index.write_document(lucene.Document()) + + log.info("Started pyLucene %s, index at %s" % (lucene.VERSION, index_dir())) + + register_indexer(model.Category, CategoryIndexer) + register_indexer(model.Issue, IssueIndexer) + register_indexer(model.Motion, MotionIndexer) + register_indexer(model.User, UserIndexer) + register_indexer(model.Comment, CommentIndexer) + + +def attach_thread(): + index.vm.attachCurrentThread() \ No newline at end of file diff --git a/adhocracy/lib/search/entityrefs.py b/adhocracy/lib/search/entityrefs.py new file mode 100644 index 000000000..4cba15bf3 --- /dev/null +++ b/adhocracy/lib/search/entityrefs.py @@ -0,0 +1,42 @@ +import logging +import re + +from adhocracy import model + +log = logging.getLogger(__name__) + +FORMAT = re.compile("@\[(.*):(.*)\]") + +TYPES = [model.Vote, + model.User, + model.Group, + model.Permission, + model.Comment, + model.Delegation, + model.Category, + model.Issue, + model.Motion, + model.Instance] + +def _index_name(cls): + return cls.__tablename__ + +def to_ref(entity): + for cls in TYPES: + if isinstance(entity, cls): + return "@[%s:%s]" % (_index_name(entity), str(entity._index_id())) + return entity + +def to_entity(ref): + match = FORMAT.match(ref) + if not match: + return ref + for cls in TYPES: + if match.group(1) == _index_name(cls): + entity = cls.find(match.group(2), + instance_filter=False) + #log.debug("entityref reloaded: %s" % repr(entity)) + return entity + log.warn("No typeformatter for: %s" % ref) + return ref + diff --git a/adhocracy/lib/search/index.py b/adhocracy/lib/search/index.py new file mode 100644 index 000000000..230d0f7ee --- /dev/null +++ b/adhocracy/lib/search/index.py @@ -0,0 +1,50 @@ +from threading import Lock + +import lucene + +store = None +_analyzer = None + +index_lock = Lock() + +def get_writer(): + return lucene.IndexWriter(store, get_analyzer()) + +def get_reader(): + return lucene.IndexReader.open(store) + +def get_searcher(): + return lucene.IndexSearcher(store) + +def write_document(doc): + index_lock.acquire() + try: + writer = get_writer() + writer.addDocument(doc) + writer.optimize() + writer.close() + finally: + index_lock.release() + +def delete_document(term): + index_lock.acquire() + try: + reader = get_reader() + reader.deleteDocuments(term) + reader.close() + finally: + index_lock.release() + +def query(q): + index_lock.acquire() + try: + return get_searcher().search(q) + finally: + index_lock.release() + +def get_analyzer(): + global _analyzer + if not _analyzer: + # TODO: obviously refine tokenization and shit + _analyzer = lucene.StandardAnalyzer() + return _analyzer diff --git a/adhocracy/lib/search/indexers.py b/adhocracy/lib/search/indexers.py new file mode 100644 index 000000000..123efc9c7 --- /dev/null +++ b/adhocracy/lib/search/indexers.py @@ -0,0 +1,219 @@ +import logging +from datetime import datetime + +from lucene import Field, Document, Term + +from adhocracy import model +from adhocracy.model import hooks +from .. import text + +import index +import entityrefs + +class Indexer(object): + + def __init__(self, entity, doc=None, boost=1.0): + self.entity = entity + self.boost = boost + self._doc = doc + + def _get_doc(self): + if not self._doc: + self._doc = Document() + self._doc.add(Field("id", str(self.entity._index_id()), + Field.Store.YES, Field.Index.UN_TOKENIZED)) + self._doc.add(Field("ref", entityrefs.to_ref(self.entity), + Field.Store.YES, Field.Index.UN_TOKENIZED)) + self._doc.add(Field("type", entityrefs._index_name(self.entity), + Field.Store.YES, Field.Index.UN_TOKENIZED)) + self._doc.add(Field("entity", "true", + Field.Store.YES, Field.Index.UN_TOKENIZED)) + return self._doc + + doc = property(_get_doc) + + def include_date(self, key, ts, boost): + if not ts: + ts = datetime.now() # for newly persited entities + ts_str = ts.strftime('%Y%m%d') + f = Field(key, ts_str, Field.Store.NO, + Field.Index.UN_TOKENIZED) + f.setBoost(boost) + self.doc.add(f) + + def delete(self): + index.delete_document(Term("ref", entityrefs.to_ref(self.entity))) + + def add(self): + self.serialize() + index.write_document(self.doc) + + +class UserIndexer(Indexer): + + def serialize(self, partial=False): + f = Field("user", self.entity.user_name, + Field.Store.NO, Field.Index.TOKENIZED) + f.setBoost(self.boost) + self.doc.add(f) + + if self.entity.display_name: + f = Field("user", self.entity.display_name, + Field.Store.NO, Field.Index.TOKENIZED) + f.setBoost(self.boost) + self.doc.add(f) + if not partial: + self.include_date("create_time", self.entity.create_time, + self.boost * 0.5) + if self.entity.bio: + f = Field("description", text.plain(self.entity.bio), + Field.Store.NO, Field.Index.TOKENIZED) + f.setBoost(self.boost * 0.5) + self.doc.add(f) + + for instance in self.entity.instances: + f = Field("instance", instance.key, + Field.Store.YES, Field.Index.UN_TOKENIZED) + f.setBoost(self.boost) + self.doc.add(f) + +class CommentIndexer(Indexer): + + def serialize(self, partial=False, depth=3): + if self.entity.delete_time or self.entity.topic.delete_time: + self.delete() + return + + if not self.entity.canonical: + self.boost = self.boost * 0.9 + + creator = UserIndexer(self.entity.creator, doc=self.doc, + boost=self.boost*0.5) + creator.serialize(partial=True) + + f = Field("canonical", "true" if self.entity.canonical else "false", + Field.Store.NO, Field.Index.UN_TOKENIZED) + f.setBoost(self.boost * 0.1) + self.doc.add(f) + + for revision in self.entity.revisions[:depth]: + self.include_date("create_time", self.entity.create_time, + self.boost * 0.5) + + f = Field("description", text.plain(revision.text), + Field.Store.NO, Field.Index.TOKENIZED) + f.setBoost(self.boost * 0.9) + self.doc.add(f) + + user = UserIndexer(revision.user, doc=self.doc, + boost=self.boost*0.5) + user.serialize(partial=True) + + if not partial: + didx = DelegateableIndexer(self.entity.topic, doc=self.doc, + boost=self.boost * 0.5) + didx.serialize(partial=True) + +class DelegateableIndexer(Indexer): + + def serialize(self, partial=False): + # funny dispatch ftw! + if self.entity.delete_time: + self.delete() + return + + if isinstance(self.entity, model.Motion): + midx = MotionIndexer(self.entity, doc=self.doc, + boost=self.boost) + midx.serialize(partial=partial) + elif isinstance(self.entity, model.Issue): + iidx = IssueIndexer(self.entity, doc=self.doc, + boost=self.boost) + iidx.serialize(partial=partial) + elif isinstance(self.entity, model.Category): + cidx = CategoryIndexer(self.entity, doc=self.doc, + boost=self.boost) + cidx.serialize(partial=partial) + else: + self.serialize_delegateable(partial=partial) + + def serialize_delegateable(self, partial=False): + f = Field("label", self.entity.label, + Field.Store.NO, Field.Index.TOKENIZED) + f.setBoost(self.boost * 2.0) + self.doc.add(f) + + f = Field("instance", self.entity.instance.key, + Field.Store.YES, Field.Index.UN_TOKENIZED) + f.setBoost(self.boost) + self.doc.add(f) + + creator = UserIndexer(self.entity.creator, doc=self.doc, + boost=self.boost*0.9) + creator.serialize(partial=True) + + if not partial: + for comment in self.entity.comments: + cidx = CommentIndexer(comment, doc=self.doc, boost=0.8) + cidx.serialize(partial=True) + +class CategoryIndexer(DelegateableIndexer): + + def serialize(self, partial=False): + if self.entity.instance.root == self.entity: + return + if self.entity.description: + f = Field("description", text.plain(self.entity.description), + Field.Store.NO, Field.Index.TOKENIZED) + f.setBoost(self.boost * 1.0) + self.doc.add(f) + + self.serialize_delegateable(partial=partial) + +class IssueIndexer(DelegateableIndexer): + + def serialize(self, partial=False): + self.serialize_delegateable(partial=partial) + + if not partial: + for motion in self.entity.children: + midx = MotionIndexer(motion, doc=self.doc, + boost=self.boost * 0.7) + midx.serialize(partial=True) + +class MotionIndexer(DelegateableIndexer): + + def serialize(self, partial=False): + self.serialize_delegateable(partial=partial) + + if not partial and self.entity.issue: + iidx = IssueIndexer(self.entity.issue, doc=self.doc, + boost=self.boost * 0.7) + iidx.serialize(partial=True) + +def insert(indexer_cls): + def f(entity): + #model.meta.Session.refresh(entity) + indexer = indexer_cls(entity) + indexer.add() + return f + +def update(indexer_cls): + def f(entity): + #model.meta.Session.refresh(entity) + indexer = indexer_cls(entity) + indexer.delete() + indexer.add() + return f + +def delete(indexer_cls): + def f(entity): + indexer = indexer_cls(entity) + indexer.delete() + return f + +def register_indexer(cls, indexer_cls): + hooks.patch(cls, hooks.POSTINSERT, insert(indexer_cls)) + hooks.patch(cls, hooks.POSTUPDATE, update(indexer_cls)) + hooks.patch(cls, hooks.POSTDELETE, delete(indexer_cls)) + diff --git a/adhocracy/lib/search/query.py b/adhocracy/lib/search/query.py new file mode 100644 index 000000000..e160b6d0e --- /dev/null +++ b/adhocracy/lib/search/query.py @@ -0,0 +1,46 @@ +import logging + +from lucene import Term, TermQuery, MultiFieldQueryParser, \ + BooleanClause, BooleanQuery, Hit + +import entityrefs +import index + +log = logging.getLogger(__name__) + +def run(terms, instance=None, cls=None, fields=['label', 'description', 'user']): + bquery = BooleanQuery() + + equery = TermQuery(Term("entity", "true")) + bquery.add(BooleanClause(equery, BooleanClause.Occur.MUST)) + + if instance: + iquery = TermQuery(Term("instance", str(instance.key))) + bquery.add(BooleanClause(iquery, BooleanClause.Occur.MUST)) + + if cls: + tquery = TermQuery(Term("type", entityrefs._index_name(cls))) + bquery.add(BooleanClause(tquery, BooleanClause.Occur.MUST)) + + mquery = MultiFieldQueryParser.parse(terms, fields, + [BooleanClause.Occur.SHOULD] * len(fields), + index.get_analyzer()) + bquery.add(BooleanClause(mquery, BooleanClause.Occur.MUST)) + + log.debug("Entity query: %s" % bquery.toString().encode("ascii", "replace")) + + hits = index.query(bquery) + + results = [] + for hit in hits: + hit = Hit.cast_(hit) + ref = hit.getDocument().getField("ref").stringValue() + entity = entityrefs.to_entity(ref) + score = hit.getScore() + + if entity: + results.append(entity) + log.debug(" Result: %s (type: %s), score: %s" % (repr(entity), + repr(entity.__class__), + score)) + return results \ No newline at end of file diff --git a/adhocracy/lib/social.py b/adhocracy/lib/social.py new file mode 100644 index 000000000..633d0a482 --- /dev/null +++ b/adhocracy/lib/social.py @@ -0,0 +1,20 @@ +from pylons import tmpl_context as c + +def _popular_agents(delegations, count=10): + """ + For a given set of delegations, find at most 'count' agents that + occur most in the set. Returns a list of tuples, (agent, occurence_count) + """ + agents = [d.agent for d in delegations if not d.revoke_time] + freq = {} + for agent in agents: + freq[agent] = freq.get(agent, 0) + 1 + popular = sorted(freq.items(), cmp=lambda a, b: b[1] - a[1]) + popular = filter(lambda (u, v): u != c.user, popular) + return popular[0:count] + +def delegateable_popular_agents(delegateable, count=10): + return _popular_agents(delegateable.delegations, count=count) + +def user_popular_agents(user, count=10): + return _popular_agents(user.delegated, count=count) \ No newline at end of file diff --git a/adhocracy/lib/sorting.py b/adhocracy/lib/sorting.py new file mode 100644 index 000000000..9224d421b --- /dev/null +++ b/adhocracy/lib/sorting.py @@ -0,0 +1,43 @@ + +import event.stats as estats +import karma + +def delegateable_label(entities): + return sorted(entities, key=lambda e: e.label.lower()) + +def user_name(entities): + return sorted(entities, key=lambda e: e.name.lower()) + +def entity_newest(entities): + return sorted(entities, key=lambda e: e.create_time, reverse=True) + +def entity_oldest(entities): + return sorted(entities, key=lambda e: e.create_time, reverse=False) + +def category_activity(categories): + return sorted(categories, key=lambda c: estats.category_activity(c)) + +def issue_activity(issues): + return sorted(issues, key=lambda i: estats.issue_activity(i)) + +def motion_activity(motions): + return sorted(motions, key=lambda m: estats.motion_activity(m)) + +def instance_activity(instances): + return sorted(instances, key=lambda i: estats.instance_activity(i)) + +def user_activity(users): + return sorted(users, key=lambda u: estats.instance_activity(u)) + +def user_karma(users): + return sorted(users, key=lambda u: karma.user_score(u), reverse=True) + +def dict_value_sorter(dict): + def _sort(items): + return sorted(items, key=lambda i: dict.get(i)) + return _sort + +def comment_karma(comments): + return sorted(comments, + key=lambda c: karma.comment_score(c, recurse=True), + reverse=True) diff --git a/adhocracy/lib/templating.py b/adhocracy/lib/templating.py new file mode 100644 index 000000000..e190ba11d --- /dev/null +++ b/adhocracy/lib/templating.py @@ -0,0 +1,108 @@ +import urllib +import math + +from pylons.templating import render_mako, render_mako_def +from pylons import request, tmpl_context as c + +import formencode +from formencode import foreach, validators, htmlfill + +import tiles + +def tpl_vars(): + vars = dict() + vars['tiles'] = tiles + return vars + + +def render(template_name, extra_vars=None, cache_key=None, + cache_type=None, cache_expire=None): + """ + Signature matches that of pylons actual render_mako. + """ + if not extra_vars: + extra_vars = {} + + extra_vars.update(tpl_vars()) + + return render_mako(template_name, extra_vars=extra_vars, + cache_key=cache_key, cache_type=cache_type, + cache_expire=cache_expire) + +def render_def(template_name, def_name, extra_vars=None, cache_key=None, + cache_type=None, cache_expire=None, **kwargs): + """ + Signature matches that of pylons actual render_mako_def. + """ + if not extra_vars: + extra_vars = {} + + extra_vars.update(tpl_vars()) + + return render_mako_def(template_name, def_name, extra_vars=extra_vars, + cache_key=cache_key, cache_type=cache_type, + cache_expire=cache_expire, **kwargs) + +class NamedPager(object): + """ + A ``NamedPager`` is a list generator for the UI. The ``name`` is required + in order to distinguish multiple pagers working on the same page. + """ + + def __init__(self, name, items, itemfunc, count=10, sorts={}, default_sort=None, **kwargs): + self.name = name + self._items = [] + for i in items: # stable set() - ugly, can this be done differently? + if not i in self._items: + self._items.append(i) + self.itemfunc = itemfunc + self.count = count + self.sorts = sorts + if len(sorts.values()): + self.selected_sort = sorts.values().index(default_sort) + 1 + else: + self.selected_sort = 0 + self.sorted = False + self.kwargs = kwargs + self._parse_request() + + def _parse_request(self): + page_val = validators.Int(if_empty=1, not_empty=False) + self.page = page_val.to_python(request.params.get("%s_page" % self.name)) + + count_val = validators.Int(if_empty=self.count, if_invalid=self.count, + max=250, not_empty=False) + self.count = count_val.to_python(request.params.get("%s_count" % self.name)) + + sort_val = validators.Int(if_empty=self.selected_sort, if_invalid=self.selected_sort, + min=1, max=len(self.sorts.keys()), not_empty=False) + self.selected_sort = sort_val.to_python(request.params.get("%s_sort" % self.name)) + + def _get_items(self): + if not self.sorted and len(self.sorts.values()): + sorter = self.sorts.values()[self.selected_sort - 1] + self._items = sorter(self._items) + self.sorted = True + + offset = (self.page-1)*self.count + return self._items[offset:offset+self.count] + + items = property(_get_items) + + def pages(self): + return int(math.ceil(len(self._items)/float(self.count))) + + def rel_page(self, step=1): + cand = self.page + step + return cand if cand > 0 and cand <= self.pages() else None + + def serialize(self, page=None, count=None, sort=None): + query = {"%s_page" % self.name: page if page else self.page, + "%s_count" % self.name: count if count else self.count, + "%s_sort" % self.name: sort if sort else self.selected_sort} + self.kwargs.update(query) + return "?" + urllib.urlencode(self.kwargs.items()) + + def here(self): + return render_def('/pager.html', 'namedpager', pager=self) + \ No newline at end of file diff --git a/adhocracy/lib/text/__init__.py b/adhocracy/lib/text/__init__.py new file mode 100644 index 000000000..b1c350af3 --- /dev/null +++ b/adhocracy/lib/text/__init__.py @@ -0,0 +1,43 @@ +import re +import cgi + +from BeautifulSoup import BeautifulSoup, NavigableString + +import adhocracy.model as model +import adhocracy.contrib.markdown2 as markdown + +from diff import textDiff as html_diff + +DEFAULT_TAGS = ['a', 'b', 'strong', 'h1', 'h2', + 'h3', 'h4', 'h5', 'h6', 'i', + 'table', 'tr', 'th', 'td', 'abbr', + 'code', 'blockquote', 'em', 'hr', + 'ul', 'ol', 'li', 'p', 'pre', + 'strike', 'u', 'img', 'cite', 'br'] + +DEFAULT_ATTRS = ['href', 'width', 'align', 'src', + 'alt', 'title'] + +META_RE = re.compile("(\n|\t|\")", re.MULTILINE) + +markdowner = markdown.Markdown() + +def meta_escape(text, markdown=True): + if markdown: + text = plain(text) + text = META_RE.sub(" ", text) + text = text.strip() + return text + +def plain(html): + soup = BeautifulSoup(render(html)) + return u"".join(map(unicode, soup.findChildren(text=True))) + +def cleanup(text): + return text + +def render(text): + if text: + text = cgi.escape(text) + text = markdowner.convert(text) + return text \ No newline at end of file diff --git a/adhocracy/lib/text/diff.py b/adhocracy/lib/text/diff.py new file mode 100644 index 000000000..b4955e0fc --- /dev/null +++ b/adhocracy/lib/text/diff.py @@ -0,0 +1,66 @@ +#!/usr/bin/python2.2 +"""HTML Diff: http://www.aaronsw.com/2002/diff +Rough code, badly documented. Send me comments and patches.""" + +__author__ = 'Aaron Swartz <me@aaronsw.com>' +__copyright__ = '(C) 2003 Aaron Swartz. GNU GPL 2.' +__version__ = '0.22' + +import difflib, string + +def isTag(x): return x[0] == "<" and x[-1] == ">" + +def textDiff(a, b): + """Takes in strings a and b and returns a human-readable HTML diff.""" + + out = [] + a, b = html2list(a), html2list(b) + s = difflib.SequenceMatcher(None, a, b) + for e in s.get_opcodes(): + if e[0] == "replace": + # @@ need to do something more complicated here + # call textDiff but not for html, but for some html... ugh + # gonna cop-out for now + out.append('<del class="diff modified">'+''.join(a[e[1]:e[2]]) + '</del><ins class="diff modified">'+''.join(b[e[3]:e[4]])+"</ins>") + elif e[0] == "delete": + out.append('<del class="diff">'+ ''.join(a[e[1]:e[2]]) + "</del>") + elif e[0] == "insert": + out.append('<ins class="diff">'+''.join(b[e[3]:e[4]]) + "</ins>") + elif e[0] == "equal": + out.append(''.join(b[e[3]:e[4]])) + else: + raise "Um, something's broken. I didn't expect a '" + `e[0]` + "'." + return ''.join(out) + +def html2list(x, b=0): + mode = 'char' + cur = '' + out = [] + for c in x: + if mode == 'tag': + if c == '>': + if b: cur += ']' + else: cur += c + out.append(cur); cur = ''; mode = 'char' + else: cur += c + elif mode == 'char': + if c == '<': + out.append(cur) + if b: cur = '[' + else: cur = c + mode = 'tag' + elif c in string.whitespace: out.append(cur+c); cur = '' + else: cur += c + out.append(cur) + return filter(lambda x: x is not '', out) + +if __name__ == '__main__': + import sys + try: + a, b = sys.argv[1:3] + except ValueError: + print "htmldiff: highlight the differences between two html files" + print "usage: " + sys.argv[0] + " a b" + sys.exit(1) + print textDiff(open(a).read(), open(b).read()) + \ No newline at end of file diff --git a/adhocracy/lib/text/i18n.py b/adhocracy/lib/text/i18n.py new file mode 100644 index 000000000..4fcefb46a --- /dev/null +++ b/adhocracy/lib/text/i18n.py @@ -0,0 +1,60 @@ +from datetime import datetime, timedelta + +import formencode + +from pylons import request, response, session, tmpl_context as c +from pylons.i18n import _, add_fallback, get_lang, set_lang, gettext +import webhelpers.date as date + +import babel +from babel import Locale +import babel.dates + + + +LOCALES = [babel.Locale('en', 'US'), + babel.Locale('de', 'DE'), + babel.Locale('fr', 'FR')] + +DEFAULT = babel.Locale('en', 'US') + +def handle_request(): + """ + Given a request, try to determine the appropriate locale to use for the + request. When a user is logged in, his or her settings will first be queried. + Otherwise, an appropriate locale will be negotiated between the browser + accept headers and the available locales. + """ + if c.user and c.user.locale: + c.locale = c.user.locale + else: + lang = Locale.negotiate(request.languages, map(str, LOCALES)) \ + or str(DEFAULT) + c.locale = Locale.parse(lang) + + # pylons + set_lang(c.locale.language) + add_fallback(DEFAULT.language) + formencode.api.set_stdtranslation(domain="FormEncode", languages=[c.locale.language]) + +def relative_date(time): + """ Date only, not date & time. """ + date = time.date() + today = date.today() + if date == today: + return _("Today") + elif date == (today - timedelta(days=1)): + return _("Yesterday") + else: + return dates.format_date(date, 'long', c.locale) + +def format_timedelta(td): + # version shit + if hasattr(babel.dates, 'format_timedelta'): + return babel.dates.format_timedelta(td, locale=c.locale.language) + else: + return date.time_ago_in_words(datetime.now() - td) + +def relative_time(dt): + """ A short statement giving the time distance since ``dt``. """ + return _("%(ts)s ago") % {'ts': format_timedelta(dt - datetime.now())} \ No newline at end of file diff --git a/adhocracy/lib/tiles/__init__.py b/adhocracy/lib/tiles/__init__.py new file mode 100644 index 000000000..8c656bbb2 --- /dev/null +++ b/adhocracy/lib/tiles/__init__.py @@ -0,0 +1,32 @@ +import logging + +from adhocracy import model + +import issue_tiles as issue +import instance_tiles as instance +import category_tiles as category +import comment_tiles as comment +import user_tiles as user +import motion_tiles as motion +import event_tiles as event +import decision_tiles as decision +import delegation_tiles as delegation +import revision_tiles as revision + +log = logging.getLogger(__name__) + +def dispatch_row(entity): + if isinstance(entity, model.User): + return user.row(entity) + elif isinstance(entity, model.Instance): + return instance.row(entity) + elif isinstance(entity, model.Category): + return category.row(entity) + elif isinstance(entity, model.Issue): + return issue.row(entity) + elif isinstance(entity, model.Motion): + return motion.detail_row(entity) + elif isinstance(entity, model.Comment): + return comment.row(entity) + else: + log.warn("WARNING: Cannot render %s!" % repr(entity)) \ No newline at end of file diff --git a/adhocracy/lib/tiles/category_tiles.py b/adhocracy/lib/tiles/category_tiles.py new file mode 100644 index 000000000..4b7de6e8c --- /dev/null +++ b/adhocracy/lib/tiles/category_tiles.py @@ -0,0 +1,85 @@ +from util import render_tile, BaseTile + +from pylons import request, response, session, tmpl_context as c +from webhelpers.text import truncate + +import adhocracy.model as model +from .. import helpers as h +from .. import text +from .. import authorization as auth +from .. import karma + +from delegateable_tiles import DelegateableTile + +class CategoryTile(DelegateableTile): + + def __init__(self, category): + self.category = category + self.__issues = None + self.__categories = None + DelegateableTile.__init__(self, category) + + def _issues(self): + if not self.__issues: + self.__issues = list(set(self.category.search_children(recurse=True, cls=model.Issue))) + return self.__issues + + issues = property(_issues) + + def _num_issues(self): + return len(self.issues) + + num_issues = property(_num_issues) + + def _categories(self): + if not self.__categories: + self.__categories = self.category.search_children(recurse=False, cls=model.Category) + return self.__categories + + categories = property(_categories) + + def _num_categories(self): + return len(self.categories) + + num_categories = property(_num_categories) + + can_edit = property(DelegateableTile.prop_has_permkarma('category.edit')) + lack_edit_karma = property(BaseTile.prop_lack_karma('category.edit')) + + can_delete = property(DelegateableTile.prop_has_permkarma('category.delete')) + lack_delete_karma = property(BaseTile.prop_lack_karma('category.delete')) + + can_create_issue = property(DelegateableTile.prop_has_permkarma('issue.create', allow_creator=False)) + lack_create_issue_karma = property(BaseTile.prop_lack_karma('issue.create')) + + can_create_category = property(DelegateableTile.prop_has_permkarma('category.create', allow_creator=False)) + lack_create_category_karma = property(BaseTile.prop_lack_karma('category.create')) + + def _is_root(self): + return self.category == self.category.instance.root + + is_root = property(_is_root) + + def _tagline(self): + if self.category.description: + tagline = text.plain(self.category.description) + return truncate(tagline, length=140, indicator="...", whole_word=True) + return "" + + tagline = property(_tagline) + + def _description(self): + if self.category.description: + return text.render(self.category.description) + return "" + + description = property(_description) + +def list_item(category): + return render_tile('/category/tiles.html', 'list_item', + CategoryTile(category), category=category) + +def row(category): + return render_tile('/category/tiles.html', 'row', CategoryTile(category), category=category) + + diff --git a/adhocracy/lib/tiles/comment_tiles.py b/adhocracy/lib/tiles/comment_tiles.py new file mode 100644 index 000000000..fd0c43d48 --- /dev/null +++ b/adhocracy/lib/tiles/comment_tiles.py @@ -0,0 +1,147 @@ +from pylons import tmpl_context as c +from webhelpers.text import truncate + +import adhocracy.model as model +from .. import text +from .. import karma +from .. import authorization as auth +from .. import democracy +from .. import sorting + +from util import render_tile, BaseTile + +class CommentTile(BaseTile): + + def __init__(self, comment): + self.comment = comment + + def _num_motions(self): + return len(self.issue.motions) + + num_motions = property(_num_motions) + + def _text(self): + if self.comment and self.comment.latest: + return text.render(self.comment.latest.text) + return "" + + text = property(_text) + + def _tagline(self): + if self.comment.latest: + tagline = text.plain(self.comment.latest.text) + return truncate(tagline, length=140, indicator="...", whole_word=True) + return "" + + tagline = property(_tagline) + + def _on_motion(self): + return isinstance(self.comment.topic, model.Motion) + + on_motion = property(_on_motion) + + def _on_issue(self): + return isinstance(self.comment.topic, model.Issue) + + on_issue = property(_on_issue) + + def _can_edit(self): + return auth.on_comment(self.comment, 'comment.edit') \ + and not self.is_deleted and not self.is_immutable + + can_edit = property(_can_edit) + lack_edit_karma = property(BaseTile.prop_lack_karma('comment.edit')) + + def _can_delete(self): + if auth.on_comment(self.comment, 'comment.delete') \ + and not self.is_deleted and not self.is_immutable: + if self.comment.reply or self.comment.canonical: + return True + return False + + can_delete = property(_can_delete) + lack_delete_karma = property(BaseTile.prop_lack_karma('comment.delete')) + + def _can_reply(self): + return auth.on_delegateable(self.comment.topic, 'comment.create') \ + and not self.is_deleted + + can_reply = property(_can_reply) + lack_reply_karma = property(BaseTile.prop_lack_karma('comment.create')) + + def _can_give_karma(self): + return auth.on_comment(self.comment, 'karma.give') \ + and not self.is_own \ + and not self.is_deleted and not self.is_immutable + + can_give_karma = property(_can_give_karma) + lack_give_karma_karma = property(BaseTile.prop_lack_karma('karma.give')) + + def _is_own(self): + return self.comment.creator == c.user + + is_own = property(_is_own) + + def _is_edited(self): + if self.is_deleted: + return False + return len(self.comment.revisions) > 1 + + is_edited = property(_is_edited) + + def _is_deleted(self): + return self.comment.delete_time + + is_deleted = property(_is_deleted) + + def _is_immutable(self): + return not democracy.is_comment_mutable(self.comment) + + is_immutable = property(_is_immutable) + + def _show(self): + if self.is_deleted: + children = map(lambda c: CommentTile(c)._show(), self.comment.replies) + if not True in children: + return False + return True + + show = property(_show) + + def _num_children(self): + num = len(filter(lambda c: not c.delete_time, self.comment.replies)) + num += sum(map(lambda c: CommentTile(c)._num_children(), self.comment.replies)) + return num + + num_children = property(_num_children) + + def _karma_score(self): + return karma.comment_score(self.comment) + + karma_score = property(_karma_score) + + def _karma_position(self): + if not c.user: + return None + pos = karma.position(self.comment, c.user) + if (pos and pos.value == 1) or self.comment.creator == c.user: + return "upvoted" + elif pos and pos.value == -1: + return "downvoted" + return pos + + karma_position = property(_karma_position) + + def _replies(self): + return sorting.comment_karma(self.comment.replies) + + replies = property(_replies) + + +def row(comment): + return render_tile('/comment/tiles.html', 'row', CommentTile(comment), comment=comment) + +def full(comment, recurse=True, collapse=True, link_discussion=False): + return render_tile('/comment/tiles.html', 'full', CommentTile(comment), + recurse=recurse, comment=comment, collapse=collapse, link_discussion=link_discussion) + diff --git a/adhocracy/lib/tiles/decision_tiles.py b/adhocracy/lib/tiles/decision_tiles.py new file mode 100644 index 000000000..14a6a4a4d --- /dev/null +++ b/adhocracy/lib/tiles/decision_tiles.py @@ -0,0 +1,17 @@ +from .. import democracy +from .. import helpers as h +from .. import authorization as auth + +from util import render_tile, BaseTile + +class DecisionTile(BaseTile): + + def __init__(self, decision): + self.decision = decision + +def motion_row(decision): + return render_tile('/decision/tiles.html', 'row', DecisionTile(decision), decision=decision, focus_user=True) + +def user_row(decision): + return render_tile('/decision/tiles.html', 'row', DecisionTile(decision), decision=decision, focus_motion=True) + \ No newline at end of file diff --git a/adhocracy/lib/tiles/delegateable_tiles.py b/adhocracy/lib/tiles/delegateable_tiles.py new file mode 100644 index 000000000..b2965eedd --- /dev/null +++ b/adhocracy/lib/tiles/delegateable_tiles.py @@ -0,0 +1,58 @@ +from util import BaseTile + +from pylons import request, response, session, tmpl_context as c + +from .. import democracy +from .. import helpers as h +from .. import authorization as auth + +from comment_tiles import CommentTile + +class DelegateableTile(BaseTile): + + def __init__(self, delegateable): + self.delegateable = delegateable + self.__dnode = None + self.__delegations = None + self.__num_principals = None + + def _dnode(self): + if not self.__dnode: + self.__dnode = democracy.DelegationNode(c.user, self.delegateable) + return self.__dnode + + dnode = property(_dnode) + + def _delegations(self): + if not self.__delegations: + self.__delegations = self.dnode.outbound() + return self.__delegations + + delegations = property(_delegations) + + def _num_principals(self): + if self.__num_principals == None: + principals = set(map(lambda d: d.principal, self.dnode.transitive_inbound())) + self.__num_principals = len(principals) + return self.__num_principals + + num_principals = property(_num_principals) + + def _has_delegated(self): + return len(self.delegations) > 0 + + has_delegated = property(_has_delegated) + + def _has_overridden(self): + return False + + has_overridden = property(_has_overridden) + + @classmethod + def prop_has_permkarma(cls, perm, allow_creator=True): + return lambda self: auth.on_delegateable(self.delegateable, perm, + allow_creator=allow_creator) + + can_vote = property(BaseTile.prop_has_perm('vote.cast')) + can_delegate = can_vote + diff --git a/adhocracy/lib/tiles/delegation_tiles.py b/adhocracy/lib/tiles/delegation_tiles.py new file mode 100644 index 000000000..7516e39fb --- /dev/null +++ b/adhocracy/lib/tiles/delegation_tiles.py @@ -0,0 +1,18 @@ +from util import render_tile, BaseTile + +from pylons import tmpl_context as c + +class DelegationTile(BaseTile): + + def __init__(self, delegation): + self.delegation = delegation + + + +def inbound(delegation): + return render_tile('/delegation/tiles.html', 'inbound', + DelegationTile(delegation), delegation=delegation) + +def outbound(delegation): + return render_tile('/delegation/tiles.html', 'outbound', + DelegationTile(delegation), delegation=delegation) \ No newline at end of file diff --git a/adhocracy/lib/tiles/event_tiles.py b/adhocracy/lib/tiles/event_tiles.py new file mode 100644 index 000000000..d49fe4094 --- /dev/null +++ b/adhocracy/lib/tiles/event_tiles.py @@ -0,0 +1,14 @@ +from util import render_tile + +from pylons import tmpl_context as c + +class EventTile(): + + def __init__(self, event): + self.event = event + + + +def list_item(event): + return render_tile('/event/tiles.html', 'list_item', + EventTile(event), event=event) \ No newline at end of file diff --git a/adhocracy/lib/tiles/instance_tiles.py b/adhocracy/lib/tiles/instance_tiles.py new file mode 100644 index 000000000..e2331a430 --- /dev/null +++ b/adhocracy/lib/tiles/instance_tiles.py @@ -0,0 +1,65 @@ +from util import render_tile, BaseTile +from datetime import timedelta + +from pylons import tmpl_context as c + +from webhelpers.text import truncate +from .. import text +from .. import authorization as auth +from .. import helpers as h + + +from category_tiles import CategoryTile + +class InstanceTile(CategoryTile): + + def __init__(self, instance): + self.instance = instance + self.__issues = None + CategoryTile.__init__(self, instance.root) + + def _tagline(self): + if self.instance.description: + tagline = text.plain(self.instance.description) + return truncate(tagline, length=140, indicator="...", whole_word=True) + return "" + + tagline = property(_tagline) + + def _description(self): + if self.instance.description: + return text.render(self.instance.description) + return "" + + description = property(_description) + + def _activation_delay(self): + return text.i18n.format_timedelta(timedelta(days=self.instance.activation_delay)) + + activation_delay = property(_activation_delay) + + def _required_majority(self): + return "%s%%" % int(self.instance.required_majority * 100) + + required_majority = property(_required_majority) + + def _can_join(self): + return (c.user and not c.user.is_member(self.instance)) \ + and h.has_permission("instance.join") + + can_join = property(_can_join) + + def _can_leave(self): + return c.user and c.user.is_member(self.instance) \ + and h.has_permission("instance.leave") \ + and not c.user == self.instance.creator + + can_leave = property(_can_leave) + can_admin = property(BaseTile.prop_has_perm('instance.admin')) + +def list_item(instance): + return render_tile('/instance/tiles.html', 'list_item', + InstanceTile(instance), instance=instance) + +def row(instance): + return render_tile('/instance/tiles.html', 'row', InstanceTile(instance), instance=instance) diff --git a/adhocracy/lib/tiles/issue_tiles.py b/adhocracy/lib/tiles/issue_tiles.py new file mode 100644 index 000000000..58e416792 --- /dev/null +++ b/adhocracy/lib/tiles/issue_tiles.py @@ -0,0 +1,58 @@ +from util import render_tile, BaseTile +from webhelpers.text import truncate + +from .. import text +from .. import helpers as h +from .. import authorization as auth +from .. import democracy + +from delegateable_tiles import DelegateableTile +from comment_tiles import CommentTile + +class IssueTile(DelegateableTile): + + def __init__(self, issue): + self.issue = issue + self.__comment_tile = None + DelegateableTile.__init__(self, issue) + + def _num_motions(self): + return len(self.issue.motions) + + num_motions = property(_num_motions) + + def _tagline(self): + if self.issue.comment and self.issue.comment.latest: + tagline = text.plain(self.issue.comment.latest.text) + return truncate(tagline, length=140, indicator="...", whole_word=True) + return "" + + tagline = property(_tagline) + + can_edit = property(DelegateableTile.prop_has_permkarma('issue.edit')) + lack_edit_karma = property(BaseTile.prop_lack_karma('issue.edit')) + + def _can_delete(self): + for motion in self.issue.motions: + if not democracy.is_motion_mutable(motion) and \ + not democracy.can_motion_cancel(motion): + return False + return auth.on_delegateable(self.issue, "issue.delete", + allow_creator=False if len(self.issue.motions) else True) + + can_delete = property(_can_delete) + lack_delete_karma = property(BaseTile.prop_lack_karma('issue.delete')) + + can_create_motion = property(DelegateableTile.prop_has_permkarma('motion.create', allow_creator=False)) + lack_create_motion_karma = property(BaseTile.prop_lack_karma('motion.create')) + + def _comment_tile(self): + if not self.__comment_tile: + self.__comment_tile = CommentTile(self.issue.comment) + return self.__comment_tile + + comment_tile = property(_comment_tile) + + +def row(issue): + return render_tile('/issue/tiles.html', 'row', IssueTile(issue), issue=issue) diff --git a/adhocracy/lib/tiles/motion_tiles.py b/adhocracy/lib/tiles/motion_tiles.py new file mode 100644 index 000000000..e2b6dc879 --- /dev/null +++ b/adhocracy/lib/tiles/motion_tiles.py @@ -0,0 +1,177 @@ +from util import render_tile + +from pylons import tmpl_context as c +from webhelpers.text import truncate + +from .. import democracy +from .. import helpers as h +from .. import text +from .. import authorization as auth +from .. import sorting + +from delegateable_tiles import DelegateableTile +from comment_tiles import CommentTile + +class MotionTile(DelegateableTile): + + def __init__(self, motion): + self.motion = motion + self.__poll = None + self.__result = None + self.__decision = None + self.__num_principals = None + self.__comment_tile = None + DelegateableTile.__init__(self, motion) + + def _tagline(self): + if self.motion.comment and self.motion.comment.latest: + tagline = text.plain(self.motion.comment.latest.text) + return truncate(tagline, length=140, indicator="...", whole_word=True) + return "" + + tagline = property(_tagline) + + def _poll(self): + if not self.__poll: + self.__poll = self.motion.poll + return self.__poll + + poll = property(_poll) + + def _result(self): + if not self.__result: + self.__result = democracy.Result(self.motion, poll=self.poll) + return self.__result + + result = property(_result) + + def _decision(self): + if not self.__decision and c.user: + self.__decision = democracy.Decision(c.user, self.poll) + return self.__decision + + decision = property(_decision) + + def _canonicals(self): + cs = [] + for comment in self.motion.comments: + if comment.canonical and not comment.delete_time: + cs.append(comment) + return sorting.comment_karma(cs) + + canonicals = property(_canonicals) + + def _can_edit(self): + return auth.on_delegateable(self.motion, 'motion.edit') \ + and not self.is_immutable + + can_edit = property(_can_edit) + lack_edit_karma = property(DelegateableTile.prop_lack_karma('motion.edit')) + + def _can_delete(self): + return auth.on_delegateable(self.motion, 'motion.delete') \ + and not self.is_immutable + + can_delete = property(_can_delete) + lack_delete_karma = property(DelegateableTile.prop_lack_karma('motion.delete')) + + def _has_overridden(self): + if self.decision.self_made(): + return True + return False + + def _can_create_canonical(self): + return auth.on_delegateable(self.motion, 'comment.create') \ + and not self.is_immutable + + can_create_canonical = property(_can_create_canonical) + lack_create_canonical_karma = property(DelegateableTile.prop_lack_karma('comment.create')) + + def _has_canonicals(self): + return len(self.canonicals) > 0 + + has_canonicals = property(_has_canonicals) + + def _can_begin_poll(self): + if not self.has_canonicals: + return False + if self.poll: + return False + return auth.on_delegateable(self.motion, 'poll.create') + + can_begin_poll = property(_can_begin_poll) + lack_begin_poll_karma = property(DelegateableTile.prop_lack_karma('poll.create')) + + def _can_end_poll(self): + if not self.poll: + return False + if auth.on_delegateable(self.motion, 'poll.abort'): + return self.result.can_cancel + + can_end_poll = property(_can_end_poll) + lack_end_poll_karma = property(DelegateableTile.prop_lack_karma('poll.abort')) + + def _is_immutable(self): + return not democracy.is_motion_mutable(self.motion) + + is_immutable = property(_is_immutable) + + def _delegates(self): + agents = [] + if not c.user: + return [] + for delegation in self.dnode.outbound(): + agents.append(delegation.agent) + return set(agents) + + delegates = property(_delegates) + + def delegates_result(self, result): + agents = [] + for agent in self.delegates: + decision = democracy.Decision(agent, self.motion, poll=self.poll) + if decision.made() and decision.result == result: + agents.append(agent) + return agents + + def _num_principals(self): + if self.__num_principals == None: + principals = set(map(lambda d: d.principal, self.dnode.transitive_inbound())) + if self.poll: + principals = filter(lambda p: not democracy.Decision(c.user, self.motion, poll=self.poll).self_made(), + principals) + self.__num_principals = len(principals) + return self.__num_principals + + num_principals = property(_num_principals) + + def _comment_tile(self): + if not self.__comment_tile: + self.__comment_tile = CommentTile(self.motion.comment) + return self.__comment_tile + + comment_tile = property(_comment_tile) + + def _result_affirm(self): + return round(self.result.tally.rel_for * 100.0, 1) + + result_affirm = property(_result_affirm) + + def _result_dissent(self): + return round(self.result.tally.rel_against * 100.0, 1) + + result_dissent = property(_result_dissent) + + +def row(motion, detail=False): + return render_tile('/motion/tiles.html', 'row', MotionTile(motion), motion=motion, detail=detail) + +def detail_row(motion): + return row(motion, detail=True) + +def list_item(motion): + return render_tile('/motion/tiles.html', 'list_item', + MotionTile(motion), motion=motion) + +def state_flag(state): + return render_tile('/motion/tiles.html', 'state_flag', None, state=state) \ No newline at end of file diff --git a/adhocracy/lib/tiles/revision_tiles.py b/adhocracy/lib/tiles/revision_tiles.py new file mode 100644 index 000000000..ffaee62dd --- /dev/null +++ b/adhocracy/lib/tiles/revision_tiles.py @@ -0,0 +1,62 @@ +from pylons import tmpl_context as c +from webhelpers.text import truncate + +import adhocracy.model as model +from .. import text + +from util import render_tile, BaseTile +from comment_tiles import CommentTile + +class RevisionTile(BaseTile): + + def __init__(self, revision): + self.revision = revision + self.comment_tile = CommentTile(revision.comment) + + def _can_edit(self): + return self.comment_tile.can_edit + + can_edit = property(_can_edit) + + def _can_revert(self): + return self.comment_tile.can_edit and not self.is_latest + + can_revert = property(_can_revert) + + def _is_earliest(self): + return self.revision == min(self.revision.comment.revisions, key=lambda r: r.create_time) + + is_earliest = property(_is_earliest) + + def _is_latest(self): + return self.revision.comment.latest == self.revision + + is_latest = property(_is_latest) + + def _previous(self): + if self.is_earliest: + return None + smaller = filter(lambda r: r.create_time < self.revision.create_time, + self.revision.comment.revisions) + return max(smaller, key=lambda r: r.create_time) + + previous = property(_previous) + + def _diff_text(self): + previous = self.previous + if not previous: + return text.render(self.revision.text) + return text.html_diff(text.render(previous.text), + text.render(self.revision.text)) + + diff_text = property(_diff_text) + + def _index(self): + return len(self.revision.comment.revisions) - self.revision.comment.revisions.index(self.revision) + + index = property(_index) + + +def row(revision): + return render_tile('/comment/revision_tiles.html', 'row', RevisionTile(revision), revision=revision) + diff --git a/adhocracy/lib/tiles/user_tiles.py b/adhocracy/lib/tiles/user_tiles.py new file mode 100644 index 000000000..20c3d3f1a --- /dev/null +++ b/adhocracy/lib/tiles/user_tiles.py @@ -0,0 +1,75 @@ +from util import render_tile, BaseTile + +from pylons import request, response, session, tmpl_context as c + +from webhelpers.text import truncate + +import adhocracy.model as model +from .. import helpers as h +from .. import text +from .. import karma + +class UserTile(BaseTile): + + def __init__(self, user): + self.user = user + + def _bio(self): + if self.user.bio: + return text.render(self.user.bio) + return "" + + bio = property(_bio) + + def _tagline(self): + if self.user.bio: + tagline = text.plain(self.user.bio) + return truncate(tagline, length=140, indicator="...", whole_word=True) + return "" + + tagline = property(_tagline) + + def _can_edit(self): + return (h.has_permission("user-edit") and c.user == self.user) \ + or h.has_permission("user-manage") + + can_edit = property(_can_edit) + + def _karma(self): + return karma.user_score(self.user) + + karma = property(_karma) + + def _num_issues(self): + return len(filter(lambda d: isinstance(d, model.Issue) and d.instance==c.instance, + self.user.delegateables)) + + num_issues = property(_num_issues) + + def _num_motions(self): + return len(filter(lambda d: isinstance(d, model.Motion) and d.instance==c.instance, + self.user.delegateables)) + + num_motions = property(_num_motions) + + def _num_comments(self): + return len(filter(lambda cm: cm.topic.instance == c.instance, self.user.comments)) + + num_comments = property(_num_comments) + + def karmas(self): + return filter(lambda k: k.comment.topic.instance == c.instance, self.user.karma_received) + + def _num_karma_up(self): + return len(filter(lambda k: k.value > 0, self.karmas())) + + num_karma_up = property(_num_karma_up) + + def _num_karma_down(self): + return len(filter(lambda k: k.value < 0, self.karmas())) + + num_karma_down = property(_num_karma_down) + +def row(user): + return render_tile('/user/tiles.html', 'row', UserTile(user), user=user) + diff --git a/adhocracy/lib/tiles/util.py b/adhocracy/lib/tiles/util.py new file mode 100644 index 000000000..4e10a4025 --- /dev/null +++ b/adhocracy/lib/tiles/util.py @@ -0,0 +1,29 @@ + +from pylons import request, response, session, tmpl_context as c + +from .. import helpers as h +from .. import authorization as auth +from .. import karma + +class BaseTile(object): + + @classmethod + def lack_karma(cls, perm): + if h.has_permission(perm) and \ + not karma.threshold.has(c.user, perm): + return karma.threshold.message(perm) + return None + + @classmethod + def prop_lack_karma(cls, perm): + return lambda self: BaseTile.lack_karma(perm) + + @classmethod + def prop_has_perm(cls, perm): + return lambda self: h.has_permission(perm) + + + +def render_tile(template_name, def_name, tile, **kwargs): + from .. import templating + return templating.render_def(template_name, def_name, tile=tile, **kwargs) \ No newline at end of file diff --git a/adhocracy/lib/util.py b/adhocracy/lib/util.py new file mode 100644 index 000000000..9c1a7c2c4 --- /dev/null +++ b/adhocracy/lib/util.py @@ -0,0 +1,8 @@ +import uuid + +def timedelta2seconds(delta): + return delta.microseconds / 1000000.0 \ + + delta.seconds + delta.days * 60*60*24 + +def random_token(): + return str(uuid.uuid4()).split('-').pop() \ No newline at end of file diff --git a/adhocracy/lib/version.py b/adhocracy/lib/version.py new file mode 100644 index 000000000..012f44339 --- /dev/null +++ b/adhocracy/lib/version.py @@ -0,0 +1,18 @@ +""" +Versioning for the application, especially for the UI. +""" + +import re + +svn_revision = "$Revision: 323 $" +svn_rev_re = re.compile("\$Revision: (\d*) \$") + +rev_num = int(svn_rev_re.match(svn_revision).group(1)) + +REV_TEMPLATE = "beta 2 (r%s)" +# Modify this: +# foo schnasel // version bumpign + +def get_version(): + """ Get a version identifier for use in the public user interface """ + return REV_TEMPLATE % rev_num \ No newline at end of file diff --git a/adhocracy/lib/xsrf.py b/adhocracy/lib/xsrf.py new file mode 100644 index 000000000..64b8d9dd1 --- /dev/null +++ b/adhocracy/lib/xsrf.py @@ -0,0 +1,75 @@ +""" +XSRF is Cross-Site Request Forgery, where an attacker has a user follow a link that triggers an +action on an Adhocracy site which the user did not intentionally want to perform (i.e. vote in +a certain way). To prevent this, some actions are only possible if authorized via HTTP or if a +modtoken - a shared SHA1 hash - is included. +""" + +import random +import hashlib +from urlparse import urlparse +from decorator import decorator + +from pylons import session, request +from pylons.controllers.util import abort +from pylons.i18n import _ + +def RequireInternalRequest(methods=['POST', 'GET', 'PUT', 'DELETE']): + """ + XSRF Spoof Filter + + TODO: There is still a scenario in which an attacker opens an adhocracy + page in an iframe, extracts a valid modtoken via javascript and uses this + token to execute the request. + """ + def _decorate(f, *a, **kw): + def check(): + if not request.method in methods: + return True + if not request.environ.get("AUTH_TYPE") == "cookie": + return True + + if request.environ.get('HTTP_REFERER'): + ref_url = urlparse(request.environ.get('HTTP_REFERER')) + ref_host = ref_url.hostname + if ref_url.port: + ref_host += ":" + str(ref_url.port) + + if ref_host.endswith(request.environ['adhocracy.active.domain']): + if request.method != 'GET': + return True + + if request.method == 'GET' and has_token(): + return True + + return False + if check(): + return f(*a, **kw) + else: + abort(403, _("Action failed. You were probably trying to re-perform " + + "an action after using your browser's 'Back' button. This " + + "is prohibited for security reasons.")) + return decorator(_decorate) + +def make_token(): + token = hashlib.sha1(str(random.random())).hexdigest() + tokens = session.get('modtokens', []) + tokens.append(token) + session['modtokens'] = tokens + session.save() + return token + +def url_token(): + return "_csrftoken=%s" % make_token() + +def field_token(): + return '<input name="_csrftoken" type="hidden" value="%s" />' % make_token() + +def has_token(): + if request.params.get('_csrftoken', None) in session['modtokens']: + tokens = session['modtokens'] + tokens.remove(request.params.get('_csrftoken')) + session['modtokens'] = tokens + session.save() + return True + return False diff --git a/adhocracy/model/__init__.py b/adhocracy/model/__init__.py new file mode 100644 index 000000000..f485a0ca1 --- /dev/null +++ b/adhocracy/model/__init__.py @@ -0,0 +1,67 @@ +"""The application's model objects""" +import sqlalchemy as sa +from sqlalchemy import orm + +from adhocracy.model import meta + +from adhocracy.model import user +from adhocracy.model.user import User + +from adhocracy.model import group +from adhocracy.model.group import Group + +from adhocracy.model import permission +from adhocracy.model.permission import Permission + +from adhocracy.model import delegateable +from adhocracy.model.delegateable import Delegateable + +from adhocracy.model import category +from adhocracy.model.category import Category + +from adhocracy.model import issue +from adhocracy.model.issue import Issue + +from adhocracy.model import delegation +from adhocracy.model.delegation import Delegation + +from adhocracy.model import motion +from adhocracy.model.motion import Motion + +from adhocracy.model import poll +from adhocracy.model.poll import Poll + +from adhocracy.model import vote +from adhocracy.model.vote import Vote + +from adhocracy.model import revision +from adhocracy.model.revision import Revision + +from adhocracy.model import comment +from adhocracy.model.comment import Comment + +from adhocracy.model import instance +from adhocracy.model.instance import Instance + +from adhocracy.model import membership +from adhocracy.model.membership import Membership + +from adhocracy.model import karma +from adhocracy.model.karma import Karma + +from adhocracy.model import alternative +from adhocracy.model.alternative import Alternative + +from adhocracy.model import dependency +from adhocracy.model.dependency import Dependency + +from adhocracy.model import filter + +def init_model(engine): + """Call me before using any of the tables or classes in the model""" + sm = orm.sessionmaker(autoflush=True, bind=engine) + meta.engine = engine + meta.Session = orm.scoped_session(sm) + + + diff --git a/adhocracy/model/alternative.py b/adhocracy/model/alternative.py new file mode 100644 index 000000000..093334636 --- /dev/null +++ b/adhocracy/model/alternative.py @@ -0,0 +1,36 @@ +from sqlalchemy import Column, Integer, Unicode, ForeignKey, DateTime, func +from sqlalchemy.orm import relation, backref + +from meta import Base +from motion import Motion + +class Alternative(Base): + __tablename__ = "alternative" + + id = Column(Integer, primary_key=True) + create_time = Column(DateTime, default=func.now()) + delete_time = Column(DateTime, nullable=True) + + left_id = Column(Unicode(10), ForeignKey('motion.id'), nullable=False) + right_id = Column(Unicode(10), ForeignKey('motion.id'), nullable=False) + + def __init__(self, left, right): + if left == right: + raise ValueError() + (left, right) = sorted([left, right], key=lambda m: m.id) + self.left = left + self.right = right + + def __repr__(self): + return "<Alternative(%s,%s)>" % (self.left_id, self.right_id) + + def other(self, this): + return self.left if this==self.right else self.right + + +Alternative.left = relation(Motion, primaryjoin="Alternative.left_id==Motion.id", + foreign_keys=[Alternative.left_id], + backref=backref('left_alternatives', cascade='all')) +Alternative.right = relation(Motion, primaryjoin="Alternative.right_id==Motion.id", + foreign_keys=[Alternative.right_id], + backref=backref('right_alternatives', cascade='all')) \ No newline at end of file diff --git a/adhocracy/model/category.py b/adhocracy/model/category.py new file mode 100644 index 000000000..2b22e2e4e --- /dev/null +++ b/adhocracy/model/category.py @@ -0,0 +1,47 @@ +from sqlalchemy import Column, Unicode, UnicodeText, ForeignKey +import meta +import filter +from delegateable import Delegateable +from issue import Issue + +class Category(Delegateable): + __tablename__ = 'category' + __mapper_args__ = {'polymorphic_identity': 'category'} + + id = Column(Unicode(10), ForeignKey('delegateable.id'), primary_key=True) + description = Column(UnicodeText(), nullable=True) + + def __init__(self, instance, label, creator): + self.init_child(instance, label, creator) + + def __repr__(self): + return u"<Category(%s)>" % (self.id) + + def search_children(self, recurse=False, cls=Delegateable): + """ + Get all child elements of type "cls". Uses DFS. + """ + children = [] + for child in self.children: + if child.delete_time: + continue + if isinstance(child, cls): + children.append(child) + if recurse and isinstance(child, Category): + children = children + child.search_children(recurse=True, cls=cls) + if recurse and isinstance(child, Issue): + children = children + child.search_children(cls=cls) + return children + + @classmethod + def find(cls, id, instance_filter=True): + id = unicode(id.upper()) + try: + q = meta.Session.query(Category) + q = q.filter(Category.id==id) + q = q.filter(Category.delete_time==None) + if filter.has_instance() and instance_filter: + q = q.filter(Category.instance_id==filter.get_instance().id) + return q.one() + except: + return None \ No newline at end of file diff --git a/adhocracy/model/comment.py b/adhocracy/model/comment.py new file mode 100644 index 000000000..315878463 --- /dev/null +++ b/adhocracy/model/comment.py @@ -0,0 +1,59 @@ +from sqlalchemy import Column, Integer, Unicode, ForeignKey, DateTime, Boolean, func +from sqlalchemy.orm import relation, backref + +import meta +from meta import Base +import user +import delegateable + +class Comment(Base): + __tablename__ = 'comment' + + id = Column(Integer, primary_key=True) + create_time = Column(DateTime, default=func.now()) + delete_time = Column(DateTime, default=None, nullable=True) + + creator_id = Column(Integer, ForeignKey('user.id'), nullable=False) + creator = relation(user.User, backref=backref('comments')) + + topic_id = Column(Unicode(10), ForeignKey('delegateable.id'), nullable=False) + topic = relation(delegateable.Delegateable, backref=backref('comments', cascade='all')) + + canonical = Column(Boolean, default=False) + + reply_id = Column(Integer, ForeignKey('comment.id'), nullable=True) + + def __init__(self, topic, creator,): + self.topic = topic + self.creator = creator + + def __repr__(self): + return "<Comment(%d,%s,%s,%s)>" % (self.id, self.creator.user_name, + self.topic_id, self.create_time) + + def _get_latest(self): + if not len(self.revisions): + raise ValueError("No latest revision exists") + return self.revisions[0] + + def _set_latest(self, rev): + self.revisions.insert(0, rev) + + latest = property(_get_latest, _set_latest) + + @classmethod + def find(cls, id, instance_filter=True): + try: + q = meta.Session.query(Comment) + q = q.filter(Comment.id==id) + return q.one() + except: + return None + + def _index_id(self): + return self.id + + +Comment.reply = relation(Comment, cascade='delete', + remote_side=Comment.id, + backref=backref('replies')) \ No newline at end of file diff --git a/adhocracy/model/delegateable.py b/adhocracy/model/delegateable.py new file mode 100644 index 000000000..5a84ee6be --- /dev/null +++ b/adhocracy/model/delegateable.py @@ -0,0 +1,108 @@ +import random + +from sqlalchemy import Table, Column, Integer, Unicode, String, ForeignKey, DateTime, func +from sqlalchemy.orm import relation, backref +from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound + +import meta +import filter +from meta import Base +from user import User + +category_graph = Table('category_graph', Base.metadata, + Column('parent_id', Unicode(10), ForeignKey('delegateable.id')), + Column('child_id', Unicode(10), ForeignKey('delegateable.id')) + ) + +class Delegateable(Base): + PK_LENGTH = 5 + PK_ALPHABET = "abcdefgehijklmnopqrstuvwxyz0123456789" + + __tablename__ = 'delegateable' + + id = Column(Unicode(10), primary_key=True) + label = Column(Unicode(255), nullable=False) + delgateable_type = Column('type', String(50)) + + create_time = Column(DateTime, default=func.now()) + access_time = Column(DateTime, default=func.now(), onupdate=func.now()) + delete_time = Column(DateTime, nullable=True) + + creator_id = Column(Integer, ForeignKey('user.id'), nullable=False) + creator = relation(User, + primaryjoin="Delegateable.creator_id==User.id", + backref=backref('delegateables', cascade='delete')) + + instance_id = Column(Integer, ForeignKey('instance.id'), nullable=False) + instance = relation('Instance', + primaryjoin="Delegateable.instance_id==Instance.id", + backref=backref('delegateables', cascade='delete')) + + __mapper_args__ = {'polymorphic_on': delgateable_type, + 'extension': Base.__mapper_args__.get('extension')} + + def __init__(self): + raise Exception("Make a category or a motion instead!") + + def init_child(self, instance, label, creator): + self.instance = instance + self.id = self.make_key() + self.label = label + self.creator = creator + + def __repr__(self): + return u"<Delegateable(%s,%s)>" % (self.id, self.instance.key) + + def __hash__(self): + return int(self.id, len(Delegateable.PK_ALPHABET) - 1) + + def is_super(self, delegateable): + if delegateable in self.children: + return True + for child in self.children: + r = child.is_super(delegateable) + if r: + return True + return False + + def is_sub(self, delegateable): + return delegateable.is_super(self) + + @classmethod + def find(cls, id, instance_filter=True): + id = unicode(id.upper()) + try: + q = meta.Session.query(Delegateable) + q = q.filter(Delegateable.id==id) + q = q.filter(Delegateable.delete_time==None) + if filter.has_instance() and instance_filter: + q = q.filter(Delegateable.instance_id==filter.get_instance().id) + return q.one() + except: + return None + + def _index_id(self): + return self.id.upper() + + @staticmethod + def make_key(): + """ + Generate an alphabet-based unique key for a Delegateable. + Time needed for this grows as key space becomes less sparse. + """ + while True: + candidate = u''.join(random.sample(Delegateable.PK_ALPHABET, Delegateable.PK_LENGTH)).upper() + try: + meta.Session.query(Delegateable).filter(Delegateable.id==candidate).one() + except NoResultFound: + return candidate + except MultipleResultsFound: + pass + +Delegateable.__mapper__.add_property('parents', relation(Delegateable, lazy=False, secondary=category_graph, + primaryjoin=Delegateable.__table__.c.id == category_graph.c.parent_id, + secondaryjoin=category_graph.c.child_id == Delegateable.__table__.c.id)) + +Delegateable.__mapper__.add_property('children', relation(Delegateable, lazy=False, secondary=category_graph, + primaryjoin=Delegateable.__table__.c.id == category_graph.c.child_id, + secondaryjoin=category_graph.c.parent_id == Delegateable.__table__.c.id)) \ No newline at end of file diff --git a/adhocracy/model/delegation.py b/adhocracy/model/delegation.py new file mode 100644 index 000000000..ae1490258 --- /dev/null +++ b/adhocracy/model/delegation.py @@ -0,0 +1,67 @@ +from sqlalchemy import Column, Integer, Unicode, ForeignKey, DateTime, func +from sqlalchemy.orm import relation, backref + +import meta +import filter as ifilter +from meta import Base +from user import User +from delegateable import Delegateable + +class Delegation(Base): + __tablename__ = 'delegation' + + id = Column(Integer, primary_key=True) + + agent_id = Column(Integer, ForeignKey('user.id'), nullable=False) + agent = relation(User, + primaryjoin="Delegation.agent_id==User.id", + backref=backref('agencies', cascade='all')) + + principal_id = Column(Integer, ForeignKey('user.id'), nullable=False) + principal = relation(User, + primaryjoin="Delegation.principal_id==User.id", + backref=backref('delegated', cascade='all')) + + scope_id = Column(Unicode(10), ForeignKey('delegateable.id'), nullable=False) + scope = relation(Delegateable, + primaryjoin="Delegation.scope_id==Delegateable.id", + backref=backref('delegations', cascade='all')) + + create_time = Column(DateTime, default=func.now()) + revoke_time = Column(DateTime, default=None, nullable=True) + + def __init__(self, principal, agent, scope): + self.principal = principal + self.agent = agent + self.scope = scope + + def __repr__(self): + return u"<Delegation(%s,%s->%s,%s)>" % (self.id, + self.principal.user_name, + self.agent.user_name, + self.scope.id) + + def is_match(self, delegateable): + if self.revoke_time: + return False + if not self.principal.has_permission("vote.cast"): + return False + return self.scope == delegateable or self.scope.is_super(delegateable) + + @classmethod + def find(cls, id, instance_filter=True): + try: + q = meta.Session.query(Delegation) + q = q.filter(Delegation.id==id) + d = q.one() + if ifilter.has_instance() and instance_filter: + if d.scope.instance != ifilter.get_instance(): + return None + return d + except Exception: + return None + + def _index_id(self): + return self.id + + \ No newline at end of file diff --git a/adhocracy/model/dependency.py b/adhocracy/model/dependency.py new file mode 100644 index 000000000..1d89a79b0 --- /dev/null +++ b/adhocracy/model/dependency.py @@ -0,0 +1,32 @@ +from sqlalchemy import Column, Integer, Unicode, ForeignKey, DateTime, func +from sqlalchemy.orm import relation, backref + +from meta import Base +from motion import Motion + +class Dependency(Base): + __tablename__ = "dependency" + + id = Column(Integer, primary_key=True) + create_time = Column(DateTime, default=func.now()) + delete_time = Column(DateTime, nullable=True) + + motion_id = Column(Unicode(10), ForeignKey('motion.id'), nullable=False) + requirement_id = Column(Unicode(10), ForeignKey('motion.id'), nullable=False) + + def __init__(self, motion, requirement): + if motion == requirement: + raise ValueError() + self.motion = motion + self.requirement = requirement + + def __repr__(self): + return "<Depdendency(%s,%s)>" % (self.motion_id, self.requirement_id) + +Dependency.motion = relation(Motion, primaryjoin="Dependency.motion_id==Motion.id", + foreign_keys=[Dependency.motion_id], + backref=backref('dependencies', cascade='all')) + +Dependency.requirement = relation(Motion, primaryjoin="Dependency.requirement_id==Motion.id", + foreign_keys=[Dependency.requirement_id], + backref=backref('dependents', cascade='all')) \ No newline at end of file diff --git a/adhocracy/model/filter.py b/adhocracy/model/filter.py new file mode 100644 index 000000000..74af520cb --- /dev/null +++ b/adhocracy/model/filter.py @@ -0,0 +1,15 @@ +from beaker.util import ThreadLocal + +thread_instance = ThreadLocal() + +def setup_thread(instance): + global thread_instance + thread_instance.put(instance) + +def has_instance(): + return thread_instance.get() != None + +def get_instance(): + return thread_instance.get() + + diff --git a/adhocracy/model/forms.py b/adhocracy/model/forms.py new file mode 100644 index 000000000..3b7824f8e --- /dev/null +++ b/adhocracy/model/forms.py @@ -0,0 +1,187 @@ +import re +import formencode +from formencode import validators, foreach + +from pylons.i18n.translation import * + +import meta +import user +import vote +import delegateable +import motion +import category +import issue +import group +import revision +import comment +import instance + +FORBIDDEN_NAMES = ["www", "static", "mail", "edit", "create", "settings", "join", "leave", + "control", "test", "support"] + +VALIDUSER = re.compile("^[a-zA-Z0-9_\-]{3,255}$") +class UniqueUsername(formencode.FancyValidator): + def _to_python(self, value, state): + if not value: + raise formencode.Invalid( + _('No username is given'), + value, state) + if not VALIDUSER.match(value) or value in FORBIDDEN_NAMES: + raise formencode.Invalid( + _('The username is invalid'), + value, state) + if meta.Session.query(user.User.user_name).filter(user.User.user_name == value).all(): + raise formencode.Invalid( + _('That username already exists'), + value, state) + return value + +class UniqueEmail(formencode.FancyValidator): + def _to_python(self, value, state): + email = value.lower() + if meta.Session.query(user.User.email).filter(user.User.email == email).all(): + raise formencode.Invalid( + _('That email is already registered'), + value, state) + return value + +class UniqueInstanceKey(formencode.FancyValidator): + def _to_python(self, value, state): + if not value: + raise formencode.Invalid( + _('No instance key is given'), + value, state) + if not instance.Instance.INSTANCE_KEY.match(value) or value in FORBIDDEN_NAMES: + raise formencode.Invalid( + _('The instance key is invalid'), + value, state) + if instance.Instance.find(value): + raise formencode.Invalid( + _('An instance with that key already exists'), + value, state) + return value + +class ValidDelegateable(formencode.FancyValidator): + def _to_python(self, value, state): + dgb = delegateable.Delegateable.find(value) + if not dgb: + raise formencode.Invalid( + _("No entity with ID '%s' exists") % value, + value, state) + return dgb + +class ValidCategory(formencode.FancyValidator): + def _to_python(self, value, state): + cat = category.Category.find(value) + if not cat: + raise formencode.Invalid( + _("No category with ID '%s' exists") % value, + value, state) + return cat + +class ValidIssue(formencode.FancyValidator): + def _to_python(self, value, state): + iss = issue.Issue.find(value) + if not iss: + raise formencode.Invalid( + _("No issue with ID '%s' exists") % value, + value, state) + return iss + +class ValidMotion(formencode.FancyValidator): + def _to_python(self, value, state): + mot = motion.Motion.find(value) + if not mot: + raise formencode.Invalid( + _("No motion with ID '%s' exists") % value, + value, state) + return mot + +class ValidGroup(formencode.FancyValidator): + def _to_python(self, value, state): + grp = group.Group.by_code(value) + if not grp: + raise formencode.Invalid( + _("No group with ID '%s' exists") % value, + value, state) + return grp + +class ValidRevision(formencode.FancyValidator): + def _to_python(self, value, state): + rev = revision.Revision.find(value) + if not rev: + raise formencode.Invalid( + _("No revision with ID '%s' exists") % value, + value, state) + return rev + +class ValidComment(formencode.FancyValidator): + def _to_python(self, value, state): + cmt = comment.Comment.find(value) + if not cmt: + raise formencode.Invalid( + _("No comment with ID '%s' exists") % value, + value, state) + return cmt + +class ValidMotionState(formencode.FancyValidator): + def _to_python(self, value, state): + if not value in motion.Motion.STATES: + raise formencode.Invalid( + _("'%s' is not a valid motion state."), + value, state) + return value + +class ExistingUserName(formencode.FancyValidator): + def _to_python(self, value, state): + u = user.User.find(value) + if not u: + raise formencode.Invalid( + _("No user with the user name '%s' exists") % value, + value, state) + return u + + +class EditorAddForm(formencode.Schema): + allow_extra_fields = True + editor = ExistingUserName(not_empty=True) + motion = ValidMotion(not_emtpy=True) + +class EditorRemoveForm(formencode.Schema): + allow_extra_fields = True + editor = ExistingUserName(not_empty=True) + motion = ValidMotion(not_emtpy=True) + +class VoteCastForm(formencode.Schema): + allow_extra_fields = True + orientation = validators.Int(min=vote.Vote.NAY, max=vote.Vote.AYE, not_empty=True) + +class CategoryCreateForm(formencode.Schema): + allow_extra_fields = True + label = validators.String(max=255, min=4, not_empty=True) + description = validators.String(max=1000, if_empty=None, not_empty=False) + categories = ValidCategory(not_empty=True) + +class CategoryEditForm(formencode.Schema): + allow_extra_fields = True + label = validators.String(max=255, min=4, not_empty=True) + description = validators.String(max=1000, if_empty=None, not_empty=False) + categories = ValidCategory(not_emtpy=True) + +class DelegationCreateForm(formencode.Schema): + allow_extra_fields = True + agent = ExistingUserName() + +class AdminUpdateMembershipForm(formencode.Schema): + allow_extra_fields = True + user = ExistingUserName() + to_group = ValidGroup() + +class AdminForceLeaveForm(formencode.Schema): + allow_extra_fields = True + user = ExistingUserName() + +class EventPanelForm(formencode.Schema): + allow_extra_fields = True + event_page = validators.Int(if_missing=1, not_empty=False) + event_count = validators.Int(if_missing=None, if_invalid=None, max=100, not_empty=False) \ No newline at end of file diff --git a/adhocracy/model/group.py b/adhocracy/model/group.py new file mode 100644 index 000000000..d1cc13a82 --- /dev/null +++ b/adhocracy/model/group.py @@ -0,0 +1,60 @@ +from sqlalchemy import Column, Integer, Unicode + +import meta +from meta import Base + +class Group(Base): + __tablename__ = 'group' + + CODE_ANONYMOUS = u"anonymous" + CODE_OBSERVER = u"observer" + CODE_VOTER = u"voter" + CODE_SUPERVISOR = u"supervisor" + CODE_ADMIN = u"admin" + CODE_DEFAULT = u"default" + + INSTANCE_GROUPS = [CODE_OBSERVER, CODE_VOTER, CODE_SUPERVISOR] + INSTANCE_DEFAULT = CODE_VOTER + + id = Column(Integer, primary_key=True) + group_name = Column(Unicode(255), nullable=False, unique=True) + code = Column(Unicode(255), nullable=False, unique=True) + description = Column(Unicode(1000)) + + def __init__(self, group_name, code, description=None): + self.group_name = group_name + self.code = code + self.description = description + + def __repr__(self): + return u"<Group(%d,%s)>" % (self.id, self.code) + + @classmethod + def all(cls): + return meta.Session.query(Group).all() + + @classmethod + def find(cls, group_name, instance_filter=True): + try: + return meta.Session.query(Group).filter(Group.group_name==group_name).one() + except: + return None + + def _index_id(self): + return self.group_name + + + @classmethod + def by_id(cls, id): + try: + return meta.Session.query(Group).filter(Group.id==id).one() + except: + return None + + @classmethod + def by_code(cls, code): + try: + return meta.Session.query(Group).filter(Group.code==code).one() + except: + return None + diff --git a/adhocracy/model/hooks.py b/adhocracy/model/hooks.py new file mode 100644 index 000000000..2984cafbc --- /dev/null +++ b/adhocracy/model/hooks.py @@ -0,0 +1,74 @@ +# http://www.mail-archive.com/sqlalchemy@googlegroups.com/msg09203.html + +from sqlalchemy.orm import MapperExtension, EXT_CONTINUE + +PREINSERT = "_pre_insert" +PREDELETE = "_pre_delete" +PREUPDATE = "_pre_update" +POSTINSERT = "_post_insert" +POSTDELETE = "_post_delete" +POSTUPDATE = "_post_update" + +class HookExtension(MapperExtension): + """ Extention to add pre-commit hooks. + + Hooks will be called in Mapped classes if they define any of these + methods: + * _pre_insert() + * _pre_delete() + * _pre_update() + """ + + def before_insert(self, mapper, connection, instance): + if getattr(instance, PREINSERT, None): + instance._pre_insert() + return EXT_CONTINUE + + def before_delete(self, mapper, connection, instance): + if getattr(instance, PREDELETE, None): + instance._pre_delete() + return EXT_CONTINUE + + def before_update(self, mapper, connection, instance): + if getattr(instance, PREUPDATE, None): + instance._pre_update() + return EXT_CONTINUE + + def after_insert(self, mapper, connection, instance): + if getattr(instance, POSTINSERT, None): + instance._post_insert() + return EXT_CONTINUE + + def after_delete(self, mapper, connection, instance): + if getattr(instance, POSTDELETE, None): + instance._post_delete() + return EXT_CONTINUE + + def after_update(self, mapper, connection, instance): + if getattr(instance, POSTUPDATE, None): + instance._post_update() + return EXT_CONTINUE + +def patch_some(clazz, hooks, f): + for hook in hooks: + patch(clazz, hook, f) + +def patch_pre(clazz, f): + patch_some(clazz, [PREINSERT, PREUPDATE, PREDELETE], f) + +def patch_post(clazz, f): + patch_some(clazz, [POSTINSERT, POSTUPDATE, POSTDELETE], f) + +def patch_default(clazz, f): + patch_some(clazz, [POSTINSERT, POSTUPDATE, PREDELETE], f) + +def patch(clazz, hook, f): + prev = getattr(clazz, hook, None) + a = f + if prev: + def chain(*a, **kw): + f(*a, **kw) + prev(*a, **kw) + a = chain + setattr(clazz, hook, a) + \ No newline at end of file diff --git a/adhocracy/model/instance.py b/adhocracy/model/instance.py new file mode 100644 index 000000000..0727f3eb6 --- /dev/null +++ b/adhocracy/model/instance.py @@ -0,0 +1,102 @@ +import re + +from sqlalchemy import Column, Integer, Float, Unicode, UnicodeText, ForeignKey, DateTime, func +from sqlalchemy.orm import relation, synonym, backref + +import meta +from meta import Base +import user + +class Instance(Base): + __tablename__ = 'instance' + + INSTANCE_KEY = re.compile("^[a-zA-Z][a-zA-Z0-9_]{2,18}$") + + id = Column(Integer, primary_key=True) + _key = Column('key', Unicode(20), nullable=False, unique=True) + _label = Column('label', Unicode(255), nullable=False) + description = Column(UnicodeText(), nullable=True) + + required_majority = Column(Float, nullable=False) + activation_delay = Column(Integer, nullable=False) + + create_time = Column(DateTime, default=func.now()) + access_time = Column(DateTime, default=func.now(), onupdate=func.now()) + + creator_id = Column(Integer, ForeignKey('user.id'), nullable=False) + creator = relation(user.User, + primaryjoin="Instance.creator_id==User.id", + backref=backref('created_instances')) + + default_group_id = Column(Integer, ForeignKey('group.id'), nullable=True) + default_group = relation('Group') + + root_id = Column(Unicode(10), + ForeignKey('category.id', use_alter=True, name='inst_root_cat'), + nullable=True) + + def __init__(self, key, label, creator, description=None): + self.key = key + self.label = label + self.creator = creator + self.description = description + self.required_majority = 0.66 + self.activation_delay = 7 + + def __repr__(self): + return u"<Instance(%d,%s)>" % (self.id, self.key) + + def _get_key(self): + return self._key + + def _set_key(self, value): + self._key = value.lower() + + key = synonym('_key', descriptor=property(_get_key, + _set_key)) + + def _get_members(self): + members = [] + for membership in self.memberships: + if not membership.expire_time: + members.append(membership.user) + global_membership = model.Permission.by_code('global-member') + for group in global_membership.groups: + for membership in group.memberships: + if membership.instance == None and not membership.expire_time: + members.append(membership.user) + return members + + members = property(_get_members) + + def _get_label(self): + return self._label + + def _set_label(self, label): + self._label = label + if self.root: + self.root.label = label + + label = synonym('_label', descriptor=property(_get_label, + _set_label)) + + @classmethod + def find(cls, key, instance_filter=True): + key = unicode(key.lower()) + try: + return meta.Session.query(Instance).filter(Instance.key==key).one() + except: + return None + + def _index_id(self): + return self.key + + @classmethod + def all(cls): + return meta.Session.query(Instance).all() + + +Instance.root = relation('Category', + primaryjoin="Instance.root_id==Category.id", + foreign_keys=[Instance.root_id], + uselist=False) \ No newline at end of file diff --git a/adhocracy/model/issue.py b/adhocracy/model/issue.py new file mode 100644 index 000000000..176540c88 --- /dev/null +++ b/adhocracy/model/issue.py @@ -0,0 +1,68 @@ +from sqlalchemy import Column, Unicode, ForeignKey, Integer +from sqlalchemy.orm import relation +from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound + +import meta +import filter +from delegateable import Delegateable + +class Issue(Delegateable): + __tablename__ = 'issue' + __mapper_args__ = {'polymorphic_identity': 'issue'} + + id = Column(Unicode(10), ForeignKey('delegateable.id'), primary_key=True) + comment_id = Column(Integer, ForeignKey('comment.id'), nullable=True) + + def __init__(self, instance, label, creator): + self.init_child(instance, label, creator) + + def __repr__(self): + return u"<Issue(%s)>" % (self.id) + + def _get_motions(self): + return self.children + + def _set_motions(self, motions): + self.children = motions + + motions = property(_get_motions, _set_motions) + + def search_children(self, cls=Delegateable): + """ + Get all child elements of type "cls". Uses DFS. + """ + children = [] + for child in self.children: + if child.delete_time: + continue + if isinstance(child, cls): + children.append(child) + return children + + @classmethod + def find(cls, id, instance_filter=True): + id = unicode(id.upper()) + try: + q = meta.Session.query(Issue) + q = q.filter(Issue.id==id) + q = q.filter(Issue.delete_time==None) + if filter.has_instance() and instance_filter: + q = q.filter(Issue.instance_id==filter.get_instance().id) + return q.one() + except NoResultFound: + return None + except MultipleResultsFound: + return None + + @classmethod + def all(cls, instance=None): + q = meta.Session.query(Issue) + q = q.filter(Issue.delete_time==None) + if instance: + q.filter(Issue.instance==instance) + return q.all() + +Issue.comment = relation('Comment', + primaryjoin="Issue.comment_id==Comment.id", + foreign_keys=[Issue.comment_id], + uselist=False) \ No newline at end of file diff --git a/adhocracy/model/karma.py b/adhocracy/model/karma.py new file mode 100644 index 000000000..dea1c6861 --- /dev/null +++ b/adhocracy/model/karma.py @@ -0,0 +1,43 @@ +from sqlalchemy import Column, Integer, Unicode, ForeignKey, DateTime, func +from sqlalchemy.orm import relation, backref + +from meta import Base +import meta +import filter as ifilter + +from instance import Instance +from user import User +from comment import Comment +from delegateable import Delegateable + +class Karma(Base): + __tablename__ = 'karma' + + id = Column(Integer, primary_key=True) + value = Column(Integer, nullable=False) + create_time = Column(DateTime, default=func.now()) + + comment_id = Column(Integer, ForeignKey('comment.id'), nullable=False) + comment = relation(Comment, backref=backref('karmas', cascade='delete')) + + donor_id = Column(Integer, ForeignKey('user.id'), nullable=False) + donor = relation(User, primaryjoin="Karma.donor_id==User.id", + backref=backref('karma_given')) + + recipient_id = Column(Integer, ForeignKey('user.id'), nullable=False) + recipient = relation(User, primaryjoin="Karma.recipient_id==User.id", + backref=backref('karma_received')) + + def __init__(self, value, donor, recipient, comment): + self.value = value + self.donor = donor + self.comment = comment + self.recipient = recipient + + def __repr__(self): + return "<Karma(%s,%s,%s,%s,%s)>" % (self.id, self.donor.user_name, + self.value, self.recipient.user_name, + self.comment.id) + + def _index_id(self): + return self.id diff --git a/adhocracy/model/membership.py b/adhocracy/model/membership.py new file mode 100644 index 000000000..e71ddfa14 --- /dev/null +++ b/adhocracy/model/membership.py @@ -0,0 +1,45 @@ +from sqlalchemy import Column, Integer, ForeignKey, DateTime, func, Boolean +from sqlalchemy.orm import relation, backref + +import filter as ifilter +from meta import Base +import user + +class Membership(Base): + __tablename__ = 'membership' + + id = Column(Integer, primary_key=True) + approved = Column(Boolean, nullable=True) + + create_time = Column(DateTime, default=func.now()) + expire_time = Column(DateTime, nullable=True) + access_time = Column(DateTime, default=func.now(), onupdate=func.now()) + + user_id = Column(Integer, ForeignKey('user.id'), nullable=False) + user = relation(user.User, + primaryjoin="Membership.user_id==User.id", + backref=backref('memberships')) + + instance_id = Column(Integer, ForeignKey('instance.id'), nullable=True) + instance = relation('Instance', backref=backref('memberships')) + + group_id = Column(Integer, ForeignKey('group.id'), nullable=False) + group = relation('Group', backref=backref('memberships')) + + #def _get_context_group(self): + # if not self.instance or self.instance == ifilter.get_instance(): + # return self.group + + #context_group = property(_get_context_group) + + def __init__(self, user, instance, group, approved=True): + self.user = user + self.instance = instance + self.group = group + self.approved = approved + + def __repr__(self): + return u"<Membership(%d,%s,%s,%s)>" % (self.id, + self.user.user_name, + self.instance and self.instance.key or "", + self.group.code) \ No newline at end of file diff --git a/adhocracy/model/meta.py b/adhocracy/model/meta.py new file mode 100644 index 000000000..cc31d307b --- /dev/null +++ b/adhocracy/model/meta.py @@ -0,0 +1,20 @@ +"""SQLAlchemy Metadata and Session object""" +from sqlalchemy import MetaData +from sqlalchemy.ext.declarative import declarative_base + +import hooks + +__all__ = ['Session', 'metadata', 'Base', 'engine'] + +# SQLAlchemy database engine. Updated by model.init_model() +engine = None + +# SQLAlchemy session manager. Updated by model.init_model() +Session = None + +# Global metadata. If you have multiple databases with overlapping table +# names, you'll need a metadata for each database +metadata = MetaData() + +Base = declarative_base(metadata=metadata) +Base.__mapper_args__ = {'extension': hooks.HookExtension()} \ No newline at end of file diff --git a/adhocracy/model/motion.py b/adhocracy/model/motion.py new file mode 100644 index 000000000..3626b7d32 --- /dev/null +++ b/adhocracy/model/motion.py @@ -0,0 +1,68 @@ +import logging + +from sqlalchemy import Column, Unicode, ForeignKey, Integer +from sqlalchemy.orm import relation + +import meta +import filter + +from delegateable import Delegateable + +log = logging.getLogger(__name__) + +class Motion(Delegateable): + __tablename__ = 'motion' + __mapper_args__ = {'polymorphic_identity': 'motion'} + + id = Column(Unicode(10), ForeignKey('delegateable.id'), primary_key=True) + comment_id = Column(Integer, ForeignKey('comment.id'), nullable=True) + + def __init__(self, instance, label, creator): + self.init_child(instance, label, creator) + + def __repr__(self): + return u"<Motion(%s)>" % self.id + + def _get_issue(self): + if len(self.parents) != 1: + raise ValueError(_("Motion doesn't have a distinct parent issue.")) + return self.parents[0] + + def _set_issue(self, issue): + self.parents = [issue] + + issue = property(_get_issue, _set_issue) + + def _get_poll(self): + for poll in self.polls: + if not poll.end_time: + return poll + return None + + poll = property(_get_poll) + + @classmethod + def find(cls, id, instance_filter=True): + id = unicode(id.upper()) + try: + q = meta.Session.query(Motion) + q = q.filter(Motion.id==id) + q = q.filter(Motion.delete_time==None) + if filter.has_instance() and instance_filter: + q = q.filter(Motion.instance_id==filter.get_instance().id) + return q.one() + except: + return None + + @classmethod + def all(cls, instance=None): + q = meta.Session.query(Motion) + q = q.filter(Motion.delete_time==None) + if instance: + q.filter(Motion.instance==instance) + return q.all() + +Motion.comment = relation('Comment', + primaryjoin="Motion.comment_id==Comment.id", + foreign_keys=[Motion.comment_id], + uselist=False) \ No newline at end of file diff --git a/adhocracy/model/permission.py b/adhocracy/model/permission.py new file mode 100644 index 000000000..c67ce6f74 --- /dev/null +++ b/adhocracy/model/permission.py @@ -0,0 +1,42 @@ +from sqlalchemy import Table, Column, Integer, Unicode, String, ForeignKey, DateTime, func, Boolean +from sqlalchemy.orm import relation, synonym, backref +from pylons import g + +import meta +from meta import Base + +group_permission = Table('group_permission', Base.metadata, + Column('group_id', Integer, ForeignKey('group.id', + onupdate="CASCADE", ondelete="CASCADE")), + Column('permission_id', Integer, ForeignKey('permission.id', + onupdate="CASCADE", ondelete="CASCADE")) +) + +class Permission(Base): + __tablename__ = 'permission' + + id = Column(Integer, primary_key=True) + permission_name = Column(Unicode(255), nullable=False, unique=True) + + groups = relation('Group', secondary=group_permission, + backref='permissions') + + def __init__(self, permission_name): + self.permission_name = permission_name + + def __repr__(self): + return u"<Permission(%d,%s)>" % (self.id, self.code) + + @classmethod + def find(cls, permission_name, instance_filter=True): + try: + return meta.Session.query(Permission).filter(Permission.permission_name==permission_name).one() + except: + return None + + def _index_id(self): + return self.permission_name + + @classmethod + def all(cls): + return meta.Session.query(Permission).all() \ No newline at end of file diff --git a/adhocracy/model/poll.py b/adhocracy/model/poll.py new file mode 100644 index 000000000..d51993bde --- /dev/null +++ b/adhocracy/model/poll.py @@ -0,0 +1,49 @@ +from sqlalchemy import Column, Integer, Unicode, ForeignKey, DateTime, func +from sqlalchemy.orm import relation, backref + +from meta import Base +import user +import motion + +class Poll(Base): + __tablename__ = 'poll' + + id = Column(Integer, primary_key=True) + begin_time = Column(DateTime, default=func.now()) + end_time = Column(DateTime, nullable=True) + + begin_user_id = Column(Integer, ForeignKey('user.id'), nullable=False) + begin_user = relation(user.User, + primaryjoin="Poll.begin_user_id==User.id") + + end_user_id = Column(Integer, ForeignKey('user.id'), nullable=True) + end_user = relation(user.User, + primaryjoin="Poll.end_user_id==User.id") + + motion_id = Column(Unicode(10), ForeignKey('motion.id'), nullable=False) + + def __init__(self, motion, begin_user): + self.motion = motion + self.begin_user = begin_user + + def __repr__(self): + return u"<Poll(%d,%s,%s,%s)>" % (self.id, + self.motion_id, + self.begin_time, + self.end_time) + + @classmethod + def find(cls, id, instance_filter=True): + try: + q = meta.Session.query(Poll) + q = q.filter(Poll.id==int(id)) + poll = q.one() + if ifilter.has_instance() and instance_filter: + poll = poll.motion.instance == ifilter.get_instance() \ + and poll or None + return poll + except Exception: + return None + +Poll.motion = relation(motion.Motion, backref=backref('polls', cascade='delete', + lazy=False, order_by=Poll.begin_time.desc())) \ No newline at end of file diff --git a/adhocracy/model/revision.py b/adhocracy/model/revision.py new file mode 100644 index 000000000..d0e66e387 --- /dev/null +++ b/adhocracy/model/revision.py @@ -0,0 +1,47 @@ +from sqlalchemy import Column, Integer, UnicodeText, ForeignKey, DateTime, func +from sqlalchemy.orm import relation, backref + +import meta +from meta import Base +import user +import comment + +class Revision(Base): + __tablename__ = 'revision' + + id = Column(Integer, primary_key=True) + create_time = Column(DateTime, default=func.now()) + text = Column(UnicodeText(), nullable=False) + sentiment = Column(Integer, default=0) + + user_id = Column(Integer, ForeignKey('user.id'), nullable=False) + user = relation(user.User, primaryjoin="Revision.user_id==User.id", + backref=backref('revisions')) + + comment_id = Column(Integer, ForeignKey('comment.id'), nullable=False) + + def __init__(self, comment, user, text): + self.comment = comment + self.user = user + self.text = text + + def __repr__(self): + return u"<Revision(%d,%s,%s)>" % (self.id, + self.user.user_name, + self.comment_id) + + @classmethod + def find(cls, id, instance_filter=True): + try: + return meta.Session.query(Revision).filter(Revision.id==id).one() + except: + return None + + def _index_id(self): + return self.id + + +Revision.comment = relation(comment.Comment, + backref=backref('revisions', cascade='all', + lazy=False, + order_by=Revision.create_time.desc())) diff --git a/adhocracy/model/user.py b/adhocracy/model/user.py new file mode 100644 index 000000000..1c8260f15 --- /dev/null +++ b/adhocracy/model/user.py @@ -0,0 +1,178 @@ +import hashlib +import os +import logging + +from sqlalchemy import Column, Integer, Unicode, UnicodeText, Boolean, DateTime, func, or_ +from sqlalchemy.orm import synonym +from sqlalchemy.ext.associationproxy import association_proxy + +from babel import Locale + +import meta +import filter as ifilter +from meta import Base +import group + +log = logging.getLogger(__name__) + +class User(Base): + __tablename__ = 'user' + + id = Column(Integer, primary_key=True) + user_name = Column(Unicode(255), nullable=False, unique=True, index=True) + display_name = Column(Unicode(255), nullable=True) + bio = Column(UnicodeText(), nullable=True) + email = Column(Unicode(255), nullable=False, unique=False) + activation_code = Column(Unicode(255), nullable=True, unique=False) + reset_code = Column(Unicode(255), nullable=True, unique=False) + _password = Column('password', Unicode(80), nullable=False) + _locale = Column('locale', Unicode(7), nullable=True) + create_time = Column(DateTime, default=func.now()) + access_time = Column(DateTime, default=func.now(), onupdate=func.now()) + + def __init__(self, user_name, email, password, display_name=None, bio=None): + self.user_name = user_name + self.email = email + self.password = password + self.display_name = display_name + self.bio = bio + + def __repr__(self): + return u"<User(%s,%s)>" % (self.id, self.user_name) + + def _get_name(self): + return self.display_name.strip() \ + if self.display_name and len(self.display_name.strip()) > 0 \ + else self.user_name + + name = property(_get_name) + + def _get_locale(self): + if not self._locale: + return None + return Locale.parse(self._locale) + + def _set_locale(self, locale): + self._locale = str(locale) + + locale = synonym('_locale', descriptor=property(_get_locale, + _set_locale)) + + def _get_alternatives(self): + return [] + + def _set_alternatives(self, alternatives): + pass + + alternatives = property(_get_alternatives, _set_alternatives) + + def _get_context_groups(self): + groups = [] + for membership in self.memberships: + if membership.expire_time: + continue + if not membership.instance or membership.instance == ifilter.get_instance(): + groups.append(membership.group) + return groups + + groups = property(_get_context_groups) + + def has_permission(self, permission_name): + for group in self.groups: + for perm in group.permissions: + if perm.permission_name == permission_name: + return True + return False + + def is_member(self, instance): + for membership in self.memberships: + if membership.expire_time: + continue + if membership.instance == instance: + return True + return self.has_permission('global-member') + + def _get_instances(self): + instances = [] + for membership in self.memberships: + if membership.expire_time: + continue + if membership.instance: + instances.append(membership.instance) + return list(set(instances)) + + instances = property(_get_instances) + + def _set_password(self, password): + """Hash password on the fly.""" + + if isinstance(password, unicode): + password_8bit = password.encode('UTF-8') + else: + password_8bit = password + + salt = hashlib.sha1(os.urandom(60)) + hash = hashlib.sha1(password_8bit + salt.hexdigest()) + hashed_password = salt.hexdigest() + hash.hexdigest() + + if not isinstance(hashed_password, unicode): + hashed_password = hashed_password.decode('UTF-8') + + self._password = hashed_password + + def _get_password(self): + """Return the password hashed""" + return self._password + + password = synonym('_password', descriptor=property(_get_password, + _set_password)) + + def validate_password(self, password): + """ + Check the password against existing credentials. + + :param password: the password that was provided by the user to + try and authenticate. This is the clear text version that we will + need to match against the hashed one in the database. + :type password: unicode object. + :return: Whether the password is valid. + :rtype: bool + + """ + hashed_pass = hashlib.sha1(password + self.password[:40]) + return self.password[40:] == hashed_pass.hexdigest() + + @classmethod + def complete(cls, prefix, limit=5, instance_filter=True): + q = meta.Session.query(User) + q = q.filter(or_(User.user_name.like(prefix + "%"), + User.display_name.like(prefix + "%"))) + q = q.limit(limit) + completions = q.all() + if ifilter.has_instance() and instance_filter: + inst = ifilter.get_instance() + completions = filter(lambda u: u.is_member(inst), completions) + return completions + + @classmethod + def find(cls, user_name, instance_filter=True): + try: + q = meta.Session.query(User) + q = q.filter(User.user_name==unicode(user_name)) + user = q.one() + if ifilter.has_instance() and instance_filter: + user = user.is_member(ifilter.get_instance()) and user or None + return user + except Exception: + return None + + def _index_id(self): + return self.user_name + + @classmethod + def all(cls, instance_filter=True): + users = meta.Session.query(User).all() + if ifilter.has_instance() and instance_filter: + users = filter(lambda user: user.is_member(ifilter.get_instance()), + users) + return users \ No newline at end of file diff --git a/adhocracy/model/vote.py b/adhocracy/model/vote.py new file mode 100644 index 000000000..e3fad1594 --- /dev/null +++ b/adhocracy/model/vote.py @@ -0,0 +1,68 @@ +from sqlalchemy import Column, Integer, Unicode, ForeignKey, DateTime, func +from sqlalchemy.orm import relation, backref + +from meta import Base +import meta +import filter as ifilter + +from user import User +from delegation import Delegation +from poll import Poll + +class Vote(Base): + AYE = 1 + NAY = -1 + ABSTAIN = 0 + + __tablename__ = 'vote' + + id = Column(Integer, primary_key=True) + orientation = Column(Integer, nullable=False) + create_time = Column(DateTime, default=func.now()) + + user_id = Column(Integer, ForeignKey('user.id'), nullable=False) + # user See below + + poll_id = Column(Integer, ForeignKey('poll.id'), nullable=False) + # motion See below + + delegation_id = Column(Integer, ForeignKey('delegation.id'), nullable=True) + delegation = relation(Delegation, + primaryjoin="Vote.delegation_id==Delegation.id", + backref=backref('votes', cascade='delete')) + + def __init__(self, user, poll, orientation, delegation=None): + self.user = user + self.poll = poll + self.orientation = orientation + self.delegation = delegation + + def __repr__(self): + return "<Vote(%s,%s,%s,%s,%s)>" % (self.id, + self.user.user_name, + self.poll.id, + self.orientation, + self.delegation.id if self.delegation else "DIRECT") + + @classmethod + def find(cls, id, instance_filter=True): + try: + q = meta.Session.query(Vote) + q = q.filter(Vote.id==int(id)) + vote = q.one() + if ifilter.has_instance() and instance_filter: + vote = vote.poll.motion.instance == ifilter.get_instance() \ + and vote or None + return vote + except Exception, e: + return None + + def _index_id(self): + return self.id + +Vote.user = relation(User, + primaryjoin="Vote.user_id==User.id", + backref=backref('votes', cascade='delete', order_by=Vote.create_time.desc())) + +Vote.poll = relation(Poll, + backref=backref('votes', order_by=Vote.create_time.desc())) \ No newline at end of file diff --git a/adhocracy/public/favicon.ico b/adhocracy/public/favicon.ico new file mode 100644 index 000000000..5b24529eb Binary files /dev/null and b/adhocracy/public/favicon.ico differ diff --git a/adhocracy/public/img/about.gif b/adhocracy/public/img/about.gif new file mode 100644 index 000000000..d8c345a20 Binary files /dev/null and b/adhocracy/public/img/about.gif differ diff --git a/adhocracy/public/img/about_page.png b/adhocracy/public/img/about_page.png new file mode 100644 index 000000000..57df8be82 Binary files /dev/null and b/adhocracy/public/img/about_page.png differ diff --git a/adhocracy/public/img/adhocracy_1_motion.png b/adhocracy/public/img/adhocracy_1_motion.png new file mode 100644 index 000000000..7016e09e6 Binary files /dev/null and b/adhocracy/public/img/adhocracy_1_motion.png differ diff --git a/adhocracy/public/img/adhocracy_2_discussion.png b/adhocracy/public/img/adhocracy_2_discussion.png new file mode 100644 index 000000000..44eddf8f7 Binary files /dev/null and b/adhocracy/public/img/adhocracy_2_discussion.png differ diff --git a/adhocracy/public/img/adhocracy_3_delegation.png b/adhocracy/public/img/adhocracy_3_delegation.png new file mode 100644 index 000000000..adcfedd68 Binary files /dev/null and b/adhocracy/public/img/adhocracy_3_delegation.png differ diff --git a/adhocracy/public/img/adhocracy_4_vote.png b/adhocracy/public/img/adhocracy_4_vote.png new file mode 100644 index 000000000..7dc487e93 Binary files /dev/null and b/adhocracy/public/img/adhocracy_4_vote.png differ diff --git a/adhocracy/public/img/adhocracy_button.png b/adhocracy/public/img/adhocracy_button.png new file mode 100644 index 000000000..730f299c0 Binary files /dev/null and b/adhocracy/public/img/adhocracy_button.png differ diff --git a/adhocracy/public/img/adhocracy_large.png b/adhocracy/public/img/adhocracy_large.png new file mode 100644 index 000000000..05d92f626 Binary files /dev/null and b/adhocracy/public/img/adhocracy_large.png differ diff --git a/adhocracy/public/img/arrows/adown.png b/adhocracy/public/img/arrows/adown.png new file mode 100644 index 000000000..4e904df29 Binary files /dev/null and b/adhocracy/public/img/arrows/adown.png differ diff --git a/adhocracy/public/img/arrows/adown_a.png b/adhocracy/public/img/arrows/adown_a.png new file mode 100644 index 000000000..accd24e9a Binary files /dev/null and b/adhocracy/public/img/arrows/adown_a.png differ diff --git a/adhocracy/public/img/arrows/aup.png b/adhocracy/public/img/arrows/aup.png new file mode 100644 index 000000000..f41ef576e Binary files /dev/null and b/adhocracy/public/img/arrows/aup.png differ diff --git a/adhocracy/public/img/arrows/aup_a.png b/adhocracy/public/img/arrows/aup_a.png new file mode 100644 index 000000000..7ddea7f00 Binary files /dev/null and b/adhocracy/public/img/arrows/aup_a.png differ diff --git a/adhocracy/public/img/arrows/down_grey.png b/adhocracy/public/img/arrows/down_grey.png new file mode 100644 index 000000000..8c4e78ab2 Binary files /dev/null and b/adhocracy/public/img/arrows/down_grey.png differ diff --git a/adhocracy/public/img/arrows/up_grey.png b/adhocracy/public/img/arrows/up_grey.png new file mode 100644 index 000000000..fdb115e90 Binary files /dev/null and b/adhocracy/public/img/arrows/up_grey.png differ diff --git a/adhocracy/public/img/groups.png b/adhocracy/public/img/groups.png new file mode 100644 index 000000000..fa11a8f58 Binary files /dev/null and b/adhocracy/public/img/groups.png differ diff --git a/adhocracy/public/img/header_bg.png b/adhocracy/public/img/header_bg.png new file mode 100644 index 000000000..df12e7d42 Binary files /dev/null and b/adhocracy/public/img/header_bg.png differ diff --git a/adhocracy/public/img/header_logo.png b/adhocracy/public/img/header_logo.png new file mode 100644 index 000000000..2e9884e89 Binary files /dev/null and b/adhocracy/public/img/header_logo.png differ diff --git a/adhocracy/public/img/icons/abstain.png b/adhocracy/public/img/icons/abstain.png new file mode 100644 index 000000000..f6bc72100 Binary files /dev/null and b/adhocracy/public/img/icons/abstain.png differ diff --git a/adhocracy/public/img/icons/add.png b/adhocracy/public/img/icons/add.png new file mode 100644 index 000000000..e7e5fd37d Binary files /dev/null and b/adhocracy/public/img/icons/add.png differ diff --git a/adhocracy/public/img/icons/aktive.png b/adhocracy/public/img/icons/aktive.png new file mode 100755 index 000000000..f53ba825d Binary files /dev/null and b/adhocracy/public/img/icons/aktive.png differ diff --git a/adhocracy/public/img/icons/aktive_16.png b/adhocracy/public/img/icons/aktive_16.png new file mode 100755 index 000000000..73eea8f20 Binary files /dev/null and b/adhocracy/public/img/icons/aktive_16.png differ diff --git a/adhocracy/public/img/icons/aktive_20.png b/adhocracy/public/img/icons/aktive_20.png new file mode 100755 index 000000000..c30715aca Binary files /dev/null and b/adhocracy/public/img/icons/aktive_20.png differ diff --git a/adhocracy/public/img/icons/aktive_24.png b/adhocracy/public/img/icons/aktive_24.png new file mode 100755 index 000000000..c5eeaaca4 Binary files /dev/null and b/adhocracy/public/img/icons/aktive_24.png differ diff --git a/adhocracy/public/img/icons/aktive_32.png b/adhocracy/public/img/icons/aktive_32.png new file mode 100755 index 000000000..7223d6949 Binary files /dev/null and b/adhocracy/public/img/icons/aktive_32.png differ diff --git a/adhocracy/public/img/icons/aktive_48.png b/adhocracy/public/img/icons/aktive_48.png new file mode 100755 index 000000000..3341347a5 Binary files /dev/null and b/adhocracy/public/img/icons/aktive_48.png differ diff --git a/adhocracy/public/img/icons/aktive_64.png b/adhocracy/public/img/icons/aktive_64.png new file mode 100755 index 000000000..083fa83e7 Binary files /dev/null and b/adhocracy/public/img/icons/aktive_64.png differ diff --git a/adhocracy/public/img/icons/alt_nay.png b/adhocracy/public/img/icons/alt_nay.png new file mode 100644 index 000000000..c2d0bb7b2 Binary files /dev/null and b/adhocracy/public/img/icons/alt_nay.png differ diff --git a/adhocracy/public/img/icons/aye.png b/adhocracy/public/img/icons/aye.png new file mode 100644 index 000000000..c541e6efc Binary files /dev/null and b/adhocracy/public/img/icons/aye.png differ diff --git a/adhocracy/public/img/icons/bonsai_16.png b/adhocracy/public/img/icons/bonsai_16.png new file mode 100755 index 000000000..8eb96e83f Binary files /dev/null and b/adhocracy/public/img/icons/bonsai_16.png differ diff --git a/adhocracy/public/img/icons/bonsai_20.png b/adhocracy/public/img/icons/bonsai_20.png new file mode 100755 index 000000000..1f6e14679 Binary files /dev/null and b/adhocracy/public/img/icons/bonsai_20.png differ diff --git a/adhocracy/public/img/icons/bonsai_24.png b/adhocracy/public/img/icons/bonsai_24.png new file mode 100755 index 000000000..6306fb376 Binary files /dev/null and b/adhocracy/public/img/icons/bonsai_24.png differ diff --git a/adhocracy/public/img/icons/bonsai_32.png b/adhocracy/public/img/icons/bonsai_32.png new file mode 100755 index 000000000..86580c395 Binary files /dev/null and b/adhocracy/public/img/icons/bonsai_32.png differ diff --git a/adhocracy/public/img/icons/bonsai_64.png b/adhocracy/public/img/icons/bonsai_64.png new file mode 100755 index 000000000..4095101d6 Binary files /dev/null and b/adhocracy/public/img/icons/bonsai_64.png differ diff --git a/adhocracy/public/img/icons/cancel.png b/adhocracy/public/img/icons/cancel.png new file mode 100644 index 000000000..217e33ba4 Binary files /dev/null and b/adhocracy/public/img/icons/cancel.png differ diff --git a/adhocracy/public/img/icons/categories.png b/adhocracy/public/img/icons/categories.png new file mode 100755 index 000000000..464ca94d6 Binary files /dev/null and b/adhocracy/public/img/icons/categories.png differ diff --git a/adhocracy/public/img/icons/categories_16.png b/adhocracy/public/img/icons/categories_16.png new file mode 100755 index 000000000..b358a7fb4 Binary files /dev/null and b/adhocracy/public/img/icons/categories_16.png differ diff --git a/adhocracy/public/img/icons/categories_20.png b/adhocracy/public/img/icons/categories_20.png new file mode 100755 index 000000000..b6dfed388 Binary files /dev/null and b/adhocracy/public/img/icons/categories_20.png differ diff --git a/adhocracy/public/img/icons/categories_24.png b/adhocracy/public/img/icons/categories_24.png new file mode 100755 index 000000000..003dd4641 Binary files /dev/null and b/adhocracy/public/img/icons/categories_24.png differ diff --git a/adhocracy/public/img/icons/categories_32.png b/adhocracy/public/img/icons/categories_32.png new file mode 100755 index 000000000..b83cf4399 Binary files /dev/null and b/adhocracy/public/img/icons/categories_32.png differ diff --git a/adhocracy/public/img/icons/categories_48.png b/adhocracy/public/img/icons/categories_48.png new file mode 100755 index 000000000..d3efdd2a5 Binary files /dev/null and b/adhocracy/public/img/icons/categories_48.png differ diff --git a/adhocracy/public/img/icons/categories_64.png b/adhocracy/public/img/icons/categories_64.png new file mode 100755 index 000000000..4f9ca7a29 Binary files /dev/null and b/adhocracy/public/img/icons/categories_64.png differ diff --git a/adhocracy/public/img/icons/category.png b/adhocracy/public/img/icons/category.png new file mode 100644 index 000000000..ad7889481 Binary files /dev/null and b/adhocracy/public/img/icons/category.png differ diff --git a/adhocracy/public/img/icons/category_create.png b/adhocracy/public/img/icons/category_create.png new file mode 100644 index 000000000..fee7c044a Binary files /dev/null and b/adhocracy/public/img/icons/category_create.png differ diff --git a/adhocracy/public/img/icons/condition_met.png b/adhocracy/public/img/icons/condition_met.png new file mode 100644 index 000000000..0ea0e9f39 Binary files /dev/null and b/adhocracy/public/img/icons/condition_met.png differ diff --git a/adhocracy/public/img/icons/condition_unmet.png b/adhocracy/public/img/icons/condition_unmet.png new file mode 100644 index 000000000..237702ace Binary files /dev/null and b/adhocracy/public/img/icons/condition_unmet.png differ diff --git a/adhocracy/public/img/icons/delegate.png b/adhocracy/public/img/icons/delegate.png new file mode 100644 index 000000000..41f4ed7ea Binary files /dev/null and b/adhocracy/public/img/icons/delegate.png differ diff --git a/adhocracy/public/img/icons/delegate2.png b/adhocracy/public/img/icons/delegate2.png new file mode 100644 index 000000000..48772c23d Binary files /dev/null and b/adhocracy/public/img/icons/delegate2.png differ diff --git a/adhocracy/public/img/icons/delegate3.png b/adhocracy/public/img/icons/delegate3.png new file mode 100644 index 000000000..b3b8d0f93 Binary files /dev/null and b/adhocracy/public/img/icons/delegate3.png differ diff --git a/adhocracy/public/img/icons/delegate_16.png b/adhocracy/public/img/icons/delegate_16.png new file mode 100755 index 000000000..667bc43ad Binary files /dev/null and b/adhocracy/public/img/icons/delegate_16.png differ diff --git a/adhocracy/public/img/icons/delegate_20.png b/adhocracy/public/img/icons/delegate_20.png new file mode 100755 index 000000000..a9d6b15b7 Binary files /dev/null and b/adhocracy/public/img/icons/delegate_20.png differ diff --git a/adhocracy/public/img/icons/delegate_24.png b/adhocracy/public/img/icons/delegate_24.png new file mode 100755 index 000000000..9bc0a773b Binary files /dev/null and b/adhocracy/public/img/icons/delegate_24.png differ diff --git a/adhocracy/public/img/icons/delegate_32.png b/adhocracy/public/img/icons/delegate_32.png new file mode 100755 index 000000000..0ba780d1f Binary files /dev/null and b/adhocracy/public/img/icons/delegate_32.png differ diff --git a/adhocracy/public/img/icons/delegate_48.png b/adhocracy/public/img/icons/delegate_48.png new file mode 100755 index 000000000..a967b2b3e Binary files /dev/null and b/adhocracy/public/img/icons/delegate_48.png differ diff --git a/adhocracy/public/img/icons/delegate_64.png b/adhocracy/public/img/icons/delegate_64.png new file mode 100755 index 000000000..21085f74c Binary files /dev/null and b/adhocracy/public/img/icons/delegate_64.png differ diff --git a/adhocracy/public/img/icons/delegate_to.png b/adhocracy/public/img/icons/delegate_to.png new file mode 100755 index 000000000..fcbd28873 Binary files /dev/null and b/adhocracy/public/img/icons/delegate_to.png differ diff --git a/adhocracy/public/img/icons/delegate_to_16.png b/adhocracy/public/img/icons/delegate_to_16.png new file mode 100755 index 000000000..8415ef022 Binary files /dev/null and b/adhocracy/public/img/icons/delegate_to_16.png differ diff --git a/adhocracy/public/img/icons/delegate_to_20.png b/adhocracy/public/img/icons/delegate_to_20.png new file mode 100755 index 000000000..ed4c029f0 Binary files /dev/null and b/adhocracy/public/img/icons/delegate_to_20.png differ diff --git a/adhocracy/public/img/icons/delegate_to_24.png b/adhocracy/public/img/icons/delegate_to_24.png new file mode 100755 index 000000000..1a25ad534 Binary files /dev/null and b/adhocracy/public/img/icons/delegate_to_24.png differ diff --git a/adhocracy/public/img/icons/delegate_to_32.png b/adhocracy/public/img/icons/delegate_to_32.png new file mode 100755 index 000000000..0e70b8d46 Binary files /dev/null and b/adhocracy/public/img/icons/delegate_to_32.png differ diff --git a/adhocracy/public/img/icons/delegate_to_48.png b/adhocracy/public/img/icons/delegate_to_48.png new file mode 100755 index 000000000..888f4d8dc Binary files /dev/null and b/adhocracy/public/img/icons/delegate_to_48.png differ diff --git a/adhocracy/public/img/icons/delegate_to_64.png b/adhocracy/public/img/icons/delegate_to_64.png new file mode 100755 index 000000000..e180d25e3 Binary files /dev/null and b/adhocracy/public/img/icons/delegate_to_64.png differ diff --git a/adhocracy/public/img/icons/delegated_to.png b/adhocracy/public/img/icons/delegated_to.png new file mode 100755 index 000000000..c9b62b3fd Binary files /dev/null and b/adhocracy/public/img/icons/delegated_to.png differ diff --git a/adhocracy/public/img/icons/delegated_to_16.png b/adhocracy/public/img/icons/delegated_to_16.png new file mode 100755 index 000000000..33c6ac8d2 Binary files /dev/null and b/adhocracy/public/img/icons/delegated_to_16.png differ diff --git a/adhocracy/public/img/icons/delegated_to_20.png b/adhocracy/public/img/icons/delegated_to_20.png new file mode 100755 index 000000000..1e759b639 Binary files /dev/null and b/adhocracy/public/img/icons/delegated_to_20.png differ diff --git a/adhocracy/public/img/icons/delegated_to_24.png b/adhocracy/public/img/icons/delegated_to_24.png new file mode 100755 index 000000000..0f1394050 Binary files /dev/null and b/adhocracy/public/img/icons/delegated_to_24.png differ diff --git a/adhocracy/public/img/icons/delegated_to_32.png b/adhocracy/public/img/icons/delegated_to_32.png new file mode 100755 index 000000000..c8a1fdaa3 Binary files /dev/null and b/adhocracy/public/img/icons/delegated_to_32.png differ diff --git a/adhocracy/public/img/icons/delegated_to_48.png b/adhocracy/public/img/icons/delegated_to_48.png new file mode 100755 index 000000000..24af3c07d Binary files /dev/null and b/adhocracy/public/img/icons/delegated_to_48.png differ diff --git a/adhocracy/public/img/icons/delegated_to_64.png b/adhocracy/public/img/icons/delegated_to_64.png new file mode 100755 index 000000000..2bff0c425 Binary files /dev/null and b/adhocracy/public/img/icons/delegated_to_64.png differ diff --git a/adhocracy/public/img/icons/delete.png b/adhocracy/public/img/icons/delete.png new file mode 100644 index 000000000..184f76285 Binary files /dev/null and b/adhocracy/public/img/icons/delete.png differ diff --git a/adhocracy/public/img/icons/diff.png b/adhocracy/public/img/icons/diff.png new file mode 100644 index 000000000..47854fc67 Binary files /dev/null and b/adhocracy/public/img/icons/diff.png differ diff --git a/adhocracy/public/img/icons/discuss_16.png b/adhocracy/public/img/icons/discuss_16.png new file mode 100755 index 000000000..89cf41e6b Binary files /dev/null and b/adhocracy/public/img/icons/discuss_16.png differ diff --git a/adhocracy/public/img/icons/discuss_20.png b/adhocracy/public/img/icons/discuss_20.png new file mode 100755 index 000000000..f77c64694 Binary files /dev/null and b/adhocracy/public/img/icons/discuss_20.png differ diff --git a/adhocracy/public/img/icons/discuss_24.png b/adhocracy/public/img/icons/discuss_24.png new file mode 100755 index 000000000..84def8fde Binary files /dev/null and b/adhocracy/public/img/icons/discuss_24.png differ diff --git a/adhocracy/public/img/icons/discuss_32.png b/adhocracy/public/img/icons/discuss_32.png new file mode 100755 index 000000000..5b14aba00 Binary files /dev/null and b/adhocracy/public/img/icons/discuss_32.png differ diff --git a/adhocracy/public/img/icons/discuss_48.png b/adhocracy/public/img/icons/discuss_48.png new file mode 100755 index 000000000..f5ae19ec4 Binary files /dev/null and b/adhocracy/public/img/icons/discuss_48.png differ diff --git a/adhocracy/public/img/icons/discuss_64.png b/adhocracy/public/img/icons/discuss_64.png new file mode 100755 index 000000000..c1c994105 Binary files /dev/null and b/adhocracy/public/img/icons/discuss_64.png differ diff --git a/adhocracy/public/img/icons/edit.png b/adhocracy/public/img/icons/edit.png new file mode 100644 index 000000000..f1646e751 Binary files /dev/null and b/adhocracy/public/img/icons/edit.png differ diff --git a/adhocracy/public/img/icons/history.png b/adhocracy/public/img/icons/history.png new file mode 100644 index 000000000..c3fc1269b Binary files /dev/null and b/adhocracy/public/img/icons/history.png differ diff --git a/adhocracy/public/img/icons/info_16.png b/adhocracy/public/img/icons/info_16.png new file mode 100644 index 000000000..2555904ee Binary files /dev/null and b/adhocracy/public/img/icons/info_16.png differ diff --git a/adhocracy/public/img/icons/issue_16.png b/adhocracy/public/img/icons/issue_16.png new file mode 100755 index 000000000..474e42138 Binary files /dev/null and b/adhocracy/public/img/icons/issue_16.png differ diff --git a/adhocracy/public/img/icons/issue_20.png b/adhocracy/public/img/icons/issue_20.png new file mode 100755 index 000000000..6b9142959 Binary files /dev/null and b/adhocracy/public/img/icons/issue_20.png differ diff --git a/adhocracy/public/img/icons/issue_24.png b/adhocracy/public/img/icons/issue_24.png new file mode 100644 index 000000000..644d5a969 Binary files /dev/null and b/adhocracy/public/img/icons/issue_24.png differ diff --git a/adhocracy/public/img/icons/issue_32.png b/adhocracy/public/img/icons/issue_32.png new file mode 100644 index 000000000..d6eba42bd Binary files /dev/null and b/adhocracy/public/img/icons/issue_32.png differ diff --git a/adhocracy/public/img/icons/karma.png b/adhocracy/public/img/icons/karma.png new file mode 100644 index 000000000..68b8bf4c1 Binary files /dev/null and b/adhocracy/public/img/icons/karma.png differ diff --git a/adhocracy/public/img/icons/manage.png b/adhocracy/public/img/icons/manage.png new file mode 100644 index 000000000..51583807f Binary files /dev/null and b/adhocracy/public/img/icons/manage.png differ diff --git a/adhocracy/public/img/icons/motion.png b/adhocracy/public/img/icons/motion.png new file mode 100644 index 000000000..656275c82 Binary files /dev/null and b/adhocracy/public/img/icons/motion.png differ diff --git a/adhocracy/public/img/icons/motion_16.png b/adhocracy/public/img/icons/motion_16.png new file mode 100755 index 000000000..69d9d8175 Binary files /dev/null and b/adhocracy/public/img/icons/motion_16.png differ diff --git a/adhocracy/public/img/icons/motion_20.png b/adhocracy/public/img/icons/motion_20.png new file mode 100755 index 000000000..e70465b15 Binary files /dev/null and b/adhocracy/public/img/icons/motion_20.png differ diff --git a/adhocracy/public/img/icons/motion_24.png b/adhocracy/public/img/icons/motion_24.png new file mode 100755 index 000000000..81f60a071 Binary files /dev/null and b/adhocracy/public/img/icons/motion_24.png differ diff --git a/adhocracy/public/img/icons/motion_32.png b/adhocracy/public/img/icons/motion_32.png new file mode 100755 index 000000000..2c8e82be1 Binary files /dev/null and b/adhocracy/public/img/icons/motion_32.png differ diff --git a/adhocracy/public/img/icons/motion_back.png b/adhocracy/public/img/icons/motion_back.png new file mode 100644 index 000000000..982553ed5 Binary files /dev/null and b/adhocracy/public/img/icons/motion_back.png differ diff --git a/adhocracy/public/img/icons/motion_create.png b/adhocracy/public/img/icons/motion_create.png new file mode 100644 index 000000000..5e92fa570 Binary files /dev/null and b/adhocracy/public/img/icons/motion_create.png differ diff --git a/adhocracy/public/img/icons/motions.png b/adhocracy/public/img/icons/motions.png new file mode 100644 index 000000000..585579aef Binary files /dev/null and b/adhocracy/public/img/icons/motions.png differ diff --git a/adhocracy/public/img/icons/nay.png b/adhocracy/public/img/icons/nay.png new file mode 100644 index 000000000..6b8361e31 Binary files /dev/null and b/adhocracy/public/img/icons/nay.png differ diff --git a/adhocracy/public/img/icons/save.png b/adhocracy/public/img/icons/save.png new file mode 100644 index 000000000..7b72455a5 Binary files /dev/null and b/adhocracy/public/img/icons/save.png differ diff --git a/adhocracy/public/img/icons/search.png b/adhocracy/public/img/icons/search.png new file mode 100644 index 000000000..e96f05e59 Binary files /dev/null and b/adhocracy/public/img/icons/search.png differ diff --git a/adhocracy/public/img/icons/settings.png b/adhocracy/public/img/icons/settings.png new file mode 100644 index 000000000..2eda10da3 Binary files /dev/null and b/adhocracy/public/img/icons/settings.png differ diff --git a/adhocracy/public/img/icons/stack.png b/adhocracy/public/img/icons/stack.png new file mode 100755 index 000000000..e21bf6a26 Binary files /dev/null and b/adhocracy/public/img/icons/stack.png differ diff --git a/adhocracy/public/img/icons/stack_16.png b/adhocracy/public/img/icons/stack_16.png new file mode 100644 index 000000000..1ea7109d4 Binary files /dev/null and b/adhocracy/public/img/icons/stack_16.png differ diff --git a/adhocracy/public/img/icons/stack_20.png b/adhocracy/public/img/icons/stack_20.png new file mode 100755 index 000000000..28265f324 Binary files /dev/null and b/adhocracy/public/img/icons/stack_20.png differ diff --git a/adhocracy/public/img/icons/stack_24.png b/adhocracy/public/img/icons/stack_24.png new file mode 100755 index 000000000..5bd340fbe Binary files /dev/null and b/adhocracy/public/img/icons/stack_24.png differ diff --git a/adhocracy/public/img/icons/stack_32.png b/adhocracy/public/img/icons/stack_32.png new file mode 100755 index 000000000..0665c5b0f Binary files /dev/null and b/adhocracy/public/img/icons/stack_32.png differ diff --git a/adhocracy/public/img/icons/stack_48.png b/adhocracy/public/img/icons/stack_48.png new file mode 100755 index 000000000..b48e2f0df Binary files /dev/null and b/adhocracy/public/img/icons/stack_48.png differ diff --git a/adhocracy/public/img/icons/stack_64.png b/adhocracy/public/img/icons/stack_64.png new file mode 100755 index 000000000..f2f2b6edf Binary files /dev/null and b/adhocracy/public/img/icons/stack_64.png differ diff --git a/adhocracy/public/img/icons/user.png b/adhocracy/public/img/icons/user.png new file mode 100755 index 000000000..ab232630c Binary files /dev/null and b/adhocracy/public/img/icons/user.png differ diff --git a/adhocracy/public/img/icons/user_16.png b/adhocracy/public/img/icons/user_16.png new file mode 100755 index 000000000..afe433433 Binary files /dev/null and b/adhocracy/public/img/icons/user_16.png differ diff --git a/adhocracy/public/img/icons/user_20.png b/adhocracy/public/img/icons/user_20.png new file mode 100755 index 000000000..8b0a3443f Binary files /dev/null and b/adhocracy/public/img/icons/user_20.png differ diff --git a/adhocracy/public/img/icons/user_24.png b/adhocracy/public/img/icons/user_24.png new file mode 100755 index 000000000..81d84cc2c Binary files /dev/null and b/adhocracy/public/img/icons/user_24.png differ diff --git a/adhocracy/public/img/icons/user_32.png b/adhocracy/public/img/icons/user_32.png new file mode 100755 index 000000000..f0b9156d4 Binary files /dev/null and b/adhocracy/public/img/icons/user_32.png differ diff --git a/adhocracy/public/img/icons/user_48.png b/adhocracy/public/img/icons/user_48.png new file mode 100755 index 000000000..323fce7e8 Binary files /dev/null and b/adhocracy/public/img/icons/user_48.png differ diff --git a/adhocracy/public/img/icons/vote.png b/adhocracy/public/img/icons/vote.png new file mode 100644 index 000000000..685c022b4 Binary files /dev/null and b/adhocracy/public/img/icons/vote.png differ diff --git a/adhocracy/public/img/icons/vote.png_1 b/adhocracy/public/img/icons/vote.png_1 new file mode 100644 index 000000000..3f8cdadad Binary files /dev/null and b/adhocracy/public/img/icons/vote.png_1 differ diff --git a/adhocracy/public/img/icons/vote_16.png b/adhocracy/public/img/icons/vote_16.png new file mode 100755 index 000000000..8e0d65313 Binary files /dev/null and b/adhocracy/public/img/icons/vote_16.png differ diff --git a/adhocracy/public/img/icons/vote_20.png b/adhocracy/public/img/icons/vote_20.png new file mode 100755 index 000000000..1a3173d97 Binary files /dev/null and b/adhocracy/public/img/icons/vote_20.png differ diff --git a/adhocracy/public/img/icons/vote_24.png b/adhocracy/public/img/icons/vote_24.png new file mode 100755 index 000000000..a3a69a8bc Binary files /dev/null and b/adhocracy/public/img/icons/vote_24.png differ diff --git a/adhocracy/public/img/icons/vote_32.png b/adhocracy/public/img/icons/vote_32.png new file mode 100755 index 000000000..7276d245d Binary files /dev/null and b/adhocracy/public/img/icons/vote_32.png differ diff --git a/adhocracy/public/img/icons/vote_48.png b/adhocracy/public/img/icons/vote_48.png new file mode 100755 index 000000000..c7d749473 Binary files /dev/null and b/adhocracy/public/img/icons/vote_48.png differ diff --git a/adhocracy/public/img/icons/vote_64.png b/adhocracy/public/img/icons/vote_64.png new file mode 100755 index 000000000..ae0fd840e Binary files /dev/null and b/adhocracy/public/img/icons/vote_64.png differ diff --git a/adhocracy/public/img/icons/vote_abstain.png b/adhocracy/public/img/icons/vote_abstain.png new file mode 100644 index 000000000..e1fb7d813 Binary files /dev/null and b/adhocracy/public/img/icons/vote_abstain.png differ diff --git a/adhocracy/public/img/icons/vote_against.png b/adhocracy/public/img/icons/vote_against.png new file mode 100644 index 000000000..caebcec3e Binary files /dev/null and b/adhocracy/public/img/icons/vote_against.png differ diff --git a/adhocracy/public/img/icons/vote_for.png b/adhocracy/public/img/icons/vote_for.png new file mode 100644 index 000000000..df251896e Binary files /dev/null and b/adhocracy/public/img/icons/vote_for.png differ diff --git a/adhocracy/public/img/icons/vote_icons.png b/adhocracy/public/img/icons/vote_icons.png new file mode 100644 index 000000000..144dc7fdd Binary files /dev/null and b/adhocracy/public/img/icons/vote_icons.png differ diff --git a/adhocracy/public/img/icons/votes.png b/adhocracy/public/img/icons/votes.png new file mode 100644 index 000000000..fe9567cbb Binary files /dev/null and b/adhocracy/public/img/icons/votes.png differ diff --git a/adhocracy/public/img/logo.png b/adhocracy/public/img/logo.png new file mode 100644 index 000000000..a173ddd40 Binary files /dev/null and b/adhocracy/public/img/logo.png differ diff --git a/adhocracy/public/img/page_bg.png b/adhocracy/public/img/page_bg.png new file mode 100644 index 000000000..f0f02eabb Binary files /dev/null and b/adhocracy/public/img/page_bg.png differ diff --git a/adhocracy/public/img/rss.png b/adhocracy/public/img/rss.png new file mode 100644 index 000000000..655542a80 Binary files /dev/null and b/adhocracy/public/img/rss.png differ diff --git a/adhocracy/public/img/welcome_logo.png b/adhocracy/public/img/welcome_logo.png new file mode 100644 index 000000000..9edc27810 Binary files /dev/null and b/adhocracy/public/img/welcome_logo.png differ diff --git a/adhocracy/public/js/adhocracy.js b/adhocracy/public/js/adhocracy.js new file mode 100644 index 000000000..a20cb5998 --- /dev/null +++ b/adhocracy/public/js/adhocracy.js @@ -0,0 +1,191 @@ +$(document).ready(function() { + + adhocracyDomain = function() { + return document.domain.substring(document.domain.split('.')[0].length); + } + + _get = function(e) { + if (e.type=="textarea") { + return $(e).text(); + } else { + return $(e).val(); + } + } + + _set = function(e, s) { + if (e.type=="textarea") { + $(e).text(s); + } else { + $(e).val(s); + } + } + + _unset = function(e) { + _set(e, ""); + } + + arm = function(e, hint) { + + $(e).focus(function(){ + if ($(e).hasClass("armed")) { + _unset(e); + $(e).removeClass("armed"); + } + }); + + on_blur = function() { + if ($(e).hasClass("armed")) { + return; + } + + if (jQuery.trim($(e).val()).length == 0) { + _set(e, hint); + $(e).addClass("armed"); + } + } + + $(e).blur(on_blur); + on_blur(); + } + + $(".armlabel").each(function(e) { + hint = $("[for=" + $(this).attr("name") + "]").text(); + arm(this, hint); + }); + + $("form").submit(function() { + $("[name=" + this.name + "] .armed").each(function(i) { + _unset(this); + }); + }); + + /* Auto-appends */ + appendRelation = function() { + newElem = $(".relation.prototype").clone(); + newElem.removeClass('prototype'); + newElem.slideDown('fast'); + newElem = newElem.insertAfter($(".relation:last")); + return false; + } + + appendCanonical = function() { + newElem = $(".canonical.prototype").clone(); + newElem = newElem.insertAfter($(".canonical:last")); + newElem.slideDown('fast'); + newElem.removeClass('prototype'); + return false; + } + + + + + $(".userCompleted").autocomplete('/user/complete', { + autoFill: false, + formatItem: function(d, i, n, q) { + return eval('(' + d + ')').s + }, + formatResult: function(d, i, n, q) { + return eval('(' + d + ')').k + }, + }); + + comment_reply = function(id) { + $("#tile_c" + id + " .reply_form").slideToggle('fast'); + return false; + } + + comment_link_discussion = function(id) { + $("#c" + id + " .link_discussion").slideToggle('fast'); + return false; + } + + comment_edit = function(id) { + $("#tile_c" + id + " .hide_edit").slideToggle('fast'); + $("#tile_c" + id + " .edit_form").slideToggle('fast'); + return false; + } + + comment_hide = function(id) { + $("#tile_c" + id + " .hide").hide(); + $("#tile_c" + id + " .meta").hide(); + $("#tile_c" + id + " .text").hide(); + $("#tile_c" + id + " .edit_form").hide(); + $("#tile_c" + id + " .reply_form").hide(); + $("#c" + id + " .sub").hide(); + $("#tile_c" + id + " .show").show(); + return false; + } + + comment_show = function(id) { + $("#tile_c" + id + " .show").hide(); + $("#tile_c" + id + " .meta").show(); + $("#tile_c" + id + " .text").show(); + $("#c" + id + " .sub").show(); + $("#tile_c" + id + " .hide").show(); + $("#c" + id + " .edit_form").hide(); + $("#c" + id + " .reply_form").hide(); + return false; + } + + comment_karma = function(id, value) { + $.getJSON('/karma/give.json', {comment: id, value: value}, + function(data) { + if (value == -1) { + $("#tile_c" + id + ".upvoted").removeClass("upvoted"); + $("#tile_c" + id + "").addClass("downvoted"); + } else { + $("#tile_c" + id + ".downvoted").removeClass("downvoted"); + $("#tile_c" + id + "").addClass("upvoted"); + } + $("#tile_c" + id + " .score").text(data.score); + }); + return false; + } + + add_canonical = function() { + $(".add_canonical").slideToggle('normal'); + return false; + } + + $(".comment .hide").show(); + + anchor = document.location.hash; + if (anchor.length > 1) { + $("#tile_" + anchor.substring(1)).addClass("anchor"); + setTimeout(function() { + $("#tile_" + anchor.substring(1)).removeClass("anchor"); + }, 3500); + } + + hide_tutu = function() { + if (!$.cookie('hide_tutu')) { + $(".tutu").slideUp('slow'); + } else { + $(".tutu").hide(); + } + $.cookie('hide_tutu', 'yes', {expire: 1000, domain: adhocracyDomain()}) + } + + check_tutu = function() { + if($.cookie('hide_tutu')) { + hide_tutu(); + } else { + $('.tutu').slideDown('fast'); + } + } + + check_tutu(); + + /* Hovering title warnings */ + current_htWarn_title = ""; + $(".htwarn").hover( + function() { + current_htWarn_title = $(this).attr('title'); + $(this).append($("<div class='htwarnbox'>" + current_htWarn_title + "</div>")); + $(this).attr('title', ''); + }, + function() { + $(this).find(".htwarnbox").remove(); + $(this).attr('title', current_htWarn_title); + }); +}); \ No newline at end of file diff --git a/adhocracy/public/js/jquery.autocomplete.min.js b/adhocracy/public/js/jquery.autocomplete.min.js new file mode 100644 index 000000000..c9ddfb220 --- /dev/null +++ b/adhocracy/public/js/jquery.autocomplete.min.js @@ -0,0 +1,15 @@ +/* + * Autocomplete - jQuery plugin 1.0.2 + * + * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $ + * + */;(function($){$.fn.extend({autocomplete:function(urlOrData,options){var isUrl=typeof urlOrData=="string";options=$.extend({},$.Autocompleter.defaults,{url:isUrl?urlOrData:null,data:isUrl?null:urlOrData,delay:isUrl?$.Autocompleter.defaults.delay:10,max:options&&!options.scroll?10:150},options);options.highlight=options.highlight||function(value){return value;};options.formatMatch=options.formatMatch||options.formatItem;return this.each(function(){new $.Autocompleter(this,options);});},result:function(handler){return this.bind("result",handler);},search:function(handler){return this.trigger("search",[handler]);},flushCache:function(){return this.trigger("flushCache");},setOptions:function(options){return this.trigger("setOptions",[options]);},unautocomplete:function(){return this.trigger("unautocomplete");}});$.Autocompleter=function(input,options){var KEY={UP:38,DOWN:40,DEL:46,TAB:9,RETURN:13,ESC:27,COMMA:188,PAGEUP:33,PAGEDOWN:34,BACKSPACE:8};var $input=$(input).attr("autocomplete","off").addClass(options.inputClass);var timeout;var previousValue="";var cache=$.Autocompleter.Cache(options);var hasFocus=0;var lastKeyPressCode;var config={mouseDownOnSelect:false};var select=$.Autocompleter.Select(options,input,selectCurrent,config);var blockSubmit;$.browser.opera&&$(input.form).bind("submit.autocomplete",function(){if(blockSubmit){blockSubmit=false;return false;}});$input.bind(($.browser.opera?"keypress":"keydown")+".autocomplete",function(event){lastKeyPressCode=event.keyCode;switch(event.keyCode){case KEY.UP:event.preventDefault();if(select.visible()){select.prev();}else{onChange(0,true);}break;case KEY.DOWN:event.preventDefault();if(select.visible()){select.next();}else{onChange(0,true);}break;case KEY.PAGEUP:event.preventDefault();if(select.visible()){select.pageUp();}else{onChange(0,true);}break;case KEY.PAGEDOWN:event.preventDefault();if(select.visible()){select.pageDown();}else{onChange(0,true);}break;case options.multiple&&$.trim(options.multipleSeparator)==","&&KEY.COMMA:case KEY.TAB:case KEY.RETURN:if(selectCurrent()){event.preventDefault();blockSubmit=true;return false;}break;case KEY.ESC:select.hide();break;default:clearTimeout(timeout);timeout=setTimeout(onChange,options.delay);break;}}).focus(function(){hasFocus++;}).blur(function(){hasFocus=0;if(!config.mouseDownOnSelect){hideResults();}}).click(function(){if(hasFocus++>1&&!select.visible()){onChange(0,true);}}).bind("search",function(){var fn=(arguments.length>1)?arguments[1]:null;function findValueCallback(q,data){var result;if(data&&data.length){for(var i=0;i<data.length;i++){if(data[i].result.toLowerCase()==q.toLowerCase()){result=data[i];break;}}}if(typeof fn=="function")fn(result);else $input.trigger("result",result&&[result.data,result.value]);}$.each(trimWords($input.val()),function(i,value){request(value,findValueCallback,findValueCallback);});}).bind("flushCache",function(){cache.flush();}).bind("setOptions",function(){$.extend(options,arguments[1]);if("data"in arguments[1])cache.populate();}).bind("unautocomplete",function(){select.unbind();$input.unbind();$(input.form).unbind(".autocomplete");});function selectCurrent(){var selected=select.selected();if(!selected)return false;var v=selected.result;previousValue=v;if(options.multiple){var words=trimWords($input.val());if(words.length>1){v=words.slice(0,words.length-1).join(options.multipleSeparator)+options.multipleSeparator+v;}v+=options.multipleSeparator;}$input.val(v);hideResultsNow();$input.trigger("result",[selected.data,selected.value]);return true;}function onChange(crap,skipPrevCheck){if(lastKeyPressCode==KEY.DEL){select.hide();return;}var currentValue=$input.val();if(!skipPrevCheck&¤tValue==previousValue)return;previousValue=currentValue;currentValue=lastWord(currentValue);if(currentValue.length>=options.minChars){$input.addClass(options.loadingClass);if(!options.matchCase)currentValue=currentValue.toLowerCase();request(currentValue,receiveData,hideResultsNow);}else{stopLoading();select.hide();}};function trimWords(value){if(!value){return[""];}var words=value.split(options.multipleSeparator);var result=[];$.each(words,function(i,value){if($.trim(value))result[i]=$.trim(value);});return result;}function lastWord(value){if(!options.multiple)return value;var words=trimWords(value);return words[words.length-1];}function autoFill(q,sValue){if(options.autoFill&&(lastWord($input.val()).toLowerCase()==q.toLowerCase())&&lastKeyPressCode!=KEY.BACKSPACE){$input.val($input.val()+sValue.substring(lastWord(previousValue).length));$.Autocompleter.Selection(input,previousValue.length,previousValue.length+sValue.length);}};function hideResults(){clearTimeout(timeout);timeout=setTimeout(hideResultsNow,200);};function hideResultsNow(){var wasVisible=select.visible();select.hide();clearTimeout(timeout);stopLoading();if(options.mustMatch){$input.search(function(result){if(!result){if(options.multiple){var words=trimWords($input.val()).slice(0,-1);$input.val(words.join(options.multipleSeparator)+(words.length?options.multipleSeparator:""));}else +$input.val("");}});}if(wasVisible)$.Autocompleter.Selection(input,input.value.length,input.value.length);};function receiveData(q,data){if(data&&data.length&&hasFocus){stopLoading();select.display(data,q);autoFill(q,data[0].value);select.show();}else{hideResultsNow();}};function request(term,success,failure){if(!options.matchCase)term=term.toLowerCase();var data=cache.load(term);if(data&&data.length){success(term,data);}else if((typeof options.url=="string")&&(options.url.length>0)){var extraParams={timestamp:+new Date()};$.each(options.extraParams,function(key,param){extraParams[key]=typeof param=="function"?param():param;});$.ajax({mode:"abort",port:"autocomplete"+input.name,dataType:options.dataType,url:options.url,data:$.extend({q:lastWord(term),limit:options.max},extraParams),success:function(data){var parsed=options.parse&&options.parse(data)||parse(data);cache.add(term,parsed);success(term,parsed);}});}else{select.emptyList();failure(term);}};function parse(data){var parsed=[];var rows=data.split("\n");for(var i=0;i<rows.length;i++){var row=$.trim(rows[i]);if(row){row=row.split("|");parsed[parsed.length]={data:row,value:row[0],result:options.formatResult&&options.formatResult(row,row[0])||row[0]};}}return parsed;};function stopLoading(){$input.removeClass(options.loadingClass);};};$.Autocompleter.defaults={inputClass:"ac_input",resultsClass:"ac_results",loadingClass:"ac_loading",minChars:1,delay:400,matchCase:false,matchSubset:true,matchContains:false,cacheLength:10,max:100,mustMatch:false,extraParams:{},selectFirst:true,formatItem:function(row){return row[0];},formatMatch:null,autoFill:false,width:0,multiple:false,multipleSeparator:", ",highlight:function(value,term){return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)("+term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi,"\\$1")+")(?![^<>]*>)(?![^&;]+;)","gi"),"<strong>$1</strong>");},scroll:true,scrollHeight:180};$.Autocompleter.Cache=function(options){var data={};var length=0;function matchSubset(s,sub){if(!options.matchCase)s=s.toLowerCase();var i=s.indexOf(sub);if(i==-1)return false;return i==0||options.matchContains;};function add(q,value){if(length>options.cacheLength){flush();}if(!data[q]){length++;}data[q]=value;}function populate(){if(!options.data)return false;var stMatchSets={},nullData=0;if(!options.url)options.cacheLength=1;stMatchSets[""]=[];for(var i=0,ol=options.data.length;i<ol;i++){var rawValue=options.data[i];rawValue=(typeof rawValue=="string")?[rawValue]:rawValue;var value=options.formatMatch(rawValue,i+1,options.data.length);if(value===false)continue;var firstChar=value.charAt(0).toLowerCase();if(!stMatchSets[firstChar])stMatchSets[firstChar]=[];var row={value:value,data:rawValue,result:options.formatResult&&options.formatResult(rawValue)||value};stMatchSets[firstChar].push(row);if(nullData++<options.max){stMatchSets[""].push(row);}};$.each(stMatchSets,function(i,value){options.cacheLength++;add(i,value);});}setTimeout(populate,25);function flush(){data={};length=0;}return{flush:flush,add:add,populate:populate,load:function(q){if(!options.cacheLength||!length)return null;if(!options.url&&options.matchContains){var csub=[];for(var k in data){if(k.length>0){var c=data[k];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub.push(x);}});}}return csub;}else +if(data[q]){return data[q];}else +if(options.matchSubset){for(var i=q.length-1;i>=options.minChars;i--){var c=data[q.substr(0,i)];if(c){var csub=[];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub[csub.length]=x;}});return csub;}}}return null;}};};$.Autocompleter.Select=function(options,input,select,config){var CLASSES={ACTIVE:"ac_over"};var listItems,active=-1,data,term="",needsInit=true,element,list;function init(){if(!needsInit)return;element=$("<div/>").hide().addClass(options.resultsClass).css("position","absolute").appendTo(document.body);list=$("<ul/>").appendTo(element).mouseover(function(event){if(target(event).nodeName&&target(event).nodeName.toUpperCase()=='LI'){active=$("li",list).removeClass(CLASSES.ACTIVE).index(target(event));$(target(event)).addClass(CLASSES.ACTIVE);}}).click(function(event){$(target(event)).addClass(CLASSES.ACTIVE);select();input.focus();return false;}).mousedown(function(){config.mouseDownOnSelect=true;}).mouseup(function(){config.mouseDownOnSelect=false;});if(options.width>0)element.css("width",options.width);needsInit=false;}function target(event){var element=event.target;while(element&&element.tagName!="LI")element=element.parentNode;if(!element)return[];return element;}function moveSelect(step){listItems.slice(active,active+1).removeClass(CLASSES.ACTIVE);movePosition(step);var activeItem=listItems.slice(active,active+1).addClass(CLASSES.ACTIVE);if(options.scroll){var offset=0;listItems.slice(0,active).each(function(){offset+=this.offsetHeight;});if((offset+activeItem[0].offsetHeight-list.scrollTop())>list[0].clientHeight){list.scrollTop(offset+activeItem[0].offsetHeight-list.innerHeight());}else if(offset<list.scrollTop()){list.scrollTop(offset);}}};function movePosition(step){active+=step;if(active<0){active=listItems.size()-1;}else if(active>=listItems.size()){active=0;}}function limitNumberOfItems(available){return options.max&&options.max<available?options.max:available;}function fillList(){list.empty();var max=limitNumberOfItems(data.length);for(var i=0;i<max;i++){if(!data[i])continue;var formatted=options.formatItem(data[i].data,i+1,max,data[i].value,term);if(formatted===false)continue;var li=$("<li/>").html(options.highlight(formatted,term)).addClass(i%2==0?"ac_even":"ac_odd").appendTo(list)[0];$.data(li,"ac_data",data[i]);}listItems=list.find("li");if(options.selectFirst){listItems.slice(0,1).addClass(CLASSES.ACTIVE);active=0;}if($.fn.bgiframe)list.bgiframe();}return{display:function(d,q){init();data=d;term=q;fillList();},next:function(){moveSelect(1);},prev:function(){moveSelect(-1);},pageUp:function(){if(active!=0&&active-8<0){moveSelect(-active);}else{moveSelect(-8);}},pageDown:function(){if(active!=listItems.size()-1&&active+8>listItems.size()){moveSelect(listItems.size()-1-active);}else{moveSelect(8);}},hide:function(){element&&element.hide();listItems&&listItems.removeClass(CLASSES.ACTIVE);active=-1;},visible:function(){return element&&element.is(":visible");},current:function(){return this.visible()&&(listItems.filter("."+CLASSES.ACTIVE)[0]||options.selectFirst&&listItems[0]);},show:function(){var offset=$(input).offset();element.css({width:typeof options.width=="string"||options.width>0?options.width:$(input).width(),top:offset.top+input.offsetHeight,left:offset.left}).show();if(options.scroll){list.scrollTop(0);list.css({maxHeight:options.scrollHeight,overflow:'auto'});if($.browser.msie&&typeof document.body.style.maxHeight==="undefined"){var listHeight=0;listItems.each(function(){listHeight+=this.offsetHeight;});var scrollbarsVisible=listHeight>options.scrollHeight;list.css('height',scrollbarsVisible?options.scrollHeight:listHeight);if(!scrollbarsVisible){listItems.width(list.width()-parseInt(listItems.css("padding-left"))-parseInt(listItems.css("padding-right")));}}}},selected:function(){var selected=listItems&&listItems.filter("."+CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);return selected&&selected.length&&$.data(selected[0],"ac_data");},emptyList:function(){list&&list.empty();},unbind:function(){element&&element.remove();}};};$.Autocompleter.Selection=function(field,start,end){if(field.createTextRange){var selRange=field.createTextRange();selRange.collapse(true);selRange.moveStart("character",start);selRange.moveEnd("character",end);selRange.select();}else if(field.setSelectionRange){field.setSelectionRange(start,end);}else{if(field.selectionStart){field.selectionStart=start;field.selectionEnd=end;}}field.focus();};})(jQuery); \ No newline at end of file diff --git a/adhocracy/public/js/jquery.cookie.js b/adhocracy/public/js/jquery.cookie.js new file mode 100644 index 000000000..6df1faca2 --- /dev/null +++ b/adhocracy/public/js/jquery.cookie.js @@ -0,0 +1,96 @@ +/** + * Cookie plugin + * + * Copyright (c) 2006 Klaus Hartl (stilbuero.de) + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ + +/** + * Create a cookie with the given name and value and other optional parameters. + * + * @example $.cookie('the_cookie', 'the_value'); + * @desc Set the value of a cookie. + * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true }); + * @desc Create a cookie with all available options. + * @example $.cookie('the_cookie', 'the_value'); + * @desc Create a session cookie. + * @example $.cookie('the_cookie', null); + * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain + * used when the cookie was set. + * + * @param String name The name of the cookie. + * @param String value The value of the cookie. + * @param Object options An object literal containing key/value pairs to provide optional cookie attributes. + * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object. + * If a negative value is specified (e.g. a date in the past), the cookie will be deleted. + * If set to null or omitted, the cookie will be a session cookie and will not be retained + * when the the browser exits. + * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie). + * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie). + * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will + * require a secure protocol (like HTTPS). + * @type undefined + * + * @name $.cookie + * @cat Plugins/Cookie + * @author Klaus Hartl/klaus.hartl@stilbuero.de + */ + +/** + * Get the value of a cookie with the given name. + * + * @example $.cookie('the_cookie'); + * @desc Get the value of a cookie. + * + * @param String name The name of the cookie. + * @return The value of the cookie. + * @type String + * + * @name $.cookie + * @cat Plugins/Cookie + * @author Klaus Hartl/klaus.hartl@stilbuero.de + */ +jQuery.cookie = function(name, value, options) { + if (typeof value != 'undefined') { // name and value given, set cookie + options = options || {}; + if (value === null) { + value = ''; + options.expires = -1; + } + var expires = ''; + if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { + var date; + if (typeof options.expires == 'number') { + date = new Date(); + date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); + } else { + date = options.expires; + } + expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE + } + // CAUTION: Needed to parenthesize options.path and options.domain + // in the following expressions, otherwise they evaluate to undefined + // in the packed version for some reason... + var path = options.path ? '; path=' + (options.path) : ''; + var domain = options.domain ? '; domain=' + (options.domain) : ''; + var secure = options.secure ? '; secure' : ''; + document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); + } else { // only name given, get cookie + var cookieValue = null; + if (document.cookie && document.cookie != '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } +}; \ No newline at end of file diff --git a/adhocracy/public/robots.txt b/adhocracy/public/robots.txt new file mode 100644 index 000000000..98ae312f9 --- /dev/null +++ b/adhocracy/public/robots.txt @@ -0,0 +1 @@ +Sitemap: /sitemap.xml \ No newline at end of file diff --git a/adhocracy/public/style/base.css b/adhocracy/public/style/base.css new file mode 100644 index 000000000..ea5a23a1a --- /dev/null +++ b/adhocracy/public/style/base.css @@ -0,0 +1,1276 @@ +/* @group HTML Element Defs */ + +body, td, input, button, textarea, select { + font-family: "lucida grande", "Verdana", arial, sans-serif; + font-size: 10pt; +} + +div, body, ul, li, button { + padding: 0px; + margin: 0px; + border: 0px; +} + +body { + +} + +:focus { outline: 0; } +body { line-height: 1; color: black; background: white; } +ul { list-style: none; } + +a { + color: #336699; + text-decoration: none; +} + +a img { border: 0px; } + +a:hover { + text-decoration: underline; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: bold; +} + +h1 { + font-size: 16pt; +} + +h2 { + font-size: 14pt; +} + +h3 { + font-size: 12pt; +} + +h4, h5, h6 { + font-size: 10pt; +} + +/* @end */ + +/* @group Form Elements */ + +button { + color: #336699; + text-align: left; + cursor: pointer; + background-color: inherit; +} + +button img { + margin-top: 5px; +} + +textarea { + width: 98%; + min-height: 10em; +} + +label { + display: block; + padding-top: 7px; + font-weight: bold; +} + +label.armhint { + display: none; +} + +input, +textarea { + border: 1px solid #666; + padding: 3px; + margin-top: 3px; + margin-bottom: 3px; + font-size: 9pt; +} + +input[type=submit] { + display: block; + margin-top: 20px; + padding: 7px; + background-color: #1553a4; + color: #fff; + font-weight: bold; + border: 1px solid #fff; + min-width: 6em; +} + +input:focus, +input:hover, +#page_title input:focus, +#page_title input:hover, +textarea:focus, +textarea:hover { + border: 1px solid #000; +} + +.armed { + color: #666; +} + +input.title { + font-size: 16pt; + display: block; + width: 97%; + /*margin: -7px;*/ + padding: 7px; +} + +.hint, +.formatting { + font-size: 11px; + color: #666; + display: block; + padding: 5px; + padding-left: 0px; +} + +.formatting { + float: right; +} + +#registerform,#loginform { + overflow: hidden; +} + +#loginform { + float: left; + padding-right: 2em; + width: 50%; +} + +.savebox { + padding-top: 20px; +} + +.savebox button, +.savebox a { + font-weight: bold; +} + +.savebox button { + padding: 0.2em; + padding-bottom: 0.4em; + min-width: 6em; + text-align: center; + border: 1px solid #1553a4; + color: #fff; + background-color: #1553a4; +} + +/* @end */ + +/* @group Page Layout */ + +body { + background: #fff url(/img/page_bg.png) repeat-x; + position: absolute; + width: 100%; + height: 100%; + text-align: center; +} + +#frame { + /* + position: absolute; + width: 100%; + height: 100%; + text-align: center; + */ + width: 900px; + /*overflow: hidden;*/ + margin: 0 auto 0 auto; + text-align: left; +} + +#page, +#header #menu { + /* + width: 900px; + overflow: hidden; + margin: 0 auto 0 auto; + text-align: left; + */ +} + +#page { + clear: both; + text-align: left; +} + +#innerframe { + /*padding: 20px;*/ + /*background-color: #fff;*/ + z-index: 1; + /*overflow: hidden;*/ + /*border-bottom: 2px solid #d6d6d6; + border-right: 1px solid #d6d6d6;*/ +} + +#breadcrumbs { + /*margin-left: -15px;*/ + font-weight: bold; + margin-top: 0.5em; + font-size: 1.0em; + padding-top: 0.5em; + margin-bottom: 0.1em; + width: inherit; + font-variant: small-caps; + text-transform: lowercase; + color: #666; +} + +.user_link { + white-space: nowrap; +} + +.user_icon { + margin-bottom: -3px; +} + +/* @end */ + +/* @group Header */ + +#header { + /*background: #003366 url(/img/header_bg.png) repeat-x top left;*/ + padding-top: 0; + width: 100%; + clear: both; + border: 0px; +} + +#header td { + vertical-align: bottom; +} + +#header a { + text-align: left; + color: #fff; +} + +#header .site_title { + min-width: 222px; + min-height: 62px; +} + +#header #menu { + width: 100%; +} + +#header #menu ul { + list-style: none; + margin-bottom: 0em; + margin-left: 2em; + width: 100%; + font-weight: bold; +} + +#header #menu li { + display: inline-block; + padding: 0.5em 0.6em 0.5em 0.6em; + margin-right: 0.2em; + font-size: 1.1em; + font-weight: bold; + text-align: center; + background-color: #666; + background-image: url(/img/header_bg.png); + background-repeat: repeat-x; + background-position: top left; + /*min-width: 5em;*/ +} + +#header #menu li a { + display: inline-block; + width: 100%; + height: 100%; + text-align: center; + text-decoration: none; +} + +#header #menu li ul { + display: none; +} + +#header #menu li:hover { + background-color: #1553a4; +} + +#header #menu li:hover ul { + display: block; + position: absolute; + width: auto; + margin-left: -0.6em; + margin-top: 0.5em; + /* + border-right: 1px solid #ddd; + border-bottom: 1px solid #ddd;*/ +} + +#header #menu li:hover ul li { + display: block; + font-size: 0.9em; + padding: 0.5em; + border-top: 1px solid white; +} + +#header #menu li ul li a { + text-align: left; +} + +#account { + float: right; + text-align: right; + display: block; + line-height: 1.5em; + color: black; + background-color: #eaeaea; + border-bottom: 1px solid #ccc; + border-right: 1px solid #ccc; + padding-left: 0.7em; + padding-right: 0.7em; + padding-bottom: 0.4em; + padding-top: 0.2em; +} + +#account form { + display: inline; + padding-left: 15px; +} + +#account a { + text-transform: lowercase; +} + +#account a.user_name { + /*color: white;*/ + text-decoration: none; + font-weight: bold; +} + +#account .join { + text-decoration: none; + font-weight: bolder; + padding: 0.4em; + float: left; +} + +#account img { + padding-right: 0px; + margin-bottom: -3px; +} + +#account input { + border: 1px solid white; +} + +#account button img { + float: none; + margin-bottom: -3px; +} + +/* @end */ + +/* @group Footer */ + +#footer { + clear: both; + vertical-align: baseline; + bottom: 0px; + margin-top: 1em; + padding-top: 1em; + border-top: 1px solid #aaa; + margin-bottom: 20px; + color: #222; +} + +#footer a { + color: #222; + text-decoration: underline; +} + +#footer a.nolink { + text-decoration: none; +} + +#footer #links { + font-size: 11px; + line-height: 16px; +} + +#footer #links #button { + float: left; + padding-right: 10px; + margin-top: -2px; +} + +#footer #license { + float: right; +} + +/* @end */ + +/* @group Error Messages */ + +#flash_messages, +.warning_box { + border-top: 1px solid #de5614; + border-bottom: 1px solid #de5614; + background-color: #fcea9a; + color: #de5614; + padding: 20px; +} + +#flash_messages { + font-weight: bold; +} + +.error-message { + background: url(/img/icons/cancel.png) top left no-repeat; + padding-left: 20px; + display: block; + padding-top: 2px; + margin-bottom: -3px; + color: #de5614; +} + +.error { + border: 1px solid #de5614; + background-color: #fcea9a; +} + +/* @end */ + +/* @group State Flags */ + +.state { + font-variant: small-caps; + text-transform: lowercase; + font-weight: bolder; + padding: 0.0em 0.2em 0.1em 0.2em; + color: white; + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.state.draft { + background-color: #666; +} + +.state.voting { + background-color: #5766ff; +} + +.state.activating { + background-color: #82f200; +} + +.state.deactivating { + background-color: #ee9805; +} + +.state.unmet, .state.blocked { + background-color: #9d0000; +} + +.state.active { + background-color: #009800; +} + +/* @end */ + +/* @group Typography */ + +#content { + width: inherit; +} + +#content p { + line-height: 1.3em; +} + +#content ul { + list-style-type: square; + list-style-position: inside; +} + +#content ul li, +#content ol li { + margin: 5px; + margin-right: 7px; + line-height: 1.3em; + list-style-position: outside; + margin-left: 20px; +} + +ins.diff, ins.modified { + background-color: rgba(57,241,6,0.5); + color: #000; +} + +del.diff, del.modified { + background-color: rgba(217,25,19,0.5); + color: #000; +} + +p img[align=left] { + margin-right: 7px; +} + +.doc h1, .doc h2, .doc h3, .doc h4 { + border-bottom: 0px; +} + +/* @end */ + +/* @group Autocompleter */ + +.ac_results { + padding: 0px; + border: 1px solid #737275; + background-color: white; + overflow: hidden; + z-index: 99999; + min-width: 120px; +} + +.ac_results ul { + width: 100%; + list-style-position: outside; + list-style: none; + padding: 0; + margin: 0; +} + +.ac_results li { + margin: 0px; + padding: 3px 9px; + cursor: default; + display: block; + font-size: 12px; + line-height: 14px; + overflow: hidden; +} + +.ac_over { + background-color: #737275; + color: white; +} + +/* @end */ + +h1, h2, h3 { + padding-bottom: 0.3em; + margin-bottom: 0.7em; + border-bottom: 1px solid #aaa; +} + +h1 img.cd, +h2 img.cd, +h3 img.cd, +h4 img.cd, +.page_title img.cd { + float: left; + margin-top: -0px; + padding-right: 5px; +} + +.tile { + background-color: #f3f3f3; + padding: 0.3em; + padding-top: 0.1em; + border-top: 1px solid #f0f0f0; + border-left: 1px solid #f0f0f0; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + margin-bottom: 0.3em; +} + +/* +.tile.title { + margin-bottom: 0.5em; + padding-bottom: 0.5em; + border-bottom: 1px solid #666; +}*/ + +.tile .logo { + float: left; + padding-right: 1em; + padding-top: 0.4em; +} + +.tile p { + margin-top: 0; +} + +.tile .text { + padding: 0.2em; + padding-bottom: 0.5em; + margin: 0; +} + +.tile .text ul li { + list-style-type: square; +} + +.tile .text ol li { + list-style-type: decimal; +} + +.tile .meta { + /*float: right;*/ + clear: both; + overflow: hidden; + display: block; + width: auto; + color: #666; + font-size: 0.9em; + text-align: right; + padding: 0.5em; + padding-right: 0em; + padding-top: 1em; +} + +.tile .meta img { + margin-bottom: -4px; +} + +.tile .meta a { + font-weight: bold; +} + +.tile h2, +.tile h3 { + margin-bottom: 0.0em; + margin-top: 0.2em; +} + +.table { + margin-left: 10px; + margin-right: 0px; + overflow: hidden; +} + +.table .tile h3 { + border-bottom: 0px; +} + +.table .tile { + margin-bottom: 1em; + width: inherit; +} + +.table .tile .meta { + padding-top: 0em; +} + +.tile.decision .affirm { + font-weight: bold; + color: #009800; +} + +.tile.decision .dissent { + font-weight: bold; + color: #9d0000; +} + +.tile.decision .abstain { + font-weight: bold; + color: #666; +} + +.tile.decision .undecided { + font-weight: bold; + color: #ffe700; +} + +.tile.voting_booth { + overflow: hidden; +} + +.tile.voting_booth table { + border-collapse: collapse; +} + + +.tile.voting_booth th { + font-size: small; + color: #666; + font-variant: small-caps; + text-transform: lowercase; +} + +.tile.voting_booth th, +.tile.voting_booth td { + vertical-align: top; + border-bottom: 1px solid ; + padding-top: 0.3em; + padding-bottom: 0.3em; + margin-bottom: 0.2em; + border-color: #eaeaea; +} + +.tile.voting_booth .summary td { + border-bottom: 0px; + vertical-align: bottom; +} + +.tile.voting_booth td .icon { + display: block; + width: 16px; + height: 16px; + float: right; + background-image: url(/img/icons/vote_icons.png); +} + +.tile.voting_booth tr.affirm .icon { + background-position: 16px 0px; +} + +.tile.voting_booth tr.dissent .icon { + background-position: 16px 32px; +} + +.tile.voting_booth tr.abstain .icon { + background-position: 16px 16px; +} + +.tile.voting_booth tr.affirm .option { + font-weight: bold; + color: #009800; +} + +.tile.voting_booth tr.dissent .option { + font-weight: bold; + color: #9d0000; +} + +.tile.voting_booth tr.abstain .option { + font-weight: bold; + color: #666; +} + + +.tile.voting_booth .decision td { + background-color: #fdfbc0; + border: 1px solid #fdf59a; +} + +.tile.voting_booth ul, +.tile.voting_booth li { + padding: 0 !important; + margin: 0 !important; +} + +.tile.voting_booth li { + list-style-type: none !important; +} + +.tile.voting_booth input { + margin-top: 0px; +} + + +h2 { + width: inherit; +} + +.tile .text { + overflow: hidden; + line-height: 1.4em; +} + + +/* TUTU Shit */ + +.tutu { + background-image: none !important; + padding: 1em 3em 1em 3em !important; + margin-bottom: 2em !important; + display: none !important; +} + +.tutu .stfu { + float: right; + margin-top: -0.4em; + margin-right: -2em; + font-weight: bold; + width: 20px; + height: 20px; + text-align: center; + border: 1px solid #336699; + text-decoration: none; +} + +.tutu td { + font-size: 1.3em; + padding-bottom: 0.5em; + vertical-align: middle; +} + +.tutu td.num { + font-size: 2.2em; + color: #aaa; + font-weight: bold; + min-width: 2em; +} + +.tutu h2 { + border-bottom: 0px; +} + +.tutu .tagline { + font-size: 0.9em; + font-weight: normal; +} + +.tutu td.fn { + font-size: 0.9em !important; +} + +.tutu .hint { + font-size: 0.9em; +} + +.nobullet { + list-style-type: none !important; +} + +table.delegations { + width: 100%; +} + +table.delegations td, +table.delegations th { + border-bottom: 1px solid #ddd; + width: 30%; + vertical-align: top; + padding: 0.5em 0 0.5em; + line-height: 1.3em; +} + +table.delegations td li { + margin: 0 !important; +} + +table.delegations td .dgb_link { + font-weight: bold; +} + +table.delegations td .hint { + padding: 0; +} + +table.delegations th { + border-bottom: 1px solid #aaa; + font-size: 1.1em; +} + +.list_tile { + list-style-type: none; + padding-bottom: 0.3em; + margin-left: 0em !important; +} + +.list_tile.category { + padding-left: 1.6em; + background: url(/img/icons/stack_16.png) top left no-repeat; +} + +.list_tile .text { + font-size: 1.1em; + font-weight: bold; +} + +.list_tile .meta { + font-size: 0.9em; + color: #666; +} + +.list_tile.event { + line-height: 1.0em !important; + padding-bottom: 0.1em; + margin: 0px !important; + margin-bottom: 0.2em !important; +} + +.list_tile.event .text { + font-size: 1.0em; + font-weight: normal; + width: inherit; +} + +.list_tile.event .meta { + float: right; + margin-top: 0.4em; + font-size: 0.8em; + font-variant: small-caps; + color: #666; +} + +.sidebar { + float: right; + margin-left: 20px; + width: 300px; + max-width: 300px; + overflow: hidden; +} + +.mainbar { + width: 580px; +} + +.description p { + margin-top: 0em; +} + +.boxed { + background-color: #fff; + border: 1px solid #ddd; + margin: 0.2em; +} + +.boxed .inner_boxed { + padding: 0.5em; +} + +.boxed h4 { + margin-top: 0em; +} + +.sidebar .category_tree li { + list-style-type: none; +} + + +a.button { + float: right; + font-size: 0.9em; + font-size: 100%; + margin-top: 0.9em; + padding: 0.4em; + min-width: 3em; + text-align: center; + text-transform: lowercase; + border-bottom: 1px solid #eee; + border-right: 1px solid #eee; + color: white; + font-weight: bold; + margin-left: 0.5em; +} + +a.button:hover { + text-decoration: none; +} + + +a.button.title { + margin-top: 0.0em; +} + +a.button.edit{ + background-color: grey; +} + +a.button.add { + background-color: #009800; +} + +a.button.delete { + background-color: #9d0000; +} + +a.button.admin { + background-color: #ffa700; +} + +a.button.inactive { + background-color: #f3f3f3; + cursor: pointer; + color: #ccc; +} + +.htwarnbox { + background-color: #9d0000; + font-weight: bold; + font-size: 10pt; + width: 200px; + position: absolute; + padding: 1em; + text-align: left; + color: #fff; + margin-top: 0.5em; +} + +.infobox { + background-repeat: no-repeat; + background-position: center left; + background-image: url(/img/icons/info_16.png); + background-color: #fdfbc0; + padding: 0.8em; + padding-left: 48px; + margin: 0.2em; + display: block; + font-size: 1.0em; + color: #333; + line-height: 1.4em; +} + +.pager { + margin-top: 15px; + margin-bottom: 7px; + text-align: center; + font-size: 0.9em; +} + +.pager .prev_page { + float: left; + text-align: left; +} + +.pager .next_page { + float: right; + text-align: right; +} + +.pager_sorts { + color: #666; + font-size: 0.8em; + text-transform: lowercase; + display: block; + float: right; + padding-bottom: 7px; +} + +.pager_sorts .selected { + font-weight: bold; +} + + + +.comment .tile { + border: 0px; + background-color: #fff; + min-height: 30px; + margin-top: 7px; + margin-left: -24px; +} + +.comment .tile.canonical .text{ + padding-left: 5px; + /*border: 1px solid #b1b8fd;*/ + background-color: rgba(215,209,255,0.5); +} + +.add_canonical { + display: none; + padding: 5px; + margin-bottom: 5px; +} + +.comment .karma { + padding-top: 0.0em; + padding-right: 8px; + text-align: center; +} + +.comment .karma a { + background-repeat: no-repeat; + display: block; + width: 12px; + height: 12px; + margin-bottom: 0.1em; + text-decoration: none; + max-width: 12px; +} + +.comment .karma .score { + font-weight: bold; + color: #666; + font-size: 0.9em; +} + +.comment .karma a.inactive { + cursor: pointer; +} + +.comment .karma a.down { + margin-top: 0.2em; +} + +.comment .karma a.up, +.comment .karma a.up.inactive:hover { + background-image: url(/img/arrows/aup.png); +} + +.comment .karma a.down, +.comment .karma a.down.inactive:hover { + background-image: url(/img/arrows/adown.png); +} + +.comment .karma a.up:hover, +.comment .upvoted .karma a.up { + background-image: url(/img/arrows/aup_a.png); +} + +.comment .karma a.down:hover, +.comment .downvoted .karma a.down { + background-image: url(/img/arrows/adown_a.png); +} + + +.comment .base { + overflow: hidden; +} + +.comment .anchor { + background-color: #fdfbc0; +} + +.comment .meta { + padding-top: 0; + margin-top: -1.1em; + text-align: left; + padding-left: 0px; +} + +.comment a.inactive { + color: #ccc; + text-decoration: none; + cursor: pointer; +} + +.comment .text { + padding: 0.3em; + padding-left: 0em; + padding-bottom: 0.3em; +} + +.comment .sub { + /*margin-top: -5px;*/ + margin-left: 25px; +} + +.comment .pre { + font-size: 0.8em; + margin-bottom: 0.6em; + color: #666; +} + + +.comment .hide, +.comment .show { + display: none; + font-size: 8pt; + min-width: 0.7em; +} + +.comment .button { + margin-top: 0; + font-weight: bold; + font-size: 0.9em; +} + +.comment .reply_form, +.comment .edit_form, +.headrev .edit_form { + padding-bottom: 0.7em; + width: inherit; + display: none; +} + +.comment .reply_form.base_reply, +.headrev .reply_form.base_reply { + display: none; +} + +.comment textarea, +.headrev textarea { + min-height: 8em !important; +} + +.comment .savebox, +.headrev .savebox { + padding-top: 5px; +} + +.comment .savebox button, +.headrev .savebox button { + padding: 0.1em; +} + +.comment .savebox button img, +.headrev .savebox button img { + display: none; +} + +.comment .savebox .cancel, +.headrev .savebox .cancel { + display: none; +} + +.link_discussion { + display: none; +} + +.rev_index { + float: left; + min-width: 2em; + font-size: 2.3em; + color: #999; + padding: 0; + margin: 0; + font-weight: bold; + padding-top: 0.1em; +} + +.headrev { + padding-left: 2em; + padding-right: 1em; + padding-bottom: 1em; +} + +.headrev .text { + padding: 0.4em; + background-color: #ffffd0; +} + +.welcome h1, .welcome h2, .welcome h3 { + border-bottom: 0px; +} + +.canonical { + list-style-type: none; +} + +.prototype { + display: none; +} + +.smallarea { + min-height: 4em; +} + +.jsadd { + width: 98%; + text-align: right; +} + +.jsadd input { + display: inline; + margin: 0; +} + +.checklist li { + list-style-image: url(/img/icons/condition_met.png); +} + +.checklist li.unmet { + list-style-image: url(/img/icons/condition_unmet.png); +} + diff --git a/adhocracy/public/style/iphone.css b/adhocracy/public/style/iphone.css new file mode 100644 index 000000000..ff7596d6b --- /dev/null +++ b/adhocracy/public/style/iphone.css @@ -0,0 +1,28 @@ + +#frame { + width: 95%; + padding: 0 2em 0 2em; +} + +#loginform, #registerform { + width: inherit; +} + +#loginform { + float: none; +} + +.sidebar { + float: none; + width: inherit; + max-width: inherit; + margin-left: 0px; +} + +.mainbar { + width: inherit; +} + +#account { + font-size: large; +} \ No newline at end of file diff --git a/adhocracy/public/style/print.css b/adhocracy/public/style/print.css new file mode 100644 index 000000000..2846eec36 --- /dev/null +++ b/adhocracy/public/style/print.css @@ -0,0 +1,11 @@ + +.account, #account, +.sidebar, +.button, .pager_sorts, +.menu, #menu { + display: none; +} + +.mainbar { + width: auto; +} \ No newline at end of file diff --git a/adhocracy/templates/admin/members.html b/adhocracy/templates/admin/members.html new file mode 100644 index 000000000..7a32d885d --- /dev/null +++ b/adhocracy/templates/admin/members.html @@ -0,0 +1,53 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Members: %s") % c.instance.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.instance.root)|n} » ${_("Members")} +</%def> + +<h1>${_("Members: %s") % c.instance.label}</h1> + +<table border="0"> + <tr> + <th></th> + </tr> + %for membership in c.instance.memberships: + <% + if membership.expire_time: + continue + code = membership.group.code + %> + <tr> + <td>${h.user_link(membership.user)|n}<a name="u_${membership.user.user_name}"> </a></td> + <td>${_("is a %s") % _(membership.group.group_name)} + (<a href="/admin/force_leave?user=${membership.user.user_name}&${h.url_token()}">${_("force to leave")}</a>) + </td> + </tr> + <tr> + <td></td> + <td> + ${_("move to Group:")} + %if code != c.model.Group.CODE_OBSERVER: + <a href="/admin/update_membership?user=${membership.user.user_name}&to_group=${c.model.Group.CODE_OBSERVER}&${h.url_token()}"> + ${_("Observer")} + </a>   + %endif + %if not membership.user.has_permission("vote-cast") or code == c.model.Group.CODE_SUPERVISOR: + <a href="/admin/update_membership?user=${membership.user.user_name}&to_group=${c.model.Group.CODE_VOTER}&${h.url_token()}"> + ${_("Voter")} + </a>   + %endif + %if code != c.model.Group.CODE_SUPERVISOR: + <a href="/admin/update_membership?user=${membership.user.user_name}&to_group=${c.model.Group.CODE_SUPERVISOR}&${h.url_token()}"> + ${_("Supervisor")} + </a> + %endif + </td> + </tr> + <tr> + <td></td> + <td></td> + </tr> + %endfor +</table> \ No newline at end of file diff --git a/adhocracy/templates/admin/permissions.html b/adhocracy/templates/admin/permissions.html new file mode 100644 index 000000000..7eecc5ecf --- /dev/null +++ b/adhocracy/templates/admin/permissions.html @@ -0,0 +1,45 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Admin: Group Permissions")}</%def> + +<%def name="breadcrumbs()"> + ${_("Admin » Group Permissions")|n} +</%def> + +<form name="settings" class="inplace" method="POST" action="/admin/permissions"> + ${h.field_token()|n} + <h1>${_("Group Permissions")}</h1> + + <strong> + ${_("WARNING: This allows you to shut yourself out of your adhocracy. Handle with care!")} + </strong> + <br/><br/> + + <% + groups = c.model.Group.all() + %> + <table border="0"> + <tr> + <th></th> + %for group in groups: + <th>${_(group.group_name)}</th> + %endfor + </tr> + %for permission in c.model.Permission.all(): + <tr> + <td>${permission.permission_name}</td> + %for group in groups: + <td> + <input name="${group.code}-${permission.permission_name}" + value="True" type="checkbox" style="width: auto" + %if permission in group.permissions: + checked="checked" + %endif + /> + </td> + %endfor + </tr> + %endfor + </table> + ${components.savebox("/")} +</form> \ No newline at end of file diff --git a/adhocracy/templates/category/create.html b/adhocracy/templates/category/create.html new file mode 100644 index 000000000..073b4e977 --- /dev/null +++ b/adhocracy/templates/category/create.html @@ -0,0 +1,24 @@ +<%inherit file="/template.html" /> +<%namespace name="catlist" file="/category/tree.html"/> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("New category")}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.instance.root)|n} » ${_("New category")} +</%def> + +<form name="create_category" class="inplace" method="POST" action="/category/create"> + ${h.field_token()|n} + <div id="page_title"> + <label for="label" class="armhint">${_("New category")}</label> + <input tabindex="1" class="title armlabel" name="label" /> + </div> + + ${catlist.contextselect(type="radio", root=True)} + <div class="mainbar"> + <h3>${_("Category description:")}</h3> + <textarea tabindex="2" class="description" name="description"></textarea> + ${components.formatting()} + ${components.savebox("/category/%s" % request.params.get('cat'))} + </div> +</form> \ No newline at end of file diff --git a/adhocracy/templates/category/edit.html b/adhocracy/templates/category/edit.html new file mode 100644 index 000000000..072046a3d --- /dev/null +++ b/adhocracy/templates/category/edit.html @@ -0,0 +1,27 @@ +<%inherit file="/template.html" /> +<%namespace name="catlist" file="/category/tree.html"/> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Edit %s") % c.category.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.category)|n} » ${_("Edit")} +</%def> + +<form name="create_category" class="inplace" method="POST" action="/category/edit/${c.category.id}"> + ${h.field_token()|n} + <div class="page_title"> + <label for="label" class="armhint">${_("Category title")}</label> + <input tabindex="1" class="title armlabel" name="label" value="${c.category.label}"/> + </div> + + <div class="sidebar"> + ${catlist.contextselect(type="radio", sels=c.category.parents, root=True)} + </div> + + <div class="mainbar"> + <h3>${_("Category description")}</h3> + <textarea tabindex="2" class="description" name="description">${c.category.description}</textarea> + ${components.formatting()} + ${components.savebox("/category/%s" % c.category.id)} + </div> +</form> \ No newline at end of file diff --git a/adhocracy/templates/category/tiles.html b/adhocracy/templates/category/tiles.html new file mode 100644 index 000000000..f464ec70b --- /dev/null +++ b/adhocracy/templates/category/tiles.html @@ -0,0 +1,28 @@ + +<%def name="list_item(tile, category)"> + <li class="list_tile category"> + <span class="text"><a href="/category/${category.id}">${category.label}</a></span> + + <span class="meta"> + (${ungettext("%s issue", "%s issues", tile.num_issues) % tile.num_issues}) + </span> + </li> +</%def> + +<%def name="row(tile, category)"> + <div class="tile"> + <div class="logo"> + <img src="/img/icons/stack_24.png" /> + </div> + <h3><a class="link" href="/category/${category.id}">${category.label}</a></h3> + <div class="text"> + ${tile.tagline} + </div> + <div class="meta"> + ${h.user_link(category.creator)|n} + · ${_("created %s") % h.relative_time(category.create_time)} + · ${ungettext("%s category", "%s categories", tile.num_categories) % tile.num_categories} + · ${ungettext("%s issue", "%s issues", tile.num_issues) % tile.num_issues} + </div> + </div> +</%def> \ No newline at end of file diff --git a/adhocracy/templates/category/tree.html b/adhocracy/templates/category/tree.html new file mode 100644 index 000000000..b0c499bf8 --- /dev/null +++ b/adhocracy/templates/category/tree.html @@ -0,0 +1,53 @@ +<%def name="catboxen(category, type, sels=[], link=True, root=False)"> +<% +children = [] +if root: + root = c.instance.root + children.append((root, + len(root.search_children(recurse=True, cls=c.model.Motion)))) +else: + if not category: + category = c.instance.root + children = category.search_children(cls=c.model.Category) + children = [(ch, len(set(ch.search_children(recurse=True, cls=c.model.Motion)))) for ch in children] + children = sorted(children, lambda a, b: b[1]-a[1]) + +selected = [se.id for se in sels] +selected.append(request.params.get("cat")) + +%> +<ul class="category_tree"> + %for (child, motions) in children: + <li> + <input name="categories" value="${child.id}" type="${type}" + %if child.id in selected: + checked="checked" + %endif + > + %if link: + <a href="/category/${child.id}"> + %endif + ${child.label} + %if link: + </a> + %endif + %if len(child.search_children(cls=c.model.Category)): + ${catboxen(child, type, sels=sels, link=link)} + %endif + </li> + %endfor +</ul> +</%def> + + +<%def name="contextselect(type, sels=[], help=None, root=False)"> + <div class="sidebar"> + <h3>${_("Categorization")}</h3> + %if help: + <p> + ${help} + </p> + %endif + ${self.catboxen(None, type, sels=sels, link=False, root=root)} + </div> +</%def> \ No newline at end of file diff --git a/adhocracy/templates/category/view.html b/adhocracy/templates/category/view.html new file mode 100644 index 000000000..905bed0c5 --- /dev/null +++ b/adhocracy/templates/category/view.html @@ -0,0 +1,83 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${c.category.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.category)|n} +</%def> + +%if c.tile.can_delete: + <a class="button title delete" href="/category/delete/${c.category.id}?${h.url_token()}">${_("delete")}</a> +%elif c.tile.lack_delete_karma: + <a class="button title inactive htwarn" title="${c.tile.lack_delete_karma}">${_("delete")}</a> +%endif + +%if c.tile.can_edit: + <a class="button title edit" href="/category/edit/${c.category.id}">${_("edit")}</a> +%elif c.tile.lack_edit_karma: + <a class="button title inactive htwarn" title="${c.tile.lack_edit_karma}">${_("edit")}</a> +%endif + +%if c.tile.can_delegate: + <a class="button title add" href="/delegation/create?scope=${c.category.id}">${_("delegate")}</a> +%endif + +<h1 class="page_title"><img src="/img/icons/stack_24.png" class="cd" /> ${_("Category: %s") % c.category.label}</h1> + +<div class="sidebar"> + <div class="tile"> + <div class="text"> + %if len(c.tile.description): + ${c.tile.description|n} + %endif + </div> + ${components.delegation_sidebar(c.category)} + <div class="meta"> + <!-- ${h.user_link(c.category.creator)|n} + · + --> + ${_("created %s") % h.relative_time(c.category.create_time)} + <!-- · <a href="javascript:alert('not implemented!')">${_("history")}</a> --> + · ${h.rss_link("/category/%s.rss" % c.category.id)|n} + </div> + </div> + <br/> + + %if c.tile.can_create_category: + <a class="button add" href="/category/create?cat=${c.category.id}">${_("new")}</a> + %elif c.tile.lack_create_category_karma: + <a class="button inactive htwarn" title="${c.tile.lack_create_category_karma}">${_("new")}</a> + %endif + <h2>${_("Subcategories")} (${c.tile.num_categories})</h2> + %if c.tile.num_categories == 0 and c.tile.can_create_category: + <span class="infobox"> + ${_("Create new categories to further structure the debate.")} + </span> + <br/><br/> + %else: + <ul> + ${c.subcats_pager.here()} + </ul> + %endif +</div> + +<div class="mainbar"> + <br/> + %if c.tile.can_create_issue: + <a class="button add" href="/issue/create?cat=${c.category.id}">${_("new")}</a> + %elif c.tile.lack_create_issue_karma: + <a class="button inactive htwarn" title="${c.tile.lack_create_issue_karma}">${_("new")}</a> + %endif + <h2>${_("Issues")} (${c.tile.num_issues})</h2> + %if c.tile.num_issues == 0: + %if c.tile.can_create_category: + <span class="infobox"> + ${_("Create an issue to discuss a new topic within the current category.")} + </span> + %endif + %else: + <div class="table"> + ${c.issues_pager.here()} + </div> + %endif +</div> \ No newline at end of file diff --git a/adhocracy/templates/comment/create.html b/adhocracy/templates/comment/create.html new file mode 100644 index 000000000..252b3f075 --- /dev/null +++ b/adhocracy/templates/comment/create.html @@ -0,0 +1,14 @@ +<%inherit file="/template.html" /> +<%namespace name="t" file="/comment/tiles.html"/> +<%def name="title()">${_("New comment")}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.instance.root)|n} » ${_("New comment")} +</%def> + +<h1>${_("Comment")}</h1> + +<div class="sidebar"> </div> +<div class="mainbar"> + ${t.create_form(None, None)} +</div> \ No newline at end of file diff --git a/adhocracy/templates/comment/edit.html b/adhocracy/templates/comment/edit.html new file mode 100644 index 000000000..a57c5e983 --- /dev/null +++ b/adhocracy/templates/comment/edit.html @@ -0,0 +1,17 @@ +<%inherit file="/template.html" /> +<%namespace name="t" file="/comment/tiles.html"/> +<%def name="title()">${_("Edit comment")}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.comment.topic)|n} » ${_("Edit comment")} +</%def> + +<h1>${_("Comment on: %s") % c.comment.topic.label}</h1> + +<div class="sidebar"> </div> +<div class="mainbar"> + ${t.edit_form(c.comment)} +</div> + + + diff --git a/adhocracy/templates/comment/history.html b/adhocracy/templates/comment/history.html new file mode 100644 index 000000000..d1da78445 --- /dev/null +++ b/adhocracy/templates/comment/history.html @@ -0,0 +1,19 @@ +<%inherit file="/template.html" /> +<%def name="title()">${_("Comment History")}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.comment.topic)|n} » ${_("Comment History")} +</%def> + +<h1><img src="/img/icons/discuss_24.png" class="cd" /> ${_("Comment History")}</h1> + +<div class="sidebar"> +   +</div> +<div class="mainbar"> + <div class="table"> + <div id="tile_c${c.comment.id}"> + ${c.revisions_pager.here()} + </div> + </div> +</div> diff --git a/adhocracy/templates/comment/revision_tiles.html b/adhocracy/templates/comment/revision_tiles.html new file mode 100644 index 000000000..83f93fe4b --- /dev/null +++ b/adhocracy/templates/comment/revision_tiles.html @@ -0,0 +1,42 @@ +<%namespace name="comment_tiles" file="/comment/tiles.html"/> + +<%def name="row(tile, revision)"> + <div class="tile"> + <div class="rev_index">${tile.index}</div> + ${h.user_link(revision.user)|n} + · ${_("created %s") % h.relative_time(revision.create_time)} + + <div class="meta" style="clear: none;"> +   + + %if tile.can_revert: + <a href="/comment/${revision.comment.id}/revert?${h.url_token()}&to=${revision.id}">${_("revert here")}</a> + %endif + + %if tile.is_latest: + <a href="/comment/r/${revision.comment.id}">${_("view comment")}</a> + %if tile.can_edit: + · <a href="/comment/edit/${revision.comment.id}" onClick="return comment_edit(${revision.comment.id})" >${_("edit")}</a> + %endif + %endif + </div> + </div> + + + <div class="headrev"> + %if tile.is_latest and tile.can_edit: + <div class="edit_form"> + ${comment_tiles.edit_form(revision.comment)} + </div> + %endif + %if tile.is_latest: + <div class="hide_edit"> + %endif + <div class="text"> + ${tile.diff_text|n} + </div> + %if tile.is_latest: + </div> + %endif + </div> +</%def> \ No newline at end of file diff --git a/adhocracy/templates/comment/tiles.html b/adhocracy/templates/comment/tiles.html new file mode 100644 index 000000000..c5320f611 --- /dev/null +++ b/adhocracy/templates/comment/tiles.html @@ -0,0 +1,174 @@ +<%namespace name="components" file="/components.html"/> + + +<%def name="row(tile, comment)"> + <div class="tile"> + + <div class="logo"> + <img src="/img/icons/discuss_24.png" /> + </div> + + %if len(tile.tagline): + <div class="text"> + <a href="/comment/r/${comment.id}">${_("Comment")}</a>: ${tile.tagline} + </div> + %endif + + <div class="meta"> + ${h.user_link(comment.creator)|n} + %if len(comment.revisions) == 1: + · ${_("created %s") % h.relative_time(comment.create_time)} + %else: + · ${_("edited %s") % h.relative_time(comment.latest.create_time)} + %endif + + + · ${_("in")} + %if tile.on_issue: + <img src="/img/icons/issue_16.png" /> + %elif tile.on_motion: + <img src="/img/icons/motion_16.png" /> + %endif + <a href="/d/${comment.topic.id}">${comment.topic.label}</a> + </div> + </div> +</%def> + + + + +<%def name="full(tile, comment, recurse=True, collapse=True, link_discussion=False)"> + <% + if not tile.show: + return + %> + <div class="comment" id="c${comment.id}"> + <div class="tile ${'canonical' if comment.canonical else ''} ${tile.karma_position}" id="tile_c${comment.id}"> + <div class="logo karma"> + %if tile.can_give_karma: + <a class="up" href="/karma/give?comment=${comment.id}&value=1" onclick="return comment_karma(${comment.id}, 1);"> +   + </a> + <span class="score">${tile.karma_score}</span> + <a class="down" href="/karma/give?comment=${comment.id}&value=-1" onclick="return comment_karma(${comment.id}, -1);"> +   + </a> + %elif not c.user: + <a class="up inactive htwarn" title="${_('Sign in to rate comments')}"> </a> + <span class="score">${tile.karma_score}</span> + <a class="down inactive htwarn" title="${_('Sign in to rate comments')}"> </a> + %elif tile.is_own: + <a class="up inactive htwarn" title="${_('This is your own comment')}"> </a> + <span class="score">${tile.karma_score}</span> + <a class="down inactive htwarn" title="${_('This is your own comment')}"> </a> + %elif tile.is_immutable: + <a class="up inactive htwarn" title="${h.immutable_motion_message()}"> </a> + <span class="score">${tile.karma_score}</span> + <a class="down inactive htwarn" title="${h.immutable_motion_message()}"> </a> + %elif tile.lack_give_karma_karma: + <a class="up inactive htwarn" title="${tile.lack_give_karma_karma}"> </a> + <span class="score">${tile.karma_score}</span> + <a class="down inactive htwarn" title="${tile.lack_give_karma_karma}"> </a> + %endif + </div> + + <div class="base"> + <div class="pre"> + %if link_discussion: + %if tile.num_children: + <a class="button add reply" onclick="return comment_link_discussion(${comment.id})" href="/comment/${comment.id}">${_("discussion")} (${tile.num_children}) »</a> + %endif + %endif + + %if collapse: + <a class="button delete hide" href="javascript:comment_hide(${comment.id})">-</a> + <a class="button add show" href="javascript:comment_show(${comment.id})">+</a> + %endif + + %if tile.can_reply and (recurse or link_discussion): + <a class="button add reply" onClick="return comment_reply(${comment.id})" href="/comment/create?reply=${comment.id}&topic=${comment.topic.id}">${_("reply")}</a> + %elif tile.lack_reply_karma and recurse: + <a class="button reply inactive htwarn" title="${tile.lack_reply_karma}">${_("reply")}</a> + %endif + + %if tile.can_edit: + <a class="button edit" onClick="return comment_edit(${comment.id})" href="/comment/edit/${comment.id}">${_("edit")}</a> + %elif tile.is_immutable: + <a class="button edit inactive htwarn" title="${h.immutable_motion_message()}">${_("edit")}</a> + %elif tile.lack_edit_karma: + <a class="button edit inactive htwarn" title="${tile.lack_edit_karma}">${_("edit")}</a> + %endif + + %if tile.can_delete: + <a class="button delete" href="/comment/delete/${comment.id}?${h.url_token()}">${_("delete")}</a> + %endif + + %if tile.is_deleted: + ${_("deleted %s") % h.relative_time(comment.delete_time)} + %else: + <b>${h.user_link(comment.creator)|n}</b> + %if not tile.is_edited: + · ${_("%s") % h.relative_time(comment.create_time)} + %else: + · + %if comment.latest.user == comment.creator: + ${_("edited %s") % h.relative_time(comment.latest.create_time)|n} + %else: + ${_("edited %s by %s") % (h.relative_time(comment.latest.create_time), + h.user_link(comment.latest.user))|n} + %endif + (<a href="/comment/${comment.id}/history">${_("history")}</a>) + %endif + %endif + + <a name="c${comment.id}"> </a> + </div> + <div class="text hide_edit"> + %if not tile.is_deleted: + ${tile.text|n} + %else: + <p> + <span class="hint">${_("This comment has been deleted.")}</span> + </p> + %endif + </div> + + <div class="edit_form"> + ${self.edit_form(comment)} + </div> + <div class="reply_form sub ${'base_reply' if reply_here else ''}"> + ${self.create_form(comment, comment.topic)} + </div> + </div> + </div> + %if recurse or link_discussion: + <div class="sub ${'link_discussion' if link_discussion else ''}"> + %for reply in tile.replies: + ${self.full(tile.__class__(reply), reply, collapse=collapse)} + %endfor + </div> + %endif + </div> +</%def> + +<%def name="edit_form(comment)"> + <form name="edit_comment" class="inplace" method="POST" action="/comment/edit/${comment.id}"> + ${h.field_token()|n} + <textarea tabindex="2" class="description" name="text">${comment.latest.text}</textarea> + ${components.formatting()} + ${components.savebox("/d/%s" % comment.topic.id)} + </form> +</%def> + +<%def name="create_form(parent, topic, canonical=0)"> + <form name="create_comment" class="inplace" method="POST" action="/comment/create"> + ${h.field_token()|n} + <input type="hidden" name="topic" value="${topic.id if topic else request.params.get('topic')}" /> + <input type="hidden" name="reply" value="${parent.id if parent else request.params.get('reply')}" /> + <input type="hidden" name="canonical" value="${request.params.get('canonical', canonical)}" /> + + <textarea tabindex="2" class="description" name="text"></textarea><br/> + ${components.formatting()} + ${components.savebox("/d/%s" % parent.topic.id if parent else request.params.get('topic'))} + </form> +</%def> diff --git a/adhocracy/templates/comment/view.html b/adhocracy/templates/comment/view.html new file mode 100644 index 000000000..f447ff551 --- /dev/null +++ b/adhocracy/templates/comment/view.html @@ -0,0 +1,12 @@ +<%inherit file="/template.html" /> +<%def name="title()">${_("Comment")}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.comment.topic)|n} » ${_("Comment")} +</%def> + +<h1><img src="/img/icons/discuss_24.png" class="cd" /> ${_("Discussion on %s") % ("<a href='/d/%s'>%s</a>" % (c.comment.topic.id, c.comment.topic.label))|n}</h1> + +<div class="mainbar"> + ${tiles.comment.full(c.comment)} +</div> diff --git a/adhocracy/templates/components.html b/adhocracy/templates/components.html new file mode 100644 index 000000000..b2a1a7163 --- /dev/null +++ b/adhocracy/templates/components.html @@ -0,0 +1,107 @@ + +<%def name="formatting()"> + <div class="formatting"> + <a target="_new" href="/page/formatting.html">${_("formatting hints")|n}</a> + </div> +</%def> + +<%def name="savebox(cancel_url)"> + <div class="savebox"> + <button type="submit"><img src="/img/icons/save.png"> + ${_("Save")}</button> + <span class="cancel"> + ${_("or")} <a href="${cancel_url}">${_("cancel")}</a> + </span> + </div> +</%def> + +<%def name="login_ctxitem()"> + %if not c.user: + <div class="contextitem"> + <h3>${_("Say hi")}</h3> + <%include file="/account/login_form.html"/> + + <p>${_("If you're not registered yet, <a href='/register'>sign up here</a>.")}</p> + </div> + %endif +</%def> + +<%def name="delegation_sidebar(delegateable)"> + %if c.tile.can_vote: + <div class="boxed"> + <div class="inner_boxed"> + <h4><img src="/img/icons/delegate_16.png" class="cd" /> ${_("Delegate voting")}</h4> + <ul> + <li style="list-style-image: url(/img/icons/delegate_to_16.png); padding-left: 5px;"> + %if not c.tile.has_delegated: + %if c.tile.has_overridden: + ${_("You have voted yourself and not delegated voting.")} + <a href="/page/faq.html#Whatisdelegatedvoting">${_("Info...")}</a> + %else: + ${_("You have not delegated voting.")} + <a href="/delegation/create?scope=${delegateable.id}">${_("delegate")}</a> · + <a href="/page/faq.html#Whatisdelegatedvoting">${_("info...")}</a> + %endif + %else: + %if c.tile.has_overridden: + ${_("By voting yourself, you have overridden:")} + %else: + ${_("You have delegated voting to:")} + %endif + %endif + <ul> + %for delegation in c.tile.delegations: + <li> + ${h.user_link(delegation.agent)|n} + · ${_("on")} <a href="/d/${delegation.scope.id}">${delegation.scope.label}</a> + · <a href="/delegation/${delegation.id}">${_("review")}</a> + </li> + %endfor + </ul> + </li> + %if c.tile.num_principals: + + <li style="list-style-image: url(/img/icons/delegated_to_16.png); padding-left: 5px;"> + %if c.tile.num_principals == 1: + ${_("You hold an additional vote.")} + %else: + ${_("You hold %s additional votes.") % c.tile.num_principals} + %endif + </li> + %endif + </ul> + </div> + </div> + %endif +</%def> + +<%def name="tutu()"> + <div class="infobox tutu"> + <a class="stfu" href="javascript:hide_tutu()">x</a> + <h2>${_("What now?")} <span class="tagline">${_("— using Adhocracy in 3<sup>½</sup> steps:")|n}</span></h2> + + <table> + <tr> + <td class="num">1.</td> + <td width="20%"><img src="/img/icons/issue_32.png" /></td> + <td>${_("Create and discuss issues that need solutions.")}</td> + </tr> + <tr> + <td class="num">2.</td> + <td><img src="/img/icons/motion_32.png" /></td> + <td>${_("Cooperate to develop proposals affecting the issues.")}</td> + </tr><tr> + <td class="num">3.</td> + <td><img src="/img/icons/vote_32.png" /></td> + <td>${_("Vote on proposals to collectively make decisions.*")}</td> + </tr><tr> + <td></td> + <td></td> + <td class="fn"> + ${_("*Or — if you like — delegate voting in some fields to a peer.")|n} + <br/><br/> + </td> + </tr> + </table> + </div> +</%def> diff --git a/adhocracy/templates/decision/tiles.html b/adhocracy/templates/decision/tiles.html new file mode 100644 index 000000000..a9b0f5b5d --- /dev/null +++ b/adhocracy/templates/decision/tiles.html @@ -0,0 +1,71 @@ + +<%def name="row(tile, decision, focus_motion=False, focus_user=False)"> + <div class="tile decision"> + <div class="logo"> + %if decision.result == 1: + <img src="/img/icons/vote_for.png" /> + %elif decision.result == -1: + <img src="/img/icons/vote_against.png" /> + %else: + <img src="/img/icons/vote_abstain.png" /> + %endif + </div> + + <h3> + %if decision.result == 1: + <span class="affirm">${_("for")}</span>: + %elif decision.result == -1: + <span class="dissent">${_("against")}</span>: + %elif decision.result == 0: + <span class="abstain">${_("abstained")}</span>: + %else: + <span class="undecided">${_("undecided")}</span>: + %endif + + %if focus_user: + <a href="/user/${decision.user.user_name}">${decision.user.name}</a> + %endif + + %if focus_motion: + <a href="/motion/${decision.poll.motion.id}">${decision.poll.motion.label}</a> + %endif + </h3> + + <div class="text"> + %if not decision.made(): + ${_("The user's delegates have voted, but no consensus was" + + "reached among them. The decision is deferred.")} + %else: + %if decision.self_made(): + ${_("The decision was made without delegations.")} + %else: + ${_("The decision was determined as a result of the following delegations:")} + %endif + %endif + %if not decision.self_made(): + <ul> + %for delegation in decision.delegations: + <li> + ${h.user_link(delegation.agent)|n} + · ${_("on")} <a href="/d/${delegation.scope.id}">${delegation.scope.label}</a> + · <a href="/delegation/${delegation.id}">${_("review")}</a> + </li> + %endfor + </ul> + %endif + </div> + <div class="meta"> + ${h.relative_time(decision.create_time)|n} + %if not focus_motion: + · <img src="/img/icons/motion_16.png" /> + <a href="/motion/${decision.poll.motion.id}">${decision.poll.motion.label}</a> + %endif + %if decision.poll: + · ${_("in a poll %s") % h.relative_time(decision.poll.begin_time)} + %endif + %if not focus_user: + · ${h.user_link(decision.user)|n} + %endif + </div> + </div> +</%def> \ No newline at end of file diff --git a/adhocracy/templates/delegation/create.html b/adhocracy/templates/delegation/create.html new file mode 100644 index 000000000..e7528016e --- /dev/null +++ b/adhocracy/templates/delegation/create.html @@ -0,0 +1,60 @@ +<%inherit file="/template.html" /> +<%namespace name="catlist" file="/category/tree.html"/> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Delegate: %s") % c.scope.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.scope)|n} » ${_("Delegation")} +</%def> + + +<h1 class="page_title"><img src="/img/icons/delegate_24.png" class="cd"/> ${_("Delegate: %s") % c.scope.label}</h1> + +<form name="create_delegation" class="inplace" method="POST" action="/delegation/create"> + ${h.field_token()|n} + <input name="scope" value="${c.scope.id}" type="hidden" /> + <div class="sidebar"> + <div class="infobox"> + ${_("This page is obviously a placeholder. Nice, karma-based recs are coming soon.")} + </div> + </div> + <div class="mainbar"> + ${h.field_token()|n} + + <% + scope_agents = c.lib.delegateable_popular_agents(c.scope) + %> + %if len(scope_agents): + <h3>${_("Popular delegates for %s") % c.scope.label}</h3> + <ul> + %for (user, score) in scope_agents: + <li> + ${h.user_link(user, link="/delegation/create?agent=%s&scope=%s" % (user.user_name, c.scope.id))|n} (${score}) + </li> + %endfor + </ul> + %endif + + <% + my_agents = c.lib.user_popular_agents(c.user) + %> + %if len(my_agents): + <h3>${_("Your favourite delegates")}</h3> + <ul> + %for (user, score) in my_agents: + <li> + ${h.user_link(user, link="/delegation/create?agent=%s&scope=%s" % (user.user_name, c.scope.id))|n} (${score}) + </li> + %endfor + </ul> + %endif + </div> + + <br/><br/> + + <label for="agent">${_("Delegate to:")}</label> + <input name="agent" class="userCompleted"/> + + ${components.savebox("/d/%s" % c.scope.id)} + +</form> \ No newline at end of file diff --git a/adhocracy/templates/delegation/graph.dot b/adhocracy/templates/delegation/graph.dot new file mode 100644 index 000000000..1418f7c42 --- /dev/null +++ b/adhocracy/templates/delegation/graph.dot @@ -0,0 +1,18 @@ +digraph A { + overlap=false; + node [fontsize=10,fontname="/System/Library/Fonts/Helvetica.dfont"]; + edge [fontsize=7,fontname="/System/Library/Fonts/Helvetica.dfont"]; + %for user in c.users: + ${user.user_name} [label="${user.name}"]; + %endfor + %for delegation in c.delegations: + %if not delegation.revoke_time: + ${delegation.principal.user_name} -> ${delegation.agent.user_name} [label="${delegation.scope.label}"]; + %endif + %endfor + %for delegation in c.delegations: + %if delegation.revoke_time: + ${delegation.principal.user_name} -> ${delegation.agent.user_name} [label="${delegation.scope.label}",style="dotted"]; + %endif + %endfor +} \ No newline at end of file diff --git a/adhocracy/templates/delegation/review.html b/adhocracy/templates/delegation/review.html new file mode 100644 index 000000000..603b09e5c --- /dev/null +++ b/adhocracy/templates/delegation/review.html @@ -0,0 +1,45 @@ +<%inherit file="/template.html" /> +<%def name="title()">${_("Delegation: %s") % c.scope.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.scope)|n} » ${_("Delegation")} +</%def> + +%if c.user and c.delegation.principal == c.user: + <a class="button title delete" href="/delegation/revoke/${c.delegation.id}?${h.url_token()}">${_("revoke")}</a> +%endif +<h1 class="page_title"><img src="/img/icons/delegate_24.png" class="cd"/> ${_("Delegation: %s") % c.scope.label}</h1> + +<div class="sidebar"> + <div class="tile"> + <div class="text"> + <ul> + <li class="nobullet"><img class='user_icon' src="/img/icons/delegate_to_16.png" /> ${_("from %s") % h.user_link(c.delegation.principal)|n}</li> + <li class="nobullet"><img class='user_icon' src="/img/icons/delegated_to_16.png" /> ${_("to %s") % h.user_link(c.delegation.agent)|n}</li> + </ul> + </div> + <div class="meta"> + ${_("established %s") % h.relative_time(c.delegation.create_time)} + %if c.delegation.revoke_time: + · ${_("revoked %s") % h.relative_time(c.delegation.revoke_time)} + %endif + </div> + </div> + <br/> + <span class="infobox"> + ${_("The delegation can be overridden or revoked at any time.")} + </span> + <br/> +</div> + +<div class="mainbar"> + %if c.decisions_pager.items: + <div class="table"> + ${c.decisions_pager.here()} + </div> + %else: + <div class="infobox"> + ${_("No decisions have been based on this delegation yet. As soon as this delegation leads to any decisions, they will be listed here.")} + </div> + %endif +</div> diff --git a/adhocracy/templates/delegation/tiles.html b/adhocracy/templates/delegation/tiles.html new file mode 100644 index 000000000..3c45974b4 --- /dev/null +++ b/adhocracy/templates/delegation/tiles.html @@ -0,0 +1,15 @@ + +<%def name="inbound(tile, delegation)"> + <img class='user_icon' src="/img/icons/delegated_to_16.png" /> + ${_("from %s") % h.user_link(delegation.principal)|n}: + <a href="/delegation/${delegation.id}">${_("track record")}</a> +</%def> + +<%def name="outbound(tile, delegation)"> + <img class='user_icon' src="/img/icons/delegate_to_16.png" /> + ${_("to %s") % h.user_link(delegation.agent)|n}: + <a href="/delegation/${delegation.id}">${_("track record")}</a> + %if delegation.principal == c.user: + · <a href="/delegation/revoke/${delegation.id}?${h.url_token()}">${_("revoke")}</a> + %endif +</%def> \ No newline at end of file diff --git a/adhocracy/templates/error/http.html b/adhocracy/templates/error/http.html new file mode 100644 index 000000000..5eed0d24d --- /dev/null +++ b/adhocracy/templates/error/http.html @@ -0,0 +1,8 @@ +<%inherit file="../template.html" /> +<%def name="title()">${_("Error %s") % c.error_code}</%def> + +<h1>${_("Error %s") % c.error_code}</h1> + +<p>${c.error_message|n}</p> + +<p>${_("If this error continues to occur, please <a href='/page/imprint.html'>notify us</a> with a description of what you were trying to do.")|n}</p> \ No newline at end of file diff --git a/adhocracy/templates/event/all.html b/adhocracy/templates/event/all.html new file mode 100644 index 000000000..86c9c160c --- /dev/null +++ b/adhocracy/templates/event/all.html @@ -0,0 +1,16 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Whazza")}</%def> + +<%def name="breadcrumbs()"> + ${_("All current events in Adhocracy.")} +</%def> + + +<h1>${_("Whazza")}</h1> + +<div class="sidebar"> </div> + +<div class="mainbar"> + ${c.event_pager.here()} +</div> diff --git a/adhocracy/templates/event/tiles.html b/adhocracy/templates/event/tiles.html new file mode 100644 index 000000000..0b9a808e2 --- /dev/null +++ b/adhocracy/templates/event/tiles.html @@ -0,0 +1,12 @@ + +<%def name="list_item(tile, event)"> + <li class="list_tile event"> + <span class="meta"> + ${h.relative_time(event.time)} + </span> + <span class="text"> + ${h.user_link(event.agent)|n} + ${event.html()|n} + </span> + </li> +</%def> \ No newline at end of file diff --git a/adhocracy/templates/index.html b/adhocracy/templates/index.html new file mode 100644 index 000000000..2654a50e4 --- /dev/null +++ b/adhocracy/templates/index.html @@ -0,0 +1,81 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Welcome")}</%def> + +<h1 class="page_title">${_("Welcome to Adhocracy")}</h1> + +<div class="sidebar"> + %if not c.user: + <a class="button add" href="/register">${_("sign up")}</a> + <h2>${_("Say hi")}</h2> + <div class="tile"> + + <%include file="/user/login_form.html"/> + + <div class="meta">${_("If you're not registered, <a href='/register'>sign up.</a>")|n}</div> + </div> + <br/> + %endif + %if c.user: + <a class="button edit" href="/adhocracies">${_("more")}</a> + <a class="button add" href="/instance/create">${_("new")}</a> + <h2>${_("My Adhocracies")}</h2> + %if len(c.user.instances): + <ul> + %for instance in c.user.instances: + ${tiles.instance.list_item(instance)} + %endfor + </ul> + %else: + <div class="infobox"> + ${_("Join a few Adhocracies to contribute to their policies.")} + </div> + %endif + + %else: + <a class="button edit" href="/adhocracies">${_("more")}</a> + <a class="button add" href="/instance/create">${_("new")}</a> + <h2>${_("Adhocracies")}</h2> + <ul> + %for instance in c.instances: + ${tiles.instance.list_item(instance)} + %endfor + </ul> + %endif + <p> + + <br/> + +</div> + +<div class="mainbar welcome"> + <h3 id="claim">${_("Adhocracy helps <strong>groups</strong> make <strong>decisions</strong>.")|n}</h3> + + <p> + ${_("Adhocracy is a platform for virtual direct democracies where you can cooperate to create and" + + "select solutions to your organization's challenges.")} + </p> + <br/> + <h3>${_("What groups?")}</h3> + <p> + <img align="left" src="/img/groups.png" /> + ${_("Think NGOs, open projects. Distributed, loosely-knit groups in search of " + + "a common strategy or groups with complex internal policies like Wikipedia.")} + <a href="/page/faq.html#WhatisLiquidDemocracy">${_("Read more...")}</a> + </p> + <br/> + <h3>${_("What decisions?")}</h3> + <img align="right" src="/img/icons/vote_64.png" /> + <p> + ${_("Anything that discusses a method of solving a problem. This " + + "could be laws, strategy decisions or even design patterns.")} + <a href="/page/faq.html#Howdoesvotingwork">${_("Read more...")}</a> + </p> + + %if c.user and c.user.has_permission("instance-view"): + <h2>${_("Activity in my Adhocracies")}</h2> + <ul> + ${c.events_pager.here()} + </ul> + %endif +</div> \ No newline at end of file diff --git a/adhocracy/templates/instance/create.html b/adhocracy/templates/instance/create.html new file mode 100644 index 000000000..14b6e585b --- /dev/null +++ b/adhocracy/templates/instance/create.html @@ -0,0 +1,27 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("New Adhocracy")}</%def> + +<%def name="breadcrumbs()"> + ${_("New Adhocracy")} +</%def> + +<form name="create_instance" class="inplace" method="POST" action="/instance/create"> + ${h.field_token()|n} + <div id="page_title"> + <label for="label" class="armhint">${_("New Adhocracy")}</label> + <input tabindex="1" class="title armlabel" name="label" /> + </div> + + <div class="sidebar"> </div> + <div class="mainbar"> + <h3>${_("Adhocracy address:")}</h3> + <span class="hint">${_("The address may only contain alpha-numeric characters. Please note that this key cannot be changed after the Adhocracy has been created.")|n}</span> + http://<input tabindex="2" class="" style="text-align: right;" name="key" />.${request.environ['adhocracy.active.domain']} + + <h3>${_("Description:")}</h3> + <textarea tabindex="2" class="description" name="description"></textarea> + ${components.formatting()} + ${components.savebox("/adhocracies")} + </div> +</form> \ No newline at end of file diff --git a/adhocracy/templates/instance/edit.html b/adhocracy/templates/instance/edit.html new file mode 100644 index 000000000..3e1d51dfb --- /dev/null +++ b/adhocracy/templates/instance/edit.html @@ -0,0 +1,64 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Manage: %s") % c.page_instance.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.page_instance.root)|n} » ${_("Manage")} +</%def> + +<form name="create_instance" class="inplace" method="POST" + enctype="multipart/form-data" action="/instance/edit/${c.page_instance.key}"> + ${h.field_token()|n} + + <div id="page_title"> + <label for="label" class="armhint">${_("Adhocracy Name")}</label> + <input tabindex="1" class="title armlabel" name="label" /> + </div> + + <div class="sidebar"> + <h3>${_("Voting Rules")}</h3> + <label for="required_majority">${_("Majority:")}</label> + <span class="hint">${_("In order to become active, a motion must reach the given proportion of approval.")}</span> + <select name="required_majority"> + <option value="0.5">${_("A simple majority (½ of vote)")|n}</option> + <option value="0.66">${_("A two-thirds majority")}</option> + <option value="0.98">${_("In Soviet Russia, motion votes you.")}</option> + </select> + + <label for="activation_delay">${_("Delay:")}</label> + <span class="hint">${_("Before activating, the defined majority must be continuously held by the moton for the specified interval.")}</span> + <select name="activation_delay"> + <option value="0">${_("No delay")}</option> + <option value="1">${_("1 Day")}</option> + <option value="2">${_("2 Days")}</option> + <option value="7">${_("One Week")}</option> + <option value="14">${_("Two Weeks")}</option> + <option value="30">${_("Four Weeks")}</option> + </select> + + <h3>${_("Membership Options")}</h3> + <label for="default_group">${_("Default group:")}</label> + <span class="hint">${_("When a new member joins, he or she will be a member of this user group.")}</span> + <select name="default_group"> + %for possible_group in c._Group.INSTANCE_GROUPS: + <% + group = c._Group.by_code(possible_group) + %> + <option value="${group.code}">${_(group.group_name)|n}</option> + %endfor + </select> + + <h3>${_("Logo")}</h3> + <label for="logo">${_("File upload:")}</label> + <span class="hint">${_("Select a logo file to appear in the header area of this Adhocracy.")}</span> + <input name="logo" type="file" /> + + </div> + + <div class="mainbar"> + <h3>${_("Description:")}</h3> + <textarea tabindex="2" class="description" name="description">${c.page_instance.description}</textarea> + ${components.formatting()} + ${components.savebox("/instance/%s" % c.page_instance.key)} + </div> +</form> \ No newline at end of file diff --git a/adhocracy/templates/instance/index.html b/adhocracy/templates/instance/index.html new file mode 100644 index 000000000..8d13758a5 --- /dev/null +++ b/adhocracy/templates/instance/index.html @@ -0,0 +1,25 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Adhocracies")}</%def> + +<%def name="breadcrumbs()"> +</%def> + + + +<a class="button title add" href="/instance/create">${_("new")}</a> + +<h1 class="page_title">${_("Adhocracies")}</h1> + +<div class="sidebar"> + <div class="infobox"> + ${_("Adhocracies are little democracies that are ran by their community.")} + </div> +   +</div> + +<div class="mainbar"> + <div class="table"> + ${c.instances_pager.here()} + </div> +</div> \ No newline at end of file diff --git a/adhocracy/templates/instance/tiles.html b/adhocracy/templates/instance/tiles.html new file mode 100644 index 000000000..c7a99e940 --- /dev/null +++ b/adhocracy/templates/instance/tiles.html @@ -0,0 +1,41 @@ + +<%def name="list_item(tile, instance)"> + <li class="list_tile"> + <span class="text"><a href="${h.instance_url(instance)}">${instance.label}</a></span> + <span class="meta"> + (${ungettext("%s issue", "%s issues", tile.num_issues) % tile.num_issues}) + %if tile.can_join: + · <a href="/instance/join/${instance.key}?${h.url_token()}">${_("join")}</a> + %endif + %if tile.can_leave: + · <a href="/instance/leave/${instance.key}?${h.url_token()}">${_("leave")}</a> + %endif + + </span> + </li> +</%def> + +<%def name="row(tile, instance)"> + <div class="tile"> + <div class="logo"> + <img src="/img/icons/bonsai_24.png" /> + </div> + <h3><a class="link" href="${h.instance_url(instance)}">${instance.label}</a></h3> + <div class="text"> + %if len(tile.tagline): + ${tile.tagline} + %endif + </div> + <div class="meta"> + ${h.user_link(instance.creator)|n} + · ${_("created %s") % h.relative_time(instance.create_time)} + · ${ungettext("%s issue", "%s issues", tile.num_issues) % tile.num_issues} + %if tile.can_join: + · <a href="/instance/join/${instance.key}">${_("join")}</a> + %endif + %if tile.can_leave: + · <a href="/instance/leave/${instance.key}">${_("leave")}</a> + %endif + </div> + </div> +</%def> \ No newline at end of file diff --git a/adhocracy/templates/instance/view.html b/adhocracy/templates/instance/view.html new file mode 100644 index 000000000..b8c331559 --- /dev/null +++ b/adhocracy/templates/instance/view.html @@ -0,0 +1,88 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${c.page_instance.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.page_instance.root)|n} » ${_("Home")} +</%def> + +%if c.tile.can_admin: + <a class="button title admin" href="/admin/members">${_("members")}</a> + <a class="button title admin" href="/instance/edit/${c.page_instance.key}">${_("edit")}</a> +%endif +%if c.tile.can_join: + <a class="button title add" href="/instance/join/${c.page_instance.key}?${h.url_token()}">${_("join")}</a> +%endif +%if not c.user: + <a class="button title add" href="/register">${_("join")}</a> +%endif +%if c.tile.can_leave: + <a class="button title delete" href="/instance/leave/${c.page_instance.key}?${h.url_token()}">${_("leave")}</a> +%endif +%if c.tile.can_delegate: + <a class="button title add" href="/delegation/create?scope=${c.page_instance.root.id}">${_("delegate")}</a> +%endif + +<h1 class="page_title"><img src="/img/icons/bonsai_24.png" class="cd" /> ${c.page_instance.label}</h1> + +${components.tutu()} + +<div class="sidebar"> + <div class="tile"> + <div class="tile boxed"> + <div class="inner_boxed"> + <h4><img src="/img/icons/vote_16.png" class="cd"> ${_("Voting Rules")}</h4> + ${_("Required Majority:")} <b>${c.tile.required_majority}</b> + <span class="hint">${_("To become active, a motion must reach the given proportion of approval.")}</span> + + ${_("Activation Delay:")} <b>${c.tile.activation_delay}</b> + <span class="hint">${_("Before becoming active, the majority must be held for the specified interval.")}</span> + </div> + </div> + <br/> + ${components.delegation_sidebar(c.page_instance.root)} + <div class="meta"> + ${_("Subscribe to RSS feed:")} + ${h.rss_link("/instance/%s.rss" % c.page_instance.key)|n} + </div> + </div> + <br/> + %if c.tile.can_create_category: + <a class="button add" href="/category/create?cat=${c.page_instance.root.id}">${_("new")}</a> + %elif c.tile.lack_create_category_karma: + <a class="button inactive htwarn" title="${c.tile.lack_create_category_karma}">${_("new")}</a> + %endif + <h2>${_("Categories")} (${c.tile.num_categories})</h2> + %if c.tile.num_categories == 0 and c.tile.can_create_category: + <span class="hint"> + ${_("Create new categories to further structure the debate.")} + </span> + %endif + <ul> + ${c.subcats_pager.here()} + </ul> +</div> + +<div class="mainbar"> + <div class="description"> + ${c.tile.description|n} + </div> + <br /> + %if c.tile.can_create_issue: + <a class="button add" href="/issue/create?cat=${c.page_instance.root.id}">${_("new")}</a> + %elif c.tile.lack_create_issue_karma: + <a class="button inactive htwarn" title="${c.tile.lack_create_issue_karma}">${_("new")}</a> + %endif + <h2>${_("Issues")} (${c.tile.num_issues})</h2> + %if c.tile.num_issues == 0: + %if c.tile.can_create_category: + <span class="infobox"> + ${_("Create an issue to start debating in this Adhocracy.")} + </span> + %endif + %else: + <div class="table"> + ${c.issues_pager.here()} + </div> + %endif +</div> \ No newline at end of file diff --git a/adhocracy/templates/issue/create.html b/adhocracy/templates/issue/create.html new file mode 100644 index 000000000..20d03a694 --- /dev/null +++ b/adhocracy/templates/issue/create.html @@ -0,0 +1,33 @@ +<%inherit file="/template.html" /> +<%namespace name="catlist" file="/category/tree.html"/> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("New issue")}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.instance.root)|n} » ${_("New issue")} +</%def> + +<%def name="contextbox()"> + ${catlist.contextselect(type="checkbox", root=False)} +</%def> + + +<form name="create_issue" class="inplace" method="POST" action="/issue/create"> + ${h.field_token()|n} + <div class="page_title"> + <label for="label" class="armhint">${_("New issue")}</label> + <input tabindex="1" class="title armlabel" name="label" /> + </div> + + <div class="sidebar"> + ${catlist.contextselect(type="checkbox", root=False)} + </div> + + <div class="mainbar"> + <h3>${_("Issue description:")}</h3> + <textarea tabindex="2" class="description" name="text"></textarea> + ${components.formatting()} + ${components.savebox("/category/%s" % request.params.get('cat', c.instance.root.id))} + </div> + +</form> \ No newline at end of file diff --git a/adhocracy/templates/issue/edit.html b/adhocracy/templates/issue/edit.html new file mode 100644 index 000000000..4b6d6e0c2 --- /dev/null +++ b/adhocracy/templates/issue/edit.html @@ -0,0 +1,25 @@ +<%inherit file="/template.html" /> +<%namespace name="catlist" file="/category/tree.html"/> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Edit %s") % c.issue.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.issue)|n} » ${_("Edit")} +</%def> + +<form name="edit_issue" class="inplace" method="POST" action="/issue/edit/${c.issue.id}"> + ${h.field_token()|n} + <div class="page_title"> + <label for="label" class="armhint">${_("Issue Title")}</label> + <input tabindex="1" class="title armlabel" name="label" value="${c.issue.label}"/> + </div> + + <div class="sidebar"> + ${catlist.contextselect(type="checkbox", sels=c.issue.parents, root=False)} + </div> + + <div class="mainbar"> + + ${components.savebox("/issue/%s" % c.issue.id)} + </div> +</form> \ No newline at end of file diff --git a/adhocracy/templates/issue/tiles.html b/adhocracy/templates/issue/tiles.html new file mode 100644 index 000000000..0864ca71d --- /dev/null +++ b/adhocracy/templates/issue/tiles.html @@ -0,0 +1,20 @@ + +<%def name="row(tile, issue)"> + <div class="tile"> + <div class="logo"> + <img src="/img/icons/issue_24.png" /> + </div> + <h3><a class="link" href="/issue/${issue.id}">${issue.label}</a></h3> + <div class="text"> + %if len(tile.tagline): + ${tile.tagline} + %endif + </div> + <div class="meta"> + ${h.user_link(issue.creator)|n} + · ${_("created %s") % h.relative_time(issue.create_time)} + · ${ungettext("%s comment", "%s comments", (len(issue.comments)-1)) % (len(issue.comments)-1)} + · ${ungettext("%s motion", "%s motions", tile.num_motions) % tile.num_motions} + </div> + </div> +</%def> diff --git a/adhocracy/templates/issue/view.html b/adhocracy/templates/issue/view.html new file mode 100644 index 000000000..15152291c --- /dev/null +++ b/adhocracy/templates/issue/view.html @@ -0,0 +1,76 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%namespace name="comment_tiles" file="/comment/tiles.html"/> + +<%def name="title()">${c.issue.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.issue)|n} +</%def> + +%if c.tile.can_delete: + <a class="button title delete" href="/issue/delete/${c.issue.id}?${h.url_token()}">${_("delete")}</a> +%elif c.tile.lack_delete_karma: + <a class="button title inactive htwarn" title="${c.tile.lack_delete_karma}">${_("delete")}</a> +%endif + +%if c.tile.can_edit: + <a class="button title edit" href="/issue/edit/${c.issue.id}">${_("edit")}</a> +%elif c.tile.lack_edit_karma: + <a class="button title inactive htwarn" title="${c.tile.lack_edit_karma}">${_("edit")}</a> +%endif + +%if c.tile.can_delegate and not c.tile.has_overridden: + <a class="button title add" href="/delegation/create?scope=${c.issue.id}">${_("delegate")}</a> +%endif + +<h1 class="page_title"><img src="/img/icons/issue_24.png" class="cd" />${_("Issue: %s") % c.issue.label}</h1> + +<div class="sidebar"> + <div class="tile"> + <div class="text"> + <ul> + %for category in c.issue.parents: + <li> + ${_("in %s") % h.breadcrumbs(category)|n} + </li> + %endfor + </ul> + </div> + ${components.delegation_sidebar(c.issue)} + <div class="meta"> + ${_("created %s") % h.relative_time(c.issue.create_time)} + · ${h.rss_link("/issue/%s.rss" % c.issue.id)|n} + </div> + </div> +</div> +<div class="mainbar"> + %if c.issue.comment: + ${tiles.comment.full(c.issue.comment, recurse=False, collapse=False)} + %endif + + %if c.tile.can_create_motion: + <a class="button add" href="/motion/create?issue=${c.issue.id}">${_("new")}</a> + %elif c.tile.lack_create_motion_karma: + <a class="button inactive htwarn" title="${c.tile.lack_create_motion_karma}">${_("new")}</a> + %endif + <h2>${_("Motions")}<sup><a href="/page/faq.html#Howdoesvotingwork">?</a></sup> (${c.tile.num_motions})</h2> + %if not c.tile.num_motions: + <span class="infobox">${_("Motions are <b>proposals</b> that solve some or all of the problems described by this issue. A motion can be <b>discussed and voted</b> upon.")|n}</span> + %else: + <div class="table"> + ${c.motions_pager.here()} + </div> + %endif + + <h2><img src="/img/icons/discuss_20.png" class="cd" /> ${_("Discussion")}</h2> + %if c.tile.comment_tile.can_reply: + <div class="comment"> + ${comment_tiles.create_form(c.issue.comment, c.issue)} + </div> + %endif + %for reply in c.tile.comment_tile.replies: + ${tiles.comment.full(reply, recurse=True)} + %endfor +</div> + diff --git a/adhocracy/templates/motion/create.html b/adhocracy/templates/motion/create.html new file mode 100644 index 000000000..e525cc9ba --- /dev/null +++ b/adhocracy/templates/motion/create.html @@ -0,0 +1,98 @@ +<%inherit file="/template.html" /> +<%namespace name="catlist" file="/category/tree.html"/> +<%namespace name="tiles_html" file="/motion/tiles.html"/> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("New motion")}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.issue)|n} » ${_("New motion")} +</%def> + +<form name="create_motion" class="inplace" method="POST" action="/motion/create"> + ${h.field_token()|n} + <input name="issue" type="hidden" value="${c.issue.id}"/> + <div class="page_title"> + <label for="label" class="armhint">${_("New motion")}</label> + <input tabindex="1" class="title armlabel" name="label" /> + </div> + + <h3>${_("Informal description")}</h3> + <div class="sidebar"> + <div class="infobox">${_("Give a brief description of <b>what</b> your proposal will do.")|n}</div> + </div> + <div class="mainbar"> + <label for="text" class="armhint">The proposal addresses ${c.issue.label} by...</label> + <textarea tabindex="2" class="description smallarea armlabel" name="text"></textarea> + + </div> + + <h3>${_("Provisions to implement")}</h3> + <div class="sidebar"> + <div class="infobox">${_("Describe <b>how</b> your proposal will achieve the desired effect." + + "<br/><br/>" + + "Splitting the proposal into several parts will facilitate its discussion." + + "<br/><br/>" + + "You can also leave this blank if you want to specify provisions later.")|n}</div> + </div> + <div class="mainbar"> + <label for="canonicals" class="armhint">${_("A specific provision that is part of this proposal.")|n}</label> + <ul> + <li class="canonical prototype"> + <textarea class="canonicals smallarea" name="canonicals"></textarea> + </li> + %for p_text in c.canonicals: + <li class="canonical"> + <textarea class="canonicals smallarea armlabel" name="canonicals">${p_text|n}</textarea> + </li> + %endfor + </ul> + <div class="jsadd"> + <input type="button" value="${_("Add a provision")}" onclick="return appendCanonical();" /> + </div> + </div> + + <div style="clear:both;"></div> + <h3>${_("Relations to other motions")}</h3> + <div class="sidebar"> + <div class="infobox"> + ${_("Specify which motions <b>contradict</b> or <b>enable</b> this proposal." + + "<br/><br/>" + + "Contradicting motions cannot be in power at the same time." + + "<br/><br/>" + + "Requirements must be satisfied before the motion can activate.")|n} + </div> + <br/> + </div> + <div class="mainbar"> + <input type="hidden" name="rel_error" /> + <table width="100%"> + <tr> + <th width="20%">${_("Relation")}</th> + <th>${_("Motion")}</th> + </tr> + <% + options = [m for m in c.motions if not m in c.relations.keys()] + %> + %if len(options): + ${tiles_html.relation(rel_type='n', options=options, prototype=True)} + %endif + %for motion, type in c.relations.items(): + ${tiles_html.relation(rel_type=type, rel_motion=motion)} + %endfor + %if len(options): + ${tiles_html.relation(rel_type='n', options=options)} + %endif + </table> + %if len(options): + <div class="jsadd"> + <input type="button" value="${_("Add a relation")}" onclick="return appendRelation();" /> + </div> + %endif + </div> + + <div style="clear:both;"></div> + <div class="sidebar"> </div> + <div class="mainbar"> + ${components.savebox("/issue/%s" % str(c.issue.id))} + </div> +</form> \ No newline at end of file diff --git a/adhocracy/templates/motion/edit.html b/adhocracy/templates/motion/edit.html new file mode 100644 index 000000000..ff47d5055 --- /dev/null +++ b/adhocracy/templates/motion/edit.html @@ -0,0 +1,89 @@ +<%inherit file="/template.html" /> +<%namespace name="catlist" file="/category/tree.html"/> +<%namespace name="tiles_html" file="/motion/tiles.html"/> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Edit %s") % c.motion.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.motion)|n} » ${_("Edit")} +</%def> + +<form name="edit_motion" class="inplace" method="POST" action="/motion/edit/${c.motion.id}"> + ${h.field_token()|n} + <div class="page_title"> + <label for="label" class="armhint">${_("New Motion")}</label> + <input tabindex="1" class="title armlabel" name="label" value="${c.motion.label}"/> + </div> + + <h3>${_("Issue")}</h3> + <div class="sidebar"> + <div class="infobox"> + ${_("If necessary, move the motion to a different issue to keep things organized.")} + </div> + </div> + <div class="mainbar"> + <table width="100%"> + <tr> + <td width="20%" valign="top"> + <label for="issue">${_("Issue")}</label> + </td> + <td valign="bottom"> + <select name="issue"> + %for issue in c.issues: + <option value="${issue.id}" ${"selected='selected'" if issue==c.motion.issue else ''}>${issue.label}</option> + %endfor + </select> + </td> + </tr> + </table> + </div> + + <div style="clear:both;"></div> + <h3>${_("Relations to other motions")}</h3> + <div class="sidebar"> + <div class="infobox"> + ${_("Specify which motions <b>contradict</b> or <b>enable</b> this proposal." + + "<br/><br/>" + + "Contradicting motions cannot be in power at the same time." + + "<br/><br/>" + + "Requirements must be satisfied before the motion can activate.")|n} + </div> + <br/> + </div> + <div class="mainbar"> + <input type="hidden" name="rel_error" /> + <table width="100%"> + <tr> + <th width="20%">${_("Relation")}</th> + <th>${_("Motion")}</th> + </tr> + <% + options = [m for m in c.motions if not m in c.relations.keys()] + if c.motion in options: + options.remove(c.motion) + %> + %if len(options): + ${tiles_html.relation(rel_type='n', options=options, prototype=True)} + %endif + %for motion, type in c.relations.items(): + ${tiles_html.relation(rel_type=type, rel_motion=motion)} + %endfor + %if len(options): + ${tiles_html.relation(rel_type='n', options=options)} + %endif + </table> + %if len(options): + <div class="jsadd"> + <input type="button" value="${_("Add a relation")}" onclick="return appendRelation();" /> + </div> + %endif + </div> + + <div style="clear:both;"></div> + <div class="sidebar"> +   + </div> + <div class="mainbar"> + ${components.savebox("/motion/%s" % c.motion.id)} + </div> +</form> \ No newline at end of file diff --git a/adhocracy/templates/motion/index.html b/adhocracy/templates/motion/index.html new file mode 100644 index 000000000..c37812ba5 --- /dev/null +++ b/adhocracy/templates/motion/index.html @@ -0,0 +1,29 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Critical Motions in %s") % c.instance.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.instance.root)|n} » ${_("Motions")} +</%def> + +<h1 class="page_title"><img src="/img/icons/motion_24.png" class="cd" /> ${_("Critical Motions")}</h1> + +<div class="sidebar"> + %if len(c.motions_pager.items): + <span class="infobox"> + ${_("These motions are currently voting and might be nearing a decision. Vote now to make your " + + "voice heard.")} + </span> + %endif + <br /><br /> +</div> + +<div class="mainbar"> + %if len(c.motions_pager.items): + <div class="table"> + ${c.motions_pager.here()} + </div> + %else: + <div class="infobox">${_("There are currently no polls open. Call a motion into voting state to begin a poll.")}</div> + %endif +</div> \ No newline at end of file diff --git a/adhocracy/templates/motion/tiles.html b/adhocracy/templates/motion/tiles.html new file mode 100644 index 000000000..838987bc6 --- /dev/null +++ b/adhocracy/templates/motion/tiles.html @@ -0,0 +1,79 @@ + +<%def name="row(tile, motion, detail=False)"> + <div class="tile"> + <div class="logo"> + <img src="/img/icons/motion_24.png" /> + </div> + <h3><a class="link" href="/motion/${motion.id}">${motion.label}</a></h3> + <div class="text"> + %if len(tile.tagline): + ${tile.tagline} + %endif + </div> + <div class="meta"> + ${h.user_link(motion.creator)|n} + + · ${_("created %s") % h.relative_time(motion.create_time)} + %if tile.result.polling: + · ${_("%s votes cast") % len(tile.result.state.tally)} + · ${_("+%s%%/-%s%% split") % ("TODO", "TODO")} + %endif + · ${ungettext("%s comment", "%s comments", (len(motion.comments)-1)) % (len(motion.comments)-1)} + + %if detail and motion.issue: + · ${_("in")} ${h.delegateable_link(motion.issue)|n}</a> + %endif + </div> + </div> +</%def> + +<%def name="list_item(tile, motion)"> + <li class="list_tile"> + <span class="text"><a href="/motion/${motion.id}">${motion.label}</a></span> + <span class="meta"> + ${self.state_flag(tile.result.state)} · + %if tile.result.state == c.model.Motion.STATE_DRAFT: + ${_("created %s") % h.relative_time(motion.create_time)} + %else: + ${_("%s votes cast") % len(tile.poll.decisions)} + %endif + · ${ungettext("%s comment", "%s comments", len(motion.comments)) % len(motion.comments)} + </span> + </li> +</%def> + + +<%def name="relation(rel_type=None, rel_motion=None, options=None, prototype=False)"> + <tr class="relation ${'prototype' if prototype else ''}"> + <td> + <select name="rel_type"> + <option value="a" ${"selected='selected'" if rel_type=='a' else ''}> + ${_("contradicts")} + </option> + <option value="d" ${"selected='selected'" if rel_type=='d' else ''}> + ${_("requires")} + </option> + <option value="n" ${"selected='selected'" if rel_type=='n' else ''}> + ${_("no relation")} + </option> + </select> + </td> + <td> + %if options: + <select name="rel_motion"> + %if not rel_motion: + <option>${_("(Select a motion)")}</option> + %endif + %for motion in options: + <option value="${motion.id}" ${"selected='selected'" if motion==rel_motion else ''}> + ${motion.label} + </option> + %endfor + </select> + %elif rel_motion: + ${h.delegateable_link(rel_motion)|n} + <input type="hidden" name="rel_motion" value="${rel_motion.id}"> + %endif + </td> + </tr> +</%def> diff --git a/adhocracy/templates/motion/view.html b/adhocracy/templates/motion/view.html new file mode 100644 index 000000000..a48bbd3e6 --- /dev/null +++ b/adhocracy/templates/motion/view.html @@ -0,0 +1,265 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%namespace name="comment_tiles" file="/comment/tiles.html"/> +<%namespace name="state" file="/poll/state.html"/> + +<%def name="title()">${c.motion.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.motion, id=c.motion.id)|n} +</%def> + +%if c.tile.can_delete: + <a class="button title delete" href="/motion/delete/${c.motion.id}?${h.url_token()}">${_("delete")}</a> +%elif c.user and c.tile.is_immutable: + <a class="button title inactive htwarn" title="${h.immutable_motion_message()}">${_("delete")}</a> +%elif c.user and c.tile.lack_delete_karma: + <a class="button title inactive htwarn" title="${c.tile.lack_delete_karma}">${_("delete")}</a> +%endif + +%if c.tile.can_edit: + <a class="button title edit" href="/motion/edit/${c.motion.id}">${_("edit")}</a> +%elif c.user and c.tile.is_immutable: + <a class="button title inactive htwarn" title="${h.immutable_motion_message()}">${_("edit")}</a> +%elif c.user and c.tile.lack_edit_karma: + <a class="button title inactive htwarn" title="${c.tile.lack_edit_karma}">${_("edit")}</a> +%endif + +%if c.tile.can_delegate and not c.tile.has_overridden: + <a class="button title add" href="/delegation/create?scope=${c.motion.id}">${_("delegate")}</a> +%endif + +%if c.tile.can_begin_poll: + <a class="button title add" href="/poll/create/${c.motion.id}">${_("call a vote")}</a> +%elif c.user and not c.tile.has_canonicals: + <a class="button title inactive htwarn" title="${_('The motion cannot be voted upon since it does not have any provisions yet.')}">${_("call a vote")}</a> +%elif c.user and c.tile.lack_begin_poll_karma and not c.tile.poll: + <a class="button title inactive htwarn" title="${c.tile.lack_begin_poll_karma}">${_("call a vote")}</a> +%endif + + +<h1 class="page_title">${c.motion.label}</h1> + +<div class="sidebar"> + + <!-- div class="tile" --> + <!-- h4>${_("Coming into effect")}</h4 --> + ${state.checklist(c.tile.result)|n} + <!-- /div --> + + <div class="tile"> + <div class="text"> + <p> + <img src="/img/icons/issue_16.png" /> <a href="/issue/${c.motion.issue.id}">${c.motion.issue.label}</a>: + ${c.issue_tile.tagline} + </p> + </div> + ${components.delegation_sidebar(c.motion)} + <div class="meta"> + ${_("created %s") % h.relative_time(c.motion.create_time)} + · ${h.rss_link("/motion/%s.rss" % c.motion.id)|n} + </div> + </div> + + <h3>${_("Alternatives")}</h3> + + <span class="hint">${_("Alternatives are contradicting motions. Only one of the listed alternatives can be active at any time.")}</span> + <ul> + %for alternative in c.motion.left_alternatives: + %if not alternative.delete_time: + <li>${h.delegateable_link(alternative.right)|n}</li> + %endif + %endfor + %for alternative in c.motion.right_alternatives: + %if not alternative.delete_time: + <li>${h.delegateable_link(alternative.left)|n}</li> + %endif + %endfor + </ul> + + %if len(c.motion.dependencies) or len(c.motion.dependents): + <h3>${_("Dependencies")}</h3> + %endif + + %if len(c.motion.dependencies): + <span class="hint">${_("In order for this motion to become active, these required motions will also have to be active.")}</span> + <ul> + %for dependency in c.motion.dependencies: + <li>${h.delegateable_link(dependency.requirement)|n}</li> + %endfor + </ul> + %endif + + %if len(c.motion.dependents): + <span class="hint">${_("The following motions depend on this proposal in order to become active.")}</span> + <ul> + %for dependency in c.motion.dependents: + <li>${h.delegateable_link(dependency.motion)|n}</li> + %endfor + </ul> + %endif + +</div> + +<div class="mainbar"> + + %if c.motion.comment: + ${tiles.comment.full(c.motion.comment, recurse=False, collapse=False)} + %endif + %if c.tile.poll: + %if c.tile.can_end_poll: + <a class="button delete" href="/poll/abort/${c.motion.id}">${_("cancel")}</a> + %elif c.user and c.tile.is_immutable: + <a class="button inactive htwarn" title="${_('This vote has recieved the required majority and cannot be cancelled')}">${_("cancel")}</a> + %elif c.user and c.tile.lack_end_poll_karma: + <a class="button inactive htwarn" title="${c.tile.lack_end_poll_karma}">${_("cancel")}</a> + %endif + <h2><img src="/img/icons/vote_24.png" class="cd" /> ${_("Vote")}<sup><a href="/page/faq.html#Howdoesvotingwork">?</a></sup></h2> + <div class="tile voting_booth"> + <!-- div class="text"--> + + %if c.tile.can_vote: + <form action="/vote/cast/${c.motion.id}" method="GET"> + ${h.field_token()|n} + %endif + + <table border="0" width="100%"> + <tr> + <th></th> + <th></th> + <th>${_("Option")}</th> + <th> + %if c.tile.can_vote: + ${_("Delegate Recommendations")} + %endif + </th> + <th class="votes">${_("Votes")}</th> + <th>${_("Percent")}</th> + </tr> + <tr class="affirm ${'decision' if c.user and c.tile.decision.result == 1 else ''}"> + <td> + %if c.tile.can_vote: + <input type="radio" name="orientation" value="1" + ${'checked' if c.tile.decision.result == 1 else ''|n} /> + %endif + </td> + <td class="iconcol"> + <span class="icon"> </span> + </td> + <td class="option">${_("Affirm")}</td> + <td> + <ul> + %for agent in c.tile.delegates_result(1): + <li>${h.user_link(agent)|n}</li> + %endfor + </ul> + </td> + <td class="votes"><a href="/motion/${c.motion.id}/votes?result=1">${c.tile.result.tally.num_affirm}</a></td> + <td valign="top">${c.tile.result_affirm}%</td> + </tr> + <tr class="dissent ${'decision' if c.user and c.tile.decision.result == -1 else ''}"> + <td class="selector"> + %if c.tile.can_vote: + <input type="radio" name="orientation" value="-1" + ${'checked' if c.tile.decision.result == -1 else ''|n} /> + %endif + </td> + <td class="iconcol"> + <span class="icon"> </span> + </td> + <td class="option">${_("Dissent")}</td> + <td> + <ul> + %for agent in c.tile.delegates_result(-1): + <li>${h.user_link(agent)|n}</li> + %endfor + </ul> + </td> + <td class="votes"><a href="/motion/${c.motion.id}/votes?result=-1">${c.tile.result.tally.num_dissent}</a></td> + <td valign="top">${c.tile.result_dissent}%</td> + </tr> + <tr class="abstain ${'decision' if c.user and c.tile.decision.result == 0 else ''}"> + <td class="selector"> + %if c.tile.can_vote: + <input type="radio" name="orientation" value="0" + ${'checked' if c.tile.decision.result == 0 else ''|n} /> + %endif + </td> + <td class="iconcol"> + <span class="icon"> </span> + </td> + <td class="option">${_("Abstain")}</td> + <td> + <ul> + %for agent in c.tile.delegates_result(0): + <li>${h.user_link(agent)|n}</li> + %endfor + </ul> + </td> + <td class="votes"><a href="/motion/${c.motion.id}/votes?result=0">${c.tile.result.tally.num_abstain}</a></td> + <td valign="top"> </td> + </tr> + <tr class="summary"> + <td colspan="3" rowspan="2"> + %if c.tile.can_vote: + <input type="submit" value="${_("vote")}" /> + %endif + </td> + <td> + <div class="hint" style="padding: 0; text-align: right"> + ${_("Of the required %s votes, the motion has:") % "TODO"} + </div> + </td> + <td colspan="2"> + <a href="/motion/${c.motion.id}/votes"> + ${_("%d votes") % len(c.tile.result.tally)} + </a> + </td> + </tr> + <tr class="summary"><td colspan="2"></td></tr> + </table> + + %if c.tile.can_vote: + </form> + %endif + + <!-- /div --> + <div class="meta"> + ${_("poll started %s") % h.relative_time(c.tile.poll.begin_time)} + · <a href="/page/faq.html#Howdoesvotingwork">${_("help")}</a> + </div> + </div> + %endif + + + %if c.tile.can_create_canonical: + <a class="button add" onclick="return add_canonical()" href="/comment/create?topic=${c.motion.id}&canonical=1">${_("new")}</a> + %elif c.user and c.tile.is_immutable: + <a class="button inactive htwarn" title="${h.immutable_motion_message()}">${_("new")}</a> + %elif c.user and c.tile.lack_create_canonical_karma: + <a class="button inactive htwarn" title="${c.tile.lack_create_canonical_karma}">${_("new")}</a> + %endif + + <h2><img src="/img/icons/motion_24.png" class="cd" /> ${_("Provisions")}<sup><a href="/page/faq.html#Howdoesvotingwork">?</a></sup></h2> + %if not c.tile.has_canonicals: + <span class="infobox">${_("<b>Provisions</b> are the body of a motion: together they form the language that will be voted upon. You will need to have at least one clause in order to call for a vote.")|n}</span> + %endif + + <div class="add_canonical comment"> + ${comment_tiles.create_form(None, c.motion, canonical=1)} + </div> + + %for canonical in c.tile.canonicals: + ${tiles.comment.full(canonical, recurse=False, collapse=False, link_discussion=True)} + %endfor + + <h2><img src="/img/icons/discuss_20.png" class="cd" /> ${_("Discussion")}</h2> + %if c.tile.comment_tile.can_reply: + <div class="comment"> + ${comment_tiles.create_form(c.motion.comment, c.motion)} + </div> + %endif + %for reply in c.tile.comment_tile.replies: + ${tiles.comment.full(reply, recurse=True)} + %endfor +</div> \ No newline at end of file diff --git a/adhocracy/templates/motion/votes.html b/adhocracy/templates/motion/votes.html new file mode 100644 index 000000000..48ad2a2f5 --- /dev/null +++ b/adhocracy/templates/motion/votes.html @@ -0,0 +1,25 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%namespace name="comment_tiles" file="/comment/tiles.html"/> + +<%def name="title()">${_("Votes: %s") % c.motion.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.motion, id=c.motion.id)|n} » ${_("Votes")} +</%def> + + +<h1 class="page_title"><img src="/img/icons/vote_24.png" class="cd" /> ${_("Votes:")} + <a href="/motion/${c.motion.id}">${c.motion.label}</a></h1> + +<div class="sidebar"> +   +</div> + +<div class="mainbar"> + %if c.motion.poll: + <div class="table"> + ${c.decisions_pager.here()} + </div> + %endif +</div> \ No newline at end of file diff --git a/adhocracy/templates/pager.html b/adhocracy/templates/pager.html new file mode 100644 index 000000000..cc6de9e01 --- /dev/null +++ b/adhocracy/templates/pager.html @@ -0,0 +1,49 @@ + +<%def name="namedpager(pager)"> + + %if len(pager.sorts.keys()) > 1: + <span class="pager_sorts"> + ${_("sort by")} | + %for sort, i in zip(pager.sorts.keys(), xrange(1, len(pager.sorts.keys()) + 1)): + <a class="${'selected' if i == pager.selected_sort else ''}" + href="${pager.serialize(sort=i)}">${sort}</a> + %if i < len(pager.sorts.keys()): + | + %endif + %endfor + </span> + %endif + + <div style="clear: both;"> + + %for item in pager.items: + ${pager.itemfunc(item)} + %endfor + </div> + <div class="pager"> + <div class="prev_page"> + %if pager.rel_page(step=-1): + <a href="${pager.serialize(page=pager.rel_page(step=-1))}">« ${_("previous")}</a> + %endif + </div> + %for i in (-3, -2, -1): + %if pager.rel_page(step=i): + <a href="${pager.serialize(page=pager.rel_page(step=i))}">${pager.rel_page(step=i)}</a> + %endif + %endfor + %if pager.pages() > 1: + ${pager.page} + %endif + %for i in (1, 2, 3): + %if pager.rel_page(step=i): + <a href="${pager.serialize(page=pager.rel_page(step=i))}">${pager.rel_page(step=i)}</a> + %endif + %endfor + <div class="next_page"> + %if pager.rel_page(step=1): + <a href="${pager.serialize(page=pager.rel_page(step=1))}">${_("next")} »</a> + %endif + </div> + </div> + +</%def> \ No newline at end of file diff --git a/adhocracy/templates/poll/abort.html b/adhocracy/templates/poll/abort.html new file mode 100644 index 000000000..cc682cb0c --- /dev/null +++ b/adhocracy/templates/poll/abort.html @@ -0,0 +1,23 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${c.motion.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.motion)|n} » ${_("End a vote")} +</%def> + +<h1 class="page_title">${_("Cancel vote on: %s") % c.motion.label}</h1> + +<form name="release_motion" class="inplace" method="POST" action="/poll/abort/${c.motion.id}"> + ${h.field_token()|n} + <div class="sidebar"> </div> + <div class="mainbar"> + <div class="warning_box"> + <p> + ${_("You are about to re-draft this motion. This means that the motion will become editable again " + + "but that all votes that have been cast at this time <strong>will be invalidated</strong>.")|n} + </p> + </div> + <input type="submit" value="${_("Confirm and Proceed")}" /> + </div> +</form> \ No newline at end of file diff --git a/adhocracy/templates/poll/create.html b/adhocracy/templates/poll/create.html new file mode 100644 index 000000000..0afa5b31f --- /dev/null +++ b/adhocracy/templates/poll/create.html @@ -0,0 +1,23 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${c.motion.label}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.motion)|n} » ${_("Call a vote")} +</%def> + +<h1 class="page_title">${_("Call a vote on: %s") % c.motion.label}</h1> + +<form name="release_motion" class="inplace" method="POST" action="/poll/create/${c.motion.id}"> + ${h.field_token()|n} + <div class="sidebar"> </div> + <div class="mainbar"> + <div class="warning_box"> + <p> + ${_("You are about to release this motion and call for a vote. When you do this, you will lose " + + "the ability to <strong>change the motion's wording</strong> unless you re-draft it.")|n} + </p> + </div> + <input type="submit" value="${_("Confirm and Proceed")}" /> + </div> +</form> \ No newline at end of file diff --git a/adhocracy/templates/poll/state.html b/adhocracy/templates/poll/state.html new file mode 100644 index 000000000..eeaf80be7 --- /dev/null +++ b/adhocracy/templates/poll/state.html @@ -0,0 +1,14 @@ + +<%def name="checklist(result)"> + <ul class="checklist"> + %if not result.polling: + <li class="unmet">${_("The motions isn't polling.")}</li> + %else: + + <li class="met">I'm a banana at 5% of the vote.</li> + <li class="unmet">My spoon is too big. I live in a giant bucket.</li> + + + %endif + </ul> +</%def> \ No newline at end of file diff --git a/adhocracy/templates/search/results.html b/adhocracy/templates/search/results.html new file mode 100644 index 000000000..ff6f057c9 --- /dev/null +++ b/adhocracy/templates/search/results.html @@ -0,0 +1,33 @@ +<%inherit file="/template.html" /> +<%def name="title()">${_("Search") if not c.query else "'%s'" % c.query}</%def> + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.instance.root if c.instance else None)|n} » ${_("Search")} +</%def> + +<h1 class="page_title"> + %if len(c.query): + ${_("Search for '%s'") % c.query} + %else: + ${_("Search")} + %endif +</h1> + +<div class="sidebar"> +   +</div> + +<div class="mainbar"> + %if c.query: + %if c.entities: + <div class="table"> + ${c.entities_pager.here()|n} + </div> + %else: + <p> + ${_("No entries could be found that match your criteria. Try a more general search term.")} + </p> + <br/> + %endif + %endif +</div> diff --git a/adhocracy/templates/sitemap.xml b/adhocracy/templates/sitemap.xml new file mode 100644 index 000000000..6a3329434 --- /dev/null +++ b/adhocracy/templates/sitemap.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> +%for d in c.delegateables: + <url> + <% + controller = "d" + if c.instance: + if type(d) == c.model.Motion: + controller = "motion" + elif type(d) == c.model.Category: + controller = "category" + elif type(d) == c.model.Issue: + controller = "issue" + + %> + <loc>${h.instance_url(c.instance, path="/%s/%s" % (controller, d.id))}</loc> + <lastmod>${d.access_time.strftime("%Y-%m-%d")}</lastmod> + <changefreq>weekly</changefreq> + <priority>0.8</priority> + </url> +%endfor +</urlset> \ No newline at end of file diff --git a/adhocracy/templates/static/about.html b/adhocracy/templates/static/about.html new file mode 100644 index 000000000..a67d865d6 --- /dev/null +++ b/adhocracy/templates/static/about.html @@ -0,0 +1,37 @@ +<html> +<head> + <title>About Adhocracy + + +

+ While democracy as a form of governance is often credited with + creating very complex and well discussed decisions, it is not + generally considered to be terribly efficient. Decision-making + is sluggish at best and once a decision is made, improving the + decisions is hard and often frowned upon. +

+

+ This is especially true for direct democracies, where decisions + aren't made by representatives but by the people + themselves. +

+

+ Adhocracy uses social technologies to change the rules for + democracy: it makes democratic decisions as easy as editing a wiki, + as fast as twitter and as flexible as the net itself. +

+ Graph Democracy, Yay! +

+ This means that democracy becomes a feasible mode of decision-making + for groups that couldn't have it before. Anyone can open an Adhocracy + in just a few moments. +

+

+ There, members can propose a problem solution or a piece of + regulation for the group in just a few instants and get the whole group + to discuss, develop and approve their idea. This adds direct + legitimation to all decisions, giving them both internal and external + credence. +

+ + \ No newline at end of file diff --git a/adhocracy/templates/static/faq.html b/adhocracy/templates/static/faq.html new file mode 100644 index 000000000..554ae7a44 --- /dev/null +++ b/adhocracy/templates/static/faq.html @@ -0,0 +1,297 @@ + + +Frequently Asked Questions + + +

What is Adhocracy?

+

+ A weird piece of code that does a half-baked version of Liquid Democracy +

+

What is Liquid Democracy?

+

+ A buzzword created + in 2003. While many use the term Liquid Democracy to describe internet-based + Delegated Voting (a voting mechanism), Adhocracy goes beyond that. The essence of + Liquid Democracy is the tight coupling of a networked voting system to an open system for + the discussion and development of policy alternatives. +

+

+ While traditional online discussion systems are a great way to +generate new ideas, they frequently fail to prioritize and filter them. +This leads to situations in which the most outrageous and absurd +proposals receive the most detailed discussion, while more realistic +options are drowned in the noise. A convenient method to filter such +proposals is to allow users to vote on them. This way, the community can +learn whether a proposal lacks essential support among its members, +whether the proposal needs some more work or whether it is generally +accepted and can become part of the group's policies or even of the +political agenda of that organization. +

+

+ Involving many users in an online poll can be hard, since the +degree to which individual users engage in an online discussion is +highly varied. Yet any meaningful poll must reflect not only the loud +and ever-present voices, but also those who cannot and do not want to +spare as much time on a specific debate as others. Liquid Democracy +allows these users to make a simpler choice instead: they can name an agent whose position in +one or many debates they want to support. This agent will be given the +right to vote on their principal's behalf. Yet this delegation, unlike a +proper voting proxy, is a soft connection which can be reviewed, +overridden or entirely cancelled at any moment. In a way, delegated +voting is a recommendation mechanism that works like the song +recommendations a service like last.fm produces. +

+

+ In Liquid Democracy, opinions are no longer just a private matter but instead become the +subject of a dynamic network based around both topics and opinion +leaders. This reflects the structure of a networked public implied in +tools such as Facebook and Twitter. +

+

+ An extreme use case of this, of course, would be to do actual +lawmaking. Adhocracy is not made for this, and personally I don't think +its a very smart idea. Lawmaking should be based on a fairly +conservative, safe process. If you want to do it, go ahead but don't +blame the tool if it fails. +

+ +

How does voting work?

+ +

Adhocracy allows users to vote on proposals - so-called motions - +to determine the degree of support they have within the community. In +order to bring an idea to be voted upon, the following steps are needed: +

+
    +
  1. A motion is created within an appropriate issue. It will + have both a formal and an informal description.
  2. +
  3. The formal descriptions, so-called provisions, are modular. + While a motion needs only a single provision to be voted upon, it can + have an arbitrary number of them. +
      +
    • In order to be considered for a vote, any provision must + have been up-voted at least once, and have a positive score. Any + provision that was down-voted will be discarded for the length of the + poll.
    • +
    +
  4. +
  5. A motion needs to be explicitly called into the voting state + to allow for ballots to be cast. +
      +
    • The vote can be called by the motion's creator, an + administrator or any user who has met the "call for a vote" karma + threshold. Future versions of Adhocracy might require multiple users + to agree for a vote begin.
    • +
    • A vote can be called at any time. There is no pre-defined + schedule that mandates voting in specific intervals or at defined + dates.
    • +
    • Polls can last for any number of days or weeks and need not + be stopped at any time. A successful vote (reaching the 'active' + state) will last for as long as the motion is in effect. This + principle could be called Continuous Legitimization.
    • +
    • While voting, the provisions that have been included in the + poll cannot be modified. This is required to allow decisions to be + made on a fixed proposal.
    • +
    • While voting, the motion can adopt various states: it is + "voting" when it has not yet reached the required majority; + "activating" when it has reached its majority but not yet held it for + the required delay or "active" when all conditions are met.
    • +
    • While voting, neither a motion nor the issue to which it is + associated can be modified. The vote needs to be cancelled first.
    • +
    +
  6. +
  7. A vote can be cancelled at any time unless it has acquired the + required majority. When that happens, a motion becomes entirely + immutable and cannot be modified without first losing its support among + the voters.
  8. +
+ +

What are motion states?

+

Motions have a life cycle that leads them from being drafts to +community-approved documents. Which state a motion is in depends on +whether it has been released and on the number of votes it has +for/against it.

+
    +
  • draft: A draft motion is not yet finished and can still be + edited by any member of the Adhocracy.
  • +
  • voting: After a vote was called, a motion is "voting"; any + user can cast a vote on the motion.
  • +
  • activating: After gathering approval by the required majority + of the participants (usually two thirds of the voters), a motion is + activating and must maintain that majority for a certain delay (usually + 7 days) in order to become active.
  • +
  • active: The motion is approved by the community and can be + seen as part of their established codex.
  • +
  • deactivating: When a motion loses the required majority, it is + re-transferred to the voting state. Similar to the activating state + this transition is delayed. Deactivating motions are, therefore, still + an active part of an accepted consensus.
  • +
+

+ +

What is delegated voting?

+

Delegation helps voters to make their voice heard in a decision +even if they cannot or do not want to explicitly vote on certain +proposals. A delegation is an authorization of one user (the principal) +to allow another user (the agent) to vote on his or her behalf. Each +delegation is valid within a certain scope, e.g. with regards to a +political area like climate policy. This means that the agent can vote +on the principal's behalf in all decisions that are marked to be inside +the given scope. The delegations of a voter can thus be seen as his or +her personal cabinet. In this cabinet, one agent is responsible for +making decisions regarding the subject area A, while another agent is +responsible for making decisions in a second subject area, B.

+

The special properties of Adhocracy's delegation system include: +

+
    +
  • Unanimity: When a voter has delegated voting on a certain + proposal to two or more agents, his or her vote will only be counted if + both agents make the same decision. If the agents disagree, no vote + will be cast by their principal unless he or she does so manually. This + can be used to intentionally limit the power of an agent in a specific + domain by requiring a second delegate to concur.
  • +
  • Transitivity: If a delegation is created in which the agent + has delegated their voting powers themselves, the principals vote will + be available to the agents agent. In other words: If A delegates to B + and B delegates to C, then C can vote on A's behalf. This can be used + to quickly reproduce the delegation decisions another voter has made: A + voter can just delegate to the other voter, instead of reproducing all + individual delegations.
  • +
+ +

Who should one delegate +voting to?

+

The Adhocracy platform provides a lot of information that can be +used to make delegation decisions:

+
    +
  • Voting records are the most objective source of documented + prior behavior and can help to identify a users' positions.
  • +
  • Comments within a discussion can give more specific insight on + a user's approach and position regarding a specific topic.
  • +
  • Aggregate Karma scores with regards to an issue or category + can help to identify regular contributors.
  • +
  • Off-channel communication, such as blogs and Twitter can also + be a valuable source for identifying the opinion leaders which most + closely represent one's opinions.
  • +
+

One of the major goals of the next version of Adhocracy will be to introduce more +intelligent automated comparisons among the users to generate reliable +recommendations.

+

Are there secret votes in +Adhocracy?

+

No. A full public voting record is available for all users, at +any time. There are three reasons to do this:

+
    +
  • At its core, Adhocracy is a discussion tool. Having the + participants state their opinions on an issue is a part of this + discussion and should thus happen in public.
  • +
  • Without voting records, delegation becomes useless. If the + voting record of delegation agents wasn't published, one would have to + trust the agents to honestly identify their intentions regarding each + vote. This would create a critical trust limitation against delegation. + While one could imagine a system in which only those who wish to + receive delegations would publish their voting records, such a system + would create unequal conditions for voting between public and private + voters, thus violating another important principle of any democratic + voting system.
  • +
  • Keeping voting records a secret is hard. Trust in the system + would be predicated on the assumption that no leaks - including those + resulting from indirect attacks - occurred. This assumption is bound to + be compromised at some stage, creating a system with dubious security + and privacy. Publishing all voting records is thus a step avoid + creating misconceptions among users regarding privacy. The voters + themselves can then decide whether a particular topic is fit for public + voting and otherwise go off-channel, using paper ballots.
  • +
+ +

Which voting system does +Adhocracy use?

+

Adhocracy currently only supports voting on individual motions, +not the comparative polling on different alternatives regarding an +issue. Starting with the next version of Adhocracy, the software will use a fairly complex variant of approval voting. In this +process, users will be able to specify which motions depend upon each +other and which motions contradict each other. Motions will then become +active only if they meet all their dependencies and have the highest +level of approvement among their respective cluster of contradicting +motions.

+

While this process is somewhat complicated internally, the voter +will only have to make a binary decision on whether he thinks a motion +is acceptable or not. Tactical voting is not needed in most approval +voting systems.

+

Unlike instant runoff systems, approval voting also doesn't +require users to priorize their choices, a process that generates more +information than strictly necessary and that leads to various +inconsistencies such as Arrow's paradox.

+ +

Can Adhocracy be used to +hold elections?

+

While it is technically possible to elect members of the +community into an office using Adhocracy, this is highly discouraged. +Arguing for or against the capabilities and personality of a candidate +is a process that is fundamentally different from arguing a position on +an issue. A secret vote is, therefore, an essential element of any +election. Adhocracy cannot offer any degree of secrecy. Additionally, +the delegation system will produce odd incentives when used in an +election setting.

+ +

Is Adhocracy secure?

+

For a long time, smart people have tried to build voting machines +that no attacker can tamper with. Concepts for this usually integrate +strong cryptographic mechanisms that allow the voting authority to +verify the identity of the voter. Unfortunately, most of these proposals +are extremely complicated and require the user to install special +software on their PC, follow non-trivial processes and to manage +cryptographic keys. Yet, even these tools do not guarantee perfect +security: shit just happens.

+

Adhocracy values access and usability over ultimate security. +Since the goal is to structure a debate and not to command an army, the +focus, instead, is on creating tools that will make it more obvious when +shit starts happening. The core principle of this, an idea called Soft Security, +demands that Adhocracy should keep you informed about your past actions +and the way in which polling results come about.

+ +

What is Karma?

+

Karma is a way for a community to structure its debates: +Contributions are rated by their readers. Good contributions score +higher than boring or redundant ones, and are thus shown above the +others. This helps the group to keep focussed.

+

When people aggregate Karma points (i.e. upvotes on their +contributions), they gain the ability to perform certain tasks that are +usually restricted. Thus all those actions are potentially available to +everyone, but have to be 'earned' by advancing the debate. These tasks +include:

+
    +
  • Scoring on (5 points) and editing (30 points) other people's + comments
  • +
  • Editing arbitrary motions or issues (200 points each)
  • +
  • Beginning and cancelling votes on a proposal. (300 points + each)
  • +
  • Deleting comments or motions (500 points)
  • +
  • Creating and editing categories (600 points)
  • +
  • Deleting categories or issues (1000 points)
  • +
  • Voting is, of course, excepted from the karma system
  • +
+

This allows for users who have proven their interest in a rich +debate to gradually become moderators of that debate. It also keeps +trolls or extremists from overly influencing the debate.

+ +

Why can others edit my +comments?

+

To avoid redundancy. In most internet forums, when a good +argument has been made, there is usually a stream of additions and +examples provided by other users that end up spamming the thread. Having +the ability to edit the original argument allows you to decide whether +to add your extension to the original comment or to create a new one.

+

This way, the new comment is no longer a static step of the +conversation but can become a living and growing argument that is +supported by its own community of editors.

+ +

You stole ideas +from StackOverflow/Reddit/Digg/HN!

+

True. These tools, I think, are the pinnacle of what online +debate can look like at the moment. StackOverflow, +unfortunately, is based on the assumption that there is a right answer +to any question. This, of course, is not true in a political setting. So +I used some SO ideas (including the graduate membership thing and, I'll +admit, the layout) and repackaged them.

+ + \ No newline at end of file diff --git a/adhocracy/templates/static/formatting.html b/adhocracy/templates/static/formatting.html new file mode 100644 index 000000000..99d676ae9 --- /dev/null +++ b/adhocracy/templates/static/formatting.html @@ -0,0 +1,54 @@ + + + Formatting Help + + +

+ Adhocracy uses a fairly simple text formatting system known as Markdown. Markdown was + invented to allow you to easily apply simple formatting options to your text without the need + to know the HTML language. Here are some of the essential options that Markdown provides: +

+
    +
  • + Italic text: put single asterisks around your text + to produce italic text:
    *this is italic*
    +
  • +
  • + Bold text: double asterisks around your text will + generate a bold passage:
    **I'm a bold snippet**
    +
  • +
  • + Lists are produced by starting each line with an asterisk or a number. Asterisks + will produce bullet points, while numbers will generate a numbered list: +
    * This is a list
    +* Which has bullet items
    +
    +1. While this list
    +2. Is numbered
    + Take care to put a single space in front of each list item. If you want to create several levels + of lists, increase the number of spaces before each level of the list. +
  • +
  • + Hyperlinks: to link to a page, use the following formatting: +
    [Link title](http://your.link.address.com)
    +
  • +
  • + Images can be included in a very similar fashion: +
    ![Image description](http://image.host.com/picture.png)
    +
  • +
  • Headings: structure your text into sections by using headings. They are produced + by underlining your text with equals signs or dashes: +
    This is a large heading
    +=======================
    +
    +This is a smaller heading
    +-------------------------
    + +
  • +
+

+ For a more complete reference of the Markdown options, check out + the full documentation. +

+ + \ No newline at end of file diff --git a/adhocracy/templates/static/imprint.html b/adhocracy/templates/static/imprint.html new file mode 100644 index 000000000..fb9b82ed1 --- /dev/null +++ b/adhocracy/templates/static/imprint.html @@ -0,0 +1,50 @@ + + + Imprint + + + +

Contact

+

+ Web: development site + Mail: support@adhocracy.cc
+ Twitter: @adhocracy_cc +

+ +

Hyperlinks

+

+ This website contains links to third party web pages, which are not under + control of Adhocracy. Therefore we are not responsible for any linked site. + Upon the time of linking, we have carefully examined the external pages and + could not find any illegitimate contents. +

+ +

Content

+

+ Adhocracy does not assume liability nor warranty for the correctness, + completeness, quality or topicality of the provided information. Of course + we examine the provided information regularly and update these in case of + innovation or change. We reserve the right to accomplish additions or + revisions of the available information at any time. +

+ +

ViSdP

+

Adhocracy is a non-profit site operated by:

+ +

+ Friedrich Lindenberg
+ Rotenweg 8
+ 79199 Kirchzarten
+ Twitter: @pudo +

+ +

Licensing

+ + Creative Commons License +

+ Adhocracy content is licensed under a Creative Commons Attribution-Share Alike 3.0 Germany License. + + The Adhocracy codebase is licensed unter a liberal BSD-style license and can be retrieved at the development site. +

+ + \ No newline at end of file diff --git a/adhocracy/templates/static/privacy.html b/adhocracy/templates/static/privacy.html new file mode 100644 index 000000000..4d038d04e --- /dev/null +++ b/adhocracy/templates/static/privacy.html @@ -0,0 +1,8 @@ + + + Privacy Policy + + + TODO + + \ No newline at end of file diff --git a/adhocracy/templates/template.html b/adhocracy/templates/template.html new file mode 100644 index 000000000..632f638e6 --- /dev/null +++ b/adhocracy/templates/template.html @@ -0,0 +1,181 @@ +<%namespace name="model" module="adhocracy.model"/> +<%namespace name="components" file="/components.html"/> + +<%def name="title()">${_("No Title")} +<%def name="page_title()">

${self.title()}

+<%def name="breadcrumbs()">  +<%def name="contextbox()">  + +<%def name="page_actions()"> +<%def name="page_actions_bar()"> +
+ ${self.page_actions()} +
+ + +<%def name="print_meta()"> + %for key, value in c.html_meta.items(): + + %endfor + + +<%def name="print_link()"> + %for link in c.html_link: + + %endfor + + + + + + + ${self.title()} - + %if c.instance: + ${c.instance.label} + %endif + ${_("Adhocracy")} + + + + + ${print_link()} + + + ${print_meta()} + + + + + + + +
+
+ %if not c.user: + ${_("sign in")} + %else: + + ${c.user.name}${h.user_karma(c.user)} + · ${_("settings")} + · ${_("logout")} + %endif + %if c.instance: +
+ + + + + %endif + %if c.user and c.instance and not c.user.is_member(c.instance): +
${_("Join %s to contribute") % c.instance.label} + %endif +
+
+ + + + + <%def name="page()"> +
+ <% messages = h.flash.pop_messages() %> + %if messages: +
+
    + %for message in messages: +
  • ${message}
  • + %endfor +
+
+ %endif + + +
+ ${self.body()} +
+
+ + + <%def name="tpl_page()"> + ${self.page()} + + + ${self.tpl_page()} + + +
+%if config.get('adhocracy.analytics') == 'true': + + +%endif + + \ No newline at end of file diff --git a/adhocracy/templates/template_doc.html b/adhocracy/templates/template_doc.html new file mode 100644 index 000000000..84153debb --- /dev/null +++ b/adhocracy/templates/template_doc.html @@ -0,0 +1,11 @@ +<%inherit file="/template.html" /> + +<%def name="title()">${c.page_title} + +

${c.page_title}

+ +
+ ${c.page_text|n} +
\ No newline at end of file diff --git a/adhocracy/templates/user/delegations.html b/adhocracy/templates/user/delegations.html new file mode 100644 index 000000000..d201abe12 --- /dev/null +++ b/adhocracy/templates/user/delegations.html @@ -0,0 +1,59 @@ +<%inherit file="/template.html" /> +<%def name="title()"> + %if c.user == c.page_user: + ${_("My Delegations")} + %else: + ${_("Delegations: %s") % c.page_user.name} + %endif + + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.instance.root)|n} » ${c.page_user.name} » ${_("Delegations")} + + +

+ + %if c.user == c.page_user: + ${_("My Delegations")} + %else: + ${_("Delegations: %s") % c.page_user.name} + %endif +

+ + + + + + + + %for delegateable in [c.category] + c.category.search_children(recurse=True): + %if h.contains_delegations(c.page_user, delegateable, recurse=False): + + + + + + %endif + %endfor +
${_("Topic")}${_("Given")}${_("Received")}
+ ${h.delegateable_link(delegateable)|n} + + %if len(delegateable.parents): + ${h.breadcrumbs(delegateable.parents[0])|n} + %endif +   + + +
    + %for delegation in filter(lambda d: d.scope == delegateable and not d.revoke_time, c.page_user.delegated): +
  • ${tiles.delegation.outbound(delegation)}
  • + %endfor +
+
+
    + %for delegation in filter(lambda d: d.scope == delegateable and not d.revoke_time, c.page_user.agencies): +
  • ${tiles.delegation.inbound(delegation)}
  • + %endfor +
+
+

diff --git a/adhocracy/templates/user/edit.html b/adhocracy/templates/user/edit.html new file mode 100644 index 000000000..f3b5ddaeb --- /dev/null +++ b/adhocracy/templates/user/edit.html @@ -0,0 +1,57 @@ +<%inherit file="/template.html" /> +<%namespace name="parts" file="/user/parts.html"/> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Settings: %s") % c.page_user.name} + +<%def name="breadcrumbs()"> + ${parts.user_breadcrumbs(c.page_user)} » ${_("Edit")} + + + +
+ ${h.field_token()|n} +
+ + +
+ + + +
+

${_("Short biography")}

+ + ${components.formatting()} + ${_("A bio will allow others to learn about you and perhaps even get you a few delegations.")} + ${components.savebox("/user/%s" % c.page_user.user_name)} +
+
\ No newline at end of file diff --git a/adhocracy/templates/user/index.html b/adhocracy/templates/user/index.html new file mode 100644 index 000000000..5639ced7a --- /dev/null +++ b/adhocracy/templates/user/index.html @@ -0,0 +1,20 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%def name="title()">${_("Users in %s") % c.instance.label if c.instance else _("Users")} + +<%def name="breadcrumbs()"> + ${h.breadcrumbs(c.instance.root if c.instance else None)|n} » ${_("Users")} + + +

${_("Users in %s") % c.instance.label if c.instance else _("Users")} (${len(c.users)})

+ + + +
+
+ ${c.users_pager.here()} +
+
\ No newline at end of file diff --git a/adhocracy/templates/user/login.html b/adhocracy/templates/user/login.html new file mode 100644 index 000000000..155280f1f --- /dev/null +++ b/adhocracy/templates/user/login.html @@ -0,0 +1,18 @@ +<%inherit file="/template.html" /> +<%def name="title()">${_("Who are you, then?")} + +
+

${_("Login")}

+ ${_("If you already have an account, sign in here.")} + <%include file="/user/login_form.html"/> +
+
+ ${_("If you have an account but you've lost your password, click here.")|n} +
+
+ +
+

${_("Register")}

+ ${_("Creating an account is easy; all you need is a user name, password and email address.")} + <%include file="/user/register_form.html"/> +
\ No newline at end of file diff --git a/adhocracy/templates/user/login_form.html b/adhocracy/templates/user/login_form.html new file mode 100644 index 000000000..7b6bf6635 --- /dev/null +++ b/adhocracy/templates/user/login_form.html @@ -0,0 +1,9 @@ +
+ + + + + + + +
\ No newline at end of file diff --git a/adhocracy/templates/user/parts.html b/adhocracy/templates/user/parts.html new file mode 100644 index 000000000..9b4fa1b17 --- /dev/null +++ b/adhocracy/templates/user/parts.html @@ -0,0 +1,8 @@ +<%def name="user_breadcrumbs(user)"> + %if c.instance: + ${c.instance.label} + %else: + ${_("Adhocracy")} + %endif + » ${user.name} + diff --git a/adhocracy/templates/user/register_form.html b/adhocracy/templates/user/register_form.html new file mode 100644 index 000000000..3768f85c3 --- /dev/null +++ b/adhocracy/templates/user/register_form.html @@ -0,0 +1,20 @@ + + +
+ ${h.field_token()|n} + + ${_("Can only contain letters and numbers.")} + + + + ${_("We don't spam.")} + + + + + + + + + +
\ No newline at end of file diff --git a/adhocracy/templates/user/reset_form.html b/adhocracy/templates/user/reset_form.html new file mode 100644 index 000000000..a9168b9ad --- /dev/null +++ b/adhocracy/templates/user/reset_form.html @@ -0,0 +1,20 @@ +<%inherit file="/template.html" /> +<%def name="title()">${_("Password reset")} + +

${_("Password reset")}

+ +
+ +
+ ${_("In order to retrieve your login, you will have to enter your email adress.")} +

+ + + +
+ +
+
\ No newline at end of file diff --git a/adhocracy/templates/user/reset_pending.html b/adhocracy/templates/user/reset_pending.html new file mode 100644 index 000000000..c63028376 --- /dev/null +++ b/adhocracy/templates/user/reset_pending.html @@ -0,0 +1,13 @@ +<%inherit file="/template.html" /> +<%def name="title()">${_("Password reset")} + +

${_("Password reset")}: ${_("Confirmation pending")}

+ + +
+
+ ${_("You have recieved an email containing a link. Please open that link in order to reset you password")} +
+
diff --git a/adhocracy/templates/user/tiles.html b/adhocracy/templates/user/tiles.html new file mode 100644 index 000000000..0987f9ba8 --- /dev/null +++ b/adhocracy/templates/user/tiles.html @@ -0,0 +1,22 @@ + +<%def name="row(tile, user)"> +
+ +
+

${user.name}

+ %if tile.tagline: +
+ ${tile.tagline} +
+ %endif +
+ %if c.instance: + ${_("%s karma") % tile.karma} · + %endif + ${_("signed up %s") % h.relative_time(user.create_time)} +
+
+
+ \ No newline at end of file diff --git a/adhocracy/templates/user/view.html b/adhocracy/templates/user/view.html new file mode 100644 index 000000000..3edca722e --- /dev/null +++ b/adhocracy/templates/user/view.html @@ -0,0 +1,61 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%namespace name="parts" file="/user/parts.html"/> +<%def name="title()">${c.page_user.name} + +<%def name="breadcrumbs()"> + ${parts.user_breadcrumbs(c.page_user)} + + +%if c.tile.can_edit: + ${_("edit")} +%endif + +

+ + ${self.title()}${c.tile.karma} +

+ +
+
+ %if c.tile.bio: + ${c.tile.bio|n} + %else: + ${_("%s does not have a bio") % c.page_user.name} + %endif +
+
+ %if c.instance: + ${c.tile.num_karma_up} · + ${c.tile.num_karma_down} · + + ${ungettext("%s issue", "%s issues", c.tile.num_issues) % c.tile.num_issues} · + ${ungettext("%s motion", "%s motions", c.tile.num_motions) % c.tile.num_motions} · + ${ungettext("%s comment", "%s comments", c.tile.num_comments) % c.tile.num_comments} · + + ${_("delegations")} · + %endif + ${_("signed up %s") % h.relative_time(c.page_user.create_time)} +
+
+ + + +
+

${_("Activity")}

+ +
    + ${c.events_pager.here()} +
+
\ No newline at end of file diff --git a/adhocracy/templates/user/votes.html b/adhocracy/templates/user/votes.html new file mode 100644 index 000000000..f889d7304 --- /dev/null +++ b/adhocracy/templates/user/votes.html @@ -0,0 +1,26 @@ +<%inherit file="/template.html" /> +<%namespace name="components" file="/components.html"/> +<%namespace name="parts" file="/user/parts.html"/> + +<%def name="title()">${_("Votes: %s") % c.page_user.name} + +<%def name="breadcrumbs()"> + ${parts.user_breadcrumbs(c.page_user)} » ${_("Votes")} + + +%if c.user and c.user == c.page_user: +

${_("Review your voting track")}

+%else: +

${_("Votes:")} + ${c.page_user.name}

+%endif + + + +
+
+ ${c.decisions_pager.here()} +
+
\ No newline at end of file diff --git a/adhocracy/tests/__init__.py b/adhocracy/tests/__init__.py new file mode 100644 index 000000000..9de44bb71 --- /dev/null +++ b/adhocracy/tests/__init__.py @@ -0,0 +1,38 @@ +"""Pylons application test package + +This package assumes the Pylons environment is already loaded, such as +when this script is imported from the `nosetests --with-pylons=test.ini` +command. + +This module initializes the application via ``websetup`` (`paster +setup-app`) and provides the base testing objects. +""" +from unittest import TestCase + +from paste.deploy import loadapp +from paste.script.appinstall import SetupCommand +from pylons import config, url +from routes.util import URLGenerator +from webtest import TestApp + +import pylons.test + +from testtools import * + +__all__ = ['environ', 'url', 'TestController'] + +# Invoke websetup with the current config file +SetupCommand('setup-app').run([config['__file__']]) + +environ = {} + +class TestController(TestCase): + + def __init__(self, *args, **kwargs): + if pylons.test.pylonsapp: + wsgiapp = pylons.test.pylonsapp + else: + wsgiapp = loadapp('config:%s' % config['__file__']) + self.app = TestApp(wsgiapp) + url._push_object(URLGenerator(config['routes.map'], environ)) + TestCase.__init__(self, *args, **kwargs) diff --git a/adhocracy/tests/functional/__init__.py b/adhocracy/tests/functional/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adhocracy/tests/functional/test_account.py b/adhocracy/tests/functional/test_account.py new file mode 100644 index 000000000..8fa667451 --- /dev/null +++ b/adhocracy/tests/functional/test_account.py @@ -0,0 +1,4 @@ +from adhocracy.tests import * + +class TestAccountController(TestController): + pass diff --git a/adhocracy/tests/functional/test_admin.py b/adhocracy/tests/functional/test_admin.py new file mode 100644 index 000000000..91e978c42 --- /dev/null +++ b/adhocracy/tests/functional/test_admin.py @@ -0,0 +1,8 @@ +from adhocracy.tests import * + +class TestAdminController(TestController): + + def test_index(self): + pass + #response = self.app.get(url(controller='admin', action='index')) + # Test response... diff --git a/adhocracy/tests/functional/test_category.py b/adhocracy/tests/functional/test_category.py new file mode 100644 index 000000000..b03173e2c --- /dev/null +++ b/adhocracy/tests/functional/test_category.py @@ -0,0 +1,4 @@ +from adhocracy.tests import * + +class TestCategoryController(TestController): + pass diff --git a/adhocracy/tests/functional/test_comment.py b/adhocracy/tests/functional/test_comment.py new file mode 100644 index 000000000..0a9f94970 --- /dev/null +++ b/adhocracy/tests/functional/test_comment.py @@ -0,0 +1,8 @@ +from adhocracy.tests import * + +class TestCommentController(TestController): + + def test_index(self): + pass + #response = self.app.get(url(controller='comment', action='index')) + # Test response... diff --git a/adhocracy/tests/functional/test_delegation.py b/adhocracy/tests/functional/test_delegation.py new file mode 100644 index 000000000..770a1506f --- /dev/null +++ b/adhocracy/tests/functional/test_delegation.py @@ -0,0 +1,4 @@ +from adhocracy.tests import * + +class TestDelegationController(TestController): + pass diff --git a/adhocracy/tests/functional/test_editor.py b/adhocracy/tests/functional/test_editor.py new file mode 100644 index 000000000..c92dcd6e1 --- /dev/null +++ b/adhocracy/tests/functional/test_editor.py @@ -0,0 +1,8 @@ +from adhocracy.tests import * + +class TestEditorController(TestController): + + def test_index(self): + pass + #response = self.app.get(url(controller='editor', action='index')) + # Test response... diff --git a/adhocracy/tests/functional/test_event.py b/adhocracy/tests/functional/test_event.py new file mode 100644 index 000000000..2b32fd212 --- /dev/null +++ b/adhocracy/tests/functional/test_event.py @@ -0,0 +1,8 @@ +from adhocracy.tests import * + +class TestEventController(TestController): + + def test_index(self): + pass + #response = self.app.get(url(controller='event', action='index')) + # Test response... diff --git a/adhocracy/tests/functional/test_instance.py b/adhocracy/tests/functional/test_instance.py new file mode 100644 index 000000000..6addf85e1 --- /dev/null +++ b/adhocracy/tests/functional/test_instance.py @@ -0,0 +1,8 @@ +from adhocracy.tests import * + +class TestInstanceController(TestController): + + def test_index(self): + pass + #response = self.app.get(url(controller='instance', action='index')) + # Test response... diff --git a/adhocracy/tests/functional/test_issue.py b/adhocracy/tests/functional/test_issue.py new file mode 100644 index 000000000..25e0c7868 --- /dev/null +++ b/adhocracy/tests/functional/test_issue.py @@ -0,0 +1,8 @@ +from adhocracy.tests import * + +class TestIssueController(TestController): + + def test_index(self): + #response = self.app.get(url(controller='issue', action='index')) + # Test response... + pass diff --git a/adhocracy/tests/functional/test_karma.py b/adhocracy/tests/functional/test_karma.py new file mode 100644 index 000000000..0f36b8d33 --- /dev/null +++ b/adhocracy/tests/functional/test_karma.py @@ -0,0 +1,8 @@ +from adhocracy.tests import * + +class TestKarmaController(TestController): + + def test_index(self): + #response = self.app.get(url(controller='karma', action='index')) + # Test response... + pass diff --git a/adhocracy/tests/functional/test_motion.py b/adhocracy/tests/functional/test_motion.py new file mode 100644 index 000000000..3c5a3274e --- /dev/null +++ b/adhocracy/tests/functional/test_motion.py @@ -0,0 +1,4 @@ +from adhocracy.tests import * + +class TestMotionController(TestController): + pass diff --git a/adhocracy/tests/functional/test_poll.py b/adhocracy/tests/functional/test_poll.py new file mode 100644 index 000000000..457e21960 --- /dev/null +++ b/adhocracy/tests/functional/test_poll.py @@ -0,0 +1,7 @@ +from adhocracy.tests import * + +class TestPollController(TestController): + + def test_index(self): + response = self.app.get(url(controller='poll', action='index')) + # Test response... diff --git a/adhocracy/tests/functional/test_root.py b/adhocracy/tests/functional/test_root.py new file mode 100644 index 000000000..4a989348a --- /dev/null +++ b/adhocracy/tests/functional/test_root.py @@ -0,0 +1,4 @@ +from adhocracy.tests import * + +class TestRootController(TestController): + pass diff --git a/adhocracy/tests/functional/test_search.py b/adhocracy/tests/functional/test_search.py new file mode 100644 index 000000000..ddaeec015 --- /dev/null +++ b/adhocracy/tests/functional/test_search.py @@ -0,0 +1,8 @@ +from adhocracy.tests import * + +class TestSearchController(TestController): + + def test_index(self): + pass + #response = self.app.get(url(controller='search', action='index')) + # Test response... diff --git a/adhocracy/tests/functional/test_vote.py b/adhocracy/tests/functional/test_vote.py new file mode 100644 index 000000000..e810e2cfd --- /dev/null +++ b/adhocracy/tests/functional/test_vote.py @@ -0,0 +1,4 @@ +from adhocracy.tests import * + +class TestVoteController(TestController): + pass diff --git a/adhocracy/tests/lib/__init__.py b/adhocracy/tests/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adhocracy/tests/lib/democracy/__init__.py b/adhocracy/tests/lib/democracy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adhocracy/tests/lib/democracy/test_decision.py b/adhocracy/tests/lib/democracy/test_decision.py new file mode 100644 index 000000000..e8789784b --- /dev/null +++ b/adhocracy/tests/lib/democracy/test_decision.py @@ -0,0 +1,121 @@ +from datetime import datetime +import time + +from adhocracy.tests import * +from adhocracy.tests.testtools import * +from nose.tools import * + +import adhocracy.lib.democracy as decision +from adhocracy.lib.democracy import Decision, Poll, DelegationNode +import adhocracy.model as model + +class TestDecision(TestController): + + def test_direct_vcount(self): + motion = tt_make_motion(voting=True) + dec = Decision(motion.creator, motion) + assert len(dec.votes) == 0 + assert len(dec.relevant_votes) == 0 + time.sleep(1) + dec.make(model.Vote.AYE) + #dec = Decision(motion.creator, motion) + assert len(dec.votes) == 1 + assert len(dec.relevant_votes) == 1 + dec.make(model.Vote.AYE) + assert len(dec.votes) == 2 + assert len(dec.relevant_votes) == 1 + dec2 = Decision(motion.creator, motion) + assert len(dec2.votes) == 2 + assert len(dec2.relevant_votes) == 1 + + + def test_made(self): + motion = tt_make_motion(voting=True) + time.sleep(1) + dec = Decision(motion.creator, motion) + assert not dec.made() + assert not dec.self_made() + dec.make(model.Vote.AYE) + time.sleep(1) + assert dec.made() + assert dec.self_made() + + def test_delegation(self): + motion = tt_make_motion(voting=True) + #time.sleep(1) + instance = tt_get_instance() + user1 = tt_make_user() + user2 = tt_make_user() + user3 = tt_make_user() + + d1to2 = model.Delegation(user1, user2, instance.root) + model.meta.Session.add(d1to2) + model.meta.Session.commit() + print d1to2 + Decision(user2, motion).make(model.Vote.AYE) + time.sleep(1) + dec = Decision(user1, motion) + assert dec.result == model.Vote.AYE + assert len(dec.relevant_votes) == 1 + + # lower scope + d1to3 = model.Delegation(user1, user3, motion) + model.meta.Session.add(d1to3) + model.meta.Session.commit() + time.sleep(1) + Decision(user3, motion).make(model.Vote.NAY) + time.sleep(1) + assert len(dec.relevant_votes) == 1 + assert dec.result == model.Vote.NAY + + # equal scope, same opinion + d1to2_2 = model.Delegation(user1, user2, motion) + model.meta.Session.add(d1to2_2) + model.meta.Session.commit() + #print d1to2_2 + Decision(user2, motion).make(model.Vote.NAY) + time.sleep(1) + dec = Decision(user1, motion) + assert dec.result == model.Vote.NAY + assert len(dec.relevant_votes) == 2 + + # equal scope, different opinion + #time.sleep(1) + #Decision(user2, motion).make(model.Vote.AYE) + #time.sleep(1) + #dec = Decision(user1, motion) + #time.sleep(1) + #assert len(dec.relevant_votes) == 2 + #assert not dec.result + + # self override + Decision(user1, motion).make(model.Vote.ABSTAIN) + time.sleep(1) + dec = Decision(user1, motion) + assert dec.result == model.Vote.ABSTAIN + assert len(dec.relevant_votes) == 1 + + def test_delegation_override(self): + motion = tt_make_motion(voting=True) + time.sleep(1) + instance = tt_get_instance() + user1 = tt_make_user() + user2 = tt_make_user() + + wide = model.Delegation(user1, user2, instance.root) + model.meta.Session.add(wide) + model.meta.Session.commit() + time.sleep(1) + Decision(user2, motion).make(model.Vote.AYE) + time.sleep(1) + small = model.Delegation(user1, user2, motion) + model.meta.Session.add(small) + model.meta.Session.commit() + time.sleep(1) + Decision(user2, motion).make(model.Vote.NAY) + time.sleep(1) + dec = Decision(user1, motion) + + assert len(dec.relevant_votes) == 1 + assert dec.result == model.Vote.NAY + \ No newline at end of file diff --git a/adhocracy/tests/lib/democracy/test_delegation_node.py b/adhocracy/tests/lib/democracy/test_delegation_node.py new file mode 100644 index 000000000..d1f7684d5 --- /dev/null +++ b/adhocracy/tests/lib/democracy/test_delegation_node.py @@ -0,0 +1,118 @@ +from datetime import datetime +import time + +from adhocracy.tests import * +from adhocracy.tests.testtools import * +from nose.tools import * + +import adhocracy.lib.democracy as poll +from adhocracy.lib.democracy import Poll, Decision, DelegationNode +import adhocracy.model as model + +class TestDelegationNode(TestController): + + def test_queries(self): + motion = tt_make_motion(voting=True) + time.sleep(1) + instance = tt_get_instance() + user1 = tt_make_user() + user2 = tt_make_user() + user3 = tt_make_user() + + d1to2 = model.Delegation(user1, user2, instance.root) + model.meta.Session.add(d1to2) + model.meta.Session.commit() + + dn = DelegationNode(user1, instance.root) + assert len(dn.outbound()) == 1 + + dn = DelegationNode(user1, motion) + assert len(dn.outbound()) == 1 + + dn = DelegationNode(user2, instance.root) + assert len(dn.inbound()) == 1 + + dn = DelegationNode(user2, motion) + assert len(dn.inbound()) == 1 + + d3to2 = model.Delegation(user3, user2, motion) + model.meta.Session.add(d3to2) + model.meta.Session.commit() + + dn = DelegationNode(user2, instance.root) + assert len(dn.inbound()) == 1# + + dn = DelegationNode(user2, motion) + assert len(dn.inbound()) == 2 + + dn = DelegationNode(user2, motion) + assert len(dn.inbound(recurse=False)) == 1 + + def test_propagate(self): + motion = tt_make_motion(voting=True) + time.sleep(1) + user1 = tt_make_user() + user2 = tt_make_user() + user3 = tt_make_user() + user4 = tt_make_user() + userA = tt_make_user() + + d2to1 = model.Delegation(user2, user1, motion) + model.meta.Session.add(d2to1) + + dAto1 = model.Delegation(userA, user1, motion) + model.meta.Session.add(dAto1) + + d3to2 = model.Delegation(user3, user2, motion) + model.meta.Session.add(d3to2) + + d4to3 = model.Delegation(user4, user3, motion) + model.meta.Session.add(d4to3) + model.meta.Session.commit() + + dn = DelegationNode(user1, motion) + assert len(dn.inbound()) == 2 + + def inp(user, deleg, edge): + return "foo" + assert len(dn.propagate(inp)) == 5 + + def test_detach(self): + motion = tt_make_motion(voting=True) + time.sleep(1) + user1 = tt_make_user() + user2 = tt_make_user() + user3 = tt_make_user() + + d2to1 = model.Delegation(user2, user1, motion) + model.meta.Session.add(d2to1) + + d3to1 = model.Delegation(user3, user1, motion) + model.meta.Session.add(d3to1) + model.meta.Session.commit() + + dn = DelegationNode(user1, motion) + assert len(dn.inbound()) == 2 + + DelegationNode.detach(user1, tt_get_instance()) + + dn = DelegationNode(user1, motion) + assert len(dn.inbound()) == 0 + + def test_filter(self): + motion = tt_make_motion(voting=True) + instance = tt_get_instance() + user1 = tt_make_user() + user2 = tt_make_user() + user3 = tt_make_user() + + small = model.Delegation(user1, user2, motion) + model.meta.Session.add(small) + + large = model.Delegation(user1, user3, instance.root) + model.meta.Session.add(large) + model.meta.Session.commit() + + res = DelegationNode.filter_delegations([small, large]) + assert small in res + assert large not in res \ No newline at end of file diff --git a/adhocracy/tests/lib/democracy/test_poll.py b/adhocracy/tests/lib/democracy/test_poll.py new file mode 100644 index 000000000..030e48899 --- /dev/null +++ b/adhocracy/tests/lib/democracy/test_poll.py @@ -0,0 +1,100 @@ +from datetime import datetime +import time + +from adhocracy.tests import * +from adhocracy.tests.testtools import * +from nose.tools import * + +import adhocracy.lib.democracy as poll +from adhocracy.lib.democracy import Poll, Decision +import adhocracy.model as model + +class TestPoll(TestController): + + def test_nopoll(self): + motion = tt_make_motion(voting=False) + assert len(list(Poll.for_motion(motion))) == 0 + assert_raises(poll.NoPollException, Poll, motion) + + def test_begin_end(self): + motion = tt_make_motion(voting=False) + p = Poll.begin(motion, motion.creator) + time.sleep(1) + assert not p.end_transition + assert p.end(motion.creator) + assert p.end_transition + time.sleep(1) + assert_raises(poll.NoPollException, Poll, motion) + + def test_poll(self): + motion = tt_make_motion(voting=True) + poll = Poll(motion) + assert poll + assert poll.begin_transition + assert not poll.end_transition + assert not len(list(poll.votes)) + assert not len(poll.voters) + assert not len(poll.decisions) + time.sleep(1) + Decision(motion.creator, motion).make(model.Vote.AYE) + time.sleep(1) + assert len(motion.votes) == 1 + poll = Poll(motion) + assert len(poll.votes) == 1 + assert len(poll.voters) == 1 + assert len(poll.decisions) == 1 + + def test_decisions(self): + motion = tt_make_motion(voting=True) + Decision(motion.creator, motion).make(model.Vote.AYE) + poll = Poll(motion) + + def test_vote_discard(self): + motion = tt_make_motion(voting=True) + time.sleep(1) + Decision(motion.creator, motion).make(model.Vote.AYE) + Decision(tt_make_user(), motion).make(model.Vote.AYE) + Decision(tt_make_user(), motion).make(model.Vote.AYE) + Decision(tt_make_user(), motion).make(model.Vote.AYE) + time.sleep(2) + p = Poll(motion) + assert len(p.votes) == 4 + p.end(motion.creator) + time.sleep(1) + p2 = Poll.begin(motion, motion.creator) + assert len(p2.votes) == 0 + time.sleep(1) + Decision(motion.creator, motion).make(model.Vote.AYE) + time.sleep(1) + p3 = Poll(motion) + assert len(p3.votes) == 1 + assert len(list(Poll.for_motion(motion))) == 2 + + def test_stats(self): + motion = tt_make_motion(voting=True) + time.sleep(1) + Decision(motion.creator, motion).make(model.Vote.AYE) + time.sleep(1) + p = Poll(motion) + assert len(p.voters) == 1 + assert p.num_affirm == 1 + assert p.rel_for == 1.0 + Decision(tt_make_user(), motion).make(model.Vote.NAY) + time.sleep(1) + p = Poll(motion) + assert len(p.voters) == 2 + assert p.num_dissent == 1 + assert p.rel_for == 1.0/2.0 + Decision(tt_make_user(), motion).make(model.Vote.ABSTAIN) + time.sleep(1) + p = Poll(motion) + assert len(p.voters) == 3 + assert p.num_abstain == 1 + + def test_average(self): + # how to test this?? + avg = Poll.average_decisions(tt_get_instance()) + print "AVERAGE ", avg + #raise ValueError + + diff --git a/adhocracy/tests/lib/democracy/test_result.py b/adhocracy/tests/lib/democracy/test_result.py new file mode 100644 index 000000000..196b047e9 --- /dev/null +++ b/adhocracy/tests/lib/democracy/test_result.py @@ -0,0 +1,36 @@ +from datetime import datetime +import time + +from adhocracy.tests import * +from adhocracy.tests.testtools import * +from nose.tools import * + +from adhocracy.lib.democracy import Poll, Decision +from adhocracy.lib.democracy.result import BaseResult +import adhocracy.model as model + +class TestBaseResult(TestController): + + def test_state(self): + motion = tt_make_motion(voting=True) + time.sleep(1) + Decision(motion.creator, motion).make(model.Vote.AYE) + Decision(tt_make_user(), motion).make(model.Vote.AYE) + Decision(tt_make_user(), motion).make(model.Vote.AYE) + Decision(tt_make_user(), motion).make(model.Vote.NAY) + time.sleep(1) + p = Poll(motion) + r = BaseResult(p) + + assert r.state == model.Motion.STATE_ACTIVE + + Decision(tt_make_user(), motion).make(model.Vote.NAY) + Decision(tt_make_user(), motion).make(model.Vote.NAY) + Decision(tt_make_user(), motion).make(model.Vote.NAY) + time.sleep(1) + p2 = Poll(motion) + r2 = BaseResult(p2) + + assert r2.state == model.Motion.STATE_VOTING + + \ No newline at end of file diff --git a/adhocracy/tests/lib/test_event.py b/adhocracy/tests/lib/test_event.py new file mode 100644 index 000000000..e5d425acb --- /dev/null +++ b/adhocracy/tests/lib/test_event.py @@ -0,0 +1,71 @@ +from adhocracy.tests import * +from adhocracy.tests.testtools import * +from datetime import datetime +import adhocracy.lib.event as e +import adhocracy.model as model + +class TestEvent(TestController): + + def test_emit(self): + admin = tt_get_admin() + evt = e.emit(e.T_TEST, {'test': "test"}, admin) + assert evt.exists() + evt.delete() + + def test_all_persistence(self): + topic = tt_make_str() + admin = tt_get_admin() + + evt = e.Event(e.T_TEST, {'test': "test"}, admin) + assert evt.agent != None + assert evt.time != None + assert evt.topics != None + assert evt.scopes != None + assert not evt.exists() + + evt.persist() + assert evt.exists() + + evt.delete() + assert not evt.exists() + + def test_hash(self): + admin = tt_get_admin() + tm = datetime.now() + + evt1 = e.Event(e.T_TEST, {'test': "test"}, admin, time=tm) + evt2 = e.Event(e.T_TEST, {'test': "test"}, admin, time=tm) + assert evt1 == evt1 + assert evt1.time == evt2.time + assert evt1 == evt2 + assert hash(evt1) == hash(evt2) + evt1.persist() + assert evt2.exists() + + def test_query(self): + topic1 = tt_make_str() + topic2 = tt_make_str() + admin = tt_get_admin() + #print "TOPIC ", topic + e.emit(e.T_TEST, {'test': 'test'}, admin, + topics=[topic1], scopes=[topic1]) + e.emit(e.T_TEST, {'test': 'test'}, admin, + topics=[topic2]) + e.emit(e.T_TEST, {'test': 'test'}, admin, + topics=[topic1, topic2]) + + r = e.q.run(e.q.topic(topic1)) + assert len(r) == 2 + r = e.q.run(e.q.topic(topic2)) + assert len(r) == 2 + r = e.q.run(e.q._or(e.q.topic(topic1), e.q.topic(topic2))) + assert len(r) == 3 + r = e.q.run(e.q._and(e.q.topic(topic1), e.q.topic(topic2))) + assert len(r) == 1 + r = e.q.run(e.q._and(e.q.topic(topic1), e.q._not(e.q.topic(topic2)))) + assert len(r) == 1 + r = e.q.run(e.q.scope(topic1)) + assert len(r) == 1 + r = e.q.run(e.q._and(e.q.topic(topic1), e.q.agent(admin))) + assert len(r) == 2 + \ No newline at end of file diff --git a/adhocracy/tests/model/__init__.py b/adhocracy/tests/model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/adhocracy/tests/model/test_user.py b/adhocracy/tests/model/test_user.py new file mode 100644 index 000000000..3b62715f0 --- /dev/null +++ b/adhocracy/tests/model/test_user.py @@ -0,0 +1,10 @@ + +from adhocracy.tests import * +import adhocracy.model.meta as meta +import adhocracy.model as mdl + +class TestUserController(TestController): + + def test_init(self): + # And that, ladies and gents, is all the testing we have here. + pass \ No newline at end of file diff --git a/adhocracy/tests/testtools.py b/adhocracy/tests/testtools.py new file mode 100644 index 000000000..689dfacb8 --- /dev/null +++ b/adhocracy/tests/testtools.py @@ -0,0 +1,55 @@ +from datetime import datetime, timedelta +import random, string, time +import adhocracy.model as model + +def tt_get_admin(): + return model.User.find("admin") + +def tt_get_instance(): + instance = model.Instance.find("test") + model.filter.setup_thread(instance) + return instance + +def tt_make_str(length=20): + return u''.join([random.choice(string.letters) for i in range(length)]) + +def tt_make_motion(creator=None, voting=False): + instance = tt_get_instance() + if not creator: + creator = tt_make_user() + + cat = instance.root.search_children(cls=model.Category)[0] + motion = model.Motion(instance, tt_make_str(), creator) + motion.parents = [cat] + model.meta.Session.add(motion) + model.meta.Session.commit() + model.meta.Session.refresh(motion) + + if voting: + tr = model.Transition(motion, model.Motion.STATE_VOTING, creator) + motion.transitions.append(tr) + model.meta.Session.add(motion) + model.meta.Session.commit() + return motion + +def tt_make_user(group=None): + user = None + while True: + uname = tt_make_str() + if not model.User.find(uname): + user = model.User(uname, u"test@test.test", u"test") + break + defgrp = model.Group.by_code(model.Group.CODE_DEFAULT) + defmembr = model.Membership(user, None, defgrp) + + instance = tt_get_instance() + if not group: + group = model.Group.by_code(model.Group.CODE_VOTER) + grpmembr = model.Membership(user, instance, group) + model.meta.Session.add(user) + user.memberships += [defmembr, grpmembr] + model.meta.Session.add(defmembr) + model.meta.Session.add(grpmembr) + model.meta.Session.commit() + model.meta.Session.refresh(user) + return user diff --git a/adhocracy/websetup.py b/adhocracy/websetup.py new file mode 100644 index 000000000..f9ac6a8d1 --- /dev/null +++ b/adhocracy/websetup.py @@ -0,0 +1,26 @@ +"""Setup the adhocracy application""" +import logging + +from adhocracy.config.environment import load_environment +from adhocracy import model +from adhocracy.lib import install, search +from adhocracy.model import meta +from pylons import config + +log = logging.getLogger(__name__) + +def setup_app(command, conf, vars): + """Place any commands to setup adhocracy here""" + load_environment(conf.global_conf, conf.local_conf) + + if config.get('adhocracy.setup.drop', "OH_NOES") == "KILL_EM_ALL": + log.warn("DELETING DATABASE AND SEARCH/EVENT INDEX") + meta.metadata.drop_all(bind=meta.engine) + import shutil + shutil.rmtree(search.index_dir()) + search.setup_search() + + # Create the tables if they don't already exist + meta.metadata.create_all(bind=meta.engine) + + install.setup_entities() \ No newline at end of file diff --git a/adhocracy_mate.tmproj b/adhocracy_mate.tmproj new file mode 100644 index 000000000..4eafd238b --- /dev/null +++ b/adhocracy_mate.tmproj @@ -0,0 +1,780 @@ + + + + + currentDocument + adhocracy/lib/auth.py + documents + + + expanded + + name + adhocracy + regexFolderFilter + !.*/(\.[^/]*|CVS|_darcs|_MTN|\{arch\}|blib|.*~\.nib|.*\.(framework|app|pbproj|pbxproj|xcode(proj)?|bundle))$ + sourceDirectory + + + + fileHierarchyDrawerWidth + 232 + metaData + + adhocracy/config/routing.py + + caret + + column + 34 + line + 33 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/controllers/category.py + + caret + + column + 36 + line + 25 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 7 + selectFrom + + column + 8 + line + 25 + + selectTo + + column + 36 + line + 25 + + + adhocracy/controllers/delegation.py + + caret + + column + 49 + line + 45 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 27 + selectFrom + + column + 4 + line + 45 + + selectTo + + column + 49 + line + 45 + + + adhocracy/controllers/motion.py + + caret + + column + 0 + line + 11 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 0 + selectFrom + + column + 60 + line + 11 + + selectTo + + column + 0 + line + 11 + + + adhocracy/controllers/root.py + + caret + + column + 20 + line + 20 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/controllers/vote.py + + caret + + column + 9 + line + 25 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/lib/auth.py + + caret + + column + 5 + line + 56 + + firstVisibleColumn + 0 + firstVisibleLine + 11 + + adhocracy/lib/base.py + + caret + + column + 12 + line + 22 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/lib/helpers.py + + caret + + column + 23 + line + 38 + + firstVisibleColumn + 0 + firstVisibleLine + 5 + + adhocracy/model/__init__.py + + caret + + column + 0 + line + 19 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/model/delegateable.py + + caret + + column + 0 + line + 66 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 26 + selectFrom + + column + 31 + line + 72 + + selectTo + + column + 0 + line + 66 + + + adhocracy/model/delegation.py + + caret + + column + 54 + line + 63 + + firstVisibleColumn + 0 + firstVisibleLine + 2 + + adhocracy/model/forms.py + + caret + + column + 18 + line + 40 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/model/group.py + + caret + + column + 70 + line + 44 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/model/motion.py + + caret + + column + 36 + line + 93 + + firstVisibleColumn + 0 + firstVisibleLine + 9 + + adhocracy/model/permission.py + + caret + + column + 0 + line + 29 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/model/user.py + + caret + + column + 21 + line + 133 + + firstVisibleColumn + 0 + firstVisibleLine + 136 + + adhocracy/model/vote.py + + caret + + column + 50 + line + 44 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/templates/category/create.html + + caret + + column + 0 + line + 17 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 0 + selectFrom + + column + 7 + line + 20 + + selectTo + + column + 0 + line + 17 + + + adhocracy/templates/category/edit.html + + caret + + column + 0 + line + 0 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 0 + selectFrom + + column + 7 + line + 43 + + selectTo + + column + 0 + line + 0 + + + adhocracy/templates/category/tree.html + + caret + + column + 39 + line + 17 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/templates/category/view.html + + caret + + column + 29 + line + 19 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/templates/components.html + + caret + + column + 39 + line + 76 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 46 + selectFrom + + column + 5 + line + 76 + + selectTo + + column + 39 + line + 76 + + + adhocracy/templates/delegation/create.html + + caret + + column + 31 + line + 44 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/templates/delegation/graph.dot + + caret + + column + 10 + line + 0 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/templates/delegation/review.html + + caret + + column + 53 + line + 16 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 0 + selectFrom + + column + 17 + line + 16 + + selectTo + + column + 53 + line + 16 + + + adhocracy/templates/index.html + + caret + + column + 2 + line + 18 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/templates/motion/browse.html + + caret + + column + 50 + line + 73 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 25 + selectFrom + + column + 18 + line + 73 + + selectTo + + column + 50 + line + 73 + + + adhocracy/templates/motion/create.html + + caret + + column + 26 + line + 17 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 0 + selectFrom + + column + 0 + line + 16 + + selectTo + + column + 26 + line + 17 + + + adhocracy/templates/motion/edit.html + + caret + + column + 0 + line + 31 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 0 + selectFrom + + column + 7 + line + 41 + + selectTo + + column + 0 + line + 31 + + + adhocracy/templates/motion/view.html + + caret + + column + 14 + line + 34 + + columnSelection + + firstVisibleColumn + 0 + firstVisibleLine + 2 + selectFrom + + column + 0 + line + 0 + + selectTo + + column + 4 + line + 76 + + + adhocracy/templates/static/faq.html + + caret + + column + 0 + line + 0 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/templates/static/imprint.html + + caret + + column + 19 + line + 5 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + adhocracy/templates/template.html + + caret + + column + 46 + line + 118 + + firstVisibleColumn + 0 + firstVisibleLine + 95 + + adhocracy/templates/vote/browse.html + + caret + + column + 0 + line + 43 + + firstVisibleColumn + 0 + firstVisibleLine + 48 + + adhocracy/websetup.py + + caret + + column + 41 + line + 64 + + firstVisibleColumn + 0 + firstVisibleLine + 18 + + development.ini + + caret + + column + 12 + line + 69 + + firstVisibleColumn + 0 + firstVisibleLine + 0 + + + openDocuments + + adhocracy/model/user.py + adhocracy/templates/static/faq.html + adhocracy/templates/static/imprint.html + adhocracy/controllers/root.py + adhocracy/controllers/delegation.py + adhocracy/templates/delegation/graph.dot + adhocracy/templates/category/edit.html + adhocracy/config/routing.py + adhocracy/websetup.py + adhocracy/model/forms.py + adhocracy/model/__init__.py + adhocracy/templates/index.html + adhocracy/model/group.py + adhocracy/templates/category/create.html + adhocracy/templates/delegation/create.html + adhocracy/templates/motion/create.html + adhocracy/templates/motion/edit.html + adhocracy/lib/helpers.py + adhocracy/templates/template.html + adhocracy/templates/category/view.html + adhocracy/controllers/motion.py + adhocracy/controllers/vote.py + adhocracy/model/vote.py + adhocracy/model/motion.py + adhocracy/model/permission.py + adhocracy/lib/auth.py + development.ini + adhocracy/lib/base.py + adhocracy/controllers/category.py + adhocracy/model/delegation.py + adhocracy/templates/motion/view.html + adhocracy/templates/motion/browse.html + adhocracy/templates/components.html + adhocracy/templates/delegation/review.html + adhocracy/templates/vote/browse.html + adhocracy/model/delegateable.py + adhocracy/templates/category/tree.html + + showFileHierarchyDrawer + + windowFrame + {{945, 202}, {872, 886}} + + diff --git a/development.ini b/development.ini new file mode 100644 index 000000000..a5bd4a13a --- /dev/null +++ b/development.ini @@ -0,0 +1,111 @@ +# +# The %(here)s variable will be replaced with the parent directory of this file +# +[DEFAULT] +debug = true +# Uncomment and replace with the address which should receive any error reports +#email_to = you@yourdomain.com +smtp_server = fuckup.pudo.org +error_email_from = paste@localhost + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 5000 + +[app:main] +use = egg:adhocracy +full_stack = true +static_files = true + +cache_dir = %(here)s/data +beaker.session.key = adhocracy_state +beaker.session.secret = somesecret +#beaker.session.cookie_domain = .adhocracy.lan +#session.domain = .adhocracy.lan + +# If you'd like to fine-tune the individual locations of the cache data dirs +# for the Cache data, or the Session saves, un-comment the desired settings +# here: +beaker.cache.data_dir = %(here)s/data/cache +beaker.session.data_dir = %(here)s/data/sessions +memcached.server = 127.0.0.1:11211 + +# SQLAlchemy database URL +#sqlalchemy.url = sqlite:///%(here)s/development.db +sqlalchemy.url = mysql://pyliqd:pyliqd@localhost/pyliqd + +# Yes, that's a magic string. +## adhocracy.setup.drop = KILL_EM_ALL +# + +# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* +# Debug mode will enable the interactive debugging tool, allowing ANYONE to +# execute malicious code after an exception is raised. +#set debug = false + +# Used by the instance filter to determine the active instance: +adhocracy.domains = adhocracy.lan + +adhocracy.email.from = mail@adhocracy.cc + +# Karma threshold configuration +adhocracy.karma.category.create = 600 +adhocracy.karma.category.edit = 600 +adhocracy.karma.category.delete = 1000 +adhocracy.karma.issue.create = 0 +adhocracy.karma.issue.edit = 200 +adhocracy.karma.issue.delete = 1000 +adhocracy.karma.motion.create = 0 +adhocracy.karma.motion.edit = 200 +adhocracy.karma.motion.delete = 500 +adhocracy.karma.motion.beginpoll = 300 +adhocracy.karma.motion.endpoll = 300 +adhocracy.karma.comment.create = 0 +adhocracy.karma.comment.edit = 30 +adhocracy.karma.comment.delete = 500 +adhocracy.karma.karma.give = 5 + +# Logging configuration +[loggers] +keys = root, routes, adhocracy, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_routes] +level = INFO +handlers = +qualname = routes.middleware +# "level = DEBUG" logs the route matched and routing variables. + +[logger_adhocracy] +level = DEBUG +handlers = +qualname = adhocracy + +[logger_sqlalchemy] +#level = INFO +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..d2647100f --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,89 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/adhocracy.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/adhocracy.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 000000000..db4357d5c --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,33 @@ + +API Documentation +================= + +Adhocracy does not have a public programming API. There is a plan to develop a +common *Kernel* interface for *Liquid Democracy* projects. Once such an API +becomes available, Adhocracy will be modified to implement that API. + +The :mod:`adhocracy.lib` Module +------------------------------- + +.. automodule:: adhocracy.lib + :members: + :undoc-members: + +:mod:`adhocracy.lib.democracy` -- Core polling logic +==================================================== + +.. automodule:: adhocracy.lib.democracy.decision + :members: + +.. automodule:: adhocracy.lib.democracy.poll + :members: + +.. automodule:: adhocracy.lib.democracy.result + :members: + + +Delegation management and traversal +----------------------------------- + +.. automodule:: adhocracy.lib.democracy.delegation_node + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..1d34efc7c --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# +# adhocracy documentation build configuration file, created by +# sphinx-quickstart on Fri Sep 18 13:09:21 2009. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.append(os.path.abspath('.')) +sys.path.append(os.path.abspath('..')) + +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'adhocracy' +copyright = u'2009, Liquid Democracy e.V.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.2' +# The full version, including alpha/beta/rc tags. +release = '0.2' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'adhocracydoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'a4' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'adhocracy.tex', u'adhocracy Documentation', + u'Liquid Democracy e.V.', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..8bdbb2393 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +.. adhocracy documentation master file, created by + sphinx-quickstart on Fri Sep 18 13:09:21 2009. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to adhocracy's documentation! +===================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + api + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/repo_test.txt b/docs/repo_test.txt new file mode 100644 index 000000000..a7bb2c067 --- /dev/null +++ b/docs/repo_test.txt @@ -0,0 +1 @@ +Hallo, Welt! diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 000000000..d24e845e5 --- /dev/null +++ b/ez_setup.py @@ -0,0 +1,276 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c9" +DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', + 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', + 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', + 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', + 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', + 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', + 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', + 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', + 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', + 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', + 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', + 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', + 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', + 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', + 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', + 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', + 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', + 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', + 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', + 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', + 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', + 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', + 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', +} + +import sys, os +try: from hashlib import md5 +except ImportError: from md5 import md5 + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules + def do_download(): + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + try: + import pkg_resources + except ImportError: + return do_download() + try: + pkg_resources.require("setuptools>="+version); return + except pkg_resources.VersionConflict, e: + if was_imported: + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first, using 'easy_install -U setuptools'." + "\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return do_download() + except pkg_resources.DistributionNotFound: + return do_download() + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + + diff --git a/graphics/BONSAI_TREES_Ligustrum.psd b/graphics/BONSAI_TREES_Ligustrum.psd new file mode 100644 index 000000000..093804a44 Binary files /dev/null and b/graphics/BONSAI_TREES_Ligustrum.psd differ diff --git a/graphics/arrow.png b/graphics/arrow.png new file mode 100644 index 000000000..cd11dac03 Binary files /dev/null and b/graphics/arrow.png differ diff --git a/graphics/button.psd b/graphics/button.psd new file mode 100644 index 000000000..7e572209f Binary files /dev/null and b/graphics/button.psd differ diff --git a/graphics/gradient.psd b/graphics/gradient.psd new file mode 100644 index 000000000..0b4e62936 Binary files /dev/null and b/graphics/gradient.psd differ diff --git a/graphics/green_top.psd b/graphics/green_top.psd new file mode 100644 index 000000000..4fab1413f Binary files /dev/null and b/graphics/green_top.psd differ diff --git a/graphics/karma_buttons.psd b/graphics/karma_buttons.psd new file mode 100644 index 000000000..a158ee1d2 Binary files /dev/null and b/graphics/karma_buttons.psd differ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..95ff23022 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,34 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true + +[easy_install] +find_links = http://www.pylonshq.com/download/ + +[nosetests] +with-pylons = test.ini +#verbose=True +#verbosity=2 +#detailed-errors=1 + +# Babel configuration +[compile_catalog] +domain = adhocracy +directory = adhocracy/i18n +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = adhocracy/i18n/adhocracy.pot +width = 80 + +[init_catalog] +domain = adhocracy +input_file = adhocracy/i18n/adhocracy.pot +output_dir = adhocracy/i18n + +[update_catalog] +domain = adhocracy +input_file = adhocracy/i18n/adhocracy.pot +output_dir = adhocracy/i18n +previous = true diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..970371c4a --- /dev/null +++ b/setup.py @@ -0,0 +1,44 @@ +try: + from setuptools import setup, find_packages +except ImportError: + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages + +setup( + name='adhocracy', + version='0.2', + description='Community decision process manager', + author='Liquid Democracy e.V.', + author_email='friedrich@adhocracy.de', + url='http://www.adhocracy.de', + install_requires=[ + "Pylons>=0.9.7", + "SQLAlchemy>=0.5", + "FormEncode>=1.2.2", + "repoze.who>=1.0.15", + "repoze.what>=1.0.8", + "repoze.who.plugins.sa>=1.0rc2", + "repoze.what-pylons>=1.0", + "repoze.what.plugins.sql>=1.0rc1", + "repoze.who-friendlyform>=1.0b3" + ], + setup_requires=["PasteScript>=1.6.3", "setuptools>=0.6c6"], # fix OS X 10.5.7 + packages=find_packages(exclude=['ez_setup']), + include_package_data=True, + test_suite='nose.collector', + package_data={'adhocracy': ['i18n/*/LC_MESSAGES/*.mo']}, + message_extractors={'adhocracy': [ + ('**.py', 'python', None), + ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}), + ('public/**', 'ignore', None)]}, + zip_safe=False, + paster_plugins=['PasteScript', 'Pylons'], + entry_points=""" + [paste.app_factory] + main = adhocracy.config.middleware:make_app + + [paste.app_install] + main = pylons.util:PylonsInstaller + """, +) diff --git a/test.ini b/test.ini new file mode 100644 index 000000000..8e07bc4a6 --- /dev/null +++ b/test.ini @@ -0,0 +1,25 @@ +# +# adhocracy - Pylons testing environment configuration +# +# The %(here)s variable will be replaced with the parent directory of this file +# +[DEFAULT] +debug = true +# Uncomment and replace with the address which should receive any error reports +#email_to = you@yourdomain.com +smtp_server = localhost +error_email_from = paste@localhost + +[server:main] +use = egg:Paste#http +host = 127.0.0.1 +port = 5000 + +[app:main] +use = config:development.ini + +sqlalchemy.url = sqlite:///%(here)s/test.db + +# Add additional test specific configuration options as necessary. +adhocracy.setup.drop = KILL_EM_ALL +lucene.index.dir = %(here)s/test-data/index \ No newline at end of file diff --git a/unicode/core.zip b/unicode/core.zip new file mode 100644 index 000000000..f61af789e Binary files /dev/null and b/unicode/core.zip differ diff --git a/unicode/install_babel.sh b/unicode/install_babel.sh new file mode 100755 index 000000000..76a709721 --- /dev/null +++ b/unicode/install_babel.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "Installing Babel..." +wget http://unicode.org/Public/cldr/1.6.1/core.zip +cd ../adhocracy/contrib/babel/ +./setup.py egg_info +./scripts/import_cldr.py ../../../unicode/ +sudo easy_install ElementTree +sudo ./setup.py install