diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 00000000..2857047b --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,55 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python application + +on: + push: + branches: [ development ] + pull_request: + branches: [ development ] + +jobs: + build: + + runs-on: ubuntu-latest + + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + #MYSQL_DATABASE: laravel_tags + ports: + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest mysqlclient + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Run tests + run: | + python manage.py test -v 2 + env: + W2_DATABASE_HOST: 127.0.0.1 + W2_DATABASE_PORT: ${{ job.services.mysql.ports[3306] }} + W2_DATABASE_PASSWORD: + W2_DATABASE_USER: root + W2_SECRET_KEY: supersecretkey-only-used-for-testing + + - name: Deploy to heroku + if: false # Heroku Github integration is temporarily broken because of a security issue + uses: akhileshns/heroku-deploy@v3.8.9 # This is the action + with: + heroku_api_key: ${{ secrets.HEROKU_API_KEY }} + heroku_app_name: 'wasa2il-development' + heroku_email: ${{ secrets.MY_EMAIL }} + branch: 'development' diff --git a/.gitignore b/.gitignore index 55694319..740df485 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,45 @@ -*.pyc -# Generated by django compilemessages utility +# This file enumerates various files/patterns to ignore in git. +# Please keep this file tidy and neat + +# Local environment file +.env + +# Django translation files *.mo + +# Sqlite DB files +*.sqlite + +# Log files +*.log + +# Virtualenvs +/.venv/ +/venv/ + +# Coverage +/reports/* +/!reports/index.html +/.coverage +/coverage.xml + +# Cache files +__pycache__/ +.sass-cache + +# Editors/IDEs *.swp *.swo +.idea +*~ + +# Misc/unknowns +# TODO: Trim this list down/figure out reason for ignoring +wasa2il/locale/wasa2il.pot +pip-selfcheck.json bin include lib/python2.7 +lib/ local share -.idea -wasa2il/wasa2il.sqlite -*~ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..0da0aa4a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,63 @@ +image: docker:git +services: + - docker:dind + +stages: + - build + - test + - deploy + - release + +variables: + CONTAINER_RELEASE_IMAGE: registry.gitlab.com/piratar/wasa2il/image:latest + +build: + stage: build + image: python:2-onbuild + script: + - echo "This is where we build it..." + +test: + stage: test + image: python:2-onbuild + script: + - pip install -r requirements.txt + - coverage run --source=. manage.py test -v 2 + - coverage html + - flake8 --htmldir reports/flake8 --format=html -v + artifacts: + paths: + - reports/ + +pages: + stage: deploy + script: + - mv reports/ public/ + artifacts: + paths: + - public + +staging: + stage: deploy + only: + - master + script: + - git remote add heroku https://heroku:$HEROKU_STAGING_API_KEY@git.heroku.com/wasa2il-staging.git + - git push -f heroku HEAD:master + +development: + stage: deploy + only: + - development + script: + - git remote add heroku https://heroku:$HEROKU_STAGING_API_KEY@git.heroku.com/wasa2il-development.git + - git push -f heroku HEAD:master + +release-image: + stage: release + only: + - tags + script: + - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com + - docker build -t $CONTAINER_RELEASE_IMAGE . + - docker push $CONTAINER_RELEASE_IMAGE diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..214b521f --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.7.13 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..0065ba05 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,33 @@ +language: python +python: +- '3.7' +env: + global: + - W2_DATABASE_HOST=127.0.0.1 + - W2_DATABASE_PASSWORD= + - W2_DATABASE_USER=root + - W2_SECRET_KEY=`head /dev/urandom | sha256sum` +before_install: +- printenv W2_DATABASE_HOST +- printenv W2_DATABASE_PASSWORD +- printenv W2_DATABASE_USER +- cat /etc/os-release +install: pip install -r requirements.txt && pip install -r requirements-mysql.txt +script: +- coverage run --source=. manage.py test -v 2 +- coverage xml +- if [ "$CODACY_PROJECT_TOKEN" ]; then python-codacy-coverage -r coverage.xml; fi +services: +- mysql +deploy: + provider: heroku + api_key: + secure: LRuBdelPHheZTzy275pa2Bhn/gYg5C91rIOAQz26yVyfIfU5aaMbNjROK2m2EpPN+yuMCtVQk/NkQqj4xNUdnqSXBwkmrt1NUbCSL5PJREqNlzqDJYRg445IP47OCWGOIZR5TxGaKNQFSgF7S8Z0dKuG9cipVtvTXqQBf3kFp4s= + app: + development: wasa2il-development + master: wasa2il-staging + run: + - python manage.py migrate + - python manage.py compilemessages +after_success: +- "./tag.sh" diff --git a/AODS/aods.py b/AODS/aods.py deleted file mode 100644 index 0de705a3..00000000 --- a/AODS/aods.py +++ /dev/null @@ -1,222 +0,0 @@ -from Crypto.PublicKey import RSA -import simplejson as json -from hashlib import sha1, sha512 -import random - -DEFAULT_KEY_SIZE = 4097 - - -class AODSKey: - def __init__(self): - pass - - def generate(self, size=DEFAULT_KEY_SIZE): - self.keypair = RSA.generate(size) - self.privkey = self.keypair.__getstate__() - self.pubkey = self.keypair.publickey().__getstate__() - self.privatekey = json.dumps(self.privkey).encode("base64") - self.publickey = json.dumps(self.pubkey).encode("base64") - - def read_keyblock(self, text): - j = text.decode("base64") - state = json.loads(j) - state["e"] = long(state["e"]) - return state - - def set_pubkey(self, publickey): - state = self.read_keyblock(publickey) - rsa = RSA.construct((state["n"], state["e"])) - self.keypair = rsa - self.pubkey = state - self.publickey = json.dumps(self.pubkey).encode("base64") - - def set_privkey(self, privatekey): - state = self.read_keyblock(privatekey) - rsa = RSA.construct((state["n"], state["e"])) - rsa.__setstate__(state) - self.keypair = rsa - self.privkey = self.keypair.__getstate__() - self.pubkey = self.keypair.publickey().__getstate__() - self.privatekey = json.dumps(self.privkey).encode("base64") - self.publickey = json.dumps(self.pubkey).encode("base64") - - def get_pubkey(self): - return self.publickey - - def get_privkey(self): - return self.privatekey - - def get_fingerprint(self): - m = sha1() - m.update(self.publickey) - return "SHA1:" + m.hexdigest() - - def write_public(self, filename): - ke = open(filename, "w") - ke.write(json.dumps({'public': self.publickey})) - ke.close() - - def write_private(self, filename): - ke = open(filename, "w") - ke.write(json.dumps({'private': self.privatekey})) - ke.close() - - def read_public(self, filename): - ke = open(filename, "r") - input = json.loads(ke.read()) - ke.close() - self.set_pubkey(input["public"]) - - def read_private(self, filename): - ke = open(filename, "r") - input = json.loads(ke.read()) - ke.close() - self.set_privkey(input["private"]) - - -class AODS: - def __init__(self): - self._items = [] - self._keys = {} - self.initializer = "AODS INIT" + str(random.sample(range(0, 10000000), 50)) - - def load(self): - pass - - def get_values(self): - return [x[0] for x in self._items] - - def get_hashes(self): - return [x[1] for x in self._items] - - def get_signatures(self): - return [x[2] for x in self._items] - - def get_keys(self): - return self._keys.values() - - def get_keyids(self): - return self._keys.keys() - - def append(self, item, key): - assert(isinstance(key, AODSKey)) - assert(key.keypair.has_private()) - - hash = self.hash(len(self) - 1) - signature = str(key.keypair.sign(hash, None)[0]).encode("base64") - - self._items.append((item, hash, signature)) - self._keys[key.get_fingerprint()] = key.get_pubkey() - - def hash(self, id): - if id == -1: - item = self.initializer - hash = "" - sign = "" - else: - item = self._items[id][0].encode("base64") - hash = self._items[id][1].encode("base64") - sign = self._items[id][2].encode("base64") - - code = "%s:%s:%s" % (item, hash, sign) - m = sha512() - m.update(code) - return "SHA512:" + m.hexdigest() - - def verify(self, start=0): - end = len(self) - for i in range(start, end): - if self.verify_link(i) == False: - print "Error in verification of %d" % i - return False - - return True - - def verify_link(self, id): - if id == 0: - # First item is automagically verified - return True - - parent = id - 1 - child = id - - # Verify hash - h = self.hash(parent) - if not h == self._items[child][1]: - return False - - # Verify signature - pass - - return True - - def __getitem__(self, id): - return self._item[id] - - def __len__(self): - return len(self._items) - - def __iter__(self): - i = 0 - while True: - if len(self._items) < i: - yield None - next = self._items[i] - yield next - i += 0 - - def __hash__(self): - pass - - -if __name__ == "__main__": - print "Running tests..." - - print "Generating key..." - key = AODSKey() - key.generate() - - print "Making list..." - aods = AODS() - print "Adding 'foo' to list of length %d" % len(aods) - aods.append("foo", key) - print "Adding 'bar' to list of length %d" % len(aods) - aods.append("bar", key) - print "Adding 'baz' to list of length %d" % len(aods) - aods.append("baz", key) - print "List has length %d" % len(aods) - - print "Verifying history:" - print aods.verify() - - print "Damaging list with fake hash." - oh = aods._items[1][1] - fh = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da33" - aods._items[1] = (aods._items[1][0], fh, aods._items[1][2]) - print "Verifying history:" - print aods.verify() - - print "Restoring history:" - aods._items[1] = (aods._items[1][0], oh, aods._items[1][2]) - print aods.verify() - - for i in aods.get_hashes(): - print "%s..." % i[:15] - - print "Adding 'garg' to list of length %d" % len(aods) - aods.append("garg", key) - - for i in aods.get_hashes(): - print "%s..." % i[:15] - - print "Values:" - for i in aods.get_values(): - print "'%s'" % i - - print "Keys:" - for i in aods.get_keyids(): - print "'%s'" % i - - print "Signatures:" - for i in aods.get_signatures(): - print "'%s'" % i diff --git a/AUTHORS b/AUTHORS index a5f02e09..c6873883 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,6 +2,9 @@ Authors: Smári McCarthy Tómas Árni Jónasson + Helgi Hrafn Gunnarsson + Björn Leví Gunnarsson + Bjarni Rúnar Einarsson Contributors: @@ -10,6 +13,10 @@ Contributors: Zineb Belmkaddem Þórgnýr Thoroddssen Stefán Vignir Skarphéðinsson + Jóhann Haukur Gunnarsson + Steinn Eldjárn Sigurðarson + Björgvin Ragnarsson + Viktor Smári Translations: @@ -18,7 +25,7 @@ Dutch: Ruben Bloemgarten / Chokepoint Project Icelandic: - Eva Þurríðardóttir + Eva Þuríðardóttir Smári McCarthy Tómas Árni Jónasson diff --git a/Dockerfile b/Dockerfile index f692cf14..a0d835f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,9 @@ +# Please use "docker-compose up" to build, not "docker build" # Usage: # docker build -t wasa2il . # docker run -it -p 8000:8000 wasa2il -FROM python:2-onbuild +FROM python:3-onbuild -RUN apt-get update && apt-get install tofrodos -RUN fromdos initial_setup.py -RUN fromdos wasa2il/manage.py +WORKDIR /usr/src/app -CMD ./initial_setup.py && cd wasa2il && ./manage.py runserver $(hostname -i):8000 +CMD python manage.py runserver 0.0.0.0:8000 diff --git a/INSTALL.Debian.txt b/INSTALL.Debian.txt deleted file mode 100644 index ee3a38e4..00000000 --- a/INSTALL.Debian.txt +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash -# Wasa2il installation instructions for Debian-based systems -# Document version 1.3 -# Python package versions for reference: -# - Django (1.7) -# - django-bootstrap-form (3.1) -# - django-registration-redux (1.1) -# - lxml (3.4.0) -# - markdown2 (2.3.0) -# - MySQL-python (1.2.5) -# - Pillow (2.5.3) -# - suds (0.4) - -# For the daring, you may run these instruction notes by issuing the command: "bash INSTALL.Debian.txt" -# Please note that this method is only intended for experienced users and it may very well royally screw -# an existing instance if you've already run it once. -# If you're unsure, just read these instructions and copy/paste the commands as you go. - -# Install all the Debian packages we might need. -# Debian packages required: git pip python-dev libxslt1-dev libjpeg8-dev -# Optional for MySQL: libmysqlclient-dev mysql-server -# Optional for virtualenv: python-virtualenv -# If you don't have 'sudo' installed, comment the line that begins with 'sudo' and uncomment the line below which beings with 'su -c' -sudo apt-get -y install git python-pip python-dev libxslt1-dev python-virtualenv libmysqlclient-dev mysql-server libjpeg8-dev -#su -c "apt-get -y install git python-pip python-dev libxslt1-dev python-virtualenv libmysqlclient-dev mysql-server" - -# Prepare MySQL database. -# NOTE: This assumes that you have a MySQL server running locally and that you have the root password. -mysql -u root -p -e "CREATE DATABASE wasa2il;" -mysql -u root -p -e "GRANT ALL ON wasa2il.* TO 'wasa2il'@'localhost' IDENTIFIED BY 'wasa2il-pass';" - -# Retrieve source code. -git clone https://github.com/piratar/wasa2il.git - -# Setup and enable virtualenv. -# NOTE: This step is not required but is recommended to isolate the Python packages required -# to run Wasa2il from the rest of the system. If you skip this step, you will probably need root -# access for the 'pip' command below and your packages will be system-wide. -# If you're unsure, follow this step. Virtualenv is cool. -virtualenv --no-site-packages wasa2il -source wasa2il/bin/activate - -# Install Python dependency packages. -pip install -r wasa2il/requirements.txt - -# Create local settings skeleton. -cp wasa2il/wasa2il/local_settings.py-example wasa2il/wasa2il/local_settings.py - -# Open local_settings.py with a text editor and configure (in this case, with 'nano'). -nano wasa2il/wasa2il/local_settings.py - -# Configure the database, example is provided for MySQL as configured above. -# DATABASE_ENGINE = 'django.db.backends.mysql' -# DATABASE_HOST = 'localhost' -# DATABASE_PORT = '' -# DATABASE_NAME = 'wasa2il' -# DATABASE_USER = 'wasa2il' -# DATABASE_PASSWORD = 'wasa2il-pass' - -# Configure a secret, random key - just bash the keyboard for a bit. -# IMPORTANT: MAKE YOUR OWN STRING! DO NOT USE ONE FROM INSTRUCTIONS! -# SECRET_KEY = '34gjap48jg34hgoahra3g4oagq0220fj20fj1fj1jag344h009gr' - -# If you intend to use email, uncomment and configure mail settings. -# Otherwise, the default email backend prints to console, useful for development. -# Assuming a command line, run: python -m smtpd -n -c DebuggingServer localhost:25000 -# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -# EMAIL_HOST = 'localhost' -# EMAIL_PORT = '25000' -# EMAIL_HOST_USER = '' -# EMAIL_HOST_PASSWORD = '' -# EMAIL_USE_TLS = False - -# Create database tables and relationships -# NOTE: If you're using Virtualenv, it is assumed that you've run "source wasa2il/bin/activate" as above. -wasa2il/wasa2il/manage.py migrate - -# Create superuser account. -# For the remainder of this tutorial we will assume the username 'johndoe'. -# NOTE: If you're using Virtualenv, it is assumed that you've run "source wasa2il/bin/activate" as above. -wasa2il/wasa2il/manage.py createsuperuser - -# Compile the translation files. This is optional. -# NOTE: If you're using Virtualenv, it is assumed that you've run "source wasa2il/bin/activate" as above. -ls wasa2il/wasa2il/locale | xargs -n 1 pybabel compile -d wasa2il/wasa2il/locale/ -D django -l - -# Run the server. -# The default port is 8000 and doesn't need to be specified. -# However, to allow connections from outside of localhost, for example if you running -# the project in a virtual machine, you should add "0.0.0.0:8000". -# NOTE: If you're using Virtualenv, it is assumed that you've run "source wasa2il/bin/activate" as above. -wasa2il/wasa2il/manage.py runserver 0.0.0.0:8000 - -# Open the website with your favorite browser. -# Typically the address will be http://localhost:8000 - although if you're running -# the project in a virtual machine, you will have to figure out the networking yourself. -# VirtualBox hint: VM Settings -> Network -> Adapter 1 -> Attached to: "Bridged Adapter" - -# Login to the website, using the credentials you declared when running 'syncdb' above. -# In the case of this tutorial, it is "johndoe". - -# After login, follow the page for creating a new polity. -# This first polity automatically becomes the main polity, called a front polity. - -# We're done! You now should have a running Wasa2il instance ready to mess around with! - diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..df71b871 --- /dev/null +++ b/Makefile @@ -0,0 +1,117 @@ + +# NOTE: This is not the most perfect makefile. +# There's a few things we could do to improve performance. +# But since this is new, we can strive for clarity and simplicity here. +# Could use many of these suggestions: +# https://docs.cloudposse.com/reference/best-practices/make-best-practices/ + + +# On _some_ systems, make might not default to bash, which might produce +# unexpected output/errors. It's assumed that all of the recipes here are written +# in bash syntax. +SHELL = /bin/bash +.SHELLFLAGS = -e -u -o pipefail -c + + +default: help + + +# TODO: Add more descriptions to makefile targets +.PHONY: help +help: + @echo -e "\nThese are some useful commands to work with this project." + @echo -e "Please refer to the README.md for more details.\n" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + @echo -e "\nFor more information, please read the Makefile\n" + + +.env: + @echo "Missing '.env' file. Creating one using 'env.example' as a template" + @cp env.example .env + @echo "Please open the '.env' file and adjust to your local setup!" + @exit 1 + + +.venv: .env ## Creates a virtualenv at `./.venv` + @python -m venv .venv + + +.PHONY: setup +setup: .env .venv requirements.txt.log requirements-mysql.txt.log requirements-postgresql.txt.log ## Sets up the project (installs dependencies etc.) + + +requirements.txt.log: .venv requirements.txt.freeze + @. .venv/bin/activate && pip install -r requirements.txt.freeze | tee .requirements.txt.tmp.log + @mv .requirements.txt.tmp.log requirements.txt.log + + +requirements-mysql.txt.log: .env .venv requirements-mysql.txt.freeze +ifeq ($(shell grep '^W2_DATABASE_ENGINE' .env 2>/dev/null | grep 'mysql' > /dev/null && echo yes || echo no), yes) + @. .venv/bin/activate && pip install -r requirements-mysql.txt.freeze | tee .requirements-mysql.txt.tmp.log +else + @echo "Not using MySQL" | tee .requirements-mysql.txt.tmp.log +endif + @mv .requirements-mysql.txt.tmp.log requirements-mysql.txt.log + + +requirements-postgresql.txt.log: .env .venv requirements-postgresql.txt.freeze +ifeq ($(shell grep '^W2_DATABASE_ENGINE' .env 2>/dev/null | grep 'psyco' > /dev/null && echo yes || echo no), yes) + @. .venv/bin/activate && pip install -r requirements-postgresql.txt.freeze | tee .requirements-postgresql.txt.tmp.log +else + @echo "Not using PostgreSQL" | tee .requirements-postgresql.txt.tmp.log +endif + @mv .requirements-postgresql.txt.tmp.log requirements-postgresql.txt.log + + +.PHONY: freeze-dependencies +freeze-dependencies: + @. .venv/bin/activate && pip freeze | grep -v 'mysqlclient' > requirements.txt.freeze +ifeq ($(shell grep '^W2_DATABASE_ENGINE' .env 2>/dev/null | grep 'mysql' > /dev/null && echo yes || echo no), yes) + @. .venv/bin/activate && pip freeze | grep 'mysqlclient' > requirements-mysql.txt.freeze +endif +ifeq ($(shell grep '^W2_DATABASE_ENGINE' .env 2>/dev/null | grep 'psyco' > /dev/null && echo yes || echo no), yes) + @ .venv/bin/activate && pip freeze | grep 'psycopg' > requirements-postgresql.txt.freeze +endif + + +.PHONY: update-dependencies +update-dependencies: + @rm -rf .venv/ + @make .venv + @. .venv/bin/activate && pip install -r requirements.txt + @make freeze-dependencies + + +.PHONY: test +test: setup ## Runs the unit tests + @. .venv/bin/activate && ./manage.py test + + +.PHONY: migrate +migrate: setup ## Runs the migrate Django management command + @. .venv/bin/activate && ./manage.py migrate + + +.PHONY: load_fake_data +load_fake_data: setup ## Loads up fake data using custom Django management command + @. .venv/bin/activate && ./manage.py load_fake_data --full --reset + +.PHONY: run +run: setup ## Runs the Django development server + @. .venv/bin/activate && ./manage.py runserver + + +.PHONY: clean +clean: ## Removes cached python files and virtualenv + @echo "Deleting '__pycache__/' directories" + @find . -name "__pycache__" -exec rm -rf {} \+ + @echo "Deleting requirement log files" + @rm requirements*.log 2>/dev/null || true + @echo "Deleting the virtualenv ('./.venv/')" + @rm -rf .venv 2>/dev/null || true + + +.PHONY: docker/build +docker/build: ## Builds a Docker image and tags + docker build -t wasa2il . + diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..de615f0c --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn wasa2il.wsgi \ No newline at end of file diff --git a/README.md b/README.md index 7d17452e..f9be3aa7 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,137 @@ # Wasa2il - ‫وسائل -Wasa2il is a participatory democracy software project. It is based around the core -idea of polities - political entities - which users of the system can join or leave, +[![Build Status](https://travis-ci.org/piratar/wasa2il.svg?branch=development)](https://travis-ci.org/piratar/wasa2il) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/0bb6ea0bc27d4428ab044d97be638684)](https://www.codacy.com/app/7oi/wasa2il?utm_source=github.com&utm_medium=referral&utm_content=piratar/wasa2il&utm_campaign=Badge_Grade) +[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/0bb6ea0bc27d4428ab044d97be638684)](https://www.codacy.com/app/7oi/wasa2il?utm_source=github.com&utm_medium=referral&utm_content=piratar/wasa2il&utm_campaign=Badge_Coverage) + +**Wasa2il** is a participatory democracy software project. It is based around the core +idea of polities - political entities - which users of the system can join or leave, make proposals in, alter existing proposals, and adopt laws to self-govern. -The goal of this is to make it easy for groups on any scale - from the local +The goal of this is to make it easy for groups on any scale - from the local whiskey club to the largest nation - to self-organize and manage their intents, goals and mutual understandings. +# Staging/demo + +The current branches are deployed to Heroku when changes are merged. + +### master branch: +[https://wasa2il-staging.herokuapp.com](https://wasa2il-staging.herokuapp.com) + +### development branch: +[https://wasa2il-development.herokuapp.com](https://wasa2il-development.herokuapp.com) + +**Login** with any of the following 4 **user:pass** (a:a, b:b, c:c, d:d) + # Setup _Note: Setup instructions are OS-agnostic unless otherwise specified. We test on Linux, Mac OS X and Windows._ -For production use, Wasa2il must be set up on a web server capable of running Django. Instructions on setting up a Django production environment are however beyond the scope of this project, as well as Git cloning, Python installation and how to use a command line. Plenty of tutorials on these topics exist in various places online and we suggest you take a look at them if any of this seems confusing. +### For production use +Wasa2il must be set up on a web server capable of running Django. Instructions on setting up a Django production environment are however beyond the scope of this project, as well as Git cloning, Python installation and how to use a command line. Plenty of tutorials on these topics exist in various places online and we suggest you take a look at them if any of this seems confusing. + +### For development use + +**We recommend** the use of `virtualenv`, but we also try to support `docker-compose` below. + +1. Clone the project + +1. Create your personal `.env` file so you can easily override `ENV` vars like language, `W2_SECRET_KEY` etc. + + `cp env.example .env` + +1. Run `make help` to see what make commands are available. + + +#### Using make + +You can use `make` to do _most_ of the things you might need to. Behind the scenes +much of what follows runs in a virtualenv under `./.venv`. It's encouraged to get +to know how to use the virtualenv directly (see next section below). + +1. Create a virtual environment: `make venv` +1. Install dependencies etc: `make setup` (edit the created `.env` file!) +1. Create the database: `make migrate` +1. **Optional:** Create some fake data: `make load_fake_data` +1. Run the development server: `make run` + +#### Using virtualenv + +1. Create a virtual environment (usually either kept under `.venv` or `venv`) + + `python3 -m venv venv` + +1. Load the virtual environment in your current shell: + + `source venv/bin/activate` + +1. Install dependencies + + `pip install -r requirements.txt` + +1. Create the database by running migrations (Only the first time) -Long story short, to set up Wasa2il for development and/or testing: + `python manage.py migrate` -1. Install Python. You will need **pip** installed which is included by default in Python versions 2.7.9 and newer but can be downloaded separately for older versions. (URL: https://www.python.org/) +1. **Optional:** Reset the database and populate with a large volume of test data. + This should populate the database with a small amount of random data, including four users with varying levels of access (users `a`, `b`, `c` and `d` - each with their own username as a password). -2. Clone the Wasa2il Git project (URL: https://github.com/piratar/wasa2il.git) + `manage.py load_fake_data --full --reset` -3. In a command line, run the script **initial_setup.py**, which should guide you through the rest of the process. +1. Start server -That should be it! + `python manage.py runserver` + + +#### Docker Compose + +If you have `docker-compose` installed, you need to: + +1. Start web + db containers: +`docker-compose up` + +1. Run database migrations (if needed): +`docker-compose run app python manage.py migrate` + +1. If the db container is not started before the web container tries to access it, resulting in a Django error, restart the web container: +`docker-compose restart app` + + +#### Vagrant (Virtual machines) + +Three are a few examples of virtual machines setting up the project. These are found under the `vagrants/` directory. + +These are mostly meant to make sure that we have solid examples of how to set up the project on a brand new computer, including necessary system packages. + +So these can be used to debug system-level problems, and also to test out changes made at that level, as opposed to at the python package level. + + +#### SASS / SCSS / CSS +To watch and compile `.scss` automatically: +`cd core/static/css` and `scss --watch application.scss` + + +#### Tests +Run tests with `./manage.py test` + + +## Contributing + +Pull requests are welcome. Update translations by running **manage.py +makemessages** and edit the appropriate translation file, such as +`wasa2il/locale/is/LC_MESSAGES/django.po`. + +## Development process +1. Select an unassigned issue from the backlog, assign it to yourself, create a branch from the issue and check out the branch on your local machine + +2. Make changes to the code that address the issue (and preferably don't stuff much more in there) and push them. Note: small changes will make for way quicker reviews + +3. Monitor the pipeline on Gitlab, cross your fingers and hope for it to pass through + +4. Create a merge request into development branch and assign it to someone. + +5. If all is well the merge request will be approved into to development and the cahnges deployed to [staging](https://wasa2il-staging.herokuapp.com) # Project concepts @@ -30,20 +139,20 @@ That should be it! A polity is a political entity which consists of a group of people and a set of laws the group has decided to adhere to. In an abstract sense, membership in a polity -grants a person certain rights and priviledges. For instance, membership in a +grants a person certain rights and priviledges. For instance, membership in a school's student body may grant you the right to attend their annual prom, -and membership in a country (i.e. residency or citizenship) grants you a right -to live there and do certain things there, such as start companies. stand in +and membership in a country (i.e. residency or citizenship) grants you a right +to live there and do certain things there, such as start companies. stand in elections, and so on. -Each polity has different rules - these are also called statutes, bylaws or laws - +Each polity has different rules - these are also called statutes, bylaws or laws - which affect the polity on two different levels. Firstly, there are meta-rules, which describe how rules are formed, how -decisions are made, how meetings happen, and how governance in general -happens. Wasa2il has to be flexible enough to accomodate the varying -meta-rules of a given polity, otherwise the polity may decide that Wasa2il isn't -useful to them. Sometimes these rules are referred to as "rules of procedure" +decisions are made, how meetings happen, and how governance in general +happens. Wasa2il has to be flexible enough to accomodate the varying +meta-rules of a given polity, otherwise the polity may decide that Wasa2il isn't +useful to them. Sometimes these rules are referred to as "rules of procedure" or "constitution", depending on the type of polity which is using them. Secondly there are external rules, which are the decisions the polity makes which @@ -61,7 +170,7 @@ discussed. The members of a polity decide which topics are relevant to them. The way in which this is decided depends on the meta-rules of the polity. Topics are used to manage and display a list of issues. Issues are conversations -which have been brought to discussion within a polity, and can belong to +which have been brought to discussion within a polity, and can belong to numerous topics. The purpose of an issue and its associated conversation is to arrive at a decision. The decision is represented by a document which can be adopted or rejected. @@ -85,4 +194,3 @@ results are being forced out, whereas if there is a simple method to postpone an issue indefinitely opponents could gang up to game the system and eliminate the possibility of a Condorcet winner. Some middle ground should exist, and Wasa2il should support the creation of that.] - diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..dabff2f1 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.12.6 diff --git a/wasa2il/core/__init__.py b/cookiesdirective/__init__.py similarity index 100% rename from wasa2il/core/__init__.py rename to cookiesdirective/__init__.py diff --git a/cookiesdirective/apps.py b/cookiesdirective/apps.py new file mode 100644 index 00000000..fbd531a3 --- /dev/null +++ b/cookiesdirective/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class CookiesdirectiveConfig(AppConfig): + name = 'cookiesdirective' diff --git a/cookiesdirective/middleware.py b/cookiesdirective/middleware.py new file mode 100644 index 00000000..5fc132b9 --- /dev/null +++ b/cookiesdirective/middleware.py @@ -0,0 +1,14 @@ +class CookiesDirectiveMiddleware(object): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + return self.get_response(request) + + def process_response(self, request, response): + # Prevent cookies from being set, unless the user has specifically + # consentend to their use. + if request.COOKIES.get('cookiesDirective') != '1': + response.cookies.clear() + + return response diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 00000000..17acc4cd --- /dev/null +++ b/core/__init__.py @@ -0,0 +1 @@ +default_app_config = 'core.apps.CoreConfig' diff --git a/core/admin.py b/core/admin.py new file mode 100644 index 00000000..f9aa616f --- /dev/null +++ b/core/admin.py @@ -0,0 +1,39 @@ +from django.contrib import admin +from django.contrib import auth + +from core.models import UserProfile + + +def getDerivedAdmin(base_admin, **kwargs): + class DerivedAdmin(base_admin): + pass + derived = DerivedAdmin + for k, v in kwargs.iteritems(): + setattr(derived, k, getattr(base_admin, k, []) + v) + return derived + + +def save_model(self, request, obj, form, change): + if getattr(obj, 'created_by', None) is None: + obj.created_by = request.user + obj.modified_by = request.user + obj.save() + + +class UserProfileInline(admin.StackedInline): + model = UserProfile + can_delete = False + + +class UserAdmin(auth.admin.UserAdmin): + inlines = (UserProfileInline, ) + + +# Register the admins +register = admin.site.register + +# User profile mucking +admin.site.unregister(auth.models.User) +register(auth.models.User, UserAdmin) + +register(UserProfile) diff --git a/wasa2il/core/management/__init__.py b/core/ajax/__init__.py similarity index 100% rename from wasa2il/core/management/__init__.py rename to core/ajax/__init__.py diff --git a/wasa2il/core/ajax/utils.py b/core/ajax/utils.py similarity index 79% rename from wasa2il/core/ajax/utils.py rename to core/ajax/utils.py index 05d56146..1abff853 100644 --- a/wasa2il/core/ajax/utils.py +++ b/core/ajax/utils.py @@ -8,7 +8,7 @@ def wrapped(*args, **kwargs): m = f(*args, **kwargs) if isinstance(m, HttpResponse): return m - return HttpResponse(json.dumps(m)) + return HttpResponse(json.dumps(m), content_type='application/json') return wrapped diff --git a/core/apps.py b/core/apps.py new file mode 100644 index 00000000..a4a1af30 --- /dev/null +++ b/core/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + +from core.django_mdmail import convert_md_templates + +class CoreConfig(AppConfig): + name = 'core' + verbose_name = 'Wasa2il Core' + + def ready(self): + import core.signals + + convert_md_templates() diff --git a/core/authentication.py b/core/authentication.py new file mode 100644 index 00000000..d6278b3d --- /dev/null +++ b/core/authentication.py @@ -0,0 +1,40 @@ +from django import forms +from django.conf import settings +from django.contrib.auth import authenticate, get_user_model +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.models import User +from django.contrib.auth.forms import AuthenticationForm +from django.utils.translation import ugettext, ugettext_lazy as _ + + +class CustomAuthenticationBackend(ModelBackend): + def authenticate(self, request, username=None, password=None): + if username is None or password is None: + return None + + user = self.custom_get_user(username) + if user and user.check_password(password): + return user + else: + return None + + +class EmailAuthenticationBackend(CustomAuthenticationBackend): + """Allow users to log in using their e-mail address""" + def custom_get_user(self, email): + try: + return User.objects.get(email=email) + except (User.DoesNotExist, User.MultipleObjectsReturned): + return None + + +class SSNAuthenticationBackend(CustomAuthenticationBackend): + """Allow users to log in using their SSN""" + def custom_get_user(self, ssn): + # FIXME: This may be Iceland specific; we ignore dashes. + ssn = ssn.replace('-', '').strip() + + try: + return User.objects.get(userprofile__verified_ssn=ssn) + except (User.DoesNotExist, User.MultipleObjectsReturned): + return None diff --git a/core/certs/Oll_kedjan.pem b/core/certs/Oll_kedjan.pem new file mode 100644 index 00000000..956a02bd --- /dev/null +++ b/core/certs/Oll_kedjan.pem @@ -0,0 +1,101 @@ +-----BEGIN CERTIFICATE----- +MIIF/TCCA+WgAwIBAgICAsowDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVMx +EzARBgNVBAUTCjU1MDE2OTI4MjkxGjAYBgNVBAoTEUZqYXJtYWxhcmFkdW5leXRp +MRYwFAYDVQQLEw1Sb3RhcnNraWxyaWtpMRMwEQYDVQQDEwpJc2xhbmRzcm90MCAX +DTE1MTIwMjE5NTYzMVoYDzIwNTUxMjAyMTk1NjMxWjBrMQswCQYDVQQGEwJJUzET +MBEGA1UEBRMKNTUwMTY5MjgyOTEaMBgGA1UEChMRRmphcm1hbGFyYWR1bmV5dGkx +FjAUBgNVBAsTDVJvdGFyc2tpbHJpa2kxEzARBgNVBAMTCklzbGFuZHNyb3QwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDICG56w7ryRAz1Tv/QaBsZ060i +wqLOVN891fKiMjWBwCVSGC/QNYCOARXAwqPpMmvQPyqi/663Dil7iiT1DkEhErCi +CHVFltIWaStGFUhiOuZSxaQxRjb3bS8TgkYaQwvQXtbtVOewHWTf0mu5+FCTVQt8 +FseiKeR7kcTJAyQbtJuDJVMlas9QIZTDPZ7VWtYSablNNa9KnWqy6wEYu9L5G498 +Su24sPc2zi7rALAL8nKNaGrxYoSfXCNSjRS9Sxf+9a+RVaQc4udGeORgrr2L5ZBp +qJLlaboFUS7eKu+G49dcsC78iJljXx5hgsy7DATsdJxIJX7oGTPKAvcThpj+NqTu +oGMwo1hUfIO3ymuxVGJANNifCyzXCL5fSRvPQNsIrJZVyioqniB6lHqpHmLT+p2b +G6AxFjWMntPKxDSqjzDA2TcOX+e1jnmtWDQoxw8c5bsYYNomSXEzgQQMrneApv3A +waTJ2u5wjrCEh5k8PYZpNyditqQSLh1UZHHwdSY4bUqoMAYFDIKrxTGqPi14PMbB +83Vh0gm28FyksDICuLlwcb193POUAO+zWhfdpZUnZYHvP/POhjP8KLXA4Zsh5/4x +4+/LoxoTs2BJIF8D7ulMCyvJHHZ0CCEuIn6vyTBkOUkMwFJXwpuzGGAsB1QCTH0R +pj+xWDxmlbAfRmB7HQIDAQABo4GoMIGlMA8GA1UdEwEB/wQFMAMBAf8wYwYDVR0g +BFwwWjBYBgdggmABAQEBME0wJQYIKwYBBQUHAgIwGRoXQ2VydGlmaWNhdGUgUm9v +dCBQb2xpY3kwJAYIKwYBBQUHAgEWGGh0dHA6Ly9jcC5pc2xhbmRzcm90LmlzLzAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFGV8Ul8MuWrF0jBaAr2g+4BAsh66MA0G +CSqGSIb3DQEBCwUAA4ICAQBOoOwTYo76jxBKaZCVSyJ7OIKfJWoYO4LbVnph0Dl9 +s2MnSp2YunJr+m7GRZdswuQB+PgQ3eOyDcSQaXt2x/d08uGGq7goJjszSzQp9S4y +oPMnoAw8l3V8WFrkUDkMCIf+EP6zZiex8eXz2zuxjaPFlKBct3+IHCu6umW3qWsZ +aXKC7hOo1VKjrpPhogDB8uPuGxbv+EYn031h/2bcC5Hg7l9l9HcVHipuWaTAc3YO +aVZjFI2CNccO3/Mk4X7jaV2R8o++w0OsJSS99IBfis9KDoa89RmA+wBoJFtZxkRQ +5IPkuh6N92myjj0HlMqAB3b95tVKzTdx9GvAGQqMezI6+kcONLB3IW72JJLlllVx +rO5m0BXL1rfOpsb+gCAl655KxWyxkz8psAPtgfvDNiey2niofxt0LTyyXTPHLRse +dIKk2LpOOJEvZ+hQ/BonlUHuTUkouyvk9drDMpzBs9S5BcxKze2VbltyZcTrRGc/ +DONwMQT11hLkRAURzKxe4Jny9mOx47+95klo0kFqk0B4dGqMiB2td3URdB5YKZhi +Aq2LoJpgLpNWnQ2gsb6nKdHdRRbq27pxfSSr/tAwCEVSxXoz1+7jduPl9Yh1gswc +ajaT1USu2BPTXyHbIK+NwZwXbhB4KX+d0Xncsg+jgZomp1XhGleWEic9Zz3QylCI +jw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFfDCCA2SgAwIBAgICAyQwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVMx +EzARBgNVBAUTCjU1MDE2OTI4MjkxGjAYBgNVBAoTEUZqYXJtYWxhcmFkdW5leXRp +MRYwFAYDVQQLEw1Sb3RhcnNraWxyaWtpMRMwEQYDVQQDEwpJc2xhbmRzcm90MB4X +DTE3MTExNjEzMjgzM1oXDTMyMTExNjEzMjgzM1owfjELMAkGA1UEBhMCSVMxEzAR +BgNVBAUTCjUyMTAwMDI3OTAxFTATBgNVBAoTDEF1ZGtlbm5pIGhmLjEnMCUGA1UE +CxMeVXRnZWZhbmRpIGZ1bGxnaWxkcmEgc2tpbHJpa2phMRowGAYDVQQDExFGdWxs +Z2lsdCBhdWRrZW5uaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyn +5tktN9C5QTrErtjS5YM+0k29pTy5a8TYl56Ma70OJT6aO17z5NsPjwSnTswlEZ34 ++zjzZm5MVxi8/fQpbDZlE/4N4YZlYBD1rm8AZB3mJUyPVNYQpTirnWPAIXUjYD0B +25K7NU6V7MTibr5Z0VRgljvSbaKut4Y2ld3bx6Kmzc4pHKLU2hjbK+BSrPTbKuLH +8Xae9WP6O2Zxd2d3elDUmh+0uhFexf3DuBNxvp4ildmISdDG2x538i+jBOUyv9GX +wwbPGadt2FIdI15xgC0Y4L/AC8snfjEMcwH0xuaqdmAA/iOSWiprgTLQUmcnQQyT +6lpM21Ai/EaqDvT1AWkCAwEAAaOCARUwggERMBIGA1UdEwEB/wQIMAYBAf8CAQAw +agYDVR0gBGMwYTBfBgdggmABAQEBMFQwLQYIKwYBBQUHAgIwIRofSW50ZXJtZWRp +YXRlIENlcnRpZmljYXRlIFBvbGljeTAjBggrBgEFBQcCARYXaHR0cDovL2NwLmlz +bGFuZHNyb3QuaXMwDgYDVR0PAQH/BAQDAgEGMB8GA1UdIwQYMBaAFGV8Ul8MuWrF +0jBaAr2g+4BAsh66MD8GA1UdHwQ4MDYwNKAyoDCGLmh0dHA6Ly9jcmwuaXNsYW5k +c3JvdC5pcy9pc2xhbmRzcm90L2xhdGVzdC5jcmwwHQYDVR0OBBYEFMIpPob/hsTa +NR9ppqT/AYM8SjOpMA0GCSqGSIb3DQEBCwUAA4ICAQBOBaAGxLPntnC3fVeHeQsD +13AT2a3+Ry9R3AFP063Cwnc4+SYlCt+LSPa2wTCcDXAjvPgaxjRrTuTF1sCfj1uu +8GVMeo7e6suYggf2PLxsKWjaGOJhUtZnNC1rh2mt4TVYTR2D0AHYju0nNjzZlXU3 +1Ea//HDCQkV9+sSINTTrFL0Y5kB05WyVBXLHSTl/bKY8ULik2JImofrF+nI7GifH +CMLFkCOkAUDsI3Fd0Fh7v3NguxpOM4sov2jowMZzxqkS9B8B0qRO41h6spuLvsNS +tnFZHIbTrMaINUm9X6C49lr0fpRri4UNQa2prMgNsK9dwsYurlMga1WpO6fwkzU3 +mLYjitUxV9iYJ1VWj2jhJt0ofDsB4xLCVu8n0gekde09P5EdWzLvXD03PLtkEiGt +HElpluMFYaFjHhofYhai3u5eLVFTcNkEcyZO470EZTZ123dP3JQpSBKFjH7Z5CSS +wgvFB/zOOdnEDS49Iidetk5y2D8Pg2NB4qGJcmkit9Zjv2cOj9b5gKu7XMzkja9v +MahgfOrAKZJS7nrxr6NNR4rTMw8Rb9nvkS2sGk9ZGnbMLM+j84LDpT0rFMLNkegD +FVob63pewil6mH43mVOLq6tyYMiU7mgzYcui1rGnhtLiSqpI6L1UaQ2IqT4PJGdX +PIeO6oWVJpZaF7hWABD7sw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF4zCCBMugAwIBAgIDIt2xMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAklT +MRMwEQYDVQQFEwo1MjEwMDAyNzkwMRUwEwYDVQQKEwxBdWRrZW5uaSBoZi4xJzAl +BgNVBAsTHlV0Z2VmYW5kaSBmdWxsZ2lsZHJhIHNraWxyaWtqYTEaMBgGA1UEAxMR +RnVsbGdpbHQgYXVka2VubmkwHhcNMjEwNzAyMTM1ODAzWhcNMjIwNzAyMTM1ODAz +WjCBuTEXMBUGA1UECxMOMjAyMTA3MDIxMzA1MTAxHjAcBgNVBAoMFcOeasOzw7Bz +a3LDoSDDjXNsYW5kczELMAkGA1UEBhMCSVMxGDAWBgNVBAsTD0J1bmFkYXJza2ls +cmlraTEiMCAGA1UECwwZQXXDsGtlbm5pbmcgb2cgdW5kaXJyaXR1bjETMBEGA1UE +BRMKNjUwMzc2MDY0OTEeMBwGA1UEAxMVSW5uc2tyYW5pbmcgSXNsYW5kLmlzMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwu34ZNkrr8OcvtCpZ2BHEsaI +xtta5hnvMFpolOzaMnYaEIgdsbcVJzVF+8mwwg9AwGsnVE09qYqv8pH5Nmk0nR0o +EvlASEwbgVO0coJdRoBqpmDpS6GmIOIv7SDjOD/LtefoZ45NrzPSiD1Ml5a77ro1 +iwok1MgawkypdDTU0EdNvoUpv7qwsjZZEB0wexGt0hSLFS2v2KG2ib3MpxhzcOHu +NO7aave3fuOECIvBf6wnu4lH4gytLAHZKXx3SjIgG8vMRLCO/x3H+2sTOrBMwdkg +cVJTBgIt9R3aYEjdGKEs2jE38hmTDfWS4VmVwHQ1Noo15jghgTe2PPapOnN0wQID +AQABo4ICLDCCAigwDAYDVR0TAQH/BAIwADCCAQsGA1UdIASCAQIwgf8wgfwGCWCC +YAECAQEBAjCB7jCBxAYIKwYBBQUHAgIwgbcagbRUaGlzIGNlcnRpZmljYXRlIGlz +IGludGVuZGVkIGZvciBkaWdpdGFsIHNpZ25hdHVyZXMgYW5kIGF1dGhlbnRpY2F0 +aW9uLiBUaGlzIGNlcnRpZmljYXRlIGZ1bGZpbHMgdGhlIHJlcXVpcmVtZW50cyBv +ZiBub3JtYWxpemVkIGNlcnRpZmljYXRlIHBvbGljeSAoTkNQKSBkZWZpbmVkIGlu +IEVUU0kgVFMgMTAyIDA0Mi4wJQYIKwYBBQUHAgEWGWh0dHBzOi8vcmVwby5hdWRr +ZW5uaS5pcy8wdwYIKwYBBQUHAQEEazBpMCMGCCsGAQUFBzABhhdodHRwOi8vb2Nz +cC5hdWRrZW5uaS5pczBCBggrBgEFBQcwAoY2aHR0cDovL2NkcC5pc2xhbmRzcm90 +LmlzL3NraWxyaWtpL2Z1bGxnaWx0YXVka2VubmkucDdiMAsGA1UdDwQEAwIF4DAf +BgNVHSMEGDAWgBTCKT6G/4bE2jUfaaak/wGDPEozqTBDBgNVHR8EPDA6MDigNqA0 +hjJodHRwOi8vY3JsLmF1ZGtlbm5pLmlzL2Z1bGxnaWx0YXVka2VubmkvbGF0ZXN0 +LmNybDAdBgNVHQ4EFgQU3vPsh9yMh65rDY8hbQzMNMFwiSgwDQYJKoZIhvcNAQEL +BQADggEBAJyDYHAyxfIJLWhWi1tCXtsld8z088SlNSAgZZ+ZUMF1vdoFaI2L9hOF +yB4jqZIMsULxxo5Z2Qsw4FL2je/n1DM8B0aBxjpJ0ty6pIzd5hrcQscseUlhTFye +kApoBosxsMBAUCaR6Q8sAbiZwNE7sQzEPfzLlZrIEKz7bfgRwmsnBPLz8wWewmKp +kEzTxbEjxP/NA46bDce4tHvgpappgUbpgx0jl0svaeKuhBph9qtO+eYwsOjMRhUU +isUWS1tinJI5rKkEC1SNSuKuEYoXHJaZFyDGnEUYQMVDUfS2BwqVYF6o/NMhFA6v +N9OLNZueh+ekZgxkpeT65QjqBwB5bTw= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/core/contextprocessors.py b/core/contextprocessors.py new file mode 100644 index 00000000..3f2d025a --- /dev/null +++ b/core/contextprocessors.py @@ -0,0 +1,31 @@ +from django.conf import settings +from django.utils.translation import ugettext as _ + + +def globals(request): + + ctx = { + 'ORGANIZATION_NAME': settings.ORGANIZATION_NAME, + 'INSTANCE_NAME': settings.INSTANCE_NAME, + 'INSTANCE_URL': settings.INSTANCE_URL.strip('/'), + 'INSTANCE_FACEBOOK_IMAGE': settings.INSTANCE_FACEBOOK_IMAGE, + 'INSTANCE_FACEBOOK_APP_ID': settings.INSTANCE_FACEBOOK_APP_ID, + 'INSTANCE_VERSION': settings.WASA2IL_VERSION, + 'FEATURES': settings.FEATURES, + 'GCM_APP_ID': settings.GCM_APP_ID, + 'settings': settings + } + + # Get global variables from GlobalsMiddleWare. + ctx.update(request.globals) + + return ctx + + +def auto_logged_out(request): + if hasattr(request, 'auto_logged_out') and request.auto_logged_out: + return { + 'splash_message': _('For security reasons, you have been automatically logged out due to inactivity.') + } + + return {} diff --git a/core/dataviews.py b/core/dataviews.py new file mode 100644 index 00000000..55d4edc5 --- /dev/null +++ b/core/dataviews.py @@ -0,0 +1,94 @@ +from core.ajax.utils import jsonize +from django.urls import reverse +from election.models import Election +from issue.models import Issue + +@jsonize +def recent_activity(request): + + # Names prefixed with "q_" to distinguish them as queries. We will be + # returning the same data in JSON format, which we will call "issues" and + # "elections". + q_issues = Issue.objects.select_related('polity').recent().order_by('polity__id', '-deadline_votes') + q_elections = Election.objects.prefetch_related( + 'electionvote_set', + 'candidate_set' + ).select_related( + 'result' + ).recent() + + issues = [] + for q_issue in q_issues: + issues.append({ + # Web location for further info on the issue. + 'url': request.build_absolute_uri( + reverse('issue', args=(q_issue.polity_id, q_issue.id)) + ), + + # The polity to which this issue belongs. + 'polity': q_issue.polity.name, + + # The polity's short name, if available. + 'polity_shortname': q_issue.polity.name_short, + + # A unique identifier for formal reference. Example: 6/2019 + 'log_number': '%d/%d' % (q_issue.issue_num, q_issue.issue_year), + + # The issue's name or title. + 'name': q_issue.name, + + # Options are: concluded/voting/accepting_proposals/discussion + # Note that the state does not give the *result*, i.e. whether the + # proposal was accepted or rejected, but rather where the issue is + # currently in the decision-making process. Therefore "concluded" + # only means that the issue has concluded, but does not tell us + # *how* it concluded. + 'state': q_issue.issue_state(), + + # Translated, human-readable version of the issue state. + 'state_human_readable': q_issue.get_issue_state_display(), + + # A boolean indicating whether the issue has been approved or not. + 'majority_reached': q_issue.majority_reached(), + + # Translated, human-readable version of the result. + 'majority_reached_human_readable': q_issue.get_majority_reached_display(), + + # When the issue's fate is not determined by vote from within + # Wasa2il, for example when a vote is made outside of Wasa2il but + # still placed here for reference or historical reasons, or when + # an issue is retracted without ever coming to a vote. + # + # Consider displaying only this value if it is non-null, and the + # `majority_reached` value only if this is null. + 'special_process': q_issue.special_process, + + # Translated, human-readable version of the result. + 'special_process_human_readable': q_issue.get_special_process_display(), + + # Comment count. + 'comment_count': q_issue.comment_count, + + # Vote count. + 'vote_count': q_issue.votecount, + }) + + elections = [] + for q_election in q_elections: + # See comments for issue above, which are more detailed but are mostly + # applicable to this section as well. + elections.append({ + 'url': request.build_absolute_uri(reverse('election', args=(q_election.polity_id, q_election.id))), + 'polity': q_election.polity.name, + 'polity_shortname': q_election.polity.name_short, + 'name': q_election.name, + 'state': q_election.election_state(), + 'state_human_readable': q_election.get_election_state_display(), + 'candidate_count': q_election.candidate_set.count(), + 'vote_count': q_election.get_vote_count(), + }) + + return { + 'elections': elections, + 'issues': issues, + } diff --git a/core/django_mdmail.py b/core/django_mdmail.py new file mode 100644 index 00000000..ea09a300 --- /dev/null +++ b/core/django_mdmail.py @@ -0,0 +1,148 @@ +# django_mdmail +# Version: 0.4 +# Authors: Helgi Hrafn Gunnarsson +# Repository: https://github.com/binary-is/django_mdmail +# License: MIT +# +# Description: django_mdmail bridges the gap between the `mdmail` package and +# Django. `mdmail` is a Python module that allows the sending of Markdown +# email. Django is a web development framework which contains its own wrapper +# functions for sending email. This package is a simple wrapper for `mdmail` +# utilizing Django's email settings and imitating Django's email function +# signature. Ideally, a Django user should be able to replace... +# +# from django.core.mail import send_mail +# +# ...with... +# +# from django_mdmail import send_mail +# +# ...to send Markdown emails with all the features of `mdmail`. +# +# Installation: This package is a single file with all the necessary +# information provided in comments at the top. To install or upgrade, simply +# place this file wherever you wish, import the `send_mail` function as +# described above and use it according to Django's documentation. +# +# You will need to have these Python packages installed: mdmail django +# +# Note: The parameter `html_message` can be used to override the HTML +# generated from Markdown but this feature should only be used under special +# circumstances because it defies the whole point of using `django_mdmail` in +# the first place. +# +# A function is also provided for processing *.md files into *.txt and *.html +# in Django's template directories, for use with built-in functionality like +# Django's forgotten-password mechanism. Run the function from some Django +# app's ready() hook (see AppConfig.ready() in Django documentation) and +# you'll only need to provide a Markdown version of those emails. Note that +# you may still have to do some coding of your own to make sure that both text +# and HTML emails are being sent (as opposed to only text emails). +# +# Limitations: +# * No inline images in email templates created by `convert_md_templates`. +import os +import sys + +from django.conf import settings +from django.core.mail import EmailMultiAlternatives +if sys.version_info[0] == 3: + from email.mime.image import MIMEImage +else: + from email.MIMEImage import MIMEImage +from mdmail import EmailContent + + +# Warning to be placed in generated text and HTML files. +OVERRIDE_WARNING = 'WARNING! THIS FILE IS AUTO-GENERATED by django_mdmail upon Django startup. Changes to this file WILL be overwritten. In the same directory, there should be a file with the same name, except an ".md" ending (for Markdown). Edit that instead and restart Django.' + + +def send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None, connection=None, html_message=None): + + # Have `mdmail` do its Markdown magic. + content = EmailContent(message) + + # Create the email message and fill it with the relevant data. + email = EmailMultiAlternatives( + subject, + content.text, + from_email, + recipient_list + ) + email.attach_alternative(html_message or content.html, 'text/html') + email.mixed_subtype = 'related' + + for filename, data in content.inline_images: + # Create the image from the image data. + image = MIMEImage(data.read()) + + # Give the image an ID so that it can be found via HTML. + image.add_header('Content-ID', '<{}>'.format(filename)) + + # This header allows users of some email clients (for example + # Thunderbird) to view the images as attachments when displaying the + # message as plaintext, without it interrupting those users who view + # it as HTML. + image.add_header( + 'Content-Disposition', 'attachment; filename=%s' % filename + ) + + # Attach the image. + email.attach(image) + + # Finally, send the message. + email.send(fail_silently) + + +def convert_md_templates(): + ''' + Scans template directories for .md files and generates text and + email-client-friendly HTML files from them, intended for email use. + + Use with AppConfig.ready() hooks (see Django documentation) to run at + Django startup. + + Example `core/apps.py` file: + + from django.apps import AppConfig + + from django_mdmail import convert_md_templates + + class CoreConfig(AppConfig): + name = 'core' + + def ready(self): + convert_md_templates() + ''' + + override_comment = '{%% comment %%}%s{%% endcomment %%}\n' % OVERRIDE_WARNING + + # Find all the template directories we'll need to process and put them + # in a flat list. + template_dirs = [] + for template_conf in settings.TEMPLATES: + template_dirs += [d for d in template_conf['DIRS']] + + # Iterate the template directories. + for template_dir in template_dirs: + for root, subdirs, filenames in os.walk(template_dir): + for filename in filenames: + if filename[-3:] == '.md': + md_path = os.path.join(root, filename) + txt_path = '%s.txt' % md_path[:-3] + html_path = '%s.html' % md_path[:-3] + + with open(md_path, 'r') as f: + md_content = f.read() + f.close() + + # Generate email-client-friendly HTML. + content = EmailContent(md_content) + + with open(txt_path, 'w') as f: + f.write(override_comment + content.text) + f.close() + + with open(html_path, 'w') as f: + f.write(override_comment + content.html) + f.close() diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 00000000..2795bb88 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth.forms import UsernameField +from django.forms import Form +from django.forms import CharField +from django.forms import EmailField +from django.forms import TextInput +from django.forms import TypedChoiceField +from django.forms import ValidationError +from django.forms.widgets import ChoiceWidget +from django.utils.translation import ugettext as _ + +from registration.forms import RegistrationForm + +from wasa2il.forms import Wasa2ilForm + +from core.models import UserProfile + + +class EmailWantedField(ChoiceWidget): + template_name = 'forms/widgets/email_wanted.html' + + +class UserProfileForm(Wasa2ilForm): + email = EmailField(label=_("E-mail"), help_text=_("You can change your email address, but will then need to verify it.")) + + class Meta: + model = UserProfile + fields = ('displayname', 'email', 'phone', 'picture', 'bio', 'declaration_of_interests', 'language', 'email_wanted') + + # We need to keep the 'request' object for certain kinds of validation ('picture' in this case) + def __init__(self, *args, **kwargs): + self.request = kwargs.pop('request', None) + super(UserProfileForm, self).__init__(*args, **kwargs) + + def clean_picture(self): + data = self.cleaned_data['picture'] + + picture = self.request.FILES.get('picture') + if picture: + if picture.name.find('.') == -1: + raise ValidationError(_('Filename must contain file extension')) + else: + ext = picture.name.split('.')[-1].lower() + allowed_exts = ['jpg', 'jpeg', 'png', 'gif'] + if ext not in allowed_exts: + raise ValidationError(u'%s: %s' % ( + _('Only the following file endings are allowed'), + ', '.join(allowed_exts) + )) + + return data + + +class Wasa2ilRegistrationForm(RegistrationForm): + username = UsernameField( + widget=TextInput(attrs={'autofocus': True}), + label=_('Username'), + help_text=_('Only letters, numbers and the symbols @/./+/-/_ are allowed.') #_('Aðeins er leyfilegt að nota bókstafi, tölustafi og táknin @/./+/-/_') + ) + email_wanted = TypedChoiceField( + choices=((True, _('Yes')), (False, _('No'))), + widget=EmailWantedField, + label=_('Consent for sending email') + ) + +class PushNotificationForm(Form): + text = CharField(label=_('Message')) diff --git a/wasa2il/core/management/commands/__init__.py b/core/management/__init__.py similarity index 100% rename from wasa2il/core/management/commands/__init__.py rename to core/management/__init__.py diff --git a/wasa2il/core/templatetags/__init__.py b/core/management/commands/__init__.py similarity index 100% rename from wasa2il/core/templatetags/__init__.py rename to core/management/commands/__init__.py diff --git a/wasa2il/core/management/commands/closeissues.py b/core/management/commands/closeissues.py similarity index 100% rename from wasa2il/core/management/commands/closeissues.py rename to core/management/commands/closeissues.py diff --git a/core/management/commands/export_db.py b/core/management/commands/export_db.py new file mode 100644 index 00000000..00383208 --- /dev/null +++ b/core/management/commands/export_db.py @@ -0,0 +1,147 @@ +import random +import string +import subprocess + +from datetime import timedelta + +from django.utils import timezone + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand +from django.db.models import F + +from election.models import ElectionVote + +from issue.models import Vote + +SUPPORTED_ENGINES = ['django.db.backends.mysql'] + +class Command(BaseCommand): + + def handle(self, *args, **options): + + # Creates a random datetime from the beginning of 2010 to now. + def random_time(): + lowest = timezone.datetime(2010, 1, 1) + highest = timezone.now() + difference = highest - lowest + seconds = random.randint(0, int(difference.total_seconds())) + return lowest + timedelta(seconds=seconds) + + # Creates a random string according to specifications. + def ran(length_min, length_max=0, lc=False, uc=False, digits=False): + if lc == uc == digits == False: + raise Exception('ran function: At last one of parameters "lc", "uc" or "digits" must be True.') + + chars = '' + if lc: + chars += string.ascii_lowercase + if uc: + chars += string.ascii_uppercase + if digits: + chars += string.digits + + if length_max == 0: + length = length_min + else: + length = random.randint(length_min, length_max) + + return ''.join(random.choice(chars) for _ in range(length)) + + # Creates a string that alternates vowels and characters to give them + # the impression that they might be names in an exotic language. + def random_name(): + chars = { + 'v': 'eyuioa', # Vowels + 'c': 'qwrtpsdfghjklzxcvbnm' # Consonants, + } + length = random.randint(3, 10) + + result = '' + last_type = 'c' if random.randint(0, 1) == 0 else 'v' + while len(result) < length: + this_type = 'c' if last_type == 'v' else 'v' + last_type = this_type + + result += random.choice(chars[this_type]) + + # Capitalize first letter. + result = result[0].upper() + result[1:] + + return result + + # Create an exact copy of the working (default) database according to + # the export database, which is presumabyl configured in the settings. + self.mirror_databases() + + # Remove votes, in case an issue or election is in process. + Vote.objects.using('export').all().delete() + ElectionVote.objects.using('export').all().delete() + + # Replace personal data with random garbage. + for user in User.objects.using('export').select_related('userprofile'): + user.username = ran(6, 12, lc=True) + user.email = '%s@%s.%s' % (ran(4, 10, lc=True), ran(4, 10, lc=True), ran(2, lc=True)) + user.date_joined = random_time() + + if hasattr(user, 'userprofile'): + user.userprofile.verified_ssn = ran(10, digits=True) + user.userprofile.verified_name = '%s %s' % (random_name(), random_name()) + user.userprofile.verified_token = ran(30, lc=True, uc=True, digits=True) + user.userprofile.verified_assertion_id = ran(30, lc=True, uc=True, digits=True) + user.userprofile.verified_timing = random_time() + user.userprofile.bio = 'The entire bio has been replaced with this mysterious text.' + user.userprofile.declaration_of_interests = 'The interest rate is currently around 470%.' + user.userprofile.picture = None + user.userprofile.joined_org = user.date_joined + + user.userprofile.displayname = user.userprofile.verified_name + + user.userprofile.save() + + user.save() + + + # Create an export database, an exact replica of the default database. + def mirror_databases(self): + + if not 'export' in settings.DATABASES: + raise Exception('This function only works if an export database is defined in settings.') + + if settings.DATABASES['default']['ENGINE'] != settings.DATABASES['export']['ENGINE']: + raise Exception('Database engine of default and export databases must be the same.') + + engine = settings.DATABASES['default']['ENGINE'] + username = settings.DATABASES['default']['USER'] + password = settings.DATABASES['default']['PASSWORD'] + db_default = settings.DATABASES['default']['NAME'] + db_export = settings.DATABASES['export']['NAME'] + + if engine not in SUPPORTED_ENGINES: + raise Exception('Database engine %s not (yet) supported for exporting.' % engine) + + if engine == 'django.db.backends.mysql': + # Make sure that database is empty and that it exists. + subprocess.check_output( + ['mysql', '-u', username, '-p%s' % password, '-e', 'DROP DATABASE IF EXISTS `%s`;' % db_export] + ) + subprocess.check_output( + ['mysql', '-u', username, '-p%s' % password, '-e', 'CREATE DATABASE `%s`;' % db_export] + ) + + # Transfer schema and data from default database to export + # database. + ps = subprocess.Popen( + ['mysqldump', db_default, '-u', username, '-p%s' % password], + stdout=subprocess.PIPE + ) + output = subprocess.check_output( + ['mysql', db_export, '-u', username, '-p%s' % password], + stdin=ps.stdout + ) + ps.wait() + + # At this point, the export database should contain an exact + # replica of the default database, but with personal data either + # scrambled for anonymity or removed entirely. diff --git a/core/management/commands/heartbeat.py b/core/management/commands/heartbeat.py new file mode 100644 index 00000000..e41bf3bb --- /dev/null +++ b/core/management/commands/heartbeat.py @@ -0,0 +1,16 @@ +from sys import stdout, stderr + +from django.core.management.base import BaseCommand + +from core.utils import heartbeat + +class Command(BaseCommand): + def handle(self, *args, **options): + """ + heartbeat is a wasa2il command intended to be run once per minute, + for instance through a cron script. It manages things like sending + due push notifications and cleaning things. + """ + stdout.write('Running heartbeat...\n') + stdout.flush() + heartbeat() diff --git a/core/management/commands/load_fake_data.py b/core/management/commands/load_fake_data.py new file mode 100644 index 00000000..617cdef4 --- /dev/null +++ b/core/management/commands/load_fake_data.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- +# +# This comment will create fake polities, users, issues and elections to +# facilitate testing. +# +import random +import traceback +from datetime import datetime, timedelta + +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.utils import IntegrityError +from django.http import HttpRequest + +from core.models import * +from polity.models import Polity, PolityRuleset +from topic.models import Topic +from issue.models import Issue, Document, DocumentContent +from election.models import Election, Candidate, ElectionVote +import core.views + + +ADJECTIVES = ('Sad', 'Happy', 'Good', 'Evil', 'Liberal', 'Dadaist', 'Hungry') +THINGS = ('Chicken', 'Human', 'Automobile', 'Fish', 'Puppy', 'Kitten', + 'Infant', 'Lobster', 'Nature') +ACTIONS = ('Farming', 'Justice', 'Education', 'Welfare', 'Entertainment') +ACTACTS = ('Stop', 'Improve', 'Defuzz', 'Budget', 'Plan', 'Restrict', + 'Disarm', 'Avoid', 'Protest', 'Support') + + +def req(method, user, data, **kwargs): + hr = HttpRequest() + hr.REQUEST = data + hr.method = 'GET' if (not data) else 'POST' + hr.user = user + return method(hr, **kwargs) + + +class Command(BaseCommand): + + def add_arguments(self, parser): + for flag in ('users', 'topics', 'documents', 'elections', + 'reset', 'full'): + parser.add_argument('--%s' % flag, action='store_true', dest=flag) + + @transaction.atomic + def handle(self, *args, **options): + now = datetime.now() + + if not options.get('full'): + print() + print('NOTE: Creating small test data set, use --full for MOAR DATA.') + print() + + reset = False + if options.get('reset'): + yn = raw_input('Are you sure you want to delete precious data? [y/N] ') + if yn.strip().lower() == 'y': + reset = True + else: + return + + create_all = not (options.get('users', False) or + options.get('topics', False) or + options.get('elections', False) or + options.get('documents', False)) + + userlist = [ + ('a', 'a@example.com', 'Alpha'), + ('b', 'b@example.com', 'Beta'), + ('c', 'c@example.com', 'Foo'), + ('d', 'd@example.com', 'Baz')] + userlist += [ + ('user%s' % i, 'user%s@example.com' % i, 'User %s' % i) + for i in range(0, 1110)] + serial_ssn = 0 + if options.get('users') or create_all: + if not options.get('full'): + userlist = userlist[:20] + print('Generating %d users ...' % len(userlist)) + users = {} + if reset: + User.objects.all().delete() + for u, email, name in userlist: + if User.objects.filter(username=u).first() is None: + if len(u) == 1: + users[u] = User.objects.create_user(u, password=u) + users[u].is_staff = True + users[u].is_superuser = True + print(' * Creating user "%s" with password "%s"' % (u, u)) + else: + users[u] = User.objects.create_user(u) + users[u].email = email + users[u].save() + + # Update the user's UserProfile with demo data. + # (UserProfile is automatically created when User is + # saved, via signal. + up = users[u].userprofile + up.verified_ssn = '%10.10d' % serial_ssn, + up.joined_org = now - timedelta(hours=random.randint(0, 24 * 5)) + up.save() + + serial_ssn += 1 + else: + # User already exists + users[u] = User.objects.get(username=u) + + print('Generating/updating 4 polities of varying sizes ...') + pollist = [ + ('d', 'The Big Polity', 'abc', 1000), + ('c', 'The Medium Polity', 'abc', 100), + ('b', 'The Small Polity', 'ab', 10), + ('a', 'The Dinky Polity', 'a', 1)] + if not options.get('full'): + pollist = pollist[2:] + topiclist = [] + for t1 in ADJECTIVES: + for t2 in THINGS: + for t3 in ACTIONS: + topiclist.append('%s %s %s' % (t1, t2, t3)) + polities = {} + documents = {} + for u, name, members, size in pollist: + print(' + %s (size=%d)' % (name, size)) + usr = User.objects.get(username=u) + try: + p = Polity.objects.get(name=name) + new = False + except: + p = Polity(name=name, + slug=name.lower().replace(' ', '-'), + description='A polity with about %d things' % size) + p.created = now - timedelta(hours=random.randint(0, 24 * 5)) + p.created_by = usr + p.modified_by = usr + p.save() + PolityRuleset( + polity=p, + name='Silly rules', + issue_majority=50, + issue_discussion_time=timedelta(hours=24), + issue_proposal_time=timedelta(hours=24), + issue_vote_time=timedelta(hours=24) + ).save() + new = True + polities[u] = (p, size) + + if new or options.get('topics') or create_all: + n = 1 + min(size//5, len(topiclist)) + print(' - Creating %d topics' % n) + if reset: + Topic.objects.filter(polity=p).delete() + for topic in random.sample(topiclist, n): + Topic(name=topic, + polity=p, + created_by=usr).save() + + if new or options.get('users') or create_all: + print(' - Adding ~%d users' % size) + for m in set([m for m in members] + + random.sample(users.keys(), size)): + try: + # User d is a member of no polities + if m != 'd': + p.members.add(users[m]) + except: + pass + + if options.get('elections') or create_all: + # Create 3 elections per polity: + # one soliciting candidates, one voting, one finished + print(' - Creating 3 elections') + if reset: + Election.objects.filter(polity=p).delete() + for dc, dv in ((1, 2), (-1, 1), (-2, -1)): + e = Election( + name="%s Manager" % random.choice(THINGS), + polity=p, + voting_system='schulze', + deadline_candidacy=now + timedelta(days=dc), + deadline_votes=now + timedelta(days=dv), + deadline_joined_org=now + timedelta(days=dv)) + e.save() + + if (dc < 0) or (dv < 0): + candidatec = min(p.members.count(), 15) + voterc = 0 + else: + candidatec = min(p.members.count(), 5) + voterc = min(p.members.count(), 5) + + candidates = [] + for cand in random.sample(list(p.members.all()), candidatec): + c = Candidate(election=e, user=cand) + c.save() + candidates.append(c) + for voter in random.sample(list(p.members.all()), voterc): + random.shuffle(candidates) + for rank, cand in enumerate(candidates): + ElectionVote( + election=e, + user=voter, + candidate=cand, + value=rank).save() + + if (dv < 0) and voterc and candidatec: + try: + e.process() + except: + traceback.print_exc() + print('Votes cast on %s: %s' % (e, ElectionVote.objects.filter(election=e).count())) + + if new or options.get('documents') or create_all: + # We create a list of authors biased towards the first + # users created, so some users will have lots of documents + # and others will have less. + ul = [username for username, e, n in userlist] + aw = [(m.username, max(20 - ul.index(m.username), 1)) + for m in p.members.all()] + authors = [a for a, w in aw for i in range(0, w)] + + # Get a list of topics... + topics = Topic.objects.filter(polity=p) + + print(' - Creating %d documents' % size) + if reset: + Document.objects.filter(polity=p).delete() + for docn in range(0, size): + topic = random.choice(topics) + subject = '%s %s with %s' % (random.choice(ACTACTS), + topic.name, + random.choice(THINGS) + 's') + author = User.objects.get(username=random.choice(authors)) + doc = Document( + name=subject, + user=author, + polity=p) + doc.save() + doc.created = now - timedelta(hours=random.randint(0, 24 * 3)) + doc.save() + + documents[doc.id] = (topic, doc) + text = subject + for version in range(0, random.randint(1, 3)): + text = '%s\n%s' % (text, text) + docc = DocumentContent(document=doc, user=author, text=text) + docc.status = 'proposed' + docc.order = version + docc.save() + + if options.get('documents') or create_all: + # Put max(3, 10%) of all the documents up for election + howmany = min(max(3, len(documents) // 10), len(documents)) + print('Creating issues for %d documents.' % howmany) + j = 1 + for dk in random.sample(documents.keys(), howmany): + topic, doc = documents[dk] + i = Issue( + name=doc.name, + polity=doc.polity, + created_by=doc.user, + issue_num=j, + issue_year=2018, + ruleset=PolityRuleset.objects.filter(polity=doc.polity)[0], + majority_percentage=50, + documentcontent=doc.preferred_version()) + i.save() + i.created = doc.created + i.apply_ruleset(now=doc.created) + i.save() + i.topics.add(topic) + j += 1 diff --git a/core/management/commands/lookup_usernames.py b/core/management/commands/lookup_usernames.py new file mode 100644 index 00000000..3a54bd17 --- /dev/null +++ b/core/management/commands/lookup_usernames.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# This comment will look up a list of usernames, returning names. +# +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand + +from core.models import * + + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument('username', nargs='+', action='append') + + def handle(self, *args, **options): + count = 1 + for usernames in options.get('username', [[]])[0]: + for u in (un.strip() for un in usernames.split(',') if un): + u = u.decode('utf-8') + try: + name = User.objects.get(username=u).get_name() + except: + name = '[no such user]' + print ('%d. %s (%s)' % (count, name, u)).encode('utf-8') + count += 1 diff --git a/core/management/commands/processissues.py b/core/management/commands/processissues.py new file mode 100644 index 00000000..82b4b798 --- /dev/null +++ b/core/management/commands/processissues.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from sys import stdout, stderr +from datetime import datetime + +from django.core.management.base import BaseCommand + +from core.models import * + +class Command(BaseCommand): + + def handle(self, *args, **options): + + now = datetime.now() + + unprocessed_issues = Issue.objects.filter( + deadline_votes__lte=now, + is_processed=False + ).order_by('deadline_votes', 'id') + + for issue in unprocessed_issues: + issue_name = issue.name.encode('utf-8') + + stdout.write('Processing issue %s...' % issue_name) + stdout.flush() + + if issue.process(): + stdout.write(' done\n') + else: + stdout.write(' failed!\n') diff --git a/core/middleware.py b/core/middleware.py new file mode 100644 index 00000000..dcd46d68 --- /dev/null +++ b/core/middleware.py @@ -0,0 +1,126 @@ + +from django.conf import settings +from django.shortcuts import redirect, render +from django.urls import resolve +from django.utils.deprecation import MiddlewareMixin + +from core.models import UserProfile + +from polity.models import Polity + +from django.contrib import auth + +from datetime import datetime, timedelta + +# A middleware to make certain variables available to both templates and views. +class GlobalsMiddleware(MiddlewareMixin): + def process_request(self, request): + + global_vars = { + 'polity': None, + 'user_is_member': False, + 'user_is_officer': False, + 'user_is_wrangler': False, + 'WASA2IL_VERSION': settings.WASA2IL_VERSION, + 'WASA2IL_HASH': settings.WASA2IL_HASH, + 'CONTACT_EMAIL': settings.CONTACT_EMAIL, + 'ORGANIZATION_NEWS_URL': settings.ORGANIZATION_NEWS_URL, + 'using_saml': len(settings.SAML['URL']) > 0, + } + + try: + match = resolve(request.path) + + if 'polity_id' in match.kwargs: + polity_id = int(match.kwargs['polity_id']) + global_vars['polity'] = polity = Polity.objects.prefetch_related( + 'members', + 'officers', + 'wranglers' + ).get(id=polity_id) + + if not request.user.is_anonymous: + global_vars['user_is_member'] = request.user in polity.members.all() + global_vars['user_is_officer'] = request.user in polity.officers.all() + # Officers are automatically wranglers. + if global_vars['user_is_officer']: + global_vars['user_is_wrangler'] = True + else: + global_vars['user_is_wrangler'] = request.user in polity.wranglers.all() + except: + # Basically only 404-errors and such cause errors here. Besides, + # we'll want to move on with our lives anyway. + pass + + request.globals = global_vars + + +# Middleware for automatically logging out a user once AUTO_LOGOUT_DELAY +# seconds have been reached without activity. +class AutoLogoutMiddleware(MiddlewareMixin): + def process_request(self, request): + if hasattr(settings, 'AUTO_LOGOUT_DELAY'): + + now = datetime.now() + + if not request.user.is_authenticated : + # Set the last visit to now when attempting to log in, so that + # auto-logout feature doesn't immediately log the user out + # when the user is already logged out but the session is still + # active. + if request.path_info == '/accounts/login/' and request.method == 'POST': + request.session['last_visit'] = now.strftime('%Y-%m-%d %H:%M:%S') + + # Can't log out if not logged in + return + + if 'last_visit' in request.session: + last_visit = datetime.strptime(request.session['last_visit'], '%Y-%m-%d %H:%M:%S') + if now - last_visit > timedelta(0, settings.AUTO_LOGOUT_DELAY * 60, 0): + auth.logout(request) + request.auto_logged_out = True + + request.session['last_visit'] = now.strftime('%Y-%m-%d %H:%M:%S') + + +# Middleware for requiring SAML verification before allowing a logged in user +# to do anything else. +class SamlMiddleware(MiddlewareMixin): + def process_request(self, request): + + if settings.SAML['URL']: # Is SAML support enabled? + + if hasattr(settings, 'SAML_VERIFICATION_EXCLUDE_URL_PREFIX_LIST'): + exclude_urls = settings.SAML_VERIFICATION_EXCLUDE_URL_PREFIX_LIST + else: + exclude_urls = [] + + # Short-hands. + path_ok = request.path_info in [ + '/accounts/verify/', + '/accounts/logout/', + '/accounts/login-or-saml-redirect/' + ] or any([request.path_info.find(p) == 0 for p in exclude_urls]) + logged_in = request.user.is_authenticated + verified = request.user.userprofile.verified if logged_in else False + + if logged_in and not verified and not path_ok: + ctx = { 'auth_url': settings.SAML['URL'] } + return render(request, 'registration/verification_needed.html', ctx) + + def process_response(self, request, response): + + if settings.SAML['URL'] and hasattr(request, 'user'): + logged_in = request.user.is_authenticated + verified = request.user.userprofile.verified if logged_in else False + just_logged_in = ( + request.path == '/accounts/login/' + and response.status_code == 302 + and response.url == settings.LOGIN_REDIRECT_URL + ) + + if logged_in and just_logged_in and not verified: + return redirect('/accounts/login-or-saml-redirect/') + + return response + return response diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 00000000..43d63551 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-07-12 14:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('verified_ssn', models.CharField(blank=True, max_length=30, null=True, unique=True)), + ('verified_name', models.CharField(blank=True, max_length=100, null=True)), + ('verified_token', models.CharField(blank=True, max_length=100, null=True)), + ('verified_timing', models.DateTimeField(blank=True, null=True)), + ('verified', models.BooleanField(default=False)), + ('displayname', models.CharField(blank=True, help_text='The name to display on the site.', max_length=255, null=True, verbose_name='Name')), + ('email_visible', models.BooleanField(default=False, help_text='Whether to display your email address on your profile page.', verbose_name='E-mail visible')), + ('bio', models.TextField(blank=True, null=True, verbose_name='Bio')), + ('picture', models.ImageField(blank=True, null=True, upload_to=b'profiles', verbose_name='Picture')), + ('joined_org', models.DateTimeField(blank=True, null=True)), + ('email_wanted', models.NullBooleanField(default=False, help_text='Whether to consent to receiving notifications via email.', verbose_name='Consent for sending email')), + ('language', models.CharField(choices=[(b'is', b'\xc3\x8dslenska'), (b'en', b'English')], default=b'en', max_length=6, verbose_name='Language')), + ('topics_showall', models.BooleanField(default=True, help_text='Whether to show all topics in a polity, or only starred.')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0002_reset_migrations.py b/core/migrations/0002_reset_migrations.py new file mode 100644 index 00000000..0d18d52f --- /dev/null +++ b/core/migrations/0002_reset_migrations.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-07-12 14:19 +from __future__ import unicode_literals + +from django.db import migrations + +''' +Having refactored the entire project, splitting the 'core' app into several +different ones and changed the data design in many places, and despite the +ingenuity with which this feat was performed, migrations had become a bit of a +booboo. They were many and complicated, un-squashable and were even starting +to break new installations due to changing runtime environments (or so it is +believed). + +To remedy the problem, the courageous and noble decision was made to restart +the migrations in their entirety, by deleting the old ones, replacing them +merely with new initial files. + +In order to make this actually work, one single, new, ELIDABLE (look it up), +custom RunSQL-migration was required to remove the possibility that future +migrations' names might clash with older ones, in Django's own bookkeeping on +which migrations had yet actually been performed. + +And so, we present to you, in all its magnificence, this custom migration +which fixes the problem forever... + +...except for those who were running outdated versions in a production +environment and didn't first migrate to version 0.10.13 before upgrading to +the next one. For those, this actually causes problems (duly solved by +upgrading first to 0.10.13 before upgrading to the next one). But strong, +nearly irrefutable lack of evidence for any such scenario being in any way +remotely close to being likely, means we don't really give a damn... + +...so, at least for everyone else; FOREVER! + +(No assurance or guarantee is provided for a single word scribbled here being +anything more than meaningless gobbledygook. Users read and get upset about it +entirely at their own risk.) + +''' + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.RunSQL([ + (""" + DELETE FROM + django_migrations + WHERE + app = '%s' + AND + name != '0001_initial'""" % app + ) for app in ['core', 'polity', 'issue', 'tasks', 'topic', 'election']] + ) + ] diff --git a/core/migrations/0003_event.py b/core/migrations/0003_event.py new file mode 100644 index 00000000..a3d554b6 --- /dev/null +++ b/core/migrations/0003_event.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2018-12-08 18:06 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0002_reset_migrations'), + ] + + operations = [ + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(auto_now=True)), + ('module', models.CharField(max_length=32)), + ('action', models.CharField(max_length=32)), + ('category', models.CharField(blank=True, max_length=64)), + ('event', models.CharField(blank=True, max_length=1024)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0004_userprofile_declaration_of_interests.py b/core/migrations/0004_userprofile_declaration_of_interests.py new file mode 100644 index 00000000..28735d96 --- /dev/null +++ b/core/migrations/0004_userprofile_declaration_of_interests.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2019-07-23 13:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_event'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='declaration_of_interests', + field=models.TextField(blank=True, null=True, verbose_name='Declaration of interests'), + ), + ] diff --git a/core/migrations/0005_auto_20190822_2006.py b/core/migrations/0005_auto_20190822_2006.py new file mode 100644 index 00000000..fd7cca2a --- /dev/null +++ b/core/migrations/0005_auto_20190822_2006.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.4 on 2019-08-22 20:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_userprofile_declaration_of_interests'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='userprofile', + name='language', + field=models.CharField(choices=[('is', 'Íslenska'), ('en', 'English')], default='en', max_length=6, verbose_name='Language'), + ), + migrations.AlterField( + model_name='userprofile', + name='picture', + field=models.ImageField(blank=True, null=True, upload_to='profiles', verbose_name='Picture'), + ), + ] diff --git a/core/migrations/0005_userprofile_verified_assertion_id.py b/core/migrations/0005_userprofile_verified_assertion_id.py new file mode 100644 index 00000000..a3a9ec40 --- /dev/null +++ b/core/migrations/0005_userprofile_verified_assertion_id.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-08-24 21:31 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_userprofile_declaration_of_interests'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='verified_assertion_id', + field=models.CharField(blank=True, max_length=50, null=True), + ), + ] diff --git a/core/migrations/0006_userprofile_phone.py b/core/migrations/0006_userprofile_phone.py new file mode 100644 index 00000000..4f2888d5 --- /dev/null +++ b/core/migrations/0006_userprofile_phone.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-08-29 18:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_userprofile_verified_assertion_id'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='phone', + field=models.CharField(blank=True, help_text='Mostly intended for active participants such as volunteers and candidates.', max_length=30, null=True, verbose_name='Phone'), + ), + ] diff --git a/core/migrations/0007_merge_20191019_1939.py b/core/migrations/0007_merge_20191019_1939.py new file mode 100644 index 00000000..6f72e9d5 --- /dev/null +++ b/core/migrations/0007_merge_20191019_1939.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.6 on 2019-10-19 19:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_auto_20190822_2006'), + ('core', '0006_userprofile_phone'), + ] + + operations = [ + ] diff --git a/core/migrations/0008_auto_20210116_2004.py b/core/migrations/0008_auto_20210116_2004.py new file mode 100644 index 00000000..92e06859 --- /dev/null +++ b/core/migrations/0008_auto_20210116_2004.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.5 on 2021-01-16 20:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_merge_20191019_1939'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='email_wanted', + field=models.BooleanField(default=False, help_text='Whether to consent to receiving notifications via email.', null=True, verbose_name='Consent for sending email'), + ), + ] diff --git a/core/migrations/0009_auto_20210827_1638.py b/core/migrations/0009_auto_20210827_1638.py new file mode 100644 index 00000000..136e47f8 --- /dev/null +++ b/core/migrations/0009_auto_20210827_1638.py @@ -0,0 +1,31 @@ +# Generated by Django 2.2.24 on 2021-08-27 16:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ('core', '0008_auto_20210116_2004'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('auth.user',), + ), + migrations.AlterField( + model_name='event', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.User'), + ), + ] diff --git a/wasa2il/forum/__init__.py b/core/migrations/__init__.py similarity index 100% rename from wasa2il/forum/__init__.py rename to core/migrations/__init__.py diff --git a/core/models.py b/core/models.py new file mode 100644 index 00000000..75d86cf7 --- /dev/null +++ b/core/models.py @@ -0,0 +1,201 @@ +#coding:utf-8 +import os +import re +import json + +from datetime import datetime, timedelta +from django.conf import settings +from django.db import models +from django.db.models import BooleanField +from django.db.models import CASCADE +from django.db.models import Case +from django.db.models import Count +from django.db.models import IntegerField +from django.db.models import Q +from django.db.models import SET_NULL +from django.db.models import When +from django.contrib.auth.models import User as BaseUser +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone +from registration.signals import user_registered + +from diff_match_patch.diff_match_patch import diff_match_patch + +from issue.models import DocumentContent +from issue.models import Issue + +import inspect + + +# See: https://docs.djangoproject.com/en/3.2/topics/db/managers/#custom-managers +class UserManager(models.Manager): + # Annotates basic task statistics. + def annotate_task_stats(self): + return self.annotate( + tasks_applied_count=Count('taskrequest'), + tasks_completed_count=Count('taskrequest', filter=Q(taskrequest__task__is_done=True)), + tasks_accepted_count=Count('taskrequest', filter=Q(taskrequest__is_accepted=True)) + ) + + +# A proxy model only so that we can add our own model manager and functions +# for dealing with project-specific things. +# See: https://docs.djangoproject.com/en/3.2/topics/db/models/#proxy-models +class User(BaseUser): + objects = UserManager() + + # Produces percentages out of task statistics produced through the + # `annotate_task_stats()` function in our model manager. + def tasks_percent(self): + # Make sure that we instruct the programmer properly if they're using + # this function without using the proper annotation function that + # produces the required data. + needed_attrs = ['tasks_applied_count', 'tasks_accepted_count', 'tasks_completed_count'] + if not all(hasattr(self, a) for a in needed_attrs): + raise Exception('User.tasks_percent() function can only be called when User.objects.annotate_task_stats() has been applied') + + # Let's not bother calculating things if everything is zero anyway. + if self.tasks_applied_count == 0: + return {'applied': 0, 'accepted': 0, 'completed': 100} + + return { + 'applied': 100*(self.tasks_applied_count - self.tasks_accepted_count - self.tasks_completed_count) / float(self.tasks_applied_count), + 'accepted': 100*(self.tasks_accepted_count - self.tasks_completed_count) / float(self.tasks_applied_count), + 'completed': 100*(self.tasks_completed_count) / float(self.tasks_applied_count) + } + + class Meta: + proxy = True + + +class UserProfile(models.Model): + """A user's profile data. Contains various informative areas, plus various settings.""" + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=CASCADE) + + # Verification + # Field `verified_token` was used with SAML 1.2 whereas + # `verified_assertion_id` has been used since adopting SAML 2. + verified_ssn = models.CharField(max_length=30, null=True, blank=True, unique=True) + verified_name = models.CharField(max_length=100, null=True, blank=True) + verified_token = models.CharField(max_length=100, null=True, blank=True) + verified_assertion_id = models.CharField(max_length=50, null=True, blank=True) + verified_timing = models.DateTimeField(null=True, blank=True) + # When using SAML, the 'verified' field is set to true if verified_ssn, + # verified_name and verified_timing have all been set with actual content. + # Otherwise, it should be the same as User.is_active. + verified = models.BooleanField(default=False) + + # User information + displayname = models.CharField(max_length=255, verbose_name=_("Name"), help_text=_("The name to display on the site."), null=True, blank=True) + phone = models.CharField(max_length=30, verbose_name=_('Phone'), help_text=_('Mostly intended for active participants such as volunteers and candidates.'), null=True, blank=True) + email_visible = models.BooleanField(default=False, verbose_name=_("E-mail visible"), help_text=_("Whether to display your email address on your profile page.")) + bio = models.TextField(verbose_name=_("Bio"), null=True, blank=True) + declaration_of_interests = models.TextField(verbose_name=_('Declaration of interests'), null=True, blank=True) + picture = models.ImageField(upload_to='profiles', verbose_name=_("Picture"), null=True, blank=True) + joined_org = models.DateTimeField(null=True, blank=True) # Time when user joined organization, as opposed to registered in the system + + # When this is null (None), it means that the user has not consented to, + # nor specifically rejected receiving email. This is a left-over state + # from when implied consent sufficed, but should gradually be decreased + # until all users have either consented or not. No new members should have + # this field as null (None). + email_wanted = models.BooleanField( + default=False, + null=True, + verbose_name=_('Consent for sending email'), + help_text=_('Whether to consent to receiving notifications via email.') + ) + + language = models.CharField(max_length=6, default='en', choices=settings.LANGUAGES, verbose_name=_("Language")) + topics_showall = models.BooleanField(default=True, help_text=_("Whether to show all topics in a polity, or only starred.")) + + def save(self, *largs, **kwargs): + is_new = self.pk is None + + if is_new: + self.language = settings.LANGUAGE_CODE + + if not self.picture: + self.picture.name = os.path.join( + self.picture.field.upload_to, + 'default.jpg' + ) + + if settings.SAML['URL']: + self.verified = all(( + self.verified_ssn is not None and len(self.verified_ssn) > 0, + self.verified_name is not None and len(self.verified_name) > 0 + )) + else: + self.verified = self.user.is_active + + super(UserProfile, self).save(*largs, **kwargs) + + def __str__(self): + return u'Profile for %s (%d)' % (self.user, self.user.id) + + def get_polity_ids(self): + return [x.id for x in self.user.polities.all()] + +# Make sure registration creates profiles +def _create_user_profile(**kwargs): + UserProfile.objects.get_or_create(user=kwargs['user']) + +user_registered.connect(_create_user_profile) + + +# TODO: Deprecate this function. +# This function should be deprecated in favor of calling +# `.userprofile.displayname` directly, the reason being that this +# function hides the need for using `select_related()`, but also because +# a `UserProfile` is now automatically created so it accounts for a +# scenario that no longer occurs. +def get_name(user): + name = "" + if user: + try: + name = user.userprofile.displayname + except AttributeError: + print('User with id %d missing profile?' % user.id) + pass + + if not name: + name = user.username + + return name + +# We need to monkey-patch both `BaseUser` and `User` because we've added +# `User` as a proxy model. Both monkey-patches should removed when +# `get_name` gets refactored out. +BaseUser.get_name = get_name +User.get_name = get_name + + +class Event(models.Model): + timestamp = models.DateTimeField(auto_now=True) + user = models.ForeignKey(User, blank=True, null=True, on_delete=SET_NULL) + module = models.CharField(max_length=32, blank=False) + action = models.CharField(max_length=32, blank=False) + category = models.CharField(max_length=64, blank=True) + event = models.CharField(max_length=1024, blank=True) + + def __str__(self): + return "[%s][%s.%s/%s@%s] %s" % (self.timestamp, self.module, self.action, self.category, self.user, self.event) + +def event_register(action, category="", event={}, user=None): + e = Event() + e.user = user + frm = inspect.stack()[1] + mod = inspect.getmodule(frm[0]) + e.module = mod.__name__ + e.action = action + e.category = category + e.event = json.dumps(event) + e.save() + +def event_time_since_last(module, action): + e = Event.objects.filter(module=module, action=action).order_by('-timestamp') + if len(e) == 0: + return timedelta(100000000) + else: + return datetime.now() - e[0].timestamp diff --git a/core/saml.py b/core/saml.py new file mode 100644 index 00000000..c436ede1 --- /dev/null +++ b/core/saml.py @@ -0,0 +1,61 @@ +from datetime import datetime +from signxml import XMLVerifier +from xml.etree import ElementTree + +from django.conf import settings +from django.contrib.auth import login +from django.contrib.auth.models import User +from django.http import HttpResponseRedirect + + +class SamlException(Exception): + pass + + +def authenticate(input_xml, ca_pem_file): + + # @test: Check signature and retrieve the XML that's guaranteed to be signed. + signed_xml = XMLVerifier().verify(input_xml, require_x509=True, ca_pem_file=ca_pem_file).signed_xml + + # @test: Verify they're not sending us multiple root level assertions. + if len(signed_xml.findall('./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion')) > 1: + raise SamlException('Too many assertion matched') + + # @process: Obtain the assertion. + assertion = signed_xml.find('./{urn:oasis:names:tc:SAML:2.0:assertion}Assertion') + if not assertion: + raise SamlException('Could not find valid assertion') + + # @process: Obtain the conditions. + conds_xml = assertion.find('./{urn:oasis:names:tc:SAML:2.0:assertion}Conditions') + if not conds_xml: + raise SamlException('Could not find valid conditions statement. This is required.') + + # @test: Verify audience. + audience = conds_xml.find("{urn:oasis:names:tc:SAML:2.0:assertion}AudienceRestriction/{urn:oasis:names:tc:SAML:2.0:assertion}Audience").text + if audience not in settings.ALLOWED_HOSTS: + raise SamlException('Incorrect audience specified') + + # @test: Verify date boundaries + # Apparently the datetimes have 7 digits of microseconds when normally + # they should be 6 when parsed. We'll leave them out by only using the + # first 19 characters. + time_limit_lower = datetime.strptime(conds_xml.attrib['NotBefore'][:19],'%Y-%m-%dT%H:%M:%S') + time_limit_upper = datetime.strptime(conds_xml.attrib['NotOnOrAfter'][:19],'%Y-%m-%dT%H:%M:%S') + now = datetime.now() + if time_limit_lower > now or time_limit_upper < now: + raise SamlException('Remote authentication expired') + + # @process: obtain ID. + # Find the assertion ID for our records. This is not needed for + # functionality's sake and is only kept for the hypothetical scenario + # where a particular authentication needs to be matched with records on + # the identity provider's side. + assertion_id = assertion.attrib['ID'] + + # @process: Translate SAML attributes into a handy dictionary. + attributes = {} + for attribute in assertion.findall('.//{urn:oasis:names:tc:SAML:2.0:assertion}Attribute'): + attributes[attribute.attrib['Name']] = attribute.find('.//{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue').text + + return assertion_id, attributes diff --git a/core/signals.py b/core/signals.py new file mode 100644 index 00000000..85831d70 --- /dev/null +++ b/core/signals.py @@ -0,0 +1,25 @@ +from django.contrib.auth.models import User +from django.contrib.auth.signals import user_logged_in +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.dispatch import Signal + +from core.models import UserProfile + +from languagecontrol.utils import set_language + + +user_verified = Signal(providing_args=['user', 'request']) + + +@receiver(post_save, sender=User) +def ensure_userprofile(sender, instance, **kwargs): + ''' + Make sure that a user profile always exists per user. + ''' + UserProfile.objects.get_or_create(user_id=instance.id) + + +@receiver(user_logged_in) +def set_language_on_login(sender, user, request, **kwargs): + set_language(request, user.userprofile.language) diff --git a/wasa2il/core/static/bootstrap/css/bootstrap-theme.css b/core/static/bootstrap/css/bootstrap-theme.css similarity index 100% rename from wasa2il/core/static/bootstrap/css/bootstrap-theme.css rename to core/static/bootstrap/css/bootstrap-theme.css diff --git a/wasa2il/core/static/bootstrap/css/bootstrap-theme.css.map b/core/static/bootstrap/css/bootstrap-theme.css.map similarity index 100% rename from wasa2il/core/static/bootstrap/css/bootstrap-theme.css.map rename to core/static/bootstrap/css/bootstrap-theme.css.map diff --git a/wasa2il/core/static/bootstrap/css/bootstrap-theme.min.css b/core/static/bootstrap/css/bootstrap-theme.min.css similarity index 100% rename from wasa2il/core/static/bootstrap/css/bootstrap-theme.min.css rename to core/static/bootstrap/css/bootstrap-theme.min.css diff --git a/wasa2il/core/static/bootstrap/css/bootstrap.css b/core/static/bootstrap/css/bootstrap.css similarity index 100% rename from wasa2il/core/static/bootstrap/css/bootstrap.css rename to core/static/bootstrap/css/bootstrap.css diff --git a/wasa2il/core/static/bootstrap/css/bootstrap.css.map b/core/static/bootstrap/css/bootstrap.css.map similarity index 100% rename from wasa2il/core/static/bootstrap/css/bootstrap.css.map rename to core/static/bootstrap/css/bootstrap.css.map diff --git a/wasa2il/core/static/bootstrap/css/bootstrap.min.css b/core/static/bootstrap/css/bootstrap.min.css similarity index 100% rename from wasa2il/core/static/bootstrap/css/bootstrap.min.css rename to core/static/bootstrap/css/bootstrap.min.css diff --git a/wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.eot b/core/static/bootstrap/fonts/glyphicons-halflings-regular.eot similarity index 100% rename from wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.eot rename to core/static/bootstrap/fonts/glyphicons-halflings-regular.eot diff --git a/wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.svg b/core/static/bootstrap/fonts/glyphicons-halflings-regular.svg similarity index 100% rename from wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.svg rename to core/static/bootstrap/fonts/glyphicons-halflings-regular.svg diff --git a/wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.ttf b/core/static/bootstrap/fonts/glyphicons-halflings-regular.ttf similarity index 100% rename from wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.ttf rename to core/static/bootstrap/fonts/glyphicons-halflings-regular.ttf diff --git a/wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.woff b/core/static/bootstrap/fonts/glyphicons-halflings-regular.woff similarity index 100% rename from wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.woff rename to core/static/bootstrap/fonts/glyphicons-halflings-regular.woff diff --git a/wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.woff2 b/core/static/bootstrap/fonts/glyphicons-halflings-regular.woff2 similarity index 100% rename from wasa2il/core/static/bootstrap/fonts/glyphicons-halflings-regular.woff2 rename to core/static/bootstrap/fonts/glyphicons-halflings-regular.woff2 diff --git a/wasa2il/core/static/bootstrap/js/bootstrap.js b/core/static/bootstrap/js/bootstrap.js similarity index 100% rename from wasa2il/core/static/bootstrap/js/bootstrap.js rename to core/static/bootstrap/js/bootstrap.js diff --git a/wasa2il/core/static/bootstrap/js/bootstrap.min.js b/core/static/bootstrap/js/bootstrap.min.js similarity index 100% rename from wasa2il/core/static/bootstrap/js/bootstrap.min.js rename to core/static/bootstrap/js/bootstrap.min.js diff --git a/wasa2il/core/static/bootstrap/js/npm.js b/core/static/bootstrap/js/npm.js similarity index 100% rename from wasa2il/core/static/bootstrap/js/npm.js rename to core/static/bootstrap/js/npm.js diff --git a/core/static/css/README.md b/core/static/css/README.md new file mode 100644 index 00000000..7f636504 --- /dev/null +++ b/core/static/css/README.md @@ -0,0 +1,10 @@ +Do not edit `application.css` directly. Instead, edit `application.scss` or +other dependencies and compile using SASS. For example: +``` +scss application.scss:application.css +``` + +For running on a loop during development, try: +``` +scss --watch application.scss:application.css +``` diff --git a/core/static/css/_constants.scss b/core/static/css/_constants.scss new file mode 100644 index 00000000..f7e20431 --- /dev/null +++ b/core/static/css/_constants.scss @@ -0,0 +1,46 @@ + +$piratepurple: #503087; + +$background-lightgray: #eee; +$background-menubar: #000; +$background-headerbar: #37215D; +$background-footer: #000; +$background-dropdown: #fff; + +$font-color-light: #fff; +$font-color-dark: #000; +$font-color-link: $piratepurple; +$font-color-link-hover: lighten($piratepurple, 20%); + +$font-heading: "Montserrat"; +$font-text: Arial, sans-serif; +$font-size: 14pt; + +$color-btn-light: #eee; +$color-btn-primary: lighten($piratepurple, 20%); + +$white: #fff; +$black: #000; +$task-purple: $piratepurple; +$task-grey: #eee; +$homepage-about-grey: $task-grey; +$task-background-grey: $task-grey; +$task-border-grey: darken($task-grey, 20%); +$purple: $piratepurple; +$grey-dark: darken($task-grey, 50%); +$cta-green: #4daf6f; + +$font-color-light: #fff; +$font-color-dark: #000; +$font-color-link: $piratepurple; +$font-color-link-hover: lighten($piratepurple, 5%); + +$font-heading: "Montserrat"; +$main-family: $font-heading; +$font-text: Arial, sans-serif; +$secondary-family: $font-text; + +$color-btn-light: #eee; +$color-btn-primary: lighten($piratepurple, 10%); + +$sidebar-width: 290px; diff --git a/core/static/css/_fonts.scss b/core/static/css/_fonts.scss new file mode 100644 index 00000000..87b9cb96 --- /dev/null +++ b/core/static/css/_fonts.scss @@ -0,0 +1,2 @@ +/* Downloaded from: https://fonts.googleapis.com/css?family=Montserrat:400,600,700,900&subset=cyrillic,cyrillic-ext,latin-ext */ +@import "downloads/fonts.googleapis.com-Montserrat.css"; diff --git a/core/static/css/_lists.scss b/core/static/css/_lists.scss new file mode 100644 index 00000000..38c4418e --- /dev/null +++ b/core/static/css/_lists.scss @@ -0,0 +1,34 @@ +.list-wrapper { + +} + +.list-item:first-of-type { + margin-top: 25px; + border-top: 1px solid #62388d; +} + +.list-item { + position: relative; + display: block; + min-height: 120px; + padding: 16px; + margin-bottom: 15px; + border-bottom: 1px solid #62388d; +} + +.list-item-agreement { + +} + +.list-item-agreement h3 { + margin-top: 0px; + padding-top: 0px; +} + +.list-item-agreement .date { +} + +.list-item-agreement .voteresults { + bottom: 10px; + margin-top: 10px; +} diff --git a/core/static/css/_media-mixins.scss b/core/static/css/_media-mixins.scss new file mode 100644 index 00000000..11e12745 --- /dev/null +++ b/core/static/css/_media-mixins.scss @@ -0,0 +1,69 @@ +/********************* +* Media query mixins * +*********************/ + +@mixin phone-portrait() { + @media screen and (orientation: portrait) { + @content; + } +} + +@mixin phone-landscape() { + @media screen and (orientation: landscape) { + @content; + } +} + +@mixin phone-lg() { + @media screen and (min-width: 480px) { + @content; + } +} + +@mixin phone-lg-portrait() { + @media screen and (min-width: 480px) and (orientation: portrait) { + @content; + } +} + +@mixin phone-lg-landscape() { + @media screen and (min-width: 480px) and (orientation: landscape) { + @content; + } +} + +@mixin tablet() { + @media screen and (min-width: 768px) { + @content; + } +} + +@mixin tablet-portrait() { + @media screen and (min-width: 768px) and (orientation: portrait) { + @content; + } +} + +@mixin tablet-landscape() { + @media screen and (min-width: 768px) and (orientation: landscape) { + @content; + } +} + +@mixin desktop() { + @media screen and (min-width: 1200px) { + @content; + } +} + +@mixin desktop-lg() { + @media screen and (min-width: 1600px) { + @content; + } +} + +@mixin desktop-xlg() { + @media screen and (min-width: 2000px) { + @content; + } +} diff --git a/core/static/css/_navbar.scss b/core/static/css/_navbar.scss new file mode 100644 index 00000000..cee94213 --- /dev/null +++ b/core/static/css/_navbar.scss @@ -0,0 +1,76 @@ +.navbar { + background-color: $background-menubar; + height: 70px; + padding-right: 50px; + + .btn, button { + color: $font-color-light; + font-size: 25px; + } + + .navbar-brand { margin: 0px; padding: 0px; color: #fff; } + .navbar-brand img { max-height: 70px; } + .nav > .li, .navbar .nav > li > a:hover, .navbar-nav li > a { + color: #fff; + font-family: $font-heading; + font-size: 18px; + font-weight: 300; + line-height: 25px; + margin-top: 10px; + margin-left: 20px; + } + .navbar-nav li a:hover, .navbar-nav li a:active, .navbar-nav li a:focus { + background-color: $background-menubar; + } + .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover { + background-image: none; + background-color: $background-dropdown; + border-bottom: 2px solid $piratepurple; + } + .dropdown-menu { + li > a, li a { + background-color: $background-dropdown; + border-bottom: 2px solid $background-dropdown; + color: $font-color-dark; + margin-left: 0; + } + background-color: $background-dropdown; + } +} + + +@media(min-width: 1200px) { + .subpolity-list-menu { + position: absolute; + margin-left: auto; + margin-right: auto; + width: 400%; + } +} + +.nav .subpolity-list-nav, .row .subpolity-list-nav { + list-style: none; + padding-left: 0px; + li { + text-indent: 20px; + line-height: 40px; + a { + display: block; + text-decoration: none; + color: $font-color-dark; + font-family: $font-heading; + font-size: 18px; + i, svg { + color: $piratepurple; + margin-right: 7px; + } + } + + a:hover, a:active, a:focus { + text-decoration: none; + color: #000; + background: #ddd; + } + + } +} \ No newline at end of file diff --git a/core/static/css/_profile.scss b/core/static/css/_profile.scss new file mode 100644 index 00000000..16529d8f --- /dev/null +++ b/core/static/css/_profile.scss @@ -0,0 +1,50 @@ + + +.profile { + width: 100%; + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; + align-content: stretch; +} + + + +@include tablet() { + .profile { + flex-direction: row; + + div { + h1 { + font-size: 30px; + color: #000; + } + } + + &-picture { + float: right; + width: 40%; + padding: 15px; + } + + &-bio { + width: 50%; + text-align: justify; + } + + &-thingsdone { + width: 50%; + padding-left: 20px; + } + + &-candidacies { + width: 100%; + } + + &-proposals { + + } + + } +} diff --git a/core/static/css/_sidebar.scss b/core/static/css/_sidebar.scss new file mode 100644 index 00000000..8ff38821 --- /dev/null +++ b/core/static/css/_sidebar.scss @@ -0,0 +1,113 @@ +#wrapper { + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; +} + +#sidebar-wrapper { + z-index: 1000; + position: fixed; + left: $sidebar-width; + width: 0; + height: 100%; + margin-left: -$sidebar-width; + overflow-y: auto; + background: $background-lightgray; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; +} + +#wrapper.toggled #sidebar-wrapper { + width: $sidebar-width; +} + +.sidebar-nav { + position: absolute; + top: 0; + width: $sidebar-width; + height: 100%; + margin: 0; + padding: 0; + list-style: none; +} + +.mobile-nav { + position: relative; + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid $background-lightgray; +} + + +.sidebar-nav .dropdown-menu { + width: $sidebar-width; +} + +.sidebar-nav .dropdown-menu li { + text-indent: 0px; + border-bottom: 1px solid $background-lightgray; +} + +.sidebar-nav .dropdown-menu li > a { + white-space: normal; + padding-left: 10px; +} + +.sidebar-nav li { + text-indent: 10px; + line-height: 40px; +} + +.sidebar-nav li a { + display: block; + text-decoration: none; + color: $font-color-dark; + font-family: $font-heading; + font-size: 18px; + i, svg { + color: $piratepurple; + margin-right: 7px; + } +} + +.sidebar-nav li a:hover, .sidebar-nav li.active a { + background: #ddd; +} + +.sidebar-nav li a:active, .sidebar-nav li a:focus { + background: #ddd; +} + +.sidebar-nav li > span.label { + margin-left: 10px; +} + +.navbar-toggle { + float: left; + margin-left: 12px; + margin-right: 6px; +} + +@media(max-width:767px) { + #sidebar-wrapper { + box-shadow: 1px 0px 4px #ccc; + } +} + +@media(min-width:768px) { + #wrapper { + padding-left: $sidebar-width + 20px; + } + #wrapper.toggled { + padding-left: $sidebar-width; + } + #sidebar-wrapper { + width: $sidebar-width; + } + #wrapper.toggled #sidebar-wrapper { + width: $sidebar-width; + } +} diff --git a/core/static/css/_task-applications.scss b/core/static/css/_task-applications.scss new file mode 100644 index 00000000..4a3a0168 --- /dev/null +++ b/core/static/css/_task-applications.scss @@ -0,0 +1,73 @@ + +.task-item { + margin-bottom: 10px; + border-bottom: 2px solid $background-lightgray; + padding-bottom: 10px; +} + +.task-stats { + margin-bottom: 15px; +} + +.task-applications-list { + position: relative; + flex: 0 0 auto; + width: 100%; + display: flex; + flex-flow: column; + justify-content: flex-start; + font-family: $main-family; + margin-bottom: 20px; +} + +@include tablet() { + .task-application { + width: 48%; + margin-bottom: 40px; + } +} + +@include desktop() { + .task-application { + width: 30%; + margin-right: 5%; + margin-bottom: 24px; + max-width: none; + + &:nth-child(3n) { + margin-right: 0; + } + } +} + + +.task-application { + background: $background-lightgray; + padding: 15px; + padding-top: 0px; + + .user-application-bar { + display: block; + width: 100%; + height: 10px; + overflow: visible; + white-space: nowrap; + margin-bottom: 20px; + + &__red { + display: inline-block; + background-color: #f00; + height: 10px; + } + &__green { + display: inline-block; + background-color: #070; + height: 10px; + } + &__yellow { + display: inline-block; + background-color: #ff0; + height: 10px; + } + } +} diff --git a/core/static/css/_task-list.scss b/core/static/css/_task-list.scss new file mode 100644 index 00000000..0fd56f53 --- /dev/null +++ b/core/static/css/_task-list.scss @@ -0,0 +1,23 @@ +.task-list { + display: flex; + flex-flow: column nowrap; + align-items: center; + width: 100%; +} + +@include tablet() { + .task-list { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + padding: 40px 0px 0px; + align-items: flex-start; + } +} +@include desktop() { + .task-list { + justify-content: flex-start; + padding: 50px 0px 16px; + + } +} diff --git a/core/static/css/_task-single.scss b/core/static/css/_task-single.scss new file mode 100644 index 00000000..58480750 --- /dev/null +++ b/core/static/css/_task-single.scss @@ -0,0 +1,172 @@ +.task { + width: 100%; + display: flex; + flex-flow: column; + font-family: $main-family; + &__info { + display: flex; + flex-flow: column; + background-color: $white; + } + &__title { + font-size: 24px; + font-weight: bold; + + } + &__info-title { + font-size: 18px; + text-align: left; + font-weight: bold; + margin: 38px 0px 20px; + } + &__request { + position: relative; + display: flex; + flex-flow: column; + padding: 10px 20px 10px; + background-color: $task-background-grey; + // border-bottom: 1px solid $task-border-grey; + + } + &__border { + &:after { + content: ''; + position: relative; + display: block; + width: 100%; + height: 1px; + margin: 10px auto 14px; + background-color: $task-border-grey; + } + &:before { + content: ''; + position: relative; + display: block; + width: 100%; + height: 1px; + margin: 10px auto 14px; + background-color: $task-border-grey; + } + } + &__category-name { + display: flex; + align-items: center; + justify-content: flex-start; + margin: 34px 0px 40px; + img { + width: 20px; + height: 20px; + margin-right: 12px; + } + } + &__request-title { + font-size: 16px; + } + &__line { + display: flex; + flex-wrap: wrap; + text-align: left; + + } + &__needs-answer { + font-size: 16px; + font-weight: bold; + margin-bottom: 12px; + } + &__why-me { + position: relative; + display: flex; + flex-flow: column; + background-color: $task-background-grey; + padding: 20px; + } + &__why-me-title { + margin: 52px 0px 26px; + font-size: 18px; + font-weight: bold; + } + &__why-me-messages { + margin-bottom: 20px; + } + &__why-me-textarea { + width: 100%; + height: 250px; + margin-bottom: 24px; + padding: 14px 20px; + font-size: 16px; + color: $grey-dark; + border: 1px solid $grey-dark; + resize: none; + + } + &__why-me-button { + display: block; + margin: 0 auto; + width: 100%; + } + +} + +@include tablet() { + .task { + &__info { + padding: 40px 44px; + } + &__title { + font-size: 32px; + margin-bottom: 18px; + } + &__info-title { + font-size: 24px; + margin: 46px 0px 34px; + } + &__needs { + padding: 0px 40px; + background-color: $white; + line-height: 1.13; + } + &__request { + padding: 10px 46px; + } + &__category-name { + } + &__why-me { + padding: 10px 46px; + margin: 40px 0px 15px; + } + &__why-me-title { + margin: 26px 0 12px; + } + &__tablet-margin { + margin-bottom: 36px; + } + &__why-me-textarea { + height: 125px; + } + &__why-me-button { + margin-bottom: 26px; + } + } +} + +@include desktop() { + .task { + flex-flow: row; + + &__info-title { + margin-bottom: 30px; + } + &__info { + width: 50%; + padding-left: 0px; + padding-top: 0px; + padding-right: 40px; + padding-bottom: 52px; + } + &__needs { + width: 50%; + padding: 0px; + + } + } +} diff --git a/core/static/css/_task.scss b/core/static/css/_task.scss new file mode 100644 index 00000000..56fe9606 --- /dev/null +++ b/core/static/css/_task.scss @@ -0,0 +1,82 @@ +.task-preview { + position: relative; + flex: 0 0 auto; + width: 100%; + max-width: 405px; + display: flex; + flex-flow: column; + justify-content: flex-start; + font-family: $main-family; + margin-bottom: 20px; + + &__link { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + &__title { + min-height: 52px; + display: flex; + justify-content: flex-start; + align-items: center; + padding-left: 20px; + font-weight: bold; + color: $white; + background-color: $task-purple; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + } + + &__info-wrapper { + flex-basis: 230px; + display: flex; + flex-flow: column; + justify-content: space-between; + padding: 30px 20px; + text-align: left; + border-left: 3px solid $task-grey; + border-right: 3px solid $task-grey; + + } + + &__main-info { + display: flex; + flex-flow: column; + } + + &__footer { + display: flex; + align-items: center; + justify-content: space-between; + height: 52px; + padding: 0px 20px 0px 20px; + font-size: 16px; + background-color: $task-grey; + color: $black; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } +} + +@include tablet() { + .task-preview { + width: 48%; + margin-bottom: 40px; + } +} + +@include desktop() { + .task-preview { + width: 30%; + margin-right: 5%; + margin-bottom: 24px; + max-width: none; + + &:nth-child(3n) { + margin-right: 0; + } + } +} diff --git a/core/static/css/_widgets.scss b/core/static/css/_widgets.scss new file mode 100644 index 00000000..ba9d58ad --- /dev/null +++ b/core/static/css/_widgets.scss @@ -0,0 +1,119 @@ + +.btn { + margin-bottom: 3px; + font-family: $font-heading; +} + +.btn-default { + background: none; + background-image: none; + background-color: $color-btn-light; +} + +.btn-primary { + background-image: none; + background-color: $color-btn-primary; +} + + +.hero { + margin: -20px; + margin-top: 0px; + left: 0px; + right: 0px; + max-height: 550px; + height: 70vh; + display: flex; + position: relative; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + flex-flow: column; + box-sizing: border-box; + justify-content: center; + align-items: center; + + .actions { + position: absolute; + top: 15px; + right: 20px; + } + + h1 { + color: $font-color-light; + font-size: 60px; + line-height: 1.1; + text-align: center; + margin-bottom: 16px; + } + + h2 { + margin-bottom: 60px; + font-size: 36px; + color: $font-color-light; + text-align: center; + vertical-align: baseline; + } +} + +.lesser-hero { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + display: flex; + flex-flow: row; + max-height: 430px; + padding: 70px 88px 0px 0px; + overflow-y: hidden; + background-color: $background-lightgray; + margin: -20px; + margin-top: 20px; + + .left { + background-repeat: no-repeat; + background-position: bottom; + max-width: 570px; + width: 100%; + height: auto; + margin-bottom: 0px; + } + .right { + padding-left: 120px; + display: flex; + flex-flow: column; + h1 { + font-size: 48px; + } + h2 { + font-size: 24px; + } + } +} + + +.toolbar { + border-bottom: 2px solid $background-lightgray; + padding-bottom: 15px; + margin-bottom: 10px; + display: flex; + flex-flow: row; + align-items: center; + width: 100%; + + &-small { + border-bottom: none; + padding-bottom: 0px; + } + + form { + display: inline-block; + } + + h1, h2 { + color: $font-color-dark; + flex: 0 0 60%; + } + .tools { + justify-content: flex-end; + align-content: flex-start; + } +} diff --git a/core/static/css/application.css b/core/static/css/application.css new file mode 100644 index 00000000..f9bd8a08 --- /dev/null +++ b/core/static/css/application.css @@ -0,0 +1,1103 @@ +/********************* +* Media query mixins * +*********************/ +/* Downloaded from: https://fonts.googleapis.com/css?family=Montserrat:400,600,700,900&subset=cyrillic,cyrillic-ext,latin-ext */ +@import "downloads/fonts.googleapis.com-Montserrat.css"; +body { + margin-top: 70px; + padding-bottom: 40px; + font-size: 14pt; +} + +section.content { + padding-top: 20px; + padding-bottom: 30px; +} + +footer { + z-index: 2000; + position: fixed; + bottom: 0px; + width: 100%; + display: block; + background: #000; + font-size: 9pt; + font-family: "Montserrat"; + height: 40px; + padding: 15px; + color: #fff; +} +footer a { + color: #fff; +} + +.navbar { + background-color: #000; + height: 70px; + padding-right: 50px; +} +.navbar .btn, .navbar button { + color: #fff; + font-size: 25px; +} +.navbar .navbar-brand { + margin: 0px; + padding: 0px; + color: #fff; +} +.navbar .navbar-brand img { + max-height: 70px; +} +.navbar .nav > .li, .navbar .navbar .nav > li > a:hover, .navbar .navbar-nav li > a { + color: #fff; + font-family: "Montserrat"; + font-size: 18px; + font-weight: 300; + line-height: 25px; + margin-top: 10px; + margin-left: 20px; +} +.navbar .navbar-nav li a:hover, .navbar .navbar-nav li a:active, .navbar .navbar-nav li a:focus { + background-color: #000; +} +.navbar .dropdown-menu > li > a:focus, .navbar .dropdown-menu > li > a:hover { + background-image: none; + background-color: #fff; + border-bottom: 2px solid #503087; +} +.navbar .dropdown-menu { + background-color: #fff; +} +.navbar .dropdown-menu li > a, .navbar .dropdown-menu li a { + background-color: #fff; + border-bottom: 2px solid #fff; + color: #000; + margin-left: 0; +} + +@media (min-width: 1200px) { + .subpolity-list-menu { + position: absolute; + margin-left: auto; + margin-right: auto; + width: 400%; + } +} +.nav .subpolity-list-nav, .row .subpolity-list-nav { + list-style: none; + padding-left: 0px; +} +.nav .subpolity-list-nav li, .row .subpolity-list-nav li { + text-indent: 20px; + line-height: 40px; +} +.nav .subpolity-list-nav li a, .row .subpolity-list-nav li a { + display: block; + text-decoration: none; + color: #000; + font-family: "Montserrat"; + font-size: 18px; +} +.nav .subpolity-list-nav li a i, .nav .subpolity-list-nav li a svg, .row .subpolity-list-nav li a i, .row .subpolity-list-nav li a svg { + color: #503087; + margin-right: 7px; +} +.nav .subpolity-list-nav li a:hover, .nav .subpolity-list-nav li a:active, .nav .subpolity-list-nav li a:focus, .row .subpolity-list-nav li a:hover, .row .subpolity-list-nav li a:active, .row .subpolity-list-nav li a:focus { + text-decoration: none; + color: #000; + background: #ddd; +} + +.btn { + margin-bottom: 3px; + font-family: "Montserrat"; +} + +.btn-default { + background: none; + background-image: none; + background-color: #eee; +} + +.btn-primary { + background-image: none; + background-color: #663dad; +} + +.hero { + margin: -20px; + margin-top: 0px; + left: 0px; + right: 0px; + max-height: 550px; + height: 70vh; + display: flex; + position: relative; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + flex-flow: column; + box-sizing: border-box; + justify-content: center; + align-items: center; +} +.hero .actions { + position: absolute; + top: 15px; + right: 20px; +} +.hero h1 { + color: #fff; + font-size: 60px; + line-height: 1.1; + text-align: center; + margin-bottom: 16px; +} +.hero h2 { + margin-bottom: 60px; + font-size: 36px; + color: #fff; + text-align: center; + vertical-align: baseline; +} + +.lesser-hero { + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + display: flex; + flex-flow: row; + max-height: 430px; + padding: 70px 88px 0px 0px; + overflow-y: hidden; + background-color: #eee; + margin: -20px; + margin-top: 20px; +} +.lesser-hero .left { + background-repeat: no-repeat; + background-position: bottom; + max-width: 570px; + width: 100%; + height: auto; + margin-bottom: 0px; +} +.lesser-hero .right { + padding-left: 120px; + display: flex; + flex-flow: column; +} +.lesser-hero .right h1 { + font-size: 48px; +} +.lesser-hero .right h2 { + font-size: 24px; +} + +.toolbar { + border-bottom: 2px solid #eee; + padding-bottom: 15px; + margin-bottom: 10px; + display: flex; + flex-flow: row; + align-items: center; + width: 100%; +} +.toolbar-small { + border-bottom: none; + padding-bottom: 0px; +} +.toolbar form { + display: inline-block; +} +.toolbar h1, .toolbar h2 { + color: #000; + flex: 0 0 60%; +} +.toolbar .tools { + justify-content: flex-end; + align-content: flex-start; +} + +#wrapper { + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; +} + +#sidebar-wrapper { + z-index: 1000; + position: fixed; + left: 290px; + width: 0; + height: 100%; + margin-left: -290px; + overflow-y: auto; + background: #eee; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; +} + +#wrapper.toggled #sidebar-wrapper { + width: 290px; +} + +.sidebar-nav { + position: absolute; + top: 0; + width: 290px; + height: 100%; + margin: 0; + padding: 0; + list-style: none; +} + +.mobile-nav { + position: relative; + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #eee; +} + +.sidebar-nav .dropdown-menu { + width: 290px; +} + +.sidebar-nav .dropdown-menu li { + text-indent: 0px; + border-bottom: 1px solid #eee; +} + +.sidebar-nav .dropdown-menu li > a { + white-space: normal; + padding-left: 10px; +} + +.sidebar-nav li { + text-indent: 10px; + line-height: 40px; +} + +.sidebar-nav li a { + display: block; + text-decoration: none; + color: #000; + font-family: "Montserrat"; + font-size: 18px; +} +.sidebar-nav li a i, .sidebar-nav li a svg { + color: #503087; + margin-right: 7px; +} + +.sidebar-nav li a:hover, .sidebar-nav li.active a { + background: #ddd; +} + +.sidebar-nav li a:active, .sidebar-nav li a:focus { + background: #ddd; +} + +.sidebar-nav li > span.label { + margin-left: 10px; +} + +.navbar-toggle { + float: left; + margin-left: 12px; + margin-right: 6px; +} + +@media (max-width: 767px) { + #sidebar-wrapper { + box-shadow: 1px 0px 4px #ccc; + } +} +@media (min-width: 768px) { + #wrapper { + padding-left: 310px; + } + + #wrapper.toggled { + padding-left: 290px; + } + + #sidebar-wrapper { + width: 290px; + } + + #wrapper.toggled #sidebar-wrapper { + width: 290px; + } +} +.list-item:first-of-type { + margin-top: 25px; + border-top: 1px solid #62388d; +} + +.list-item { + position: relative; + display: block; + min-height: 120px; + padding: 16px; + margin-bottom: 15px; + border-bottom: 1px solid #62388d; +} + +.list-item-agreement h3 { + margin-top: 0px; + padding-top: 0px; +} + +.list-item-agreement .voteresults { + bottom: 10px; + margin-top: 10px; +} + +.task-preview { + position: relative; + flex: 0 0 auto; + width: 100%; + max-width: 405px; + display: flex; + flex-flow: column; + justify-content: flex-start; + font-family: "Montserrat"; + margin-bottom: 20px; +} +.task-preview__link { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.task-preview__title { + min-height: 52px; + display: flex; + justify-content: flex-start; + align-items: center; + padding-left: 20px; + font-weight: bold; + color: #fff; + background-color: #503087; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} +.task-preview__info-wrapper { + flex-basis: 230px; + display: flex; + flex-flow: column; + justify-content: space-between; + padding: 30px 20px; + text-align: left; + border-left: 3px solid #eee; + border-right: 3px solid #eee; +} +.task-preview__main-info { + display: flex; + flex-flow: column; +} +.task-preview__footer { + display: flex; + align-items: center; + justify-content: space-between; + height: 52px; + padding: 0px 20px 0px 20px; + font-size: 16px; + background-color: #eee; + color: #000; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +} + +@media screen and (min-width: 768px) { + .task-preview { + width: 48%; + margin-bottom: 40px; + } +} +@media screen and (min-width: 1200px) { + .task-preview { + width: 30%; + margin-right: 5%; + margin-bottom: 24px; + max-width: none; + } + .task-preview:nth-child(3n) { + margin-right: 0; + } +} +.task-list { + display: flex; + flex-flow: column nowrap; + align-items: center; + width: 100%; +} + +@media screen and (min-width: 768px) { + .task-list { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + padding: 40px 0px 0px; + align-items: flex-start; + } +} +@media screen and (min-width: 1200px) { + .task-list { + justify-content: flex-start; + padding: 50px 0px 16px; + } +} +.task-item { + margin-bottom: 10px; + border-bottom: 2px solid #eee; + padding-bottom: 10px; +} + +.task-stats { + margin-bottom: 15px; +} + +.task-applications-list { + position: relative; + flex: 0 0 auto; + width: 100%; + display: flex; + flex-flow: column; + justify-content: flex-start; + font-family: "Montserrat"; + margin-bottom: 20px; +} + +@media screen and (min-width: 768px) { + .task-application { + width: 48%; + margin-bottom: 40px; + } +} +@media screen and (min-width: 1200px) { + .task-application { + width: 30%; + margin-right: 5%; + margin-bottom: 24px; + max-width: none; + } + .task-application:nth-child(3n) { + margin-right: 0; + } +} +.task-application { + background: #eee; + padding: 15px; + padding-top: 0px; +} +.task-application .user-application-bar { + display: block; + width: 100%; + height: 10px; + overflow: visible; + white-space: nowrap; + margin-bottom: 20px; +} +.task-application .user-application-bar__red { + display: inline-block; + background-color: #f00; + height: 10px; +} +.task-application .user-application-bar__green { + display: inline-block; + background-color: #070; + height: 10px; +} +.task-application .user-application-bar__yellow { + display: inline-block; + background-color: #ff0; + height: 10px; +} + +.task { + width: 100%; + display: flex; + flex-flow: column; + font-family: "Montserrat"; +} +.task__info { + display: flex; + flex-flow: column; + background-color: #fff; +} +.task__title { + font-size: 24px; + font-weight: bold; +} +.task__info-title { + font-size: 18px; + text-align: left; + font-weight: bold; + margin: 38px 0px 20px; +} +.task__request { + position: relative; + display: flex; + flex-flow: column; + padding: 10px 20px 10px; + background-color: #eee; +} +.task__border:after { + content: ""; + position: relative; + display: block; + width: 100%; + height: 1px; + margin: 10px auto 14px; + background-color: #bbbbbb; +} +.task__border:before { + content: ""; + position: relative; + display: block; + width: 100%; + height: 1px; + margin: 10px auto 14px; + background-color: #bbbbbb; +} +.task__category-name { + display: flex; + align-items: center; + justify-content: flex-start; + margin: 34px 0px 40px; +} +.task__category-name img { + width: 20px; + height: 20px; + margin-right: 12px; +} +.task__request-title { + font-size: 16px; +} +.task__line { + display: flex; + flex-wrap: wrap; + text-align: left; +} +.task__needs-answer { + font-size: 16px; + font-weight: bold; + margin-bottom: 12px; +} +.task__why-me { + position: relative; + display: flex; + flex-flow: column; + background-color: #eee; + padding: 20px; +} +.task__why-me-title { + margin: 52px 0px 26px; + font-size: 18px; + font-weight: bold; +} +.task__why-me-messages { + margin-bottom: 20px; +} +.task__why-me-textarea { + width: 100%; + height: 250px; + margin-bottom: 24px; + padding: 14px 20px; + font-size: 16px; + color: #6f6f6f; + border: 1px solid #6f6f6f; + resize: none; +} +.task__why-me-button { + display: block; + margin: 0 auto; + width: 100%; +} + +@media screen and (min-width: 768px) { + .task__info { + padding: 40px 44px; + } + .task__title { + font-size: 32px; + margin-bottom: 18px; + } + .task__info-title { + font-size: 24px; + margin: 46px 0px 34px; + } + .task__needs { + padding: 0px 40px; + background-color: #fff; + line-height: 1.13; + } + .task__request { + padding: 10px 46px; + } + .task__why-me { + padding: 10px 46px; + margin: 40px 0px 15px; + } + .task__why-me-title { + margin: 26px 0 12px; + } + .task__tablet-margin { + margin-bottom: 36px; + } + .task__why-me-textarea { + height: 125px; + } + .task__why-me-button { + margin-bottom: 26px; + } +} +@media screen and (min-width: 1200px) { + .task { + flex-flow: row; + } + .task__info-title { + margin-bottom: 30px; + } + .task__info { + width: 50%; + padding-left: 0px; + padding-top: 0px; + padding-right: 40px; + padding-bottom: 52px; + } + .task__needs { + width: 50%; + padding: 0px; + } +} +.profile { + width: 100%; + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; + align-content: stretch; +} + +@media screen and (min-width: 768px) { + .profile { + flex-direction: row; + } + .profile div h1 { + font-size: 30px; + color: #000; + } + .profile-picture { + float: right; + width: 40%; + padding: 15px; + } + .profile-bio { + width: 50%; + text-align: justify; + } + .profile-thingsdone { + width: 50%; + padding-left: 20px; + } + .profile-candidacies { + width: 100%; + } +} +/* Global search field */ +@media (max-width: 767px) { + .piratepurple { + background-color: #503087; + } + + .navbar .nav > .li, .navbar .nav > li > a:hover, .navbar .nav > li > a { + font-size: 20px; + margin: 7px 0; + } + + #polity-info { + display: none; + } +} +@media (max-width: 1024px) { + footer { + display: none; + } +} +@media (min-width: 768px) { + form.navbar-form { + padding: 0px; + max-width: 220px; + } + + #polity-info-toggle { + display: none; + } + + .mobile-nav { + display: none; + } +} +.img-negpad { + margin-top: -5px; + margin-bottom: -5px; +} + +/* For minimizing the width of elements while still not wrapping content */ +.minimize { + width: 1px; + white-space: nowrap; +} + +/* Dates that express whether they've passed or not */ +.not-expired { + color: #090; +} + +.expired { + color: #a00; +} + +/* Colors for expressing whether an issue was accepted or rejected */ +.accepted { + color: #090; +} + +.rejected { + color: #a00; +} + +.abstained { + color: grey; +} + +#ajax-status-messages { + position: absolute; + top: 55px; + width: 100%; +} + +#ajax-status-messages div { + max-width: 400px; + margin: 0 auto 0 auto; +} + +/* Headers */ +h1 { + font-family: "Montserrat"; + font-size: 200%; + font-weight: 700; + color: #503087; +} + +h2 { + font-family: "Montserrat"; + font-size: 170%; + font-weight: 500; + color: #000; +} + +h3 { + font-family: "Montserrat"; + font-size: 150%; + font-weight: 400; + color: #000; +} + +/* Links */ +a { + color: #503087; +} + +a:hover { + color: #5b379a; +} + +ul.errorlist { + margin-left: 4px; + list-style: none; + color: #f00; +} + +div#submit-working { + background: #bbb; +} + +#content { + visibility: hidden; + height: 350px; + margin-top: 2em; + margin-bottom: 2em; + border: 1px solid transparent; + border-radius: 5px; + -webkit-box-shadow: 3px 3px 13px rgba(50, 50, 50, 0.75); + -moz-box-shadow: 3px 3px 13px rgba(50, 50, 50, 0.75); + box-shadow: 3px 3px 13px rgba(50, 50, 50, 0.75); +} + +.document { + position: relative; +} + +/* Date fields, typically in a table. */ +.date { + text-align: right !important; + white-space: nowrap; +} + +/* Container for cancel URL, for use with cancel-buttons. */ +#cancel-url { + display: none; +} + +.dropdown-menu { + cursor: pointer; +} + +.nav-tabs > li.active > a { + border-left-width: 2px; + border-right-width: 2px; + border-top-width: 2px; +} + +/* Helpful color classes. */ +.icon-grey { + color: #c0d0c0; +} + +/* Election-related stuff */ +.candidates p { + padding: 10px; + border: 1px solid #ddd; +} + +.candidates li { + font-size: 15px; + padding: 5px; + border-top: 1px solid #ddd; +} + +.candidates li div:hover { + text-decoration: none; +} + +#candidates { + padding-left: 0; +} + +#election_candidates ul { + list-style-type: none; +} + +/* Issue-related stuff */ +.vote-yes { + float: right; +} + +.vote-image { + height: 50px; +} + +#votes { + list-style-type: decimal; + background: #fff; +} + +#vote { + padding-left: 7px; +} + +#votes li { + display: list-item; +} + +.vote-tip { + font-size: 15px; + padding: 15px 5px; + border-bottom: 1px solid #ccc; +} + +/* Displaying of document contents, differences between them and such. */ +#legal-text { + overflow: overlay; + margin-bottom: 10px; + padding: 12px 24px 12px 24px; + text-align: justify; +} + +#legal-text h1 { + font-weight: bold; + margin: 4px 0 4px 4px; +} + +#legal-text h2 { + font-weight: bold; + margin: 4px 0 4px 4px; +} + +#legal-text ol { + margin: 12px 0 0 24px; +} + +#legal-text li { + margin: 0 0 18px 0; + padding: 0 0 0 0; +} + +#legal-text-editor { + font-family: Courier New; + width: 100%; + height: 400px; +} + +#legal-text-editor-preview { + width: 100%; +} + +#legal-text-diff ins { + background-color: #afa !important; +} + +#legal-text-diff del { + background-color: #faa !important; +} + +.legal-text-container #siblings-container { + float: right; + padding: 0 0 0 12px; + border-bottom: 1px solid #e0e0e0; + border-left: 1px solid #e0e0e0; + background: #f8f8f8; +} + +.legal-text-container #siblings { + margin: 0; +} + +#diff-content { + overflow: inherit; + white-space: pre-wrap; +} + +#propose-change .comments { + width: 100%; +} + +#propose-change .comments textarea { + width: 100%; +} + +/* Instructions. */ +.instructions img.screenshot { + display: block; + max-width: 800px; + margin-top: 48px; + margin-bottom: 48px; + border: 1px solid #a0a0a0; +} + +.instructions p, .instructions ul { + max-width: 800px; + margin-top: 16px; + margin-bottom: 16px; +} + +.instructions ul li { + padding-bottom: 12px; +} + +.instructions .instructions-nav p { + margin: 0; +} + +.instructions .instructions-nav label { + float: left; + clear: both; + width: 100px; + margin: 0; + padding: 0; +} + +/* Profile. */ +.profile-document-data { + margin-top: 12px; +} + +.profile-document-data p.document { + font-size: 18px; + font-weight: bold; +} + +.profile-document-data p.documentcontent { + padding-left: 18px; +} + +.profile-document-data p.documentcontent small { + padding-left: 8px; +} + +.profile .img-polaroid { + float: right; + margin-bottom: 16px; + margin-left: 16px; + border: 1px solid #606060; +} + +.profile .tab-content { + padding-top: 16px; + text-align: justify; +} + +/* Comment section. */ +.comment { + display: grid; + overflow: overlay; + margin: 2px; + padding: 7px; + border: 1px solid #ddd; + border-radius: 3px; + background: #f7f7f7; + grid-template-columns: 40px 1fr; + grid-gap: 6px; +} + +.comment:nth-child(even) { + background: #eee; +} + +.comment > .profilepic { + display: inline-block; +} + +.comment > .content { + display: inline-block; +} + +.comment > .content > .created_by { + font-size: 9pt; + font-weight: bold; + display: block; +} + +.comment > .content > .created { + font-size: 8pt; + font-style: italic; + margin-top: 3px; + color: #333; +} + +.comment > .content > .text { + display: inline; +} + +.mb-2 { + margin-bottom: 2em; +} + +/*# sourceMappingURL=application.css.map */ diff --git a/core/static/css/application.css.map b/core/static/css/application.css.map new file mode 100644 index 00000000..63c5dce4 --- /dev/null +++ b/core/static/css/application.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["_media-mixins.scss","_fonts.scss","application.scss","_constants.scss","_navbar.scss","_widgets.scss","_sidebar.scss","_lists.scss","_task.scss","_task-list.scss","_task-applications.scss","_task-single.scss","_profile.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;ACAA;AACQ;ACGR;EACE;EACA;EACA,WCSU;;;ADNZ;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA,YCfkB;EDgBlB;EACA,aCca;EDbb;EACA;EACA,OCMiB;;ADLjB;EACE,OCIe;;;AChCnB;EACG,kBDGkB;ECFlB;EACA;;AAEA;EACE,OD0Bc;ECzBd;;AAGF;EAAgB;EAAa;EAAc;;AAC3C;EAAoB;;AACpB;EACE;EACA,aDuBU;ECtBV;EACA;EACA;EACA;EACA;;AAEF;EACE,kBDlBgB;;ACoBlB;EACE;EACA,kBDnBiB;ECoBjB;;AAEF;EAOE,kBD7BiB;;ACuBjB;EACE,kBDxBe;ECyBf;EACA;EACA;;;AAOP;EACE;IACE;IACA;IACA;IACA;;;AAIJ;EACE;EACA;;AACA;EACE;EACA;;AACA;EACE;EACA;EACA,OD1BY;EC2BZ,aDvBS;ECwBT;;AACA;EACE,OD9DO;EC+DP;;AAIJ;EACE;EACA;EACA;;;ACtEN;EACE;EACA,aFkCa;;;AE/Bf;EACE;EACA;EACA,kBFiCgB;;;AE9BlB;EACE;EACA,kBF6BkB;;;AEzBpB;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE,OFVe;EEWf;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA,OFpBe;EEqBf;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBF/DqB;EEgErB;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEF;EACE;EACA;EACA;;AACA;EACE;;AAEF;EACE;;;AAMN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAGF;EACE,OF9Ec;EE+Ed;;AAEF;EACE;EACA;;;ACpHJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA,MHmCc;EGlCd;EACA;EACA;EACA;EACA,YHZqB;EGarB;EACA;EACA;EACA;;;AAGF;EACE,OHsBc;;;AGnBhB;EACE;EACA;EACA,OHgBc;EGfd;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAIF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA,OHjCgB;EGkChB,aH9Ba;EG+Bb;;AACA;EACE,OHrEW;EGsEX;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;IACE;;;AAIJ;EACE;IACE;;;EAEF;IACE,cH3DY;;;EG6Dd;IACE,OH9DY;;;EGgEd;IACE,OHjEY;;;AIzChB;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAOF;EACE;EACA;;;AAMF;EACE;EACA;;;AChCF;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aL6BW;EK5BX;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA,OLLA;EKMA,kBL1BO;EK2BP;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA,kBLhCI;EKiCJ,OLnCA;EKoCA;EACA;;;ARxBJ;EQ6BA;IACI;IACA;;;ARbJ;EQkBA;IACI;IACA;IACA;IACA;;EAEA;IACI;;;AC9EZ;EACI;EACA;EACA;EACA;;;AT+BA;ES3BA;IACI;IACA;IACA;IACA;IACA;;;ATwCJ;ESpCA;IACI;IACA;;;AClBR;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA,aPmBa;EOlBb;;;AVgBE;EUZA;IACI;IACA;;;AV4BJ;EUvBA;IACI;IACA;IACA;IACA;;EAEA;IACI;;;AAMZ;EACE,YPzCqB;EO0CrB;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEF;EACE;EACA;EACA;;AAEF;EACE;EACA;EACA;;;ACrEN;EACI;EACA;EACA;EACA,aRiCW;;AQhCX;EACI;EACA;EACA,kBRaA;;AQXJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;EACA,kBRFI;;AQOJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA,kBRXO;;AQaX;EACI;EACA;EACA;EACA;EACA;EACA;EACA,kBRpBO;;AQuBf;EACI;EACA;EACA;EACA;;AACA;EACI;EACA;EACA;;AAGR;EACI;;AAEJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA,kBRvDI;EQwDJ;;AAEJ;EACI;EACA;EACA;;AAEJ;EACI;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA,ORnEI;EQoEJ;EACA;;AAGJ;EACI;EACA;EACA;;;AXrEJ;EW4EI;IACI;;EAEJ;IACI;IACA;;EAEJ;IACI;IACA;;EAEJ;IACI;IACA,kBRvGJ;IQwGI;;EAEJ;IACI;;EAIJ;IACI;IACA;;EAEJ;IACI;;EAEJ;IACI;;EAEJ;IACI;;EAEJ;IACI;;;AX7FR;EWmGA;IACI;;EAEA;IACI;;EAEJ;IACI;IACA;IACA;IACA;IACA;;EAEJ;IACI;IACA;;;ACrKZ;EACE;EACA;EACD;EACA;EACA;EACA;;;AZ2BG;EYrBF;IACE;;EAGE;IACE;IACA;;EAIJ;IACE;IACA;IACA;;EAGF;IACE;IACA;;EAGF;IACE;IACA;;EAGF;IACE;;;AVIN;AAEA;EACC;IAAgB,kBC/CF;;;EDgDd;IAAyE;IAAiB;;;EAC1F;IAAe;;;AAGhB;EACE;IAAS;;;AAGX;EACC;IAAmB;IAAc;;;EACjC;IAAsB;;;EACtB;IAAc;;;AAGf;EAAc;EAAkB;;;AAEhC;AACA;EAAY;EAAY;;;AAExB;AACA;EAAe;;;AACf;EAAW;;;AAEX;AACA;EAAY;;;AACZ;EAAY;;;AACZ;EAAa;;;AAEb;EAAwB;EAAoB;EAAW;;;AACvD;EAA4B;EAAkB;;;AAE9C;AACA;EACE,aC7Ca;ED8Cb;EACA;EACA,OCpFa;;;ADsFf;EACE,aCnDa;EDoDb;EACA;EACA;;;AAEF;EACE,aCzDa;ED0Db;EACA;EACA;;;AAGF;AACA;EAAI,OCpGW;;;ADqGf;EAAU,OCnEc;;;ADqExB;EAAe;EAAkB;EAAkB;;;AAEnD;EAAqB;;;AAErB;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EAAY;;;AAEZ;AACA;EAAQ;EAA8B;;;AAEtC;AACA;EAAc;;;AAEd;EAAiB;;;AAEjB;EACE;EACA;EACA;;;AAGF;AACA;EAAa;;;AAEb;AACA;EAAgB;EAAe;;;AAC/B;EAAiB;EAAiB;EAAc;;;AAChD;EAA2B;;;AAC3B;EAAc;;;AACd;EAA0B;;;AAE1B;AACA;EAAY;;;AACZ;EAAc;;;AACd;EAAS;EAA0B;;;AACnC;EAAQ;;;AACR;EAAY;;;AACZ;EAAY;EAAiB;EAAmB;;;AAEhD;AACA;EAAc;EAAmB;EAAqB;EAA8B;;;AACpF;EAAiB;EAAmB;;;AACpC;EAAiB;EAAmB;;;AACpC;EAAiB;;;AACjB;EAAiB;EAAoB;;;AACrC;EAAqB;EAA0B;EAAa;;;AAC5D;EAA6B;;;AAC7B;EAAuB;;;AACvB;EAAuB;;;AACvB;EAA4C;EAAc;EAAqB;EAAkC;EAAgC;;;AACjJ;EAAkC;;;AAClC;EAAgB;EAAmB;;;AACnC;EAA4B;;;AAC5B;EAAqC;;;AAErC;AACA;EAA+B;EAAgB;EAAkB;EAAkB;EAAqB;;;AACxG;EAAmC;EAAkB;EAAkB;;;AACvE;EAAsB;;;AACtB;EAAoC;;;AACpC;EAAwC;EAAa;EAAa;EAAc;EAAW;;;AAE3F;AACA;EAAyB;;;AACzB;EAAoC;EAAiB;;;AACrD;EAA2C;;;AAC3C;EAAiD;;;AACjD;EAAyB;EAAc;EAAqB;EAAmB;;;AAC/E;EAAwB;EAAmB;;;AAE3C;AACA;EAAW;EAAe;EAAmB;EAAa;EAAc;EAAwB;EAAoB;EAAqB;EAAiC;;;AAC1K;EAA2B;;;AAC3B;EAAyB;;;AACzB;EAAsB;;;AACtB;EAAoC;EAAgB;EAAmB;;;AACvE;EAAiC;EAAgB;EAAoB;EAAiB;;;AACtF;EAA8B;;;AAE9B;EAAQ","file":"application.css"} \ No newline at end of file diff --git a/core/static/css/application.scss b/core/static/css/application.scss new file mode 100644 index 00000000..29a4237c --- /dev/null +++ b/core/static/css/application.scss @@ -0,0 +1,195 @@ +@import "_media-mixins.scss"; +@import "_fonts.scss"; +@import "_constants.scss"; + +body { + margin-top: 70px; + padding-bottom: 40px; + font-size: $font-size; +} + +section.content { + padding-top: 20px; + padding-bottom: 30px; +} + +footer { + z-index: 2000; + position: fixed; + bottom: 0px; + width: 100%; + display: block; + background: $background-footer; + font-size: 9pt; + font-family: $font-heading; + height: 40px; + padding: 15px; + color: $font-color-light; + a { + color: $font-color-light; + } +} + +@import "_navbar.scss"; +@import "_widgets.scss"; +@import "_sidebar.scss"; +@import "_lists.scss"; + +@import "_task.scss"; +@import "_task-list.scss"; +@import "_task-applications.scss"; +@import "_task-single.scss"; + +@import "_profile.scss"; + + +/* Global search field */ + +@media (max-width: 767px) { + .piratepurple { background-color: $piratepurple; } + .navbar .nav > .li, .navbar .nav > li > a:hover, .navbar .nav > li > a { font-size: 20px; margin: 7px 0; } + #polity-info { display: none; } +} + +@media (max-width: 1024px) { + footer { display: none; } +} + +@media (min-width: 768px) { + form.navbar-form { padding: 0px; max-width: 220px; } + #polity-info-toggle { display: none; } + .mobile-nav { display: none; } +} + +.img-negpad { margin-top: -5px; margin-bottom: -5px; } + +/* For minimizing the width of elements while still not wrapping content */ +.minimize { width: 1px; white-space: nowrap; } + +/* Dates that express whether they've passed or not */ +.not-expired { color: #090; } +.expired { color: #a00; } + +/* Colors for expressing whether an issue was accepted or rejected */ +.accepted { color: #090; } +.rejected { color: #a00; } +.abstained { color: grey; } + +#ajax-status-messages { position: absolute; top: 55px; width: 100%; } +#ajax-status-messages div { max-width: 400px; margin: 0 auto 0 auto; } + +/* Headers */ +h1 { + font-family: $font-heading; + font-size: 200%; + font-weight: 700; + color: $piratepurple; +} +h2 { + font-family: $font-heading; + font-size: 170%; + font-weight: 500; + color: #000; +} +h3 { + font-family: $font-heading; + font-size: 150%; + font-weight: 400; + color: #000; +} + +/* Links */ +a { color: $font-color-link; } +a:hover { color: $font-color-link-hover; } + +ul.errorlist { margin-left: 4px; list-style: none; color: #f00; } + +div#submit-working { background: #bbb; } + +#content { + visibility: hidden; + height: 350px; + margin-top: 2em; + margin-bottom: 2em; + border: 1px solid transparent; + border-radius: 5px; + -webkit-box-shadow: 3px 3px 13px rgba(50, 50, 50, .75); + -moz-box-shadow: 3px 3px 13px rgba(50, 50, 50, .75); + box-shadow: 3px 3px 13px rgba(50, 50, 50, .75); +} + +.document { position: relative; } + +/* Date fields, typically in a table. */ +.date { text-align: right !important; white-space: nowrap; } + +/* Container for cancel URL, for use with cancel-buttons. */ +#cancel-url { display: none; } + +.dropdown-menu { cursor: pointer; } + +.nav-tabs>li.active>a { + border-left-width: 2px; + border-right-width: 2px; + border-top-width: 2px; +} + +/* Helpful color classes. */ +.icon-grey { color: #c0d0c0; } + +/* Election-related stuff */ +.candidates p { padding: 10px; border: 1px solid #ddd; } +.candidates li { font-size: 15px; padding: 5px; border-top: 1px solid #ddd; } +.candidates li div:hover { text-decoration: none; } +#candidates { padding-left: 0; } +#election_candidates ul { list-style-type: none; } + +/* Issue-related stuff */ +.vote-yes { float: right; } +.vote-image { height: 50px; } +#votes { list-style-type: decimal; background: #fff; } +#vote { padding-left: 7px; } +#votes li { display: list-item; } +.vote-tip { font-size: 15px; padding: 15px 5px; border-bottom: 1px solid #ccc; } + +/* Displaying of document contents, differences between them and such. */ +#legal-text { overflow: overlay; margin-bottom: 10px; padding: 12px 24px 12px 24px; text-align: justify; } +#legal-text h1 { font-weight: bold; margin: 4px 0 4px 4px; } +#legal-text h2 { font-weight: bold; margin: 4px 0 4px 4px; } +#legal-text ol { margin: 12px 0 0 24px; } +#legal-text li { margin: 0 0 18px 0; padding: 0 0 0 0; } +#legal-text-editor { font-family: Courier New; width: 100%; height: 400px; } +#legal-text-editor-preview { width: 100%; } +#legal-text-diff ins { background-color: #afa !important; } +#legal-text-diff del { background-color: #faa !important; } +.legal-text-container #siblings-container { float: right; padding: 0 0 0 12px; border-bottom: 1px solid #e0e0e0; border-left: 1px solid #e0e0e0; background: #f8f8f8; } +.legal-text-container #siblings { margin: 0; } +#diff-content { overflow: inherit; white-space: pre-wrap; } +#propose-change .comments { width: 100%; } +#propose-change .comments textarea { width: 100%; } + +/* Instructions. */ +.instructions img.screenshot { display: block; max-width: 800px; margin-top: 48px; margin-bottom: 48px; border: 1px solid #a0a0a0; } +.instructions p,.instructions ul { max-width: 800px; margin-top: 16px; margin-bottom: 16px; } +.instructions ul li { padding-bottom: 12px; } +.instructions .instructions-nav p { margin: 0; } +.instructions .instructions-nav label { float: left; clear: both; width: 100px; margin: 0; padding: 0; } + +/* Profile. */ +.profile-document-data { margin-top: 12px; } +.profile-document-data p.document { font-size: 18px; font-weight: bold; } +.profile-document-data p.documentcontent { padding-left: 18px; } +.profile-document-data p.documentcontent small { padding-left: 8px; } +.profile .img-polaroid { float: right; margin-bottom: 16px; margin-left: 16px; border: 1px solid #606060; } +.profile .tab-content { padding-top: 16px; text-align: justify; } + +/* Comment section. */ +.comment { display: grid; overflow: overlay; margin: 2px; padding: 7px; border: 1px solid #ddd; border-radius: 3px; background: #f7f7f7; grid-template-columns: 40px 1fr; grid-gap: 6px; } +.comment:nth-child(even) { background: #eee; } +.comment > .profilepic { display: inline-block; } +.comment > .content { display: inline-block; } +.comment > .content > .created_by { font-size: 9pt; font-weight: bold; display: block; } +.comment > .content > .created { font-size: 8pt; font-style: italic; margin-top: 3px; color: #333; } +.comment > .content > .text { display: inline; } + +.mb-2 { margin-bottom: 2em; } diff --git a/core/static/css/downloads/fonts.googleapis.com-Montserrat.css b/core/static/css/downloads/fonts.googleapis.com-Montserrat.css new file mode 100644 index 00000000..d52004a2 --- /dev/null +++ b/core/static/css/downloads/fonts.googleapis.com-Montserrat.css @@ -0,0 +1,160 @@ +/* cyrillic-ext */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 400; + src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://fonts.gstatic.com/s/montserrat/v12/JTUSjIg1_i6t8kCHKm459WRhyzbi.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 400; + src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://fonts.gstatic.com/s/montserrat/v12/JTUSjIg1_i6t8kCHKm459W1hyzbi.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* vietnamese */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 400; + src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://fonts.gstatic.com/s/montserrat/v12/JTUSjIg1_i6t8kCHKm459WZhyzbi.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 400; + src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://fonts.gstatic.com/s/montserrat/v12/JTUSjIg1_i6t8kCHKm459Wdhyzbi.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 400; + src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://fonts.gstatic.com/s/montserrat/v12/JTUSjIg1_i6t8kCHKm459Wlhyw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 600; + src: local('Montserrat SemiBold'), local('Montserrat-SemiBold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_bZF3gTD_u50.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 600; + src: local('Montserrat SemiBold'), local('Montserrat-SemiBold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_bZF3g3D_u50.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* vietnamese */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 600; + src: local('Montserrat SemiBold'), local('Montserrat-SemiBold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_bZF3gbD_u50.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 600; + src: local('Montserrat SemiBold'), local('Montserrat-SemiBold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_bZF3gfD_u50.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 600; + src: local('Montserrat SemiBold'), local('Montserrat-SemiBold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_bZF3gnD_g.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 700; + src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_dJE3gTD_u50.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 700; + src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_dJE3g3D_u50.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* vietnamese */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 700; + src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_dJE3gbD_u50.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 700; + src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_dJE3gfD_u50.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 700; + src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_dJE3gnD_g.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 900; + src: local('Montserrat Black'), local('Montserrat-Black'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_epG3gTD_u50.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 900; + src: local('Montserrat Black'), local('Montserrat-Black'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_epG3g3D_u50.woff2) format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* vietnamese */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 900; + src: local('Montserrat Black'), local('Montserrat-Black'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_epG3gbD_u50.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 900; + src: local('Montserrat Black'), local('Montserrat-Black'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_epG3gfD_u50.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Montserrat'; + font-style: normal; + font-weight: 900; + src: local('Montserrat Black'), local('Montserrat-Black'), url(https://fonts.gstatic.com/s/montserrat/v12/JTURjIg1_i6t8kCHKm45_epG3gnD_g.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/core/static/fontawesome/LICENSE.txt b/core/static/fontawesome/LICENSE.txt new file mode 100644 index 00000000..28c1c4bc --- /dev/null +++ b/core/static/fontawesome/LICENSE.txt @@ -0,0 +1,34 @@ +Font Awesome Free License +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OLF license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OLF, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/core/static/fontawesome/VERSION b/core/static/fontawesome/VERSION new file mode 100644 index 00000000..2d6c0bcf --- /dev/null +++ b/core/static/fontawesome/VERSION @@ -0,0 +1 @@ +5.0.4 diff --git a/core/static/fontawesome/css/fa-brands.css b/core/static/fontawesome/css/fa-brands.css new file mode 100644 index 00000000..d4775f3f --- /dev/null +++ b/core/static/fontawesome/css/fa-brands.css @@ -0,0 +1,13 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@font-face { + font-family: 'Font Awesome 5 Brands'; + font-style: normal; + font-weight: normal; + src: url("../webfonts/fa-brands-400.eot"); + src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } + +.fab { + font-family: 'Font Awesome 5 Brands'; } diff --git a/core/static/fontawesome/css/fa-brands.min.css b/core/static/fontawesome/css/fa-brands.min.css new file mode 100644 index 00000000..244a01ef --- /dev/null +++ b/core/static/fontawesome/css/fa-brands.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@font-face{font-family:Font Awesome\ 5 Brands;font-style:normal;font-weight:400;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:Font Awesome\ 5 Brands} \ No newline at end of file diff --git a/core/static/fontawesome/css/fa-regular.css b/core/static/fontawesome/css/fa-regular.css new file mode 100644 index 00000000..a811f1aa --- /dev/null +++ b/core/static/fontawesome/css/fa-regular.css @@ -0,0 +1,14 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 400; + src: url("../webfonts/fa-regular-400.eot"); + src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } + +.far { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } diff --git a/core/static/fontawesome/css/fa-regular.min.css b/core/static/fontawesome/css/fa-regular.min.css new file mode 100644 index 00000000..c15366b7 --- /dev/null +++ b/core/static/fontawesome/css/fa-regular.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:Font Awesome\ 5 Free;font-weight:400} \ No newline at end of file diff --git a/core/static/fontawesome/css/fa-solid.css b/core/static/fontawesome/css/fa-solid.css new file mode 100644 index 00000000..db603ff7 --- /dev/null +++ b/core/static/fontawesome/css/fa-solid.css @@ -0,0 +1,15 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 900; + src: url("../webfonts/fa-solid-900.eot"); + src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } + +.fa, +.fas { + font-family: 'Font Awesome 5 Free'; + font-weight: 900; } diff --git a/core/static/fontawesome/css/fa-solid.min.css b/core/static/fontawesome/css/fa-solid.min.css new file mode 100644 index 00000000..99a88fc5 --- /dev/null +++ b/core/static/fontawesome/css/fa-solid.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:900;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:Font Awesome\ 5 Free;font-weight:900} \ No newline at end of file diff --git a/core/static/fontawesome/css/fontawesome-all.css b/core/static/fontawesome/css/fontawesome-all.css new file mode 100644 index 00000000..571212a1 --- /dev/null +++ b/core/static/fontawesome/css/fontawesome-all.css @@ -0,0 +1,2609 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa, +.fas, +.far, +.fal, +.fab { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; } + +.fa-lg { + font-size: 1.33333em; + line-height: 0.75em; + vertical-align: -.0667em; } + +.fa-xs { + font-size: .75em; } + +.fa-sm { + font-size: .875em; } + +.fa-1x { + font-size: 1em; } + +.fa-2x { + font-size: 2em; } + +.fa-3x { + font-size: 3em; } + +.fa-4x { + font-size: 4em; } + +.fa-5x { + font-size: 5em; } + +.fa-6x { + font-size: 6em; } + +.fa-7x { + font-size: 7em; } + +.fa-8x { + font-size: 8em; } + +.fa-9x { + font-size: 9em; } + +.fa-10x { + font-size: 10em; } + +.fa-fw { + text-align: center; + width: 1.25em; } + +.fa-ul { + list-style-type: none; + margin-left: 2.5em; + padding-left: 0; } + .fa-ul > li { + position: relative; } + +.fa-li { + left: -2em; + position: absolute; + text-align: center; + width: 2em; + line-height: inherit; } + +.fa-border { + border: solid 0.08em #eee; + border-radius: .1em; + padding: .2em .25em .15em; } + +.fa-pull-left { + float: left; } + +.fa-pull-right { + float: right; } + +.fa.fa-pull-left, +.fas.fa-pull-left, +.far.fa-pull-left, +.fal.fa-pull-left, +.fab.fa-pull-left { + margin-right: .3em; } + +.fa.fa-pull-right, +.fas.fa-pull-right, +.far.fa-pull-right, +.fal.fa-pull-right, +.fab.fa-pull-right { + margin-left: .3em; } + +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; } + +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); } + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +.fa-flip-horizontal.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); } + +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + -webkit-filter: none; + filter: none; } + +.fa-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2em; } + +.fa-stack-1x, +.fa-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: #fff; } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ +.fa-500px:before { + content: "\f26e"; } + +.fa-accessible-icon:before { + content: "\f368"; } + +.fa-accusoft:before { + content: "\f369"; } + +.fa-address-book:before { + content: "\f2b9"; } + +.fa-address-card:before { + content: "\f2bb"; } + +.fa-adjust:before { + content: "\f042"; } + +.fa-adn:before { + content: "\f170"; } + +.fa-adversal:before { + content: "\f36a"; } + +.fa-affiliatetheme:before { + content: "\f36b"; } + +.fa-algolia:before { + content: "\f36c"; } + +.fa-align-center:before { + content: "\f037"; } + +.fa-align-justify:before { + content: "\f039"; } + +.fa-align-left:before { + content: "\f036"; } + +.fa-align-right:before { + content: "\f038"; } + +.fa-amazon:before { + content: "\f270"; } + +.fa-amazon-pay:before { + content: "\f42c"; } + +.fa-ambulance:before { + content: "\f0f9"; } + +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; } + +.fa-amilia:before { + content: "\f36d"; } + +.fa-anchor:before { + content: "\f13d"; } + +.fa-android:before { + content: "\f17b"; } + +.fa-angellist:before { + content: "\f209"; } + +.fa-angle-double-down:before { + content: "\f103"; } + +.fa-angle-double-left:before { + content: "\f100"; } + +.fa-angle-double-right:before { + content: "\f101"; } + +.fa-angle-double-up:before { + content: "\f102"; } + +.fa-angle-down:before { + content: "\f107"; } + +.fa-angle-left:before { + content: "\f104"; } + +.fa-angle-right:before { + content: "\f105"; } + +.fa-angle-up:before { + content: "\f106"; } + +.fa-angrycreative:before { + content: "\f36e"; } + +.fa-angular:before { + content: "\f420"; } + +.fa-app-store:before { + content: "\f36f"; } + +.fa-app-store-ios:before { + content: "\f370"; } + +.fa-apper:before { + content: "\f371"; } + +.fa-apple:before { + content: "\f179"; } + +.fa-apple-pay:before { + content: "\f415"; } + +.fa-archive:before { + content: "\f187"; } + +.fa-arrow-alt-circle-down:before { + content: "\f358"; } + +.fa-arrow-alt-circle-left:before { + content: "\f359"; } + +.fa-arrow-alt-circle-right:before { + content: "\f35a"; } + +.fa-arrow-alt-circle-up:before { + content: "\f35b"; } + +.fa-arrow-circle-down:before { + content: "\f0ab"; } + +.fa-arrow-circle-left:before { + content: "\f0a8"; } + +.fa-arrow-circle-right:before { + content: "\f0a9"; } + +.fa-arrow-circle-up:before { + content: "\f0aa"; } + +.fa-arrow-down:before { + content: "\f063"; } + +.fa-arrow-left:before { + content: "\f060"; } + +.fa-arrow-right:before { + content: "\f061"; } + +.fa-arrow-up:before { + content: "\f062"; } + +.fa-arrows-alt:before { + content: "\f0b2"; } + +.fa-arrows-alt-h:before { + content: "\f337"; } + +.fa-arrows-alt-v:before { + content: "\f338"; } + +.fa-assistive-listening-systems:before { + content: "\f2a2"; } + +.fa-asterisk:before { + content: "\f069"; } + +.fa-asymmetrik:before { + content: "\f372"; } + +.fa-at:before { + content: "\f1fa"; } + +.fa-audible:before { + content: "\f373"; } + +.fa-audio-description:before { + content: "\f29e"; } + +.fa-autoprefixer:before { + content: "\f41c"; } + +.fa-avianex:before { + content: "\f374"; } + +.fa-aviato:before { + content: "\f421"; } + +.fa-aws:before { + content: "\f375"; } + +.fa-backward:before { + content: "\f04a"; } + +.fa-balance-scale:before { + content: "\f24e"; } + +.fa-ban:before { + content: "\f05e"; } + +.fa-bandcamp:before { + content: "\f2d5"; } + +.fa-barcode:before { + content: "\f02a"; } + +.fa-bars:before { + content: "\f0c9"; } + +.fa-bath:before { + content: "\f2cd"; } + +.fa-battery-empty:before { + content: "\f244"; } + +.fa-battery-full:before { + content: "\f240"; } + +.fa-battery-half:before { + content: "\f242"; } + +.fa-battery-quarter:before { + content: "\f243"; } + +.fa-battery-three-quarters:before { + content: "\f241"; } + +.fa-bed:before { + content: "\f236"; } + +.fa-beer:before { + content: "\f0fc"; } + +.fa-behance:before { + content: "\f1b4"; } + +.fa-behance-square:before { + content: "\f1b5"; } + +.fa-bell:before { + content: "\f0f3"; } + +.fa-bell-slash:before { + content: "\f1f6"; } + +.fa-bicycle:before { + content: "\f206"; } + +.fa-bimobject:before { + content: "\f378"; } + +.fa-binoculars:before { + content: "\f1e5"; } + +.fa-birthday-cake:before { + content: "\f1fd"; } + +.fa-bitbucket:before { + content: "\f171"; } + +.fa-bitcoin:before { + content: "\f379"; } + +.fa-bity:before { + content: "\f37a"; } + +.fa-black-tie:before { + content: "\f27e"; } + +.fa-blackberry:before { + content: "\f37b"; } + +.fa-blind:before { + content: "\f29d"; } + +.fa-blogger:before { + content: "\f37c"; } + +.fa-blogger-b:before { + content: "\f37d"; } + +.fa-bluetooth:before { + content: "\f293"; } + +.fa-bluetooth-b:before { + content: "\f294"; } + +.fa-bold:before { + content: "\f032"; } + +.fa-bolt:before { + content: "\f0e7"; } + +.fa-bomb:before { + content: "\f1e2"; } + +.fa-book:before { + content: "\f02d"; } + +.fa-bookmark:before { + content: "\f02e"; } + +.fa-braille:before { + content: "\f2a1"; } + +.fa-briefcase:before { + content: "\f0b1"; } + +.fa-btc:before { + content: "\f15a"; } + +.fa-bug:before { + content: "\f188"; } + +.fa-building:before { + content: "\f1ad"; } + +.fa-bullhorn:before { + content: "\f0a1"; } + +.fa-bullseye:before { + content: "\f140"; } + +.fa-buromobelexperte:before { + content: "\f37f"; } + +.fa-bus:before { + content: "\f207"; } + +.fa-buysellads:before { + content: "\f20d"; } + +.fa-calculator:before { + content: "\f1ec"; } + +.fa-calendar:before { + content: "\f133"; } + +.fa-calendar-alt:before { + content: "\f073"; } + +.fa-calendar-check:before { + content: "\f274"; } + +.fa-calendar-minus:before { + content: "\f272"; } + +.fa-calendar-plus:before { + content: "\f271"; } + +.fa-calendar-times:before { + content: "\f273"; } + +.fa-camera:before { + content: "\f030"; } + +.fa-camera-retro:before { + content: "\f083"; } + +.fa-car:before { + content: "\f1b9"; } + +.fa-caret-down:before { + content: "\f0d7"; } + +.fa-caret-left:before { + content: "\f0d9"; } + +.fa-caret-right:before { + content: "\f0da"; } + +.fa-caret-square-down:before { + content: "\f150"; } + +.fa-caret-square-left:before { + content: "\f191"; } + +.fa-caret-square-right:before { + content: "\f152"; } + +.fa-caret-square-up:before { + content: "\f151"; } + +.fa-caret-up:before { + content: "\f0d8"; } + +.fa-cart-arrow-down:before { + content: "\f218"; } + +.fa-cart-plus:before { + content: "\f217"; } + +.fa-cc-amazon-pay:before { + content: "\f42d"; } + +.fa-cc-amex:before { + content: "\f1f3"; } + +.fa-cc-apple-pay:before { + content: "\f416"; } + +.fa-cc-diners-club:before { + content: "\f24c"; } + +.fa-cc-discover:before { + content: "\f1f2"; } + +.fa-cc-jcb:before { + content: "\f24b"; } + +.fa-cc-mastercard:before { + content: "\f1f1"; } + +.fa-cc-paypal:before { + content: "\f1f4"; } + +.fa-cc-stripe:before { + content: "\f1f5"; } + +.fa-cc-visa:before { + content: "\f1f0"; } + +.fa-centercode:before { + content: "\f380"; } + +.fa-certificate:before { + content: "\f0a3"; } + +.fa-chart-area:before { + content: "\f1fe"; } + +.fa-chart-bar:before { + content: "\f080"; } + +.fa-chart-line:before { + content: "\f201"; } + +.fa-chart-pie:before { + content: "\f200"; } + +.fa-check:before { + content: "\f00c"; } + +.fa-check-circle:before { + content: "\f058"; } + +.fa-check-square:before { + content: "\f14a"; } + +.fa-chevron-circle-down:before { + content: "\f13a"; } + +.fa-chevron-circle-left:before { + content: "\f137"; } + +.fa-chevron-circle-right:before { + content: "\f138"; } + +.fa-chevron-circle-up:before { + content: "\f139"; } + +.fa-chevron-down:before { + content: "\f078"; } + +.fa-chevron-left:before { + content: "\f053"; } + +.fa-chevron-right:before { + content: "\f054"; } + +.fa-chevron-up:before { + content: "\f077"; } + +.fa-child:before { + content: "\f1ae"; } + +.fa-chrome:before { + content: "\f268"; } + +.fa-circle:before { + content: "\f111"; } + +.fa-circle-notch:before { + content: "\f1ce"; } + +.fa-clipboard:before { + content: "\f328"; } + +.fa-clock:before { + content: "\f017"; } + +.fa-clone:before { + content: "\f24d"; } + +.fa-closed-captioning:before { + content: "\f20a"; } + +.fa-cloud:before { + content: "\f0c2"; } + +.fa-cloud-download-alt:before { + content: "\f381"; } + +.fa-cloud-upload-alt:before { + content: "\f382"; } + +.fa-cloudscale:before { + content: "\f383"; } + +.fa-cloudsmith:before { + content: "\f384"; } + +.fa-cloudversify:before { + content: "\f385"; } + +.fa-code:before { + content: "\f121"; } + +.fa-code-branch:before { + content: "\f126"; } + +.fa-codepen:before { + content: "\f1cb"; } + +.fa-codiepie:before { + content: "\f284"; } + +.fa-coffee:before { + content: "\f0f4"; } + +.fa-cog:before { + content: "\f013"; } + +.fa-cogs:before { + content: "\f085"; } + +.fa-columns:before { + content: "\f0db"; } + +.fa-comment:before { + content: "\f075"; } + +.fa-comment-alt:before { + content: "\f27a"; } + +.fa-comments:before { + content: "\f086"; } + +.fa-compass:before { + content: "\f14e"; } + +.fa-compress:before { + content: "\f066"; } + +.fa-connectdevelop:before { + content: "\f20e"; } + +.fa-contao:before { + content: "\f26d"; } + +.fa-copy:before { + content: "\f0c5"; } + +.fa-copyright:before { + content: "\f1f9"; } + +.fa-cpanel:before { + content: "\f388"; } + +.fa-creative-commons:before { + content: "\f25e"; } + +.fa-credit-card:before { + content: "\f09d"; } + +.fa-crop:before { + content: "\f125"; } + +.fa-crosshairs:before { + content: "\f05b"; } + +.fa-css3:before { + content: "\f13c"; } + +.fa-css3-alt:before { + content: "\f38b"; } + +.fa-cube:before { + content: "\f1b2"; } + +.fa-cubes:before { + content: "\f1b3"; } + +.fa-cut:before { + content: "\f0c4"; } + +.fa-cuttlefish:before { + content: "\f38c"; } + +.fa-d-and-d:before { + content: "\f38d"; } + +.fa-dashcube:before { + content: "\f210"; } + +.fa-database:before { + content: "\f1c0"; } + +.fa-deaf:before { + content: "\f2a4"; } + +.fa-delicious:before { + content: "\f1a5"; } + +.fa-deploydog:before { + content: "\f38e"; } + +.fa-deskpro:before { + content: "\f38f"; } + +.fa-desktop:before { + content: "\f108"; } + +.fa-deviantart:before { + content: "\f1bd"; } + +.fa-digg:before { + content: "\f1a6"; } + +.fa-digital-ocean:before { + content: "\f391"; } + +.fa-discord:before { + content: "\f392"; } + +.fa-discourse:before { + content: "\f393"; } + +.fa-dochub:before { + content: "\f394"; } + +.fa-docker:before { + content: "\f395"; } + +.fa-dollar-sign:before { + content: "\f155"; } + +.fa-dot-circle:before { + content: "\f192"; } + +.fa-download:before { + content: "\f019"; } + +.fa-draft2digital:before { + content: "\f396"; } + +.fa-dribbble:before { + content: "\f17d"; } + +.fa-dribbble-square:before { + content: "\f397"; } + +.fa-dropbox:before { + content: "\f16b"; } + +.fa-drupal:before { + content: "\f1a9"; } + +.fa-dyalog:before { + content: "\f399"; } + +.fa-earlybirds:before { + content: "\f39a"; } + +.fa-edge:before { + content: "\f282"; } + +.fa-edit:before { + content: "\f044"; } + +.fa-eject:before { + content: "\f052"; } + +.fa-elementor:before { + content: "\f430"; } + +.fa-ellipsis-h:before { + content: "\f141"; } + +.fa-ellipsis-v:before { + content: "\f142"; } + +.fa-ember:before { + content: "\f423"; } + +.fa-empire:before { + content: "\f1d1"; } + +.fa-envelope:before { + content: "\f0e0"; } + +.fa-envelope-open:before { + content: "\f2b6"; } + +.fa-envelope-square:before { + content: "\f199"; } + +.fa-envira:before { + content: "\f299"; } + +.fa-eraser:before { + content: "\f12d"; } + +.fa-erlang:before { + content: "\f39d"; } + +.fa-ethereum:before { + content: "\f42e"; } + +.fa-etsy:before { + content: "\f2d7"; } + +.fa-euro-sign:before { + content: "\f153"; } + +.fa-exchange-alt:before { + content: "\f362"; } + +.fa-exclamation:before { + content: "\f12a"; } + +.fa-exclamation-circle:before { + content: "\f06a"; } + +.fa-exclamation-triangle:before { + content: "\f071"; } + +.fa-expand:before { + content: "\f065"; } + +.fa-expand-arrows-alt:before { + content: "\f31e"; } + +.fa-expeditedssl:before { + content: "\f23e"; } + +.fa-external-link-alt:before { + content: "\f35d"; } + +.fa-external-link-square-alt:before { + content: "\f360"; } + +.fa-eye:before { + content: "\f06e"; } + +.fa-eye-dropper:before { + content: "\f1fb"; } + +.fa-eye-slash:before { + content: "\f070"; } + +.fa-facebook:before { + content: "\f09a"; } + +.fa-facebook-f:before { + content: "\f39e"; } + +.fa-facebook-messenger:before { + content: "\f39f"; } + +.fa-facebook-square:before { + content: "\f082"; } + +.fa-fast-backward:before { + content: "\f049"; } + +.fa-fast-forward:before { + content: "\f050"; } + +.fa-fax:before { + content: "\f1ac"; } + +.fa-female:before { + content: "\f182"; } + +.fa-fighter-jet:before { + content: "\f0fb"; } + +.fa-file:before { + content: "\f15b"; } + +.fa-file-alt:before { + content: "\f15c"; } + +.fa-file-archive:before { + content: "\f1c6"; } + +.fa-file-audio:before { + content: "\f1c7"; } + +.fa-file-code:before { + content: "\f1c9"; } + +.fa-file-excel:before { + content: "\f1c3"; } + +.fa-file-image:before { + content: "\f1c5"; } + +.fa-file-pdf:before { + content: "\f1c1"; } + +.fa-file-powerpoint:before { + content: "\f1c4"; } + +.fa-file-video:before { + content: "\f1c8"; } + +.fa-file-word:before { + content: "\f1c2"; } + +.fa-film:before { + content: "\f008"; } + +.fa-filter:before { + content: "\f0b0"; } + +.fa-fire:before { + content: "\f06d"; } + +.fa-fire-extinguisher:before { + content: "\f134"; } + +.fa-firefox:before { + content: "\f269"; } + +.fa-first-order:before { + content: "\f2b0"; } + +.fa-firstdraft:before { + content: "\f3a1"; } + +.fa-flag:before { + content: "\f024"; } + +.fa-flag-checkered:before { + content: "\f11e"; } + +.fa-flask:before { + content: "\f0c3"; } + +.fa-flickr:before { + content: "\f16e"; } + +.fa-fly:before { + content: "\f417"; } + +.fa-folder:before { + content: "\f07b"; } + +.fa-folder-open:before { + content: "\f07c"; } + +.fa-font:before { + content: "\f031"; } + +.fa-font-awesome:before { + content: "\f2b4"; } + +.fa-font-awesome-alt:before { + content: "\f35c"; } + +.fa-font-awesome-flag:before { + content: "\f425"; } + +.fa-fonticons:before { + content: "\f280"; } + +.fa-fonticons-fi:before { + content: "\f3a2"; } + +.fa-fort-awesome:before { + content: "\f286"; } + +.fa-fort-awesome-alt:before { + content: "\f3a3"; } + +.fa-forumbee:before { + content: "\f211"; } + +.fa-forward:before { + content: "\f04e"; } + +.fa-foursquare:before { + content: "\f180"; } + +.fa-free-code-camp:before { + content: "\f2c5"; } + +.fa-freebsd:before { + content: "\f3a4"; } + +.fa-frown:before { + content: "\f119"; } + +.fa-futbol:before { + content: "\f1e3"; } + +.fa-gamepad:before { + content: "\f11b"; } + +.fa-gavel:before { + content: "\f0e3"; } + +.fa-gem:before { + content: "\f3a5"; } + +.fa-genderless:before { + content: "\f22d"; } + +.fa-get-pocket:before { + content: "\f265"; } + +.fa-gg:before { + content: "\f260"; } + +.fa-gg-circle:before { + content: "\f261"; } + +.fa-gift:before { + content: "\f06b"; } + +.fa-git:before { + content: "\f1d3"; } + +.fa-git-square:before { + content: "\f1d2"; } + +.fa-github:before { + content: "\f09b"; } + +.fa-github-alt:before { + content: "\f113"; } + +.fa-github-square:before { + content: "\f092"; } + +.fa-gitkraken:before { + content: "\f3a6"; } + +.fa-gitlab:before { + content: "\f296"; } + +.fa-gitter:before { + content: "\f426"; } + +.fa-glass-martini:before { + content: "\f000"; } + +.fa-glide:before { + content: "\f2a5"; } + +.fa-glide-g:before { + content: "\f2a6"; } + +.fa-globe:before { + content: "\f0ac"; } + +.fa-gofore:before { + content: "\f3a7"; } + +.fa-goodreads:before { + content: "\f3a8"; } + +.fa-goodreads-g:before { + content: "\f3a9"; } + +.fa-google:before { + content: "\f1a0"; } + +.fa-google-drive:before { + content: "\f3aa"; } + +.fa-google-play:before { + content: "\f3ab"; } + +.fa-google-plus:before { + content: "\f2b3"; } + +.fa-google-plus-g:before { + content: "\f0d5"; } + +.fa-google-plus-square:before { + content: "\f0d4"; } + +.fa-google-wallet:before { + content: "\f1ee"; } + +.fa-graduation-cap:before { + content: "\f19d"; } + +.fa-gratipay:before { + content: "\f184"; } + +.fa-grav:before { + content: "\f2d6"; } + +.fa-gripfire:before { + content: "\f3ac"; } + +.fa-grunt:before { + content: "\f3ad"; } + +.fa-gulp:before { + content: "\f3ae"; } + +.fa-h-square:before { + content: "\f0fd"; } + +.fa-hacker-news:before { + content: "\f1d4"; } + +.fa-hacker-news-square:before { + content: "\f3af"; } + +.fa-hand-lizard:before { + content: "\f258"; } + +.fa-hand-paper:before { + content: "\f256"; } + +.fa-hand-peace:before { + content: "\f25b"; } + +.fa-hand-point-down:before { + content: "\f0a7"; } + +.fa-hand-point-left:before { + content: "\f0a5"; } + +.fa-hand-point-right:before { + content: "\f0a4"; } + +.fa-hand-point-up:before { + content: "\f0a6"; } + +.fa-hand-pointer:before { + content: "\f25a"; } + +.fa-hand-rock:before { + content: "\f255"; } + +.fa-hand-scissors:before { + content: "\f257"; } + +.fa-hand-spock:before { + content: "\f259"; } + +.fa-handshake:before { + content: "\f2b5"; } + +.fa-hashtag:before { + content: "\f292"; } + +.fa-hdd:before { + content: "\f0a0"; } + +.fa-heading:before { + content: "\f1dc"; } + +.fa-headphones:before { + content: "\f025"; } + +.fa-heart:before { + content: "\f004"; } + +.fa-heartbeat:before { + content: "\f21e"; } + +.fa-hire-a-helper:before { + content: "\f3b0"; } + +.fa-history:before { + content: "\f1da"; } + +.fa-home:before { + content: "\f015"; } + +.fa-hooli:before { + content: "\f427"; } + +.fa-hospital:before { + content: "\f0f8"; } + +.fa-hotjar:before { + content: "\f3b1"; } + +.fa-hourglass:before { + content: "\f254"; } + +.fa-hourglass-end:before { + content: "\f253"; } + +.fa-hourglass-half:before { + content: "\f252"; } + +.fa-hourglass-start:before { + content: "\f251"; } + +.fa-houzz:before { + content: "\f27c"; } + +.fa-html5:before { + content: "\f13b"; } + +.fa-hubspot:before { + content: "\f3b2"; } + +.fa-i-cursor:before { + content: "\f246"; } + +.fa-id-badge:before { + content: "\f2c1"; } + +.fa-id-card:before { + content: "\f2c2"; } + +.fa-image:before { + content: "\f03e"; } + +.fa-images:before { + content: "\f302"; } + +.fa-imdb:before { + content: "\f2d8"; } + +.fa-inbox:before { + content: "\f01c"; } + +.fa-indent:before { + content: "\f03c"; } + +.fa-industry:before { + content: "\f275"; } + +.fa-info:before { + content: "\f129"; } + +.fa-info-circle:before { + content: "\f05a"; } + +.fa-instagram:before { + content: "\f16d"; } + +.fa-internet-explorer:before { + content: "\f26b"; } + +.fa-ioxhost:before { + content: "\f208"; } + +.fa-italic:before { + content: "\f033"; } + +.fa-itunes:before { + content: "\f3b4"; } + +.fa-itunes-note:before { + content: "\f3b5"; } + +.fa-jenkins:before { + content: "\f3b6"; } + +.fa-joget:before { + content: "\f3b7"; } + +.fa-joomla:before { + content: "\f1aa"; } + +.fa-js:before { + content: "\f3b8"; } + +.fa-js-square:before { + content: "\f3b9"; } + +.fa-jsfiddle:before { + content: "\f1cc"; } + +.fa-key:before { + content: "\f084"; } + +.fa-keyboard:before { + content: "\f11c"; } + +.fa-keycdn:before { + content: "\f3ba"; } + +.fa-kickstarter:before { + content: "\f3bb"; } + +.fa-kickstarter-k:before { + content: "\f3bc"; } + +.fa-korvue:before { + content: "\f42f"; } + +.fa-language:before { + content: "\f1ab"; } + +.fa-laptop:before { + content: "\f109"; } + +.fa-laravel:before { + content: "\f3bd"; } + +.fa-lastfm:before { + content: "\f202"; } + +.fa-lastfm-square:before { + content: "\f203"; } + +.fa-leaf:before { + content: "\f06c"; } + +.fa-leanpub:before { + content: "\f212"; } + +.fa-lemon:before { + content: "\f094"; } + +.fa-less:before { + content: "\f41d"; } + +.fa-level-down-alt:before { + content: "\f3be"; } + +.fa-level-up-alt:before { + content: "\f3bf"; } + +.fa-life-ring:before { + content: "\f1cd"; } + +.fa-lightbulb:before { + content: "\f0eb"; } + +.fa-line:before { + content: "\f3c0"; } + +.fa-link:before { + content: "\f0c1"; } + +.fa-linkedin:before { + content: "\f08c"; } + +.fa-linkedin-in:before { + content: "\f0e1"; } + +.fa-linode:before { + content: "\f2b8"; } + +.fa-linux:before { + content: "\f17c"; } + +.fa-lira-sign:before { + content: "\f195"; } + +.fa-list:before { + content: "\f03a"; } + +.fa-list-alt:before { + content: "\f022"; } + +.fa-list-ol:before { + content: "\f0cb"; } + +.fa-list-ul:before { + content: "\f0ca"; } + +.fa-location-arrow:before { + content: "\f124"; } + +.fa-lock:before { + content: "\f023"; } + +.fa-lock-open:before { + content: "\f3c1"; } + +.fa-long-arrow-alt-down:before { + content: "\f309"; } + +.fa-long-arrow-alt-left:before { + content: "\f30a"; } + +.fa-long-arrow-alt-right:before { + content: "\f30b"; } + +.fa-long-arrow-alt-up:before { + content: "\f30c"; } + +.fa-low-vision:before { + content: "\f2a8"; } + +.fa-lyft:before { + content: "\f3c3"; } + +.fa-magento:before { + content: "\f3c4"; } + +.fa-magic:before { + content: "\f0d0"; } + +.fa-magnet:before { + content: "\f076"; } + +.fa-male:before { + content: "\f183"; } + +.fa-map:before { + content: "\f279"; } + +.fa-map-marker:before { + content: "\f041"; } + +.fa-map-marker-alt:before { + content: "\f3c5"; } + +.fa-map-pin:before { + content: "\f276"; } + +.fa-map-signs:before { + content: "\f277"; } + +.fa-mars:before { + content: "\f222"; } + +.fa-mars-double:before { + content: "\f227"; } + +.fa-mars-stroke:before { + content: "\f229"; } + +.fa-mars-stroke-h:before { + content: "\f22b"; } + +.fa-mars-stroke-v:before { + content: "\f22a"; } + +.fa-maxcdn:before { + content: "\f136"; } + +.fa-medapps:before { + content: "\f3c6"; } + +.fa-medium:before { + content: "\f23a"; } + +.fa-medium-m:before { + content: "\f3c7"; } + +.fa-medkit:before { + content: "\f0fa"; } + +.fa-medrt:before { + content: "\f3c8"; } + +.fa-meetup:before { + content: "\f2e0"; } + +.fa-meh:before { + content: "\f11a"; } + +.fa-mercury:before { + content: "\f223"; } + +.fa-microchip:before { + content: "\f2db"; } + +.fa-microphone:before { + content: "\f130"; } + +.fa-microphone-slash:before { + content: "\f131"; } + +.fa-microsoft:before { + content: "\f3ca"; } + +.fa-minus:before { + content: "\f068"; } + +.fa-minus-circle:before { + content: "\f056"; } + +.fa-minus-square:before { + content: "\f146"; } + +.fa-mix:before { + content: "\f3cb"; } + +.fa-mixcloud:before { + content: "\f289"; } + +.fa-mizuni:before { + content: "\f3cc"; } + +.fa-mobile:before { + content: "\f10b"; } + +.fa-mobile-alt:before { + content: "\f3cd"; } + +.fa-modx:before { + content: "\f285"; } + +.fa-monero:before { + content: "\f3d0"; } + +.fa-money-bill-alt:before { + content: "\f3d1"; } + +.fa-moon:before { + content: "\f186"; } + +.fa-motorcycle:before { + content: "\f21c"; } + +.fa-mouse-pointer:before { + content: "\f245"; } + +.fa-music:before { + content: "\f001"; } + +.fa-napster:before { + content: "\f3d2"; } + +.fa-neuter:before { + content: "\f22c"; } + +.fa-newspaper:before { + content: "\f1ea"; } + +.fa-nintendo-switch:before { + content: "\f418"; } + +.fa-node:before { + content: "\f419"; } + +.fa-node-js:before { + content: "\f3d3"; } + +.fa-npm:before { + content: "\f3d4"; } + +.fa-ns8:before { + content: "\f3d5"; } + +.fa-nutritionix:before { + content: "\f3d6"; } + +.fa-object-group:before { + content: "\f247"; } + +.fa-object-ungroup:before { + content: "\f248"; } + +.fa-odnoklassniki:before { + content: "\f263"; } + +.fa-odnoklassniki-square:before { + content: "\f264"; } + +.fa-opencart:before { + content: "\f23d"; } + +.fa-openid:before { + content: "\f19b"; } + +.fa-opera:before { + content: "\f26a"; } + +.fa-optin-monster:before { + content: "\f23c"; } + +.fa-osi:before { + content: "\f41a"; } + +.fa-outdent:before { + content: "\f03b"; } + +.fa-page4:before { + content: "\f3d7"; } + +.fa-pagelines:before { + content: "\f18c"; } + +.fa-paint-brush:before { + content: "\f1fc"; } + +.fa-palfed:before { + content: "\f3d8"; } + +.fa-paper-plane:before { + content: "\f1d8"; } + +.fa-paperclip:before { + content: "\f0c6"; } + +.fa-paragraph:before { + content: "\f1dd"; } + +.fa-paste:before { + content: "\f0ea"; } + +.fa-patreon:before { + content: "\f3d9"; } + +.fa-pause:before { + content: "\f04c"; } + +.fa-pause-circle:before { + content: "\f28b"; } + +.fa-paw:before { + content: "\f1b0"; } + +.fa-paypal:before { + content: "\f1ed"; } + +.fa-pen-square:before { + content: "\f14b"; } + +.fa-pencil-alt:before { + content: "\f303"; } + +.fa-percent:before { + content: "\f295"; } + +.fa-periscope:before { + content: "\f3da"; } + +.fa-phabricator:before { + content: "\f3db"; } + +.fa-phoenix-framework:before { + content: "\f3dc"; } + +.fa-phone:before { + content: "\f095"; } + +.fa-phone-square:before { + content: "\f098"; } + +.fa-phone-volume:before { + content: "\f2a0"; } + +.fa-pied-piper:before { + content: "\f2ae"; } + +.fa-pied-piper-alt:before { + content: "\f1a8"; } + +.fa-pied-piper-pp:before { + content: "\f1a7"; } + +.fa-pinterest:before { + content: "\f0d2"; } + +.fa-pinterest-p:before { + content: "\f231"; } + +.fa-pinterest-square:before { + content: "\f0d3"; } + +.fa-plane:before { + content: "\f072"; } + +.fa-play:before { + content: "\f04b"; } + +.fa-play-circle:before { + content: "\f144"; } + +.fa-playstation:before { + content: "\f3df"; } + +.fa-plug:before { + content: "\f1e6"; } + +.fa-plus:before { + content: "\f067"; } + +.fa-plus-circle:before { + content: "\f055"; } + +.fa-plus-square:before { + content: "\f0fe"; } + +.fa-podcast:before { + content: "\f2ce"; } + +.fa-pound-sign:before { + content: "\f154"; } + +.fa-power-off:before { + content: "\f011"; } + +.fa-print:before { + content: "\f02f"; } + +.fa-product-hunt:before { + content: "\f288"; } + +.fa-pushed:before { + content: "\f3e1"; } + +.fa-puzzle-piece:before { + content: "\f12e"; } + +.fa-python:before { + content: "\f3e2"; } + +.fa-qq:before { + content: "\f1d6"; } + +.fa-qrcode:before { + content: "\f029"; } + +.fa-question:before { + content: "\f128"; } + +.fa-question-circle:before { + content: "\f059"; } + +.fa-quora:before { + content: "\f2c4"; } + +.fa-quote-left:before { + content: "\f10d"; } + +.fa-quote-right:before { + content: "\f10e"; } + +.fa-random:before { + content: "\f074"; } + +.fa-ravelry:before { + content: "\f2d9"; } + +.fa-react:before { + content: "\f41b"; } + +.fa-rebel:before { + content: "\f1d0"; } + +.fa-recycle:before { + content: "\f1b8"; } + +.fa-red-river:before { + content: "\f3e3"; } + +.fa-reddit:before { + content: "\f1a1"; } + +.fa-reddit-alien:before { + content: "\f281"; } + +.fa-reddit-square:before { + content: "\f1a2"; } + +.fa-redo:before { + content: "\f01e"; } + +.fa-redo-alt:before { + content: "\f2f9"; } + +.fa-registered:before { + content: "\f25d"; } + +.fa-rendact:before { + content: "\f3e4"; } + +.fa-renren:before { + content: "\f18b"; } + +.fa-reply:before { + content: "\f3e5"; } + +.fa-reply-all:before { + content: "\f122"; } + +.fa-replyd:before { + content: "\f3e6"; } + +.fa-resolving:before { + content: "\f3e7"; } + +.fa-retweet:before { + content: "\f079"; } + +.fa-road:before { + content: "\f018"; } + +.fa-rocket:before { + content: "\f135"; } + +.fa-rocketchat:before { + content: "\f3e8"; } + +.fa-rockrms:before { + content: "\f3e9"; } + +.fa-rss:before { + content: "\f09e"; } + +.fa-rss-square:before { + content: "\f143"; } + +.fa-ruble-sign:before { + content: "\f158"; } + +.fa-rupee-sign:before { + content: "\f156"; } + +.fa-safari:before { + content: "\f267"; } + +.fa-sass:before { + content: "\f41e"; } + +.fa-save:before { + content: "\f0c7"; } + +.fa-schlix:before { + content: "\f3ea"; } + +.fa-scribd:before { + content: "\f28a"; } + +.fa-search:before { + content: "\f002"; } + +.fa-search-minus:before { + content: "\f010"; } + +.fa-search-plus:before { + content: "\f00e"; } + +.fa-searchengin:before { + content: "\f3eb"; } + +.fa-sellcast:before { + content: "\f2da"; } + +.fa-sellsy:before { + content: "\f213"; } + +.fa-server:before { + content: "\f233"; } + +.fa-servicestack:before { + content: "\f3ec"; } + +.fa-share:before { + content: "\f064"; } + +.fa-share-alt:before { + content: "\f1e0"; } + +.fa-share-alt-square:before { + content: "\f1e1"; } + +.fa-share-square:before { + content: "\f14d"; } + +.fa-shekel-sign:before { + content: "\f20b"; } + +.fa-shield-alt:before { + content: "\f3ed"; } + +.fa-ship:before { + content: "\f21a"; } + +.fa-shirtsinbulk:before { + content: "\f214"; } + +.fa-shopping-bag:before { + content: "\f290"; } + +.fa-shopping-basket:before { + content: "\f291"; } + +.fa-shopping-cart:before { + content: "\f07a"; } + +.fa-shower:before { + content: "\f2cc"; } + +.fa-sign-in-alt:before { + content: "\f2f6"; } + +.fa-sign-language:before { + content: "\f2a7"; } + +.fa-sign-out-alt:before { + content: "\f2f5"; } + +.fa-signal:before { + content: "\f012"; } + +.fa-simplybuilt:before { + content: "\f215"; } + +.fa-sistrix:before { + content: "\f3ee"; } + +.fa-sitemap:before { + content: "\f0e8"; } + +.fa-skyatlas:before { + content: "\f216"; } + +.fa-skype:before { + content: "\f17e"; } + +.fa-slack:before { + content: "\f198"; } + +.fa-slack-hash:before { + content: "\f3ef"; } + +.fa-sliders-h:before { + content: "\f1de"; } + +.fa-slideshare:before { + content: "\f1e7"; } + +.fa-smile:before { + content: "\f118"; } + +.fa-snapchat:before { + content: "\f2ab"; } + +.fa-snapchat-ghost:before { + content: "\f2ac"; } + +.fa-snapchat-square:before { + content: "\f2ad"; } + +.fa-snowflake:before { + content: "\f2dc"; } + +.fa-sort:before { + content: "\f0dc"; } + +.fa-sort-alpha-down:before { + content: "\f15d"; } + +.fa-sort-alpha-up:before { + content: "\f15e"; } + +.fa-sort-amount-down:before { + content: "\f160"; } + +.fa-sort-amount-up:before { + content: "\f161"; } + +.fa-sort-down:before { + content: "\f0dd"; } + +.fa-sort-numeric-down:before { + content: "\f162"; } + +.fa-sort-numeric-up:before { + content: "\f163"; } + +.fa-sort-up:before { + content: "\f0de"; } + +.fa-soundcloud:before { + content: "\f1be"; } + +.fa-space-shuttle:before { + content: "\f197"; } + +.fa-speakap:before { + content: "\f3f3"; } + +.fa-spinner:before { + content: "\f110"; } + +.fa-spotify:before { + content: "\f1bc"; } + +.fa-square:before { + content: "\f0c8"; } + +.fa-stack-exchange:before { + content: "\f18d"; } + +.fa-stack-overflow:before { + content: "\f16c"; } + +.fa-star:before { + content: "\f005"; } + +.fa-star-half:before { + content: "\f089"; } + +.fa-staylinked:before { + content: "\f3f5"; } + +.fa-steam:before { + content: "\f1b6"; } + +.fa-steam-square:before { + content: "\f1b7"; } + +.fa-steam-symbol:before { + content: "\f3f6"; } + +.fa-step-backward:before { + content: "\f048"; } + +.fa-step-forward:before { + content: "\f051"; } + +.fa-stethoscope:before { + content: "\f0f1"; } + +.fa-sticker-mule:before { + content: "\f3f7"; } + +.fa-sticky-note:before { + content: "\f249"; } + +.fa-stop:before { + content: "\f04d"; } + +.fa-stop-circle:before { + content: "\f28d"; } + +.fa-stopwatch:before { + content: "\f2f2"; } + +.fa-strava:before { + content: "\f428"; } + +.fa-street-view:before { + content: "\f21d"; } + +.fa-strikethrough:before { + content: "\f0cc"; } + +.fa-stripe:before { + content: "\f429"; } + +.fa-stripe-s:before { + content: "\f42a"; } + +.fa-studiovinari:before { + content: "\f3f8"; } + +.fa-stumbleupon:before { + content: "\f1a4"; } + +.fa-stumbleupon-circle:before { + content: "\f1a3"; } + +.fa-subscript:before { + content: "\f12c"; } + +.fa-subway:before { + content: "\f239"; } + +.fa-suitcase:before { + content: "\f0f2"; } + +.fa-sun:before { + content: "\f185"; } + +.fa-superpowers:before { + content: "\f2dd"; } + +.fa-superscript:before { + content: "\f12b"; } + +.fa-supple:before { + content: "\f3f9"; } + +.fa-sync:before { + content: "\f021"; } + +.fa-sync-alt:before { + content: "\f2f1"; } + +.fa-table:before { + content: "\f0ce"; } + +.fa-tablet:before { + content: "\f10a"; } + +.fa-tablet-alt:before { + content: "\f3fa"; } + +.fa-tachometer-alt:before { + content: "\f3fd"; } + +.fa-tag:before { + content: "\f02b"; } + +.fa-tags:before { + content: "\f02c"; } + +.fa-tasks:before { + content: "\f0ae"; } + +.fa-taxi:before { + content: "\f1ba"; } + +.fa-telegram:before { + content: "\f2c6"; } + +.fa-telegram-plane:before { + content: "\f3fe"; } + +.fa-tencent-weibo:before { + content: "\f1d5"; } + +.fa-terminal:before { + content: "\f120"; } + +.fa-text-height:before { + content: "\f034"; } + +.fa-text-width:before { + content: "\f035"; } + +.fa-th:before { + content: "\f00a"; } + +.fa-th-large:before { + content: "\f009"; } + +.fa-th-list:before { + content: "\f00b"; } + +.fa-themeisle:before { + content: "\f2b2"; } + +.fa-thermometer-empty:before { + content: "\f2cb"; } + +.fa-thermometer-full:before { + content: "\f2c7"; } + +.fa-thermometer-half:before { + content: "\f2c9"; } + +.fa-thermometer-quarter:before { + content: "\f2ca"; } + +.fa-thermometer-three-quarters:before { + content: "\f2c8"; } + +.fa-thumbs-down:before { + content: "\f165"; } + +.fa-thumbs-up:before { + content: "\f164"; } + +.fa-thumbtack:before { + content: "\f08d"; } + +.fa-ticket-alt:before { + content: "\f3ff"; } + +.fa-times:before { + content: "\f00d"; } + +.fa-times-circle:before { + content: "\f057"; } + +.fa-tint:before { + content: "\f043"; } + +.fa-toggle-off:before { + content: "\f204"; } + +.fa-toggle-on:before { + content: "\f205"; } + +.fa-trademark:before { + content: "\f25c"; } + +.fa-train:before { + content: "\f238"; } + +.fa-transgender:before { + content: "\f224"; } + +.fa-transgender-alt:before { + content: "\f225"; } + +.fa-trash:before { + content: "\f1f8"; } + +.fa-trash-alt:before { + content: "\f2ed"; } + +.fa-tree:before { + content: "\f1bb"; } + +.fa-trello:before { + content: "\f181"; } + +.fa-tripadvisor:before { + content: "\f262"; } + +.fa-trophy:before { + content: "\f091"; } + +.fa-truck:before { + content: "\f0d1"; } + +.fa-tty:before { + content: "\f1e4"; } + +.fa-tumblr:before { + content: "\f173"; } + +.fa-tumblr-square:before { + content: "\f174"; } + +.fa-tv:before { + content: "\f26c"; } + +.fa-twitch:before { + content: "\f1e8"; } + +.fa-twitter:before { + content: "\f099"; } + +.fa-twitter-square:before { + content: "\f081"; } + +.fa-typo3:before { + content: "\f42b"; } + +.fa-uber:before { + content: "\f402"; } + +.fa-uikit:before { + content: "\f403"; } + +.fa-umbrella:before { + content: "\f0e9"; } + +.fa-underline:before { + content: "\f0cd"; } + +.fa-undo:before { + content: "\f0e2"; } + +.fa-undo-alt:before { + content: "\f2ea"; } + +.fa-uniregistry:before { + content: "\f404"; } + +.fa-universal-access:before { + content: "\f29a"; } + +.fa-university:before { + content: "\f19c"; } + +.fa-unlink:before { + content: "\f127"; } + +.fa-unlock:before { + content: "\f09c"; } + +.fa-unlock-alt:before { + content: "\f13e"; } + +.fa-untappd:before { + content: "\f405"; } + +.fa-upload:before { + content: "\f093"; } + +.fa-usb:before { + content: "\f287"; } + +.fa-user:before { + content: "\f007"; } + +.fa-user-circle:before { + content: "\f2bd"; } + +.fa-user-md:before { + content: "\f0f0"; } + +.fa-user-plus:before { + content: "\f234"; } + +.fa-user-secret:before { + content: "\f21b"; } + +.fa-user-times:before { + content: "\f235"; } + +.fa-users:before { + content: "\f0c0"; } + +.fa-ussunnah:before { + content: "\f407"; } + +.fa-utensil-spoon:before { + content: "\f2e5"; } + +.fa-utensils:before { + content: "\f2e7"; } + +.fa-vaadin:before { + content: "\f408"; } + +.fa-venus:before { + content: "\f221"; } + +.fa-venus-double:before { + content: "\f226"; } + +.fa-venus-mars:before { + content: "\f228"; } + +.fa-viacoin:before { + content: "\f237"; } + +.fa-viadeo:before { + content: "\f2a9"; } + +.fa-viadeo-square:before { + content: "\f2aa"; } + +.fa-viber:before { + content: "\f409"; } + +.fa-video:before { + content: "\f03d"; } + +.fa-vimeo:before { + content: "\f40a"; } + +.fa-vimeo-square:before { + content: "\f194"; } + +.fa-vimeo-v:before { + content: "\f27d"; } + +.fa-vine:before { + content: "\f1ca"; } + +.fa-vk:before { + content: "\f189"; } + +.fa-vnv:before { + content: "\f40b"; } + +.fa-volume-down:before { + content: "\f027"; } + +.fa-volume-off:before { + content: "\f026"; } + +.fa-volume-up:before { + content: "\f028"; } + +.fa-vuejs:before { + content: "\f41f"; } + +.fa-weibo:before { + content: "\f18a"; } + +.fa-weixin:before { + content: "\f1d7"; } + +.fa-whatsapp:before { + content: "\f232"; } + +.fa-whatsapp-square:before { + content: "\f40c"; } + +.fa-wheelchair:before { + content: "\f193"; } + +.fa-whmcs:before { + content: "\f40d"; } + +.fa-wifi:before { + content: "\f1eb"; } + +.fa-wikipedia-w:before { + content: "\f266"; } + +.fa-window-close:before { + content: "\f410"; } + +.fa-window-maximize:before { + content: "\f2d0"; } + +.fa-window-minimize:before { + content: "\f2d1"; } + +.fa-window-restore:before { + content: "\f2d2"; } + +.fa-windows:before { + content: "\f17a"; } + +.fa-won-sign:before { + content: "\f159"; } + +.fa-wordpress:before { + content: "\f19a"; } + +.fa-wordpress-simple:before { + content: "\f411"; } + +.fa-wpbeginner:before { + content: "\f297"; } + +.fa-wpexplorer:before { + content: "\f2de"; } + +.fa-wpforms:before { + content: "\f298"; } + +.fa-wrench:before { + content: "\f0ad"; } + +.fa-xbox:before { + content: "\f412"; } + +.fa-xing:before { + content: "\f168"; } + +.fa-xing-square:before { + content: "\f169"; } + +.fa-y-combinator:before { + content: "\f23b"; } + +.fa-yahoo:before { + content: "\f19e"; } + +.fa-yandex:before { + content: "\f413"; } + +.fa-yandex-international:before { + content: "\f414"; } + +.fa-yelp:before { + content: "\f1e9"; } + +.fa-yen-sign:before { + content: "\f157"; } + +.fa-yoast:before { + content: "\f2b1"; } + +.fa-youtube:before { + content: "\f167"; } + +.fa-youtube-square:before { + content: "\f431"; } + +.sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } + +.sr-only-focusable:active, .sr-only-focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; } +@font-face { + font-family: 'Font Awesome 5 Brands'; + font-style: normal; + font-weight: normal; + src: url("../webfonts/fa-brands-400.eot"); + src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } + +.fab { + font-family: 'Font Awesome 5 Brands'; } +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 400; + src: url("../webfonts/fa-regular-400.eot"); + src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } + +.far { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; } +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 900; + src: url("../webfonts/fa-solid-900.eot"); + src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } + +.fa, +.fas { + font-family: 'Font Awesome 5 Free'; + font-weight: 900; } diff --git a/core/static/fontawesome/css/fontawesome-all.min.css b/core/static/fontawesome/css/fontawesome-all.min.css new file mode 100644 index 00000000..05d0860e --- /dev/null +++ b/core/static/fontawesome/css/fontawesome-all.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:a 2s infinite linear;animation:a 2s infinite linear}.fa-pulse{-webkit-animation:a 1s infinite steps(8);animation:a 1s infinite steps(8)}@-webkit-keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-aws:before{content:"\f375"}.fa-backward:before{content:"\f04a"}.fa-balance-scale:before{content:"\f24e"}.fa-ban:before{content:"\f05e"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bicycle:before{content:"\f206"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blind:before{content:"\f29d"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-braille:before{content:"\f2a1"}.fa-briefcase:before{content:"\f0b1"}.fa-btc:before{content:"\f15a"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-car:before{content:"\f1b9"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-certificate:before{content:"\f0a3"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-square:before{content:"\f14a"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-clipboard:before{content:"\f328"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comments:before{content:"\f086"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-credit-card:before{content:"\f09d"}.fa-crop:before{content:"\f125"}.fa-crosshairs:before{content:"\f05b"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-deviantart:before{content:"\f1bd"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dollar-sign:before{content:"\f155"}.fa-dot-circle:before{content:"\f192"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drupal:before{content:"\f1a9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-excel:before{content:"\f1c3"}.fa-file-image:before{content:"\f1c5"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fire:before{content:"\f06d"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-order:before{content:"\f2b0"}.fa-firstdraft:before{content:"\f3a1"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frown:before{content:"\f119"}.fa-futbol:before{content:"\f1e3"}.fa-gamepad:before{content:"\f11b"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-gift:before{content:"\f06b"}.fa-git:before{content:"\f1d3"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-martini:before{content:"\f000"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-gofore:before{content:"\f3a7"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-handshake:before{content:"\f2b5"}.fa-hashtag:before{content:"\f292"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-heart:before{content:"\f004"}.fa-heartbeat:before{content:"\f21e"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hospital:before{content:"\f0f8"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-houzz:before{content:"\f27c"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-internet-explorer:before{content:"\f26b"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-jenkins:before{content:"\f3b6"}.fa-joget:before{content:"\f3b7"}.fa-joomla:before{content:"\f1aa"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-key:before{content:"\f084"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-korvue:before{content:"\f42f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-male:before{content:"\f183"}.fa-map:before{content:"\f279"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-maxcdn:before{content:"\f136"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-meh:before{content:"\f11a"}.fa-mercury:before{content:"\f223"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-moon:before{content:"\f186"}.fa-motorcycle:before{content:"\f21c"}.fa-mouse-pointer:before{content:"\f245"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nintendo-switch:before{content:"\f418"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-paint-brush:before{content:"\f1fc"}.fa-palfed:before{content:"\f3d8"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-paragraph:before{content:"\f1dd"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-percent:before{content:"\f295"}.fa-periscope:before{content:"\f3da"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phone:before{content:"\f095"}.fa-phone-square:before{content:"\f098"}.fa-phone-volume:before{content:"\f2a0"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-plane:before{content:"\f072"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-print:before{content:"\f02f"}.fa-product-hunt:before{content:"\f288"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-random:before{content:"\f074"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-rebel:before{content:"\f1d0"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-rendact:before{content:"\f3e4"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-resolving:before{content:"\f3e7"}.fa-retweet:before{content:"\f079"}.fa-road:before{content:"\f018"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-rupee-sign:before{content:"\f156"}.fa-safari:before{content:"\f267"}.fa-sass:before{content:"\f41e"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-scribd:before{content:"\f28a"}.fa-search:before{content:"\f002"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shower:before{content:"\f2cc"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowflake:before{content:"\f2dc"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-spinner:before{content:"\f110"}.fa-spotify:before{content:"\f1bc"}.fa-square:before{content:"\f0c8"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-star:before{content:"\f005"}.fa-star-half:before{content:"\f089"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-strava:before{content:"\f428"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-table:before{content:"\f0ce"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-trademark:before{content:"\f25c"}.fa-train:before{content:"\f238"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-circle:before{content:"\f2bd"}.fa-user-md:before{content:"\f0f0"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-volume-down:before{content:"\f027"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vuejs:before{content:"\f41f"}.fa-weibo:before{content:"\f18a"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wrench:before{content:"\f0ad"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}@font-face{font-family:Font Awesome\ 5 Brands;font-style:normal;font-weight:400;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:Font Awesome\ 5 Brands}@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-weight:400}@font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:900;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.far,.fas{font-family:Font Awesome\ 5 Free}.fa,.fas{font-weight:900} \ No newline at end of file diff --git a/core/static/fontawesome/css/fontawesome.css b/core/static/fontawesome/css/fontawesome.css new file mode 100644 index 00000000..9f724189 --- /dev/null +++ b/core/static/fontawesome/css/fontawesome.css @@ -0,0 +1,2579 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa, +.fas, +.far, +.fal, +.fab { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; } + +.fa-lg { + font-size: 1.33333em; + line-height: 0.75em; + vertical-align: -.0667em; } + +.fa-xs { + font-size: .75em; } + +.fa-sm { + font-size: .875em; } + +.fa-1x { + font-size: 1em; } + +.fa-2x { + font-size: 2em; } + +.fa-3x { + font-size: 3em; } + +.fa-4x { + font-size: 4em; } + +.fa-5x { + font-size: 5em; } + +.fa-6x { + font-size: 6em; } + +.fa-7x { + font-size: 7em; } + +.fa-8x { + font-size: 8em; } + +.fa-9x { + font-size: 9em; } + +.fa-10x { + font-size: 10em; } + +.fa-fw { + text-align: center; + width: 1.25em; } + +.fa-ul { + list-style-type: none; + margin-left: 2.5em; + padding-left: 0; } + .fa-ul > li { + position: relative; } + +.fa-li { + left: -2em; + position: absolute; + text-align: center; + width: 2em; + line-height: inherit; } + +.fa-border { + border: solid 0.08em #eee; + border-radius: .1em; + padding: .2em .25em .15em; } + +.fa-pull-left { + float: left; } + +.fa-pull-right { + float: right; } + +.fa.fa-pull-left, +.fas.fa-pull-left, +.far.fa-pull-left, +.fal.fa-pull-left, +.fab.fa-pull-left { + margin-right: .3em; } + +.fa.fa-pull-right, +.fas.fa-pull-right, +.far.fa-pull-right, +.fal.fa-pull-right, +.fab.fa-pull-right { + margin-left: .3em; } + +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; } + +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); } + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +.fa-flip-horizontal.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(-1, -1); + transform: scale(-1, -1); } + +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + -webkit-filter: none; + filter: none; } + +.fa-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2em; } + +.fa-stack-1x, +.fa-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: #fff; } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ +.fa-500px:before { + content: "\f26e"; } + +.fa-accessible-icon:before { + content: "\f368"; } + +.fa-accusoft:before { + content: "\f369"; } + +.fa-address-book:before { + content: "\f2b9"; } + +.fa-address-card:before { + content: "\f2bb"; } + +.fa-adjust:before { + content: "\f042"; } + +.fa-adn:before { + content: "\f170"; } + +.fa-adversal:before { + content: "\f36a"; } + +.fa-affiliatetheme:before { + content: "\f36b"; } + +.fa-algolia:before { + content: "\f36c"; } + +.fa-align-center:before { + content: "\f037"; } + +.fa-align-justify:before { + content: "\f039"; } + +.fa-align-left:before { + content: "\f036"; } + +.fa-align-right:before { + content: "\f038"; } + +.fa-amazon:before { + content: "\f270"; } + +.fa-amazon-pay:before { + content: "\f42c"; } + +.fa-ambulance:before { + content: "\f0f9"; } + +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; } + +.fa-amilia:before { + content: "\f36d"; } + +.fa-anchor:before { + content: "\f13d"; } + +.fa-android:before { + content: "\f17b"; } + +.fa-angellist:before { + content: "\f209"; } + +.fa-angle-double-down:before { + content: "\f103"; } + +.fa-angle-double-left:before { + content: "\f100"; } + +.fa-angle-double-right:before { + content: "\f101"; } + +.fa-angle-double-up:before { + content: "\f102"; } + +.fa-angle-down:before { + content: "\f107"; } + +.fa-angle-left:before { + content: "\f104"; } + +.fa-angle-right:before { + content: "\f105"; } + +.fa-angle-up:before { + content: "\f106"; } + +.fa-angrycreative:before { + content: "\f36e"; } + +.fa-angular:before { + content: "\f420"; } + +.fa-app-store:before { + content: "\f36f"; } + +.fa-app-store-ios:before { + content: "\f370"; } + +.fa-apper:before { + content: "\f371"; } + +.fa-apple:before { + content: "\f179"; } + +.fa-apple-pay:before { + content: "\f415"; } + +.fa-archive:before { + content: "\f187"; } + +.fa-arrow-alt-circle-down:before { + content: "\f358"; } + +.fa-arrow-alt-circle-left:before { + content: "\f359"; } + +.fa-arrow-alt-circle-right:before { + content: "\f35a"; } + +.fa-arrow-alt-circle-up:before { + content: "\f35b"; } + +.fa-arrow-circle-down:before { + content: "\f0ab"; } + +.fa-arrow-circle-left:before { + content: "\f0a8"; } + +.fa-arrow-circle-right:before { + content: "\f0a9"; } + +.fa-arrow-circle-up:before { + content: "\f0aa"; } + +.fa-arrow-down:before { + content: "\f063"; } + +.fa-arrow-left:before { + content: "\f060"; } + +.fa-arrow-right:before { + content: "\f061"; } + +.fa-arrow-up:before { + content: "\f062"; } + +.fa-arrows-alt:before { + content: "\f0b2"; } + +.fa-arrows-alt-h:before { + content: "\f337"; } + +.fa-arrows-alt-v:before { + content: "\f338"; } + +.fa-assistive-listening-systems:before { + content: "\f2a2"; } + +.fa-asterisk:before { + content: "\f069"; } + +.fa-asymmetrik:before { + content: "\f372"; } + +.fa-at:before { + content: "\f1fa"; } + +.fa-audible:before { + content: "\f373"; } + +.fa-audio-description:before { + content: "\f29e"; } + +.fa-autoprefixer:before { + content: "\f41c"; } + +.fa-avianex:before { + content: "\f374"; } + +.fa-aviato:before { + content: "\f421"; } + +.fa-aws:before { + content: "\f375"; } + +.fa-backward:before { + content: "\f04a"; } + +.fa-balance-scale:before { + content: "\f24e"; } + +.fa-ban:before { + content: "\f05e"; } + +.fa-bandcamp:before { + content: "\f2d5"; } + +.fa-barcode:before { + content: "\f02a"; } + +.fa-bars:before { + content: "\f0c9"; } + +.fa-bath:before { + content: "\f2cd"; } + +.fa-battery-empty:before { + content: "\f244"; } + +.fa-battery-full:before { + content: "\f240"; } + +.fa-battery-half:before { + content: "\f242"; } + +.fa-battery-quarter:before { + content: "\f243"; } + +.fa-battery-three-quarters:before { + content: "\f241"; } + +.fa-bed:before { + content: "\f236"; } + +.fa-beer:before { + content: "\f0fc"; } + +.fa-behance:before { + content: "\f1b4"; } + +.fa-behance-square:before { + content: "\f1b5"; } + +.fa-bell:before { + content: "\f0f3"; } + +.fa-bell-slash:before { + content: "\f1f6"; } + +.fa-bicycle:before { + content: "\f206"; } + +.fa-bimobject:before { + content: "\f378"; } + +.fa-binoculars:before { + content: "\f1e5"; } + +.fa-birthday-cake:before { + content: "\f1fd"; } + +.fa-bitbucket:before { + content: "\f171"; } + +.fa-bitcoin:before { + content: "\f379"; } + +.fa-bity:before { + content: "\f37a"; } + +.fa-black-tie:before { + content: "\f27e"; } + +.fa-blackberry:before { + content: "\f37b"; } + +.fa-blind:before { + content: "\f29d"; } + +.fa-blogger:before { + content: "\f37c"; } + +.fa-blogger-b:before { + content: "\f37d"; } + +.fa-bluetooth:before { + content: "\f293"; } + +.fa-bluetooth-b:before { + content: "\f294"; } + +.fa-bold:before { + content: "\f032"; } + +.fa-bolt:before { + content: "\f0e7"; } + +.fa-bomb:before { + content: "\f1e2"; } + +.fa-book:before { + content: "\f02d"; } + +.fa-bookmark:before { + content: "\f02e"; } + +.fa-braille:before { + content: "\f2a1"; } + +.fa-briefcase:before { + content: "\f0b1"; } + +.fa-btc:before { + content: "\f15a"; } + +.fa-bug:before { + content: "\f188"; } + +.fa-building:before { + content: "\f1ad"; } + +.fa-bullhorn:before { + content: "\f0a1"; } + +.fa-bullseye:before { + content: "\f140"; } + +.fa-buromobelexperte:before { + content: "\f37f"; } + +.fa-bus:before { + content: "\f207"; } + +.fa-buysellads:before { + content: "\f20d"; } + +.fa-calculator:before { + content: "\f1ec"; } + +.fa-calendar:before { + content: "\f133"; } + +.fa-calendar-alt:before { + content: "\f073"; } + +.fa-calendar-check:before { + content: "\f274"; } + +.fa-calendar-minus:before { + content: "\f272"; } + +.fa-calendar-plus:before { + content: "\f271"; } + +.fa-calendar-times:before { + content: "\f273"; } + +.fa-camera:before { + content: "\f030"; } + +.fa-camera-retro:before { + content: "\f083"; } + +.fa-car:before { + content: "\f1b9"; } + +.fa-caret-down:before { + content: "\f0d7"; } + +.fa-caret-left:before { + content: "\f0d9"; } + +.fa-caret-right:before { + content: "\f0da"; } + +.fa-caret-square-down:before { + content: "\f150"; } + +.fa-caret-square-left:before { + content: "\f191"; } + +.fa-caret-square-right:before { + content: "\f152"; } + +.fa-caret-square-up:before { + content: "\f151"; } + +.fa-caret-up:before { + content: "\f0d8"; } + +.fa-cart-arrow-down:before { + content: "\f218"; } + +.fa-cart-plus:before { + content: "\f217"; } + +.fa-cc-amazon-pay:before { + content: "\f42d"; } + +.fa-cc-amex:before { + content: "\f1f3"; } + +.fa-cc-apple-pay:before { + content: "\f416"; } + +.fa-cc-diners-club:before { + content: "\f24c"; } + +.fa-cc-discover:before { + content: "\f1f2"; } + +.fa-cc-jcb:before { + content: "\f24b"; } + +.fa-cc-mastercard:before { + content: "\f1f1"; } + +.fa-cc-paypal:before { + content: "\f1f4"; } + +.fa-cc-stripe:before { + content: "\f1f5"; } + +.fa-cc-visa:before { + content: "\f1f0"; } + +.fa-centercode:before { + content: "\f380"; } + +.fa-certificate:before { + content: "\f0a3"; } + +.fa-chart-area:before { + content: "\f1fe"; } + +.fa-chart-bar:before { + content: "\f080"; } + +.fa-chart-line:before { + content: "\f201"; } + +.fa-chart-pie:before { + content: "\f200"; } + +.fa-check:before { + content: "\f00c"; } + +.fa-check-circle:before { + content: "\f058"; } + +.fa-check-square:before { + content: "\f14a"; } + +.fa-chevron-circle-down:before { + content: "\f13a"; } + +.fa-chevron-circle-left:before { + content: "\f137"; } + +.fa-chevron-circle-right:before { + content: "\f138"; } + +.fa-chevron-circle-up:before { + content: "\f139"; } + +.fa-chevron-down:before { + content: "\f078"; } + +.fa-chevron-left:before { + content: "\f053"; } + +.fa-chevron-right:before { + content: "\f054"; } + +.fa-chevron-up:before { + content: "\f077"; } + +.fa-child:before { + content: "\f1ae"; } + +.fa-chrome:before { + content: "\f268"; } + +.fa-circle:before { + content: "\f111"; } + +.fa-circle-notch:before { + content: "\f1ce"; } + +.fa-clipboard:before { + content: "\f328"; } + +.fa-clock:before { + content: "\f017"; } + +.fa-clone:before { + content: "\f24d"; } + +.fa-closed-captioning:before { + content: "\f20a"; } + +.fa-cloud:before { + content: "\f0c2"; } + +.fa-cloud-download-alt:before { + content: "\f381"; } + +.fa-cloud-upload-alt:before { + content: "\f382"; } + +.fa-cloudscale:before { + content: "\f383"; } + +.fa-cloudsmith:before { + content: "\f384"; } + +.fa-cloudversify:before { + content: "\f385"; } + +.fa-code:before { + content: "\f121"; } + +.fa-code-branch:before { + content: "\f126"; } + +.fa-codepen:before { + content: "\f1cb"; } + +.fa-codiepie:before { + content: "\f284"; } + +.fa-coffee:before { + content: "\f0f4"; } + +.fa-cog:before { + content: "\f013"; } + +.fa-cogs:before { + content: "\f085"; } + +.fa-columns:before { + content: "\f0db"; } + +.fa-comment:before { + content: "\f075"; } + +.fa-comment-alt:before { + content: "\f27a"; } + +.fa-comments:before { + content: "\f086"; } + +.fa-compass:before { + content: "\f14e"; } + +.fa-compress:before { + content: "\f066"; } + +.fa-connectdevelop:before { + content: "\f20e"; } + +.fa-contao:before { + content: "\f26d"; } + +.fa-copy:before { + content: "\f0c5"; } + +.fa-copyright:before { + content: "\f1f9"; } + +.fa-cpanel:before { + content: "\f388"; } + +.fa-creative-commons:before { + content: "\f25e"; } + +.fa-credit-card:before { + content: "\f09d"; } + +.fa-crop:before { + content: "\f125"; } + +.fa-crosshairs:before { + content: "\f05b"; } + +.fa-css3:before { + content: "\f13c"; } + +.fa-css3-alt:before { + content: "\f38b"; } + +.fa-cube:before { + content: "\f1b2"; } + +.fa-cubes:before { + content: "\f1b3"; } + +.fa-cut:before { + content: "\f0c4"; } + +.fa-cuttlefish:before { + content: "\f38c"; } + +.fa-d-and-d:before { + content: "\f38d"; } + +.fa-dashcube:before { + content: "\f210"; } + +.fa-database:before { + content: "\f1c0"; } + +.fa-deaf:before { + content: "\f2a4"; } + +.fa-delicious:before { + content: "\f1a5"; } + +.fa-deploydog:before { + content: "\f38e"; } + +.fa-deskpro:before { + content: "\f38f"; } + +.fa-desktop:before { + content: "\f108"; } + +.fa-deviantart:before { + content: "\f1bd"; } + +.fa-digg:before { + content: "\f1a6"; } + +.fa-digital-ocean:before { + content: "\f391"; } + +.fa-discord:before { + content: "\f392"; } + +.fa-discourse:before { + content: "\f393"; } + +.fa-dochub:before { + content: "\f394"; } + +.fa-docker:before { + content: "\f395"; } + +.fa-dollar-sign:before { + content: "\f155"; } + +.fa-dot-circle:before { + content: "\f192"; } + +.fa-download:before { + content: "\f019"; } + +.fa-draft2digital:before { + content: "\f396"; } + +.fa-dribbble:before { + content: "\f17d"; } + +.fa-dribbble-square:before { + content: "\f397"; } + +.fa-dropbox:before { + content: "\f16b"; } + +.fa-drupal:before { + content: "\f1a9"; } + +.fa-dyalog:before { + content: "\f399"; } + +.fa-earlybirds:before { + content: "\f39a"; } + +.fa-edge:before { + content: "\f282"; } + +.fa-edit:before { + content: "\f044"; } + +.fa-eject:before { + content: "\f052"; } + +.fa-elementor:before { + content: "\f430"; } + +.fa-ellipsis-h:before { + content: "\f141"; } + +.fa-ellipsis-v:before { + content: "\f142"; } + +.fa-ember:before { + content: "\f423"; } + +.fa-empire:before { + content: "\f1d1"; } + +.fa-envelope:before { + content: "\f0e0"; } + +.fa-envelope-open:before { + content: "\f2b6"; } + +.fa-envelope-square:before { + content: "\f199"; } + +.fa-envira:before { + content: "\f299"; } + +.fa-eraser:before { + content: "\f12d"; } + +.fa-erlang:before { + content: "\f39d"; } + +.fa-ethereum:before { + content: "\f42e"; } + +.fa-etsy:before { + content: "\f2d7"; } + +.fa-euro-sign:before { + content: "\f153"; } + +.fa-exchange-alt:before { + content: "\f362"; } + +.fa-exclamation:before { + content: "\f12a"; } + +.fa-exclamation-circle:before { + content: "\f06a"; } + +.fa-exclamation-triangle:before { + content: "\f071"; } + +.fa-expand:before { + content: "\f065"; } + +.fa-expand-arrows-alt:before { + content: "\f31e"; } + +.fa-expeditedssl:before { + content: "\f23e"; } + +.fa-external-link-alt:before { + content: "\f35d"; } + +.fa-external-link-square-alt:before { + content: "\f360"; } + +.fa-eye:before { + content: "\f06e"; } + +.fa-eye-dropper:before { + content: "\f1fb"; } + +.fa-eye-slash:before { + content: "\f070"; } + +.fa-facebook:before { + content: "\f09a"; } + +.fa-facebook-f:before { + content: "\f39e"; } + +.fa-facebook-messenger:before { + content: "\f39f"; } + +.fa-facebook-square:before { + content: "\f082"; } + +.fa-fast-backward:before { + content: "\f049"; } + +.fa-fast-forward:before { + content: "\f050"; } + +.fa-fax:before { + content: "\f1ac"; } + +.fa-female:before { + content: "\f182"; } + +.fa-fighter-jet:before { + content: "\f0fb"; } + +.fa-file:before { + content: "\f15b"; } + +.fa-file-alt:before { + content: "\f15c"; } + +.fa-file-archive:before { + content: "\f1c6"; } + +.fa-file-audio:before { + content: "\f1c7"; } + +.fa-file-code:before { + content: "\f1c9"; } + +.fa-file-excel:before { + content: "\f1c3"; } + +.fa-file-image:before { + content: "\f1c5"; } + +.fa-file-pdf:before { + content: "\f1c1"; } + +.fa-file-powerpoint:before { + content: "\f1c4"; } + +.fa-file-video:before { + content: "\f1c8"; } + +.fa-file-word:before { + content: "\f1c2"; } + +.fa-film:before { + content: "\f008"; } + +.fa-filter:before { + content: "\f0b0"; } + +.fa-fire:before { + content: "\f06d"; } + +.fa-fire-extinguisher:before { + content: "\f134"; } + +.fa-firefox:before { + content: "\f269"; } + +.fa-first-order:before { + content: "\f2b0"; } + +.fa-firstdraft:before { + content: "\f3a1"; } + +.fa-flag:before { + content: "\f024"; } + +.fa-flag-checkered:before { + content: "\f11e"; } + +.fa-flask:before { + content: "\f0c3"; } + +.fa-flickr:before { + content: "\f16e"; } + +.fa-fly:before { + content: "\f417"; } + +.fa-folder:before { + content: "\f07b"; } + +.fa-folder-open:before { + content: "\f07c"; } + +.fa-font:before { + content: "\f031"; } + +.fa-font-awesome:before { + content: "\f2b4"; } + +.fa-font-awesome-alt:before { + content: "\f35c"; } + +.fa-font-awesome-flag:before { + content: "\f425"; } + +.fa-fonticons:before { + content: "\f280"; } + +.fa-fonticons-fi:before { + content: "\f3a2"; } + +.fa-fort-awesome:before { + content: "\f286"; } + +.fa-fort-awesome-alt:before { + content: "\f3a3"; } + +.fa-forumbee:before { + content: "\f211"; } + +.fa-forward:before { + content: "\f04e"; } + +.fa-foursquare:before { + content: "\f180"; } + +.fa-free-code-camp:before { + content: "\f2c5"; } + +.fa-freebsd:before { + content: "\f3a4"; } + +.fa-frown:before { + content: "\f119"; } + +.fa-futbol:before { + content: "\f1e3"; } + +.fa-gamepad:before { + content: "\f11b"; } + +.fa-gavel:before { + content: "\f0e3"; } + +.fa-gem:before { + content: "\f3a5"; } + +.fa-genderless:before { + content: "\f22d"; } + +.fa-get-pocket:before { + content: "\f265"; } + +.fa-gg:before { + content: "\f260"; } + +.fa-gg-circle:before { + content: "\f261"; } + +.fa-gift:before { + content: "\f06b"; } + +.fa-git:before { + content: "\f1d3"; } + +.fa-git-square:before { + content: "\f1d2"; } + +.fa-github:before { + content: "\f09b"; } + +.fa-github-alt:before { + content: "\f113"; } + +.fa-github-square:before { + content: "\f092"; } + +.fa-gitkraken:before { + content: "\f3a6"; } + +.fa-gitlab:before { + content: "\f296"; } + +.fa-gitter:before { + content: "\f426"; } + +.fa-glass-martini:before { + content: "\f000"; } + +.fa-glide:before { + content: "\f2a5"; } + +.fa-glide-g:before { + content: "\f2a6"; } + +.fa-globe:before { + content: "\f0ac"; } + +.fa-gofore:before { + content: "\f3a7"; } + +.fa-goodreads:before { + content: "\f3a8"; } + +.fa-goodreads-g:before { + content: "\f3a9"; } + +.fa-google:before { + content: "\f1a0"; } + +.fa-google-drive:before { + content: "\f3aa"; } + +.fa-google-play:before { + content: "\f3ab"; } + +.fa-google-plus:before { + content: "\f2b3"; } + +.fa-google-plus-g:before { + content: "\f0d5"; } + +.fa-google-plus-square:before { + content: "\f0d4"; } + +.fa-google-wallet:before { + content: "\f1ee"; } + +.fa-graduation-cap:before { + content: "\f19d"; } + +.fa-gratipay:before { + content: "\f184"; } + +.fa-grav:before { + content: "\f2d6"; } + +.fa-gripfire:before { + content: "\f3ac"; } + +.fa-grunt:before { + content: "\f3ad"; } + +.fa-gulp:before { + content: "\f3ae"; } + +.fa-h-square:before { + content: "\f0fd"; } + +.fa-hacker-news:before { + content: "\f1d4"; } + +.fa-hacker-news-square:before { + content: "\f3af"; } + +.fa-hand-lizard:before { + content: "\f258"; } + +.fa-hand-paper:before { + content: "\f256"; } + +.fa-hand-peace:before { + content: "\f25b"; } + +.fa-hand-point-down:before { + content: "\f0a7"; } + +.fa-hand-point-left:before { + content: "\f0a5"; } + +.fa-hand-point-right:before { + content: "\f0a4"; } + +.fa-hand-point-up:before { + content: "\f0a6"; } + +.fa-hand-pointer:before { + content: "\f25a"; } + +.fa-hand-rock:before { + content: "\f255"; } + +.fa-hand-scissors:before { + content: "\f257"; } + +.fa-hand-spock:before { + content: "\f259"; } + +.fa-handshake:before { + content: "\f2b5"; } + +.fa-hashtag:before { + content: "\f292"; } + +.fa-hdd:before { + content: "\f0a0"; } + +.fa-heading:before { + content: "\f1dc"; } + +.fa-headphones:before { + content: "\f025"; } + +.fa-heart:before { + content: "\f004"; } + +.fa-heartbeat:before { + content: "\f21e"; } + +.fa-hire-a-helper:before { + content: "\f3b0"; } + +.fa-history:before { + content: "\f1da"; } + +.fa-home:before { + content: "\f015"; } + +.fa-hooli:before { + content: "\f427"; } + +.fa-hospital:before { + content: "\f0f8"; } + +.fa-hotjar:before { + content: "\f3b1"; } + +.fa-hourglass:before { + content: "\f254"; } + +.fa-hourglass-end:before { + content: "\f253"; } + +.fa-hourglass-half:before { + content: "\f252"; } + +.fa-hourglass-start:before { + content: "\f251"; } + +.fa-houzz:before { + content: "\f27c"; } + +.fa-html5:before { + content: "\f13b"; } + +.fa-hubspot:before { + content: "\f3b2"; } + +.fa-i-cursor:before { + content: "\f246"; } + +.fa-id-badge:before { + content: "\f2c1"; } + +.fa-id-card:before { + content: "\f2c2"; } + +.fa-image:before { + content: "\f03e"; } + +.fa-images:before { + content: "\f302"; } + +.fa-imdb:before { + content: "\f2d8"; } + +.fa-inbox:before { + content: "\f01c"; } + +.fa-indent:before { + content: "\f03c"; } + +.fa-industry:before { + content: "\f275"; } + +.fa-info:before { + content: "\f129"; } + +.fa-info-circle:before { + content: "\f05a"; } + +.fa-instagram:before { + content: "\f16d"; } + +.fa-internet-explorer:before { + content: "\f26b"; } + +.fa-ioxhost:before { + content: "\f208"; } + +.fa-italic:before { + content: "\f033"; } + +.fa-itunes:before { + content: "\f3b4"; } + +.fa-itunes-note:before { + content: "\f3b5"; } + +.fa-jenkins:before { + content: "\f3b6"; } + +.fa-joget:before { + content: "\f3b7"; } + +.fa-joomla:before { + content: "\f1aa"; } + +.fa-js:before { + content: "\f3b8"; } + +.fa-js-square:before { + content: "\f3b9"; } + +.fa-jsfiddle:before { + content: "\f1cc"; } + +.fa-key:before { + content: "\f084"; } + +.fa-keyboard:before { + content: "\f11c"; } + +.fa-keycdn:before { + content: "\f3ba"; } + +.fa-kickstarter:before { + content: "\f3bb"; } + +.fa-kickstarter-k:before { + content: "\f3bc"; } + +.fa-korvue:before { + content: "\f42f"; } + +.fa-language:before { + content: "\f1ab"; } + +.fa-laptop:before { + content: "\f109"; } + +.fa-laravel:before { + content: "\f3bd"; } + +.fa-lastfm:before { + content: "\f202"; } + +.fa-lastfm-square:before { + content: "\f203"; } + +.fa-leaf:before { + content: "\f06c"; } + +.fa-leanpub:before { + content: "\f212"; } + +.fa-lemon:before { + content: "\f094"; } + +.fa-less:before { + content: "\f41d"; } + +.fa-level-down-alt:before { + content: "\f3be"; } + +.fa-level-up-alt:before { + content: "\f3bf"; } + +.fa-life-ring:before { + content: "\f1cd"; } + +.fa-lightbulb:before { + content: "\f0eb"; } + +.fa-line:before { + content: "\f3c0"; } + +.fa-link:before { + content: "\f0c1"; } + +.fa-linkedin:before { + content: "\f08c"; } + +.fa-linkedin-in:before { + content: "\f0e1"; } + +.fa-linode:before { + content: "\f2b8"; } + +.fa-linux:before { + content: "\f17c"; } + +.fa-lira-sign:before { + content: "\f195"; } + +.fa-list:before { + content: "\f03a"; } + +.fa-list-alt:before { + content: "\f022"; } + +.fa-list-ol:before { + content: "\f0cb"; } + +.fa-list-ul:before { + content: "\f0ca"; } + +.fa-location-arrow:before { + content: "\f124"; } + +.fa-lock:before { + content: "\f023"; } + +.fa-lock-open:before { + content: "\f3c1"; } + +.fa-long-arrow-alt-down:before { + content: "\f309"; } + +.fa-long-arrow-alt-left:before { + content: "\f30a"; } + +.fa-long-arrow-alt-right:before { + content: "\f30b"; } + +.fa-long-arrow-alt-up:before { + content: "\f30c"; } + +.fa-low-vision:before { + content: "\f2a8"; } + +.fa-lyft:before { + content: "\f3c3"; } + +.fa-magento:before { + content: "\f3c4"; } + +.fa-magic:before { + content: "\f0d0"; } + +.fa-magnet:before { + content: "\f076"; } + +.fa-male:before { + content: "\f183"; } + +.fa-map:before { + content: "\f279"; } + +.fa-map-marker:before { + content: "\f041"; } + +.fa-map-marker-alt:before { + content: "\f3c5"; } + +.fa-map-pin:before { + content: "\f276"; } + +.fa-map-signs:before { + content: "\f277"; } + +.fa-mars:before { + content: "\f222"; } + +.fa-mars-double:before { + content: "\f227"; } + +.fa-mars-stroke:before { + content: "\f229"; } + +.fa-mars-stroke-h:before { + content: "\f22b"; } + +.fa-mars-stroke-v:before { + content: "\f22a"; } + +.fa-maxcdn:before { + content: "\f136"; } + +.fa-medapps:before { + content: "\f3c6"; } + +.fa-medium:before { + content: "\f23a"; } + +.fa-medium-m:before { + content: "\f3c7"; } + +.fa-medkit:before { + content: "\f0fa"; } + +.fa-medrt:before { + content: "\f3c8"; } + +.fa-meetup:before { + content: "\f2e0"; } + +.fa-meh:before { + content: "\f11a"; } + +.fa-mercury:before { + content: "\f223"; } + +.fa-microchip:before { + content: "\f2db"; } + +.fa-microphone:before { + content: "\f130"; } + +.fa-microphone-slash:before { + content: "\f131"; } + +.fa-microsoft:before { + content: "\f3ca"; } + +.fa-minus:before { + content: "\f068"; } + +.fa-minus-circle:before { + content: "\f056"; } + +.fa-minus-square:before { + content: "\f146"; } + +.fa-mix:before { + content: "\f3cb"; } + +.fa-mixcloud:before { + content: "\f289"; } + +.fa-mizuni:before { + content: "\f3cc"; } + +.fa-mobile:before { + content: "\f10b"; } + +.fa-mobile-alt:before { + content: "\f3cd"; } + +.fa-modx:before { + content: "\f285"; } + +.fa-monero:before { + content: "\f3d0"; } + +.fa-money-bill-alt:before { + content: "\f3d1"; } + +.fa-moon:before { + content: "\f186"; } + +.fa-motorcycle:before { + content: "\f21c"; } + +.fa-mouse-pointer:before { + content: "\f245"; } + +.fa-music:before { + content: "\f001"; } + +.fa-napster:before { + content: "\f3d2"; } + +.fa-neuter:before { + content: "\f22c"; } + +.fa-newspaper:before { + content: "\f1ea"; } + +.fa-nintendo-switch:before { + content: "\f418"; } + +.fa-node:before { + content: "\f419"; } + +.fa-node-js:before { + content: "\f3d3"; } + +.fa-npm:before { + content: "\f3d4"; } + +.fa-ns8:before { + content: "\f3d5"; } + +.fa-nutritionix:before { + content: "\f3d6"; } + +.fa-object-group:before { + content: "\f247"; } + +.fa-object-ungroup:before { + content: "\f248"; } + +.fa-odnoklassniki:before { + content: "\f263"; } + +.fa-odnoklassniki-square:before { + content: "\f264"; } + +.fa-opencart:before { + content: "\f23d"; } + +.fa-openid:before { + content: "\f19b"; } + +.fa-opera:before { + content: "\f26a"; } + +.fa-optin-monster:before { + content: "\f23c"; } + +.fa-osi:before { + content: "\f41a"; } + +.fa-outdent:before { + content: "\f03b"; } + +.fa-page4:before { + content: "\f3d7"; } + +.fa-pagelines:before { + content: "\f18c"; } + +.fa-paint-brush:before { + content: "\f1fc"; } + +.fa-palfed:before { + content: "\f3d8"; } + +.fa-paper-plane:before { + content: "\f1d8"; } + +.fa-paperclip:before { + content: "\f0c6"; } + +.fa-paragraph:before { + content: "\f1dd"; } + +.fa-paste:before { + content: "\f0ea"; } + +.fa-patreon:before { + content: "\f3d9"; } + +.fa-pause:before { + content: "\f04c"; } + +.fa-pause-circle:before { + content: "\f28b"; } + +.fa-paw:before { + content: "\f1b0"; } + +.fa-paypal:before { + content: "\f1ed"; } + +.fa-pen-square:before { + content: "\f14b"; } + +.fa-pencil-alt:before { + content: "\f303"; } + +.fa-percent:before { + content: "\f295"; } + +.fa-periscope:before { + content: "\f3da"; } + +.fa-phabricator:before { + content: "\f3db"; } + +.fa-phoenix-framework:before { + content: "\f3dc"; } + +.fa-phone:before { + content: "\f095"; } + +.fa-phone-square:before { + content: "\f098"; } + +.fa-phone-volume:before { + content: "\f2a0"; } + +.fa-pied-piper:before { + content: "\f2ae"; } + +.fa-pied-piper-alt:before { + content: "\f1a8"; } + +.fa-pied-piper-pp:before { + content: "\f1a7"; } + +.fa-pinterest:before { + content: "\f0d2"; } + +.fa-pinterest-p:before { + content: "\f231"; } + +.fa-pinterest-square:before { + content: "\f0d3"; } + +.fa-plane:before { + content: "\f072"; } + +.fa-play:before { + content: "\f04b"; } + +.fa-play-circle:before { + content: "\f144"; } + +.fa-playstation:before { + content: "\f3df"; } + +.fa-plug:before { + content: "\f1e6"; } + +.fa-plus:before { + content: "\f067"; } + +.fa-plus-circle:before { + content: "\f055"; } + +.fa-plus-square:before { + content: "\f0fe"; } + +.fa-podcast:before { + content: "\f2ce"; } + +.fa-pound-sign:before { + content: "\f154"; } + +.fa-power-off:before { + content: "\f011"; } + +.fa-print:before { + content: "\f02f"; } + +.fa-product-hunt:before { + content: "\f288"; } + +.fa-pushed:before { + content: "\f3e1"; } + +.fa-puzzle-piece:before { + content: "\f12e"; } + +.fa-python:before { + content: "\f3e2"; } + +.fa-qq:before { + content: "\f1d6"; } + +.fa-qrcode:before { + content: "\f029"; } + +.fa-question:before { + content: "\f128"; } + +.fa-question-circle:before { + content: "\f059"; } + +.fa-quora:before { + content: "\f2c4"; } + +.fa-quote-left:before { + content: "\f10d"; } + +.fa-quote-right:before { + content: "\f10e"; } + +.fa-random:before { + content: "\f074"; } + +.fa-ravelry:before { + content: "\f2d9"; } + +.fa-react:before { + content: "\f41b"; } + +.fa-rebel:before { + content: "\f1d0"; } + +.fa-recycle:before { + content: "\f1b8"; } + +.fa-red-river:before { + content: "\f3e3"; } + +.fa-reddit:before { + content: "\f1a1"; } + +.fa-reddit-alien:before { + content: "\f281"; } + +.fa-reddit-square:before { + content: "\f1a2"; } + +.fa-redo:before { + content: "\f01e"; } + +.fa-redo-alt:before { + content: "\f2f9"; } + +.fa-registered:before { + content: "\f25d"; } + +.fa-rendact:before { + content: "\f3e4"; } + +.fa-renren:before { + content: "\f18b"; } + +.fa-reply:before { + content: "\f3e5"; } + +.fa-reply-all:before { + content: "\f122"; } + +.fa-replyd:before { + content: "\f3e6"; } + +.fa-resolving:before { + content: "\f3e7"; } + +.fa-retweet:before { + content: "\f079"; } + +.fa-road:before { + content: "\f018"; } + +.fa-rocket:before { + content: "\f135"; } + +.fa-rocketchat:before { + content: "\f3e8"; } + +.fa-rockrms:before { + content: "\f3e9"; } + +.fa-rss:before { + content: "\f09e"; } + +.fa-rss-square:before { + content: "\f143"; } + +.fa-ruble-sign:before { + content: "\f158"; } + +.fa-rupee-sign:before { + content: "\f156"; } + +.fa-safari:before { + content: "\f267"; } + +.fa-sass:before { + content: "\f41e"; } + +.fa-save:before { + content: "\f0c7"; } + +.fa-schlix:before { + content: "\f3ea"; } + +.fa-scribd:before { + content: "\f28a"; } + +.fa-search:before { + content: "\f002"; } + +.fa-search-minus:before { + content: "\f010"; } + +.fa-search-plus:before { + content: "\f00e"; } + +.fa-searchengin:before { + content: "\f3eb"; } + +.fa-sellcast:before { + content: "\f2da"; } + +.fa-sellsy:before { + content: "\f213"; } + +.fa-server:before { + content: "\f233"; } + +.fa-servicestack:before { + content: "\f3ec"; } + +.fa-share:before { + content: "\f064"; } + +.fa-share-alt:before { + content: "\f1e0"; } + +.fa-share-alt-square:before { + content: "\f1e1"; } + +.fa-share-square:before { + content: "\f14d"; } + +.fa-shekel-sign:before { + content: "\f20b"; } + +.fa-shield-alt:before { + content: "\f3ed"; } + +.fa-ship:before { + content: "\f21a"; } + +.fa-shirtsinbulk:before { + content: "\f214"; } + +.fa-shopping-bag:before { + content: "\f290"; } + +.fa-shopping-basket:before { + content: "\f291"; } + +.fa-shopping-cart:before { + content: "\f07a"; } + +.fa-shower:before { + content: "\f2cc"; } + +.fa-sign-in-alt:before { + content: "\f2f6"; } + +.fa-sign-language:before { + content: "\f2a7"; } + +.fa-sign-out-alt:before { + content: "\f2f5"; } + +.fa-signal:before { + content: "\f012"; } + +.fa-simplybuilt:before { + content: "\f215"; } + +.fa-sistrix:before { + content: "\f3ee"; } + +.fa-sitemap:before { + content: "\f0e8"; } + +.fa-skyatlas:before { + content: "\f216"; } + +.fa-skype:before { + content: "\f17e"; } + +.fa-slack:before { + content: "\f198"; } + +.fa-slack-hash:before { + content: "\f3ef"; } + +.fa-sliders-h:before { + content: "\f1de"; } + +.fa-slideshare:before { + content: "\f1e7"; } + +.fa-smile:before { + content: "\f118"; } + +.fa-snapchat:before { + content: "\f2ab"; } + +.fa-snapchat-ghost:before { + content: "\f2ac"; } + +.fa-snapchat-square:before { + content: "\f2ad"; } + +.fa-snowflake:before { + content: "\f2dc"; } + +.fa-sort:before { + content: "\f0dc"; } + +.fa-sort-alpha-down:before { + content: "\f15d"; } + +.fa-sort-alpha-up:before { + content: "\f15e"; } + +.fa-sort-amount-down:before { + content: "\f160"; } + +.fa-sort-amount-up:before { + content: "\f161"; } + +.fa-sort-down:before { + content: "\f0dd"; } + +.fa-sort-numeric-down:before { + content: "\f162"; } + +.fa-sort-numeric-up:before { + content: "\f163"; } + +.fa-sort-up:before { + content: "\f0de"; } + +.fa-soundcloud:before { + content: "\f1be"; } + +.fa-space-shuttle:before { + content: "\f197"; } + +.fa-speakap:before { + content: "\f3f3"; } + +.fa-spinner:before { + content: "\f110"; } + +.fa-spotify:before { + content: "\f1bc"; } + +.fa-square:before { + content: "\f0c8"; } + +.fa-stack-exchange:before { + content: "\f18d"; } + +.fa-stack-overflow:before { + content: "\f16c"; } + +.fa-star:before { + content: "\f005"; } + +.fa-star-half:before { + content: "\f089"; } + +.fa-staylinked:before { + content: "\f3f5"; } + +.fa-steam:before { + content: "\f1b6"; } + +.fa-steam-square:before { + content: "\f1b7"; } + +.fa-steam-symbol:before { + content: "\f3f6"; } + +.fa-step-backward:before { + content: "\f048"; } + +.fa-step-forward:before { + content: "\f051"; } + +.fa-stethoscope:before { + content: "\f0f1"; } + +.fa-sticker-mule:before { + content: "\f3f7"; } + +.fa-sticky-note:before { + content: "\f249"; } + +.fa-stop:before { + content: "\f04d"; } + +.fa-stop-circle:before { + content: "\f28d"; } + +.fa-stopwatch:before { + content: "\f2f2"; } + +.fa-strava:before { + content: "\f428"; } + +.fa-street-view:before { + content: "\f21d"; } + +.fa-strikethrough:before { + content: "\f0cc"; } + +.fa-stripe:before { + content: "\f429"; } + +.fa-stripe-s:before { + content: "\f42a"; } + +.fa-studiovinari:before { + content: "\f3f8"; } + +.fa-stumbleupon:before { + content: "\f1a4"; } + +.fa-stumbleupon-circle:before { + content: "\f1a3"; } + +.fa-subscript:before { + content: "\f12c"; } + +.fa-subway:before { + content: "\f239"; } + +.fa-suitcase:before { + content: "\f0f2"; } + +.fa-sun:before { + content: "\f185"; } + +.fa-superpowers:before { + content: "\f2dd"; } + +.fa-superscript:before { + content: "\f12b"; } + +.fa-supple:before { + content: "\f3f9"; } + +.fa-sync:before { + content: "\f021"; } + +.fa-sync-alt:before { + content: "\f2f1"; } + +.fa-table:before { + content: "\f0ce"; } + +.fa-tablet:before { + content: "\f10a"; } + +.fa-tablet-alt:before { + content: "\f3fa"; } + +.fa-tachometer-alt:before { + content: "\f3fd"; } + +.fa-tag:before { + content: "\f02b"; } + +.fa-tags:before { + content: "\f02c"; } + +.fa-tasks:before { + content: "\f0ae"; } + +.fa-taxi:before { + content: "\f1ba"; } + +.fa-telegram:before { + content: "\f2c6"; } + +.fa-telegram-plane:before { + content: "\f3fe"; } + +.fa-tencent-weibo:before { + content: "\f1d5"; } + +.fa-terminal:before { + content: "\f120"; } + +.fa-text-height:before { + content: "\f034"; } + +.fa-text-width:before { + content: "\f035"; } + +.fa-th:before { + content: "\f00a"; } + +.fa-th-large:before { + content: "\f009"; } + +.fa-th-list:before { + content: "\f00b"; } + +.fa-themeisle:before { + content: "\f2b2"; } + +.fa-thermometer-empty:before { + content: "\f2cb"; } + +.fa-thermometer-full:before { + content: "\f2c7"; } + +.fa-thermometer-half:before { + content: "\f2c9"; } + +.fa-thermometer-quarter:before { + content: "\f2ca"; } + +.fa-thermometer-three-quarters:before { + content: "\f2c8"; } + +.fa-thumbs-down:before { + content: "\f165"; } + +.fa-thumbs-up:before { + content: "\f164"; } + +.fa-thumbtack:before { + content: "\f08d"; } + +.fa-ticket-alt:before { + content: "\f3ff"; } + +.fa-times:before { + content: "\f00d"; } + +.fa-times-circle:before { + content: "\f057"; } + +.fa-tint:before { + content: "\f043"; } + +.fa-toggle-off:before { + content: "\f204"; } + +.fa-toggle-on:before { + content: "\f205"; } + +.fa-trademark:before { + content: "\f25c"; } + +.fa-train:before { + content: "\f238"; } + +.fa-transgender:before { + content: "\f224"; } + +.fa-transgender-alt:before { + content: "\f225"; } + +.fa-trash:before { + content: "\f1f8"; } + +.fa-trash-alt:before { + content: "\f2ed"; } + +.fa-tree:before { + content: "\f1bb"; } + +.fa-trello:before { + content: "\f181"; } + +.fa-tripadvisor:before { + content: "\f262"; } + +.fa-trophy:before { + content: "\f091"; } + +.fa-truck:before { + content: "\f0d1"; } + +.fa-tty:before { + content: "\f1e4"; } + +.fa-tumblr:before { + content: "\f173"; } + +.fa-tumblr-square:before { + content: "\f174"; } + +.fa-tv:before { + content: "\f26c"; } + +.fa-twitch:before { + content: "\f1e8"; } + +.fa-twitter:before { + content: "\f099"; } + +.fa-twitter-square:before { + content: "\f081"; } + +.fa-typo3:before { + content: "\f42b"; } + +.fa-uber:before { + content: "\f402"; } + +.fa-uikit:before { + content: "\f403"; } + +.fa-umbrella:before { + content: "\f0e9"; } + +.fa-underline:before { + content: "\f0cd"; } + +.fa-undo:before { + content: "\f0e2"; } + +.fa-undo-alt:before { + content: "\f2ea"; } + +.fa-uniregistry:before { + content: "\f404"; } + +.fa-universal-access:before { + content: "\f29a"; } + +.fa-university:before { + content: "\f19c"; } + +.fa-unlink:before { + content: "\f127"; } + +.fa-unlock:before { + content: "\f09c"; } + +.fa-unlock-alt:before { + content: "\f13e"; } + +.fa-untappd:before { + content: "\f405"; } + +.fa-upload:before { + content: "\f093"; } + +.fa-usb:before { + content: "\f287"; } + +.fa-user:before { + content: "\f007"; } + +.fa-user-circle:before { + content: "\f2bd"; } + +.fa-user-md:before { + content: "\f0f0"; } + +.fa-user-plus:before { + content: "\f234"; } + +.fa-user-secret:before { + content: "\f21b"; } + +.fa-user-times:before { + content: "\f235"; } + +.fa-users:before { + content: "\f0c0"; } + +.fa-ussunnah:before { + content: "\f407"; } + +.fa-utensil-spoon:before { + content: "\f2e5"; } + +.fa-utensils:before { + content: "\f2e7"; } + +.fa-vaadin:before { + content: "\f408"; } + +.fa-venus:before { + content: "\f221"; } + +.fa-venus-double:before { + content: "\f226"; } + +.fa-venus-mars:before { + content: "\f228"; } + +.fa-viacoin:before { + content: "\f237"; } + +.fa-viadeo:before { + content: "\f2a9"; } + +.fa-viadeo-square:before { + content: "\f2aa"; } + +.fa-viber:before { + content: "\f409"; } + +.fa-video:before { + content: "\f03d"; } + +.fa-vimeo:before { + content: "\f40a"; } + +.fa-vimeo-square:before { + content: "\f194"; } + +.fa-vimeo-v:before { + content: "\f27d"; } + +.fa-vine:before { + content: "\f1ca"; } + +.fa-vk:before { + content: "\f189"; } + +.fa-vnv:before { + content: "\f40b"; } + +.fa-volume-down:before { + content: "\f027"; } + +.fa-volume-off:before { + content: "\f026"; } + +.fa-volume-up:before { + content: "\f028"; } + +.fa-vuejs:before { + content: "\f41f"; } + +.fa-weibo:before { + content: "\f18a"; } + +.fa-weixin:before { + content: "\f1d7"; } + +.fa-whatsapp:before { + content: "\f232"; } + +.fa-whatsapp-square:before { + content: "\f40c"; } + +.fa-wheelchair:before { + content: "\f193"; } + +.fa-whmcs:before { + content: "\f40d"; } + +.fa-wifi:before { + content: "\f1eb"; } + +.fa-wikipedia-w:before { + content: "\f266"; } + +.fa-window-close:before { + content: "\f410"; } + +.fa-window-maximize:before { + content: "\f2d0"; } + +.fa-window-minimize:before { + content: "\f2d1"; } + +.fa-window-restore:before { + content: "\f2d2"; } + +.fa-windows:before { + content: "\f17a"; } + +.fa-won-sign:before { + content: "\f159"; } + +.fa-wordpress:before { + content: "\f19a"; } + +.fa-wordpress-simple:before { + content: "\f411"; } + +.fa-wpbeginner:before { + content: "\f297"; } + +.fa-wpexplorer:before { + content: "\f2de"; } + +.fa-wpforms:before { + content: "\f298"; } + +.fa-wrench:before { + content: "\f0ad"; } + +.fa-xbox:before { + content: "\f412"; } + +.fa-xing:before { + content: "\f168"; } + +.fa-xing-square:before { + content: "\f169"; } + +.fa-y-combinator:before { + content: "\f23b"; } + +.fa-yahoo:before { + content: "\f19e"; } + +.fa-yandex:before { + content: "\f413"; } + +.fa-yandex-international:before { + content: "\f414"; } + +.fa-yelp:before { + content: "\f1e9"; } + +.fa-yen-sign:before { + content: "\f157"; } + +.fa-yoast:before { + content: "\f2b1"; } + +.fa-youtube:before { + content: "\f167"; } + +.fa-youtube-square:before { + content: "\f431"; } + +.sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } + +.sr-only-focusable:active, .sr-only-focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; } diff --git a/core/static/fontawesome/css/fontawesome.min.css b/core/static/fontawesome/css/fontawesome.min.css new file mode 100644 index 00000000..6b3fe1ec --- /dev/null +++ b/core/static/fontawesome/css/fontawesome.min.css @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.0.4 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +.fa,.fab,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:a 2s infinite linear;animation:a 2s infinite linear}.fa-pulse{-webkit-animation:a 1s infinite steps(8);animation:a 1s infinite steps(8)}@-webkit-keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes a{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-aws:before{content:"\f375"}.fa-backward:before{content:"\f04a"}.fa-balance-scale:before{content:"\f24e"}.fa-ban:before{content:"\f05e"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bicycle:before{content:"\f206"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blind:before{content:"\f29d"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-braille:before{content:"\f2a1"}.fa-briefcase:before{content:"\f0b1"}.fa-btc:before{content:"\f15a"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-car:before{content:"\f1b9"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-certificate:before{content:"\f0a3"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-square:before{content:"\f14a"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-clipboard:before{content:"\f328"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comments:before{content:"\f086"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-credit-card:before{content:"\f09d"}.fa-crop:before{content:"\f125"}.fa-crosshairs:before{content:"\f05b"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-deviantart:before{content:"\f1bd"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dollar-sign:before{content:"\f155"}.fa-dot-circle:before{content:"\f192"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drupal:before{content:"\f1a9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-fax:before{content:"\f1ac"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-excel:before{content:"\f1c3"}.fa-file-image:before{content:"\f1c5"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fire:before{content:"\f06d"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-first-order:before{content:"\f2b0"}.fa-firstdraft:before{content:"\f3a1"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frown:before{content:"\f119"}.fa-futbol:before{content:"\f1e3"}.fa-gamepad:before{content:"\f11b"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-gift:before{content:"\f06b"}.fa-git:before{content:"\f1d3"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-martini:before{content:"\f000"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-gofore:before{content:"\f3a7"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-spock:before{content:"\f259"}.fa-handshake:before{content:"\f2b5"}.fa-hashtag:before{content:"\f292"}.fa-hdd:before{content:"\f0a0"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-heart:before{content:"\f004"}.fa-heartbeat:before{content:"\f21e"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hospital:before{content:"\f0f8"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-houzz:before{content:"\f27c"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-internet-explorer:before{content:"\f26b"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-jenkins:before{content:"\f3b6"}.fa-joget:before{content:"\f3b7"}.fa-joomla:before{content:"\f1aa"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-key:before{content:"\f084"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-korvue:before{content:"\f42f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-male:before{content:"\f183"}.fa-map:before{content:"\f279"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-maxcdn:before{content:"\f136"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-meh:before{content:"\f11a"}.fa-mercury:before{content:"\f223"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-moon:before{content:"\f186"}.fa-motorcycle:before{content:"\f21c"}.fa-mouse-pointer:before{content:"\f245"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nintendo-switch:before{content:"\f418"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-osi:before{content:"\f41a"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-paint-brush:before{content:"\f1fc"}.fa-palfed:before{content:"\f3d8"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-paragraph:before{content:"\f1dd"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-percent:before{content:"\f295"}.fa-periscope:before{content:"\f3da"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phone:before{content:"\f095"}.fa-phone-square:before{content:"\f098"}.fa-phone-volume:before{content:"\f2a0"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-plane:before{content:"\f072"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-print:before{content:"\f02f"}.fa-product-hunt:before{content:"\f288"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-random:before{content:"\f074"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-rebel:before{content:"\f1d0"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-rendact:before{content:"\f3e4"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-resolving:before{content:"\f3e7"}.fa-retweet:before{content:"\f079"}.fa-road:before{content:"\f018"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-rupee-sign:before{content:"\f156"}.fa-safari:before{content:"\f267"}.fa-sass:before{content:"\f41e"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-scribd:before{content:"\f28a"}.fa-search:before{content:"\f002"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-ship:before{content:"\f21a"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shower:before{content:"\f2cc"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowflake:before{content:"\f2dc"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-spinner:before{content:"\f110"}.fa-spotify:before{content:"\f1bc"}.fa-square:before{content:"\f0c8"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-star:before{content:"\f005"}.fa-star-half:before{content:"\f089"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-strava:before{content:"\f428"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-table:before{content:"\f0ce"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-trademark:before{content:"\f25c"}.fa-train:before{content:"\f238"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-uikit:before{content:"\f403"}.fa-umbrella:before{content:"\f0e9"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-circle:before{content:"\f2bd"}.fa-user-md:before{content:"\f0f0"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-volume-down:before{content:"\f027"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vuejs:before{content:"\f41f"}.fa-weibo:before{content:"\f18a"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wrench:before{content:"\f0ad"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto} \ No newline at end of file diff --git a/core/static/fontawesome/webfonts/fa-brands-400.eot b/core/static/fontawesome/webfonts/fa-brands-400.eot new file mode 100644 index 00000000..45f12a12 Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-brands-400.eot differ diff --git a/core/static/fontawesome/webfonts/fa-brands-400.svg b/core/static/fontawesome/webfonts/fa-brands-400.svg new file mode 100644 index 00000000..2f26609a --- /dev/null +++ b/core/static/fontawesome/webfonts/fa-brands-400.svg @@ -0,0 +1,996 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/static/fontawesome/webfonts/fa-brands-400.ttf b/core/static/fontawesome/webfonts/fa-brands-400.ttf new file mode 100644 index 00000000..ed62125e Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-brands-400.ttf differ diff --git a/core/static/fontawesome/webfonts/fa-brands-400.woff b/core/static/fontawesome/webfonts/fa-brands-400.woff new file mode 100644 index 00000000..dc90ab13 Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-brands-400.woff differ diff --git a/core/static/fontawesome/webfonts/fa-brands-400.woff2 b/core/static/fontawesome/webfonts/fa-brands-400.woff2 new file mode 100644 index 00000000..d14f86eb Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-brands-400.woff2 differ diff --git a/core/static/fontawesome/webfonts/fa-regular-400.eot b/core/static/fontawesome/webfonts/fa-regular-400.eot new file mode 100644 index 00000000..04e70a6b Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-regular-400.eot differ diff --git a/core/static/fontawesome/webfonts/fa-regular-400.svg b/core/static/fontawesome/webfonts/fa-regular-400.svg new file mode 100644 index 00000000..53af35e1 --- /dev/null +++ b/core/static/fontawesome/webfonts/fa-regular-400.svg @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/static/fontawesome/webfonts/fa-regular-400.ttf b/core/static/fontawesome/webfonts/fa-regular-400.ttf new file mode 100644 index 00000000..c295db26 Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-regular-400.ttf differ diff --git a/core/static/fontawesome/webfonts/fa-regular-400.woff b/core/static/fontawesome/webfonts/fa-regular-400.woff new file mode 100644 index 00000000..63bc97f1 Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-regular-400.woff differ diff --git a/core/static/fontawesome/webfonts/fa-regular-400.woff2 b/core/static/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 00000000..5a43846a Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/core/static/fontawesome/webfonts/fa-solid-900.eot b/core/static/fontawesome/webfonts/fa-solid-900.eot new file mode 100644 index 00000000..9a0f667a Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-solid-900.eot differ diff --git a/core/static/fontawesome/webfonts/fa-solid-900.svg b/core/static/fontawesome/webfonts/fa-solid-900.svg new file mode 100644 index 00000000..353825d7 --- /dev/null +++ b/core/static/fontawesome/webfonts/fa-solid-900.svg @@ -0,0 +1,1413 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/static/fontawesome/webfonts/fa-solid-900.ttf b/core/static/fontawesome/webfonts/fa-solid-900.ttf new file mode 100644 index 00000000..f5e18d28 Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-solid-900.ttf differ diff --git a/core/static/fontawesome/webfonts/fa-solid-900.woff b/core/static/fontawesome/webfonts/fa-solid-900.woff new file mode 100644 index 00000000..5a97cf4b Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-solid-900.woff differ diff --git a/core/static/fontawesome/webfonts/fa-solid-900.woff2 b/core/static/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100644 index 00000000..13bf5c08 Binary files /dev/null and b/core/static/fontawesome/webfonts/fa-solid-900.woff2 differ diff --git a/core/static/html/update-in-progress.is.html b/core/static/html/update-in-progress.is.html new file mode 100644 index 00000000..2f2a5aff --- /dev/null +++ b/core/static/html/update-in-progress.is.html @@ -0,0 +1,25 @@ + + + +Verið er að uppfæra kosningakerfi Pírata. + + + + +

+

Verið er að uppfæra kosningakerfi Pírata.

+ +

Það verður (að öllum líkindum) aðgengilegt aftur innan skamms.

+ + + diff --git a/wasa2il/core/static/img/blank-user-icon.jpg b/core/static/img/blank-user-icon.jpg similarity index 100% rename from wasa2il/core/static/img/blank-user-icon.jpg rename to core/static/img/blank-user-icon.jpg diff --git a/wasa2il/core/static/img/demo-characters/candidate-1.png b/core/static/img/demo-characters/candidate-1.png similarity index 100% rename from wasa2il/core/static/img/demo-characters/candidate-1.png rename to core/static/img/demo-characters/candidate-1.png diff --git a/wasa2il/core/static/img/demo-characters/candidate-2.png b/core/static/img/demo-characters/candidate-2.png similarity index 100% rename from wasa2il/core/static/img/demo-characters/candidate-2.png rename to core/static/img/demo-characters/candidate-2.png diff --git a/wasa2il/core/static/img/demo-characters/johndoe.png b/core/static/img/demo-characters/johndoe.png similarity index 100% rename from wasa2il/core/static/img/demo-characters/johndoe.png rename to core/static/img/demo-characters/johndoe.png diff --git a/wasa2il/core/static/img/header-bg.png b/core/static/img/header-bg.png similarity index 100% rename from wasa2il/core/static/img/header-bg.png rename to core/static/img/header-bg.png diff --git a/core/static/img/heroes/fistbump.jpg b/core/static/img/heroes/fistbump.jpg new file mode 100644 index 00000000..76bd8f3a Binary files /dev/null and b/core/static/img/heroes/fistbump.jpg differ diff --git a/wasa2il/core/static/img/instructions/is/election-candidacy/1.Innskraning.png b/core/static/img/instructions/is/election-candidacy/1.Innskraning.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-candidacy/1.Innskraning.png rename to core/static/img/instructions/is/election-candidacy/1.Innskraning.png diff --git a/wasa2il/core/static/img/instructions/is/election-candidacy/2.Notandastillingar, skref 1.png b/core/static/img/instructions/is/election-candidacy/2.Notandastillingar, skref 1.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-candidacy/2.Notandastillingar, skref 1.png rename to core/static/img/instructions/is/election-candidacy/2.Notandastillingar, skref 1.png diff --git a/wasa2il/core/static/img/instructions/is/election-candidacy/3.Notandastillingar, skref 2.png b/core/static/img/instructions/is/election-candidacy/3.Notandastillingar, skref 2.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-candidacy/3.Notandastillingar, skref 2.png rename to core/static/img/instructions/is/election-candidacy/3.Notandastillingar, skref 2.png diff --git a/wasa2il/core/static/img/instructions/is/election-candidacy/4.Notandastillingar, skref 3.png b/core/static/img/instructions/is/election-candidacy/4.Notandastillingar, skref 3.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-candidacy/4.Notandastillingar, skref 3.png rename to core/static/img/instructions/is/election-candidacy/4.Notandastillingar, skref 3.png diff --git a/wasa2il/core/static/img/instructions/is/election-candidacy/5.Velja undirthing.png b/core/static/img/instructions/is/election-candidacy/5.Velja undirthing.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-candidacy/5.Velja undirthing.png rename to core/static/img/instructions/is/election-candidacy/5.Velja undirthing.png diff --git a/wasa2il/core/static/img/instructions/is/election-candidacy/6.Velja kosningu.png b/core/static/img/instructions/is/election-candidacy/6.Velja kosningu.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-candidacy/6.Velja kosningu.png rename to core/static/img/instructions/is/election-candidacy/6.Velja kosningu.png diff --git a/wasa2il/core/static/img/instructions/is/election-candidacy/7.Tilkynna frambod.png b/core/static/img/instructions/is/election-candidacy/7.Tilkynna frambod.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-candidacy/7.Tilkynna frambod.png rename to core/static/img/instructions/is/election-candidacy/7.Tilkynna frambod.png diff --git a/wasa2il/core/static/img/instructions/is/election-candidacy/8.Sja kosningu med frambodi.png b/core/static/img/instructions/is/election-candidacy/8.Sja kosningu med frambodi.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-candidacy/8.Sja kosningu med frambodi.png rename to core/static/img/instructions/is/election-candidacy/8.Sja kosningu med frambodi.png diff --git a/wasa2il/core/static/img/instructions/is/election-creation/1.Innskraning.png b/core/static/img/instructions/is/election-creation/1.Innskraning.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-creation/1.Innskraning.png rename to core/static/img/instructions/is/election-creation/1.Innskraning.png diff --git a/wasa2il/core/static/img/instructions/is/election-creation/2.Velja undirthing, skref 1.png b/core/static/img/instructions/is/election-creation/2.Velja undirthing, skref 1.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-creation/2.Velja undirthing, skref 1.png rename to core/static/img/instructions/is/election-creation/2.Velja undirthing, skref 1.png diff --git a/wasa2il/core/static/img/instructions/is/election-creation/3.Velja undirthing, skref 2.png b/core/static/img/instructions/is/election-creation/3.Velja undirthing, skref 2.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-creation/3.Velja undirthing, skref 2.png rename to core/static/img/instructions/is/election-creation/3.Velja undirthing, skref 2.png diff --git a/wasa2il/core/static/img/instructions/is/election-creation/4.Bua til kosningu, skref 1.png b/core/static/img/instructions/is/election-creation/4.Bua til kosningu, skref 1.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-creation/4.Bua til kosningu, skref 1.png rename to core/static/img/instructions/is/election-creation/4.Bua til kosningu, skref 1.png diff --git a/wasa2il/core/static/img/instructions/is/election-creation/5.Bua til kosningu, skref 2.png b/core/static/img/instructions/is/election-creation/5.Bua til kosningu, skref 2.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-creation/5.Bua til kosningu, skref 2.png rename to core/static/img/instructions/is/election-creation/5.Bua til kosningu, skref 2.png diff --git a/wasa2il/core/static/img/instructions/is/election-creation/6.Skoda kosningu.png b/core/static/img/instructions/is/election-creation/6.Skoda kosningu.png similarity index 100% rename from wasa2il/core/static/img/instructions/is/election-creation/6.Skoda kosningu.png rename to core/static/img/instructions/is/election-creation/6.Skoda kosningu.png diff --git a/core/static/img/instructions/is/join-pirat.png b/core/static/img/instructions/is/join-pirat.png new file mode 100644 index 00000000..de6aadfb Binary files /dev/null and b/core/static/img/instructions/is/join-pirat.png differ diff --git a/core/static/img/instructions/is/polity-list.png b/core/static/img/instructions/is/polity-list.png new file mode 100644 index 00000000..a00b27dd Binary files /dev/null and b/core/static/img/instructions/is/polity-list.png differ diff --git a/core/static/img/logo-100.png b/core/static/img/logo-100.png new file mode 100644 index 00000000..fb1b78c7 Binary files /dev/null and b/core/static/img/logo-100.png differ diff --git a/core/static/img/logo-101.png b/core/static/img/logo-101.png new file mode 100644 index 00000000..7a68b79b Binary files /dev/null and b/core/static/img/logo-101.png differ diff --git a/core/static/img/logo-1024.png b/core/static/img/logo-1024.png new file mode 100644 index 00000000..5f0de64b Binary files /dev/null and b/core/static/img/logo-1024.png differ diff --git a/core/static/img/logo-192.png b/core/static/img/logo-192.png new file mode 100644 index 00000000..ec8f5725 Binary files /dev/null and b/core/static/img/logo-192.png differ diff --git a/core/static/img/logo-2000.png b/core/static/img/logo-2000.png new file mode 100644 index 00000000..0ff6c12d Binary files /dev/null and b/core/static/img/logo-2000.png differ diff --git a/core/static/img/logo-256.png b/core/static/img/logo-256.png new file mode 100644 index 00000000..a3e7129b Binary files /dev/null and b/core/static/img/logo-256.png differ diff --git a/core/static/img/logo-32.png b/core/static/img/logo-32.png new file mode 100644 index 00000000..4cbad49b Binary files /dev/null and b/core/static/img/logo-32.png differ diff --git a/wasa2il/core/static/img/pirateparty-16x16-is.png b/core/static/img/pirateparty-16x16-is.png similarity index 100% rename from wasa2il/core/static/img/pirateparty-16x16-is.png rename to core/static/img/pirateparty-16x16-is.png diff --git a/wasa2il/core/static/img/pplogo-facebook.png b/core/static/img/pplogo-facebook.png similarity index 100% rename from wasa2il/core/static/img/pplogo-facebook.png rename to core/static/img/pplogo-facebook.png diff --git a/core/static/img/pplogo.png b/core/static/img/pplogo.png new file mode 100644 index 00000000..b920bf42 Binary files /dev/null and b/core/static/img/pplogo.png differ diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/external/jquery/jquery.js b/core/static/jquery-ui-1.11.4.custom/external/jquery/jquery.js similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/external/jquery/jquery.js rename to core/static/jquery-ui-1.11.4.custom/external/jquery/jquery.js diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_flat_0_aaaaaa_40x100.png b/core/static/jquery-ui-1.11.4.custom/images/ui-bg_flat_0_aaaaaa_40x100.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_flat_0_aaaaaa_40x100.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-bg_flat_0_aaaaaa_40x100.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_flat_75_ffffff_40x100.png b/core/static/jquery-ui-1.11.4.custom/images/ui-bg_flat_75_ffffff_40x100.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_flat_75_ffffff_40x100.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-bg_flat_75_ffffff_40x100.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_55_fbf9ee_1x400.png b/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_55_fbf9ee_1x400.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_55_fbf9ee_1x400.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_55_fbf9ee_1x400.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_65_ffffff_1x400.png b/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_65_ffffff_1x400.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_65_ffffff_1x400.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_65_ffffff_1x400.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_75_dadada_1x400.png b/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_75_dadada_1x400.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_75_dadada_1x400.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_75_dadada_1x400.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_75_e6e6e6_1x400.png b/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_75_e6e6e6_1x400.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_75_e6e6e6_1x400.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_75_e6e6e6_1x400.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_95_fef1ec_1x400.png b/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_95_fef1ec_1x400.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_95_fef1ec_1x400.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-bg_glass_95_fef1ec_1x400.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/core/static/jquery-ui-1.11.4.custom/images/ui-bg_highlight-soft_75_cccccc_1x100.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-bg_highlight-soft_75_cccccc_1x100.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-bg_highlight-soft_75_cccccc_1x100.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_222222_256x240.png b/core/static/jquery-ui-1.11.4.custom/images/ui-icons_222222_256x240.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_222222_256x240.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-icons_222222_256x240.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_2e83ff_256x240.png b/core/static/jquery-ui-1.11.4.custom/images/ui-icons_2e83ff_256x240.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_2e83ff_256x240.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-icons_2e83ff_256x240.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_454545_256x240.png b/core/static/jquery-ui-1.11.4.custom/images/ui-icons_454545_256x240.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_454545_256x240.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-icons_454545_256x240.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_888888_256x240.png b/core/static/jquery-ui-1.11.4.custom/images/ui-icons_888888_256x240.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_888888_256x240.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-icons_888888_256x240.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_cd0a0a_256x240.png b/core/static/jquery-ui-1.11.4.custom/images/ui-icons_cd0a0a_256x240.png similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/images/ui-icons_cd0a0a_256x240.png rename to core/static/jquery-ui-1.11.4.custom/images/ui-icons_cd0a0a_256x240.png diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/index.html b/core/static/jquery-ui-1.11.4.custom/index.html similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/index.html rename to core/static/jquery-ui-1.11.4.custom/index.html diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.css b/core/static/jquery-ui-1.11.4.custom/jquery-ui.css similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.css rename to core/static/jquery-ui-1.11.4.custom/jquery-ui.css diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.js b/core/static/jquery-ui-1.11.4.custom/jquery-ui.js similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.js rename to core/static/jquery-ui-1.11.4.custom/jquery-ui.js diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.min.css b/core/static/jquery-ui-1.11.4.custom/jquery-ui.min.css similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.min.css rename to core/static/jquery-ui-1.11.4.custom/jquery-ui.min.css diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.min.js b/core/static/jquery-ui-1.11.4.custom/jquery-ui.min.js similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.min.js rename to core/static/jquery-ui-1.11.4.custom/jquery-ui.min.js diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.structure.css b/core/static/jquery-ui-1.11.4.custom/jquery-ui.structure.css similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.structure.css rename to core/static/jquery-ui-1.11.4.custom/jquery-ui.structure.css diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.structure.min.css b/core/static/jquery-ui-1.11.4.custom/jquery-ui.structure.min.css similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.structure.min.css rename to core/static/jquery-ui-1.11.4.custom/jquery-ui.structure.min.css diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.theme.css b/core/static/jquery-ui-1.11.4.custom/jquery-ui.theme.css similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.theme.css rename to core/static/jquery-ui-1.11.4.custom/jquery-ui.theme.css diff --git a/wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.theme.min.css b/core/static/jquery-ui-1.11.4.custom/jquery-ui.theme.min.css similarity index 100% rename from wasa2il/core/static/jquery-ui-1.11.4.custom/jquery-ui.theme.min.css rename to core/static/jquery-ui-1.11.4.custom/jquery-ui.theme.min.css diff --git a/core/static/js/OneSignalSDKWorker.js b/core/static/js/OneSignalSDKWorker.js new file mode 100644 index 00000000..720b1d78 --- /dev/null +++ b/core/static/js/OneSignalSDKWorker.js @@ -0,0 +1 @@ +importScripts('https://cdn.onesignal.com/sdks/OneSignalSDKWorker.js'); diff --git a/core/static/js/bootstrap-confirmation-2.4.1/.editorconfig b/core/static/js/bootstrap-confirmation-2.4.1/.editorconfig new file mode 100644 index 00000000..0daf12d6 --- /dev/null +++ b/core/static/js/bootstrap-confirmation-2.4.1/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/core/static/js/bootstrap-confirmation-2.4.1/.gitignore b/core/static/js/bootstrap-confirmation-2.4.1/.gitignore new file mode 100644 index 00000000..b733c45a --- /dev/null +++ b/core/static/js/bootstrap-confirmation-2.4.1/.gitignore @@ -0,0 +1,4 @@ +/.idea +/*.iml +/bower_components +/node_modules \ No newline at end of file diff --git a/core/static/js/bootstrap-confirmation-2.4.1/Gruntfile.js b/core/static/js/bootstrap-confirmation-2.4.1/Gruntfile.js new file mode 100644 index 00000000..a45322fc --- /dev/null +++ b/core/static/js/bootstrap-confirmation-2.4.1/Gruntfile.js @@ -0,0 +1,106 @@ +module.exports = function(grunt) { + require('jit-grunt')(grunt); + + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + banner: '/*!\n' + + ' * Bootstrap Confirmation <%= pkg.version %>\n' + + ' * Copyright 2013 Nimit Suwannagate \n' + + ' * Copyright 2014-<%= grunt.template.today("yyyy") %> Damien "Mistic" Sorel \n' + + ' * Licensed under the Apache License, Version 2.0\n' + + ' */', + + // serve folder content + connect: { + dev: { + options: { + port: 9000, + livereload: true + } + } + }, + + // watchers + watch: { + options: { + livereload: true + }, + dev: { + files: ['bootstrap-confirmation.js', 'example/**'], + tasks: [] + } + }, + + // open example + open: { + dev: { + path: 'http://localhost:<%= connect.dev.options.port%>/example/index.html' + } + }, + + // replace version number + replace: { + dist: { + options: { + patterns: [ + { + match: /(Confirmation\.VERSION = ').*(';)/, + replacement: '$1<%= pkg.version %>$2' + } + ] + }, + files: { + 'bootstrap-confirmation.js': [ + 'bootstrap-confirmation.js' + ] + } + } + }, + + // compress js + uglify: { + options: { + banner: '<%= banner %>\n', + mangle: { + except: ['$'] + } + }, + dist: { + files: { + 'bootstrap-confirmation.min.js': [ + 'bootstrap-confirmation.js' + ] + } + } + }, + + // jshint tests + jshint: { + lib: { + files: { + src: [ + 'bootstrap-confirmation.js' + ] + } + } + } + } + ); + + grunt.registerTask('default', [ + 'replace', + 'uglify' + ]); + + grunt.registerTask('test', [ + 'jshint' + ]); + + grunt.registerTask('serve', [ + 'connect', + 'open', + 'watch' + ]); + +}; diff --git a/core/static/js/bootstrap-confirmation-2.4.1/README.md b/core/static/js/bootstrap-confirmation-2.4.1/README.md new file mode 100644 index 00000000..cea7c123 --- /dev/null +++ b/core/static/js/bootstrap-confirmation-2.4.1/README.md @@ -0,0 +1,24 @@ +# Bootstrap-Confirmation + +[![Bower version](https://img.shields.io/bower/v/bootstrap-confirmation2.svg?style=flat-square)](bootstrap-confirmation.js.org) +[![NPM version](https://img.shields.io/npm/v/bootstrap-confirmation2.svg?style=flat-square)](https://www.npmjs.com/package/bootstrap-confirmation2) + +Bootstrap plugin for on-place confirm boxes using Popover. + +## Documentation + +[bootstrap-confirmation.js.org](http://bootstrap-confirmation.js.org) + +## Installation + +#### Bootstrap 4 + +``` +npm install bootstrap-confirmation2 +``` + +#### Bootstrap 3 + +``` +npm install bootstrap-confirmation2@2.x.x +``` diff --git a/core/static/js/bootstrap-confirmation-2.4.1/bootstrap-confirmation.js b/core/static/js/bootstrap-confirmation-2.4.1/bootstrap-confirmation.js new file mode 100644 index 00000000..511a0fdf --- /dev/null +++ b/core/static/js/bootstrap-confirmation-2.4.1/bootstrap-confirmation.js @@ -0,0 +1,457 @@ +/*! + * Bootstrap Confirmation + * Copyright 2013 Nimit Suwannagate + * Copyright 2014-2017 Damien "Mistic" Sorel + * Licensed under the Apache License, Version 2.0 + */ + +(function($) { + 'use strict'; + + var activeConfirmation; + + // Confirmation extends popover.js + if (!$.fn.popover) { + throw new Error('Confirmation requires popover.js'); + } + + // CONFIRMATION PUBLIC CLASS DEFINITION + // =============================== + var Confirmation = function(element, options) { + options.trigger = 'click'; + + this.init(element, options); + }; + + Confirmation.VERSION = '2.4.1'; + + /** + * Map between keyboard events "keyCode|which" and "key" + */ + Confirmation.KEYMAP = { + 13: 'Enter', + 27: 'Escape', + 39: 'ArrowRight', + 40: 'ArrowDown' + }; + + Confirmation.DEFAULTS = $.extend({}, $.fn.popover.Constructor.DEFAULTS, { + placement: 'top', + title: 'Are you sure?', + popout: false, + singleton: false, + copyAttributes: 'href target', + buttons: null, + onConfirm: $.noop, + onCancel: $.noop, + btnOkClass: 'btn-xs btn-primary', + btnOkIcon: 'glyphicon glyphicon-ok', + btnOkLabel: 'Yes', + btnCancelClass: 'btn-xs btn-default', + btnCancelIcon: 'glyphicon glyphicon-remove', + btnCancelLabel: 'No', + // @formatter:off + // href="#" allows the buttons to be focused + template: '
' + + '
' + + '

' + + '
' + + '

' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + // @formatter:on + }); + + Confirmation.prototype = $.extend({}, $.fn.popover.Constructor.prototype); + Confirmation.prototype.constructor = Confirmation; + + /** + * Expose defaults + * @returns {object} + */ + Confirmation.prototype.getDefaults = function() { + return Confirmation.DEFAULTS; + }; + + /** + * Init the component + * @param element {jQuery} + * @param options {object} + */ + Confirmation.prototype.init = function(element, options) { + $.fn.popover.Constructor.prototype.init.call(this, 'confirmation', element, options); + + if ((this.options.popout || this.options.singleton) && !options.rootSelector) { + throw new Error('The rootSelector option is required to use popout and singleton features since jQuery 3.'); + } + + // keep trace of selectors + this.options._isDelegate = false; + if (options.selector) { // container of buttons + this.options._selector = this._options._selector = options.rootSelector + ' ' + options.selector; + } + else if (options._selector) { // children of container + this.options._selector = options._selector; + this.options._isDelegate = true; + } + else { // standalone + this.options._selector = options.rootSelector; + } + + var self = this; + + if (!this.options.selector) { + // store copied attributes + this.options._attributes = {}; + if (this.options.copyAttributes) { + if (typeof this.options.copyAttributes === 'string') { + this.options.copyAttributes = this.options.copyAttributes.split(' '); + } + } + else { + this.options.copyAttributes = []; + } + + this.options.copyAttributes.forEach(function(attr) { + this.options._attributes[attr] = this.$element.attr(attr); + }, this); + + // cancel original event + this.$element.on(this.options.trigger, function(e, ack) { + if (!ack) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + } + }); + + // manage singleton + this.$element.on('show.bs.confirmation', function(e) { + if (self.options.singleton) { + // close all other popover already initialized + $(self.options._selector).not($(this)).filter(function() { + return $(this).data('bs.confirmation') !== undefined; + }).confirmation('hide'); + } + }); + } + else { + // cancel original event + this.$element.on(this.options.trigger, this.options.selector, function(e, ack) { + if (!ack) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + } + }); + } + + if (!this.options._isDelegate) { + // manage popout + this.eventBody = false; + this.uid = this.$element[0].id || this.getUID('group_'); + + this.$element.on('shown.bs.confirmation', function(e) { + if (self.options.popout && !self.eventBody) { + self.eventBody = $('body').on('click.bs.confirmation.' + self.uid, function(e) { + if ($(self.options._selector).is(e.target)) { + return; + } + + // close all popover already initialized + $(self.options._selector).filter(function() { + return $(this).data('bs.confirmation') !== undefined; + }).confirmation('hide'); + + $('body').off('click.bs.' + self.uid); + self.eventBody = false; + }); + } + }); + } + }; + + /** + * Overrides, always show + * @returns {boolean} + */ + Confirmation.prototype.hasContent = function() { + return true; + }; + + /** + * Sets the popover content + */ + Confirmation.prototype.setContent = function() { + var self = this; + var $tip = this.tip(); + var title = this.getTitle(); + var content = this.getContent(); + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title); + + $tip.find('.confirmation-content').toggle(!!content).children().detach().end()[ + // we use append for html objects to maintain js events + this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' + ](content); + + $tip.on('click', function(e) { + e.stopPropagation(); + }); + + if (this.options.buttons) { + // configure custom buttons + var $group = $tip.find('.confirmation-buttons .btn-group').empty(); + + this.options.buttons.forEach(function(button) { + $group.append( + $('') + .addClass(button.class || 'btn btn-xs btn-default') + .html(button.label || '') + .attr(button.attr || {}) + .prepend($('').addClass(button.icon), ' ') + .one('click', function(e) { + if ($(this).attr('href') === '#') { + e.preventDefault(); + } + + if (button.onClick) { + button.onClick.call(self.$element); + } + + if (button.cancel) { + self.getOnCancel().call(self.$element, button.value); + self.$element.trigger('canceled.bs.confirmation', [button.value]); + } + else { + self.getOnConfirm().call(self.$element, button.value); + self.$element.trigger('confirmed.bs.confirmation', [button.value]); + } + + if (self.inState) { // Bootstrap 3.3.5 + self.inState.click = false; + } + + self.hide(); + }) + ); + }, this); + } + else { + // configure 'ok' button + $tip.find('[data-apply="confirmation"]') + .addClass(this.options.btnOkClass) + .html(this.options.btnOkLabel) + .attr(this.options._attributes) + .prepend($('').addClass(this.options.btnOkIcon), ' ') + .off('click') + .one('click', function(e) { + if ($(this).attr('href') === '#') { + e.preventDefault(); + } + + self.getOnConfirm().call(self.$element); + self.$element.trigger('confirmed.bs.confirmation'); + + self.$element.trigger(self.options.trigger, [true]); + + self.hide(); + }); + + // configure 'cancel' button + $tip.find('[data-dismiss="confirmation"]') + .addClass(this.options.btnCancelClass) + .html(this.options.btnCancelLabel) + .prepend($('').addClass(this.options.btnCancelIcon), ' ') + .off('click') + .one('click', function(e) { + e.preventDefault(); + + self.getOnCancel().call(self.$element); + self.$element.trigger('canceled.bs.confirmation'); + + if (self.inState) { // Bootstrap 3.3.5 + self.inState.click = false; + } + + self.hide(); + }); + } + + $tip.removeClass('fade top bottom left right in'); + + // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do + // this manually by checking the contents. + if (!$tip.find('.popover-title').html()) { + $tip.find('.popover-title').hide(); + } + + // bind key navigation + activeConfirmation = this; + $(window) + .off('keyup.bs.confirmation') + .on('keyup.bs.confirmation', this._onKeyup.bind(this)); + }; + + /** + * Remove key binding on destroy + */ + Confirmation.prototype.destroy = function() { + if (activeConfirmation === this) { + activeConfirmation = undefined; + $(window).off('keyup.bs.confirmation'); + } + $.fn.popover.Constructor.prototype.destroy.call(this); + }; + + /** + * Remove key binding on hide + */ + Confirmation.prototype.hide = function() { + if (activeConfirmation === this) { + activeConfirmation = undefined; + $(window).off('keyup.bs.confirmation'); + } + $.fn.popover.Constructor.prototype.hide.call(this); + }; + + /** + * Navigate through buttons with keyboard + * @param event + * @private + */ + Confirmation.prototype._onKeyup = function(event) { + if (!this.$tip) { + activeConfirmation = undefined; + $(window).off('keyup.bs.confirmation'); + return; + } + + var key = event.key || Confirmation.KEYMAP[event.keyCode || event.which]; + + var $group = this.$tip.find('.confirmation-buttons .btn-group'); + var $active = $group.find('.active'); + var $next; + + switch (key) { + case 'Escape': + this.hide(); + break; + + case 'ArrowRight': + if ($active.length && $active.next().length) { + $next = $active.next(); + } + else { + $next = $group.children().first(); + } + $active.removeClass('active'); + $next.addClass('active').focus(); + break; + + case 'ArrowLeft': + if ($active.length && $active.prev().length) { + $next = $active.prev(); + } + else { + $next = $group.children().last(); + } + $active.removeClass('active'); + $next.addClass('active').focus(); + break; + } + }; + + /** + * Gets the on-confirm callback + * @returns {function} + */ + Confirmation.prototype.getOnConfirm = function() { + if (this.$element.attr('data-on-confirm')) { + return getFunctionFromString(this.$element.attr('data-on-confirm')); + } + else { + return this.options.onConfirm; + } + }; + + /** + * Gets the on-cancel callback + * @returns {function} + */ + Confirmation.prototype.getOnCancel = function() { + if (this.$element.attr('data-on-cancel')) { + return getFunctionFromString(this.$element.attr('data-on-cancel')); + } + else { + return this.options.onCancel; + } + }; + + /** + * Generates an anonymous function from a function name + * function name may contain dots (.) to navigate through objects + * root context is window + */ + function getFunctionFromString(functionName) { + var context = window; + var namespaces = functionName.split('.'); + var func = namespaces.pop(); + + for (var i = 0, l = namespaces.length; i < l; i++) { + context = context[namespaces[i]]; + } + + return function() { + context[func].call(this); + }; + } + + + // CONFIRMATION PLUGIN DEFINITION + // ========================= + + var old = $.fn.confirmation; + + $.fn.confirmation = function(option) { + var options = (typeof option == 'object' && option) || {}; + options.rootSelector = this.selector || options.rootSelector; // this.selector removed in jQuery > 3 + + return this.each(function() { + var $this = $(this); + var data = $this.data('bs.confirmation'); + + if (!data && option == 'destroy') { + return; + } + if (!data) { + $this.data('bs.confirmation', (data = new Confirmation(this, options))); + } + if (typeof option == 'string') { + data[option](); + + if (option == 'hide' && data.inState) { //data.inState doesn't exist in Bootstrap < 3.3.5 + data.inState.click = false; + } + } + }); + }; + + $.fn.confirmation.Constructor = Confirmation; + + + // CONFIRMATION NO CONFLICT + // =================== + + $.fn.confirmation.noConflict = function() { + $.fn.confirmation = old; + return this; + }; + +}(jQuery)); diff --git a/core/static/js/bootstrap-confirmation-2.4.1/bootstrap-confirmation.min.js b/core/static/js/bootstrap-confirmation-2.4.1/bootstrap-confirmation.min.js new file mode 100644 index 00000000..9446bf72 --- /dev/null +++ b/core/static/js/bootstrap-confirmation-2.4.1/bootstrap-confirmation.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap Confirmation 2.4.1 + * Copyright 2013 Nimit Suwannagate + * Copyright 2014-2018 Damien "Mistic" Sorel + * Licensed under the Apache License, Version 2.0 + */ +!function($){"use strict";function a(a){for(var b=window,c=a.split("."),d=c.pop(),e=0,f=c.length;e

'}),c.prototype=$.extend({},$.fn.popover.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.init=function(a,b){if($.fn.popover.Constructor.prototype.init.call(this,"confirmation",a,b),(this.options.popout||this.options.singleton)&&!b.rootSelector)throw new Error("The rootSelector option is required to use popout and singleton features since jQuery 3.");this.options._isDelegate=!1,b.selector?this.options._selector=this._options._selector=b.rootSelector+" "+b.selector:b._selector?(this.options._selector=b._selector,this.options._isDelegate=!0):this.options._selector=b.rootSelector;var c=this;this.options.selector?this.$element.on(this.options.trigger,this.options.selector,function(a,b){b||(a.preventDefault(),a.stopPropagation(),a.stopImmediatePropagation())}):(this.options._attributes={},this.options.copyAttributes?"string"==typeof this.options.copyAttributes&&(this.options.copyAttributes=this.options.copyAttributes.split(" ")):this.options.copyAttributes=[],this.options.copyAttributes.forEach(function(a){this.options._attributes[a]=this.$element.attr(a)},this),this.$element.on(this.options.trigger,function(a,b){b||(a.preventDefault(),a.stopPropagation(),a.stopImmediatePropagation())}),this.$element.on("show.bs.confirmation",function(a){c.options.singleton&&$(c.options._selector).not($(this)).filter(function(){return void 0!==$(this).data("bs.confirmation")}).confirmation("hide")})),this.options._isDelegate||(this.eventBody=!1,this.uid=this.$element[0].id||this.getUID("group_"),this.$element.on("shown.bs.confirmation",function(a){c.options.popout&&!c.eventBody&&(c.eventBody=$("body").on("click.bs.confirmation."+c.uid,function(a){$(c.options._selector).is(a.target)||($(c.options._selector).filter(function(){return void 0!==$(this).data("bs.confirmation")}).confirmation("hide"),$("body").off("click.bs."+c.uid),c.eventBody=!1)}))}))},c.prototype.hasContent=function(){return!0},c.prototype.setContent=function(){var a=this,c=this.tip(),d=this.getTitle(),e=this.getContent();if(c.find(".popover-title")[this.options.html?"html":"text"](d),c.find(".confirmation-content").toggle(!!e).children().detach().end()[this.options.html?"string"==typeof e?"html":"append":"text"](e),c.on("click",function(a){a.stopPropagation()}),this.options.buttons){var f=c.find(".confirmation-buttons .btn-group").empty();this.options.buttons.forEach(function(b){f.append($('').addClass(b["class"]||"btn btn-xs btn-default").html(b.label||"").attr(b.attr||{}).prepend($("").addClass(b.icon)," ").one("click",function(c){"#"===$(this).attr("href")&&c.preventDefault(),b.onClick&&b.onClick.call(a.$element),b.cancel?(a.getOnCancel().call(a.$element,b.value),a.$element.trigger("canceled.bs.confirmation",[b.value])):(a.getOnConfirm().call(a.$element,b.value),a.$element.trigger("confirmed.bs.confirmation",[b.value])),a.inState&&(a.inState.click=!1),a.hide()}))},this)}else c.find('[data-apply="confirmation"]').addClass(this.options.btnOkClass).html(this.options.btnOkLabel).attr(this.options._attributes).prepend($("").addClass(this.options.btnOkIcon)," ").off("click").one("click",function(b){"#"===$(this).attr("href")&&b.preventDefault(),a.getOnConfirm().call(a.$element),a.$element.trigger("confirmed.bs.confirmation"),a.$element.trigger(a.options.trigger,[!0]),a.hide()}),c.find('[data-dismiss="confirmation"]').addClass(this.options.btnCancelClass).html(this.options.btnCancelLabel).prepend($("").addClass(this.options.btnCancelIcon)," ").off("click").one("click",function(b){b.preventDefault(),a.getOnCancel().call(a.$element),a.$element.trigger("canceled.bs.confirmation"),a.inState&&(a.inState.click=!1),a.hide()});c.removeClass("fade top bottom left right in"),c.find(".popover-title").html()||c.find(".popover-title").hide(),b=this,$(window).off("keyup.bs.confirmation").on("keyup.bs.confirmation",this._onKeyup.bind(this))},c.prototype.destroy=function(){b===this&&(b=void 0,$(window).off("keyup.bs.confirmation")),$.fn.popover.Constructor.prototype.destroy.call(this)},c.prototype.hide=function(){b===this&&(b=void 0,$(window).off("keyup.bs.confirmation")),$.fn.popover.Constructor.prototype.hide.call(this)},c.prototype._onKeyup=function(a){if(!this.$tip)return b=void 0,void $(window).off("keyup.bs.confirmation");var d,e=a.key||c.KEYMAP[a.keyCode||a.which],f=this.$tip.find(".confirmation-buttons .btn-group"),g=f.find(".active");switch(e){case"Escape":this.hide();break;case"ArrowRight":d=g.length&&g.next().length?g.next():f.children().first(),g.removeClass("active"),d.addClass("active").focus();break;case"ArrowLeft":d=g.length&&g.prev().length?g.prev():f.children().last(),g.removeClass("active"),d.addClass("active").focus()}},c.prototype.getOnConfirm=function(){return this.$element.attr("data-on-confirm")?a(this.$element.attr("data-on-confirm")):this.options.onConfirm},c.prototype.getOnCancel=function(){return this.$element.attr("data-on-cancel")?a(this.$element.attr("data-on-cancel")):this.options.onCancel};var d=$.fn.confirmation;$.fn.confirmation=function(a){var b="object"==typeof a&&a||{};return b.rootSelector=this.selector||b.rootSelector,this.each(function(){var d=$(this),e=d.data("bs.confirmation");(e||"destroy"!=a)&&(e||d.data("bs.confirmation",e=new c(this,b)),"string"==typeof a&&(e[a](),"hide"==a&&e.inState&&(e.inState.click=!1)))})},$.fn.confirmation.Constructor=c,$.fn.confirmation.noConflict=function(){return $.fn.confirmation=d,this}}(jQuery); \ No newline at end of file diff --git a/core/static/js/bootstrap-confirmation-2.4.1/bower.json b/core/static/js/bootstrap-confirmation-2.4.1/bower.json new file mode 100644 index 00000000..718da590 --- /dev/null +++ b/core/static/js/bootstrap-confirmation-2.4.1/bower.json @@ -0,0 +1,37 @@ +{ + "name": "bootstrap-confirmation2", + "homepage": "http://bootstrap-confirmation.js.org", + "description": "Bootstrap plugin for on-place confirm boxes using Popover", + "license": "Apache-2.0", + "authors": [ + { + "name": "Nimit Suwannagate", + "email": "ethaizone@hotmail.com" + }, + { + "name": "Damien \"Mistic\" Sorel", + "email": "contact@git.strangeplanet.fr", + "homepage": "http://www.strangeplanet.fr" + } + ], + "main": "bootstrap-confirmation.js", + "keywords": [ + "bootstrap", + "confirmation", + "popup" + ], + "dependencies" : { + "bootstrap": ">=3.2.0 <4" + }, + "repository": { + "type": "git", + "url": "git://github.com/mistic100/Bootstrap-Confirmation.git" + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/core/static/js/bootstrap-confirmation-2.4.1/example/index.html b/core/static/js/bootstrap-confirmation-2.4.1/example/index.html new file mode 100644 index 00000000..dd9dcd92 --- /dev/null +++ b/core/static/js/bootstrap-confirmation-2.4.1/example/index.html @@ -0,0 +1,187 @@ + + + + + + + Bootstrap Confirmation + + + + + + +
+
+ + +
+
Basic
+
+ +
+
+ +
+
Link
+ +
+ +
+
Customize
+
+ +
+
+ +
+
Directions
+
+ + + + +
+
+ +
+
Singleton
+
+ + +
+
+ +
+
Popout
+
+ + +
+
+ +
+
Delegation
+
+ + +
+
+ +
+
Custom buttons
+
+ + +
+
+
+
+ + + + + + + + + + + diff --git a/core/static/js/bootstrap-confirmation-2.4.1/package.json b/core/static/js/bootstrap-confirmation-2.4.1/package.json new file mode 100644 index 00000000..8503de48 --- /dev/null +++ b/core/static/js/bootstrap-confirmation-2.4.1/package.json @@ -0,0 +1,47 @@ +{ + "name": "bootstrap-confirmation2", + "version": "2.4.1", + "homepage": "http://bootstrap-confirmation.js.org", + "description": "Bootstrap plugin for on-place confirm boxes using Popover", + "license": "Apache-2.0", + "authors": [ + { + "name": "Nimit Suwannagate", + "email": "ethaizone@hotmail.com" + }, + { + "name": "Damien \"Mistic\" Sorel", + "email": "contact@git.strangeplanet.fr", + "homepage": "http://www.strangeplanet.fr" + } + ], + "main": "bootstrap-confirmation.js", + "keywords": [ + "bootstrap", + "confirmation", + "popup" + ], + "peerDependencies": { + "bootstrap": ">=3.2.0 <4" + }, + "devDependencies": { + "grunt": "^1.0.0", + "grunt-contrib-connect": "^1.0.0", + "grunt-contrib-jshint": "^1.0.0", + "grunt-contrib-uglify": "^1.0.0", + "grunt-contrib-watch": "^1.0.0", + "grunt-open": "^0.2.3", + "grunt-replace": "^1.0.1", + "jit-grunt": "^0.10.0" + }, + "repository": { + "type": "git", + "url": "git://github.com/mistic100/Bootstrap-Confirmation.git" + }, + "bugs": { + "url": "https://github.com/mistic100/Bootstrap-Confirmation/issues" + }, + "scripts": { + "test": "grunt test" + } +} diff --git a/wasa2il/core/static/js/bootstrap.js b/core/static/js/bootstrap.js similarity index 100% rename from wasa2il/core/static/js/bootstrap.js rename to core/static/js/bootstrap.js diff --git a/wasa2il/core/static/js/bootstrap.min.js b/core/static/js/bootstrap.min.js similarity index 100% rename from wasa2il/core/static/js/bootstrap.min.js rename to core/static/js/bootstrap.min.js diff --git a/core/static/js/cookiesdirective-2.1.0-predist/IMPORTANT b/core/static/js/cookiesdirective-2.1.0-predist/IMPORTANT new file mode 100644 index 00000000..0a18662e --- /dev/null +++ b/core/static/js/cookiesdirective-2.1.0-predist/IMPORTANT @@ -0,0 +1 @@ +This is a development version of cookiesDirective that hasn't yet been accepted into its main repository. The 2.1.0 version number is still a suggestion to the original author who is yet to review and (hopefully) accept the modifications made for Wasa2il. diff --git a/core/static/js/cookiesdirective-2.1.0-predist/LICENSE b/core/static/js/cookiesdirective-2.1.0-predist/LICENSE new file mode 100644 index 00000000..449733eb --- /dev/null +++ b/core/static/js/cookiesdirective-2.1.0-predist/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2013 by Ollie Phillips + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/core/static/js/cookiesdirective-2.1.0-predist/README b/core/static/js/cookiesdirective-2.1.0-predist/README new file mode 100644 index 00000000..75e74f95 --- /dev/null +++ b/core/static/js/cookiesdirective-2.1.0-predist/README @@ -0,0 +1,26 @@ +More information at http://cookiesdirective.com, or check out demo.html + +* jquery.cookiesdirective.js - the jQuery plugin +* demo.html - good place to start, if you're trying to set this up +* dialog.html - a basic example of a custom dialog (only relevant to the customDialogSelector setting) +* external.js - a javascript file to grab and load to the DOM, when cookies accepted or implied consent mode running + +Version 2.1.0 +============= +- Support for completely customizing dialog and its content + +Version 2.0.1 +============= +- .live('click'... replaced with .click() as former deprecated +- Removed old 1.5.js script from this package + +Version 2.0.0 +============= +- jQuery is ubiquitous. Script isn't checking/finding/loading it for you anymore +- Rewritten as a jQuery plugin +- Options. Lots of them. Configure lots of things +- Implied consent mode. For the UK. The ICO said it's ok. So less clicking and won't hold back your cookie setting scripts. + +Version 1.5 +=========== +- Old javascript version. No longer maintained. diff --git a/core/static/js/cookiesdirective-2.1.0-predist/demo.html b/core/static/js/cookiesdirective-2.1.0-predist/demo.html new file mode 100644 index 00000000..768c052d --- /dev/null +++ b/core/static/js/cookiesdirective-2.1.0-predist/demo.html @@ -0,0 +1,63 @@ + + + + + + + + + +
+ + + + + diff --git a/core/static/js/cookiesdirective-2.1.0-predist/dialog.html b/core/static/js/cookiesdirective-2.1.0-predist/dialog.html new file mode 100644 index 00000000..3aef5d93 --- /dev/null +++ b/core/static/js/cookiesdirective-2.1.0-predist/dialog.html @@ -0,0 +1,24 @@ + + diff --git a/core/static/js/cookiesdirective-2.1.0-predist/external.js b/core/static/js/cookiesdirective-2.1.0-predist/external.js new file mode 100644 index 00000000..eaf6dcca --- /dev/null +++ b/core/static/js/cookiesdirective-2.1.0-predist/external.js @@ -0,0 +1,26 @@ +function setCookie(name,value,days) { + var expires = ""; + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days*24*60*60*1000)); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + (value || "") + expires + "; path=/"; +} +function getCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; +} +function eraseCookie(name) { + document.cookie = name+'=; Max-Age=-99999999;'; +} + +setCookie('some-cookie', 'some-value'); + +alert('External Javascript loaded and running'); diff --git a/core/static/js/cookiesdirective-2.1.0-predist/jquery.cookiesdirective.js b/core/static/js/cookiesdirective-2.1.0-predist/jquery.cookiesdirective.js new file mode 100644 index 00000000..3337d000 --- /dev/null +++ b/core/static/js/cookiesdirective-2.1.0-predist/jquery.cookiesdirective.js @@ -0,0 +1,341 @@ +/* Cookies Directive - The rewrite. Now a jQuery plugin + * Version: 2.1.0 + * Author: Ollie Phillips + * 24 October 2013 + */ + +;(function($) { + $.cookiesDirective = function(options) { + + // Default Cookies Directive Settings + var settings = $.extend({ + //Options + explicitConsent: true, + position: 'top', + duration: 10, + limit: 0, + message: null, + cookieScripts: null, + privacyPolicyUri: 'privacy.html', + scriptWrapper: function(){}, + customDialogSelector: null, + // Styling + fontFamily: 'helvetica', + fontColor: '#FFFFFF', + fontSize: '13px', + backgroundColor: '#000000', + backgroundOpacity: '80', + linkColor: '#CA0000', + positionOffset: '0' + }, options); + + // Perform consent checks + if(!getCookie('cookiesDirective')) { + if(settings.limit > 0) { + // Display limit in force, record the view + if(!getCookie('cookiesDisclosureCount')) { + setCookie('cookiesDisclosureCount',1,1); + } else { + var disclosureCount = getCookie('cookiesDisclosureCount'); + disclosureCount ++; + setCookie('cookiesDisclosureCount',disclosureCount,1); + } + + // Have we reached the display limit, if not make disclosure + if(settings.limit >= getCookie('cookiesDisclosureCount')) { + disclosure(settings); + } + } else { + // No display limit + disclosure(settings); + } + + // If we don't require explicit consent, load up our script wrapping function + if(!settings.explicitConsent) { + settings.scriptWrapper.call(); + } + } else { + // Cookies accepted, load script wrapping function + settings.scriptWrapper.call(); + } + }; + + // Used to load external javascript files into the DOM + $.cookiesDirective.loadScript = function(options) { + var settings = $.extend({ + uri: '', + appendTo: 'body' + }, options); + + var elementId = String(settings.appendTo); + var sA = document.createElement("script"); + sA.src = settings.uri; + sA.type = "text/javascript"; + sA.onload = sA.onreadystatechange = function() { + if ((!sA.readyState || sA.readyState == "loaded" || sA.readyState == "complete")) { + return; + } + }; + switch(settings.appendTo) { + case 'head': + $('head').append(sA); + break; + case 'body': + $('body').append(sA); + break; + default: + $('#' + elementId).append(sA); + } + }; + + // Helper scripts + // Get cookie + var getCookie = function(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length,c.length); + } + return null; + }; + + // Set cookie + var setCookie = function(name,value,days) { + var expires = ""; + if (days) { + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + expires = "; expires="+date.toGMTString(); + } + document.cookie = name+"="+value+expires+"; path=/"; + }; + + // Detect IE < 9 + var checkIE = function(){ + var version; + if (navigator.appName == 'Microsoft Internet Explorer') { + var ua = navigator.userAgent; + var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})"); + if (re.exec(ua) !== null) { + version = parseFloat(RegExp.$1); + } + if (version <= 8.0) { + return true; + } else { + if(version == 9.0) { + if(document.compatMode == "BackCompat") { + // IE9 in quirks mode won't run the script properly, set to emulate IE8 + var mA = document.createElement("meta"); + mA.content = "IE=EmulateIE8"; + document.getElementsByTagName('head')[0].appendChild(mA); + return true; + } else { + return false; + } + } + return false; + } + } else { + return false; + } + }; + + // Disclosure routines + var disclosure = function(options) { + var settings = options; + settings.css = 'fixed'; + + // IE 9 and lower has issues with position:fixed, either out the box or in compatibility mode - fix that + if(checkIE()) { + settings.position = 'top'; + settings.css = 'absolute'; + } + + // Any cookie setting scripts to disclose + var scriptsDisclosure = ''; + if (settings.cookieScripts) { + var scripts = settings.cookieScripts.split(','); + var scriptsCount = scripts.length; + var scriptDisclosureTxt = ''; + if(scriptsCount>1) { + for(var t=0; t < scriptsCount - 1; t++) { + scriptDisclosureTxt += scripts[t] + ', '; + } + scriptsDisclosure = ' We use ' + scriptDisclosureTxt.substring(0, scriptDisclosureTxt.length - 2) + ' and ' + scripts[scriptsCount - 1] + ' scripts, which all set cookies. '; + } else { + scriptsDisclosure = ' We use a ' + scripts[0] + ' script which sets cookies.'; + } + } + + // Create overlay, vary the disclosure based on explicit/implied consent + // Set our disclosure/message if one not supplied + if (!settings.customDialogSelector) { + + // Remove the "cookiesdirective" dialog if it already exists in the HTML. + $('#cookiesdirective').remove(); + + var html = ''; + html += '
'; + html += '
'; + html += '
'; + + if(!settings.message) { + if(settings.explicitConsent) { + // Explicit consent message + settings.message = 'This site uses cookies. Some of the cookies we '; + settings.message += 'use are essential for parts of the site to operate and have already been set.'; + } else { + // Implied consent message + settings.message = 'We have placed cookies on your computer to help make this website better.'; + } + } + html += settings.message; + + // Build the rest of the disclosure for implied and explicit consent + if(settings.explicitConsent) { + // Explicit consent disclosure + html += scriptsDisclosure + 'You may delete and block all cookies from this site, but parts of the site will not work.'; + html += 'To find out more about cookies on this website, see our privacy policy.
'; + html += ''; + html += '
I accept cookies from this site  '; + html += '
'; + + } else { + // Implied consent disclosure + html += scriptsDisclosure + ' More details can be found in our privacy policy.'; + html += '
'; + } + html += '
'; + $('body').append(html); + + } else { + // Get the dialog and "cookiesdirective" div. + var $dialog = $('#cookie-dialog'); + var $cd = $dialog.find('#cookiesdirective'); + + // If explicit consent is required, the appropriate controls are + // needed for the user to be able to consent. If they are not + // added by the user (developer), we'll automatically add them + // here and issue a warning to the console. + if (settings.explicitConsent) { + if ($cd.find('#epdnotick').length == 0) { + $cd.append(''); + console.warn('cookiesDirective: Element with ID "epdnotick" does not exist in custom dialog, so automatically added'); + } + if ($cd.find('input#epdagree').length == 0) { + $cd.append(''); + console.warn('cookiesDirective: Checkbox with ID "epdagree" does not exist in custom dialog, so automatically added'); + } + if ($cd.find('input#explicitsubmit').length == 0) { + $cd.append(''); + console.warn('cookiesDirective: Submit button with ID "explicitsubmit" does not exist in custom dialog, so automatically added'); + } + // However, if implied consent is enough, we'll still need a + // button for the user to indicate that they do not want to see + // the message again. + } else { + if ($cd.find('input#impliedsubmit').length == 0) { + $cd.append(''); + console.warn('cookiesDirective: Submit button with ID "impliedsubmit" does not exist in custom dialog, so automatically added'); + } + } + + // Make sure that the custom dialog's message about explicit + // consent being required, is invisible at the start. + $dialog.find('#epdnotick').css('display', 'none'); + + // The custom dialog must start invisible. We cannot automatically + // set it at this point because it will revert to its original + // state once the cookie acceptance is complete. Instead, we warn + // the user (developer). + if ($dialog.css('display') != 'none') { + console.error('cookiesDirective: Custom dialog element should have CSS style display: "none".'); + } + + // Configure the dialog. + $cd.css(settings.position, '-300px'); + $cd.css({ + 'position': settings.css, + 'left': '0px', + 'width': '100%', + 'height': 'auto', + 'text-align': 'center', + 'z-index': '1000', + }); + + // Dialog starts hidden so that it's not visible in content + // afterwards, so it has to be explicitly made visible. + $dialog.show(); + } + + // Serve the disclosure, and be smarter about branching + var dp = settings.position.toLowerCase(); + if(dp != 'top' && dp!= 'bottom') { + dp = 'top'; + } + var opts = { in: null, out: null}; + if(dp == 'top') { + opts.in = {'top':settings.positionOffset}; + opts.out = {'top':'-300'}; + } else { + opts.in = {'bottom':settings.positionOffset}; + opts.out = {'bottom':'-300'}; + } + + // Start animation + $('#cookiesdirective').animate(opts.in, 1000, function() { + // Set event handlers depending on type of disclosure + if(settings.explicitConsent) { + // Explicit, need to check a box and click a button + $('#explicitsubmit').click(function() { + if($('#epdagree').is(':checked')) { + // Set a cookie to prevent this being displayed again + setCookie('cookiesDirective',1,365); + // Close the overlay + $('#cookiesdirective').animate(opts.out,1000,function() { + // Remove the elements from the DOM and reload page + $('#cookiesdirective').remove(); + location.reload(true); + }); + } else { + // Show message about explicit consent being required + // (CSS style display set to default) + $('#epdnotick').css('display', ''); + } + }); + } else { + // Implied consent, just a button to close it + $('#impliedsubmit').click(function() { + // Set a cookie to prevent this being displayed again + setCookie('cookiesDirective',1,365); + // Close the overlay + $('#cookiesdirective').animate(opts.out,1000,function() { + // Remove the elements from the DOM and reload page + $('#cookiesdirective').remove(); + }); + }); + } + + if(settings.duration > 0) + { + // Set a timer to remove the warning after 'settings.duration' seconds + setTimeout(function(){ + $('#cookiesdirective').animate({ + opacity:'0' + },2000, function(){ + $('#cookiesdirective').css(dp,'-300px'); + }); + }, settings.duration * 1000); + } + }); + }; +})(jQuery); diff --git a/core/static/js/csrf.js b/core/static/js/csrf.js new file mode 100755 index 00000000..9214b63b --- /dev/null +++ b/core/static/js/csrf.js @@ -0,0 +1,32 @@ + +function getCookie(name) { + 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; +} + +function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +} + +$(function () { + // Automatically send CSRF token if needed + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); + } + } + }); +}); diff --git a/core/static/js/jquery-3.2.1.min.js b/core/static/js/jquery-3.2.1.min.js new file mode 100644 index 00000000..644d35e2 --- /dev/null +++ b/core/static/js/jquery-3.2.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + """, + 3: """ +
+ %(rendered_widget)s + %(clear_button)s + +
+ + """ + } + +CLEAR_BTN_TEMPLATE = {2: """""", + 3: """"""} + + +quoted_options = set([ + 'format', + 'startDate', + 'endDate', + 'startView', + 'minView', + 'maxView', + 'todayBtn', + 'language', + 'pickerPosition', + 'viewSelect', + 'initialDate', + 'weekStart', + 'minuteStep' + 'daysOfWeekDisabled', + ]) + +# to traslate boolean object to javascript +quoted_bool_options = set([ + 'autoclose', + 'todayHighlight', + 'showMeridian', + 'clearBtn', + ]) + + +def quote(key, value): + """Certain options support string values. We want clients to be able to pass Python strings in + but we need them to be quoted in the output. Unfortunately some of those options also allow + numbers so we type check the value before wrapping it in quotes. + """ + + if key in quoted_options and isinstance(value, string_types): + return "'%s'" % value + + if key in quoted_bool_options and isinstance(value, bool): + return {True:'true',False:'false'}[value] + + return value + + +class PickerWidgetMixin(object): + + format_name = None + glyphicon = None + + def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=None): + + if bootstrap_version in [2,3]: + self.bootstrap_version = bootstrap_version + else: + # default 2 to mantain support to old implemetation of django-datetime-widget + self.bootstrap_version = 2 + + if attrs is None: + attrs = {'readonly': ''} + + self.options = options + + self.is_localized = False + self.format = None + + # We want to have a Javascript style date format specifier in the options dictionary and we + # want a Python style date format specifier as a member variable for parsing the date string + # from the form data + if usel10n is True: + # If we're doing localisation, get the local Python date format and convert it to + # Javascript data format for the options dictionary + self.is_localized = True + + # Get format from django format system + self.format = get_format(self.format_name)[0] + + # Convert Python format specifier to Javascript format specifier + self.options['format'] = toJavascript_re.sub( + lambda x: dateConversiontoJavascript[x.group()], + self.format + ) + + # Set the local language + self.options['language'] = get_supported_language(get_language()) + + else: + + # If we're not doing localisation, get the Javascript date format provided by the user, + # with a default, and convert it to a Python data format for later string parsing + format = self.options['format'] + self.format = toPython_re.sub( + lambda x: dateConversiontoPython[x.group()], + format + ) + + super(PickerWidgetMixin, self).__init__(attrs, format=self.format) + + def render(self, name, value, attrs=None, renderer=None): + final_attrs = self.build_attrs(attrs) + rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs) + + #if not set, autoclose have to be true. + self.options.setdefault('autoclose', True) + + # Build javascript options out of python dictionary + options_list = [] + for key, value in iter(self.options.items()): + options_list.append("%s: %s" % (key, quote(key, value))) + + js_options = ",\n".join(options_list) + + # Use provided id or generate hex to avoid collisions in document + id = final_attrs.get('id', uuid.uuid4().hex) + + clearBtn = quote('clearBtn', self.options.get('clearBtn', 'true')) == 'true' + + return mark_safe( + BOOTSTRAP_INPUT_TEMPLATE[self.bootstrap_version] + % dict( + id=id, + rendered_widget=rendered_widget, + clear_button=CLEAR_BTN_TEMPLATE[self.bootstrap_version] if clearBtn else "", + glyphicon=self.glyphicon, + options=js_options + ) + ) + + def _media(self): + + js = ["js/bootstrap-datetimepicker.js"] + + language = self.options.get('language', 'en') + if language != 'en': + js.append("js/locales/bootstrap-datetimepicker.%s.js" % language) + + return widgets.Media( + css={ + 'all': ('css/datetimepicker.css',) + }, + js=js + ) + + media = property(_media) + + +class DateTimeWidget(PickerWidgetMixin, DateTimeInput): + """ + DateTimeWidget is the corresponding widget for Datetime field, it renders both the date and time + sections of the datetime picker. + """ + + format_name = 'DATETIME_INPUT_FORMATS' + glyphicon = 'glyphicon-th' + + def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=None): + + if options is None: + options = {} + + # Set the default options to show only the datepicker object + options['format'] = options.get('format', 'dd/mm/yyyy hh:ii') + + super(DateTimeWidget, self).__init__(attrs, options, usel10n, bootstrap_version) + + +class DateWidget(PickerWidgetMixin, DateInput): + """ + DateWidget is the corresponding widget for Date field, it renders only the date section of + datetime picker. + """ + + format_name = 'DATE_INPUT_FORMATS' + glyphicon = 'glyphicon-calendar' + + def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=None): + + if options is None: + options = {} + + # Set the default options to show only the datepicker object + options['startView'] = options.get('startView', 2) + options['minView'] = options.get('minView', 2) + options['format'] = options.get('format', 'dd/mm/yyyy') + + super(DateWidget, self).__init__(attrs, options, usel10n, bootstrap_version) + + +class TimeWidget(PickerWidgetMixin, TimeInput): + """ + TimeWidget is the corresponding widget for Time field, it renders only the time section of + datetime picker. + """ + + format_name = 'TIME_INPUT_FORMATS' + glyphicon = 'glyphicon-time' + + def __init__(self, attrs=None, options=None, usel10n=None, bootstrap_version=None): + + if options is None: + options = {} + + # Set the default options to show only the timepicker object + options['startView'] = options.get('startView', 1) + options['minView'] = options.get('minView', 0) + options['maxView'] = options.get('maxView', 1) + options['format'] = options.get('format', 'hh:ii') + + super(TimeWidget, self).__init__(attrs, options, usel10n, bootstrap_version) + diff --git a/doc/changes.txt b/doc/changes.txt deleted file mode 100644 index 8a11199f..00000000 --- a/doc/changes.txt +++ /dev/null @@ -1,39 +0,0 @@ - -0. [blank] -1. Foo -2. Bar -3. Baz -4. Crud - - ------ -Possible changes: - -1. - Remove an item -2. - Move an item item (Change order of items) -3. - Change an item (Propose an alternative item) -4. - Add a new item (Add an item after the referenced item) - - -class ChangeProposal(self): - actiontype = models.IntegerField() # Type of change [all*] - refitem = models.IntegerField() # Number what in the sequence to act on [all*] - destination = models.IntegerField() # Destination of moved item, or of new item [move, add] - content = models.TextField() # Content for new item, or for changed item (blank=same on change) [change, add] - contenttype = models.IntegerField() # Type for new content item, or of changed item (0=same on change) [change, add] - -== Examples == - -ChangeProposal(actiontype=1, refitem=2) # Delete item 2 from the proposal -ChangeProposal(actiontype=2, refitem=2, destination=3) # Move item 2 to after item 3 (Bar after Baz) -ChangeProposal(actiontype=2, refitem=2, destination=0) # Move item 2 to after item 0 (beginning of list) -ChangeProposal(actiontype=3, refitem=2, content="Splurg") # Change text of item 2 from "Bar" to "Splurg" -ChangeProposal(actiontype=4, refitem=2, content="Splurg", contenttype=2) # Add "statement" object containing "Splurg" after "Bar" - -== Dud changes (ignore) == - -ChangeProposal(actiontype=1, refitem=0) # Delete blank dud item -ChangeProposal(actiontype=2, refitem=n, destination=n) # Move item to same place -ChangeProposal(actiontype=3, refitem=n, content="", contenttype=0) # Don't actually change anything -ChangeProposal(actiontype=4, refitem=n, content="", contenttype=0) # - diff --git a/doc/vocabulary.txt b/doc/vocabulary.txt deleted file mode 100644 index d5df97d0..00000000 --- a/doc/vocabulary.txt +++ /dev/null @@ -1,33 +0,0 @@ -Wasa2il uses a partially controlled vocabulary. This vocabulary may vary -from one language to the next, but the intent is that each word only has -one meaning that is consistent across the entire system. - -Words which do not appear on this list should not be considered controlled, -and can be used freely. - -A word which is controlled MUST be used in any reference to an object of the -stated type, and SHOULD NOT be used in reference to any object of any other -type. - -Controlled words (English base, translated versions nested): - - * Polity - (references model: Polity) - * is: Þing - * nl: Beslissingsgroep - * Topic - (references model: Topic) - * is: Málaflokkur - * Issue - (references model: Issue) - * is: Mál - * Meeting - (references model: Meeting) - * is: Fundur - * Document - (references model: Document) - * is: Skjal - * Agreement - (references model: Document, is_adopted=True) - * is: Samþykkt - diff --git a/doc/wasa2il.jpg b/doc/wasa2il.jpg deleted file mode 100644 index 71638b32..00000000 Binary files a/doc/wasa2il.jpg and /dev/null differ diff --git a/doc/wasa2il.xmind b/doc/wasa2il.xmind deleted file mode 100644 index f4d7234d..00000000 Binary files a/doc/wasa2il.xmind and /dev/null differ diff --git a/doc/wasa2il_introduction.odp b/doc/wasa2il_introduction.odp deleted file mode 100644 index a27d6c59..00000000 Binary files a/doc/wasa2il_introduction.odp and /dev/null differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..14f70f8e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '2' + +services: + app: + env_file: .env + build: . + volumes: + - .:/usr/src/app + ports: + - "8000:8000" + links: + - "db" + depends_on: + - db + environment: + - W2_DATABASE_HOST=db + - W2_DATABASE_NAME=wasa2il + - W2_DATABASE_PASSWORD=wasa2il + - W2_DATABASE_PORT=3306 + - W2_DATABASE_USER=wasa2il + db: + image: mysql:5 + environment: + - MYSQL_ROOT_PASSWORD=wasa2il + - MYSQL_DATABASE=wasa2il + - MYSQL_USER=wasa2il + - MYSQL_PASSWORD=wasa2il + # This makes the port OPEN on the server. + # Disable in production! + ports: + - "3306:3306" diff --git a/election/__init__.py b/election/__init__.py new file mode 100644 index 00000000..ff3c16bd --- /dev/null +++ b/election/__init__.py @@ -0,0 +1,4 @@ + + +def heartbeat(t): + pass diff --git a/election/admin.py b/election/admin.py new file mode 100644 index 00000000..4b918f4d --- /dev/null +++ b/election/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from election.models import Candidate +from election.models import Election + +register = admin.site.register +register(Candidate) +register(Election) diff --git a/election/dataviews.py b/election/dataviews.py new file mode 100644 index 00000000..80921673 --- /dev/null +++ b/election/dataviews.py @@ -0,0 +1,242 @@ +import random + +from datetime import datetime +from datetime import timedelta +from hashlib import md5 + +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django.db import transaction +from django.db.models import Q +from django.http import Http404 +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +from django.template.loader import render_to_string +from django.utils.text import slugify +from django.views.decorators.http import require_http_methods + +from core.ajax.utils import jsonize + +from election.models import Candidate +from election.models import Election +from election.models import ElectionVote + +from polity.models import Polity + + +def _ordered_candidates(user, all_candidates, candidates): + # This will sort the unchosen candidates in a stable order which is + # different for each individual user. Rather than make it completely + # random, the list is alphabetical, but may or may not be reversed + # and the starting point varies. + if len(all_candidates) < 1: + return [] + + if user.is_authenticated: + randish = int(md5((repr(user) + str(user.id)).encode('utf-8')).hexdigest()[:8], 16) + else: + randish = random.randint(0, 0xffffff) + + def _sname(u): + try: + return slugify(u.userprofile.displayname or u.username) + except: + return slugify(u.username) + + ordered = list(all_candidates) + ordered.sort(key=lambda c: _sname(c.user)) + + pivot = (randish // 4) % len(ordered) + if randish % 2 == 0: + ordered.reverse() + part1 = ordered[pivot:] + part2 = ordered[:pivot] + + return [c for c in (part1 + part2) if c in candidates] + + +@jsonize +def election_poll(request, **kwargs): + election = get_object_or_404(Election, + id=request.POST.get("election", request.GET.get("election", -1))) + + user_can_vote = election.can_vote(request.user) + all_candidates = election.get_candidates() + + ctx = { + "logged_out": not request.user.is_authenticated, + "election": { + "user_is_candidate": + (request.user in [x.user for x in election.candidate_set.all()]), + "election_state": election.election_state(), + "votes": election.get_vote_count(), + "candidates": all_candidates, + "vote": {}}} + + ctx["election"]["candidates"]["html"] = render_to_string( + "election/_election_candidate_list.html", { + "user_can_vote": user_can_vote, + "election": election, + "candidate_total": len(all_candidates), + "candidates": _ordered_candidates( + request.user, + Candidate.objects.filter(election=election), + election.get_unchosen_candidates(request.user)), + "candidate_selected": False}) + + ctx["election"]["vote"]["html"] = render_to_string( + "election/_election_candidate_list.html", { + "user_can_vote": user_can_vote, + "election": election, + "candidate_total": len(all_candidates), + "candidates": election.get_vote(request.user), + "candidate_selected": True}) + + for k, v in kwargs.items(): + ctx["election"][k] = v + + ctx["ok"] = kwargs.get("ok", True) + + return ctx + + +@require_http_methods(["POST"]) +@login_required +@jsonize +def election_candidacy(request): + election = get_object_or_404(Election, id=request.POST.get("election", 0)) + if election.election_state() == 'concluded': + return election_poll(request) + + val = int(request.POST.get("val", 0)) + if val == 0: + Candidate.objects.filter(user=request.user, election=election).delete() + elif election.can_be_candidate(request.user): + cand, created = Candidate.objects.get_or_create(user=request.user, election=election) + + return election_poll(request) + + +@transaction.atomic +def _record_votes(election, user, order): + ElectionVote.objects.filter(election=election, user=user).delete() + + for i in range(len(order)): + candidate = Candidate.objects.get(id=order[i]) + ElectionVote( + election=election, user=user, candidate=candidate, value=i + ).save() + + +@require_http_methods(["POST"]) +@login_required +@jsonize +def election_vote(request): + election = get_object_or_404(Election, id=request.POST.get("election", -1)) + ctx = {} + ctx["ok"] = True + + logged_in = request.user.is_authenticated + can_vote = logged_in and election.can_vote(request.user) + if not (logged_in and can_vote and election.election_state() == 'voting'): + ctx["please_login"] = not logged_in + ctx["can_vote"] = can_vote + ctx["ok"] = False + return ctx + + ok = True + try: + votes = request.POST.getlist("order[]") + _record_votes(election, request.user, votes) + except: + # FIXME: Report with more granularity what went wrong. + ok = False + + return election_poll(request, ok=ok) + + +@jsonize +def election_showclosed(request): + ctx = {} + + polity_id = int(request.GET.get('polity_id', 0)) + showclosed = int(request.GET.get('showclosed', 0)) # 0 = False, 1 = True + + try: + if polity_id: + elections = Election.objects.filter(Q(polity_id=polity_id) | Q(polity__parent_id=polity_id)) + else: + elections = Election.objects.order_by('polity__name', '-deadline_votes') + + if not showclosed: + elections = elections.recent() + + if polity_id: + polity = get_object_or_404(Polity, id=polity_id) + else: + polity = None + + html_ctx = { + 'user': request.user, + 'polity': polity, + 'elections_recent': elections, + } + + ctx['showclosed'] = showclosed + ctx['html'] = render_to_string('election/_elections_recent_table.html', html_ctx) + ctx['ok'] = True + except Exception as e: + ctx['error'] = e.__str__() if settings.DEBUG else 'Error raised. Turn on DEBUG for details.' + + return ctx + + +def election_stats_download(request, polity_id=None, election_id=None, filename=None): + election = get_object_or_404( + Election, + id=election_id, + polity_id=polity_id, + is_processed=True, + stats_publish_files=True + ) + + filetype = filename.split('.')[-1].lower() + assert(filetype in ('json', 'xlsx', 'ods', 'html')) + + response = HttpResponse( + election.get_formatted_stats(filetype, user=request.user), + content_type={ + 'json': 'application/json; charset=utf-8', + 'ods': 'application/vnd.oasis.opendocument.spreadsheet', + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'html': 'text/html; charset=utf-8' + }.get(filetype, 'application/octet-stream')) + + response['Content-Disposition'] = 'attachment; filename="%s"' % filename + return response + + +@login_required +def election_candidates_details(request, polity_id, election_id): + try: + election = Election.objects.get(id=election_id, polity_id=polity_id, polity__officers=request.user) + except Election.DoesNotExist: + raise PermissionDenied() + + candidates = election.candidate_set.select_related('user__userprofile').order_by('user__userprofile__verified_name') + + candidate_list = ['"SSN","Name from registry","Email address","Username"'] + for user in [c.user for c in candidates]: + candidate_list.append(','.join(['"%s"' % item for item in [ + user.userprofile.verified_ssn, + user.userprofile.verified_name, + user.email, + user.username, + ]])) + + filename = u'Candidates - %s.csv' % election.name + + response = HttpResponse('\n'.join(candidate_list), content_type='application/csv; charset=utf-8') + response['Content-Disposition'] = 'attachment; filename="%s"' % filename + return response diff --git a/election/forms.py b/election/forms.py new file mode 100644 index 00000000..08499126 --- /dev/null +++ b/election/forms.py @@ -0,0 +1,9 @@ +from wasa2il.forms import Wasa2ilForm + +from election.models import Election + + +class ElectionForm(Wasa2ilForm): + class Meta: + model = Election + exclude = ('polity', 'slug', 'is_processed', 'stats') diff --git a/election/management/__init__.py b/election/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/election/management/commands/__init__.py b/election/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/election/management/commands/processelections.py b/election/management/commands/processelections.py new file mode 100644 index 00000000..207be5e2 --- /dev/null +++ b/election/management/commands/processelections.py @@ -0,0 +1,58 @@ +from sys import stdout, stderr +from datetime import datetime + +from django.conf import settings +from django.core.management.base import BaseCommand + +from election.models import Election + + +class Command(BaseCommand): + + def add_arguments(self, parser): + parser.add_argument('election_id', nargs='*', type=int) + + def handle(self, *args, **options): + + try: + if not settings.BALLOT_SAVEFILE_FORMAT: + print() + print('WARNING! This command will permanently delete EVERY ballot of EVERY election!') + print('Only do this if you know what you\'re doing. You have been warned.') + print() + response = '' + while response != 'yes' and response != 'no': + response = raw_input('Are you REALLY certain that you wish to proceed? (yes/no) ').lower() + + if response == 'no': + print() + print('Chicken.') + print() + return + + elections = Election.objects.filter(is_processed=False) + + for election in elections: + if (options.get('election_id') and + election.id not in options['election_id']): + stdout.write('Skipping election %s (%s)\n' % (election, election.id)) + continue + + stdout.write('Processing election %s...' % election) + + try: + election.process() + stdout.write(' done\n') + except Election.AlreadyProcessedException: + stdout.write(' already processed\n') + except Election.ElectionInProgressException: + stdout.write(' still in progress\n') + except: + import traceback + stdout.write(' failed for unknown reasons\n') + traceback.print_exc() + + except KeyboardInterrupt: + print + quit() + diff --git a/election/migrations/0001_initial.py b/election/migrations/0001_initial.py new file mode 100644 index 00000000..75ffa4e4 --- /dev/null +++ b/election/migrations/0001_initial.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-07-12 14:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('polity', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Candidate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Election', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('slug', models.SlugField(blank=True, max_length=128)), + ('voting_system', models.CharField(choices=[(b'condorcet', 'Condorcet'), (b'schulze', 'Schulze, ordered list'), (b'schulze_old', 'Schulze, ordered list (old)'), (b'schulze_new', 'Schulze, ordered list (new)'), (b'schulze_both', 'Schulze, ordered list (both)'), (b'stcom', 'Steering Committee Election'), (b'stv1', 'STV, single winner'), (b'stv2', 'STV, two winners'), (b'stv3', 'STV, three winners'), (b'stv4', 'STV, four winners'), (b'stv5', 'STV, five winners'), (b'stv8', 'STV, eight winners'), (b'stv10', 'STV, ten winners'), (b'stonethor', 'STV partition with Schulze ranking')], max_length=30, verbose_name='Voting system')), + ('results_are_ordered', models.BooleanField(default=True, verbose_name='Results are ordered')), + ('results_limit', models.IntegerField(blank=True, null=True, verbose_name='How many candidates will be publicly listed in the results of an election')), + ('deadline_candidacy', models.DateTimeField(verbose_name='Deadline for candidacies')), + ('starttime_votes', models.DateTimeField(blank=True, null=True, verbose_name='Election begins')), + ('deadline_votes', models.DateTimeField(verbose_name='Election ends')), + ('deadline_joined_org', models.DateTimeField(blank=True, null=True, verbose_name='Membership deadline')), + ('is_processed', models.BooleanField(default=False)), + ('instructions', models.TextField(blank=True, null=True, verbose_name='Instructions')), + ('stats', models.TextField(blank=True, null=True, verbose_name='Statistics as JSON')), + ('stats_publish_ballots_basic', models.BooleanField(default=False, verbose_name='Publish basic ballot statistics')), + ('stats_publish_ballots_per_candidate', models.BooleanField(default=False, verbose_name='Publish ballot statistics for each candidate')), + ('stats_publish_files', models.BooleanField(default=False, verbose_name='Publish advanced statistics (downloadable)')), + ('candidate_polities', models.ManyToManyField(blank=True, related_name='remote_election_candidates', to='polity.Polity', verbose_name='Candidate polities')), + ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')), + ('voting_polities', models.ManyToManyField(blank=True, related_name='remote_election_votes', to='polity.Polity', verbose_name='Voting polities')), + ], + options={ + 'ordering': ['-deadline_votes'], + }, + ), + migrations.CreateModel( + name='ElectionResult', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('vote_count', models.IntegerField()), + ('election', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='result', to='election.Election')), + ], + ), + migrations.CreateModel( + name='ElectionResultRow', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.IntegerField()), + ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='election.Candidate')), + ('election_result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rows', to='election.ElectionResult')), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='ElectionVote', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.IntegerField()), + ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='election.Candidate')), + ('election', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='election.Election')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='candidate', + name='election', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='election.Election'), + ), + migrations.AddField( + model_name='candidate', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterUniqueTogether( + name='electionvote', + unique_together=set([('election', 'user', 'candidate'), ('election', 'user', 'value')]), + ), + ] diff --git a/election/migrations/0002_auto_20190822_1451.py b/election/migrations/0002_auto_20190822_1451.py new file mode 100644 index 00000000..9cec8746 --- /dev/null +++ b/election/migrations/0002_auto_20190822_1451.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-08-22 14:51 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('election', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='electionresultrow', + name='candidate', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='result_row', to='election.Candidate'), + ), + ] diff --git a/election/migrations/0003_auto_20190822_2006.py b/election/migrations/0003_auto_20190822_2006.py new file mode 100644 index 00000000..bc323383 --- /dev/null +++ b/election/migrations/0003_auto_20190822_2006.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-08-22 20:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('election', '0002_auto_20190822_1451'), + ] + + operations = [ + migrations.AlterField( + model_name='election', + name='voting_system', + field=models.CharField(choices=[('condorcet', 'Condorcet'), ('schulze', 'Schulze, ordered list'), ('schulze_old', 'Schulze, ordered list (old)'), ('schulze_new', 'Schulze, ordered list (new)'), ('schulze_both', 'Schulze, ordered list (both)'), ('stcom', 'Steering Committee Election'), ('stv1', 'STV, single winner'), ('stv2', 'STV, two winners'), ('stv3', 'STV, three winners'), ('stv4', 'STV, four winners'), ('stv5', 'STV, five winners'), ('stv8', 'STV, eight winners'), ('stv10', 'STV, ten winners'), ('stonethor', 'STV partition with Schulze ranking')], max_length=30, verbose_name='Voting system'), + ), + ] diff --git a/election/migrations/0004_auto_20200121_1340.py b/election/migrations/0004_auto_20200121_1340.py new file mode 100644 index 00000000..a4ea82ac --- /dev/null +++ b/election/migrations/0004_auto_20200121_1340.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-01-21 13:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('election', '0003_auto_20190822_2006'), + ] + + operations = [ + migrations.AlterField( + model_name='election', + name='voting_system', + field=models.CharField(choices=[('condorcet', 'Condorcet'), ('schulze', 'Schulze, ordered list'), ('stv1', 'STV, single winner'), ('stv2', 'STV, two winners'), ('stv3', 'STV, three winners'), ('stv4', 'STV, four winners'), ('stv5', 'STV, five winners'), ('stv8', 'STV, eight winners'), ('stv10', 'STV, ten winners')], max_length=30, verbose_name='Voting system'), + ), + ] diff --git a/election/migrations/0005_auto_20200909_1553.py b/election/migrations/0005_auto_20200909_1553.py new file mode 100644 index 00000000..06133884 --- /dev/null +++ b/election/migrations/0005_auto_20200909_1553.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-09-09 15:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('election', '0004_auto_20200121_1340'), + ] + + operations = [ + migrations.AlterField( + model_name='election', + name='voting_system', + field=models.CharField(choices=[('condorcet', 'Condorcet'), ('schulze', 'Schulze, ordered list'), ('stv1', 'STV, single winner'), ('stv2', 'STV, two winners'), ('stv3', 'STV, three winners'), ('stv4', 'STV, four winners'), ('stv5', 'STV, five winners'), ('stv6', 'STV, six winners'), ('stv8', 'STV, eight winners'), ('stv10', 'STV, ten winners')], max_length=30, verbose_name='Voting system'), + ), + ] diff --git a/election/migrations/0006_auto_20220102_1823.py b/election/migrations/0006_auto_20220102_1823.py new file mode 100644 index 00000000..f2853c25 --- /dev/null +++ b/election/migrations/0006_auto_20220102_1823.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.25 on 2022-01-02 18:23 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('election', '0005_auto_20200909_1553'), + ] + + operations = [ + migrations.AddField( + model_name='election', + name='conditions', + field=models.TextField(blank=True, help_text='Candidates must accept these conditions to be allowed to run in the election. Anything binding for the candidates should be placed here, for example if candidates are expected to abide by certain rules, to volunteer their time in a some way or provide particular information.', null=True, verbose_name='Conditions for candidates'), + ), + migrations.AlterField( + model_name='election', + name='instructions', + field=models.TextField(blank=True, help_text='Instructions or other information that might be of importance to those casting their votes.', null=True, verbose_name='Instructions for voters'), + ), + migrations.AlterUniqueTogether( + name='candidate', + unique_together={('user', 'election')}, + ), + ] diff --git a/election/migrations/__init__.py b/election/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/election/models.py b/election/models.py new file mode 100644 index 00000000..26aa73e9 --- /dev/null +++ b/election/models.py @@ -0,0 +1,467 @@ +import json +import os +from datetime import datetime +from datetime import timedelta + +from django.conf import settings +from django.contrib.auth.models import User +from django.db import models +from django.db import transaction +from django.db.models import CASCADE +from django.utils.translation import ugettext_lazy as _ + +from election.utils import BallotCounter + + +class ElectionQuerySet(models.QuerySet): + def recent(self): + return self.filter(deadline_votes__gt=datetime.now() - timedelta(days=settings.RECENT_ELECTION_DAYS)) + +class Election(models.Model): + """ + An election is different from an issue vote; it's a vote + on people. Users, specifically. + """ + objects = ElectionQuerySet.as_manager() + + # Note: Not used for model field options (at least not yet), but rather + # the get_election_state_display function below. + ELECTION_STATES = ( + ('concluded', _('Concluded')), + ('voting', _('Voting')), + ('waiting', _('Waiting')), + ('accepting_candidates', _('Accepting candidates')), + ) + + VOTING_SYSTEMS = BallotCounter.VOTING_SYSTEMS + + name = models.CharField(max_length=128, verbose_name=_('Name')) + slug = models.SlugField(max_length=128, blank=True) + + polity = models.ForeignKey('polity.Polity', on_delete=CASCADE) + voting_system = models.CharField(max_length=30, verbose_name=_('Voting system'), choices=VOTING_SYSTEMS) + + # Tells whether the election results page should show the winning + # candidates as an ordered list or as a set of winners. Some voting + # systems (most notably STV) do not typically give an ordered list where + # one candidate is higher or lower than another one. It would be more + # elegant to set this in a model describing the voting system in more + # detail. To achieve that, the BallotCounter.VOTING_SYSTEMS list above + # should to be turned into a proper Django model. + results_are_ordered = models.BooleanField(default=True, verbose_name=_('Results are ordered')) + + # How many candidates will be publicly listed in the results of an + # election. Officers still see entire list. Individual candidates can also + # see where they ended up in the results. + results_limit = models.IntegerField( + null=True, + blank=True, + verbose_name=_('How many candidates will be publicly listed in the results of an election') + ) + + deadline_candidacy = models.DateTimeField(verbose_name=_('Deadline for candidacies')) + starttime_votes = models.DateTimeField(null=True, blank=True, verbose_name=_('Election begins')) + deadline_votes = models.DateTimeField(verbose_name=_('Election ends')) + + # This allows one polity to host elections for one or more others, in + # particular allowing access to elections based on geographical polities + # without residency granting access to participate in all other polity + # activities. + voting_polities = models.ManyToManyField('polity.Polity', blank=True, related_name='remote_election_votes', verbose_name=_('Voting polities')) + candidate_polities = models.ManyToManyField('polity.Polity', blank=True, related_name='remote_election_candidates', verbose_name=_('Candidate polities')) + + # Sometimes elections may depend on a user having been the organization's member for an X amount of time + # This optional field lets the vote counter disregard members who are too new. + deadline_joined_org = models.DateTimeField(null=True, blank=True, verbose_name=_('Membership deadline')) + is_processed = models.BooleanField(default=False) + + instructions = models.TextField(null=True, blank=True, verbose_name=_('Instructions for voters'), help_text=_('Instructions or other information that might be of importance to those casting their votes.')) + conditions = models.TextField(null=True, blank=True, verbose_name=_('Conditions for candidates'), help_text=_('Candidates must accept these conditions to be allowed to run in the election. Anything binding for the candidates should be placed here, for example if candidates are expected to abide by certain rules, to volunteer their time in a some way or provide particular information.')) + + # These are election statistics; + stats = models.TextField(null=True, blank=True, verbose_name=_('Statistics as JSON')) + stats_publish_ballots_basic = models.BooleanField(default=False, verbose_name=_('Publish basic ballot statistics')) + stats_publish_ballots_per_candidate = models.BooleanField(default=False, verbose_name=_('Publish ballot statistics for each candidate')) + stats_publish_files = models.BooleanField(default=False, verbose_name=_('Publish advanced statistics (downloadable)')) + + class Meta: + ordering = ['-deadline_votes'] + + # An election can only be processed once, since votes are deleted during the process + class AlreadyProcessedException(Exception): + def __init__(self, message): + super(Election.AlreadyProcessedException, self).__init__(message) + + class ElectionInProgressException(Exception): + def __init__(self, message): + super(Election.ElectionInProgressException, self).__init__(message) + + def save_ballots(self, ballot_counter): + if settings.BALLOT_SAVEFILE_FORMAT is not None: + try: + filename = settings.BALLOT_SAVEFILE_FORMAT % { + 'election_id': self.id, + 'voting_system': self.voting_system} + directory = os.path.dirname(filename) + if not os.path.exists(directory): + os.mkdir(directory) + ballot_counter.save_ballots(filename) + except: + import traceback + traceback.print_exc() + return False + return True + + def load_archived_ballots(self): + bc = BallotCounter() + if settings.BALLOT_SAVEFILE_FORMAT is not None: + try: + filename = settings.BALLOT_SAVEFILE_FORMAT % { + 'election_id': self.id, + 'voting_system': self.voting_system} + bc.load_ballots(filename) + except: + import traceback + traceback.print_exc() + return bc + + @transaction.atomic + def process(self): + if self.election_state() != 'concluded': + raise Election.ElectionInProgressException('Election %s is still in progress!' % self) + + if self.is_processed: + raise Election.AlreadyProcessedException('Election %s has already been processed!' % self) + + # "Flatten" the values of votes in an election. A candidate may be + # removed from an election when voting has already started. When that + # happens, ballots with that candidate may have a gap in their values, + # for example [0, 1, 2, 4] , because the person with value 3 was + # removed from the election. Here the ballot is "flattened" so that + # gaps are eliminated and the values are made sequential, i.e. + # [0, 1, 2, 3] and not [0, 1, 2, 4]. + votes = self.electionvote_set.order_by('user_id', 'value') + last_user_id = 0 + for vote in votes: + # Reset correct value every time we start processing a new user. + if last_user_id != vote.user_id: + correct_value = 0 + + if vote.value != correct_value: + vote.value = correct_value + vote.save() + + correct_value += 1 + last_user_id = vote.user_id + + if self.candidate_set.count() == 0: + # If there are no candidates, there's no need to calculate + # anything. We're pretty confident in these being the results. + ordered_candidates = [] + vote_count = 0 + save_failed = False + + else: + ordered_candidates, ballot_counter = self.process_votes() + vote_count = self.electionvote_set.values('user').distinct().count() + + # Save anonymized ballots to a file, so we can recount later + save_failed = not self.save_ballots(ballot_counter) + + # Generate stats before deleting everything. This allows us to + # analyze the voters as well as the ballots. + self.generate_stats() + + try: + election_result = ElectionResult.objects.get(election=self) + except ElectionResult.DoesNotExist: + election_result = ElectionResult.objects.create(election=self, vote_count=vote_count) + + election_result.rows.all().delete() + order = 0 + for candidate in ordered_candidates: + order = order + 1 + election_result_row = ElectionResultRow() + election_result_row.election_result = election_result + election_result_row.candidate = candidate + election_result_row.order = order + election_result_row.save() + + # Delete the original votes (for anonymity), we have the ballots elsewhere + if not save_failed: + self.electionvote_set.all().delete() + + self.is_processed = True + self.save() + + if self.polity.push_on_election_end: + # Doing this just to force the translation string creation: + __ = _("Election results in election '%s' have been calculated.") + push_send_notification_to_polity_users(self.polity.id, "Election results in election '%s' have been calculated.", [self.name]) + + def generate_stats(self): + ballot_counter = self.load_archived_ballots() + if ballot_counter.ballots: + stats = {} + stats.update(ballot_counter.get_candidate_rank_stats()) + stats.update(ballot_counter.get_candidate_pairwise_stats()) + stats.update(ballot_counter.get_ballot_stats()) + self.stats = json.dumps(stats) + return True + else: + return False + + def get_voters(self): + if self.voting_polities.count() > 0: + voters = User.objects.filter(polities__in=self.voting_polities.all()) + else: + voters = self.polity.election_voters() + + if self.deadline_joined_org: + return voters.filter(userprofile__joined_org__lt = self.deadline_joined_org) + else: + return voters + + def can_vote(self, user=None, user_id=None): + return self.get_voters().filter( + id=(user_id if (user_id is not None) else user.id)).exists() + + def get_potential_candidates(self): + if self.candidate_polities.count() > 0: + pcands = User.objects.filter(polities__in=self.candidate_polities.all()) + else: + pcands = self.polity.election_potential_candidates() + + # NOTE: We ignore the deadline here, it's only meant to prevent + # manipulation of votes not prevent people from running for + # office or otherwise participating in things. + + return pcands + + def can_be_candidate(self, user=None, user_id=None): + return self.get_potential_candidates().filter( + id=(user_id if (user_id is not None) else user.id)).exists() + + def process_votes(self): + if self.deadline_joined_org: + votes = ElectionVote.objects.select_related('candidate__user').filter(election=self, user__userprofile__joined_org__lt = self.deadline_joined_org) + else: + votes = ElectionVote.objects.select_related('candidate__user').filter(election=self) + + votemap = {} + for vote in votes: + if not vote.user_id in votemap: + votemap[vote.user_id] = [] + votemap[vote.user_id].append(vote) + + ballots = [] + for user_id in votemap: + ballot = [(int(v.value), v.candidate) for v in votemap[user_id]] + ballots.append(ballot) + + ballot_counter = BallotCounter(ballots) + return ballot_counter.results(self.voting_system), ballot_counter + + def get_ordered_candidates_from_votes(self): + return self.process_votes()[0] + + def __str__(self): + return u'%s' % self.name + + def voting_start_time(self): + if self.starttime_votes not in (None, ""): + return max(self.starttime_votes, self.deadline_candidacy) + return self.deadline_candidacy + + def election_state(self): + # Short-hands. + now = datetime.now() + deadline_candidacy = self.deadline_candidacy + deadline_votes = self.deadline_votes + voting_start_time = self.voting_start_time() + + if deadline_votes < now: + return 'concluded' + elif voting_start_time < now: + return 'voting' + elif voting_start_time > now and deadline_candidacy < now: + return 'waiting' + elif deadline_candidacy > now: + return 'accepting_candidates' + else: + # Should never happen. + return 'unknown' + + def get_election_state_display(self): + return dict(self.ELECTION_STATES)[self.election_state()].__str__() + + def get_stats(self, user=None, load_users=True, rename_users=False): + """Load stats from the DB and convert to pythonic format. + + We expect stats to change over time, so the function provides + reasonable defaults for everything we care about even if the + JSON turns out to be incomplete. Changes to our stats logic will + not require a schema change, but stats cannot readily be queried. + Pros and cons... + """ + stats = { + 'ranking_matrix': [], + 'pairwise_matrix': [], + 'candidates': [], + 'ballot_lengths': {}, + 'ballots': 0, + 'ballot_length_average': 0, + 'ballot_length_most_common': 0} + + # Parse the stats JSON, if it exists. + try: + stats.update(json.loads(self.stats)) + except: + pass + + # Convert ballot_lengths keys (back) to ints + new_ballot_lengths = {} + for k in stats['ballot_lengths'].keys(): + new_ballot_lengths[int(k)] = stats['ballot_lengths'][k] + stats['ballot_lengths'] = new_ballot_lengths + del new_ballot_lengths + + # Censor the statistics, if we only want to publish details about + # the top N candidates. + if self.results_limit: + excluded = set([]) + if not user or not user.is_staff: + excluded |= set(cand.user.username for cand in + self.get_winners()[self.results_limit:]) + if user and user.username in excluded: + excluded.remove(user.username) + stats = BallotCounter.exclude_candidate_stats(stats, excluded) + + # Convert usernames to users. Let's hope usernames never change! + for i, c in enumerate(stats['candidates']): + try: + if not c: + pass + elif load_users: + stats['candidates'][i] = User.objects.get(username=c) + elif rename_users: + u = User.objects.get(username=c) + stats['candidates'][i] = '%s (%s)' % (u.get_name(), c) + except: + pass + + # Create more accessible representations of the tables + stats['rankings'] = {} + stats['victories'] = {} + for i, c in enumerate(stats['candidates']): + if stats.get('ranking_matrix'): + stats['rankings'][c] = stats['ranking_matrix'][i] + if stats.get('pairwise_matrix'): + stats['victories'][c] = stats['pairwise_matrix'][i] + + return stats + + def get_formatted_stats(self, fmt, user=None): + stats = self.get_stats(user=user, rename_users=True, load_users=False) + if fmt == 'json': + return json.dumps(stats, indent=1) + elif fmt in ('text', 'html'): + return BallotCounter.stats_as_text(stats) + elif fmt in ('xlsx', 'ods'): + return BallotCounter.stats_as_spreadsheet(fmt, stats) + else: + return None + + def get_winners(self): + return [r.candidate for r in self.result.rows.select_related('candidate__user__userprofile').order_by('order')] + + def get_candidates(self): + ctx = {} + ctx["count"] = self.candidate_set.count() + ctx["users"] = [{"username": x.user.username} for x in self.candidate_set.all()] + return ctx + + def get_unchosen_candidates(self, user): + if not user.is_authenticated or self.election_state() != 'voting': + return Candidate.objects.filter(election=self) + # votes = [] + votes = ElectionVote.objects.filter(election=self, user=user) + votedcands = [x.candidate.id for x in votes] + if len(votedcands) != 0: + candidates = Candidate.objects.filter(election=self).exclude(id__in=votedcands).order_by('?') + else: + candidates = Candidate.objects.filter(election=self).order_by('?') + + return candidates + + def get_vote_count(self): + if self.is_processed: + return self.result.vote_count + else: + return self.electionvote_set.values("user").distinct().count() + + def has_voted(self, user, **constraints): + if user.is_anonymous: + return False + return ElectionVote.objects.filter( + election=self, user=user, **constraints).exists() + + def get_vote(self, user): + votes = [] + if not user.is_anonymous: + votes = ElectionVote.objects.filter(election=self, user=user).order_by("value") + return [x.candidate for x in votes] + + def get_ballots(self): + ballot_box = [] + for voter in self.electionvote_set.values("user").distinct(): + user = User.objects.get(pk=voter["user"]) + ballot = [] + for vote in user.electionvote_set.filter(election=self).order_by('value'): + ballot.append(vote.candidate.user.username) + ballot_box.append(ballot) + random.shuffle(ballot_box) + return ballot_box + + +class Candidate(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE) + election = models.ForeignKey(Election, on_delete=CASCADE) + + def __lt__(self, other): + # Make it possible to sort Candidates + return str(self) < str(other) + + def __str__(self): + return u'%s' % self.user.username + + class Meta: + unique_together = ['user', 'election'] + + +class ElectionVote(models.Model): + election = models.ForeignKey(Election, on_delete=CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE) + candidate = models.ForeignKey(Candidate, on_delete=CASCADE) + value = models.IntegerField() + + class Meta: + unique_together = (('election', 'user', 'candidate'), + ('election', 'user', 'value')) + + def __str__(self): + return u'User %s has voted in election %s' % (self.user, self.election) + + +class ElectionResult(models.Model): + election = models.OneToOneField('Election', related_name='result', on_delete=CASCADE) + vote_count = models.IntegerField() + + +class ElectionResultRow(models.Model): + election_result = models.ForeignKey('ElectionResult', related_name='rows', on_delete=CASCADE) + candidate = models.OneToOneField('Candidate', related_name='result_row', on_delete=CASCADE) + order = models.IntegerField() + + class Meta: + ordering = ['order'] diff --git a/wasa2il/schulze.py b/election/schulze.py similarity index 98% rename from wasa2il/schulze.py rename to election/schulze.py index e254e16f..89898f55 100644 --- a/wasa2il/schulze.py +++ b/election/schulze.py @@ -50,8 +50,8 @@ def get_ordered_voting_results(strongest_paths): # For all candidates, compare their path strengths in both directions, the candidate that has stronger path # wins the other candidate. Order them from the candidate that wins all others, to the one that wins none. wins = defaultdict(list) - for ci in strongest_paths.iterkeys(): - for cj in strongest_paths.iterkeys(): + for ci in strongest_paths.keys(): + for cj in strongest_paths.keys(): if ci == cj: continue diff --git a/election/templatetags/__init__.py b/election/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/election/templatetags/elections.py b/election/templatetags/elections.py new file mode 100644 index 00000000..a4982296 --- /dev/null +++ b/election/templatetags/elections.py @@ -0,0 +1,34 @@ +from django import template + +from election.models import ElectionVote + +register = template.Library() + + +@register.filter(name='electionvoted') +def electionvoted(election, user): + ut = 0 + try: + ut = ElectionVote.objects.filter(user=user, election=election).count() + except TypeError: + pass + + return (ut > 0) + + +@register.filter +def sparkline(variable, skip_last=False): + if not variable: + return '' + if isinstance(variable, dict): + pairs = sorted([(k, v) for k, v in variable.items()]) + sparkline = [0] * (pairs[-1][0] + 1) + for i, v in pairs: + sparkline[i] = v + if 0 not in variable and '0' not in variable: + variable = sparkline[1:] + else: + variable = sparkline + if skip_last: + variable = variable[:-1] + return ','.join(str(v) for v in variable) diff --git a/election/test/__init__.py b/election/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/election/test/test_schulze.py b/election/test/test_schulze.py new file mode 100644 index 00000000..3a6b532e --- /dev/null +++ b/election/test/test_schulze.py @@ -0,0 +1,282 @@ +from django.test import TestCase +from election import schulze +import random + + +class SchulzeTest(TestCase): + def test_6_candidates_45_votes(self): + # Candidate 'x' is someone no-one voted for but was eligible. + # Otherwise, this test reflects the example on + # https://en.wikipedia.org/wiki/Schulze_method + candidates = ['a', 'b', 'c', 'd', 'e', 'x'] + votes = [ + [(1, 'a'), (2, 'c'), (3, 'b'), (4, 'e'), (5, 'd')], + [(1, 'a'), (2, 'c'), (3, 'b'), (4, 'e'), (5, 'd')], + [(1, 'a'), (2, 'c'), (3, 'b'), (4, 'e'), (5, 'd')], + [(1, 'a'), (2, 'c'), (3, 'b'), (4, 'e'), (5, 'd')], + [(1, 'a'), (2, 'c'), (3, 'b'), (4, 'e'), (5, 'd')], # 5 + [(1, 'a'), (2, 'd'), (3, 'e'), (4, 'c'), (5, 'b')], + [(1, 'a'), (2, 'd'), (3, 'e'), (4, 'c'), (5, 'b')], + [(1, 'a'), (2, 'd'), (3, 'e'), (4, 'c'), (5, 'b')], + [(1, 'a'), (2, 'd'), (3, 'e'), (4, 'c'), (5, 'b')], + [(1, 'a'), (2, 'd'), (3, 'e'), (4, 'c'), (5, 'b')], # 5 + [(1, 'b'), (2, 'e'), (3, 'd'), (4, 'a'), (5, 'c')], + [(1, 'b'), (2, 'e'), (3, 'd'), (4, 'a'), (5, 'c')], + [(1, 'b'), (2, 'e'), (3, 'd'), (4, 'a'), (5, 'c')], + [(1, 'b'), (2, 'e'), (3, 'd'), (4, 'a'), (5, 'c')], + [(1, 'b'), (2, 'e'), (3, 'd'), (4, 'a'), (5, 'c')], + [(1, 'b'), (2, 'e'), (3, 'd'), (4, 'a'), (5, 'c')], + [(1, 'b'), (2, 'e'), (3, 'd'), (4, 'a'), (5, 'c')], + [(1, 'b'), (2, 'e'), (3, 'd'), (4, 'a'), (5, 'c')], # 8 + [(1, 'c'), (2, 'a'), (3, 'b'), (4, 'e'), (5, 'd')], + [(1, 'c'), (2, 'a'), (3, 'b'), (4, 'e'), (5, 'd')], # 20 + [(1, 'c'), (2, 'a'), (3, 'b'), (4, 'e'), (5, 'd')], # 3 + [(1, 'c'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'e')], + [(1, 'c'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'e')], # 2 + [(1, 'd'), (2, 'c'), (3, 'e'), (4, 'b'), (5, 'a')], + [(1, 'd'), (2, 'c'), (3, 'e'), (4, 'b'), (5, 'a')], + [(1, 'd'), (2, 'c'), (3, 'e'), (4, 'b'), (5, 'a')], + [(1, 'd'), (2, 'c'), (3, 'e'), (4, 'b'), (5, 'a')], + [(1, 'd'), (2, 'c'), (3, 'e'), (4, 'b'), (5, 'a')], + [(1, 'd'), (2, 'c'), (3, 'e'), (4, 'b'), (5, 'a')], + [(1, 'd'), (2, 'c'), (3, 'e'), (4, 'b'), (5, 'a')], # 30 + [(1, 'c'), (2, 'a'), (3, 'e'), (4, 'b'), (5, 'd')], + [(1, 'c'), (2, 'a'), (3, 'e'), (4, 'b'), (5, 'd')], + [(1, 'c'), (2, 'a'), (3, 'e'), (4, 'b'), (5, 'd')], + [(1, 'c'), (2, 'a'), (3, 'e'), (4, 'b'), (5, 'd')], + [(1, 'c'), (2, 'a'), (3, 'e'), (4, 'b'), (5, 'd')], + [(1, 'c'), (2, 'a'), (3, 'e'), (4, 'b'), (5, 'd')], + [(1, 'c'), (2, 'a'), (3, 'e'), (4, 'b'), (5, 'd')], # 7 + [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], + [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], + [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], # 40 + [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], + [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], + [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], + [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')], + [(1, 'e'), (2, 'b'), (3, 'a'), (4, 'd'), (5, 'c')] # 8 + ] + + # Randomize the votes + random.shuffle(votes) + + # Create directed graph + preference = schulze.rank_votes(votes, candidates) + + # Get the strongest paths of each candidate + strongest_paths = schulze.compute_strongest_paths(preference, candidates) + + # Get final, ordered, results + results = schulze.get_ordered_voting_results(strongest_paths) + + self.assertEqual(", ".join(results.keys()), "e, a, c, b, d, x") + + def test_3_candidates_3_votes_cyclical(self): + + candidates = ['x', 'y', 'z'] + votes = [ + [(1, 'x'), (2, 'y'), (3, 'z')], + [(1, 'y'), (2, 'z'), (3, 'x')], + [(1, 'z'), (2, 'x'), (3, 'y')] + ] + + # Randomize the votes + random.shuffle(votes) + + # Create directed graph + preference = schulze.rank_votes(votes, candidates) + + # Get the strongest paths of each candidate + strongest_paths = schulze.compute_strongest_paths(preference, candidates) + + # Get final, ordered, results + results = schulze.get_ordered_voting_results(strongest_paths) + + # We should only get 3 results, even if this is cyclical tied vote. + self.assertEqual(len(results), 3) + + # All path strengths should be equal, this is a tied vote! + for sp in strongest_paths.values(): + self.assertEqual(sum(sp.values()), 4) + + +class SchulzeStatisticalTest(TestCase): + def generate_vote(self, candidate_chances): + """ + returns a list of vote choices, votes are picked based on each candidate's chances of being voted for. + [(1, 'candidate_id'), (2, 'candidate_id'), (2, 'candidate_id'), ...] + Votes are generated by determining how many candidates a voter wants to pick. + At least one pass is made through the candidate_chance list (possibly picking more candidates than chosen but never + more than are available) + for each candidate, it is randomly determined if they will be picked that round. Every candidate picked in + each round will have the same rank number. + + For example; candidate chances are: + [(0.3103760707038792, 1), (0.3368433989455909, 0), (0.40308497270067967, 4), (0.6070980766930767, 2), (0.7710239099894114, 3)] + Voter is determined to pick 3 candidates. + In the first round each candidate is checked + 0.3103760707038792 rolls 0.1 and is picked at rank 1 (first round) - 2 remaining picks + 0.3368433989455909 rolls 0.9 and is not picked + 0.40308497270067967 rolls 0.3 and is picked at rank 1 - 1 remaining pick + 0.6070980766930767 rolls a 0.5 and is picked at rank 1 - 0 remaining picks but not all candidates have been checked + 0.7710239099894114 rolls a 0.6 and is picked at rank 1 + Vote returned is [(1, 1), (1, 4), (1, 2), (1,3)] + """ + # vote for at least 1 + nr_to_vote_for = random.randint(1, len(candidate_chances)) + votes = [] + + rank = 1 + while nr_to_vote_for > 0: + for c in candidate_chances: + if random.random() < c[0]: + votes.append((rank, c[1])) + nr_to_vote_for -= 1 + if rank > 1 and nr_to_vote_for == 0: + break + rank += 1 + return votes + + def test_statistical(self, nr_candidates=10, nr_voters=1000): + """ + Runs a statistical test of Shulze with nr_candidates and nr_voters. + Each voter generates a set of choices (a vote) based on the chances of each candidate's + randomly determined chance of winning. + """ + candidates = list(range(nr_candidates)) + candidate_chances = [(random.random() / 2, c) for c in candidates] + candidate_chances.sort() + candidate_chances.reverse() + + voters = list(range(nr_voters)) + votes = [self.generate_vote(candidate_chances) for _ in voters] + + # Create directed graph + preference = schulze.rank_votes(votes, candidates) + + # Get the strongest paths of each candidate + strongest_paths = schulze.compute_strongest_paths(preference, candidates) + + # Get final, ordered, results + results = schulze.get_ordered_voting_results(strongest_paths) + + # Assert every candidate is within 1 seats of expected statistical outcome + for place, candidate in enumerate(candidate_chances): + expected_place = candidate_chances.index(candidate) + self.assertIn( + place, + (expected_place, expected_place + 1, expected_place - 1) + ) + + print("candidate %s had %s%% chance of winning" % ( + candidate[1], candidate[0])) + others = (0 if results[candidate[1]] is None + else len(results[candidate[1]])) + print("candidate was ranked above %s other candidates" % (others)) + +""" +Example results from 50 candidates and 300,000 voters: +statistical_test(50, 300000) + +candidate 26 had 0.483200948654 % chance of winning. +candidate was ranked above 49 other candidates +candidate 39 had 0.464021295542 % chance of winning. +candidate was ranked above 48 other candidates +candidate 15 had 0.431485774128 % chance of winning. +candidate was ranked above 47 other candidates +candidate 25 had 0.420753903532 % chance of winning. +candidate was ranked above 46 other candidates +candidate 42 had 0.419197158316 % chance of winning. +candidate was ranked above 44 other candidates +candidate 35 had 0.418901105954 % chance of winning. +candidate was ranked above 45 other candidates +candidate 13 had 0.401065556152 % chance of winning. +candidate was ranked above 43 other candidates +candidate 41 had 0.384035455262 % chance of winning. +candidate was ranked above 42 other candidates +candidate 7 had 0.380049427563 % chance of winning. +candidate was ranked above 41 other candidates +candidate 0 had 0.377967795101 % chance of winning. +candidate was ranked above 40 other candidates +candidate 34 had 0.371907686176 % chance of winning. +candidate was ranked above 39 other candidates +candidate 1 had 0.357778586501 % chance of winning. +candidate was ranked above 38 other candidates +candidate 14 had 0.34852486036 % chance of winning. +candidate was ranked above 37 other candidates +candidate 5 had 0.330212621555 % chance of winning. +candidate was ranked above 35 other candidates +candidate 37 had 0.3287223589 % chance of winning. +candidate was ranked above 36 other candidates +candidate 11 had 0.327884416543 % chance of winning. +candidate was ranked above 34 other candidates +candidate 29 had 0.325254931929 % chance of winning. +candidate was ranked above 33 other candidates +candidate 10 had 0.321070570805 % chance of winning. +candidate was ranked above 32 other candidates +candidate 31 had 0.31596922035 % chance of winning. +candidate was ranked above 31 other candidates +candidate 33 had 0.297191015742 % chance of winning. +candidate was ranked above 30 other candidates +candidate 17 had 0.287581522868 % chance of winning. +candidate was ranked above 29 other candidates +candidate 16 had 0.274060664402 % chance of winning. +candidate was ranked above 28 other candidates +candidate 44 had 0.258455488971 % chance of winning. +candidate was ranked above 27 other candidates +candidate 22 had 0.24412891209 % chance of winning. +candidate was ranked above 26 other candidates +candidate 8 had 0.237682466202 % chance of winning. +candidate was ranked above 25 other candidates +candidate 46 had 0.221566298556 % chance of winning. +candidate was ranked above 24 other candidates +candidate 30 had 0.219717021731 % chance of winning. +candidate was ranked above 22 other candidates +candidate 38 had 0.219333290245 % chance of winning. +candidate was ranked above 23 other candidates +candidate 4 had 0.216652418888 % chance of winning. +candidate was ranked above 21 other candidates +candidate 2 had 0.208006878111 % chance of winning. +candidate was ranked above 20 other candidates +candidate 19 had 0.171597247883 % chance of winning. +candidate was ranked above 19 other candidates +candidate 49 had 0.157472077345 % chance of winning. +candidate was ranked above 18 other candidates +candidate 28 had 0.149149256926 % chance of winning. +candidate was ranked above 17 other candidates +candidate 36 had 0.146031626084 % chance of winning. +candidate was ranked above 15 other candidates +candidate 3 had 0.143387827992 % chance of winning. +candidate was ranked above 16 other candidates +candidate 47 had 0.13546630016 % chance of winning. +candidate was ranked above 14 other candidates +candidate 45 had 0.124017754194 % chance of winning. +candidate was ranked above 13 other candidates +candidate 21 had 0.112630055751 % chance of winning. +candidate was ranked above 12 other candidates +candidate 18 had 0.102281073478 % chance of winning. +candidate was ranked above 11 other candidates +candidate 23 had 0.093419412524 % chance of winning. +candidate was ranked above 10 other candidates +candidate 20 had 0.0918015096321 % chance of winning. +candidate was ranked above 9 other candidates +candidate 12 had 0.0603144521346 % chance of winning. +candidate was ranked above 8 other candidates +candidate 48 had 0.0584980606429 % chance of winning. +candidate was ranked above 7 other candidates +candidate 6 had 0.0567705554887 % chance of winning. +candidate was ranked above 6 other candidates +candidate 27 had 0.0542038315661 % chance of winning. +candidate was ranked above 4 other candidates +candidate 43 had 0.0540082976788 % chance of winning. +candidate was ranked above 5 other candidates +candidate 40 had 0.0487528996791 % chance of winning. +candidate was ranked above 3 other candidates +candidate 32 had 0.0428817696832 % chance of winning. +candidate was ranked above 2 other candidates +candidate 24 had 0.0235691852838 % chance of winning. +candidate was ranked above 1 other candidates +candidate 9 had 0.0073776195401 % chance of winning. +candidate was ranked above 0 other candidates +[Finished in 37.9s] +""" diff --git a/election/tests.py b/election/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/election/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/election/urls.py b/election/urls.py new file mode 100644 index 00000000..c4ad6be8 --- /dev/null +++ b/election/urls.py @@ -0,0 +1,29 @@ +from django.conf.urls import url +from django.contrib.auth.decorators import login_required +from django.views.decorators.cache import never_cache +from django.views.decorators.vary import vary_on_headers + +from election.dataviews import election_candidacy +from election.dataviews import election_candidates_details +from election.dataviews import election_poll +from election.dataviews import election_vote +from election.dataviews import election_showclosed +from election.dataviews import election_stats_download +from election.views import election_add_edit +from election.views import election_list +from election.views import election_view + + +urlpatterns = [ + url(r'^polity/(?P\d+)/elections/$', never_cache(election_list), name='elections'), + url(r'^polity/(?P\d+)/election/new/$', election_add_edit, name='election_add_edit'), + url(r'^polity/(?P\d+)/election/(?P\d+)/edit/$', election_add_edit, name='election_add_edit'), + url(r'^polity/(?P\d+)/election/(?P\d+)/candidates-details/$', never_cache(election_candidates_details), name='election_candidates_details'), + url(r'^polity/(?P\d+)/election/(?P\d+)/$', never_cache(election_view), name='election'), + url(r'^polity/(?P\d+)/election/(?P\d+)/stats-dl/(?P.+)$', election_stats_download), + + url(r'^api/election/poll/$', never_cache(election_poll)), + url(r'^api/election/vote/$', never_cache(election_vote)), + url(r'^api/election/candidacy/$', never_cache(election_candidacy)), + url(r'^api/election/showclosed/$', election_showclosed), +] diff --git a/election/utils.py b/election/utils.py new file mode 100644 index 00000000..7d6da95e --- /dev/null +++ b/election/utils.py @@ -0,0 +1,637 @@ +import datetime +import copy +import json +import logging +import random +import sys +from collections import OrderedDict + +if __name__ != "__main__": + from django.utils.translation import ugettext_lazy as _ +else: + _ = lambda x: x + +from py3votecore.schulze_method import SchulzeMethod as Condorcet +from py3votecore.schulze_npr import SchulzeNPR as Schulze +from py3votecore.schulze_stv import SchulzeSTV +from py3votecore.stv import STV + +# This is the old custom Schulze code. For now we continue to use it by +# default, silently comparing with results from pyvotecore (canarying). +from election import schulze + +logger = logging.getLogger(__name__) + + +class BallotContainer(object): + """ + A container for ballots. + + Includes convenience methods for loading/saving ballots, saving + or restoring internal state during analysis, and returning the list + of ballots in a few different formats. + """ + def __init__(self, ballots=None): + """ + Ballots should be a list of lists of (rank, candidate) tuples. + """ + self.ballots = ballots or [] + self.excluded = set([]) + self.candidates = self.get_candidates() + self.collapse_gaps = True + self.states = [] + + def copy_state(self): + """Return a copy of the internal state, so we can restore later.""" + return (copy.deepcopy(self.ballots), copy.deepcopy(self.excluded)) + + def restore_state(self, state): + self.ballots = copy.deepcopy(state[0]) + self.excluded = copy.deepcopy(state[1]) + self.candidates = self.get_candidates() + + def push_state(self): + self.states.append(self.copy_state()) + return self + + def pop_state(self): + self.restore_state(self.states.pop(-1)) + return self + + def __enter__(self): + return self.push_state() + + def __exit__(self, *args, **kwargs): + return self.pop_state() + + def load_ballots(self, filename): + """Load ballots from disk""" + with open(filename, 'r') as fd: + self.ballots += json.load(fd, encoding='utf-8') + self.candidates = self.get_candidates() + return self + + def save_ballots(self, filename): + """Save the ballots to disk. + + Ballots are shuffled before saving, to further anonymize the data + (user IDs will already have been stripped). + """ + unicode_ballots = [] + for ballot in self.ballots: + unicode_ballots.append( + [(rank, str(cand)) for rank, cand in ballot]) + random.shuffle(unicode_ballots) + if filename == '-': + json.dump(unicode_ballots, sys.stdout, indent=1) + else: + with open(filename, 'w') as fd: + json.dump(unicode_ballots, fd, indent=1) + + def get_candidates(self): + candidates = {} + for ballot in self.ballots: + for rank, candidate in ballot: + if candidate not in self.excluded: + candidates[candidate] = 1 + return candidates.keys() + + def exclude_candidates(self, excluded): + self.excluded |= set(excluded) + self.candidates = self.get_candidates() + return self + + def ballots_as_lists(self): + for ballot in self.ballots: + as_list = [candidate for rank, candidate in sorted(ballot) + if candidate not in self.excluded] + if as_list: + yield(as_list) + + def ballots_as_rankings(self): + b = self.ballots_as_lists() if (self.collapse_gaps) else self.ballots + for ballot in b: + rankings = {} + ranked = enumerate(ballot) if (self.collapse_gaps) else ballot + for rank, candidate in ranked: + if candidate not in self.excluded: + rankings[candidate] = rank + yield(rankings) + + def hashes_with_counts(self, ballots): + hashes = {} + for ballot in ballots: + bkey = repr(ballot) + if bkey in hashes: + hashes[bkey]["count"] += 1 + else: + hashes[bkey] = {"count": 1, "ballot": ballot} + return hashes.values() + + +class BallotAnalyzer(BallotContainer): + """ + This class will analyze and return statistics about a set of ballots. + """ + def _cands_and_stats(self): + cands = sorted(self.get_candidates()) + return (cands, OrderedDict([ + ('ballots', len(self.ballots)), + ('candidates', cands) + ])) + + def get_ballot_stats(self): + cands, stats = self._cands_and_stats() + lengths = {} + for ballot in self.ballots: + l = len(ballot) + lengths[l] = lengths.get(l, 0) + 1 + stats['ballot_lengths'] = lengths + stats['ballot_length_average'] = float(sum( + (k * v) for k, v in iter(lengths.items()))) / len(self.ballots) + + def ls(l): + return { + "length": l, + "count": lengths[l], + "pct": float(100 * lengths[l]) / len(self.ballots)} + stats['ballot_length_most_common'] = ls(max( + (v, k) for k, v in iter(lengths.items()))[1]) + stats['ballot_length_longest'] = ls(max(lengths.keys())) + stats['ballot_length_shortest'] = ls(min(lengths.keys())) + + return stats + + def reranked_ballot(self, ballot): + """Guarantee ballot rankings are sequential""" + rank = 0 + fixed = [] + for br, candidate in sorted(ballot): + fixed.append((rank, candidate)) + rank += 1 + return fixed + + def get_candidate_rank_stats(self): + cands, stats = self._cands_and_stats() + ranks = [[0 for r in cands] for c in cands] + stats['ranking_matrix'] = ranks + for ballot in self.ballots: + for rank, candidate in self.reranked_ballot(ballot): + if candidate in cands: + ranks[cands.index(candidate)][int(rank)] += 1 + for ranking in ranks: + r = sum(ranking) + ranking.append(r) + return stats + + def get_candidate_pairwise_stats(self): + cands, stats = self._cands_and_stats() + cmatrix = [[0 for c2 in cands] for c1 in cands] + stats['pairwise_matrix'] = cmatrix + for ranking in self.ballots_as_rankings(): + for i, c1 in enumerate(cands): + for j, c2 in enumerate(cands): + if ranking.get(c1, 9999999) < ranking.get(c2, 9999999): + cmatrix[i][j] += 1 + return stats + + def get_duplicate_ballots(self, threshold=None, threshold_pct=None): + if threshold_pct: + threshold = float(threshold_pct) * len(self.ballots) / 100 + elif not threshold: + threshold = 2 + + cands, stats = self._cands_and_stats() + stats['duplicate_threshold'] = threshold + stats['duplicates'] = [] + for cbhash in self.hashes_with_counts(self.ballots_as_lists()): + if cbhash.get("count", 1) >= threshold: + stats['duplicates'].append(cbhash) + stats['duplicates'].sort(key=lambda cbh: -cbh["count"]) + return stats + + @classmethod + def exclude_candidate_stats(self, stats, excluded): + ltd = copy.deepcopy(stats) + + if ltd.get('ranking_matrix'): + rm = ltd['ranking_matrix'] + for i, c in enumerate(ltd['candidates']): + if c in excluded: + rm[i] = ['' for c in rm[i]] + + if ltd.get('pairwise_matrix'): + rm = ltd['pairwise_matrix'] + for i, c in enumerate(ltd['candidates']): + if c in excluded: + rm[i] = ['' for c in rm[i]] + for l in rm: + l[i] = '' + + return ltd + + @classmethod + def stats_as_text(self, stats): + lines = [ + '
' % (
+                datetime.datetime.now()),
+            '',
+            'Analyzed %d ballots with %d candidates.' % (
+                stats['ballots'], len(stats['candidates']))]
+
+        if 'ballot_lengths' in stats:
+            lines += ['', 'Ballots:',
+                ('   - Average ballot length: %.2f'
+                     ) % stats['ballot_length_average'],
+                ('   - Shortest ballot length: %(length)d (%(count)d '
+                        'ballots=%(pct)d%%)') % stats['ballot_length_shortest'],
+                ('   - Most common ballot length: %(length)d (%(count)d '
+                        'ballots=%(pct)d%%)') % stats['ballot_length_most_common'],
+                ('   - Longest ballot length: %(length)d (%(count)d '
+                        'ballots=%(pct)d%%)') % stats['ballot_length_longest'],
+                '   - L/B: [%s]' % (' '.join(
+                    '%d/%d' % (k, stats['ballot_lengths'][k])
+                        for k in sorted(stats['ballot_lengths'].keys())))]
+
+        if stats.get('duplicates'):
+            lines += ['',
+                'Frequent ballots: (>= %d occurrances, %d%%)' % (
+                    stats['duplicate_threshold'],
+                    (100 * stats['duplicate_threshold']) / stats['ballots'])]
+            for dup in stats['duplicates']:
+                lines += ['   - %(count)d times: %(ballot)s' % dup]
+
+        if stats.get('ranking_matrix'):
+            rm = stats['ranking_matrix']
+            lines += ['',
+                'Rankings:',
+                ' %16.16s  %s ANY' % ('CANDIDATE', ' '.join(
+                    '%3.3s' % (i+1) for i in range(0, len(rm[0])-1)))]
+            rls = []
+            for i, candidate in enumerate(stats['candidates']):
+                rls += [' %16.16s  %s' % (candidate, ' '.join(
+                    '%3.3s' % v for v in rm[i]))]
+
+            def safe_int(i):
+                try:
+                    return int(i)
+                except ValueError:
+                    return 0
+            rls.sort(key=lambda l: -safe_int(l.strip().split()[-1]))
+            lines.extend(rls)
+
+        if stats.get('pairwise_matrix'):
+            lines += ['',
+                'Pairwise victories:',
+                ' %16.16s  %s' % ('WINNER', ' '.join(
+                    '%3.3s' % c for c in stats['candidates']))]
+            for i, candidate in enumerate(stats['candidates']):
+                lines += [' %16.16s  %s' % (candidate, ' '.join(
+                    '%3.3s' % v for v in stats['pairwise_matrix'][i]))]
+
+        lines += ['', '%s
' % (' ' * 60,)] + return '\n'.join(lines) + + @classmethod + def stats_as_spreadsheet(self, fmt, stats): + pages = OrderedDict() + count = stats['ballots'] + + if 'ballot_lengths' in stats: + bl = stats['ballot_lengths'] + bls = stats['ballot_length_shortest'] + bll = stats['ballot_length_longest'] + blmc = stats['ballot_length_most_common'] + pages['Ballots'] = [ + ['Ballots', count], + [''], + ['', 'Length', 'Ballots', '%'], + ['Shortest', bls['length'], bls['count'], bls['pct']], + ['Longest', bll['length'], bll['count'], bll['pct']], + ['Average', stats['ballot_length_average'], '', ''], + ['Most common', blmc['length'], blmc['count'], blmc['pct']], + [''], + ['Ballot length', 'Ballots'] + ] + sorted([ + [l, stats['ballot_lengths'][l]] + for l in stats['ballot_lengths']]) + + if stats.get('duplicates'): + pages['Duplicates'] = page = [ + ['Frequent ballots: (>= %d occurrances, %d%%)' % ( + stats['duplicate_threshold'], + (100 * stats['duplicate_threshold']) / stats['ballots'])], + [''], + ['Count', 'Ballot ...']] + for dup in stats['duplicates']: + page += [[dup["count"]] + dup['ballot']] + + if stats.get('ranking_matrix'): + rm = stats['ranking_matrix'] + pages['Rankings'] = page = [ + ['CANDIDATE'] + + [(i+1) for i in range(0, len(rm[0])-1)] + + ['ANY']] + rls = [] + for i, candidate in enumerate(stats['candidates']): + rls.append([candidate] + rm[i]) + rls.sort(key=lambda l: -(l[-1] or 0)) + page.extend(rls) + + if stats.get('pairwise_matrix'): + pages['Pairwise Victories'] = page = [ + ['WINNER'] + stats['candidates']] + for i, candidate in enumerate(stats['candidates']): + page.append([candidate] + stats['pairwise_matrix'][i]) + + import pyexcel + import StringIO + buf = StringIO.StringIO() + pyexcel.Book(sheets=pages).save_to_memory(fmt, stream=buf) + return buf.getvalue() + + +class BallotCounter(BallotAnalyzer): + """ + This class contains the results of an election, making it easy to + tally up the results using a few different methods. + """ + + # Voting systems that don't work after Python 3 / Django 2 upgrade, are + # commented out. In fact, they should be considered for removal. + VOTING_SYSTEMS = ( + ('condorcet', _('Condorcet')), + ('schulze', _('Schulze, ordered list')), + #('schulze_old', _('Schulze, ordered list (old)')), + #('schulze_new', _('Schulze, ordered list (new)')), + #('schulze_both', _('Schulze, ordered list (both)')), + #('stcom', _('Steering Committee Election')), + ('stv1', _('STV, single winner')), + ('stv2', _('STV, two winners')), + ('stv3', _('STV, three winners')), + ('stv4', _('STV, four winners')), + ('stv5', _('STV, five winners')), + ('stv6', _('STV, six winners')), + ('stv8', _('STV, eight winners')), + ('stv10', _('STV, ten winners')), + #('stonethor', _('STV partition with Schulze ranking')) + ) + + def system_name(self, system): + return [n for m, n in self.VOTING_SYSTEMS if m == system][0] + + def schulze_results_old(self): + candidates = self.candidates + preference = schulze.rank_votes(self.ballots, candidates) + strongest_paths = schulze.compute_strongest_paths(preference, candidates) + ordered_candidates = schulze.get_ordered_voting_results(strongest_paths) + return [cand for cand in ordered_candidates] + + def schulze_results_new(self, winners=None): + if winners is None: + if self.ballots: + winners = max(len(b) for b in self.ballots) + else: + winners = 1 + return Schulze( + list(self.hashes_with_counts(self.ballots_as_rankings())), + winner_threshold=min(winners, len(self.candidates)), + ballot_notation=Schulze.BALLOT_NOTATION_RANKING, + ).as_dict()['order'] + + def schulze_results_both(self, winners=None): + """Wrapper to canary new schulze code, comparing with old""" + if self.excluded: + logger.warning('Schulze old cannot exclude, using new only.') + return self.schulze_results_new(winners=winners) + + old_style = self.schulze_results_old() + new_style = self.schulze_results_new(winners=winners) + if old_style != new_style: + logger.warning('Schulze old result does not match schulze new!') + else: + logger.info('Schulze old and new match, hooray.') + return old_style + + def schulze_stv_results(self, winners=None): + if winners is None: + winners = 1 + return sorted(list(SchulzeSTV( + list(self.hashes_with_counts(self.ballots_as_rankings())), + required_winners=min(winners, len(self.candidates)), + ballot_notation=Schulze.BALLOT_NOTATION_RANKING, + ).as_dict()['winners'])) + + def stv_results(self, winners=None): + # FIXME: Variable names here may be somewhat confusing. + # - winners: The count of required winners. + # - winnerset: The set of winners returned. + if winners is None: + winners = 1 + + winnerset = [] + + mid_result = STV(list(self.hashes_with_counts(self.ballots_as_lists())), required_winners=winners).as_dict() + winners_found = 0 + for mid_round in mid_result['rounds']: + if not 'winners' in mid_round: + continue + + found_this_time = len(mid_round['winners']) + if winners_found + found_this_time > winners: + winnerset.extend(random.sample(mid_round['winners'], winners - winners_found)) + else: + winnerset.extend(mid_round['winners']) + + winners_found += found_this_time + + if len(winnerset) < winners: + return mid_result['winners'] + + return winnerset + + def condorcet_results(self): + result = Condorcet( + list(self.hashes_with_counts(self.ballots_as_rankings())), + ballot_notation=Schulze.BALLOT_NOTATION_RANKING, + ).as_dict() + if not result.get('tied_winners'): + return [result['winner']] + else: + return [] + + def stcom_results(self, winners=None): + """Icelandic Pirate party steering committee elections. + + Returns 10 or 11 members; the first five are the steering committee, + the next five are the deputies (both selected using STV). The 11th + is the condorcet winner, if there is one. Note that the 11th result + will be a duplicate. + """ + result = self.schulze_results_new(winners=10) + stcom = result[:5] + deputies = result[5:] + condorcet = self.condorcet_results() + return sorted(stcom) + sorted(deputies) + condorcet + + def stonethor_results(self, partition=None, winners=None): + """Experimental combined STV and Schulze method. + + Partition the candidate group using STV and then rank each + partition separately using Schulze. The default partition is + one quarter of the candidate count. + """ + top = self.stv_results(winners=min( + partition or (len(self.candidates) / 4), + len(self.candidates))) + bottom = list(set(self.get_candidates()) - set(top)) + if top: + with self: + top = self.exclude_candidates(bottom).schulze_results_new() + if bottom and len(top) < winners: + with self: + bottom = self.exclude_candidates(top).schulze_results_new() + return (top + bottom)[:winners] + + def results(self, method, winners=None, sysarg=None): + assert(method in [system for system, name in self.VOTING_SYSTEMS]) + + if method == 'schulze': + return self.schulze_results_new(winners=(winners or sysarg)) + + elif method == 'schulze_old': + return self.schulze_results_old() + + elif method == 'schulze_both': + return self.schulze_results_both(winners=(winners or sysarg)) + + elif method == 'condorcet': + return self.condorcet_results() + + elif method == 'stcom': + return self.stcom_results() + + elif method.startswith('stv'): + return self.stv_results(winners=int(method[3:] or 1)) + + elif method == 'stonethor': + return self.stonethor_results(winners=winners, partition=sysarg) + + else: + raise Exception('Invalid voting method: %s' % method) + + def constrained_results(self, method, winners=None, below=None): + results = self.results(method, winners=winners) + if not below: + return results + + constrained = ['' for i in range(0, len(results) * 2)] + for candidate in results: + position = max(0, below.get(candidate, 1) - 1) + for p in range(position, len(constrained)): + if not constrained[p]: + constrained[p] = candidate + break + + return [c for c in constrained if c] + + +if __name__ == "__main__": + import sys + import argparse + + # Configure logging + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + + ap = argparse.ArgumentParser() + ap.add_argument('-e', '--exclude', action='append', + help="Candidate(s) to exclude when counting") + ap.add_argument('-b', '--below', action='append', + help="seat,candidate pairs, to constrain final ordering") + ap.add_argument('--keep-gaps', action='store_true', + help="Preserve gaps if ballots are not sequential") + ap.add_argument('operation', + help="Operation to perform (count)") + ap.add_argument('system', + help="Counting system to use (schulze, stv5, ...)") + ap.add_argument('filenames', nargs='+', + help="Ballot files to read") + args = ap.parse_args() + + system = args.system + if ':' in system: + system, sysarg = system.split(':') + sysarg = int(sysarg) + else: + sysarg = None + + bc = BallotCounter() + for fn in args.filenames: + bc.load_ballots(fn) + + bc.collapse_gaps = False if (args.keep_gaps) else True + + below = {} + for sc in args.below or []: + seat, candidate = sc.split(',') + seat = int(seat) + below[candidate] = seat + + if args.operation == 'count': + if args.exclude: + bc.exclude_candidates(args.exclude) + + print('Voting system:\n\t%s (%s)' % (bc.system_name(system), system)) + print('') + print('Loaded %d ballots from:\n\t%s' % ( + len(bc.ballots), '\n\t'.join(args.filenames))) + print('') + if below: + print(('Results(C):\n\t%s' % ', '.join(bc.constrained_results( + system, sysarg=sysarg, below=below))).encode('utf-8')) + else: + print(('Results:\n\t%s' % ', '.join(bc.results( + system, sysarg=sysarg))).encode('utf-8')) + print('') + + elif args.operation in ( + 'analyze', 'analyze:json', 'analyze:ods', 'analyze:xlsx'): + + stats = OrderedDict() + if system == 'all': + system = 'ballots,duplicates,rankings,pairs' + for method in (m.strip() for m in system.lower().split(',')): + if method == 'rankings': + stats.update(bc.get_candidate_rank_stats()) + + elif method == 'pairs': + stats.update(bc.get_candidate_pairwise_stats()) + + elif method == 'duplicates': + stats.update(bc.get_duplicate_ballots(threshold_pct=5)) + + elif method == 'ballots': + stats.update(bc.get_ballot_stats()) + + else: + raise ValueError('Unknown analysis: %s' % method) + + if args.exclude: + stats = bc.exclude_candidate_stats(stats, args.exclude) + + if args.operation == 'analyze': + print(bc.stats_as_text(stats).encode('utf-8')) + + elif args.operation == 'analyze:json': + json.dump(stats, sys.stdout, indent=1) + + elif args.operation in ('analyze:ods', 'analyze:xlsx'): + sys.stdout.write(bc.stats_as_spreadsheet( + args.operation.split(':')[1], stats)) + + else: + raise ValueError('Unknown operation: %s' % args.operation) +else: + # Suppress errors in case logging isn't configured elsewhere + logger.addHandler(logging.NullHandler()) diff --git a/election/views.py b/election/views.py new file mode 100644 index 00000000..ef7ec13b --- /dev/null +++ b/election/views.py @@ -0,0 +1,120 @@ +from datetime import datetime +from datetime import timedelta + +from django.conf import settings +from django.db.models import Count +from django.contrib.auth.decorators import login_required +from django.contrib.auth.views import redirect_to_login +from django.core.exceptions import PermissionDenied +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render +from django.urls import reverse + +from election.forms import ElectionForm +from election.models import Election + +from polity.models import Polity + + +@login_required +def election_add_edit(request, polity_id, election_id=None): + try: + polity = Polity.objects.get(id=polity_id, officers=request.user) + except Polity.DoesNotExist: + raise PermissionDenied() + + if election_id: + election = get_object_or_404(Election, id=election_id, polity=polity) + + # We don't want to edit anything that has already been processed. + if election.is_processed: + raise PermissionDenied() + else: + election = Election(polity=polity) + + if request.method == 'POST': + form = ElectionForm(request.POST, instance=election) + if form.is_valid(): + election = form.save() + return redirect(reverse('election', args=(polity_id, election.id))) + else: + form = ElectionForm(instance=election) + + ctx = { + 'polity': polity, + 'election': election, + 'form': form, + } + return render(request, 'election/election_add_edit.html', ctx) + + +def election_view(request, polity_id, election_id): + polity = get_object_or_404(Polity, id=polity_id) + election = get_object_or_404(Election, polity=polity, id=election_id) + + # People may want to run for an office today that they don't want search + # engines or third parties (potential employers, for example) knowing + # about in the future. Still, we'd like to retain some history of their + # candidacy. To try and attain both goals, we require a login for older + # elections. + election_protection_timing = datetime.now() - timedelta(days=settings.RECENT_ISSUE_DAYS) + if not request.user.is_authenticated and election.deadline_votes < election_protection_timing: + return redirect_to_login(request.path) + + voting_interface_enabled = election.election_state() == 'voting' and election.can_vote(request.user) + + if election.is_processed: + ordered_candidates = election.get_winners() + vote_count = election.result.vote_count + statistics = election.get_stats(user=request.user) + users = [c.user for c in ordered_candidates] + if request.user in users: + user_result = users.index(request.user) + 1 + else: + user_result = None + else: + # Returning nothing! Some voting systems are too slow for us to + # calculate results on the fly. + ordered_candidates = [] + vote_count = election.get_vote_count + statistics = None + user_result = None + + ctx = { + 'polity': polity, + 'election': election, + 'step': request.GET.get('step', None), + 'now': datetime.now().strftime('%d/%m/%Y %H:%I'), + 'ordered_candidates': ordered_candidates, + 'statistics': statistics, + 'vote_count': vote_count, + 'voting_interface_enabled': voting_interface_enabled, + 'user_result': user_result, + 'can_vote': (request.user is not None and election.can_vote(request.user)), + 'can_run': (request.user is not None and election.can_be_candidate(request.user)) + } + if voting_interface_enabled: + ctx.update({ + 'started_voting': election.has_voted(request.user), + 'finished_voting': False + }) + return render(request, 'election/election_view.html', ctx) + + +def election_list(request, polity_id): + polity = get_object_or_404(Polity, id=polity_id) + + elections = Election.objects.filter( + polity=polity + ).annotate( + candidate_count=Count('candidate') + ).order_by( + '-deadline_votes' + ) + + ctx = { + 'polity': polity, + 'elections': elections, + } + return render(request, 'election/election_list.html', ctx) diff --git a/emailconfirmation/__init__.py b/emailconfirmation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/emailconfirmation/admin.py b/emailconfirmation/admin.py new file mode 100644 index 00000000..13be29d9 --- /dev/null +++ b/emailconfirmation/admin.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin + +# Register your models here. diff --git a/emailconfirmation/apps.py b/emailconfirmation/apps.py new file mode 100644 index 00000000..364aa01f --- /dev/null +++ b/emailconfirmation/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class EmailconfirmationConfig(AppConfig): + name = 'emailconfirmation' diff --git a/emailconfirmation/migrations/0001_initial.py b/emailconfirmation/migrations/0001_initial.py new file mode 100644 index 00000000..87f3e636 --- /dev/null +++ b/emailconfirmation/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-08-05 15:56 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='EmailConfirmation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=40)), + ('timing_created', models.DateTimeField(auto_now=True)), + ('action', models.CharField(choices=[('email_change', 'Email change')], max_length=30)), + ('data', models.CharField(max_length=100, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_confirmations', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/emailconfirmation/migrations/__init__.py b/emailconfirmation/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/emailconfirmation/models.py b/emailconfirmation/models.py new file mode 100644 index 00000000..1c599a34 --- /dev/null +++ b/emailconfirmation/models.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import hashlib +import string + +from core.django_mdmail import send_mail + +from django.conf import settings +from django.db import models +from django.db.models import CASCADE +from django.utils.crypto import get_random_string +from django.shortcuts import reverse +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ + + +class EmailConfirmation(models.Model): + + ACTIONS = ( + ('email_change', _('Email change')), + ) + + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='email_confirmations', on_delete=CASCADE) + + key = models.CharField(max_length=40) + timing_created = models.DateTimeField(auto_now=True) + + action = models.CharField(max_length=30, choices=ACTIONS) + data = models.CharField(max_length=100, null=True) + + def save(self, send=False, *args, **kwargs): + + if self.key == '': + # Automatically generate a unique key, but make sure that it isn't + # already in use. + key = self.generate_key(); + while EmailConfirmation.objects.filter(key=key).count() > 0: + key = self.generate_key() + self.key = key + + super(EmailConfirmation, self).save(*args, **kwargs) + + def generate_key(self): + random_string = get_random_string(length=32, allowed_chars=string.printable) + return hashlib.sha1(random_string.encode('utf-8')).hexdigest() + + def send(self, request): + action_msg = dict(self.ACTIONS)[self.action] + + subject = '%s%s' % (settings.EMAIL_SUBJECT_PREFIX, action_msg) + confirmation_url = request.build_absolute_uri(reverse('email_confirmation', args=(self.key,))) + email = self.data if self.action == 'email_change' else self.user.email + + ctx = { + 'action_msg': action_msg, + 'confirmation_url': confirmation_url, + } + body = render_to_string('emailconfirmation/emailconfirmation.md', ctx) + + send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [email]) diff --git a/emailconfirmation/tests.py b/emailconfirmation/tests.py new file mode 100644 index 00000000..5982e6bc --- /dev/null +++ b/emailconfirmation/tests.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.test import TestCase + +# Create your tests here. diff --git a/emailconfirmation/urls.py b/emailconfirmation/urls.py new file mode 100644 index 00000000..5f222b47 --- /dev/null +++ b/emailconfirmation/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from emailconfirmation.views import email_confirmation + +urlpatterns = [ + url(r'^email-confirmation/(?P[a-zA-Z0-9]{40})/$', email_confirmation, name='email_confirmation'), +] diff --git a/emailconfirmation/views.py b/emailconfirmation/views.py new file mode 100644 index 00000000..f3ee0918 --- /dev/null +++ b/emailconfirmation/views.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from datetime import timedelta + +from django.conf import settings +from django.shortcuts import redirect +from django.shortcuts import render +from django.utils import timezone +from django.utils.translation import ugettext as _ + +from emailconfirmation.models import EmailConfirmation + +from gateway.utils import update_member + +def email_confirmation(request, key): + try: + con = EmailConfirmation.objects.get(key=key) + except: + return redirect('/') + + return_url = '' + return_name = '' + + if con.action == 'email_change': + con.user.email = con.data + con.user.save() + + # Update IcePirate registry, if in use. + if settings.ICEPIRATE['url']: + update_member(con.user) + + action_detail = '%s: %s' % (_('Your new email address is'), con.data) + return_url = '/' + return_name = _('Main page') + + action_msg = dict(EmailConfirmation.ACTIONS)[con.action] + + # We'll want to remove all confirmations of the same type, from the same + # user. This is to ensure that previous confirmation links that might have + # been but not received for whatever reason, are made invalid. + EmailConfirmation.objects.filter(user=con.user, action=con.action).delete() + + # Clean up expired confirmation requests, since we're here. + EmailConfirmation.objects.filter(timing_created__lt=timezone.now() - timedelta(days=1)).delete() + + ctx = { + 'action_msg': action_msg, + 'action_detail': action_detail, + 'return_url': return_url, + 'return_name': return_name, + } + return render(request, 'emailconfirmation/confirmed.html', ctx) + diff --git a/env.example b/env.example new file mode 100644 index 00000000..3ea389e5 --- /dev/null +++ b/env.example @@ -0,0 +1,80 @@ +## Base configuration +W2_DEBUG=True +W2_ADMINS=username,user@example.com +W2_ALLOWED_HOSTS=localhost,localhost:8000 +W2_BALLOT_SAVEFILE_FORMAT='elections/ballots-%(voting_system)s-%(election_id)s.json' +W2_CONTACT_EMAIL='contact@example.com' + +## Security settings +W2_AUTO_LOGOUT_DELAY=30 +W2_SECRET_KEY='createASecretRondomKeyHere' + +## Instance identity +W2_INSTANCE_LOGO='' +W2_INSTANCE_NAME='Unconfigured Wasa2il' +W2_INSTANCE_SLUG='unconfiguredwasa2il' +W2_INSTANCE_URL='' +W2_ORGANIZATION_NAME='' +W2_ORGANIZATION_NEWS_URL='' + +## Overall instance rules +W2_AGE_LIMIT=16 +W2_ALLOW_LEAVE_POLITY=False +W2_RECENT_ELECTION_DAYS=7 +W2_RECENT_ISSUE_DAYS=7 + +## Database configuration +W2_DATABASE_ENGINE=django.db.backends.mysql +# W2_DATABASE_ENGINE=django.db.backends.postgresql_psycopg2 +W2_DATABASE_HOST=127.0.0.1 +W2_DATABASE_NAME=wasa2il +W2_DATABASE_PASSWORD=wasa2il +W2_DATABASE_PORT=3306 +# W2_DATABASE_PORT=5432 +W2_DATABASE_USER=wasa2il +W2_DATABASE_EXPORT_DB_NAME= + +## Locale settings +W2_DATETIME_FORMAT='d/m/Y H:i:s' +W2_DATETIME_FORMAT_DJANGO_WIDGET='dd/mm/yyyy hh:ii' # django-datetime-widget +W2_DATE_FORMAT='d/m/Y' +W2_TIME_ZONE=Iceland +W2_LANGUAGE_CODE=en-US # For example 'en-US', 'en', 'is' etc... + +## Email settings +W2_EMAIL_BACKEND='django.core.mail.backends.console.EmailBackend' +W2_SERVER_EMAIL='' +W2_DEFAULT_FROM_EMAIL='' +W2_EMAIL_HOST='mail.example.com' +W2_EMAIL_PORT=465 +W2_EMAIL_USE_TLS=True +W2_EMAIL_USE_SSL=False +W2_EMAIL_HOST_USER='user@example.com' +W2_EMAIL_HOST_PASSWORD='some-password' +W2_EMAIL_SUBJECT_PREFIX='[Organization Name] ' + +## Push notifications +W2_GCM_APP_ID= +W2_GCM_SENDER_ID= +W2_GCM_REST_API_KEY= + +## Facebook integration +W2_INSTANCE_FACEBOOK_APP_ID='' +W2_INSTANCE_FACEBOOK_IMAGE='https://example.com/full/url/to/image.png' + +## Discourse integration +W2_DISCOURSE_URL= +W2_DISCOURSE_SECRET= + +## IcePirate integration +W2_ICEPIRATE_URL='' +W2_ICEPIRATE_KEY='' + +## SAML (2.0) support +#W2_SAML_URL='https://example.com/saml-login-service/' +#W2_SAML_CERT='core/certs/example-full-ca-cert.pem' + +## Feature knobs - set to "1" to enable +W2_FEATURE_TASKS=1 +W2_FEATURE_TOPIC=1 +W2_FEATURE_PUSH_NOTIFICATIONS=1 diff --git a/gateway/__init__.py b/gateway/__init__.py new file mode 100644 index 00000000..18445ae4 --- /dev/null +++ b/gateway/__init__.py @@ -0,0 +1 @@ +default_app_config = 'gateway.apps.IcePirateGatewayConfig' diff --git a/gateway/apps.py b/gateway/apps.py new file mode 100644 index 00000000..fbe175aa --- /dev/null +++ b/gateway/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + +class IcePirateGatewayConfig(AppConfig): + name = 'gateway' + verbose_name = 'IcePirate Gateway' + + def ready(self): + import gateway.signals + diff --git a/gateway/exceptions.py b/gateway/exceptions.py new file mode 100644 index 00000000..d581e2aa --- /dev/null +++ b/gateway/exceptions.py @@ -0,0 +1,4 @@ +class IcePirateException(Exception): + def __init__(self, msg, sub_msg=None, *args, **kwargs): + self.sub_msg = sub_msg + super(IcePirateException, self).__init__(msg, *args, **kwargs) diff --git a/gateway/register.py b/gateway/register.py new file mode 100644 index 00000000..44943fe9 --- /dev/null +++ b/gateway/register.py @@ -0,0 +1,54 @@ +import hashlib +import time + +from django.conf import settings +from django.contrib.auth import logout +from registration.backends.simple.views import RegistrationView + + +class PreverifiedRegistrationView(RegistrationView): + """ + A registration backend which accepts e-mail addresses which have been + validated by icepirate and immediately creates a user. + """ + SIG_VALIDITY = 31 * 24 * 3600 + + def _make_email_sig(self, email, when=None): + ts = '%x/' % (when or time.time()) + key = settings.ICEPIRATE['key'] + return ts + hashlib.sha1( + '%s:%s%s:%s' % (key, ts, email, key)).hexdigest() + + def _email_sig_is_ok(self, email, email_sig): + try: + ts, sig = email_sig.split('/') + ts = int(ts, 16) + if time.time() - ts > self.SIG_VALIDITY: + return False + return email_sig == self._make_email_sig(email, when=ts) + except (AttributeError, ValueError, IndexError, KeyError): + return False + + def registration_allowed(self): + # Check if there is an email_sig variable that correctly signs + # the provided e-mail address. + + email = self.request.GET.get('email') + email2 = self.request.POST.get('email') + email_sig = self.request.GET.get('email_sig') + + if email2 is None and email_sig is None: + return True + elif (email2 in (None, email) + and self._email_sig_is_ok(email, email_sig)): + return RegistrationView.registration_allowed(self) + else: + return False + + def register(self, form): + new_user = RegistrationView.register(self, form) + logout(self.request) + return new_user + + def get_success_url(self, user=None): + return 'registration_activation_complete' diff --git a/gateway/signals.py b/gateway/signals.py new file mode 100644 index 00000000..c9b5b305 --- /dev/null +++ b/gateway/signals.py @@ -0,0 +1,80 @@ +from django.conf import settings +from django.contrib.auth.signals import user_logged_in +from django.dispatch import receiver + +from core.signals import user_verified + +from gateway.exceptions import IcePirateException +from gateway.utils import add_member +from gateway.utils import apply_member_locally +from gateway.utils import get_member +from gateway.utils import update_member +from gateway.utils import user_to_member_args + + +@receiver(user_logged_in) +def login_sync(sender, user, request, **kwargs): + ''' + When a user logs in, data is retrieved from the remote IcePirate + membership registry and the local user configured accordingly. + ''' + + # No need for this if IcePirate isn't being used. + if not settings.ICEPIRATE['url']: + return + + # No point hitting the API if we don't have an SSN. + if not user.userprofile.verified_ssn: + return + + try: + success, member, error = get_member(user.userprofile.verified_ssn) + + apply_member_locally(member, user) + + # If the email address of the user and IcePirate registry member + # doesn't match, we'll correct the IcePirate registry email address, + # since we know for a fact that the one on Wasa2il's side has been + # verified and that's where the user can change it. For the same + # reason, Wasa2il never updates the user's email address on its own + # end according to the IcePirate registry. + if member['email'] != user.email: + update_member(user) + except: + # If something went wrong, we'll be on the safe side of things and + # remove membership from polities until we have confirmation from + # IcePirate on which polities the user should have access to. + user.polities.clear() + user.officers.clear() + + +@receiver(user_verified) +def verified_sync(sender, user, request, **kwargs): + + # No need for this if IcePirate isn't being used. + if not settings.ICEPIRATE['url']: + return + + try: + success, member, error = get_member(user.userprofile.verified_ssn) + + # Have any of these values changed? + changed = any([ + member['email'] != user.email, + member['email_wanted'] != user.userprofile.email_wanted, + member['username'] != user.username + ]) + if changed: + # If so, we'll update the member registry, because we've just + # verified our account here and we'll know this information better + # than the registry, if they differ. + success, member, error = update_member(user) + + if success: # Success may have changed since last time we asked. + apply_member_locally(member, user) + + except IcePirateException as e: + if e.sub_msg == 'No such member': + success, member, error = add_member(user) + if success: + apply_member_locally(member, user) diff --git a/gateway/utils.py b/gateway/utils.py new file mode 100644 index 00000000..6c4fc7cd --- /dev/null +++ b/gateway/utils.py @@ -0,0 +1,172 @@ +import json +import requests + +from datetime import datetime + +from django.conf import settings +from django.db.models import Q + +from polity.models import Polity + +from gateway.exceptions import IcePirateException + + +def user_to_member_args(user): + info = { + 'json_api_key': settings.ICEPIRATE['key'], + + 'ssn': user.userprofile.verified_ssn, + 'name': user.userprofile.verified_name, + 'email': user.email, + 'phone': user.userprofile.phone, + 'username': user.username, + 'added': user.date_joined.strftime('%Y-%m-%d %H:%M:%S'), + 'groups': [p.slug for p in user.polities.all()], + } + + # If email_wanted is None, then we still don't know the user's preference, + # so we'll say nothing about it. + if user.userprofile.email_wanted is not None: + info.update({'email_wanted': 'true' if user.userprofile.email_wanted else 'false'}) + + return info + + +def response_to_results(response): + ''' + A unified way to deal with an IcePirate response. + ''' + try: + remote_data = json.loads(response.text) + except ValueError: + raise IcePirateException('JSON parsing error') + + member = remote_data['data'] if 'data' in remote_data else None + error = remote_data['error'] if 'error' in remote_data else None + + if error is not None: + raise IcePirateException('Error in communication with member database: %s' % error, error) + + # TODO: The success-indicator and error are redundant because we are now + # throwing an exception when something goes wrong. + return remote_data['success'], member, error + + +def add_member(user): + + data = user_to_member_args(user) + + try: + response = requests.post('%s/member/api/add/' % settings.ICEPIRATE['url'], data=data) + except: + raise IcePirateException('Failed adding member to remote member registry') + + return response_to_results(response) + + +def update_member(user): + + data = user_to_member_args(user) + + try: + response = requests.post( + '%s/member/api/update/ssn/%s/' % (settings.ICEPIRATE['url'], user.userprofile.verified_ssn), + data=data + ) + except: + raise IcePirateException('Failed updating member in remote member registry') + + return response_to_results(response) + + +def get_member(ssn): + + try: + response = requests.post( + '%s/member/api/get/ssn/%s/' % (settings.ICEPIRATE['url'], ssn), + data={'json_api_key': settings.ICEPIRATE['key']} + ) + except: + raise IcePirateException('Failed getting member from remote member registry') + + return response_to_results(response) + + +def add_member_to_membergroup(user, polity): + + try: + response = requests.post( + '%s/member/api/add-to-membergroup/%s/' % ( + settings.ICEPIRATE['url'], + user.userprofile.verified_ssn + ), + data={ + 'json_api_key': settings.ICEPIRATE['key'], + 'membergroup_techname': polity.slug, + } + ) + except: + raise IcePirateException('Failed getting member from remote member registry') + + return response_to_results(response) + + +def apply_member_locally(member, user): + ''' + Takes an IcePirate member in the form of a dict and a regular local user + and applies the data in the member to the local user. Email address is + specifically excempt because its reliability is far greater in Wasa2il + than IcePirate, and is rather updated on IcePirate's side. + ''' + + # Add user to polities according to remote user's groups, as well as + # front polity, if one is designated. + membership_polities = Polity.objects.filter( + Q(slug__in=member['groups'].keys()) + | Q(is_front_polity=True) + ) + for polity in membership_polities: + polity.members.add(user) + + # Remove user from polities in which they are not a member. + non_membership_polities = Polity.objects.exclude( + Q(slug=None) + | Q(slug='') + | Q(slug__in=member['groups'].keys()) + | Q(is_front_polity=True) + ).filter(members=user) + for polity in non_membership_polities: + polity.members.remove(user) + polity.officers.remove(user) + + # Sync info on polity eligibility of user. + polities_eligible = Polity.objects.filter(slug__in=member['eligible_groups']) + for polity in polities_eligible: + polity.eligibles.add(user) + for polity in user.polities_eligible.exclude(id__in=[p.id for p in polities_eligible]): + polity.eligibles.remove(user) + + # Keep track of whether we need to save the profile. + profile_changed = False + + # Ask the member database if the user has consented to receiving + # email and update user database accordingly. + if user.userprofile.email_wanted != member['email_wanted']: + user.userprofile.email_wanted = member['email_wanted'] + profile_changed = True + + # Update local phone number accordin to member registry. + if user.userprofile.phone != member['phone']: + user.userprofile.phone = member['phone'] + profile_changed = True + + # Ask the member database when the user registered, since this may impact + # the user's right to vote. + added = datetime.strptime(member['added'], '%Y-%m-%d %H:%M:%S') + if user.userprofile.joined_org != added: + user.userprofile.joined_org = added + profile_changed = True + + # Save the profile if it has been changed. + if profile_changed: + user.userprofile.save() diff --git a/gateway/views.py b/gateway/views.py new file mode 100644 index 00000000..af1cbe53 --- /dev/null +++ b/gateway/views.py @@ -0,0 +1,3 @@ +# Create your views here. + +# NOTE: API views are defined in icepirate.py diff --git a/initial_setup.py b/initial_setup.py index 569cc060..73e688eb 100755 --- a/initial_setup.py +++ b/initial_setup.py @@ -5,15 +5,23 @@ import subprocess import fileinput import shutil +import sys from sys import stderr from sys import stdin from sys import stdout +from sys import argv import random random = random.SystemRandom() TERMINAL_WIDTH = 80 +venv_path = os.path.relpath(os.path.dirname(sys.executable), sys.path[0]) + +def get_executable_path(executable): + if len(argv) > 1 and argv[1] == '--venv': + return os.path.join(venv_path, executable) + return executable def get_random_string(length=12, @@ -62,7 +70,7 @@ def get_answer(question, proper_answers=('yes','no')): # Install (or upgrade) Python package dependencies stdout.write('Installing dependencies:\n') -result = subprocess.call(["pip", "install", "--upgrade", "-r", "requirements.txt"]) +result = subprocess.call([get_executable_path("pip"), "install", "--upgrade", "-r", "requirements.txt"]) if result == 0: stdout.write('Dependency installation complete.\n') else: @@ -126,15 +134,8 @@ def get_answer(question, proper_answers=('yes','no')): if not local_settings_changed: stdout.write('- No changes needed.\n') - -# Change to Wasa2il's directory -os.chdir('wasa2il') - - # Compile the translation files -for lang in next(os.walk(os.path.join(os.getcwd(), 'locale')))[1]: - subprocess.call(['pybabel', 'compile', '-d', os.path.join(os.getcwd(), 'locale'), '-D', 'django', '-l', lang]) - +subprocess.call([get_executable_path('python'), os.path.join(os.getcwd(), 'manage.py'), 'compilemessages']) # Setup database if needed create_database = False @@ -150,25 +151,28 @@ def get_answer(question, proper_answers=('yes','no')): if create_database: stdout.write('Setting up database (via "migrate"):\n') - migrate_result = subprocess.call(['python', os.path.join(os.getcwd(), 'manage.py'), 'migrate']) + migrate_result = subprocess.call([get_executable_path('python'), os.path.join(os.getcwd(), 'manage.py'), 'migrate']) if migrate_result != 0: stderr.write('Error: Django migration gave errors. Quitting.\n') quit(1) stdout.write('We will now create a superuser to configure polities within Wasa2il once it has been set up.\n') - subprocess.call(['python', os.path.join(os.getcwd(), 'manage.py'), 'createsuperuser']) + subprocess.call([get_executable_path('python'), os.path.join(os.getcwd(), 'manage.py'), 'createsuperuser']) + + stdout.write('Populate wasa2il with some fake data...\n') + subprocess.call([get_executable_path('python'), os.path.join(os.getcwd(), 'manage.py'), 'load_fake_data']) print "*" * TERMINAL_WIDTH print "All done!" print "To run Wasa2il and start configuring polities, follow these steps:" -print "- Go to the 'wasa2il' directory and run 'python manage.py runserver'" +print "- Run '"+get_executable_path('python')+" manage.py runserver'" print "- Open your favorite browser and type in: http://localhost:8000" print "- Log in with the superuser account created previously" print -print "(If you don't have a superuser account yet, then go to the 'wasa2il' directory" -print "and run 'python manage.py createsuperuser')" +print "Additional superuser accounts can be created with" +print "- "+get_executable_path('python')+" manage.py createsuperuser" print "*" * TERMINAL_WIDTH diff --git a/issue/__init__.py b/issue/__init__.py new file mode 100644 index 00000000..52586a4f --- /dev/null +++ b/issue/__init__.py @@ -0,0 +1,6 @@ + + +def heartbeat(t): + from issue.models import Issue + # m = Issue.objects.filter(issue_state='') + pass diff --git a/issue/admin.py b/issue/admin.py new file mode 100644 index 00000000..6492fa26 --- /dev/null +++ b/issue/admin.py @@ -0,0 +1,32 @@ +from django.contrib import admin + +from issue.models import Comment +from issue.models import Document +from issue.models import DocumentContent +from issue.models import Issue + + +class IssueAdmin(admin.ModelAdmin): + fieldsets = None + list_display = ['name', 'slug', 'description'] + exclude = ['votecount', 'votecount_yes', 'votecount_abstain', 'votecount_no'] + + +class DocumentAdmin(admin.ModelAdmin): + fieldsets = [ + (None, {'fields': ['name', 'slug']}), + ] + prepopulated_fields = {'slug': ['name']} + list_display = ['name'] + search_fields = ['name'] + + +class DocumentContentAdmin(admin.ModelAdmin): + list_display = ['document', 'order', 'comments', 'user', 'created'] + + +register = admin.site.register +register(Issue, IssueAdmin) +register(Comment) +register(Document, DocumentAdmin) +register(DocumentContent, DocumentContentAdmin) diff --git a/issue/dataviews.py b/issue/dataviews.py new file mode 100644 index 00000000..c1521286 --- /dev/null +++ b/issue/dataviews.py @@ -0,0 +1,206 @@ +import markdown2 + +from django.conf import settings +from django.db import transaction +from django.db.models import Q +from django.shortcuts import get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.template.loader import render_to_string +from django.utils import timezone +from django.utils.timesince import timesince + +from diff_match_patch.diff_match_patch import diff_match_patch + +from core.ajax.utils import error +from core.ajax.utils import jsonize +from core.templatetags.wasa2il import thumbnail + +from issue.models import Comment +from issue.models import Document +from issue.models import DocumentContent +from issue.models import Issue +from issue.models import Vote + +from polity.models import Polity + +@login_required +@jsonize +def issue_vote(request): + issue = int(request.POST.get("issue", 0)) + issue = get_object_or_404(Issue, id=issue) + + if issue.issue_state() != 'voting': + return issue_poll(request) + + if not issue.can_vote(user=request.user): + return issue_poll(request) + + val = int(request.POST.get("vote", 0)) + + try: + vote = Vote.objects.get(user=request.user, issue=issue) + except Vote.DoesNotExist: + vote = Vote(user=request.user, issue=issue) + vote.value = val + vote.save() + + # Update vote counts + issue.votecount = issue.votecount_yes = issue.votecount_abstain = issue.votecount_no = 0 + votes = issue.vote_set.all() + for vote in votes: + if vote.value == 1: + issue.votecount += 1 + issue.votecount_yes += 1 + elif vote.value == 0: + # We purposely skip adding one to the total vote count. + issue.votecount_abstain += 1 + elif vote.value == -1: + issue.votecount += 1 + issue.votecount_no += 1 + issue.save() + + return issue_poll(request) + + +@login_required +@jsonize +def issue_comment_send(request): + issue = get_object_or_404(Issue, id=request.POST.get("issue", 0)) + text = request.POST.get("comment") + comment = Comment() + comment.created_by = request.user + comment.comment = text + comment.issue = issue + comment.save() + return issue_poll(request) + + +@jsonize +def issue_poll(request): + issue = int(request.POST.get("issue", request.GET.get("issue", 0))) + issue = get_object_or_404(Issue, id=issue) + ctx = {} + comments = [ + { + "id": comment.id, + "created_by": comment.created_by.username, + "created_by_thumb": thumbnail( + comment.created_by.userprofile.picture, '40x40'), + "created": str(comment.created), + "created_since": timesince(comment.created), + "comment": comment.comment + } for comment in issue.comment_set.all().order_by("created") + ] + ctx["issue"] = {"comments": comments, "votecount": issue.votecount } + if issue.issue_state() == 'concluded': + ctx["issue"]["votecount_abstain"] = issue.votecount_abstain + ctx["ok"] = True + if not request.user.is_anonymous: + try: + v = Vote.objects.get(user=request.user, issue=issue) + ctx["issue"]["vote"] = v.value + except Vote.DoesNotExist: + pass + return ctx + + +@jsonize +def issue_showclosed(request): + ctx = {} + + polity_id = int(request.GET.get('polity_id', 0)) + showclosed = int(request.GET.get('showclosed', 0)) # 0 = False, 1 = True + + try: + issues = Issue.objects.select_related('polity') + + if polity_id: + issues = issues.filter(polity_id=polity_id) + else: + issues = issues.order_by('polity__name', '-deadline_votes') + + if not showclosed: + issues = issues.recent() + + if polity_id: + polity = get_object_or_404(Polity, id=polity_id) + else: + polity = None + + html_ctx = { + 'user': request.user, + 'polity': polity, + 'issues_recent': issues, + } + + ctx['showclosed'] = showclosed + ctx['html'] = render_to_string('issue/_issues_recent_table.html', html_ctx) + ctx['ok'] = True + except Exception as e: + ctx['error'] = e.__str__() if settings.DEBUG else 'Error raised. Turn on DEBUG for details.' + + return ctx + + +@jsonize +def documentcontent_render_diff(request): + ctx = {} + + source_id = request.GET.get('source_id') + target_id = request.GET.get('target_id') + + target = get_object_or_404(DocumentContent, id=target_id) + + ctx['source_id'] = source_id + ctx['target_id'] = target_id + ctx['diff'] = target.diff(source_id) + + return ctx + + +@login_required +@jsonize +def documentcontent_retract(request, documentcontent_id): + # Only polity officers and the documentcontent's author are allowed to do this. + try: + documentcontent = DocumentContent.objects.select_related('issue').distinct().exclude(issue=None).get( + Q(user_id=request.user.id) | Q(document__polity__officers__id=request.user.id), + id=documentcontent_id + ) + except DocumentContent.DoesNotExist: + return error('Access denied') + + # Short-hands. + issue = documentcontent.issue + now = timezone.now() + + # Issues that have already been processed cannot be retracted, since that + # would give proposers and officers the power to remove accepted policy at + # their leisure. + if issue.is_processed or issue.issue_state() == 'concluded': + return error('Access denied') + + # Set the issue's special process and who's responsible for it. + issue.special_process = 'retracted' + issue.special_process_set_by = request.user + + # Let timings make sense. + if issue.deadline_discussions > now: + issue.deadline_discussions = now + if issue.deadline_proposals > now: + issue.deadline_proposals = now + if issue.deadline_votes > now: + issue.deadline_votes = now + + # Set the documencontent's status. + documentcontent.status = 'retracted' + + # Save the state. + with transaction.atomic(): + documentcontent.save() + issue.save() + + ctx = { + 'ok': True, + } + return ctx diff --git a/issue/forms.py b/issue/forms.py new file mode 100644 index 00000000..b09f54f4 --- /dev/null +++ b/issue/forms.py @@ -0,0 +1,60 @@ +from django import forms +from django.forms import CharField +from django.forms import ValidationError +from wasa2il.forms import Wasa2ilForm + +from issue.models import Comment, Document, DocumentContent, Issue + +from django.utils.translation import ugettext as _ + +class IssueForm(Wasa2ilForm): + class Meta: + model = Issue + exclude = ( + 'polity', + 'slug', + 'issue_num', + 'issue_year', + 'documentcontent', + 'deadline_discussions', + 'deadline_proposals', + 'deadline_votes', + 'majority_percentage', + 'is_processed', + 'special_process_set_by', + 'votecount', + 'votecount_yes', + 'votecount_abstain', + 'votecount_no', + 'comment_count', + 'archived', + ) + + +class DocumentForm(Wasa2ilForm): + class Meta: + model = Document + exclude = ('user', 'polity', 'slug', 'issues') + + +class DocumentContentForm(Wasa2ilForm): + class Meta: + model = DocumentContent + exclude = ('user', 'document', 'order', 'predecessor', 'status') + + def clean_text(self): + # Make sure that the text isn't identical to the previously active + # version. Note that we replace \r\n for \n because we only store the + # texts with Unix-style newlines (\n) and so replace the input text + # here, so that the comparison is on an equal footing. + text = self.cleaned_data['text'].replace('\r\n', '\n') + pred = self.instance.document.preferred_version() + if pred is not None and pred.id != self.instance.id and pred.text.strip() == text.strip(): + raise ValidationError(_('Content must differ from previous version')) + + return text + +class CommentForm(forms.ModelForm): + class Meta: + model = Comment + exclude = ('issue',) diff --git a/issue/migrations/0001_initial.py b/issue/migrations/0001_initial.py new file mode 100644 index 00000000..fe9d7e3c --- /dev/null +++ b/issue/migrations/0001_initial.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-07-12 14:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('topic', '0001_initial'), + ('polity', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('comment', models.TextField()), + ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comment_created_by', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('slug', models.SlugField(blank=True, max_length=128)), + ('document_type', models.IntegerField(choices=[(1, 'Policy'), (2, 'Bylaw'), (3, 'Motion'), (999, 'Other')], default=1)), + ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-id'], + }, + ), + migrations.CreateModel( + name='DocumentContent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('created', models.DateTimeField(auto_now_add=True)), + ('text', models.TextField()), + ('order', models.IntegerField(default=1)), + ('comments', models.TextField(blank=True)), + ('status', models.CharField(choices=[(b'proposed', 'Proposed'), (b'accepted', 'Accepted'), (b'rejected', 'Rejected'), (b'deprecated', 'Deprecated'), (b'retracted', 'Retracted')], default=b'proposed', max_length=32)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue.Document')), + ('predecessor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='issue.DocumentContent')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Issue', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='A great issue name expresses the essence of a proposal as briefly as possible.', max_length=128, verbose_name='Name')), + ('slug', models.SlugField(blank=True, max_length=128)), + ('issue_num', models.IntegerField(null=True)), + ('issue_year', models.IntegerField(null=True)), + ('description', models.TextField(blank=True, help_text="An issue description is usually just a copy of the proposal's description, but you can customize it here if you so wish.", null=True, verbose_name='Description')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('deadline_discussions', models.DateTimeField(blank=True, null=True)), + ('deadline_proposals', models.DateTimeField(blank=True, null=True)), + ('deadline_votes', models.DateTimeField(blank=True, null=True)), + ('majority_percentage', models.DecimalField(decimal_places=2, max_digits=5)), + ('is_processed', models.BooleanField(default=False)), + ('votecount', models.IntegerField(default=0)), + ('votecount_yes', models.IntegerField(default=0)), + ('votecount_abstain', models.IntegerField(default=0)), + ('votecount_no', models.IntegerField(default=0)), + ('special_process', models.CharField(blank=True, choices=[(b'accepted_at_assembly', 'Accepted at assembly'), (b'rejected_at_assembly', 'Rejected at assembly'), (b'retracted', 'Retracted')], default=b'', max_length=32, null=True, verbose_name='Special process')), + ('comment_count', models.IntegerField(default=0)), + ('archived', models.BooleanField(default=False)), + ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issue_created_by', to=settings.AUTH_USER_MODEL)), + ('documentcontent', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issue', to='issue.DocumentContent')), + ('modified_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issue_modified_by', to=settings.AUTH_USER_MODEL)), + ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')), + ('ruleset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.PolityRuleset', verbose_name='Ruleset')), + ('special_process_set_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='special_process_issues', to=settings.AUTH_USER_MODEL)), + ('topics', models.ManyToManyField(to='topic.Topic', verbose_name='Topics')), + ], + options={ + 'ordering': ['-deadline_votes'], + }, + ), + migrations.CreateModel( + name='Vote', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.IntegerField()), + ('cast', models.DateTimeField(auto_now_add=True)), + ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue.Issue')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='comment', + name='issue', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue.Issue'), + ), + migrations.AddField( + model_name='comment', + name='modified_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comment_modified_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterUniqueTogether( + name='vote', + unique_together=set([('user', 'issue')]), + ), + migrations.AlterUniqueTogether( + name='issue', + unique_together=set([('polity', 'issue_year', 'issue_num')]), + ), + ] diff --git a/issue/migrations/0002_auto_20181124_1740.py b/issue/migrations/0002_auto_20181124_1740.py new file mode 100644 index 00000000..f9a066ac --- /dev/null +++ b/issue/migrations/0002_auto_20181124_1740.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2018-11-24 17:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('issue', '0001_initial'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='documentcontent', + unique_together=set([('document', 'order')]), + ), + ] diff --git a/issue/migrations/0003_auto_20190822_2006.py b/issue/migrations/0003_auto_20190822_2006.py new file mode 100644 index 00000000..7607a1d5 --- /dev/null +++ b/issue/migrations/0003_auto_20190822_2006.py @@ -0,0 +1,75 @@ +# Generated by Django 2.2.4 on 2019-08-22 20:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('issue', '0002_auto_20181124_1740'), + ] + + operations = [ + migrations.AlterField( + model_name='comment', + name='created_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comment_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='comment', + name='modified_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comment_modified_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='document', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='documentcontent', + name='predecessor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='issue.DocumentContent'), + ), + migrations.AlterField( + model_name='documentcontent', + name='status', + field=models.CharField(choices=[('proposed', 'Proposed'), ('accepted', 'Accepted'), ('rejected', 'Rejected'), ('deprecated', 'Deprecated'), ('retracted', 'Retracted')], default='proposed', max_length=32), + ), + migrations.AlterField( + model_name='documentcontent', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='issue', + name='created_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='issue_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='issue', + name='documentcontent', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='issue', to='issue.DocumentContent'), + ), + migrations.AlterField( + model_name='issue', + name='modified_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='issue_modified_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='issue', + name='ruleset', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='polity.PolityRuleset', verbose_name='Ruleset'), + ), + migrations.AlterField( + model_name='issue', + name='special_process', + field=models.CharField(blank=True, choices=[('accepted_at_assembly', 'Accepted at assembly'), ('rejected_at_assembly', 'Rejected at assembly'), ('retracted', 'Retracted')], default='', max_length=32, null=True, verbose_name='Special process'), + ), + migrations.AlterField( + model_name='issue', + name='special_process_set_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='special_process_issues', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/issue/migrations/0004_auto_20200914_1623.py b/issue/migrations/0004_auto_20200914_1623.py new file mode 100644 index 00000000..69cc2947 --- /dev/null +++ b/issue/migrations/0004_auto_20200914_1623.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.7 on 2020-09-14 16:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('issue', '0003_auto_20190822_2006'), + ] + + operations = [ + migrations.AlterField( + model_name='documentcontent', + name='comments', + field=models.TextField(blank=True, verbose_name='Description'), + ), + migrations.AlterField( + model_name='documentcontent', + name='text', + field=models.TextField(verbose_name='Text'), + ), + ] diff --git a/issue/migrations/__init__.py b/issue/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/issue/models.py b/issue/models.py new file mode 100644 index 00000000..35a5e226 --- /dev/null +++ b/issue/models.py @@ -0,0 +1,511 @@ +import re + +from diff_match_patch.diff_match_patch import diff_match_patch + +from django.conf import settings +from django.db import models +from django.db import transaction +from django.db.models import CASCADE +from django.db.models import PROTECT +from django.db.models import SET_NULL +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone + + +# A mixin for containing methods that need to be in both the queryset and the +# manager. We need IssueManager.get_queryset to filter out archived issues +# automatically, and since a corresponding default-queryset function does not +# seem to exist in QuerySet, we cannot simply use IssueQuerySet.as_manager() +# as is commonly recommended. Instead, this class is used as an extra +# constructor for both IssueManager and IssueQuerySet. +class IssueMixin(): + def recent(self): + return self.filter(deadline_votes__gt=timezone.now() - timezone.timedelta(days=settings.RECENT_ISSUE_DAYS)) + +class IssueManager(models.Manager, IssueMixin): + def get_queryset(self): + return IssueQuerySet(self.model, using=self._db).filter(archived=False) + +# Inherits from IssueMixin. +class IssueQuerySet(models.QuerySet, IssueMixin): + pass + + +class Issue(models.Model): + objects = IssueManager() + + SPECIAL_PROCESS_CHOICES = ( + ('accepted_at_assembly', _('Accepted at assembly')), + ('rejected_at_assembly', _('Rejected at assembly')), + ('retracted', _('Retracted')), + ) + + # Note: Not used as a set of options for a database field, but rather by + # the get_issue_state_display function below. + ISSUE_STATES = ( + ('concluded', _('Concluded')), + ('voting', _('Voting')), + ('accepting_proposals', _('Accepting proposals')), + ('discussion', _('In discussion')), + ) + + name = models.CharField(max_length=128, verbose_name=_('Name'), help_text=_( + 'A great issue name expresses the essence of a proposal as briefly as possible.' + )) + slug = models.SlugField(max_length=128, blank=True) + + issue_num = models.IntegerField(null=True) + issue_year = models.IntegerField(null=True) + + description = models.TextField(verbose_name=_("Description"), null=True, blank=True, help_text=_( + 'An issue description is usually just a copy of the proposal\'s description, but you can customize it here if you so wish.' + )) + + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + null=True, + blank=True, + related_name='issue_created_by', + on_delete=PROTECT + ) + modified_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + null=True, + blank=True, + related_name='issue_modified_by', + on_delete=PROTECT + ) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + polity = models.ForeignKey('polity.Polity', on_delete=CASCADE) + topics = models.ManyToManyField('topic.Topic', verbose_name=_('Topics')) + + documentcontent = models.OneToOneField( + 'issue.DocumentContent', + related_name='issue', + null=True, + blank=True, + on_delete=PROTECT + ) + deadline_discussions = models.DateTimeField(null=True, blank=True) + deadline_proposals = models.DateTimeField(null=True, blank=True) + deadline_votes = models.DateTimeField(null=True, blank=True) + majority_percentage = models.DecimalField(max_digits=5, decimal_places=2) + ruleset = models.ForeignKey('polity.PolityRuleset', verbose_name=_("Ruleset"), editable=True, on_delete=PROTECT) + + is_processed = models.BooleanField(default=False) + votecount = models.IntegerField(default=0) + votecount_yes = models.IntegerField(default=0) + votecount_abstain = models.IntegerField(default=0) + votecount_no = models.IntegerField(default=0) + + # + # + # proponents = models.ManyToManyField('core.User') + + # A special process is one where the result is given without votes being + # counted by the system. Examples are when an issue is accepted or + # rejected at a meeting instead of using the voting system, or when an + # issue is retracted by its proposer or an officer. + special_process = models.CharField( + max_length=32, + verbose_name=_('Special process'), + choices=SPECIAL_PROCESS_CHOICES, + default='', + null=True, + blank=True + ) + special_process_set_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + related_name='special_process_issues', + on_delete=PROTECT + ) + + comment_count = models.IntegerField(default=0) + + # Soft deletion. Recovery of archived items must be done via SQL. + archived = models.BooleanField(default=False) + + class Meta: + ordering = ["-deadline_votes"] + unique_together = ['polity', 'issue_year', 'issue_num'] + + def __str__(self): + return u'%s' % self.name + + def save(self, *args, **kwargs): + # Determine issue_num and issue_year based on available data. + if self.issue_num is None or self.issue_year is None: + self.issue_year = timezone.now().year + + with transaction.atomic(): + try: + self.issue_num = Issue.objects.filter( + polity_id=self.polity_id, + issue_year=self.issue_year + ).order_by('-issue_num')[0].issue_num + 1 + except IndexError: + self.issue_num = 1 + + super(Issue, self).save(*args, **kwargs) + else: + # No transaction needed + super(Issue, self).save(*args, **kwargs) + + # Soft deletion. Recovery of archived items must be done via SQL. + def archive(self): + # These need to be None so we don't run into unique-constraints. + self.issue_year = None + self.issue_num = None + + self.archived = True + self.save() + + def apply_ruleset(self, now=None): + now = now or timezone.now() + + if self.special_process: + self.deadline_discussions = now + self.deadline_proposals = now + self.deadline_votes = now + else: + self.deadline_discussions = now + self.ruleset.issue_discussion_time + self.deadline_proposals = now + self.ruleset.issue_proposal_time + self.deadline_votes = now + self.ruleset.issue_vote_time + + self.majority_percentage = self.ruleset.issue_majority + + def issue_state(self): + # Short-hands. + now = timezone.now() + deadline_votes = self.deadline_votes + deadline_proposals = self.deadline_proposals + deadline_discussions = self.deadline_discussions + + if deadline_votes < now: + return 'concluded' + elif deadline_proposals < now: + return 'voting' + elif deadline_discussions < now: + return 'accepting_proposals' + else: + return 'discussion' + + # For displaying the issue state in human-readable form. + def get_issue_state_display(self): + return dict(self.ISSUE_STATES)[self.issue_state()].__str__() + + def discussions_closed(self): + return timezone.now() > self.deadline_discussions + + def percentage_reached(self): + if self.votecount != 0: + return float(self.votecount_yes) / float(self.votecount) * 100 + else: + return 0.0 + + def process(self): + + # We're not interested in issues that don't have documentcontents. + # They shouldn't actually exist, by the way. They were possible in + # earlier versions but the system no longer offers creating them + # except using a documentcontent. These issues should be dealt with + # somehow, at some point. + if self.documentcontent == None: + return False + + # Short-hands. + documentcontent = self.documentcontent + document = documentcontent.document + + if self.issue_state() == 'concluded' and not self.is_processed: + + # Figure out the current documentcontent's predecessor. + # See function for details. + documentcontent.predecessor = document.preferred_version() + + # Figure out if issue was retracted, accepted or rejected. + if self.special_process == 'retracted': + documentcontent.status = 'retracted' + + elif self.majority_reached(): + documentcontent.status = 'accepted' + + # Since the new version has been accepted, deprecate + # previously accepted versions. + prev_contents = document.documentcontent_set.exclude( + id=documentcontent.id + ).filter(status='accepted') + for prev_content in prev_contents: + prev_content.status = 'deprecated' + prev_content.save() + + # Update the document's name, if it has been changed. + if document.name != documentcontent.name: + document.name = documentcontent.name + document.save() + + else: + documentcontent.status = 'rejected' + + self.vote_set.all().delete() + + self.is_processed = True + + documentcontent.save() + + self.save() + + if self.polity.push_on_vote_end: + # Forcing translation string creation. + __ = _("Voting closed on issue '%s'.") + push_send_notification_to_polity_users(issue.polity.id, "Voting closed on issue '%s'.", [issue.name]) + + return True + + + def get_voters(self): + # FIXME: This is one place to check if we've invited other groups to + # participate in an election, if we implement that feature... + return self.polity.issue_voters() + + def can_vote(self, user=None, user_id=None): + return self.get_voters().filter( + id=(user_id if (user_id is not None) else user.id)).exists() + + def user_documents(self, user): + try: + return self.document_set.filter(user=user) + except TypeError: + return [] + + def majority_reached(self): + if not self.issue_state() == 'concluded': + return False + + result = False + + if self.special_process == 'accepted_at_assembly': + result = True + else: + if self.votecount > 0: + result = float(self.votecount_yes) / self.votecount > float(self.majority_percentage) / 100 + + return result + + def get_majority_reached_display(self): + return _('Accepted').__str__() if self.majority_reached() else _('Rejected').__str__() + + def update_comment_count(self): + self.comment_count = self.comment_set.count() + self.save() + + def __str__(self): + return u'%s' % (self.name) + + +class Vote(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE) + issue = models.ForeignKey('issue.Issue', on_delete=CASCADE) + value = models.IntegerField() + cast = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = (('user', 'issue')) + + +class Comment(models.Model): + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + null=True, + blank=True, + related_name='comment_created_by', + on_delete=SET_NULL + ) + modified_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + null=True, + blank=True, + related_name='comment_modified_by', + on_delete=SET_NULL + ) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + comment = models.TextField() + issue = models.ForeignKey('issue.Issue', on_delete=CASCADE) + + def save(self, *args, **kwargs): + is_new = self.id is None + + super(Comment, self).save(*args, **kwargs) + + if is_new: + self.issue.update_comment_count() + + def delete(self): + super(Comment, self).delete() + + self.issue.update_comment_count() + + +class Document(models.Model): + + DOCUMENT_TYPE_CHOICES = ( + (1, _('Policy')), + (2, _('Bylaw')), + (3, _('Motion')), + (999, _('Other')), + ) + + name = models.CharField(max_length=128, verbose_name=_('Name')) + slug = models.SlugField(max_length=128, blank=True) + + document_type = models.IntegerField(choices=DOCUMENT_TYPE_CHOICES, default=1) + + polity = models.ForeignKey('polity.Polity', on_delete=CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=PROTECT) + + class Meta: + ordering = ["-id"] + + def get_versions(self): + return DocumentContent.objects.filter(document=self).order_by('order') + + # preferred_version() finds the most proper, previous documentcontent to build a new one on. + # It prefers the latest accepted one, but if it cannot find one, it will default to the first proposed one. + # If it finds neither a proposed nor accepted one, it will try to find the first rejected one. + # It will return None if it finds nothing and it's the calling function's responsibility to react accordingly. + # TODO: Make this faster and cached per request. Preferably still Pythonic. -helgi@binary.is, 2014-07-02 + def preferred_version(self): + # Latest accepted version... + accepted_versions = self.documentcontent_set.filter(status='accepted').order_by('-order') + if accepted_versions.count() > 0: + return accepted_versions[0] + + # ...and if none are found, find the earliest proposed one... + proposed_versions = self.documentcontent_set.filter(status='proposed').order_by('order') + if proposed_versions.count() > 0: + return proposed_versions[0] + + # ...boo, go for the first rejected one? + rejected_versions = self.documentcontent_set.filter(status='rejected').order_by('order') + if rejected_versions.count() > 0: + return rejected_versions[0] + + # ...finally and desperately search for things with unknown status + all_versions = self.documentcontent_set.order_by('order') + if all_versions.count() > 0: + return all_versions[0] + else: + return None + + # Returns true if a documentcontent in this document already has an issue in progress. + def has_open_issue(self): + documentcontent_ids = [dc.id for dc in self.documentcontent_set.all()] + count = Issue.objects.filter( + is_processed=False, + documentcontent_id__in=documentcontent_ids + ).exclude( + special_process='retracted' + ).count() + return count > 0 + + def __str__(self): + return u'%s' % (self.name) + + +class DocumentContent(models.Model): + name = models.CharField(max_length=128, verbose_name=_('Name')) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=PROTECT) + document = models.ForeignKey('issue.Document', on_delete=CASCADE) + created = models.DateTimeField(auto_now_add=True) + text = models.TextField(verbose_name=_('Text')) + order = models.IntegerField(default=1) + comments = models.TextField(verbose_name=_('Description'), blank=True) + STATUS_CHOICES = ( + ('proposed', _('Proposed')), + ('accepted', _('Accepted')), + ('rejected', _('Rejected')), + ('deprecated', _('Deprecated')), + ('retracted', _('Retracted')), + ) + status = models.CharField(max_length=32, choices=STATUS_CHOICES, default='proposed') + predecessor = models.ForeignKey('issue.DocumentContent', null=True, blank=True, on_delete=SET_NULL) + + class Meta: + unique_together = ['document', 'order'] + + # Attempt to inherit earlier issue's topic selection + def previous_topics(self): + selected_topics = [] + if self.order > 1: + # NOTE: This is entirely distinct from Document.preferred_version() and should not be replaced by it. + # This function actually regards Issues, not DocumentContents, but is determined by DocumentContent as input. + + # Find the last accepted documentcontent + prev_contents = self.document.documentcontent_set.exclude(id=self.id).order_by('-order') + selected_topics = [] + for c in prev_contents: # NOTE: We're iterating from newest to oldest. + if c.status == 'accepted': + # A previously accepted DocumentContent MUST correspond to an issue so we brutally assume so. + selected_topics = [t.id for t in c.issue.topics.all()] + break + + # If no topic list is determined from previously accepted issue, we inherit from the last Issue, if any. + if len(selected_topics) == 0: + for c in prev_contents: + try: + c_issue = c.issue.get() + selected_topics = [t.id for t in c_issue.topics.all()] + break; + except: + pass + + return selected_topics + + # Gets all DocumentContents which belong to the Document to which this DocumentContent belongs to. + def siblings(self): + siblings = DocumentContent.objects.filter(document_id=self.document_id).order_by('order') + return siblings + + # Generates a diff between this DocumentContent and the one provided to the function. + def diff(self, documentcontent_id): + earlier_content = DocumentContent.objects.get(id=documentcontent_id) + + # Basic diff_match_patch thing + dmp = diff_match_patch() + + dmp.Diff_Timeout = 0 + # dmp.Diff_EditCost = 10 # Higher value means more semantic cleanup. 4 is default which works for us right now. + + d = dmp.diff_main(earlier_content.text, self.text) + + # Calculate the diff + dmp.diff_cleanupSemantic(d) + result = dmp.diff_prettyHtml(d).replace('¶', '') + + result = re.sub(r'\r
', r'
', result) # Because we're using
 in the template, so the HTML creates two newlines.
+
+        return result
+
+    def __str__(self):
+        return u"DocumentContent (ID: %d)" % self.id
+
+    def save(self, *args, **kwargs):
+
+        # Keep a strict standard on line-endings of textx and comments. We
+        # will stick to the simple Unix-variant of a single newline character
+        # to denote a newline. (This may become important for comparing
+        # things, for example.)
+        self.text = self.text.replace('\r\n', '\n')
+        self.comments = self.comments.replace('\r\n', '\n')
+
+        super(DocumentContent, self).save(*args, **kwargs)
diff --git a/issue/tests.py b/issue/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/issue/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/issue/urls.py b/issue/urls.py
new file mode 100644
index 00000000..d1b84a1a
--- /dev/null
+++ b/issue/urls.py
@@ -0,0 +1,43 @@
+from django.conf.urls import url
+from django.contrib.auth.decorators import login_required
+from django.views.decorators.cache import never_cache
+from django.views.generic import UpdateView
+
+from issue.dataviews import issue_comment_send
+from issue.dataviews import issue_poll
+from issue.dataviews import issue_vote
+from issue.dataviews import issue_showclosed
+from issue.dataviews import documentcontent_render_diff
+from issue.dataviews import documentcontent_retract
+from issue.models import Issue
+from issue.views import document_add
+from issue.views import document_agreements
+from issue.views import document_view
+from issue.views import documentcontent_add
+from issue.views import documentcontent_edit
+from issue.views import issue_add_edit
+from issue.views import issue_view
+from issue.views import issues
+
+urlpatterns = [
+
+    url(r'^polity/(?P\d+)/issues/$', issues, name='issues'),
+    url(r'^polity/(?P\d+)/issue/(?P\d+)/edit/$', issue_add_edit, name='issue_edit'),
+    url(r'^polity/(?P\d+)/issue/new/(documentcontent/(?P\d+)/)?$', issue_add_edit, name='issue_add'),
+    url(r'^polity/(?P\d+)/issue/(?P\d+)/$', never_cache(issue_view), name='issue'),
+
+    url(r'^polity/(?P\d+)/agreements/$', document_agreements, name="agreements"),
+    url(r'^polity/(?P\d+)/document/new/$', document_add),
+    url(r'^polity/(?P\d+)/document/(?P\d+)/v(?P\d+)/$', document_view, name='document_view'),
+    url(r'^polity/(?P\d+)/document/(?P\d+)/v(?P\d+)/edit/$', documentcontent_edit, name='documentcontent_edit'),
+    url(r'^polity/(?P\d+)/document/(?P\d+)/new/$', documentcontent_add, name='documentcontent_add'),
+    url(r'^polity/(?P\d+)/document/(?P\d+)/$', document_view, name='document'),
+
+    url(r'^api/issue/comment/send/$', never_cache(issue_comment_send)),
+    url(r'^api/issue/poll/$', never_cache(issue_poll)),
+    url(r'^api/issue/vote/$', never_cache(issue_vote)),
+    url(r'^api/issue/showclosed/$', issue_showclosed),
+
+    url(r'^api/documentcontent/render-diff/$', documentcontent_render_diff),
+    url(r'^api/documentcontent/(?P\d+)/retract/$', documentcontent_retract),
+]
diff --git a/issue/views.py b/issue/views.py
new file mode 100644
index 00000000..9dd65874
--- /dev/null
+++ b/issue/views.py
@@ -0,0 +1,326 @@
+import json
+
+from datetime import datetime
+from datetime import timedelta
+
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
+from django.http import Http404
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.shortcuts import redirect
+from django.shortcuts import render
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import CreateView
+from django.views.generic import DetailView
+from django.views.generic import ListView
+
+from issue.forms import DocumentForm, DocumentContentForm, IssueForm
+from issue.models import Document, DocumentContent, Issue
+
+from polity.models import Polity
+from topic.models import Topic
+
+
+@login_required
+def issue_add_edit(request, polity_id, issue_id=None, documentcontent_id=None):
+    polity = get_object_or_404(Polity, id=polity_id)
+
+    # Make sure that user is allowed to do this.
+    if polity.is_newissue_only_officers and request.user not in polity.officers.all():
+        raise PermissionDenied()
+
+    if issue_id:
+        issue = get_object_or_404(Issue, id=issue_id, polity_id=polity_id)
+        current_content = issue.documentcontent
+
+        # We don't want to edit anything that has already been processed.
+        if issue.is_processed:
+            raise PermissionDenied()
+    else:
+        issue = Issue(polity=polity)
+        if documentcontent_id:
+            try:
+                current_content = DocumentContent.objects.select_related('document').get(id=documentcontent_id)
+            except DocumentContent.DoesNotExist:
+                raise Http404
+        else:
+            current_content = None
+
+    if request.method == 'POST':
+        form = IssueForm(request.POST, instance=issue)
+        if form.is_valid():
+            issue = form.save(commit=False)
+            issue.apply_ruleset()
+            issue.documentcontent = current_content
+            issue.special_process_set_by = request.user if issue.special_process else None
+            issue.save()
+
+            issue.topics.clear()
+            for topic in request.POST.getlist('topics'):
+                issue.topics.add(topic)
+
+            return redirect(reverse('issue', args=(polity_id, issue.id)))
+    else:
+        # Check if we need to inherit information from previous documentcontent.
+        if not issue_id and current_content:
+            name = current_content.name
+            selected_topics = []
+
+            # If this is a new issue, being made from existing content, we
+            # want to inherit the previously selected topics, and add a
+            # version number to the name.
+            if current_content.order > 1:
+                name += u', %s %d' % (_(u'version'), current_content.order)
+                selected_topics = current_content.previous_topics()
+
+            form = IssueForm(instance=issue, initial={
+                'name': name,
+                'description': current_content.comments.replace("\n", "\\n"),
+                'topics': selected_topics,
+            })
+        else:
+            form = IssueForm(instance=issue)
+
+    # Make only topics from this polity available.
+    form.fields['topics'].queryset = Topic.objects.filter(polity=polity)
+
+    ctx = {
+        'polity': polity,
+        'issue': issue,
+        'form': form,
+        'documentcontent': current_content,
+        'tab': 'diff' if current_content.order > 1 else '',
+    }
+
+    return render(request, 'issue/issue_form.html', ctx)
+
+
+def issue_view(request, polity_id, issue_id):
+    polity = get_object_or_404(Polity, id=polity_id)
+    issue = get_object_or_404(Issue, id=issue_id, polity_id=polity_id)
+
+    ctx = {}
+
+    if issue.documentcontent:
+        documentcontent = issue.documentcontent
+        if documentcontent.order > 1:
+            ctx['tab'] = 'diff'
+        else:
+            ctx['tab'] = 'view'
+
+        ctx['documentcontent'] = documentcontent
+        if issue.is_processed:
+            ctx['selected_diff_documentcontent'] = documentcontent.predecessor
+        else:
+            ctx['selected_diff_documentcontent'] = documentcontent.document.preferred_version()
+
+    ctx['polity'] = polity
+    ctx['issue'] = issue
+    ctx['can_vote'] = (request.user is not None and issue.can_vote(request.user))
+    ctx['comments_closed'] = not request.user.is_authenticated or issue.discussions_closed()
+
+    # People say crazy things on the internet. We'd like to keep the record of
+    # conversations about issues well into the future but still we'd like to
+    # protect users from having to answer for something they said a long time
+    # ago. To try and achieve both goals, we require a logged in user to see
+    # comments to older issues.
+    comment_protection_timing = datetime.now() - timedelta(days=settings.RECENT_ISSUE_DAYS)
+    if not request.user.is_authenticated and issue.deadline_votes < comment_protection_timing:
+        ctx['comments_hidden'] = True
+
+    return render(request, 'issue/issue_detail.html', ctx)
+
+
+def issues(request, polity_id):
+    polity = get_object_or_404(Polity, id=polity_id)
+
+    issues = polity.issue_set.order_by('-created')
+
+    ctx = {
+        'polity': polity,
+        'issues': issues,
+    }
+    return render(request, 'issue/issues.html', ctx)
+
+
+@login_required
+def document_add(request, polity_id):
+    try:
+        polity = Polity.objects.get(id=polity_id, members=request.user)
+    except Polity.DoesNotExist:
+        raise PermissionDenied()
+
+    document = Document(polity=polity, user=request.user)
+
+    if request.method == 'POST':
+        form = DocumentForm(request.POST)
+        if form.is_valid():
+            document = form.save(commit=False)
+            document.polity = polity
+            document.user = request.user
+            document.save()
+            return redirect(reverse('documentcontent_add', args=(polity_id, document.id)))
+    else:
+        form = DocumentForm()
+
+    ctx = {
+        'polity': polity,
+        'form': form,
+    }
+    return render(request, 'issue/document_form.html', ctx)
+
+
+def document_view(request, polity_id, document_id, version=None):
+    polity = get_object_or_404(Polity, id=polity_id)
+    document = get_object_or_404(Document, id=document_id, polity__id=polity_id)
+
+    # If version is not specified, we want the "preferred" version
+    if version is not None:
+        current_content = get_object_or_404(DocumentContent, document=document, order=version)
+    else:
+        current_content = document.preferred_version()
+
+    issue = None
+    if current_content is not None and hasattr(current_content, 'issue'):
+        issue = current_content.issue
+
+    buttons = {
+        'propose_change': False,
+        'put_to_vote': False,
+        'edit_proposal': False,
+        'retract_proposal': False,
+    }
+    if ((not issue or issue.issue_state() != 'voting')
+            and current_content is not None):
+
+        # Check if the user should be allowed to retract the issue, which is
+        # at any point in which an issue has been founded but not concluded.
+        # Officers are also allowed to retract on the behalf of users.
+        if issue and issue.issue_state() != 'concluded':
+            buttons['retract_proposal'] = 'enabled'
+
+        if current_content.status == 'accepted':
+            if request.globals['user_is_member']:
+                buttons['propose_change'] = 'enabled'
+        elif current_content.status == 'proposed':
+            if request.globals['user_is_officer'] and not issue:
+                buttons['put_to_vote'] = 'disabled' if document.has_open_issue() else 'enabled'
+            if current_content.user_id == request.user.id:
+                buttons['edit_proposal'] = 'disabled' if issue is not None else 'enabled'
+
+    ctx = {
+        'polity': polity,
+        'document': document,
+        'current_content': current_content,
+        'selected_diff_documentcontent': document.preferred_version,
+        'issue': issue,
+        'buttons': buttons,
+        'buttons_enabled': any([b != False for b in buttons.values()]),
+    }
+
+    return render(request, 'issue/document_detail.html', ctx)
+
+
+@login_required
+def documentcontent_edit(request, polity_id, document_id, version):
+
+    dc = get_object_or_404(
+        DocumentContent,
+        document_id=document_id,
+        document__polity_id=polity_id,
+        order=version
+    )
+
+    # Editing of documentcontents should only be allowed by its author or the
+    # polity's officers.
+    if dc.user_id != request.user.id:
+        raise PermissionDenied
+
+    # Editing of documentcontents is only allowed before an issue has been
+    # created. This is kept separate from the logic above both for reasons of
+    # clarity, and because this is likely to change in the future and so
+    # should remain in its own place. We may very well want to allow the user
+    # to change documentcontents in the future, as long as the issue hasn't
+    # reached a particular state, such as "voting" or "concluded".
+    if hasattr(dc, 'issue'):
+        raise PermissionDenied
+
+    if request.method == 'POST':
+        form = DocumentContentForm(request.POST, instance=dc)
+        if form.is_valid():
+            form.save()
+            return redirect(reverse('document_view', args=(polity_id, document_id, version)))
+    else:
+        form = DocumentContentForm(instance=dc)
+
+    ctx = {
+        'form': form,
+        'polity': request.globals['polity'],
+        'documentcontent': dc,
+    }
+    return render(request, 'issue/documentcontent_edit.html', ctx)
+
+
+@login_required
+def documentcontent_add(request, polity_id, document_id):
+
+    # Only members of the polity are allowed to create proposals.
+    if not request.globals['user_is_member']:
+        raise PermissionDenied
+
+    doc = Document.objects.get(id=document_id, polity_id=polity_id)
+
+    if request.method == 'POST':
+        form = DocumentContentForm(request.POST)
+
+        form.instance.document = doc
+        form.instance.user = request.user
+
+        if form.is_valid():
+
+            # The order of the new documentcontent shall be the count of those
+            # already existing, plus one.
+            form.instance.order = doc.documentcontent_set.count() + 1
+
+            form.save()
+            return redirect(reverse(
+                'document_view',
+                args=(polity_id, document_id, form.instance.order)
+            ))
+    else:
+        form = DocumentContentForm()
+
+        # Inherit the name and text from the previously active version if one
+        # exists, otherwise from the document.
+        pred = doc.preferred_version()
+        if pred is not None:
+            form.fields['name'].initial = pred.name
+            form.fields['text'].initial = pred.text
+        else:
+            form.fields['name'].initial = doc.name
+
+    ctx = {
+        'form': form,
+        'document': doc,
+    }
+    return render(request, 'issue/documentcontent_add.html', ctx)
+
+
+def document_agreements(request, polity_id):
+    polity = get_object_or_404(Polity, id=polity_id)
+
+    q = request.POST.get('q') or ''
+    if q:
+        agreements = polity.agreements(q)
+    else:
+        agreements = polity.agreements()
+
+    ctx = {
+        'q': q,
+        'polity': polity,
+        'agreements': agreements,
+    }
+    return render(request, 'issue/document_list.html', ctx)
diff --git a/languagecontrol/AUTHORS b/languagecontrol/AUTHORS
new file mode 100644
index 00000000..2dfa0721
--- /dev/null
+++ b/languagecontrol/AUTHORS
@@ -0,0 +1 @@
+Helgi Hrafn Gunnarsson 
diff --git a/languagecontrol/CHANGELOG.md b/languagecontrol/CHANGELOG.md
new file mode 100644
index 00000000..b6637ab0
--- /dev/null
+++ b/languagecontrol/CHANGELOG.md
@@ -0,0 +1,16 @@
+# CHANGELOG
+
+## 1.0
+* Initial release.
+
+## 1.0.1
+* Python package requirements updated.
+
+## 1.0.2
+* Python package requirements updated.
+
+## 1.0.3
+* Non-functional changes for compatibility with Django 2.x.
+
+## 1.0.4
+* More non-functional changes for compatibility with Django 2.2.
diff --git a/languagecontrol/LICENSE b/languagecontrol/LICENSE
new file mode 100644
index 00000000..7368a751
--- /dev/null
+++ b/languagecontrol/LICENSE
@@ -0,0 +1,7 @@
+Copyright 2018 Helgi Hrafn Gunnarsson 
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/languagecontrol/README.md b/languagecontrol/README.md
new file mode 100644
index 00000000..6010964d
--- /dev/null
+++ b/languagecontrol/README.md
@@ -0,0 +1,40 @@
+# LanguageControl
+
+## About LanguageControl
+
+Django's otherwise excellent translation mechanism incorrectly assumes that browsers are always configured to request the user's native language. In many countries however, the overwhelming majority of people use software in English instead of their native language for various reasons, and they typically neither know how to, nor care to, configure their browsers specifically to request a specific language from websites. Users will trust that websites intended to be in their native language will simply be in their native language.
+
+To put it simply: Browsers cannot, and thus do not, accurately reflect the users' native language in all countries.
+
+Django's assumption to the contrary means that in such countries, where people tend to use English-language software for one reason or another, even when a Django project has LANGUAGE_CODE set to a target audience's language, most people who speak that language will see the website in English.
+
+LanguageControl enables developers using Django to set whatever default language they see fit.
+
+### License
+
+This app is distributed under the MIT license. See file `LICENSE` for details. In short, don't worry about it.
+
+## Installation
+
+This installation guide assumes familiarity with how Django apps work, how to configure Django project settings and that `LocaleMiddleware` is being used for translating strings from English into some other language.
+
+Start by copying the app in its entirety into your project, right alongside your other apps.
+
+Add `languagecontrol.middleware.LanguageControlMiddleware` to your `MIDDLEWARE` setting, before `LocaleMiddleware` but after `SessionMiddleware`.
+
+Then add `languagecontrol` to the `INSTALLED_APPS` setting.
+
+Your project will then ignore the browser's requests and set the default language to whatever's set in your `LANGUAGE_CODE` setting.
+
+## User-selected language
+
+For projects in which users must be able to select a different language from the default, a function, `set_language(request, language)` is provided in `languagecontrol.utils`, which will set the language for the running session, reverting back to the default language on logout. It can be used when a user selects a preferred language as well, to update the language used in the running session.
+
+To set the user's preferred language at login, however, you'll have to set up a signal receiver in your project to fetch the preferred language from wherever it is stored. It's quite simple, as shown here assuming that the user's preferred language is stored in `user.userprofile.language`:
+
+    from django.contrib.auth.signals import user_logged_in
+    from django.dispatch import receiver
+
+    @receiver(user_logged_in)
+    def set_language_on_login(sender, user, request, **kwargs):
+        set_language(request, user.userprofile.language)
diff --git a/languagecontrol/VERSION b/languagecontrol/VERSION
new file mode 100644
index 00000000..ee90284c
--- /dev/null
+++ b/languagecontrol/VERSION
@@ -0,0 +1 @@
+1.0.4
diff --git a/languagecontrol/__init__.py b/languagecontrol/__init__.py
new file mode 100644
index 00000000..8bb9ef11
--- /dev/null
+++ b/languagecontrol/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'languagecontrol.apps.LanguageControlConfig'
diff --git a/languagecontrol/apps.py b/languagecontrol/apps.py
new file mode 100644
index 00000000..bbf68b05
--- /dev/null
+++ b/languagecontrol/apps.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.apps import AppConfig
+
+
+class LanguageControlConfig(AppConfig):
+    name = 'languagecontrol'
+    verbose_name = 'LanguageControl for Django'
+
+    def ready(self):
+        import languagecontrol.signals
diff --git a/languagecontrol/middleware.py b/languagecontrol/middleware.py
new file mode 100644
index 00000000..d04de26e
--- /dev/null
+++ b/languagecontrol/middleware.py
@@ -0,0 +1,6 @@
+from django.utils.deprecation import MiddlewareMixin
+
+class LanguageControlMiddleware(MiddlewareMixin):
+    def process_request(self, request):
+        if 'HTTP_ACCEPT_LANGUAGE' in request.META:
+            del request.META['HTTP_ACCEPT_LANGUAGE']
diff --git a/languagecontrol/requirements.txt b/languagecontrol/requirements.txt
new file mode 100644
index 00000000..8f65f85b
--- /dev/null
+++ b/languagecontrol/requirements.txt
@@ -0,0 +1 @@
+Django>=1.11.18
diff --git a/languagecontrol/signals.py b/languagecontrol/signals.py
new file mode 100644
index 00000000..05e498f7
--- /dev/null
+++ b/languagecontrol/signals.py
@@ -0,0 +1,11 @@
+from django.contrib.auth.signals import user_logged_out
+from django.conf import settings
+from django.dispatch import receiver
+from django.utils import translation
+
+@receiver(user_logged_out)
+def switch_to_default_language_on_logout(sender, user, request, **kwargs):
+    # When logged out, we want to set the default language.
+    if translation.LANGUAGE_SESSION_KEY in request.session:
+        translation.activate(settings.LANGUAGE_CODE)
+        del request.session[translation.LANGUAGE_SESSION_KEY]
diff --git a/languagecontrol/utils.py b/languagecontrol/utils.py
new file mode 100644
index 00000000..79f31761
--- /dev/null
+++ b/languagecontrol/utils.py
@@ -0,0 +1,5 @@
+from django.utils import translation
+
+def set_language(request, language):
+    translation.activate(language)
+    request.session[translation.LANGUAGE_SESSION_KEY] = language
diff --git a/wasa2il/manage.py b/manage.py
similarity index 61%
rename from wasa2il/manage.py
rename to manage.py
index f9726f9e..d0a34657 100755
--- a/wasa2il/manage.py
+++ b/manage.py
@@ -1,9 +1,11 @@
 #!/usr/bin/env python
 import os
 import sys
+import dotenv
 
 if __name__ == "__main__":
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
+    dotenv.read_dotenv()
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wasa2il.settings")
 
     from django.core.management import execute_from_command_line
 
diff --git a/newdoc/.gitignore b/newdoc/.gitignore
deleted file mode 100644
index e35d8850..00000000
--- a/newdoc/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-_build
diff --git a/newdoc/Makefile b/newdoc/Makefile
deleted file mode 100644
index 0795ef6f..00000000
--- a/newdoc/Makefile
+++ /dev/null
@@ -1,153 +0,0 @@
-# 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) .
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
-
-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 "  singlehtml to make a single large HTML file"
-	@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 "  devhelp    to make HTML files and a Devhelp project"
-	@echo "  epub       to make an epub"
-	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
-	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
-	@echo "  text       to make text files"
-	@echo "  man        to make manual pages"
-	@echo "  texinfo    to make Texinfo files"
-	@echo "  info       to make Texinfo files and run them through makeinfo"
-	@echo "  gettext    to make PO message catalogs"
-	@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."
-
-singlehtml:
-	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
-	@echo
-	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-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/wasa2il.qhcp"
-	@echo "To view the help file:"
-	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/wasa2il.qhc"
-
-devhelp:
-	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
-	@echo
-	@echo "Build finished."
-	@echo "To view the help file:"
-	@echo "# mkdir -p $$HOME/.local/share/devhelp/wasa2il"
-	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/wasa2il"
-	@echo "# devhelp"
-
-epub:
-	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
-	@echo
-	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo
-	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
-	@echo "Run \`make' in that directory to run these through (pdf)latex" \
-	      "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
-	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
-	@echo "Running LaTeX files through pdflatex..."
-	$(MAKE) -C $(BUILDDIR)/latex all-pdf
-	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
-	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
-	@echo
-	@echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
-	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
-	@echo
-	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-texinfo:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo
-	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
-	@echo "Run \`make' in that directory to run these through makeinfo" \
-	      "(use \`make info' here to do that automatically)."
-
-info:
-	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
-	@echo "Running Texinfo files through makeinfo..."
-	make -C $(BUILDDIR)/texinfo info
-	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-gettext:
-	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
-	@echo
-	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-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/newdoc/_static/.gitignore b/newdoc/_static/.gitignore
deleted file mode 100644
index 28c3556d..00000000
--- a/newdoc/_static/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Empty ignore file to get an empty directory into git. This directory is a 
-# part of the general sphinx project structure and is refrenced in the sphinx 
-# configuration. It might be confusing in the future if they are missing.
diff --git a/newdoc/_templates/.gitignore b/newdoc/_templates/.gitignore
deleted file mode 100644
index 28c3556d..00000000
--- a/newdoc/_templates/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Empty ignore file to get an empty directory into git. This directory is a 
-# part of the general sphinx project structure and is refrenced in the sphinx 
-# configuration. It might be confusing in the future if they are missing.
diff --git a/newdoc/api.rst b/newdoc/api.rst
deleted file mode 100644
index d318b04f..00000000
--- a/newdoc/api.rst
+++ /dev/null
@@ -1,57 +0,0 @@
-API Documentation
-=================
-
-
-core.models
------------
-
-.. autoclass:: core.models.UserProfile
-
-.. autoclass:: core.models.BaseIssue
-
-.. autoclass:: core.models.PolityRuleset
-
-.. autoclass:: core.models.Polity
-
-.. autoclass:: core.models.Topic
-
-.. autoclass:: core.models.UserTopic
-
-.. autoclass:: core.models.Issue
-
-.. autoclass:: core.models.Comment
-
-.. autoclass:: core.models.Delegate
-
-.. autoclass:: core.models.Vote
-
-.. autoclass:: core.models.MembershipVote
-
-.. autoclass:: core.models.MembershipRequest
-
-.. autoclass:: core.models.Document
-
-.. autoclass:: core.models.DocumentContent
-
-.. autoclass:: core.models.Statement
-
-.. autoclass:: core.models.StatementOption
-
-.. autoclass:: core.models.ChangeProposal
-
-.. autoclass:: core.models.Meeting
-
-.. autoclass:: core.models.MeetingRules
-
-.. autoclass:: core.models.MeetingAgenda
-
-.. autoclass:: core.models.MeetingIntervention
-
-.. autoclass:: core.models.VotingSystem
-
-.. autoclass:: core.models.Election
-
-.. autoclass:: core.models.Candidate
-
-.. autoclass:: core.models.ElectionVote
-
diff --git a/newdoc/conf.py b/newdoc/conf.py
deleted file mode 100644
index 0f9013b6..00000000
--- a/newdoc/conf.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# wasa2il documentation build configuration file, created by
-# sphinx-quickstart2 on Tue Dec 17 18:12:07 2013.
-#
-# 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.insert(0, os.path.abspath('../wasa2il'))
-
-# This will allow sphinx to run without having this environmental variable set
-# but was mainly added for automatic build from readthedocs.org
-if os.environ.get('DJANGO_SETTINGS_MODULE', None) == None:
-    os.environ['DJANGO_SETTINGS_MODULE'] = 'empty'
-
-# -- General configuration -----------------------------------------------------
-
-# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
-
-# 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.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode']
-
-# 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-sig'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General information about the project.
-project = u'wasa2il'
-copyright = u'2013, Jakob Sv. Bjarnason'
-
-# 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 = '1.0'
-# The full version, including alpha/beta/rc tags.
-release = '1.0'
-
-# 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 patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-exclude_patterns = ['_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.  See the documentation for
-# a list of builtin themes.
-#html_theme = 'default'
-html_theme = 'nature'
-
-# 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_domain_indices = 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, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = 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 = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'wasa2ildoc'
-
-
-# -- Options for LaTeX output --------------------------------------------------
-
-latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
-
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
-
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
-}
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
-latex_documents = [
-  ('index', 'wasa2il.tex', u'wasa2il Documentation',
-   u'Jakob Sv. Bjarnason', '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
-
-# If true, show page references after internal links.
-#latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-#latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_domain_indices = True
-
-
-# -- Options for manual page output --------------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
-    ('index', 'wasa2il', u'wasa2il Documentation',
-     [u'Jakob Sv. Bjarnason'], 1)
-]
-
-# If true, show URL addresses after external links.
-#man_show_urls = False
-
-
-# -- Options for Texinfo output ------------------------------------------------
-
-# Grouping the document tree into Texinfo files. List of tuples
-# (source start file, target name, title, author,
-#  dir menu entry, description, category)
-texinfo_documents = [
-  ('index', 'wasa2il', u'wasa2il Documentation',
-   u'Jakob Sv. Bjarnason', 'wasa2il', 'One line description of project.',
-   'Miscellaneous'),
-]
-
-# Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
-
-# If false, no module index is generated.
-#texinfo_domain_indices = True
-
-# How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
-
-
-# -- Options for Epub output ---------------------------------------------------
-
-# Bibliographic Dublin Core info.
-epub_title = u'wasa2il'
-epub_author = u'Jakob Sv. Bjarnason'
-epub_publisher = u'Jakob Sv. Bjarnason'
-epub_copyright = u'2013, Jakob Sv. Bjarnason'
-
-# The language of the text. It defaults to the language option
-# or en if the language is not set.
-#epub_language = ''
-
-# The scheme of the identifier. Typical schemes are ISBN or URL.
-#epub_scheme = ''
-
-# The unique identifier of the text. This can be a ISBN number
-# or the project homepage.
-#epub_identifier = ''
-
-# A unique identification for the text.
-#epub_uid = ''
-
-# A tuple containing the cover image and cover page html template filenames.
-#epub_cover = ()
-
-# HTML files that should be inserted before the pages created by sphinx.
-# The format is a list of tuples containing the path and title.
-#epub_pre_files = []
-
-# HTML files shat should be inserted after the pages created by sphinx.
-# The format is a list of tuples containing the path and title.
-#epub_post_files = []
-
-# A list of files that should not be packed into the epub file.
-#epub_exclude_files = []
-
-# The depth of the table of contents in toc.ncx.
-#epub_tocdepth = 3
-
-# Allow duplicate toc entries.
-#epub_tocdup = True
-
-todo_include_todos = True
diff --git a/newdoc/getting_started.rst b/newdoc/getting_started.rst
deleted file mode 100644
index 7063ac7c..00000000
--- a/newdoc/getting_started.rst
+++ /dev/null
@@ -1,175 +0,0 @@
-Gettings Started
-================
-
-This section will detail how to get the development environment up and running.
-
-Getting wasa2il from source
----------------------------
-
-You will need to have Git versioning control software installed on your machine
-
-Cloning wasa2il
-~~~~~~~~~~~~~~~
-
-To get the most recent development version of the source issuing the following command.
-
-::
-
-    git clone https://github.com/piratar/wasa2il.git
-
-.. hint::
-    The project and code directories share the same name, wasa2il.
-    To avoid confusion it is recommanded to rename the project directory.
-
-Initialize submodules
-~~~~~~~~~~~~~~~~~~~~~
-
-.. index::
-    single: OpenSTV
-
-wasa2il requires a 3rd party library called OpenSTV.
-OpenSTV git repository is configured as submodules to wasa2il
-To get git to fetch this project run:
-
-::
-
-    git submodule update --init
-
-This will fetch the libraries project and place it in ``lib/submodules/openSTV``
-
-.. warning::
-    This library can't be imported by default. It needs to be on the
-    python path.
-
-    .. note::
-        A simple workaround is to place a symlink to it's code. From the project directory run:
-
-        ::
-
-            prompt> ln -s lib/submodules/openSTV/SourceCode/OpenSTV-1.6/openstv
-
-.. note::
-    wasa2il uses a OpenSTV 1.6 fork since later versions are not free.
-
-.. todo::
-    Change submodule configuration to make OpenSTV available by default.
-
-
-Installing required libraries
------------------------------
-
-In the project directory there is a file called ``requirements.txt``.
-It containes all the standard libraries wasa2il needs to run.
-The content of the file is:
-
-.. literalinclude:: ../requirements.txt
-
-These library packages can easily be installed using:
-
-::
-
-    pip install -r requirements.txt
-
-.. warning::
-    This will replace a django installation of a different version (1.4)
-
-.. warning::
-    This step might fail if you don't have build tools and the python dev package install.
-
-.. note::
-    It is recommanded to use a virtual enviroment for development.
-
-
-Additional development packages
--------------------------------
-
-Documentation
-~~~~~~~~~~~~~
-
-The documenation of this project uses Sphinx.
-To be able to build the documentation from its reStructuredText
-files Sphinx needs to be installed
-
-::
-
-    pip install sphinx
-
-.. todo::
-    Add Testing to Additional development libraries
-
-Initial wasa2il configuration
------------------------------
-
-When you reach this point you should have all
-the code and it's dependancies installed.
-
-Enviromental variables
-~~~~~~~~~~~~~~~~~~~~~~
-
-For everything to work correctly you will need to set some
-enviromental variables.
-
-PYTHONPATH
-    The path to the project directory needs to be in here.
-DJANGO_SETTINGS_MODLUE
-    This variable should be assigned ``wasa2il.settings``
-
-.. note::
-    This is not needed to get the development server started.
-    This is needed for external tools (f.ex sphinx) to find and load the project.
-
-
-Configuring local_settings
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Included in the source is an example file of local_settings.py
-called ``local_settings.py-example`` that you can see here:
-
-.. literalinclude:: ../wasa2il/local_settings.py-example
-
-Make a copy of this file and call it ``local_settings.py``.
-
-Change the database configuration to match your database
-(if sqlite3 then only DATABASE_ENGINE and DATABASE_NAME needs to be defined)
-and change the SECRET_KEY variable.
-Thats all that is needed to get django running.
-
-Creating initial database
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To create the inital database you need to create the database
-tables from the models of wasa2il and it's installed apps.
-The command to run is:
-
-::
-
-  python manage.py syncdb
-
-.. warning::
-    If a superuser is created it will not have a
-    :class:`~core.models.UserProfile` assoiated with it.
-    To fix this you need to enter the shell
-
-    ::
-
-        python manage.py shell
-
-    Retrieve the created user (it will have a primary key of 1)
-    and add a :class:`~core.models.UserProfile` to it.
-
-    ::
-
-        from django.contrib.auth.models import User
-        from core.models import UserProfile
-
-        UserProfile(user=User.objects.get(pk=1)).save()
-
-Collect static media
-~~~~~~~~~~~~~~~~~~~~
-
-For everything to display correctly we need to gather in the static data and place it in the correct directory.
-
-::
-
-    python manage.py collectstatic
-
diff --git a/newdoc/index.rst b/newdoc/index.rst
deleted file mode 100644
index 803a53b8..00000000
--- a/newdoc/index.rst
+++ /dev/null
@@ -1,32 +0,0 @@
-.. wasa2il documentation master file, created by
-   sphinx-quickstart2 on Tue Dec 17 18:12:07 2013.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Welcome to wasa2il's documentation!
-===================================
-
-Contents:
-
-.. toctree::
-   :maxdepth: 2
-
-   getting_started
-   api
-
-ToDo list:
-----------
-
-.. todolist::
-
-Test
------
-
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
-
diff --git a/newdoc/make.bat b/newdoc/make.bat
deleted file mode 100644
index 373f52a9..00000000
--- a/newdoc/make.bat
+++ /dev/null
@@ -1,190 +0,0 @@
-@ECHO OFF
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
-	set SPHINXBUILD=sphinx-build2
-)
-set BUILDDIR=_build
-set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
-set I18NSPHINXOPTS=%SPHINXOPTS% .
-if NOT "%PAPER%" == "" (
-	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
-	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
-)
-
-if "%1" == "" goto help
-
-if "%1" == "help" (
-	: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.  singlehtml to make a single large HTML file
-	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.  devhelp    to make HTML files and a Devhelp project
-	echo.  epub       to make an epub
-	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
-	echo.  text       to make text files
-	echo.  man        to make manual pages
-	echo.  texinfo    to make Texinfo files
-	echo.  gettext    to make PO message catalogs
-	echo.  changes    to make an overview over 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
-	goto end
-)
-
-if "%1" == "clean" (
-	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
-	del /q /s %BUILDDIR%\*
-	goto end
-)
-
-if "%1" == "html" (
-	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
-	goto end
-)
-
-if "%1" == "dirhtml" (
-	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
-	goto end
-)
-
-if "%1" == "singlehtml" (
-	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
-	goto end
-)
-
-if "%1" == "pickle" (
-	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can process the pickle files.
-	goto end
-)
-
-if "%1" == "json" (
-	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can process the JSON files.
-	goto end
-)
-
-if "%1" == "htmlhelp" (
-	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can run HTML Help Workshop with the ^
-.hhp project file in %BUILDDIR%/htmlhelp.
-	goto end
-)
-
-if "%1" == "qthelp" (
-	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; now you can run "qcollectiongenerator" with the ^
-.qhcp project file in %BUILDDIR%/qthelp, like this:
-	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\wasa2il.qhcp
-	echo.To view the help file:
-	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\wasa2il.ghc
-	goto end
-)
-
-if "%1" == "devhelp" (
-	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished.
-	goto end
-)
-
-if "%1" == "epub" (
-	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The epub file is in %BUILDDIR%/epub.
-	goto end
-)
-
-if "%1" == "latex" (
-	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
-	goto end
-)
-
-if "%1" == "text" (
-	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The text files are in %BUILDDIR%/text.
-	goto end
-)
-
-if "%1" == "man" (
-	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The manual pages are in %BUILDDIR%/man.
-	goto end
-)
-
-if "%1" == "texinfo" (
-	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
-	goto end
-)
-
-if "%1" == "gettext" (
-	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
-	goto end
-)
-
-if "%1" == "changes" (
-	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.The overview file is in %BUILDDIR%/changes.
-	goto end
-)
-
-if "%1" == "linkcheck" (
-	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Link check complete; look for any errors in the above output ^
-or in %BUILDDIR%/linkcheck/output.txt.
-	goto end
-)
-
-if "%1" == "doctest" (
-	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
-	if errorlevel 1 exit /b 1
-	echo.
-	echo.Testing of doctests in the sources finished, look at the ^
-results in %BUILDDIR%/doctest/output.txt.
-	goto end
-)
-
-:end
diff --git a/polity/__init__.py b/polity/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/polity/admin.py b/polity/admin.py
new file mode 100644
index 00000000..f125d279
--- /dev/null
+++ b/polity/admin.py
@@ -0,0 +1,14 @@
+from django.contrib import admin
+
+from polity.models import Polity
+from polity.models import PolityRuleset
+
+
+class PolityAdmin(admin.ModelAdmin):
+    fieldsets = None
+    list_display = ['name', 'slug', 'order', 'description', 'parent']
+
+
+register = admin.site.register
+register(Polity, PolityAdmin)
+register(PolityRuleset)
diff --git a/polity/contextprocessors.py b/polity/contextprocessors.py
new file mode 100644
index 00000000..211d89e5
--- /dev/null
+++ b/polity/contextprocessors.py
@@ -0,0 +1,41 @@
+from collections import OrderedDict
+from polity.models import Polity
+
+def navigation(request):
+
+    # For now, we only support one level of sub-polities in the navigation.
+    # This is mostly because we haven't designed the interface to accommodate
+    # sub-sub-polities, and we're not likely to use them any time soon.
+    #
+    # This may seem overly convoluted (and hopefully adequately explained in
+    # comments) but the effect we want is:
+    #
+    # a) All visible sub-polities of the uppermost polities being shown.
+    # b) Only show polity types which actually contain sub-polities.
+    # c) Order polity types as they are defined in the `Polity` model.
+    # d) Sub-polity navigation being available from within sub-polities.
+
+    # The resulting object, used in the template.
+    polity_nav = OrderedDict()
+
+    # Order of polity types as defined in Polity.POLITY_TYPES.
+    type_order = [t[0] for t in Polity.POLITY_TYPES]
+
+    # Get visible sub-polities of uppermost polity.
+    polities = Polity.objects.visible().exclude(parent=None).filter(parent__parent=None)
+
+    # Sort polities by type, in the order defined in Polity.POLITY_TYPES.
+    polities = sorted(polities, key=lambda p: type_order.index(p.polity_type))
+
+    # Add polities to the navigation, organized and ordered by type.
+    for polity in polities:
+        # Create the entry of the polity type if it doesn't exist.
+        if polity.polity_type not in polity_nav:
+            polity_nav[polity.polity_type] = {
+                'polity_type_name': polity.get_polity_type_display(),
+                'polities': [],
+            }
+
+        polity_nav[polity.polity_type]['polities'].append(polity)
+
+    return { 'polity_nav': polity_nav }
diff --git a/polity/forms.py b/polity/forms.py
new file mode 100644
index 00000000..6e266fa5
--- /dev/null
+++ b/polity/forms.py
@@ -0,0 +1,14 @@
+from wasa2il.forms import Wasa2ilForm
+
+from polity.models import Polity
+
+class PolityForm(Wasa2ilForm):
+    class Meta:
+        model = Polity
+        exclude = ('slug', 'parent', 'members', 'officers', 'wranglers')
+
+
+class PolityOfficersForm(Wasa2ilForm):
+    class Meta:
+        model = Polity
+        fields = ('officers',)
diff --git a/polity/migrations/0001_initial.py b/polity/migrations/0001_initial.py
new file mode 100644
index 00000000..870cfc60
--- /dev/null
+++ b/polity/migrations/0001_initial.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-07-12 14:16
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Polity',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=128, verbose_name='Name')),
+                ('slug', models.SlugField(blank=True, max_length=128)),
+                ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
+                ('is_listed', models.BooleanField(default=True, help_text='Whether the polity is publicly listed or not.', verbose_name='Publicly listed?')),
+                ('is_newissue_only_officers', models.BooleanField(default=False, help_text="If this is checked, only officers can create new issues. If it's unchecked, any member can start a new issue.", verbose_name='Can only officers make new issues?')),
+                ('is_front_polity', models.BooleanField(default=False, help_text='If checked, this polity will be displayed on the front page. The first created polity automatically becomes the front polity.', verbose_name='Front polity?')),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('modified', models.DateTimeField(auto_now=True)),
+                ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polity_created_by', to=settings.AUTH_USER_MODEL)),
+                ('members', models.ManyToManyField(related_name='polities', to=settings.AUTH_USER_MODEL)),
+                ('modified_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polity_modified_by', to=settings.AUTH_USER_MODEL)),
+                ('officers', models.ManyToManyField(related_name='officers', to=settings.AUTH_USER_MODEL, verbose_name='Officers')),
+                ('parent', models.ForeignKey(blank=True, help_text=b'Parent polity', null=True, on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')),
+                ('wranglers', models.ManyToManyField(related_name='wranglers', to=settings.AUTH_USER_MODEL, verbose_name='Volunteer wranglers')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='PolityRuleset',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=255)),
+                ('issue_majority', models.DecimalField(decimal_places=2, max_digits=5)),
+                ('issue_discussion_time', models.DurationField()),
+                ('issue_proposal_time', models.DurationField()),
+                ('issue_vote_time', models.DurationField()),
+                ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')),
+            ],
+        ),
+    ]
diff --git a/polity/migrations/0002_auto_20181207_0944.py b/polity/migrations/0002_auto_20181207_0944.py
new file mode 100644
index 00000000..9fa0c68f
--- /dev/null
+++ b/polity/migrations/0002_auto_20181207_0944.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.16 on 2018-12-07 09:44
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='polity',
+            name='push_before_election_end',
+            field=models.BooleanField(default=False, verbose_name='Send notification an hour before election ends?'),
+        ),
+        migrations.AddField(
+            model_name='polity',
+            name='push_before_vote_end',
+            field=models.BooleanField(default=False, verbose_name='Send notification an hour before voting ends?'),
+        ),
+        migrations.AddField(
+            model_name='polity',
+            name='push_on_debate_start',
+            field=models.BooleanField(default=False, verbose_name='Send notification when debate starts?'),
+        ),
+        migrations.AddField(
+            model_name='polity',
+            name='push_on_election_end',
+            field=models.BooleanField(default=False, verbose_name='Send notification when an election ends?'),
+        ),
+        migrations.AddField(
+            model_name='polity',
+            name='push_on_election_start',
+            field=models.BooleanField(default=False, verbose_name='Send notification when an election starts?'),
+        ),
+        migrations.AddField(
+            model_name='polity',
+            name='push_on_vote_end',
+            field=models.BooleanField(default=False, verbose_name='Send notification when voting ends?'),
+        ),
+        migrations.AddField(
+            model_name='polity',
+            name='push_on_vote_start',
+            field=models.BooleanField(default=False, verbose_name='Send notification when issue goes to vote?'),
+        ),
+    ]
diff --git a/polity/migrations/0003_auto_20190822_2006.py b/polity/migrations/0003_auto_20190822_2006.py
new file mode 100644
index 00000000..cfbe1d8e
--- /dev/null
+++ b/polity/migrations/0003_auto_20190822_2006.py
@@ -0,0 +1,30 @@
+# Generated by Django 2.2.4 on 2019-08-22 20:06
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0002_auto_20181207_0944'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='polity',
+            name='created_by',
+            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='polity_created_by', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterField(
+            model_name='polity',
+            name='modified_by',
+            field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='polity_modified_by', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterField(
+            model_name='polity',
+            name='parent',
+            field=models.ForeignKey(blank=True, help_text='Parent polity', null=True, on_delete=django.db.models.deletion.SET_NULL, to='polity.Polity'),
+        ),
+    ]
diff --git a/polity/migrations/0003_polity_require_phone_for_volunteering.py b/polity/migrations/0003_polity_require_phone_for_volunteering.py
new file mode 100644
index 00000000..37c97e3f
--- /dev/null
+++ b/polity/migrations/0003_polity_require_phone_for_volunteering.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.23 on 2019-09-01 12:17
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0002_auto_20181207_0944'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='polity',
+            name='require_phone_for_volunteering',
+            field=models.BooleanField(default=True, help_text='Make users provide their phone numbers in the profiles to partake in tasks that need volunteers.', verbose_name='Require phone for volunteering'),
+        ),
+    ]
diff --git a/polity/migrations/0004_remove_polity_require_phone_for_volunteering.py b/polity/migrations/0004_remove_polity_require_phone_for_volunteering.py
new file mode 100644
index 00000000..6785a68a
--- /dev/null
+++ b/polity/migrations/0004_remove_polity_require_phone_for_volunteering.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.23 on 2019-09-01 14:26
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0003_polity_require_phone_for_volunteering'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='polity',
+            name='require_phone_for_volunteering',
+        ),
+    ]
diff --git a/polity/migrations/0005_merge_20191019_1939.py b/polity/migrations/0005_merge_20191019_1939.py
new file mode 100644
index 00000000..35ff9c11
--- /dev/null
+++ b/polity/migrations/0005_merge_20191019_1939.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.6 on 2019-10-19 19:39
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0004_remove_polity_require_phone_for_volunteering'),
+        ('polity', '0003_auto_20190822_2006'),
+    ]
+
+    operations = [
+    ]
diff --git a/polity/migrations/0006_auto_20200909_2020.py b/polity/migrations/0006_auto_20200909_2020.py
new file mode 100644
index 00000000..81f89302
--- /dev/null
+++ b/polity/migrations/0006_auto_20200909_2020.py
@@ -0,0 +1,22 @@
+# Generated by Django 2.2.7 on 2020-09-09 20:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0005_merge_20191019_1939'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='polity',
+            options={'ordering': ['order', 'name']},
+        ),
+        migrations.AddField(
+            model_name='polity',
+            name='order',
+            field=models.IntegerField(default=1, help_text='Optional, custom sort order. Polities with the same order are ordered by name.', verbose_name='Order'),
+        ),
+    ]
diff --git a/polity/migrations/0007_polity_name_short.py b/polity/migrations/0007_polity_name_short.py
new file mode 100644
index 00000000..5fd109ed
--- /dev/null
+++ b/polity/migrations/0007_polity_name_short.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.7 on 2020-09-14 16:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0006_auto_20200909_2020'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='polity',
+            name='name_short',
+            field=models.CharField(default='', help_text='Optional. Could be an abbreviation or acronym, for example.', max_length=30, verbose_name='Short name'),
+        ),
+    ]
diff --git a/polity/migrations/0008_polity_eligibles.py b/polity/migrations/0008_polity_eligibles.py
new file mode 100644
index 00000000..0ba5be21
--- /dev/null
+++ b/polity/migrations/0008_polity_eligibles.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.2.17 on 2021-01-16 20:10
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('polity', '0007_polity_name_short'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='polity',
+            name='eligibles',
+            field=models.ManyToManyField(related_name='polities_eligible', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/polity/migrations/0008_polity_polity_type.py b/polity/migrations/0008_polity_polity_type.py
new file mode 100644
index 00000000..9e7a1489
--- /dev/null
+++ b/polity/migrations/0008_polity_polity_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.5 on 2021-01-16 21:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0007_polity_name_short'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='polity',
+            name='polity_type',
+            field=models.CharField(choices=[('U', 'Unspecified'), ('S', 'Regional group'), ('C', 'Constituency group'), ('I', 'Special Interest Group')], default='U', max_length=1),
+        ),
+    ]
diff --git a/polity/migrations/0009_merge_20210118_2102.py b/polity/migrations/0009_merge_20210118_2102.py
new file mode 100644
index 00000000..4ed8dd1a
--- /dev/null
+++ b/polity/migrations/0009_merge_20210118_2102.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.1.5 on 2021-01-18 21:02
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0008_polity_polity_type'),
+        ('polity', '0008_polity_eligibles'),
+    ]
+
+    operations = [
+    ]
diff --git a/polity/migrations/0010_auto_20210118_2102.py b/polity/migrations/0010_auto_20210118_2102.py
new file mode 100644
index 00000000..d96fabf8
--- /dev/null
+++ b/polity/migrations/0010_auto_20210118_2102.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.1.5 on 2021-01-18 21:02
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('polity', '0009_merge_20210118_2102'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='polity',
+            name='eligibles',
+            field=models.ManyToManyField(blank=True, related_name='polities_eligible', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterField(
+            model_name='polity',
+            name='name_short',
+            field=models.CharField(blank=True, default='', help_text='Optional. Could be an abbreviation or acronym, for example.', max_length=30, null=True, verbose_name='Short name'),
+        ),
+        migrations.AlterField(
+            model_name='polity',
+            name='polity_type',
+            field=models.CharField(choices=[('U', 'Unspecified'), ('R', 'Regional group'), ('C', 'Constituency group'), ('I', 'Special Interest Group')], default='U', max_length=1),
+        ),
+    ]
diff --git a/polity/migrations/0011_auto_20210118_2303.py b/polity/migrations/0011_auto_20210118_2303.py
new file mode 100644
index 00000000..39c08e1e
--- /dev/null
+++ b/polity/migrations/0011_auto_20210118_2303.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.5 on 2021-01-18 23:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0010_auto_20210118_2102'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='polity',
+            name='polity_type',
+            field=models.CharField(choices=[('unspecified', 'Unspecified'), ('regional', 'Regional Group'), ('constituency', 'Constituency Group'), ('special_interest', 'Special Interest Group')], default='unspecified', max_length=20),
+        ),
+    ]
diff --git a/polity/migrations/0012_auto_20210118_2342.py b/polity/migrations/0012_auto_20210118_2342.py
new file mode 100644
index 00000000..b294b184
--- /dev/null
+++ b/polity/migrations/0012_auto_20210118_2342.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.1.5 on 2021-01-18 23:42
+
+from django.db import migrations
+
+# When polity types were first introduced, they were enumerated with a
+# single-letter with the default "U" for "unspecified" and so forth. We are
+# changing to more descriptive strings which are expected to exist and so we
+# must update previous data.
+def update_polity_types(apps, schema_editor):
+    Polity = apps.get_model('polity', 'Polity')
+    Polity.objects.filter(polity_type='U').update(polity_type='unspecified')
+    Polity.objects.filter(polity_type='R').update(polity_type='regional')
+    Polity.objects.filter(polity_type='C').update(polity_type='constituency')
+    Polity.objects.filter(polity_type='I').update(polity_type='special_interest')
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('polity', '0011_auto_20210118_2303'),
+    ]
+
+    operations = [
+        migrations.RunPython(update_polity_types)
+    ]
diff --git a/polity/migrations/__init__.py b/polity/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/polity/models.py b/polity/models.py
new file mode 100644
index 00000000..6d8815a6
--- /dev/null
+++ b/polity/models.py
@@ -0,0 +1,167 @@
+from django.apps import apps
+from django.db import models
+from django.conf import settings
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+from django.db.models import CASCADE
+from django.db.models import Q
+from django.db.models import SET_NULL
+
+
+class PolityQuerySet(models.QuerySet):
+    def visible(self):
+        return self.filter(is_listed=True)
+
+class Polity(models.Model):
+    objects = PolityQuerySet.as_manager()
+
+    POLITY_TYPES = (
+        ('unspecified', _('Unspecified')),
+        ('regional', _('Regional Group')),
+        ('constituency', _('Constituency Group')),
+        ('special_interest', _('Special Interest Group')),
+    )
+
+    """A political entity. See the manual."""
+    name = models.CharField(max_length=128, verbose_name=_('Name'))
+    name_short = models.CharField(
+        max_length=30,
+        verbose_name=_('Short name'),
+        help_text=_('Optional. Could be an abbreviation or acronym, for example.'),
+        default='',
+        null=True,
+        blank=True
+    )
+    slug = models.SlugField(max_length=128, blank=True)
+
+    description = models.TextField(verbose_name=_("Description"), null=True, blank=True)
+
+    order = models.IntegerField(default=1, verbose_name=_('Order'), help_text=_('Optional, custom sort order. Polities with the same order are ordered by name.'))
+
+    polity_type = models.CharField(max_length=20, choices=POLITY_TYPES, default='unspecified')
+
+    created_by = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        editable=False,
+        null=True,
+        blank=True,
+        related_name='polity_created_by',
+        on_delete=SET_NULL
+    )
+    modified_by = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        editable=False,
+        null=True,
+        blank=True,
+        related_name='polity_modified_by',
+        on_delete=SET_NULL
+    )
+    created = models.DateTimeField(auto_now_add=True)
+    modified = models.DateTimeField(auto_now=True)
+
+    parent = models.ForeignKey('Polity', help_text="Parent polity", null=True, blank=True, on_delete=SET_NULL)
+    members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='polities')
+    eligibles = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='polities_eligible', blank=True)
+    officers = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_("Officers"), related_name="officers")
+    wranglers = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_("Volunteer wranglers"), related_name="wranglers")
+
+    is_listed = models.BooleanField(verbose_name=_("Publicly listed?"), default=True, help_text=_("Whether the polity is publicly listed or not."))
+    is_newissue_only_officers = models.BooleanField(verbose_name=_("Can only officers make new issues?"), default=False, help_text=_("If this is checked, only officers can create new issues. If it's unchecked, any member can start a new issue."))
+    is_front_polity = models.BooleanField(verbose_name=_("Front polity?"), default=False, help_text=_("If checked, this polity will be displayed on the front page. The first created polity automatically becomes the front polity."))
+
+    push_on_debate_start = models.BooleanField(default=False,
+        verbose_name=_("Send notification when debate starts?"))
+    push_on_vote_start = models.BooleanField(default=False,
+        verbose_name=_("Send notification when issue goes to vote?"))
+    push_before_vote_end = models.BooleanField(default=False,
+        verbose_name=_("Send notification an hour before voting ends?"))
+    push_on_vote_end = models.BooleanField(default=False,
+        verbose_name=_("Send notification when voting ends?"))
+    push_on_election_start = models.BooleanField(default=False,
+        verbose_name=_("Send notification when an election starts?"))
+    push_before_election_end = models.BooleanField(default=False,
+        verbose_name=_("Send notification an hour before election ends?"))
+    push_on_election_end = models.BooleanField(default=False,
+        verbose_name=_("Send notification when an election ends?"))
+
+    def is_member(self, user):
+        return self.members.filter(id=user.id).exists()
+
+    def is_officer(self, user):
+        return self.officers.filter(id=user.id).exists()
+
+    def is_wrangler(self, user):
+        return self.wranglers.filter(id=user.id).exists()
+
+    # FIXME: If we want to have different folks participating in internal
+    #        affairs vs. elections, this would be one place to implement that.
+    def issue_voters(self):
+        return self.members
+
+    def election_voters(self):
+        return self.members
+
+    def election_potential_candidates(self):
+        return self.members
+
+    def agreements(self, query=None):
+        DocumentContent = apps.get_model('issue', 'DocumentContent')
+        res = DocumentContent.objects.select_related(
+            'document',
+            'issue'
+        ).filter(
+            status='accepted',
+            document__polity_id=self.id
+        ).order_by('-issue__deadline_votes')
+        if query:
+            res = res.filter(Q(issue__name__icontains=query)
+                           | Q(issue__description__icontains=query)
+                           | Q(text__icontains=query))
+
+        return res
+
+    def update_agreements(self):
+        Issue = apps.get_model('issue', 'Issue')
+        issues_to_process = Issue.objects.filter(is_processed=False).filter(deadline_votes__lt=timezone.now())
+        for issue in issues_to_process:
+            issue.process()
+        return None
+
+    def save(self, *args, **kwargs):
+
+        polities = Polity.objects.all()
+        if polities.count() == 0:
+            self.is_front_polity = True
+        elif self.is_front_polity:
+            for frontpolity in polities.filter(is_front_polity=True).exclude(id=self.id): # Should never return more than 1
+                frontpolity.is_front_polity = False
+                frontpolity.save()
+
+        return super(Polity, self).save(*args, **kwargs)
+
+    def __str__(self):
+        return u'%s' % (self.name)
+
+    class Meta:
+        ordering = ['order', 'name']
+
+
+class PolityRuleset(models.Model):
+    """A polity's ruleset."""
+    polity = models.ForeignKey('polity.Polity', on_delete=CASCADE)
+    name = models.CharField(max_length=255)
+
+    # Issue majority is how many percent of the polity are needed
+    # for a decision to be made on the issue.
+    issue_majority = models.DecimalField(max_digits=5, decimal_places=2)
+
+    # Denotes how many seconds an issue is in various phases.
+    issue_discussion_time = models.DurationField()
+    issue_proposal_time = models.DurationField()
+    issue_vote_time = models.DurationField()
+
+    #issue_proponents_required = models.IntegerField(help_text='The minimum number of people who must explicitly state support before the issue progresses. If zero, no automatic progression will occur.')
+    #issue_voter_quorum = models.IntegerField()
+
+    def __str__(self):
+        return u'%s' % self.name
diff --git a/polity/tests.py b/polity/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/polity/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/polity/urls.py b/polity/urls.py
new file mode 100644
index 00000000..eb299c88
--- /dev/null
+++ b/polity/urls.py
@@ -0,0 +1,21 @@
+from django.conf.urls import url
+from django.contrib.auth.decorators import login_required
+from django.urls import path
+from django.views.decorators.cache import never_cache
+
+from polity.models import Polity
+from polity.views import polity_add_edit
+from polity.views import polity_apply
+from polity.views import polity_list
+from polity.views import polity_officers
+from polity.views import polity_view
+
+
+urlpatterns = [
+    url(r'^polities/$', polity_list, name='polities'),
+    url(r'^polity/new/$', polity_add_edit, name='polity_add'),
+    url(r'^polity/(?P\d+)/edit/$', polity_add_edit, name='polity_edit'),
+    url(r'^polity/(?P\d+)/officers/$', polity_officers, name='polity_officers'),
+    url(r'^polity/(?P\d+)/$', never_cache(polity_view), name='polity'),
+    path('polity//apply/', polity_apply, name='polity_apply'),
+]
diff --git a/polity/views.py b/polity/views.py
new file mode 100644
index 00000000..4be05cad
--- /dev/null
+++ b/polity/views.py
@@ -0,0 +1,147 @@
+from datetime import datetime
+from datetime import timedelta
+
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404
+from django.shortcuts import redirect
+from django.shortcuts import render
+from django.urls import reverse
+from django.db.models import Q
+
+from core.models import UserProfile
+
+from election.models import Election
+
+from gateway.utils import add_member_to_membergroup
+from gateway.utils import apply_member_locally
+
+from issue.models import Issue
+
+from polity.forms import PolityForm
+from polity.forms import PolityOfficersForm
+from polity.models import Polity
+
+
+def polity_list(request):
+    polities = Polity.objects.all()
+
+    issues_recent = Issue.objects.recent().filter(polity__in=polities).order_by('polity__name')
+    elections_recent = Election.objects.recent().filter(polity__in=polities).order_by('polity__name')
+
+    ctx = {
+        'polities': polities,
+        'issues_recent': issues_recent,
+        'elections_recent': elections_recent,
+        'RECENT_ISSUE_DAYS': settings.RECENT_ISSUE_DAYS,
+        'RECENT_ELECTION_DAYS': settings.RECENT_ELECTION_DAYS,
+    }
+    return render(request, 'polity/polity_list.html', ctx)
+
+
+def polity_view(request, polity_id):
+    polity = get_object_or_404(Polity, id=polity_id)
+
+    polity.update_agreements()
+
+    sub_polities = polity.polity_set.all()
+
+    election_set = Election.objects.recent().filter(Q(polity=polity) | Q(polity__parent=polity))
+
+    ctx = {
+        'sub_polities': sub_polities,
+        'politytopics': polity.topic_set.listing_info(request.user).all(),
+        'agreements': polity.agreements(),
+        'issues_recent': polity.issue_set.recent(),
+        'elections_recent': election_set,
+        'RECENT_ISSUE_DAYS': settings.RECENT_ISSUE_DAYS,
+        'RECENT_ELECTION_DAYS': settings.RECENT_ELECTION_DAYS,
+        'verified_user_count': polity.members.filter(userprofile__verified=True).count(),
+    }
+
+    return render(request, 'polity/polity_detail.html', ctx)
+
+
+@login_required
+def polity_add_edit(request, polity_id=None):
+    if not request.user.is_staff and not request.globals['user_is_officer']:
+        raise PermissionDenied()
+
+    if polity_id:
+        polity = get_object_or_404(Polity, id=polity_id)
+    else:
+        polity = Polity()
+
+    if request.method == 'POST':
+        form = PolityForm(request.POST, instance=polity)
+        if form.is_valid():
+            is_new = polity.id is None
+
+            polity = form.save()
+
+            # Make sure that the creator of the polity is also a member.
+            if is_new:
+                polity.members.add(request.user)
+                polity.officers.add(request.user)
+
+            return redirect(reverse('polity', args=(polity.id,)))
+    else:
+        form = PolityForm(instance=polity)
+
+    ctx = {
+        'polity': polity,
+        'form': form,
+    }
+    return render(request, 'polity/polity_form.html', ctx)
+
+
+@login_required
+def polity_officers(request, polity_id):
+    if not request.user.is_staff and not request.globals['user_is_officer']:
+        raise PermissionDenied()
+
+    polity = request.globals['polity']
+
+    if request.method == 'POST':
+        form = PolityOfficersForm(request.POST, instance=polity)
+        if form.is_valid():
+            polity = form.save()
+
+            return redirect(reverse('polity', args=(polity.id,)))
+    else:
+        form = PolityOfficersForm(instance=polity)
+
+    # Make only members from this polity available as officers.
+    form.fields['officers'].queryset = polity.members.all()
+
+    ctx = {
+        'polity': polity,
+        'form': form,
+    }
+    return render(request, 'polity/polity_officers_form.html', ctx)
+
+
+@login_required
+def polity_apply(request, polity_id):
+
+    try:
+        polity = request.user.polities_eligible.get(id=polity_id)
+    except Polity.DoesNotExist:
+        raise PermissionDenied()
+
+    # Communicate the addition to IcePirate, get the resulting member object
+    # from IcePirate again and apply the new version locally. This will place
+    # the user in the local polity.
+    try:
+        success, member, error = add_member_to_membergroup(request.user, polity)
+        if success:
+            apply_member_locally(member, request.user)
+    except:
+        # Just being safe. If anything goes wrong here, the user is expected
+        # to speak with an administrator of some sort.
+        polity.members.remove(request.user)
+
+    # If everything went fine, we'll reload the page from which the user
+    # originated. We don't care where they're from.
+    return redirect(request.META['HTTP_REFERER'])
diff --git a/py3votecore/NOTE_FROM_WASA2IL_DEVS.md b/py3votecore/NOTE_FROM_WASA2IL_DEVS.md
new file mode 100644
index 00000000..442ef0f9
--- /dev/null
+++ b/py3votecore/NOTE_FROM_WASA2IL_DEVS.md
@@ -0,0 +1,15 @@
+# Notes on this directory from Wasa2il developers
+
+The original package of python-vote-core is at:
+
+    https://github.com/bradbeattie/python-vote-core/
+
+This is the package which is available as `python-vote-full` in the `pip` repository. It is only compatible with Python 2.x and its author no longer maintains it.
+
+Another author upgraded the package to work with Python 3, which is located here:
+
+    https://github.com/the-maldridge/python-vote-core
+
+However, this package is not available in the `pip` repository at the time of this writing (2019-07-24).
+
+For this reason, the directory `py3votecore` has been copied to this project without any modifications. **Please do not make Wasa2il-specific modifications to the code in this directory, because we need it compatible with future versions of `py3votecore`.** Hopefully one day a version that is compatible with Python 3 will become available in the `pip` repository, at which point this directory should be removed and the package name (whatever it will be) added to `requirements.txt`.
diff --git a/py3votecore/__init__.py b/py3votecore/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/py3votecore/abstract_classes.py b/py3votecore/abstract_classes.py
new file mode 100644
index 00000000..c40e11c7
--- /dev/null
+++ b/py3votecore/abstract_classes.py
@@ -0,0 +1,173 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from .tie_breaker import TieBreaker
+from abc import ABCMeta, abstractmethod
+from copy import copy, deepcopy
+import types
+
+
+# This class provides methods that most electoral systems make use of.
+class VotingSystem(object, metaclass=ABCMeta):
+    @abstractmethod
+    def __init__(self, ballots, tie_breaker=None):
+        self.ballots = ballots
+        for ballot in self.ballots:
+            if "count" not in ballot:
+                ballot["count"] = 1
+        self.tie_breaker = tie_breaker
+        if isinstance(self.tie_breaker, list):
+            self.tie_breaker = TieBreaker(self.tie_breaker)
+        self.calculate_results()
+
+    @abstractmethod
+    def as_dict(self):
+        data = dict()
+        data["candidates"] = self.candidates
+        if self.tie_breaker and self.tie_breaker.ties_broken:
+            data["tie_breaker"] = self.tie_breaker.as_list()
+        return data
+
+    def break_ties(self, tied_objects, reverse_order=False):
+        if self.tie_breaker is None:
+            self.tie_breaker = TieBreaker(self.candidates)
+        return self.tie_breaker.break_ties(tied_objects, reverse_order)
+
+
+# Given a set of candidates, return a fixed number of winners
+class FixedWinnerVotingSystem(VotingSystem, metaclass=ABCMeta):
+    @abstractmethod
+    def __init__(self, ballots, tie_breaker=None):
+        super(FixedWinnerVotingSystem, self).__init__(ballots, tie_breaker)
+
+    def as_dict(self):
+        data = super(FixedWinnerVotingSystem, self).as_dict()
+        if hasattr(self, 'tied_winners'):
+            data["tied_winners"] = self.tied_winners
+        return data
+
+
+# Given a set of candidates, return the set of winners
+class MultipleWinnerVotingSystem(FixedWinnerVotingSystem, metaclass=ABCMeta):
+    @abstractmethod
+    def __init__(self, ballots, tie_breaker=None, required_winners=1):
+        self.required_winners = required_winners
+        super(MultipleWinnerVotingSystem, self).__init__(ballots, tie_breaker)
+
+    def calculate_results(self):
+        if self.required_winners == len(self.candidates):
+            self.winners = self.candidates
+
+    def as_dict(self):
+        data = super(MultipleWinnerVotingSystem, self).as_dict()
+        data["winners"] = self.winners
+        return data
+
+
+# Given a set of candidates, return a single winners
+class SingleWinnerVotingSystem(FixedWinnerVotingSystem, metaclass=ABCMeta):
+    @abstractmethod
+    def __init__(self, ballots, tie_breaker=None):
+        super(SingleWinnerVotingSystem, self).__init__(ballots, tie_breaker)
+
+    def as_dict(self):
+        data = super(SingleWinnerVotingSystem, self).as_dict()
+        data["winner"] = self.winner
+        return data
+
+
+# Given a set of candidates, return a fixed number of winners
+class AbstractSingleWinnerVotingSystem(SingleWinnerVotingSystem, metaclass=ABCMeta):
+    @abstractmethod
+    def __init__(self, ballots, multiple_winner_class, tie_breaker=None):
+        self.multiple_winner_class = multiple_winner_class
+        super(AbstractSingleWinnerVotingSystem, self).__init__(ballots, tie_breaker=tie_breaker)
+
+    def calculate_results(self):
+        self.multiple_winner_instance = self.multiple_winner_class(self.ballots, tie_breaker=self.tie_breaker, required_winners=1)
+        self.__dict__.update(self.multiple_winner_instance.__dict__)
+        self.winner = list(self.winners)[0]
+        del self.winners
+
+    def as_dict(self):
+        data = super(AbstractSingleWinnerVotingSystem, self).as_dict()
+        data.update(self.multiple_winner_instance.as_dict())
+        del data["winners"]
+        return data
+
+
+# Given a set of candidates, return an ordering
+class OrderingVotingSystem(VotingSystem, metaclass=ABCMeta):
+    @abstractmethod
+    def __init__(self, ballots, tie_breaker=None, winner_threshold=None):
+        self.winner_threshold = winner_threshold
+        super(OrderingVotingSystem, self).__init__(ballots, tie_breaker=tie_breaker)
+
+    def as_dict(self):
+        data = super(OrderingVotingSystem, self).as_dict()
+        data["order"] = self.order
+        return data
+
+
+# Given a single winner system, generate a non-proportional ordering by
+# sequentially removing the winner and rerunning the election with the
+# smaller subset of candidates until all candidates are consumed.
+class AbstractOrderingVotingSystem(OrderingVotingSystem, metaclass=ABCMeta):
+    @abstractmethod
+    def __init__(self, ballots, single_winner_class, winner_threshold=None, tie_breaker=None):
+        self.single_winner_class = single_winner_class
+        super(AbstractOrderingVotingSystem, self).__init__(ballots, winner_threshold=winner_threshold, tie_breaker=tie_breaker)
+
+    def calculate_results(self):
+        self.order = []
+        self.rounds = []
+        remaining_ballots = deepcopy(self.ballots)
+        remaining_candidates = True
+        while (
+            (remaining_candidates is True or len(remaining_candidates) > 1)
+            and (self.winner_threshold is None or len(self.order) < self.winner_threshold)
+        ):
+
+            # Given the remaining ballots, who should win?
+            result = self.single_winner_class(deepcopy(remaining_ballots), tie_breaker=self.tie_breaker)
+
+            # Mark the candidate that won
+            r = {'winner': result.winner}
+            self.order.append(r['winner'])
+
+            # Mark any ties that might have occurred
+            if hasattr(result, 'tie_breaker'):
+                self.tie_breaker = result.tie_breaker
+                if hasattr(result, 'tied_winners'):
+                    r['tied_winners'] = result.tied_winners
+            self.rounds.append(r)
+
+            # Remove the candidate from the remaining candidates and ballots
+            if remaining_candidates is True:
+                self.candidates = result.candidates
+                remaining_candidates = copy(self.candidates)
+            remaining_candidates.remove(result.winner)
+            remaining_ballots = self.ballots_without_candidate(result.ballots, result.winner)
+
+        # Note the last remaining candidate
+        if (self.winner_threshold is None or len(self.order) < self.winner_threshold):
+            r = {'winner': list(remaining_candidates)[0]}
+            self.order.append(r['winner'])
+            self.rounds.append(r)
+
+    def as_dict(self):
+        data = super(AbstractOrderingVotingSystem, self).as_dict()
+        data["rounds"] = self.rounds
+        return data
diff --git a/py3votecore/common_functions.py b/py3votecore/common_functions.py
new file mode 100644
index 00000000..afe1ce12
--- /dev/null
+++ b/py3votecore/common_functions.py
@@ -0,0 +1,21 @@
+def matching_keys(dict, target_value):
+    return set([
+        key
+        for key, value in dict.items()
+        if value == target_value
+    ])
+
+
+def unique_permutations(xs):
+    if len(xs) < 2:
+        yield xs
+    else:
+        h = []
+        for x in xs:
+            h.append(x)
+            if x in h[:-1]:
+                continue
+            ts = xs[:]
+            ts.remove(x)
+            for ps in unique_permutations(ts):
+                yield [x] + ps
diff --git a/py3votecore/condorcet.py b/py3votecore/condorcet.py
new file mode 100644
index 00000000..ef65e6d1
--- /dev/null
+++ b/py3votecore/condorcet.py
@@ -0,0 +1,126 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from abc import ABCMeta, abstractmethod
+from .abstract_classes import SingleWinnerVotingSystem
+from pygraph.classes.digraph import digraph
+import itertools
+
+
+class CondorcetHelper(object):
+
+    BALLOT_NOTATION_GROUPING = 0
+    BALLOT_NOTATION_RANKING = 1
+    BALLOT_NOTATION_RATING = 2
+
+    def standardize_ballots(self, ballots, ballot_notation):
+
+        self.ballots = ballots
+        if ballot_notation == CondorcetHelper.BALLOT_NOTATION_GROUPING:
+            for ballot in self.ballots:
+                ballot["ballot"].reverse()
+                new_ballot = {}
+                r = 0
+                for rank in ballot["ballot"]:
+                    r += 1
+                    for candidate in rank:
+                        new_ballot[candidate] = r
+                ballot["ballot"] = new_ballot
+        elif ballot_notation == CondorcetHelper.BALLOT_NOTATION_RANKING:
+            for ballot in self.ballots:
+                for candidate, rating in ballot["ballot"].items():
+                    ballot["ballot"][candidate] = -float(rating)
+        elif ballot_notation == CondorcetHelper.BALLOT_NOTATION_RATING or ballot_notation is None:
+            for ballot in self.ballots:
+                for candidate, rating in ballot["ballot"].items():
+                    ballot["ballot"][candidate] = float(rating)
+        else:
+            raise Exception("Unknown notation specified", ballot_notation)
+
+        self.candidates = set()
+        for ballot in self.ballots:
+            self.candidates |= set(ballot["ballot"].keys())
+
+        for ballot in self.ballots:
+            lowest_preference = min(ballot["ballot"].values()) - 1
+            for candidate in self.candidates - set(ballot["ballot"].keys()):
+                ballot["ballot"][candidate] = lowest_preference
+
+    def graph_winner(self):
+        losing_candidates = set([edge[1] for edge in self.graph.edges()])
+        winning_candidates = set(self.graph.nodes()) - losing_candidates
+        if len(winning_candidates) == 1:
+            self.winner = list(winning_candidates)[0]
+        elif len(winning_candidates) > 1:
+            self.tied_winners = winning_candidates
+            self.winner = self.break_ties(winning_candidates)
+        else:
+            self.condorcet_completion_method()
+
+    @staticmethod
+    def ballots_into_graph(candidates, ballots):
+        graph = digraph()
+        graph.add_nodes(candidates)
+        for pair in itertools.permutations(candidates, 2):
+            graph.add_edge(pair, sum([
+                ballot["count"]
+                for ballot in ballots
+                if ballot["ballot"][pair[0]] > ballot["ballot"][pair[1]]
+            ]))
+        return graph
+
+    @staticmethod
+    def edge_weights(graph):
+        return dict([
+            (edge, graph.edge_weight(edge))
+            for edge in graph.edges()
+        ])
+
+    @staticmethod
+    def remove_weak_edges(graph):
+        for pair in itertools.combinations(graph.nodes(), 2):
+            pairs = (pair, (pair[1], pair[0]))
+            weights = (graph.edge_weight(pairs[0]), graph.edge_weight(pairs[1]))
+            if weights[0] >= weights[1]:
+                graph.del_edge(pairs[1])
+            if weights[1] >= weights[0]:
+                graph.del_edge(pairs[0])
+
+# This class determines the Condorcet winner if one exists.
+
+
+class CondorcetSystem(SingleWinnerVotingSystem, CondorcetHelper, metaclass=ABCMeta):
+
+    @abstractmethod
+    def __init__(self, ballots, tie_breaker=None, ballot_notation=None):
+        self.standardize_ballots(ballots, ballot_notation)
+        super(CondorcetSystem, self).__init__(self.ballots, tie_breaker=tie_breaker)
+
+    def calculate_results(self):
+        self.graph = self.ballots_into_graph(self.candidates, self.ballots)
+        self.pairs = self.edge_weights(self.graph)
+        self.remove_weak_edges(self.graph)
+        self.strong_pairs = self.edge_weights(self.graph)
+        self.graph_winner()
+
+    def as_dict(self):
+        data = super(CondorcetSystem, self).as_dict()
+        if hasattr(self, 'pairs'):
+            data["pairs"] = self.pairs
+        if hasattr(self, 'strong_pairs'):
+            data["strong_pairs"] = self.strong_pairs
+        if hasattr(self, 'tied_winners'):
+            data["tied_winners"] = self.tied_winners
+        return data
diff --git a/py3votecore/irv.py b/py3votecore/irv.py
new file mode 100644
index 00000000..d9c7b6a5
--- /dev/null
+++ b/py3votecore/irv.py
@@ -0,0 +1,24 @@
+from .abstract_classes import AbstractSingleWinnerVotingSystem
+from .stv import STV
+
+
+class IRV(AbstractSingleWinnerVotingSystem):
+
+    def __init__(self, ballots, tie_breaker=None):
+        super(IRV, self).__init__(ballots, STV, tie_breaker=tie_breaker)
+
+    def calculate_results(self):
+        super(IRV, self).calculate_results()
+        IRV.singularize(self.rounds)
+
+    def as_dict(self):
+        data = super(IRV, self).as_dict()
+        IRV.singularize(data["rounds"])
+        return data
+
+    @staticmethod
+    def singularize(rounds):
+        for r in rounds:
+            if "winners" in r:
+                r["winner"] = list(r["winners"])[0]
+                del r["winners"]
diff --git a/py3votecore/plurality.py b/py3votecore/plurality.py
new file mode 100644
index 00000000..4a8c2757
--- /dev/null
+++ b/py3votecore/plurality.py
@@ -0,0 +1,8 @@
+from .abstract_classes import AbstractSingleWinnerVotingSystem
+from .plurality_at_large import PluralityAtLarge
+
+
+class Plurality(AbstractSingleWinnerVotingSystem):
+
+    def __init__(self, ballots, tie_breaker=None):
+        super(Plurality, self).__init__(ballots, PluralityAtLarge, tie_breaker=tie_breaker)
diff --git a/py3votecore/plurality_at_large.py b/py3votecore/plurality_at_large.py
new file mode 100644
index 00000000..f185a44b
--- /dev/null
+++ b/py3votecore/plurality_at_large.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from .abstract_classes import MultipleWinnerVotingSystem
+from .common_functions import matching_keys
+import types
+import copy
+
+
+class PluralityAtLarge(MultipleWinnerVotingSystem):
+
+    def __init__(self, ballots, tie_breaker=None, required_winners=1):
+        super(PluralityAtLarge, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners)
+
+    def calculate_results(self):
+
+        # Standardize the ballot format and extract the candidates
+        self.candidates = set()
+        for ballot in self.ballots:
+
+            # Convert single candidate ballots into ballot lists
+            if not isinstance(ballot["ballot"], list):
+                ballot["ballot"] = [ballot["ballot"]]
+
+            # Ensure no ballot has an excess of votes
+            if len(ballot["ballot"]) > self.required_winners:
+                raise Exception("A ballot contained too many candidates")
+
+            # Add all candidates on the ballot to the set
+            self.candidates.update(set(ballot["ballot"]))
+
+        # Sum up all votes for each candidate
+        self.tallies = dict.fromkeys(self.candidates, 0)
+        for ballot in self.ballots:
+            for candidate in ballot["ballot"]:
+                self.tallies[candidate] += ballot["count"]
+        tallies = copy.deepcopy(self.tallies)
+
+        # Determine which candidates win
+        winning_candidates = set()
+        while len(winning_candidates) < self.required_winners:
+
+            # Find the remaining candidates with the most votes
+            largest_tally = max(tallies.values())
+            top_candidates = matching_keys(tallies, largest_tally)
+
+            # Reduce the found candidates if there are too many
+            if len(top_candidates | winning_candidates) > self.required_winners:
+                self.tied_winners = top_candidates.copy()
+                while len(top_candidates | winning_candidates) > self.required_winners:
+                    top_candidates.remove(self.break_ties(top_candidates, True))
+
+            # Move the top candidates into the winning pile
+            winning_candidates |= top_candidates
+            for candidate in top_candidates:
+                del tallies[candidate]
+
+        self.winners = winning_candidates
+
+    def as_dict(self):
+        data = super(PluralityAtLarge, self).as_dict()
+        data["tallies"] = self.tallies
+        return data
diff --git a/py3votecore/ranked_pairs.py b/py3votecore/ranked_pairs.py
new file mode 100644
index 00000000..4e326521
--- /dev/null
+++ b/py3votecore/ranked_pairs.py
@@ -0,0 +1,69 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from .condorcet import CondorcetSystem, CondorcetHelper
+from pygraph.classes.digraph import digraph
+from pygraph.algorithms.cycles import find_cycle
+from .common_functions import matching_keys
+from copy import deepcopy
+
+
+# This class implements the Schulze Method (aka the beatpath method)
+class RankedPairs(CondorcetSystem, CondorcetHelper):
+
+    def __init__(self, ballots, tie_breaker=None, ballot_notation=None):
+        super(RankedPairs, self).__init__(ballots, tie_breaker=tie_breaker, ballot_notation=ballot_notation)
+
+    def condorcet_completion_method(self):
+
+        # Initialize the candidate graph
+        self.rounds = []
+        graph = digraph()
+        graph.add_nodes(self.candidates)
+
+        # Loop until we've considered all possible pairs
+        remaining_strong_pairs = deepcopy(self.strong_pairs)
+        while len(remaining_strong_pairs) > 0:
+            r = {}
+
+            # Find the strongest pair
+            largest_strength = max(remaining_strong_pairs.values())
+            strongest_pairs = matching_keys(remaining_strong_pairs, largest_strength)
+            if len(strongest_pairs) > 1:
+                r["tied_pairs"] = strongest_pairs
+                strongest_pair = self.break_ties(strongest_pairs)
+            else:
+                strongest_pair = list(strongest_pairs)[0]
+            r["pair"] = strongest_pair
+
+            # If the pair would add a cycle, skip it
+            graph.add_edge(strongest_pair)
+            if len(find_cycle(graph)) > 0:
+                r["action"] = "skipped"
+                graph.del_edge(strongest_pair)
+            else:
+                r["action"] = "added"
+            del remaining_strong_pairs[strongest_pair]
+            self.rounds.append(r)
+
+        self.old_graph = self.graph
+        self.graph = graph
+        self.graph_winner()
+
+    def as_dict(self):
+        data = super(RankedPairs, self).as_dict()
+        if hasattr(self, 'rounds'):
+            data["rounds"] = self.rounds
+        return data
diff --git a/py3votecore/schulze_by_graph.py b/py3votecore/schulze_by_graph.py
new file mode 100644
index 00000000..43fa056a
--- /dev/null
+++ b/py3votecore/schulze_by_graph.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from .schulze_method import SchulzeMethod
+from .schulze_helper import SchulzeHelper
+from .abstract_classes import AbstractOrderingVotingSystem
+from pygraph.classes.digraph import digraph
+
+
+# This class provides Schulze Method results, but bypasses ballots and uses preference tallies instead.
+class SchulzeMethodByGraph(SchulzeMethod):
+
+    def __init__(self, edges, tie_breaker=None, ballot_notation=None):
+        self.edges = edges
+        super(SchulzeMethodByGraph, self).__init__([], tie_breaker=tie_breaker, ballot_notation=ballot_notation)
+
+    def standardize_ballots(self, ballots, ballot_notation):
+        self.ballots = []
+        self.candidates = set([edge[0] for edge, weight in self.edges.items()]) | set([edge[1] for edge, weight in self.edges.items()])
+
+    def ballots_into_graph(self, candidates, ballots):
+        graph = digraph()
+        graph.add_nodes(candidates)
+        for edge in self.edges.items():
+            graph.add_edge(edge[0], edge[1])
+        return graph
+
+# This class provides Schulze NPR results, but bypasses ballots and uses preference tallies instead.
+
+
+class SchulzeNPRByGraph(AbstractOrderingVotingSystem, SchulzeHelper):
+
+    def __init__(self, edges, winner_threshold=None, tie_breaker=None, ballot_notation=None):
+        self.edges = edges
+        self.candidates = set([edge[0] for edge, weight in edges.items()]) | set([edge[1] for edge, weight in edges.items()])
+        super(SchulzeNPRByGraph, self).__init__(
+            [],
+            single_winner_class=SchulzeMethodByGraph,
+            winner_threshold=winner_threshold,
+            tie_breaker=tie_breaker,
+        )
+
+    def ballots_without_candidate(self, ballots, candidate):
+        self.edges = dict([(edge, weight) for edge, weight in self.edges.items() if edge[0] != candidate and edge[1] != candidate])
+        return self.edges
+
+    def calculate_results(self):
+        self.ballots = self.edges
+        super(SchulzeNPRByGraph, self).calculate_results()
diff --git a/py3votecore/schulze_helper.py b/py3votecore/schulze_helper.py
new file mode 100644
index 00000000..5f5fbe12
--- /dev/null
+++ b/py3votecore/schulze_helper.py
@@ -0,0 +1,200 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from pygraph.algorithms.accessibility import accessibility, mutual_accessibility
+from pygraph.classes.digraph import digraph
+from pygraph.algorithms.minmax import maximum_flow
+from .condorcet import CondorcetHelper
+from .common_functions import matching_keys, unique_permutations
+
+PREFERRED_LESS = 1
+PREFERRED_SAME = 2
+PREFERRED_MORE = 3
+STRENGTH_TOLERANCE = 0.0000000001
+STRENGTH_THRESHOLD = 0.1
+NODE_SINK = -1
+NODE_SOURCE = -2
+
+# This class implements the Schulze Method (aka the beatpath method)
+
+
+class SchulzeHelper(CondorcetHelper):
+
+    def condorcet_completion_method(self):
+        self.schwartz_set_heuristic()
+
+    def schwartz_set_heuristic(self):
+
+        # Iterate through using the Schwartz set heuristic
+        self.actions = []
+        while len(self.graph.edges()) > 0:
+            access = accessibility(self.graph)
+            mutual_access = mutual_accessibility(self.graph)
+            candidates_to_remove = set()
+            for candidate in self.graph.nodes():
+                candidates_to_remove |= (set(access[candidate]) - set(mutual_access[candidate]))
+
+            # Remove nodes at the end of non-cycle paths
+            if len(candidates_to_remove) > 0:
+                self.actions.append({'nodes': candidates_to_remove})
+                for candidate in candidates_to_remove:
+                    self.graph.del_node(candidate)
+
+            # If none exist, remove the weakest edges
+            else:
+                edge_weights = self.edge_weights(self.graph)
+                self.actions.append({'edges': matching_keys(edge_weights, min(edge_weights.values()))})
+                for edge in self.actions[-1]["edges"]:
+                    self.graph.del_edge(edge)
+
+        self.graph_winner()
+
+    def generate_vote_management_graph(self):
+        self.vote_management_graph = digraph()
+        self.vote_management_graph.add_nodes(self.completed_patterns)
+        self.vote_management_graph.del_node(tuple([PREFERRED_MORE] * self.required_winners))
+        self.pattern_nodes = self.vote_management_graph.nodes()
+        self.vote_management_graph.add_nodes([NODE_SOURCE, NODE_SINK])
+        for pattern_node in self.pattern_nodes:
+            self.vote_management_graph.add_edge((NODE_SOURCE, pattern_node))
+        for i in range(self.required_winners):
+            self.vote_management_graph.add_node(i)
+        for pattern_node in self.pattern_nodes:
+            for i in range(self.required_winners):
+                if pattern_node[i] == 1:
+                    self.vote_management_graph.add_edge((pattern_node, i))
+        for i in range(self.required_winners):
+            self.vote_management_graph.add_edge((i, NODE_SINK))
+
+    # Generates a list of all patterns that do not contain indifference
+    def generate_completed_patterns(self):
+        self.completed_patterns = []
+        for i in range(0, self.required_winners + 1):
+            for pattern in unique_permutations(
+                    [PREFERRED_LESS] * (self.required_winners - i)
+                    + [PREFERRED_MORE] * (i)
+            ):
+                self.completed_patterns.append(tuple(pattern))
+
+    def proportional_completion(self, candidate, other_candidates):
+        profile = dict(list(zip(self.completed_patterns, [0] * len(self.completed_patterns))))
+
+        # Obtain an initial tally from the ballots
+        for ballot in self.ballots:
+            pattern = []
+            for other_candidate in other_candidates:
+                if ballot["ballot"][candidate] < ballot["ballot"][other_candidate]:
+                    pattern.append(PREFERRED_LESS)
+                elif ballot["ballot"][candidate] == ballot["ballot"][other_candidate]:
+                    pattern.append(PREFERRED_SAME)
+                else:
+                    pattern.append(PREFERRED_MORE)
+            pattern = tuple(pattern)
+            if pattern not in profile:
+                profile[pattern] = 0.0
+            profile[pattern] += ballot["count"]
+        weight_sum = sum(profile.values())
+
+        # Peel off patterns with indifference (from the most to the least) and apply proportional completion to them
+        while True:
+            m = max(pattern.count(PREFERRED_SAME) for pattern in profile)
+            if m == 0:
+                break
+            for pattern in list(profile.keys()):
+                if pattern.count(PREFERRED_SAME) == m:
+                    self.proportional_completion_round(pattern, profile)
+
+        try:
+            assert round(weight_sum, 5) == round(sum(profile.values()), 5)
+        except:
+            print("Proportional completion broke (went from %s to %s)" % (weight_sum, sum(profile.values())))
+
+        return profile
+
+    def proportional_completion_round(self, completion_pattern, profile):
+
+        # Remove pattern that contains indifference
+        weight_sum = sum(profile.values())
+        completion_pattern_weight = profile[completion_pattern]
+        del profile[completion_pattern]
+
+        patterns_to_consider = {}
+        for pattern in list(profile.keys()):
+            append = False
+            append_target = []
+            for i in range(len(completion_pattern)):
+                if completion_pattern[i] == PREFERRED_SAME:
+                    append_target.append(pattern[i])
+                    if pattern[i] != PREFERRED_SAME:
+                        append = True
+                else:
+                    append_target.append(completion_pattern[i])
+
+            if append is True:
+                append_target = tuple(append_target)
+                if append_target not in patterns_to_consider:
+                    patterns_to_consider[append_target] = set()
+                patterns_to_consider[append_target].add(pattern)
+
+        denominator = 0
+        for (append_target, patterns) in list(patterns_to_consider.items()):
+            for pattern in patterns:
+                denominator += profile[pattern]
+
+        # Reweight the remaining items
+        for pattern in list(patterns_to_consider.keys()):
+            if denominator == 0:
+                profile[pattern] += completion_pattern_weight / len(patterns_to_consider)
+            else:
+                if pattern not in profile:
+                    profile[pattern] = 0
+                profile[pattern] += sum(profile[considered_pattern] for considered_pattern in patterns_to_consider[pattern]) * completion_pattern_weight / denominator
+
+        try:
+            assert round(weight_sum, 5) == round(sum(profile.values()), 5)
+        except:
+            print("Proportional completion round broke (went from %s to %s)" % (weight_sum, sum(profile.values())))
+
+        return profile
+
+    # This method converts the voter profile into a capacity graph and iterates
+    # on the maximum flow using the Edmonds Karp algorithm. The end result is
+    # the limit of the strength of the voter management as per Markus Schulze's
+    # Calcul02.pdf (draft, 28 March 2008, abstract: "In this paper we illustrate
+    # the calculation of the strengths of the vote managements.").
+    def strength_of_vote_management(self, voter_profile):
+
+        # Initialize the graph weights
+        for pattern in self.pattern_nodes:
+            self.vote_management_graph.set_edge_weight((NODE_SOURCE, pattern), voter_profile[pattern])
+            for i in range(self.required_winners):
+                if pattern[i] == 1:
+                    self.vote_management_graph.set_edge_weight((pattern, i), voter_profile[pattern])
+
+        # Iterate towards the limit
+        r = [(float(sum(voter_profile.values())) - voter_profile[tuple([PREFERRED_MORE] * self.required_winners)]) / self.required_winners]
+        while len(r) < 2 or r[-2] - r[-1] > STRENGTH_TOLERANCE:
+            for i in range(self.required_winners):
+                self.vote_management_graph.set_edge_weight((i, NODE_SINK), r[-1])
+            max_flow = maximum_flow(self.vote_management_graph, NODE_SOURCE, NODE_SINK)
+            sink_sum = sum(v for k, v in max_flow[0].items() if k[1] == NODE_SINK)
+            r.append(sink_sum / self.required_winners)
+
+            # We expect strengths to be above a specified threshold
+            if sink_sum < STRENGTH_THRESHOLD:
+                return 0
+
+        # Return the final max flow
+        return round(r[-1], 9)
diff --git a/py3votecore/schulze_method.py b/py3votecore/schulze_method.py
new file mode 100644
index 00000000..c8d89c34
--- /dev/null
+++ b/py3votecore/schulze_method.py
@@ -0,0 +1,34 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from .schulze_helper import SchulzeHelper
+from .condorcet import CondorcetSystem
+
+
+# This class implements the Schulze Method (aka the beatpath method)
+class SchulzeMethod(CondorcetSystem, SchulzeHelper):
+
+    def __init__(self, ballots, tie_breaker=None, ballot_notation=None):
+        super(SchulzeMethod, self).__init__(
+            ballots,
+            tie_breaker=tie_breaker,
+            ballot_notation=ballot_notation,
+        )
+
+    def as_dict(self):
+        data = super(SchulzeMethod, self).as_dict()
+        if hasattr(self, 'actions'):
+            data["actions"] = self.actions
+        return data
diff --git a/py3votecore/schulze_npr.py b/py3votecore/schulze_npr.py
new file mode 100644
index 00000000..fb22f70c
--- /dev/null
+++ b/py3votecore/schulze_npr.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from .abstract_classes import AbstractOrderingVotingSystem
+from .schulze_helper import SchulzeHelper
+from .schulze_method import SchulzeMethod
+
+
+#
+class SchulzeNPR(AbstractOrderingVotingSystem, SchulzeHelper):
+
+    def __init__(self, ballots, winner_threshold=None, tie_breaker=None, ballot_notation=None):
+        self.standardize_ballots(ballots, ballot_notation)
+        super(SchulzeNPR, self).__init__(
+            self.ballots,
+            single_winner_class=SchulzeMethod,
+            winner_threshold=winner_threshold,
+            tie_breaker=tie_breaker,
+        )
+
+    @staticmethod
+    def ballots_without_candidate(ballots, candidate):
+        for ballot in ballots:
+            if candidate in ballot['ballot']:
+                del ballot['ballot'][candidate]
+        return ballots
diff --git a/py3votecore/schulze_pr.py b/py3votecore/schulze_pr.py
new file mode 100644
index 00000000..f91e3af8
--- /dev/null
+++ b/py3votecore/schulze_pr.py
@@ -0,0 +1,90 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+# This class implements the Schulze Proportional Ranking Method as defined
+# in schulze2.pdf
+from .schulze_helper import SchulzeHelper
+from .abstract_classes import OrderingVotingSystem
+from pygraph.classes.digraph import digraph
+
+
+class SchulzePR(OrderingVotingSystem, SchulzeHelper):
+
+    def __init__(self, ballots, tie_breaker=None, winner_threshold=None, ballot_notation=None):
+        self.standardize_ballots(ballots, ballot_notation)
+        super(SchulzePR, self).__init__(
+            self.ballots,
+            tie_breaker=tie_breaker,
+            winner_threshold=winner_threshold,
+        )
+
+    def calculate_results(self):
+
+        remaining_candidates = self.candidates.copy()
+        self.order = []
+        self.rounds = []
+
+        if self.winner_threshold is None:
+            winner_threshold = len(self.candidates)
+        else:
+            winner_threshold = min(len(self.candidates), self.winner_threshold + 1)
+
+        for self.required_winners in range(1, winner_threshold):
+
+            # Generate the list of patterns we need to complete
+            self.generate_completed_patterns()
+            self.generate_vote_management_graph()
+
+            # Generate the edges between nodes
+            self.graph = digraph()
+            self.graph.add_nodes(remaining_candidates)
+            self.winners = set([])
+            self.tied_winners = set([])
+
+            # Generate the edges between nodes
+            for candidate_from in remaining_candidates:
+                other_candidates = sorted(list(remaining_candidates - set([candidate_from])))
+                for candidate_to in other_candidates:
+                    completed = self.proportional_completion(candidate_from, set([candidate_to]) | set(self.order))
+                    weight = self.strength_of_vote_management(completed)
+                    if weight > 0:
+                        self.graph.add_edge((candidate_to, candidate_from), weight)
+
+            # Determine the round winner through the Schwartz set heuristic
+            self.schwartz_set_heuristic()
+
+            # Extract the winner and adjust the remaining candidates list
+            self.order.append(self.winner)
+            round = {"winner": self.winner}
+            if len(self.tied_winners) > 0:
+                round["tied_winners"] = self.tied_winners
+            self.rounds.append(round)
+            remaining_candidates -= set([self.winner])
+            del self.winner
+            del self.actions
+            if hasattr(self, 'tied_winners'):
+                del self.tied_winners
+
+        # Attach the last candidate as the sole winner if necessary
+        if self.winner_threshold is None or self.winner_threshold == len(self.candidates):
+            self.rounds.append({"winner": list(remaining_candidates)[0]})
+            self.order.append(list(remaining_candidates)[0])
+
+        del self.winner_threshold
+
+    def as_dict(self):
+        data = super(SchulzePR, self).as_dict()
+        data["rounds"] = self.rounds
+        return data
diff --git a/py3votecore/schulze_stv.py b/py3votecore/schulze_stv.py
new file mode 100644
index 00000000..4ae6ae99
--- /dev/null
+++ b/py3votecore/schulze_stv.py
@@ -0,0 +1,66 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+# This class implements Schulze STV, a proportional representation system
+from .abstract_classes import MultipleWinnerVotingSystem
+from .schulze_helper import SchulzeHelper
+from pygraph.classes.digraph import digraph
+import itertools
+
+
+class SchulzeSTV(MultipleWinnerVotingSystem, SchulzeHelper):
+
+    def __init__(self, ballots, tie_breaker=None, required_winners=1, ballot_notation=None):
+        self.standardize_ballots(ballots, ballot_notation)
+        super(SchulzeSTV, self).__init__(self.ballots, tie_breaker=tie_breaker, required_winners=required_winners)
+
+    def calculate_results(self):
+
+        # Don't bother if everyone's going to win
+        super(SchulzeSTV, self).calculate_results()
+        if hasattr(self, 'winners'):
+            return
+
+        # Generate the list of patterns we need to complete
+        self.generate_completed_patterns()
+        self.generate_vote_management_graph()
+
+        # Build the graph of possible winners
+        self.graph = digraph()
+        for candidate_set in itertools.combinations(self.candidates, self.required_winners):
+            self.graph.add_nodes([tuple(sorted(list(candidate_set)))])
+
+        # Generate the edges between nodes
+        for candidate_set in itertools.combinations(self.candidates, self.required_winners + 1):
+            for candidate in candidate_set:
+                other_candidates = sorted(set(candidate_set) - set([candidate]))
+                completed = self.proportional_completion(candidate, other_candidates)
+                weight = self.strength_of_vote_management(completed)
+                if weight > 0:
+                    for subset in itertools.combinations(other_candidates, len(other_candidates) - 1):
+                        self.graph.add_edge((tuple(other_candidates), tuple(sorted(list(subset) + [candidate]))), weight)
+
+        # Determine the winner through the Schwartz set heuristic
+        self.graph_winner()
+
+        # Split the "winner" into its candidate components
+        self.winners = set(self.winner)
+        del self.winner
+
+    def as_dict(self):
+        data = super(SchulzeSTV, self).as_dict()
+        if hasattr(self, 'actions'):
+            data['actions'] = self.actions
+        return data
diff --git a/py3votecore/stv.py b/py3votecore/stv.py
new file mode 100644
index 00000000..6f3266bf
--- /dev/null
+++ b/py3votecore/stv.py
@@ -0,0 +1,144 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from .abstract_classes import MultipleWinnerVotingSystem
+from collections import defaultdict
+from .common_functions import matching_keys
+import copy
+import math
+
+
+# This class implements the Single Transferable vote (aka STV) in its most
+# classic form (see http://en.wikipedia.org/wiki/Single_transferable_vote).
+# Alternate counting methods such as Meek's and Warren's would be nice, but
+# would need to be covered in a separate class.
+class STV(MultipleWinnerVotingSystem):
+
+    def __init__(self, ballots, tie_breaker=None, required_winners=1):
+        super(STV, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners)
+
+    def calculate_results(self):
+
+        self.candidates = set()
+        for ballot in self.ballots:
+            ballot["count"] = float(ballot["count"])
+            self.candidates.update(ballot["ballot"])
+        if len(self.candidates) < self.required_winners:
+            raise Exception("Not enough candidates provided")
+
+        self.quota = STV.droop_quota(self.ballots, self.required_winners)
+        self.rounds = []
+        self.winners = set()
+        quota = self.quota
+        ballots = copy.deepcopy(self.ballots)
+        remaining_candidates = self.candidates - self.winners
+
+        # Loop until we have enough candidates
+        while len(self.winners) < self.required_winners and len(remaining_candidates) + len(self.winners) != self.required_winners:
+
+            # Repopulate the remaining candidates if necessary
+            if not remaining_candidates:
+                remaining_candidates = self.candidates - self.winners
+
+            # If all the votes have been used up, start from scratch for the remaining candidates
+            round = {}
+            if len([ballot for ballot in ballots if ballot["count"] > 0 and ballot["ballot"]]) == 0:
+                remaining_candidates = self.candidates - self.winners
+                round["note"] = "reset"
+                ballots = copy.deepcopy(self.ballots)
+                for ballot in ballots:
+                    ballot["ballot"] = [x for x in ballot["ballot"] if x in remaining_candidates]
+                quota = STV.droop_quota(ballots, self.required_winners - len(self.winners))
+
+            round["tallies"] = self.tallies(ballots)
+            if round["tallies"]:
+
+                # If any candidates meet or exceeds the quota, they're a winner
+                if max(round["tallies"].values()) >= quota:
+
+                    # Collect candidates as winners
+                    round["winners"] = set([
+                        candidate
+                        for candidate, tally in list(round["tallies"].items())
+                        if tally >= self.quota
+                    ])
+                    self.winners |= round["winners"]
+                    remaining_candidates -= round["winners"]
+
+                    # Redistribute excess votes
+                    for ballot in ballots:
+                        if ballot["ballot"] and ballot["ballot"][0] in round["winners"]:
+                            ballot["count"] *= (round["tallies"][ballot["ballot"][0]] - self.quota) / round["tallies"][ballot["ballot"][0]]
+
+                    # Remove candidates from remaining ballots
+                    ballots = self.remove_candidates_from_ballots(round["winners"], ballots)
+
+                # If no candidate exceeds the quota, elimiate the least preferred
+                else:
+                    round.update(self.loser(round["tallies"]))
+                    remaining_candidates.remove(round["loser"])
+                    ballots = self.remove_candidates_from_ballots([round["loser"]], ballots)
+
+            # Record this round's actions
+            self.rounds.append(round)
+
+        # Append the final winner and return
+        if len(self.winners) < self.required_winners:
+            self.remaining_candidates = remaining_candidates
+            self.winners |= self.remaining_candidates
+
+    def as_dict(self):
+        data = super(STV, self).as_dict()
+        data["quota"] = self.quota
+        data["rounds"] = self.rounds
+        if hasattr(self, 'remaining_candidates'):
+            data["remaining_candidates"] = self.remaining_candidates
+        return data
+
+    def loser(self, tallies):
+        losers = matching_keys(tallies, min(tallies.values()))
+        if len(losers) == 1:
+            return {"loser": list(losers)[0]}
+        else:
+            return {
+                "tied_losers": losers,
+                "loser": self.break_ties(losers, True)
+            }
+
+    @staticmethod
+    def remove_candidates_from_ballots(candidates, ballots):
+        for ballot in ballots:
+            for candidate in candidates:
+                if candidate in ballot["ballot"]:
+                    ballot["ballot"].remove(candidate)
+        return ballots
+
+    def tallies(self, ballots):
+        tallies = dict()
+        for ballot in ballots:
+            for candidate in ballot["ballot"]:
+                tallies[candidate] = 0
+        for ballot in ballots:
+            if ballot["ballot"]:
+                tallies[ballot["ballot"][0]] += ballot["count"]
+        return dict((candidate, votes) for (candidate, votes) in tallies.items())
+
+    @staticmethod
+    def droop_quota(ballots, seats=1):
+        voters = 0
+        for ballot in ballots:
+            if ballot["ballot"]:
+                voters += ballot["count"]
+        return int(math.floor(voters / (seats + 1)) + 1)
diff --git a/py3votecore/tie_breaker.py b/py3votecore/tie_breaker.py
new file mode 100644
index 00000000..e85174bd
--- /dev/null
+++ b/py3votecore/tie_breaker.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2009, Brad Beattie
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see .
+
+from copy import copy
+import random
+import types
+
+
+# This class provides tie breaking methods
+class TieBreaker(object):
+
+    #
+    def __init__(self, candidate_range):
+        self.ties_broken = False
+        self.random_ordering = list(candidate_range)
+        if not isinstance(candidate_range, list):
+            random.shuffle(self.random_ordering)
+
+    #
+    def break_ties(self, tied_candidates, reverse=False):
+        self.ties_broken = True
+        random_ordering = copy(self.random_ordering)
+        if reverse:
+            random_ordering.reverse()
+        # The following line is from @gleb-chipiga
+        if isinstance(list(tied_candidates)[0], tuple):
+            result = self.break_complex_ties(tied_candidates, random_ordering)
+        else:
+            result = self.break_simple_ties(tied_candidates, random_ordering)
+        return result
+
+    #
+    @staticmethod
+    def break_simple_ties(tied_candidates, random_ordering):
+        for candidate in random_ordering:
+            if candidate in tied_candidates:
+                return candidate
+
+    #
+    @staticmethod
+    def break_complex_ties(tied_candidates, random_ordering):
+        max_columns = len(list(tied_candidates)[0])
+        column = 0
+        while len(tied_candidates) > 1 and column < max_columns:
+            min_index = min(random_ordering.index(list(candidate)[column]) for candidate in tied_candidates)
+            tied_candidates = set([candidate for candidate in tied_candidates if candidate[column] == random_ordering[min_index]])
+            column += 1
+        return list(tied_candidates)[0]
+
+    #
+    def as_list(self):
+        return self.random_ordering
+
+    #
+    def __str__(self):
+        return "[%s]" % ">".join(self.random_ordering)
diff --git a/reports/index.html b/reports/index.html
new file mode 100644
index 00000000..acbc2100
--- /dev/null
+++ b/reports/index.html
@@ -0,0 +1,61 @@
+
+
+  
+    Reports
+    
+ 
+    
+    
+ 
+    
+    
+    
+  
+  
+    
+ +
+
+
+

Python coverage

+

Code coverage for the python part of the project.

+ + + View report + +
+
+
+
+

Flake8

+

Shows violations of flake8 standards (python).

+ + + View report + +
+
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/requirements-mysql.txt b/requirements-mysql.txt new file mode 100644 index 00000000..cd2b34ae --- /dev/null +++ b/requirements-mysql.txt @@ -0,0 +1,2 @@ +# Packages required to run this project with MySQL as the DB +mysqlclient==1.4.6 diff --git a/requirements-mysql.txt.freeze b/requirements-mysql.txt.freeze new file mode 100644 index 00000000..9339b4e3 --- /dev/null +++ b/requirements-mysql.txt.freeze @@ -0,0 +1 @@ +mysqlclient==1.4.6 diff --git a/requirements-postgresql.txt b/requirements-postgresql.txt new file mode 100644 index 00000000..d98ce715 --- /dev/null +++ b/requirements-postgresql.txt @@ -0,0 +1,2 @@ +# Packages required to run this project with PostgreSQL as the DB +psycopg2 diff --git a/requirements-postgresql.txt.freeze b/requirements-postgresql.txt.freeze new file mode 100644 index 00000000..e69de29b diff --git a/requirements.txt b/requirements.txt index b59187f0..2f54e92b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,33 @@ -pillow -Django==1.8.7 -django-registration-redux -django-bootstrap-form -pymysql -lxml -suds -markdown2 -babel +# NOTE: Please try and keep this file tidy. +# Only add direct requirements here. Any sub-dependencies will be set through +# the `freeze-dependencies` make target. +# To update dependencies, optionally update this file and then +# run the `update-dependencies` make target, and + +# Django packages +Django==2.2.28 +django-bootstrap-form==3.4 +django-crispy-forms==1.8.1 +django-dotenv==1.4.2 +django-prosemirror==0.0.12 +django-registration-redux==2.7 +django-termsandconditions==2.0 +django-utils-six + +# Misc +Pillow==7.0.0 +commonmark==0.8.0 +diff_match_patch==20181111 +lxml==4.4.2 +markdown2==2.3.8 +mdmail==0.1.3 +pilkit==2.0 +python-dateutil==2.8.1 +python-graph-core==1.8.2 +requests==2.22.0 +signxml==2.7.2 +suds-jurko==0.6 + +# Unpinned (not found in production!) +dj_database_url +gunicorn diff --git a/requirements.txt.freeze b/requirements.txt.freeze new file mode 100644 index 00000000..ad73691c --- /dev/null +++ b/requirements.txt.freeze @@ -0,0 +1,54 @@ +asn1crypto==1.3.0 +backcall==0.1.0 +beautifulsoup4==4.8.2 +cachetools==4.0.0 +certifi==2019.11.28 +cffi==1.13.2 +chardet==3.0.4 +commonmark==0.8.0 +cryptography==2.8 +cssselect==1.1.0 +cssutils==1.0.2 +decorator==4.4.1 +diff-match-patch==20181111 +Django==2.2.28 +django-bootstrap-form==3.4 +django-crispy-forms==1.8.1 +django-dotenv==1.4.2 +django-prosemirror==0.0.12 +django-registration-redux==2.7 +django-termsandconditions==2.0 +eight==1.0.0 +emails==0.5.15 +future==0.18.2 +idna==2.8 +ipython==7.9.0 +ipython-genutils==0.2.0 +jedi==0.16.0 +lxml==4.4.2 +Markdown==3.1.1 +markdown2==2.3.8 +mdmail==0.1.3 +parso==0.6.1 +pexpect==4.8.0 +pickleshare==0.7.5 +pilkit==2.0 +Pillow==7.0.0 +premailer==3.6.1 +prompt-toolkit==2.0.10 +ptyprocess==0.6.0 +pycparser==2.19 +Pygments==2.5.2 +pyOpenSSL==19.1.0 +python-dateutil==2.8.1 +python-graph-core==1.8.2 +pytz==2019.3 +requests==2.22.0 +signxml==2.7.2 +six==1.14.0 +soupsieve==1.9.5 +sqlparse==0.3.0 +suds-jurko==0.6 +traitlets==4.3.3 +urllib3==1.25.7 +wcwidth==0.1.8 diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 00000000..3e4835ce --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.8.7 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..1509a7ba --- /dev/null +++ b/setup.cfg @@ -0,0 +1,36 @@ +[coverage:run] +omit = + *site-packages* + *migrations* + *node_modules* + +[coverage:report] +omit = + *site-packages* + *migrations* + *node_modules* +exclude_lines = + pragma: no cover + def __unicode__ + def __repr__ + if self\.debug + raise AssertionError + raise NotImplementedError + if 0: + +[coverage:html] +extra_css = cover.css +directory = reports/htmlcov + +[flake8] +exclude = + node_modules + .git, + __pycache__, + env, + .env, + venv, + .venv, +max_line_length = 120 +tee = True + diff --git a/tag.sh b/tag.sh new file mode 100755 index 00000000..ad802ef3 --- /dev/null +++ b/tag.sh @@ -0,0 +1,8 @@ +#!/bin/bash +if [ "$TRAVIS_BRANCH" == "master" ]; then + CURR_VERSION=$(cat VERSION) + git config --global user.email "builds@travis-ci.com" + git config --global user.name "Travis CI" + git tag "$CURR_VERSION" + git push --tags --quiet "https://${GH_TOKEN}@github.com/piratar/wasa2il.git" +fi \ No newline at end of file diff --git a/tasks/__init__.py b/tasks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/forms.py b/tasks/forms.py new file mode 100644 index 00000000..1a07be29 --- /dev/null +++ b/tasks/forms.py @@ -0,0 +1,30 @@ +from wasa2il.forms import Wasa2ilForm +from django.forms import CharField +from django.forms import Textarea +from django.utils.translation import ugettext_lazy as _ + +from tasks.models import Task + +class TaskForm(Wasa2ilForm): + short_description = CharField( + widget=Textarea(attrs={'rows': 2}), + label=_('Short description'), + max_length=200, + help_text=_('Clearly state the objective of the task. Maximum 200 letters.') + ) + + class Meta: + model = Task + exclude = ( + 'polity', + 'is_done', + 'created_by', + 'modified_by', + 'created', + 'modified', + 'slug', + 'categories', + 'skills', + ) + + diff --git a/tasks/migrations/0001_initial.py b/tasks/migrations/0001_initial.py new file mode 100644 index 00000000..eb1613d7 --- /dev/null +++ b/tasks/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-07-12 14:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('polity', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('slug', models.SlugField(blank=True, max_length=128)), + ('description', models.TextField(verbose_name='Description')), + ('objectives', models.TextField(verbose_name='Objectives')), + ('requirements', models.TextField(verbose_name='Requirements')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('volunteers_needed', models.IntegerField(default=1)), + ('estimated_hours_per_week', models.IntegerField(default=1)), + ('estimated_duration_weeks', models.IntegerField(default=1)), + ('is_done', models.BooleanField(default=False)), + ('is_recruiting', models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name='TaskCategory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ], + ), + migrations.CreateModel( + name='TaskRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_offered', models.DateTimeField(auto_now_add=True)), + ('is_accepted', models.BooleanField(default=False)), + ('whyme', models.TextField(verbose_name='Why me?')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.Task')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='TaskSkill', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ], + ), + migrations.AddField( + model_name='task', + name='categories', + field=models.ManyToManyField(to='tasks.TaskCategory'), + ), + migrations.AddField( + model_name='task', + name='created_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='task_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='task', + name='modified_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='task_modified_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='task', + name='polity', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity'), + ), + migrations.AddField( + model_name='task', + name='skills', + field=models.ManyToManyField(to='tasks.TaskSkill'), + ), + ] diff --git a/tasks/migrations/0002_auto_20190730_2053.py b/tasks/migrations/0002_auto_20190730_2053.py new file mode 100644 index 00000000..b66be329 --- /dev/null +++ b/tasks/migrations/0002_auto_20190730_2053.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-07-30 20:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='estimated_duration_weeks', + field=models.IntegerField(default=1, verbose_name='Estimated number of weeks'), + ), + migrations.AlterField( + model_name='task', + name='estimated_hours_per_week', + field=models.IntegerField(default=1, verbose_name='Estimated hours per week'), + ), + migrations.AlterField( + model_name='task', + name='is_recruiting', + field=models.BooleanField(default=True, verbose_name='Is recruiting'), + ), + migrations.AlterField( + model_name='task', + name='volunteers_needed', + field=models.IntegerField(default=1, verbose_name='Number of volunteers needed'), + ), + ] diff --git a/tasks/migrations/0003_taskrequest_available_time.py b/tasks/migrations/0003_taskrequest_available_time.py new file mode 100644 index 00000000..53a4c1f7 --- /dev/null +++ b/tasks/migrations/0003_taskrequest_available_time.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2019-07-30 23:50 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0002_auto_20190730_2053'), + ] + + operations = [ + migrations.AddField( + model_name='taskrequest', + name='available_time', + field=models.TextField(default='', verbose_name='What available time do I have?'), + preserve_default=False, + ), + ] diff --git a/tasks/migrations/0004_auto_20190822_2006.py b/tasks/migrations/0004_auto_20190822_2006.py new file mode 100644 index 00000000..65175149 --- /dev/null +++ b/tasks/migrations/0004_auto_20190822_2006.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.4 on 2019-08-22 20:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0003_taskrequest_available_time'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='created_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='task', + name='modified_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_modified_by', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/tasks/migrations/0004_auto_20190831_1623.py b/tasks/migrations/0004_auto_20190831_1623.py new file mode 100644 index 00000000..20f114ba --- /dev/null +++ b/tasks/migrations/0004_auto_20190831_1623.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-08-31 16:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0003_taskrequest_available_time'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='objectives', + field=models.CharField(max_length=200, verbose_name='Objectives'), + ), + ] diff --git a/tasks/migrations/0005_auto_20190831_1723.py b/tasks/migrations/0005_auto_20190831_1723.py new file mode 100644 index 00000000..8f5c9647 --- /dev/null +++ b/tasks/migrations/0005_auto_20190831_1723.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-08-31 17:23 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0004_auto_20190831_1623'), + ] + + operations = [ + migrations.RenameField( + model_name='task', + old_name='description', + new_name='detailed_description', + ), + migrations.RenameField( + model_name='task', + old_name='objectives', + new_name='short_description', + ), + ] diff --git a/tasks/migrations/0006_auto_20190831_1804.py b/tasks/migrations/0006_auto_20190831_1804.py new file mode 100644 index 00000000..0d600e4f --- /dev/null +++ b/tasks/migrations/0006_auto_20190831_1804.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-08-31 18:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0005_auto_20190831_1723'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='detailed_description', + field=models.TextField(blank=True, null=True, verbose_name='Detailed description'), + ), + migrations.AlterField( + model_name='task', + name='estimated_duration_weeks', + field=models.IntegerField(default=1, help_text='Select 0 if not applicable.', verbose_name='Estimated number of weeks'), + ), + migrations.AlterField( + model_name='task', + name='estimated_hours_per_week', + field=models.IntegerField(default=1, help_text='Select 0 if not applicable.', verbose_name='Estimated hours per week'), + ), + migrations.AlterField( + model_name='task', + name='requirements', + field=models.TextField(blank=True, null=True, verbose_name='Requirements'), + ), + migrations.AlterField( + model_name='task', + name='short_description', + field=models.CharField(max_length=200, verbose_name='Short description'), + ), + migrations.AlterField( + model_name='task', + name='volunteers_needed', + field=models.IntegerField(default=1, help_text='Select 0 if not applicable.', verbose_name='Number of volunteers needed'), + ), + ] diff --git a/tasks/migrations/0007_task_require_phone.py b/tasks/migrations/0007_task_require_phone.py new file mode 100644 index 00000000..8e3fb10f --- /dev/null +++ b/tasks/migrations/0007_task_require_phone.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-09-01 14:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0006_auto_20190831_1804'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='require_phone', + field=models.BooleanField(default=True, help_text='Make users provide their phone numbers in the profiles to partake in the task.', verbose_name='Require phone number from volunteers'), + ), + ] diff --git a/tasks/migrations/0008_merge_20191019_1939.py b/tasks/migrations/0008_merge_20191019_1939.py new file mode 100644 index 00000000..7efc2b2e --- /dev/null +++ b/tasks/migrations/0008_merge_20191019_1939.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.6 on 2019-10-19 19:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0004_auto_20190822_2006'), + ('tasks', '0007_task_require_phone'), + ] + + operations = [ + ] diff --git a/tasks/migrations/0009_auto_20220217_0934.py b/tasks/migrations/0009_auto_20220217_0934.py new file mode 100644 index 00000000..6e4a19b2 --- /dev/null +++ b/tasks/migrations/0009_auto_20220217_0934.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.25 on 2022-02-17 09:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0008_merge_20191019_1939'), + ] + + operations = [ + migrations.AlterModelOptions( + name='task', + options={'ordering': ['-created']}, + ), + ] diff --git a/tasks/migrations/__init__.py b/tasks/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/models.py b/tasks/models.py new file mode 100644 index 00000000..920a1162 --- /dev/null +++ b/tasks/models.py @@ -0,0 +1,113 @@ +from django.conf import settings +from django.db import models +from django.db.models import CASCADE +from django.db.models import SET_NULL + +from django.utils.translation import ugettext_lazy as _ + + +TASK_CATEGORIES = ( + (1, _('Equality')), + (2, _('Education')), + (3, _('Justice and Law Enforcement')), + (4, _('Economy')), + (5, _('Healthcare')), + (6, _('Housing')), + (7, _('Welfare')), + (8, _('Culture')), + (9, _('Industry')), + (10, _('Public finances')), + (11, _('Transportation')), + (12, _('Governance')), + (13, _('Municipal affairs')), + (14, _('Environment')), + (15, _('Foreign affairs')), + (16, _('Defence')), +) + +TASK_SKILLS = ( + (1, _('Physical work')), + (2, _('Training')), + (3, _('Design')), + (4, _('Research')), + (5, _('Volunteer coordination')), + (6, _('Grassroots organizing')), + (7, _('Legal writing')), + (8, _('Writing and editing')), + (9, _('Policy drafting')), + (10, _('Public representation')), + (11, _('Technical development')), + (12, _('Management')), + (13, _('Planning and execution')), + (14, _('Translating')) +) + +class Task(models.Model): + polity = models.ForeignKey('polity.Polity', on_delete=CASCADE) + categories = models.ManyToManyField('tasks.TaskCategory') + skills = models.ManyToManyField('tasks.TaskSkill') + + name = models.CharField(max_length=128, verbose_name=_('Name')) + slug = models.SlugField(max_length=128, blank=True) + + short_description = models.CharField(max_length=200, verbose_name=_("Short description")) + detailed_description = models.TextField(verbose_name=_("Detailed description"), null=True, blank=True) + requirements = models.TextField(verbose_name=_("Requirements"), null=True, blank=True) + + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + null=True, + blank=True, + related_name='task_created_by', + on_delete=SET_NULL + ) + modified_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + null=True, + blank=True, + related_name='task_modified_by', + on_delete=SET_NULL + ) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + volunteers_needed = models.IntegerField(default=1, verbose_name=_('Number of volunteers needed'), help_text=_('Select 0 if not applicable.')) + estimated_hours_per_week = models.IntegerField(default=1, verbose_name=_('Estimated hours per week'), help_text=_('Select 0 if not applicable.')) + estimated_duration_weeks = models.IntegerField(default=1, verbose_name=_('Estimated number of weeks'), help_text=_('Select 0 if not applicable.')) + + require_phone = models.BooleanField(default=True, verbose_name=_('Require phone number from volunteers'), help_text=_('Make users provide their phone numbers in the profiles to partake in the task.')) + + is_done = models.BooleanField(default=False) + is_recruiting = models.BooleanField(default=True, verbose_name=_('Is recruiting')) + + def accepted_volunteers(self): + return self.taskrequest_set.filter(is_accepted=True).select_related('user') + + def applied_volunteers(self): + return self.taskrequest_set.select_related('user') + + class Meta: + ordering = ['-created'] + + +class TaskCategory(models.Model): + name = models.CharField(max_length=128) + + def __str__(self): + return self.name + +class TaskSkill(models.Model): + name = models.CharField(max_length=128) + + def __str__(self): + return self.name + +class TaskRequest(models.Model): + task = models.ForeignKey('tasks.Task', on_delete=CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE) + date_offered = models.DateTimeField(auto_now_add=True) + is_accepted = models.BooleanField(default=False) + whyme = models.TextField(verbose_name=_("Why me?")) + available_time = models.TextField(verbose_name=_('What available time do I have?')) diff --git a/tasks/urls.py b/tasks/urls.py new file mode 100644 index 00000000..8b8db066 --- /dev/null +++ b/tasks/urls.py @@ -0,0 +1,22 @@ +from django.conf.urls import url +from django.contrib.auth.decorators import login_required +from django.views.decorators.cache import never_cache + +from tasks.models import Task +from tasks.views import task_main +from tasks.views import task_add_edit +from tasks.views import task_delete +from tasks.views import task_detail +from tasks.views import task_applications +from tasks.views import task_user_tasks + +urlpatterns = [ + url(r'^polity/(?P\d+)/tasks/$', task_main, name='task_main'), + url(r'^polity/(?P\d+)/tasks/(?P\d+)/edit/$', task_add_edit, name='task_edit'), + url(r'^polity/(?P\d+)/tasks/(?P\d+)/delete/$', task_delete, name='task_delete'), + url(r'^polity/(?P\d+)/tasks/new/$', task_add_edit, name='task_add'), + url(r'^polity/(?P\d+)/tasks/(?P\d+)/$', task_detail, name='task_detail'), + url(r'^polity/(?P\d+)/tasks/applications/$', task_applications, name='task_applications'), + + url(r'^accounts/profile/(?:(?P[^/]+)/tasks/)?$', task_user_tasks, name='task_user_tasks'), +] diff --git a/tasks/views.py b/tasks/views.py new file mode 100644 index 00000000..08b47fe2 --- /dev/null +++ b/tasks/views.py @@ -0,0 +1,207 @@ +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django.db.models import Prefetch +from django.db.models import Count +from django.db.models import Q +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render +from django.urls import reverse + +from core.models import User +from tasks.models import Task, TaskRequest +from polity.models import Polity +from tasks.forms import TaskForm + +def task_main(request, polity_id): + polity = get_object_or_404(Polity, id=polity_id) + + # Basic attributes of tasks we're interested in. + tasks = Task.objects.filter(is_recruiting=True, is_done=False) + + # Front polity's tasks are always shown. + front_tasks = tasks.filter( + polity__is_front_polity=True + ) + + # Sub-polity's tasks are only shown if they exist. + sub_polity_tasks = tasks.filter( + polity_id=polity_id, + polity__is_front_polity=False + ) + + total_task_count = len(front_tasks) + len(sub_polity_tasks) + + ctx = { + 'front_tasks': front_tasks, + 'sub_polity_tasks': sub_polity_tasks, + 'total_task_count': total_task_count, + } + return render(request, 'tasks/task_main.html', ctx) + + +@login_required +def task_user_tasks(request, username): + # Username is an optional parameter in anticipation of a future feature + # where a user can, at least under some circumstances (having gained + # permission, for example, or if a user chooses to make his/her + # participation public) can view the tasks of another user. It is + # currently not used but still required to make sure that links are + # created with this specification in mind. + + taskrequests = TaskRequest.objects.select_related( + 'task' + ).prefetch_related( + 'task__skills', + 'task__categories' + ).filter( + user_id=request.user.id + ) + + tasks = [req.task for req in taskrequests] + + ctx = { + 'tasks': tasks, + } + return render(request, 'tasks/task_user_tasks.html', ctx) + + +@login_required +def task_add_edit(request, polity_id, task_id=None): + polity = get_object_or_404(Polity, id=polity_id) + if not (polity.is_member(request.user) or polity.is_wrangler(request.user)): + raise PermissionDenied() + + if task_id: + task = get_object_or_404(Task, id=task_id, polity=polity) + # We don't want to edit anything that has already done. + if task.is_done: + raise PermissionDenied() + else: + task = Task(polity=polity) + + if request.method == 'POST': + form = TaskForm(request.POST, instance=task) + if form.is_valid(): + task = form.save() + return redirect(reverse('task_detail', args=(polity_id, task.id))) + else: + form = TaskForm(instance=task) + + ctx = { + 'task': task, + 'form': form, + } + return render(request, 'tasks/task_add_edit.html', ctx) + + +@login_required +def task_delete(request, polity_id, task_id): + if not request.globals['user_is_wrangler']: + raise PermissionDenied() + + if request.method == 'POST': + task = Task.objects.get(polity_id=polity_id, id=task_id) + task.delete() + return redirect(reverse('task_main', args=(polity_id,))) + else: + raise Http404 + + +def task_detail(request, polity_id, task_id): + task = get_object_or_404(Task, id=task_id, polity_id=polity_id) + user = request.user + + # Defaults. Altered if logged in. + has_applied = False + phone_required = False + + if user.is_authenticated: + + polity = get_object_or_404(Polity, id=polity_id) + + has_applied = task.taskrequest_set.filter(user=user).count() > 0 + phone_required = task.require_phone and not user.userprofile.phone + + if request.method == 'POST' and not has_applied and not phone_required: + whyme = request.POST.get('whyme') + available_time = request.POST.get('available_time') + if whyme.strip() != '': + tr = TaskRequest() + tr.task = task + tr.user = request.user + tr.whyme = whyme + tr.available_time = available_time + tr.save() + has_applied = True + + ctx = { + 'task': task, + 'has_applied': has_applied, + 'phone_required': phone_required, + } + return render(request, 'tasks/task_detail.html', ctx) + + +def task_applications(request, polity_id): + polity = get_object_or_404(Polity, id=polity_id) + if not (polity.is_member(request.user) or polity.is_wrangler(request.user)): + raise PermissionDenied() + + done = request.POST.get('done', None) + notdone = request.POST.get('notdone', None) + accept = request.POST.get('accept', None) + reject = request.POST.get('reject', None) + stoprecruiting = request.POST.get('stoprecruiting', None) + startrecruiting = request.POST.get('startrecruiting', None) + + if done: + tr = get_object_or_404(Task, id=done) + tr.is_done = True + tr.save() + + if notdone: + tr = get_object_or_404(Task, id=notdone) + tr.is_done = False + tr.save() + + if stoprecruiting: + tr = get_object_or_404(Task, id=stoprecruiting) + tr.is_recruiting = False + tr.save() + + if startrecruiting: + tr = get_object_or_404(Task, id=startrecruiting) + tr.is_recruiting = True + tr.save() + + if accept: + tr = get_object_or_404(TaskRequest, id=accept) + tr.is_accepted = True + tr.save() + + if reject: + tr = get_object_or_404(TaskRequest, id=reject) + tr.is_accepted = False + tr.save() + + show_done = bool(int(request.GET.get('showdone', 0))) + + tasks = polity.task_set.prefetch_related( + # Prefetch the data for the User model that we need to determine statistics and such. + Prefetch( + 'taskrequest_set__user', + queryset=User.objects.annotate_task_stats() + ), + 'taskrequest_set__user__userprofile' + ).order_by('-created') + if not show_done: + tasks = tasks.filter(is_done=False) + + ctx = { + 'polity': polity, + 'tasks': tasks, + 'show_done': show_done, + } + return render(request, 'tasks/task_applications.html', ctx) diff --git a/test_data/condorcet_cycle.json b/test_data/condorcet_cycle.json new file mode 100644 index 00000000..8c30c7ad --- /dev/null +++ b/test_data/condorcet_cycle.json @@ -0,0 +1,5 @@ +[ + [[1, "Bjarni"], [2, "Smari"], [3, "Bjorn"]], + [[2, "Bjarni"], [3, "Smari"], [1, "Bjorn"]], + [[3, "Bjarni"], [1, "Smari"], [2, "Bjorn"]] +] diff --git a/test_data/election.json b/test_data/election.json new file mode 100644 index 00000000..0d045f21 --- /dev/null +++ b/test_data/election.json @@ -0,0 +1,7 @@ +[ + [[1, "Bjarni"], [2, "Smari"], [3, "Bjorn"]], + [[2, "Bjarni"], [1, "Smari"], [3, "Bjorn"]], + [[3, "Bjarni"], [2, "Smari"], [1, "Bjorn"]], + [[3, "Bjarni"], [2, "Smari"], [1, "Bjorn"]], + [[3, "Bjarni"], [2, "Smari"], [1, "Bjorn"]] +] diff --git a/test_data/steering_committee.json b/test_data/steering_committee.json new file mode 100644 index 00000000..b8b115f7 --- /dev/null +++ b/test_data/steering_committee.json @@ -0,0 +1,5 @@ +[ + [[1,"A"], [2,"B"], [3,"C"], [4,"D"], [5,"E"], [6,"F"], [7,"G"], [8,"H"], [9,"I"], [10,"J"], [11,"K"], [12,"L"]], + [[1,"B"], [2,"C"], [3,"D"], [4,"E"], [5,"L"], [6,"F"], [7,"G"], [8,"H"], [9,"I"], [10,"J"], [11,"K"], [12,"A"]], + [[1,"C"], [2,"D"], [3,"E"], [4,"L"], [5,"B"], [6,"F"], [7,"G"], [8,"H"], [9,"I"], [10,"J"], [11,"K"], [12,"A"]] +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 00000000..240f45ef --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,43 @@ +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth.models import User + +class ViewTests(TestCase): + + @classmethod + def setUpTestData(cls): + # Runs once to set up non-modified data for all class methods. + print('---- Running tests/test_views.py ----') + pass + + def setUp(self): + # Run once for every test method to setup clean data. + user1 = User.objects.create_user(username='user1', password='password') + user1.save() + + + def test_non_endpoints(self): + response = self.client.get('/this-is-not-an-endpoint', follow=True) + self.assertEqual(response.status_code, 404) + + def test_root_endpoint(self): + response = self.client.get('/') + self.assertEqual(response.status_code, 200) + + def test_admin_endpoint(self): + response = self.client.get('/admin/login/') + self.assertEqual(response.status_code, 200) + + def test_redirect_if_not_logged_in(self): + response = self.client.get('/accounts/profile/') + self.assertRedirects(response, '/accounts/login/?next=/accounts/profile/') + + def test_user_can_log_in(self): + login = self.client.login(username='user1', password='password') + response = self.client.get(reverse('polities')) + # Check our user is logged in + self.assertEqual(str(response.context['user']), 'user1') + # Check that we got a response "success" + self.assertEqual(response.status_code, 200) + response = self.client.get('/accounts/profile/') + self.assertEqual(response.status_code, 200) diff --git a/topic/__init__.py b/topic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/topic/admin.py b/topic/admin.py new file mode 100644 index 00000000..5ccd9e97 --- /dev/null +++ b/topic/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from topic.models import Topic + +class TopicAdmin(admin.ModelAdmin): + fieldsets = None + list_display = ['name', 'slug', 'description', 'polity'] + + +register = admin.site.register +register(Topic) diff --git a/topic/dataviews.py b/topic/dataviews.py new file mode 100644 index 00000000..f722b9fb --- /dev/null +++ b/topic/dataviews.py @@ -0,0 +1,63 @@ +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404 +from django.template.loader import render_to_string + +from core.ajax.utils import jsonize +from core.models import UserProfile + +from polity.models import Polity + +from topic.models import Topic +from topic.models import UserTopic + + +@login_required +@jsonize +def topic_star(request): + ctx = {} + topicid = int(request.GET.get('topic', 0)) + if not topicid: + ctx["ok"] = False + return ctx + + topic = get_object_or_404(Topic, id=topicid) + + ctx["topic"] = topic.id + + try: + ut = UserTopic.objects.get(topic=topic, user=request.user) + ut.delete() + ctx["starred"] = False + except UserTopic.DoesNotExist: + UserTopic(topic=topic, user=request.user).save() + ctx["starred"] = True + + topics = topic.polity.topic_set.listing_info(request.user) + ctx["html"] = render_to_string("topic/_topic_list_table.html", {"topics": topics, "user": request.user, "polity": topic.polity}) + + ctx["ok"] = True + + return ctx + + +@login_required +@jsonize +def topic_showstarred(request): + ctx = {} + profile = UserProfile.objects.get(user=request.user) + profile.topics_showall = not profile.topics_showall + profile.save() + + ctx["showstarred"] = not profile.topics_showall + + polity = int(request.GET.get("polity", 0)) + if polity: + try: + polity = Polity.objects.get(id=polity) + topics = polity.topic_set.listing_info(request.user) + ctx["html"] = render_to_string("topic/_topic_list_table.html", {"topics": topics, "user": request.user, "polity": polity}) + except Exception as e: + ctx["error"] = e + + ctx["ok"] = True + return ctx diff --git a/topic/forms.py b/topic/forms.py new file mode 100644 index 00000000..4c74f922 --- /dev/null +++ b/topic/forms.py @@ -0,0 +1,9 @@ +from wasa2il.forms import Wasa2ilForm + +from topic.models import Topic + + +class TopicForm(Wasa2ilForm): + class Meta: + model = Topic + exclude = ('polity', 'slug') diff --git a/topic/migrations/0001_initial.py b/topic/migrations/0001_initial.py new file mode 100644 index 00000000..61ff2afb --- /dev/null +++ b/topic/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-07-12 14:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('polity', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Topic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('slug', models.SlugField(blank=True, max_length=128)), + ('description', models.TextField(blank=True, null=True, verbose_name='Description')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('created_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topic_created_by', to=settings.AUTH_USER_MODEL)), + ('modified_by', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='topic_modified_by', to=settings.AUTH_USER_MODEL)), + ('polity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polity.Polity')), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='UserTopic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='topic.Topic')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AlterUniqueTogether( + name='usertopic', + unique_together=set([('topic', 'user')]), + ), + ] diff --git a/topic/migrations/0002_auto_20190822_2006.py b/topic/migrations/0002_auto_20190822_2006.py new file mode 100644 index 00000000..e4f80b18 --- /dev/null +++ b/topic/migrations/0002_auto_20190822_2006.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.4 on 2019-08-22 20:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('topic', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='topic', + name='created_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='topic_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='topic', + name='modified_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='topic_modified_by', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/topic/migrations/__init__.py b/topic/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/topic/models.py b/topic/models.py new file mode 100644 index 00000000..6c0b5f12 --- /dev/null +++ b/topic/models.py @@ -0,0 +1,118 @@ +from datetime import datetime + +from django.conf import settings +from django.db import models +from django.db.models import BooleanField +from django.db.models import CASCADE +from django.db.models import Case +from django.db.models import Count +from django.db.models import IntegerField +from django.db.models import Q +from django.db.models import SET_NULL +from django.db.models import When +from django.utils.translation import ugettext_lazy as _ + +from core.models import UserProfile + + +class TopicQuerySet(models.QuerySet): + def listing_info(self, user): + ''' + Adds information relevant to listing of topics + ''' + + topics = self + now = datetime.now() + + if not user.is_anonymous: + if not UserProfile.objects.get(user=user).topics_showall: + topics = topics.filter(usertopic__user=user) + + # Annotate the user's favoriteness of topics. Note that even though + # it's intended as a boolean, it is actually produced as an integer. + # So it's 1/0, not True/False. + if not user.is_anonymous: + topics = topics.annotate( + favorited=Count( + Case( + When(usertopic__user=user, then=True), + output_field=BooleanField + ), + distinct=True + ) + ) + + # Annotate issue count. + topics = topics.annotate(issue_count=Count('issue', distinct=True)) + + # Annotate usertopic count. + topics = topics.annotate(usertopic_count=Count('usertopic', distinct=True)) + + # Annotate counts of issues that are open and/or being voted on. + topics = topics.annotate( + issues_open=Count( + Case( + When(issue__deadline_votes__gte=now, then=True), + output_field=IntegerField() + ), + distinct=True + ), + issues_voting=Count( + Case( + When(Q(issue__deadline_votes__gte=now, issue__deadline_proposals__lt=now), then=True), + output_field=IntegerField() + ), + distinct=True + ) + ) + + return topics + + +class Topic(models.Model): + """A collection of issues unified categorically.""" + objects = TopicQuerySet.as_manager() + + name = models.CharField(max_length=128, verbose_name=_('Name')) + slug = models.SlugField(max_length=128, blank=True) + + description = models.TextField(verbose_name=_("Description"), null=True, blank=True) + + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + null=True, + blank=True, + related_name='topic_created_by', + on_delete=SET_NULL + ) + modified_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + null=True, + blank=True, + related_name='topic_modified_by', + on_delete=SET_NULL + ) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + polity = models.ForeignKey('polity.Polity', on_delete=CASCADE) + + class Meta: + ordering = ["name"] + + def new_comments(self): + return Comment.objects.filter(issue__topics=self).order_by("-created")[:10] + + def __str__(self): + return u'%s' % (self.name) + + +class UserTopic(models.Model): + """Whether a user likes a topic.""" + topic = models.ForeignKey('topic.Topic', on_delete=CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE) + + class Meta: + unique_together = (("topic", "user"),) diff --git a/topic/tests.py b/topic/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/topic/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/topic/urls.py b/topic/urls.py new file mode 100644 index 00000000..bef5abe2 --- /dev/null +++ b/topic/urls.py @@ -0,0 +1,20 @@ +from django.conf.urls import url +from django.contrib.auth.decorators import login_required + +from topic.dataviews import topic_showstarred +from topic.dataviews import topic_star +from topic.views import topic_add_edit +from topic.views import topic_view +from topic.views import topic_list + + +urlpatterns = [ + url(r'^polity/(?P\d+)/topic/new/$', topic_add_edit, name='topic_add'), + url(r'^polity/(?P\d+)/topic/(?P\d+)/edit/$', topic_add_edit, name='topic_edit'), + url(r'^polity/(?P\d+)/topic/(?P\d+)/$', topic_view, name='topic'), + url(r'^polity/(?P\d+)/topics/$', topic_list, name='topics'), + + + url(r'^api/topic/star/$', topic_star), + url(r'^api/topic/showstarred/$', topic_showstarred), +] diff --git a/topic/views.py b/topic/views.py new file mode 100644 index 00000000..80dcc011 --- /dev/null +++ b/topic/views.py @@ -0,0 +1,59 @@ +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render +from django.urls import reverse + +from polity.models import Polity + +from topic.forms import TopicForm +from topic.models import Topic + + +@login_required +def topic_add_edit(request, polity_id, topic_id=None): + try: + polity = Polity.objects.get(id=polity_id, officers=request.user) + except Polity.DoesNotExist: + raise PermissionDenied() + + if topic_id: + topic = get_object_or_404(Topic, id=topic_id, polity_id=polity_id) + else: + topic = Topic(polity=polity) + + if request.method == 'POST': + form = TopicForm(request.POST, instance=topic) + if form.is_valid(): + topic = form.save() + return redirect(reverse('topic', args=(polity_id, topic.id))) + else: + form = TopicForm(instance=topic) + + ctx = { + 'polity': polity, + 'topic': topic, + 'form': form, + } + return render(request, 'topic/topic_form.html', ctx) + + +def topic_view(request, polity_id, topic_id): + polity = get_object_or_404(Polity, id=polity_id) + topic = get_object_or_404(Topic, id=topic_id, polity_id=polity_id) + + ctx = { + 'polity': polity, + 'topic': topic, + } + return render(request, 'topic/topic_detail.html', ctx) + +def topic_list(request, polity_id): + polity = get_object_or_404(Polity, id=polity_id) + + ctx = { + 'polity': polity, + 'politytopics': polity.topic_set.listing_info(request.user).all(), + } + return render(request, 'topic/topic_list.html', ctx) diff --git a/urls.py b/urls.py new file mode 100644 index 00000000..268f8bb2 --- /dev/null +++ b/urls.py @@ -0,0 +1,87 @@ +from django.conf.urls import include, url +from django.shortcuts import redirect +from django.conf import settings +from django.conf.urls import handler500 +from django.views.generic import TemplateView +from django.contrib.auth import views as auth_views +from django.urls import reverse_lazy +from django.views import static + +from registration.backends.default.views import RegistrationView + +from core import views as core_views + +from django.contrib import admin + +urlpatterns = [ + # Uncomment the admin/doc line below to enable admin documentation: + url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + # Uncomment the next line to enable the admin: + url(r'^admin/', admin.site.urls), + url(r'^admintools/$', core_views.view_admintools, name='admin_tools'), + url(r'^admintools/push/$', core_views.view_admintools_push, name='admin_tools_push'), + # Enabling i18n language changes per + # https://docs.djangoproject.com/en/1.4/topics/i18n/translation/#the-set-language-redirect-view + url(r'^i18n/', include('django.conf.urls.i18n')), + + url(r'^', include('election.urls')), + url(r'^', include('issue.urls')), + url(r'^', include('core.urls')), + url(r'^', include('polity.urls')), + url(r'^', include('topic.urls')), + url(r'^', include('emailconfirmation.urls')), + + url(r'^accounts/profile/(?:(?P[^/]+)/)?$', core_views.profile, name='profile'), + url(r'^accounts/settings/', core_views.view_settings, name='account_settings'), + url(r'^accounts/personal-data/fetch/', core_views.personal_data_fetch, name='personal_data_fetch'), + url(r'^accounts/personal-data/', core_views.personal_data, name='personal_data'), + + url(r'^accounts/sso/', core_views.sso), + url(r'^accounts/register/$', core_views.Wasa2ilRegistrationView.as_view(), name='registration_register'), + url( + r'^accounts/activate/(?P\w+)/$', + core_views.Wasa2ilActivationView.as_view(), + name='registration_activate' + ), + url( + r'^accounts/password/reset/$', + auth_views.PasswordResetView.as_view( + email_template_name='registration/password_reset_email.txt', + html_email_template_name='registration/password_reset_email.html' + ), + name='auth_password_reset' + ), + url(r'^accounts/reset-password/done/$', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), + + # SAML-related URLs. + url(r'^accounts/verify/', core_views.verify), + url(r'^accounts/login-or-saml-redirect/', core_views.login_or_saml_redirect, name='login_or_saml_redirect'), + + url(r'^accounts/', include('registration.backends.default.urls')), + + url(r'^help/$', TemplateView.as_view(template_name='help/is/index.html')), + url(r'^help/(?P.*)/$', core_views.help), + + url(r'^static/(?P.*)$', static.serve, {'document_root': settings.STATIC_ROOT}), +] + +if settings.FEATURES['tasks']: + urlpatterns.append(url(r'^', include('tasks.urls'))) + + +handler500 = 'core.views.error500' + +if settings.DEBUG: + urlpatterns += [ + url(r'^uploads/(?P.*)$', static.serve, { + 'document_root': settings.MEDIA_ROOT, + }), + ] + if 'debug_toolbar.apps.DebugToolbarConfig' in settings.INSTALLED_APPS: + try: + import debug_toolbar + urlpatterns += [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + except: + pass diff --git a/vagrants/.gitignore b/vagrants/.gitignore new file mode 100644 index 00000000..8000dd9d --- /dev/null +++ b/vagrants/.gitignore @@ -0,0 +1 @@ +.vagrant diff --git a/vagrants/README.md b/vagrants/README.md new file mode 100644 index 00000000..486edf08 --- /dev/null +++ b/vagrants/README.md @@ -0,0 +1,12 @@ +Wasa2il on Vagrant machines +=========================== + +This directory contains Vagrant virtual machine definitions that should be able +to boot up and run the code without problems. + +These are here to better make sure that the code runs in various environments, and +to figure out which system packages may be required to get the project running. + +All that should be needed is a working Vagrant setup locally (usually backed up by +VirtualBox, or VMWare, or similar), and then you should be able to go into any of +the subdirectories and run `vagrant up` and `vagrant ssh`, naviate to `/app` and run `make test` or `make run` for example. diff --git a/vagrants/archlinux_archlinux/Vagrantfile b/vagrants/archlinux_archlinux/Vagrantfile new file mode 100644 index 00000000..9c915ca0 --- /dev/null +++ b/vagrants/archlinux_archlinux/Vagrantfile @@ -0,0 +1,53 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure("2") do |config| + config.vm.box = "archlinux/archlinux" + + # Sync the local project root folder into /app + config.vm.synced_folder "../../", "/app", type: "rsync", + rsync__exclude: [".git/", ".venv/", ".env", "*.log"] + + # Enable provisioning with a shell script + config.vm.provision "shell", privileged: false, inline: <<-SHELL + + ### apt-get update + sudo pacman -S --noconfirm make + sudo pacman -S --noconfirm which + + # Decided use pyenv to run a particular python version + sudo pacman -S --noconfirm patch # to build python + sudo pacman -S --noconfirm gcc # to build python + sudo pacman -S --noconfirm pyenv + eval "$(pyenv init -)" + echo ' +export PYENV_ROOT="$HOME/.pyenv" +command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" +eval "$(pyenv init -)" + ' > ~/.bashrc + + # Install and set to version 3.8.10 + pyenv install 3.8.10 + pyenv local 3.8.10 + + # The env.template defaults to MySQL. Using MariaDB instead. + sudo pacman -S --noconfirm mariadb + sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql + sudo systemctl start mariadb.service + + # Setup the database + sudo mariadb -e "CREATE USER 'wasa2il'@'localhost' IDENTIFIED BY 'wasa2il';" + sudo mariadb -e "GRANT ALL PRIVILEGES ON *.* TO 'wasa2il'@'localhost' WITH GRANT OPTION;" + sudo mariadb -e "CREATE DATABASE wasa2il" + + # Now that prereqs are setup, let's try the app itself! + + cd /app + make .env || true + make setup + make test + make migrate + # NOTE: This script seems to be python 2.7 compatible, so disabling for now + # make load_fake_data + SHELL +end diff --git a/vagrants/ubuntu_focal64/Vagrantfile b/vagrants/ubuntu_focal64/Vagrantfile new file mode 100644 index 00000000..13f86fb3 --- /dev/null +++ b/vagrants/ubuntu_focal64/Vagrantfile @@ -0,0 +1,47 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure("2") do |config| + config.vm.box = "ubuntu/focal64" + + # Sync the local project root folder into /app + config.vm.synced_folder "../../", "/app", type: "rsync", + rsync__exclude: [".git/", ".venv/", ".env", "*.log"] + + # Enable provisioning with a shell script + config.vm.provision "shell", inline: <<-SHELL + + apt-get update + apt-get install -y make + + apt-get install -y python3-pip + + # Seems to be needed because `ensurepip` wasn't installed. Not quite sure why. + apt-get install -y python3.8-venv + + # python3 is installed, but the `python` command does not exist. + ln -s `which python3` /usr/bin/python + + # The env.template defaults to MySQL + apt-get install -y mysql-server + + apt-get install -y libmysqlclient-dev # needed for mysql_config + # Not needed as the project defaults to MySQL + # apt-get install -y libpq-dev # needed for pg_config + + # Setup the database + mysql -e "CREATE USER 'wasa2il'@'localhost' IDENTIFIED BY 'wasa2il';" + mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'wasa2il'@'localhost' WITH GRANT OPTION;" + mysql -e "CREATE DATABASE wasa2il" + + # Now that prereqs are setup, let's try the app itself! + + cd /app + sudo -u vagrant make .env || true + sudo -u vagrant make setup + sudo -u vagrant make test + sudo -u vagrant make migrate + # NOTE: This script seems to be python 2.7 compatible, so disabling for now + # sudo -u vagrant make load_fake_data + SHELL +end diff --git a/wasa2il/core/admin.py b/wasa2il/core/admin.py deleted file mode 100644 index a13ed78c..00000000 --- a/wasa2il/core/admin.py +++ /dev/null @@ -1,124 +0,0 @@ - -from django.contrib import admin -from django.contrib import auth - - -from models import ( - Polity, Topic, Issue, - Comment, Vote, - Delegate, - UserProfile, - PolityRuleset, - Document, ChangeProposal, - DocumentContent, - Election, Candidate, ElectionVote, - ) - - -def getDerivedAdmin(base_admin, **kwargs): - class DerivedAdmin(base_admin): - pass - derived = DerivedAdmin - for k, v in kwargs.iteritems(): - setattr(derived, k, getattr(base_admin, k, []) + v) - return derived - - -def save_model(self, request, obj, form, change): - if getattr(obj, 'created_by', None) is None: - obj.created_by = request.user - obj.modified_by = request.user - obj.save() - - -class NameSlugAdmin(admin.ModelAdmin): - fieldsets = [ - (None, {'fields': ['name', 'slug']}), - ] - prepopulated_fields = {'slug': ['name']} - list_display = ['name', 'get_url'] - search_fields = ['name'] - - -BaseIssueAdmin = getDerivedAdmin(NameSlugAdmin, - list_display=['description'], - search_fields=['description'], - ) -BaseIssueAdmin.fieldsets = [ - NameSlugAdmin.fieldsets[0], - (None, {'fields': ['description']}), - ] -BaseIssueAdmin.save_model = save_model - - -class PolityAdmin(BaseIssueAdmin): - fieldsets = None - list_display = BaseIssueAdmin.list_display + ['parent'] - - -class TopicAdmin(BaseIssueAdmin): - fieldsets = None - list_display = BaseIssueAdmin.list_display + ['polity'] - - -class IssueAdmin(BaseIssueAdmin): - fieldsets = None - list_display = BaseIssueAdmin.list_display + ['topics_str'] - - -class DelegateAdmin(admin.ModelAdmin): - list_display = ['user', 'delegate', 'base_issue'] - - -#class VoteOptionAdmin(NameSlugAdmin): -# pass - -class VoteAdmin(admin.ModelAdmin): - list_display = ['user'] # , 'option'] - - -class DocumentContentAdmin(admin.ModelAdmin): - list_display = ['document', 'order', 'comments', 'user', 'created'] - - -class ChangeProposalAdmin(admin.ModelAdmin): - list_display = ['content_short', 'action', 'created', 'document', 'issue'] - - -class CommentAdmin(admin.ModelAdmin): - save_model = save_model - - -class UserProfileInline(admin.StackedInline): - model = UserProfile - can_delete = False - - -class UserAdmin(auth.admin.UserAdmin): - inlines = (UserProfileInline, ) - - -# Register the admins -register = admin.site.register -register(Polity, PolityAdmin) -register(Topic, TopicAdmin) -register(Issue, IssueAdmin) -# register(VoteOption, VoteOptionAdmin) -register(Comment, CommentAdmin) -register(Delegate, DelegateAdmin) -register(Vote, VoteAdmin) - -# User profile mucking -admin.site.unregister(auth.models.User) -register(auth.models.User, UserAdmin) - -register(UserProfile) -register(PolityRuleset) - -register(Document, NameSlugAdmin) -register(DocumentContent, DocumentContentAdmin) -register(ChangeProposal, ChangeProposalAdmin) - -register(Election) -register(Candidate) -#register(ElectionVote) diff --git a/wasa2il/core/ajax/__init__.py b/wasa2il/core/ajax/__init__.py deleted file mode 100644 index ab29318d..00000000 --- a/wasa2il/core/ajax/__init__.py +++ /dev/null @@ -1,152 +0,0 @@ -from datetime import datetime - -from django.shortcuts import get_object_or_404 -from django.template.loader import render_to_string -from django.db.models import Q -from django.contrib.auth.decorators import login_required -from django.contrib.auth.models import User -from django.conf import settings - -from core.models import Election -from core.models import ElectionVote -from core.models import Candidate -from core.models import Polity -from core.models import Topic -from core.models import UserProfile -from core.models import UserTopic - -from core.ajax.utils import jsonize, error - - -@jsonize -def election_poll(request): - election = get_object_or_404(Election, id=request.GET.get("election", 0)) - user_is_member = request.user in election.polity.members.all() - ctx = {} - ctx["election"] = {} - ctx["election"]["user_is_candidate"] = (request.user in [x.user for x in election.candidate_set.all()]) - ctx["election"]["is_voting"] = election.is_voting() - ctx["election"]["votes"] = election.get_vote_count() - ctx["election"]["candidates"] = election.get_candidates() - context = {"user_is_member": user_is_member, "election": election, "candidates": election.get_unchosen_candidates(request.user), "candidate_selected": False} - ctx["election"]["candidates"]["html"] = render_to_string("core/_election_candidate_list.html", context) - ctx["election"]["vote"] = {} - context = {"user_is_member": user_is_member, "election": election, "candidates": election.get_vote(request.user), "candidate_selected": True} - ctx["election"]["vote"]["html"] = render_to_string("core/_election_candidate_list.html", context) - ctx["ok"] = True - return ctx - - -@login_required -@jsonize -def election_candidacy(request): - election = get_object_or_404(Election, id=request.GET.get("election", 0)) - if election.is_closed() or not request.user in election.polity.members.all(): - return election_poll(request) - - val = int(request.GET.get("val", 0)) - if val == 0: - Candidate.objects.filter(user=request.user, election=election).delete() - else: - cand, created = Candidate.objects.get_or_create(user=request.user, election=election) - - return election_poll(request) - - -@login_required -@jsonize -def election_vote(request): - election = get_object_or_404(Election, id=request.GET.get("election", 0)) - ctx = {} - ctx["ok"] = True - - if not election.polity.is_member(request.user) or election.is_closed(): - ctx["ok"] = False - return ctx - - order = request.GET.getlist("order[]") - - ElectionVote.objects.filter(election=election, user=request.user).delete() - - for i in range(len(order)): - candidate = Candidate.objects.get(id=order[i]) - vote = ElectionVote(election=election, user=request.user, candidate=candidate, value=i) - vote.save() - - return election_poll(request) - - -@login_required -@jsonize -def topic_star(request): - ctx = {} - topicid = int(request.GET.get('topic', 0)) - if not topicid: - ctx["ok"] = False - return ctx - - topic = get_object_or_404(Topic, id=topicid) - - ctx["topic"] = topic.id - - try: - ut = UserTopic.objects.get(topic=topic, user=request.user) - ut.delete() - ctx["starred"] = False - except UserTopic.DoesNotExist: - UserTopic(topic=topic, user=request.user).save() - ctx["starred"] = True - - topics = topic.polity.get_topic_list(request.user) - ctx["html"] = render_to_string("core/_topic_list_table.html", {"topics": topics, "user": request.user, "polity": topic.polity}) - - ctx["ok"] = True - - return ctx - - -@login_required -@jsonize -def topic_showstarred(request): - ctx = {} - profile = UserProfile.objects.get(user=request.user) - profile.topics_showall = not profile.topics_showall - profile.save() - - ctx["showstarred"] = not profile.topics_showall - - polity = int(request.GET.get("polity", 0)) - if polity: - try: - polity = Polity.objects.get(id=polity) - topics = polity.get_topic_list(request.user) - ctx["html"] = render_to_string("core/_topic_list_table.html", {"topics": topics, "user": request.user, "polity": polity}) - except Exception, e: - ctx["error"] = e - - ctx["ok"] = True - return ctx - - -@jsonize -def election_showclosed(request): - ctx = {} - - polity_id = int(request.GET.get("polity_id")) # This should work. - showclosed = int(request.GET.get('showclosed', 0)) # 0 = False, 1 = True - - try: - if showclosed == 1: - elections = Election.objects.filter(polity_id=polity_id).order_by('-deadline_votes') - else: - elections = Election.objects.filter(polity_id=polity_id, deadline_votes__gt=datetime.now()).order_by('-deadline_votes') - - ctx['showclosed'] = showclosed - ctx['html'] = render_to_string('core/_election_list_table.html', {'elections': elections }) - ctx['ok'] = True - except Exception as e: - ctx['error'] = e - - return ctx - - diff --git a/wasa2il/core/ajax/document.py b/wasa2il/core/ajax/document.py deleted file mode 100644 index 653dd960..00000000 --- a/wasa2il/core/ajax/document.py +++ /dev/null @@ -1,120 +0,0 @@ -import markdown2 - -from django.shortcuts import get_object_or_404 -from django.template.loader import render_to_string -from django.contrib.auth.decorators import login_required - -from core.models import Document, Issue, ChangeProposal, DocumentContent -from core.ajax.utils import jsonize - -from google_diff_match_patch.diff_match_patch import diff_match_patch - - -@login_required -@jsonize -def document_propose_change(request): - ctx = {"ok": True} - document = get_object_or_404(Document, id=request.POST.get("document_id", 0)) - - try: - text = request.POST['text'] - except KeyError: - raise Exception('Missing "text"') - - predecessor = document.preferred_version() - if predecessor and predecessor.text.strip() == text.strip(): - # This error message won't show anywhere. The same error is caught client-side to produce the error message. - raise Exception('Change proposal must differ from its predecessor') - - content = DocumentContent() - content.user = request.user - content.document = document - content.text = text - content.comments = request.POST.get('comments', '') - content.predecessor = predecessor - # TODO: Change this to a query that requests the maximum 'order' and adds to it. - try: - content.order = DocumentContent.objects.filter(document=document).order_by('-order')[0].order + 1 - except IndexError: - pass - - content.save() - - ctx['order'] = content.order - - return ctx - - -@login_required -@jsonize -def document_changeproposal_new(request, document, type): - ctx = {} - - doc = get_object_or_404(Document, id=document) - - if request.user not in doc.polity.members.all(): - ctx['error'] = 403 - return ctx - - s = ChangeProposal() - s.user = request.user - s.document = doc - s.contenttype = type - s.actiontype = 4 - s.refitem = request.GET.get('after') - s.destination = request.GET.get('after') # TODO: Not correct... - s.content = request.GET.get('text', '') - s.save() - - return ctx - - -@login_required -@jsonize -def document_propose(request, document, state): - ctx = {} - - document = get_object_or_404(Document, id=document) - - if request.user != document.user: - return {"error": 403} - - ctx["state"] = bool(int(state)) - document.is_proposed = bool(int(state)) - document.save() - - issue_id = int(request.REQUEST.get("issue", 0)) - if issue_id: - issue = Issue.objects.get(id=issue_id) - ctx["html_user_documents"] = render_to_string("core/_document_proposals_list_table.html", {"documents": issue.user_documents(request.user)}) - ctx["html_all_documents"] = render_to_string("core/_document_list_table.html", {"documents": issue.proposed_documents()}) - - ctx["ok"] = True - return ctx - - -@login_required -@jsonize -def render_markdown(request): - text = request.POST.get('text', 'Missing text!') - ctx = {} - ctx['content'] = markdown2.markdown(text, safe_mode='escape') - - return ctx - - -@jsonize -def documentcontent_render_diff(request): - ctx = {} - - source_id = request.GET.get('source_id') - target_id = request.GET.get('target_id') - - target = get_object_or_404(DocumentContent, id=target_id) - - ctx['source_id'] = source_id - ctx['target_id'] = target_id - ctx['diff'] = target.diff(source_id) - - return ctx - diff --git a/wasa2il/core/ajax/issue.py b/wasa2il/core/ajax/issue.py deleted file mode 100644 index 0bbdcc32..00000000 --- a/wasa2il/core/ajax/issue.py +++ /dev/null @@ -1,59 +0,0 @@ - -from django.shortcuts import get_object_or_404 -from django.contrib.auth.decorators import login_required -from django.utils.timesince import timesince - -from core.models import Issue, Vote, Comment -from core.ajax.utils import jsonize - - -@login_required -@jsonize -def issue_vote(request): - issue = int(request.REQUEST.get("issue", 0)) - issue = get_object_or_404(Issue, id=issue) - - if not issue.is_voting(): - return issue_poll(request) - - if not request.user in issue.polity.members.all(): - return issue_poll(request) - - val = int(request.REQUEST.get("vote", 0)) - - (vote, created) = Vote.objects.get_or_create(user=request.user, issue=issue) - vote.value = val - vote.save() - - return issue_poll(request) - - -@login_required -@jsonize -def issue_comment_send(request): - issue = get_object_or_404(Issue, id=request.REQUEST.get("issue", 0)) - text = request.REQUEST.get("comment") - comment = Comment() - comment.created_by = request.user - comment.comment = text - comment.issue = issue - comment.save() - return issue_poll(request) - - -@jsonize -def issue_poll(request): - issue = get_object_or_404(Issue, id=request.REQUEST.get("issue", 0)) - ctx = {} - comments = [{"id": comment.id, "created_by": comment.created_by.username, "created": str(comment.created), "created_since": timesince(comment.created), "comment": comment.comment} for comment in issue.comment_set.all().order_by("created")] - documents = [] - ctx["issue"] = {"comments": comments, "documents": documents} - ctx["ok"] = True - ctx["issue"]["votes"] = issue.get_votes() - if not request.user.is_anonymous(): - try: - v = Vote.objects.get(user=request.user, issue=issue) - ctx["issue"]["vote"] = v.get_value() - except Vote.DoesNotExist: - pass - return ctx diff --git a/wasa2il/core/authentication.py b/wasa2il/core/authentication.py deleted file mode 100644 index 363e98c7..00000000 --- a/wasa2il/core/authentication.py +++ /dev/null @@ -1,51 +0,0 @@ -from django import forms -from django.conf import settings -from django.contrib.auth import authenticate -from django.contrib.auth.backends import ModelBackend -from django.contrib.auth.models import User, check_password -from django.contrib.auth.forms import AuthenticationForm -from django.utils.translation import ugettext, ugettext_lazy as _ - -class PiratePartyMemberAuthenticationBackend(ModelBackend): - - def authenticate(self, email=None, password=None): - try: - user = User.objects.get(email=email) - if user.check_password(password): - return user - except User.DoesNotExist: - return None - -class PiratePartyMemberAuthenticationForm(AuthenticationForm): - email = forms.CharField(label=_("E-mail"), max_length=30) - password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) - - def __init__(self, request=None, *args, **kwargs): - """ - If request is passed in, the form will validate that cookies are - enabled. Note that the request (a HttpRequest object) must have set a - cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before - running this validation. - """ - self.request = request - self.user_cache = None - super(AuthenticationForm, self).__init__(*args, **kwargs) - - self.fields.pop('username') # Remove field inherited from superclass - self.fields.insert(0, 'email', self.fields['email']) # set form field order - - def clean(self): - email = self.cleaned_data.get('email') - password = self.cleaned_data.get('password') - - if email and password: - self.user_cache = authenticate(email=email, - password=password) - if self.user_cache is None: - raise forms.ValidationError( - self.error_messages['invalid_login']) - elif not self.user_cache.is_active: - raise forms.ValidationError(self.error_messages['inactive']) - self.check_for_test_cookie() - return self.cleaned_data - diff --git a/wasa2il/core/base_classes.py b/wasa2il/core/base_classes.py deleted file mode 100644 index 850199c2..00000000 --- a/wasa2il/core/base_classes.py +++ /dev/null @@ -1,38 +0,0 @@ - -from django.db import models -from fields import AutoUserField -from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ - - -class NameSlugBase(models.Model): - name = models.CharField(max_length=128, verbose_name=_('Name')) - slug = models.SlugField(max_length=128, blank=True) - - class Meta: - abstract = True - - def get_url(self, anchor=True): - if anchor: - print type(mark_safe(u'%s' % (self.slug, self.name))) - return mark_safe(u'%s' % (self.slug, self.name)) - return u'/slug/%s/' % self.slug - get_url.allow_tags = True - get_url.short_description = 'Slug-URL' - - def __unicode__(self): - return u'%s' % (self.name) - - -def getCreationBase(prefix): - - class CreationBase(models.Model): - created_by = AutoUserField(related_name='%s_created_by' % prefix) - modified_by = AutoUserField(related_name='%s_modified_by' % prefix) - created = models.DateTimeField(auto_now_add=True) - modified = models.DateTimeField(auto_now=True) - - class Meta: - abstract = True - - return CreationBase diff --git a/wasa2il/core/contextprocessors.py b/wasa2il/core/contextprocessors.py deleted file mode 100644 index b6d59d92..00000000 --- a/wasa2il/core/contextprocessors.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.conf import settings - -def globals(request): - - ctx = { - 'INSTANCE_NAME': settings.INSTANCE_NAME, - 'INSTANCE_URL': settings.INSTANCE_URL.strip('/'), - 'INSTANCE_FACEBOOK_IMAGE': settings.INSTANCE_FACEBOOK_IMAGE, - } - - return ctx diff --git a/wasa2il/core/elections.py b/wasa2il/core/elections.py deleted file mode 100644 index b820c43b..00000000 --- a/wasa2il/core/elections.py +++ /dev/null @@ -1,42 +0,0 @@ -#TODO Can we remove this file? Seems like ancient garbage -from openstv.ballots import Ballots -from openstv.plugins import getMethodPlugins - - -class RankedElection: - def __init__(self): - pass - - def load_ballot(self, ballot): - try: - dirtyBallots = Ballots() - dirtyBallots.loadKnown(bltFn, exclude0=False) - cleanBallots = dirtyBallots.getCleanBallots() - except RuntimeError, msg: - print msg - - def load_ballot_file(self, filename): - pass - - def set_election_type(self, electiontype): - pass - - def get_election_types(self): - return getMethodPlugins("byName").keys() - - def get_election_classes(self): - return getMethodPlugins("byName") - - def set_num_seats(self): - pass - - def get_num_seats(self): - pass - - def get_result(self): - pass - - -if __name__ == "__main__": - print "RANKEDELECTIONMADNESS!" - r = RankedElection() diff --git a/wasa2il/core/fields.py b/wasa2il/core/fields.py deleted file mode 100644 index 0095e19d..00000000 --- a/wasa2il/core/fields.py +++ /dev/null @@ -1,15 +0,0 @@ - -from django.db import models - -from django.contrib.auth.models import User - - -class AutoUserField(models.ForeignKey): - def __init__(self, usermodel=User, *args, **kwargs): - d = dict( - editable=False, - null=True, - blank=True, - ) - d.update(kwargs) - return super(AutoUserField, self).__init__(usermodel, *args, **d) diff --git a/wasa2il/core/forms.py b/wasa2il/core/forms.py deleted file mode 100644 index 52b9abb1..00000000 --- a/wasa2il/core/forms.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.forms import ModelForm -from django.forms import EmailField -from django.forms import ValidationError -from django.utils.translation import ugettext as _ - -from core.models import Topic, Issue, Comment, Document, Polity, UserProfile, Election - - -class TopicForm(ModelForm): - class Meta: - model = Topic - exclude = ('polity', 'slug') - - -class IssueForm(ModelForm): - class Meta: - model = Issue - exclude = ('polity', 'slug', 'documentcontent', 'deadline_discussions', 'deadline_proposals', 'deadline_votes', 'majority_percentage', 'is_processed') - - -class CommentForm(ModelForm): - class Meta: - model = Comment - exclude = ('issue',) - - -class DocumentForm(ModelForm): - class Meta: - model = Document - exclude = ('is_adopted', 'is_proposed', 'user', 'polity', 'slug', 'issues') - - -class PolityForm(ModelForm): - class Meta: - model = Polity - exclude = ('slug', 'parent', 'members') - - -class ElectionForm(ModelForm): - class Meta: - model = Election - exclude = ('polity', 'slug', 'is_processed') - - -class UserProfileForm(ModelForm): - email = EmailField(label=_("E-mail"), help_text=_("The email address you'd like to use for the site.")) - - class Meta: - model = UserProfile - fields = ('displayname', 'email', 'picture', 'bio', 'language') - - # We need to keep the 'request' object for certain kinds of validation ('picture' in this case) - def __init__(self, *args, **kwargs): - self.request = kwargs.pop('request', None) - super(UserProfileForm, self).__init__(*args, **kwargs) - - def clean_picture(self): - data = self.cleaned_data['picture'] - - picture = self.request.FILES.get('picture') - if picture: - if picture.name.find('.') == -1: - raise ValidationError(_('Filename must contain file extension')) - - return data diff --git a/wasa2il/core/management/commands/deleteproposals.py b/wasa2il/core/management/commands/deleteproposals.py deleted file mode 100644 index c5f52896..00000000 --- a/wasa2il/core/management/commands/deleteproposals.py +++ /dev/null @@ -1,31 +0,0 @@ -# Run this to delete old proposals to law which never had an effect. - -from sys import stdout, stderr -from datetime import datetime - -from django.core.management.base import BaseCommand -from django.utils import timezone - -from core.models import * - -class Command(BaseCommand): - - def handle(self, *args, **options): - - if 'YES-I-MEAN-IT' in args: - self.delete_obsolete_versions() # DANGEROUS - else: - print - print "WARNING!" - print "DO NOT RUN THIS UNLESS YOU KNOW WHAT YOU ARE DOING!" - print "IT WILL MERCILESSLY DELETE ALL CHANGE PROPOSALS!" - print "YOU HAVE BEEN WARNED!" - print - print "Run with option \"YES-I-MEAN-IT\" if you are 100% certain." - - def delete_obsolete_versions(self): - versions = DocumentContent.objects.all() - for version in versions: - if version.order > 1: - version.delete() - diff --git a/wasa2il/core/management/commands/processelections.py b/wasa2il/core/management/commands/processelections.py deleted file mode 100644 index 52b74d06..00000000 --- a/wasa2il/core/management/commands/processelections.py +++ /dev/null @@ -1,44 +0,0 @@ -from sys import stdout, stderr -from datetime import datetime - -from django.core.management.base import BaseCommand - -from core.models import * - -class Command(BaseCommand): - - def handle(self, *args, **options): - - try: - - print - print 'WARNING! This command will permanently delete EVERY ballot of EVERY election!' - print 'Only do this if you know what you\'re doing. You have been warned.' - print - response = '' - while response != 'yes' and response != 'no': - response = raw_input('Are you REALLY certain that you wish to proceed? (yes/no) ').lower() - - if response == 'no': - print - print 'Chicken.' - print - return - - elections = Election.objects.all() - - for election in elections: - stdout.write('Processing election %s...' % election) - - try: - election.process() - stdout.write(' done\n') - except Election.AlreadyProcessedException: - stdout.write(' already processed\n') - except: - stdout.write(' failed\n') - - except KeyboardInterrupt: - print - quit() - diff --git a/wasa2il/core/management/commands/processissues.py b/wasa2il/core/management/commands/processissues.py deleted file mode 100644 index 07065d49..00000000 --- a/wasa2il/core/management/commands/processissues.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -from sys import stdout, stderr -from datetime import datetime - -from django.core.management.base import BaseCommand - -from core.models import * - -class Command(BaseCommand): - - def handle(self, *args, **options): - - now = datetime.now() - - unprocessed_issues = Issue.objects.filter(is_processed=False).order_by('deadline_votes', 'id') - - for issue in unprocessed_issues: - - if issue.is_closed(): - - documentcontent = issue.documentcontent - issue_name = issue.name.encode('utf-8') - - if documentcontent is None: - stdout.write("Skipping issue '%s' (%d) since it has no DocumentContent\n" % (issue_name, issue.id)) - continue - - document = documentcontent.document - - stdout.write("Processing closed issue '%s' (%d):\n" % (issue_name, issue.id)) - - documentcontent.predecessor = document.preferred_version() - - stdout.write("* Majority reached: ") - stdout.flush() - - status = '' - - if issue.majority_reached(): - stdout.write("yes\n") - status = 'accepted' - - stdout.write("* Deprecating previously accepted versions, if any... ") - stdout.flush() - - prev_contents = document.documentcontent_set.exclude(id=documentcontent.id).filter(status='accepted') - for c in prev_contents: - c.status = 'deprecated' - c.save() - - stdout.write("done\n") - - else: - stdout.write("no\n") - status = 'rejected' - - stdout.write("* Setting status of document version %d to '%s'... " % (documentcontent.order, status)) - stdout.flush() - - documentcontent.status = status - documentcontent.save() - - stdout.write("done\n") - - stdout.write("* Setting processed-status of issue to true... ") - stdout.flush() - - issue.is_processed = True - issue.save() - - stdout.write("done\n") - - diff --git a/wasa2il/core/middleware.py b/wasa2il/core/middleware.py deleted file mode 100644 index c87c0d53..00000000 --- a/wasa2il/core/middleware.py +++ /dev/null @@ -1,40 +0,0 @@ - -from django.conf import settings -from django.shortcuts import render_to_response - -from core.models import UserProfile - -from django.contrib import auth - -from datetime import datetime, timedelta - -class UserSettingsMiddleware(object): - def __init__(self): - pass - - def process_request(self, request): - - if hasattr(settings, 'AUTO_LOGOUT_DELAY'): - if not request.user.is_authenticated() : - # Can't log out if not logged in - return - - now = datetime.now() - - if 'last_visit' in request.session: - last_visit = datetime.strptime(request.session['last_visit'], '%Y-%m-%d %H:%M:%S') - if now - last_visit > timedelta(0, settings.AUTO_LOGOUT_DELAY * 60, 0): - auth.logout(request) - return - - request.session['last_visit'] = now.strftime('%Y-%m-%d %H:%M:%S') - - if hasattr(settings, 'SAML_1'): # Is SAML 1.2 support enabled? - if not request.user.is_anonymous(): - # Make sure that the user is not only logged in, but verified - profile = request.user.userprofile # This should never fail, see login - if not profile.verified_ssn and request.path_info != '/accounts/verify/' and request.path_info != '/accounts/logout/': - ctx = { 'auth_url': settings.SAML_1['URL'] } - return render_to_response('registration/verification_needed.html', ctx) - - diff --git a/wasa2il/core/models.py b/wasa2il/core/models.py deleted file mode 100644 index 9d92b9ea..00000000 --- a/wasa2il/core/models.py +++ /dev/null @@ -1,815 +0,0 @@ -#coding:utf-8 - -import logging -import re -import random - -from base_classes import NameSlugBase, getCreationBase -from core.utils import AttrDict -from datetime import datetime, timedelta -from django.conf import settings -from django.db import models -from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy as _ - -from google_diff_match_patch.diff_match_patch import diff_match_patch - -import schulze - -nullblank = {'null': True, 'blank': True} - - -def trim(text, length): - if len(text) > length: - return '%s…' % text[:length - 1] - return text - - -class BaseIssue(NameSlugBase): - description = models.TextField(verbose_name=_("Description"), **nullblank) - - -class UserProfile(models.Model): - """A user's profile data. Contains various informative areas, plus various settings.""" - user = models.OneToOneField(User) - - # Verification - verified_ssn = models.CharField(max_length=30, null=True, blank=True, unique=True) - verified_name = models.CharField(max_length=100, null=True, blank=True) - verified_token = models.CharField(max_length=100, null=True, blank=True) - verified_timing = models.DateTimeField(null=True, blank=True) - - # User information - displayname = models.CharField(max_length="255", verbose_name=_("Name"), help_text=_("The name to display on the site."), **nullblank) - email_visible = models.BooleanField(default=False, verbose_name=_("E-mail visible"), help_text=_("Whether to display your email address on your profile page.")) - bio = models.TextField(verbose_name=_("Bio"), **nullblank) - picture = models.ImageField(upload_to="users", verbose_name=_("Picture"), **nullblank) - joined_org = models.DateTimeField(null=True, blank=True) # Time when user joined organization, as opposed to registered in the system - - # User settings - language = models.CharField(max_length="6", default="en", choices=settings.LANGUAGES, verbose_name=_("Language")) - topics_showall = models.BooleanField(default=True, help_text=_("Whether to show all topics in a polity, or only starred.")) - - def save(self, *largs, **kwargs): - if not self.picture: - self.picture.name = "default.jpg" - super(UserProfile, self).save(*largs, **kwargs) - - def __unicode__(self): - return 'Profile for %s (%d)' % (unicode(self.user), self.user.id) - - -def get_name(user): - name = "" - if user: - try: - name = user.userprofile.displayname - except AttributeError: - print 'User with id %d missing profile?' % user.id - pass - - if not name: - name = user.username - - return name - -User.get_name = get_name - - -class PolityRuleset(models.Model): - """A polity's ruleset.""" - polity = models.ForeignKey('Polity') - name = models.CharField(max_length=255) - - # Issue quora is how many members need to support a discussion - # before it goes into proposal mode. If 0, use timer. - # If issue_quora_percent, user percentage of polity members. - issue_quora_percent = models.BooleanField(default=False) - issue_quora = models.IntegerField() - - # Issue majority is how many percent of the polity are needed - # for a decision to be made on the issue. - issue_majority = models.DecimalField(max_digits=5, decimal_places=2) - - # Denotes how many seconds an issue is in various phases. - issue_discussion_time = models.IntegerField() - issue_proposal_time = models.IntegerField() - issue_vote_time = models.IntegerField() - - # Sometimes we require an issue to be confirmed with a secondary vote. - # Note that one option here is to reference the same ruleset, and thereby - # force continuous confirmation (such as with annual budgets, etc..) - # Also, can be used to create multiple discussion rounds - confirm_with = models.ForeignKey('PolityRuleset', **nullblank) - - # For multi-round discussions, we may want corresponding documents not to - # be adopted when the vote is complete, but only for the successful vote - # to allow progression into the next round. - adopted_if_accepted = models.BooleanField(default=True) - - def __unicode__(self): - return self.name - - def has_quora(self, issue): - # TODO: Return whether this has acheived quora on this ruleset - pass - - def has_majority(self, issue): - # TODO: Return whether this has majority on this ruleset - pass - - def get_phase(self, issue): - # TODO: Return information about the current phase - pass - - def get_timeline(self, issue): - # TODO: Return a data structure describing when things will happen - # Should contain reference to confirmation actions, but not expand - # on them (as this could be an infinite loop, and confirmation - # actions aren't actually determined until post-vote. - pass - - -class Polity(BaseIssue, getCreationBase('polity')): - """A political entity. See the manual.""" - - parent = models.ForeignKey('Polity', help_text="Parent polity", **nullblank) - members = models.ManyToManyField(User) - officers = models.ManyToManyField(User, verbose_name=_("Officers"), related_name="officers") - - is_listed = models.BooleanField(verbose_name=_("Publicly listed?"), default=True, help_text=_("Whether the polity is publicly listed or not.")) - is_nonmembers_readable = models.BooleanField(verbose_name=_("Publicly viewable?"), default=True, help_text=_("Whether non-members can view the polity and its activities.")) - is_newissue_only_officers = models.BooleanField(verbose_name=_("Can only officers make new issues?"), default=False, help_text=_("If this is checked, only officers can create new issues. If it's unchecked, any member can start a new issue.")) - is_front_polity = models.BooleanField(verbose_name=_("Front polity?"), default=False, help_text=_("If checked, this polity will be displayed on the front page. The first created polity automatically becomes the front polity.")) - - def get_delegation(self, user): - """Check if there is a delegation on this polity.""" - if not user.is_authenticated(): - return [] - try: - d = Delegate.objects.get(user=user, base_issue=self) - return d.get_path() - except Delegate.DoesNotExist: - pass - return [] - - def is_member(self, user): - return user in self.members.all() - - def get_topic_list(self, user): - if user.is_anonymous() or UserProfile.objects.get(user=user).topics_showall: - topics = Topic.objects.filter(polity=self).order_by('name') - else: - topics = [x.topic for x in UserTopic.objects.filter(user=user, topic__polity=self).order_by('topic__name')] - - return topics - - def agreements(self): - return DocumentContent.objects.select_related('document').filter(status='accepted', document__polity_id=self.id).order_by('-issue__deadline_votes') - - def save(self, *args, **kwargs): - - polities = Polity.objects.all() - if polities.count() == 0: - self.is_front_polity = True - elif self.is_front_polity: - for frontpolity in polities.filter(is_front_polity=True).exclude(id=self.id): # Should never return more than 1 - frontpolity.is_front_polity = False - frontpolity.save() - - return super(Polity, self).save(*args, **kwargs) - - -class Topic(BaseIssue, getCreationBase('topic')): - """A collection of issues unified categorically.""" - polity = models.ForeignKey(Polity) - - class Meta: - ordering = ["name"] - - def issues_open(self): - issues = [issue for issue in self.issue_set.all() if issue.is_open()] - return len(issues) - - def issues_voting(self): - issues = [issue for issue in self.issue_set.all() if issue.is_voting()] - return len(issues) - - def issues_closed(self): - issues = [issue for issue in self.issue_set.all() if issue.is_closed()] - return len(issues) - - def get_delegation(self, user): - """Check if there is a delegation on this topic.""" - if not user.is_authenticated(): - return [] - try: - d = Delegate.objects.get(user=user, base_issue=self) - return d.get_path() - except Delegate.DoesNotExist: - return self.polity.get_delegation(user) - - def new_comments(self): - return Comment.objects.filter(issue__topics=self).order_by("-created")[:10] - - -class UserTopic(models.Model): - """Whether a user likes a topic.""" - topic = models.ForeignKey(Topic) - user = models.ForeignKey(settings.AUTH_USER_MODEL) - - class Meta: - unique_together = (("topic", "user"),) - - -class Issue(BaseIssue, getCreationBase('issue')): - SPECIAL_PROCESS_CHOICES = ( - ('accepted_at_assembly', _('Accepted at assembly')), - ('rejected_at_assembly', _('Rejected at assembly')), - ) - - polity = models.ForeignKey(Polity) - topics = models.ManyToManyField(Topic, verbose_name=_("Topics")) - documentcontent = models.OneToOneField('DocumentContent', related_name='issue', **nullblank) - deadline_discussions = models.DateTimeField(**nullblank) - deadline_proposals = models.DateTimeField(**nullblank) - deadline_votes = models.DateTimeField(**nullblank) - majority_percentage = models.DecimalField(max_digits=5, decimal_places=2) - ruleset = models.ForeignKey(PolityRuleset, verbose_name=_("Ruleset"), editable=True) - is_processed = models.BooleanField(default=False) - special_process = models.CharField(max_length='32', verbose_name=_("Special process"), choices=SPECIAL_PROCESS_CHOICES, default='', null=True, blank=True) - - class Meta: - ordering = ["-deadline_votes"] - - def __unicode__(self): - return self.name - - def apply_ruleset(self): - now = datetime.now() - - if self.special_process: - self.deadline_discussions = now - self.deadline_proposals = now - self.deadline_votes = now - else: - self.deadline_discussions = now + timedelta(seconds=self.ruleset.issue_discussion_time) - self.deadline_proposals = self.deadline_discussions + timedelta(seconds=self.ruleset.issue_proposal_time) - self.deadline_votes = self.deadline_proposals + timedelta(seconds=self.ruleset.issue_vote_time) - - self.majority_percentage = self.ruleset.issue_majority # Doesn't mechanically matter but should be official. - - def is_open(self): - if not self.is_closed(): - return True - return False - - def is_voting(self): - if not self.deadline_proposals or not self.deadline_votes: - return False - - if datetime.now() > self.deadline_proposals and datetime.now() < self.deadline_votes: - return True - - return False - - def is_closed(self): - if not self.deadline_votes: - return False - - if datetime.now() > self.deadline_votes: - return True - - return False - - def get_delegation(self, user): - """Check if there is a delegation on this topic.""" - if not user.is_authenticated(): - return [] - try: - d = Delegate.objects.get(user=user, base_issue=self) - return d.get_path() - except Delegate.DoesNotExist: - for i in self.topics.all(): - return i.get_delegation(user) - - def topics_str(self): - return ', '.join(map(str, self.topics.all())) - - def proposed_documents(self): - return self.document_set.filter(is_proposed=True) - - def user_documents(self, user): - try: - return self.document_set.filter(user=user) - except TypeError: - return [] - - def get_votes(self): - votes = {} - if self.is_closed(): - votes["yes"] = sum([x.get_value() for x in self.vote_set.filter(value=1)]) - votes["abstain"] = sum([x.get_value() for x in self.vote_set.filter(value=0)]) - votes["no"] = -sum([x.get_value() for x in self.vote_set.filter(value=-1)]) - else: - votes["yes"] = -1 - votes["abstain"] = -1 - votes["no"] = -1 - votes["total"] = sum([x.get_value() for x in self.vote_set.all()]) - votes["count"] = self.vote_set.exclude(value=0).count() - return votes - - def majority_reached(self): - result = False - - if self.special_process == 'accepted_at_assembly': - result = True - else: - votes = self.get_votes() - if votes['count'] > 0: - result = float(votes['yes']) / votes['count'] > float(self.majority_percentage) / 100 - - return result - - -class Comment(getCreationBase('comment')): - comment = models.TextField() - issue = models.ForeignKey(Issue) - - -class Delegate(models.Model): - polity = models.ForeignKey(Polity) - user = models.ForeignKey(settings.AUTH_USER_MODEL) - delegate = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='delegate_user') - base_issue = models.ForeignKey(BaseIssue) - - class Meta: - unique_together = (('user', 'base_issue')) - - def __unicode__(self): - return "[%s:%s] %s -> %s" % (self.type(), self.base_issue, self.user, self.delegate) - - def polity(self): - """Gets the polity that the delegation exists within.""" - try: - return self.base_issue.issue.polity - except AttributeError: - pass - try: - return self.base_issue.topic.polity - except AttributeError: - pass - try: - return self.base_issue.polity - except AttributeError: - print "ERROR: Delegate's base_issue is None, apparently" - - def result(self): - """Work through the delegations and figure out where it ends""" - return self.get_path()[-1].delegate - - def type(self): - """Figure out what kind of thing is being delegated. Returns a translated string.""" - try: - self.base_issue.issue - return _("Issue") - except AttributeError: - pass - try: - self.base_issue.topic - return _("Topic") - except AttributeError: - pass - try: - self.base_issue.polity - return _("Polity") - except AttributeError: - print "ERROR: Delegate's base_issue is None, apparently" - - def get_power(self): - """Get how much power has been transferred through to this point in the (reverse) delegation chain.""" - # TODO: FIXME - pass - - def get_path(self): - """Get the delegation pathway from here to the end of the chain.""" - path = [self] - while True: - item = path[-1] - user = item.delegate - dels = user.delegate_set.filter(base_issue=item.base_issue) - if len(dels) > 0: - path.append(dels[0]) - continue - - if item.base_issue and item.base_issue.issue: - base_issue = item.base_issue.issue - if base_issue and base_issue.topics: # If this works, we are working with an "Issue" - for topic in base_issue.topics.all(): - dels = user.delegate_set.filter(base_issue=topic) - if len(dels) > 0: - # TODO: FIXME - # Problem: Whereas an issue can belong to multiple topics, this is - # basically picking the first delegation to a topic, rather than - # creating weightings. Should we do weightings? - path.append(dels[0]) - continue - - try: # If this works, we are working with an "Issue" - base_issue = item.base_issue.topic - dels = user.delegate_set.filter(base_issue=base_issue) - if len(dels) > 0: - path.append(dels[0]) - continue - except AttributeError: - pass - - break - - return path - - -class Vote(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL) - issue = models.ForeignKey(Issue) - # option = models.ForeignKey(VoteOption) - value = models.IntegerField() - cast = models.DateTimeField(auto_now_add=True) - power_when_cast = models.IntegerField() - - class Meta: - unique_together = (('user', 'issue')) - - def save(self, *largs, **kwargs): - if self.value > 1: - self.value = 1 - elif self.value < -1: - self.value = -1 - - self.power_when_cast = self.power() - super(Vote, self).save(*largs, **kwargs) - - def power(self): - # Follow reverse delgation chain to discover how much power we have. - p = 1 - - return p - - def get_value(self): - return self.power() * self.value - - -class Document(NameSlugBase): - polity = models.ForeignKey(Polity) - issues = models.ManyToManyField(Issue) # Needed for core/management/commands/refactor.py, can be deleted afterward - user = models.ForeignKey(settings.AUTH_USER_MODEL) - is_adopted = models.BooleanField(default=False) - is_proposed = models.BooleanField(default=False) - - class Meta: - ordering = ["-id"] - - def save(self, *args, **kwargs): - return super(Document, self).save(*args, **kwargs) - - def get_versions(self): - return DocumentContent.objects.filter(document=self).order_by('order') - - def get_contributors(self): - return set([dc.user for dc in self.documentcontent_set.order_by('user__username')]) - - # preferred_version() finds the most proper, previous documentcontent to build a new one on. - # It prefers the latest accepted one, but if it cannot find one, it will default to the first proposed one. - # If it finds neither a proposed nor accepted one, it will try to find the first rejected one. - # It will return None if it finds nothing and it's the calling function's responsibility to react accordingly. - # TODO: Make this faster and cached per request. Preferably still Pythonic. -helgi@binary.is, 2014-07-02 - def preferred_version(self): - documentcontent = None - - # Latest accepted version... - accepted_versions = self.documentcontent_set.filter(status='accepted').order_by('-order') - if accepted_versions.count() > 0: - documentcontent = accepted_versions[0] - else: - # ...and if none are found, find the earliest proposed one... - proposed_versions = self.documentcontent_set.filter(status='proposed').order_by('order') - if proposed_versions.count() > 0: - documentcontent = proposed_versions[0] - else: - # ...finally and desperately going for the first rejected one. - rejected_versions = self.documentcontent_set.filter(status='rejected').order_by('order') - if rejected_versions.count() > 0: - documentcontent = rejected_versions[0] - - return documentcontent - - # Returns true if a documentcontent in this document already has an issue in progress. - def has_open_issue(self): - documentcontent_ids = [dc.id for dc in self.documentcontent_set.all()] - count = Issue.objects.filter(is_processed=False, documentcontent_id__in=documentcontent_ids).count() - return count > 0 - - -class DocumentContent(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL) - document = models.ForeignKey(Document) - created = models.DateTimeField(auto_now_add=True) - text = models.TextField() - order = models.IntegerField(default=1) - comments = models.TextField(blank=True) - STATUS_CHOICES = ( - ('proposed', _('Proposed')), - ('accepted', _('Accepted')), - ('rejected', _('Rejected')), - ('deprecated', _('Deprecated')), - ) - status = models.CharField(max_length='32', choices=STATUS_CHOICES, default='proposed') - predecessor = models.ForeignKey('DocumentContent', null=True, blank=True) - - - # Attempt to inherit earlier issue's topic selection - def previous_topics(self): - selected_topics = [] - if self.order > 1: - # NOTE: This is entirely distinct from Document.preferred_version() and should not be replaced by it. - # This function actually regards Issues, not DocumentContents, but is determined by DocumentContent as input. - - # Find the last accepted documentcontent - prev_contents = self.document.documentcontent_set.exclude(id=self.id).order_by('-order') - selected_topics = [] - for c in prev_contents: # NOTE: We're iterating from newest to oldest. - if c.status == 'accepted': - # A previously accepted DocumentContent MUST correspond to an issue so we brutally assume so. - selected_topics = [t.id for t in c.issue.topics.all()] - break - - # If no topic list is determined from previously accepted issue, we inherit from the last Issue, if any. - if len(selected_topics) == 0: - for c in prev_contents: - try: - c_issue = c.issue.get() - selected_topics = [t.id for t in c_issue.topics.all()] - break; - except: - pass - - return selected_topics - - # Gets all DocumentContents which belong to the Document to which this DocumentContent belongs to. - def siblings(self): - siblings = DocumentContent.objects.filter(document_id=self.document_id).order_by('order') - return siblings - - # Generates a diff between this DocumentContent and the one provided to the function. - def diff(self, documentcontent_id): - earlier_content = DocumentContent.objects.get(id=documentcontent_id) - - # Basic diff_match_patch thing - dmp = diff_match_patch() - - dmp.Diff_Timeout = 0 - # dmp.Diff_EditCost = 10 # Higher value means more semantic cleanup. 4 is default which works for us right now. - - d = dmp.diff_main(earlier_content.text, self.text) - - # Calculate the diff - dmp.diff_cleanupSemantic(d) - result = dmp.diff_prettyHtml(d).replace('¶', '') - - result = re.sub(r'\r
', r'
', result) # Because we're using
 in the template, so the HTML creates two newlines.
-
-        return result
-
-    def __unicode__(self):
-        return "DocumentContent (ID: %d)" % self.id
-
-
-class ChangeProposal(getCreationBase('change_proposal')):
-    document = models.ForeignKey(Document)    # Document to reference
-    issue = models.ForeignKey(Issue)
-
-    CHANGE_PROPOSAL_ACTION_CHOICES = (
-        ('NEW', 'New Agreement'),
-        ('CHANGE', 'Change Agreement Text'),
-        ('CHANGE_TITLE', 'Change Agreement Title'),
-        ('RETIRE', 'Retire Agreement'),
-    )
-    action = models.CharField(max_length=20, choices=CHANGE_PROPOSAL_ACTION_CHOICES)
-
-    content = models.TextField(help_text='Content of document, or new title', **nullblank)
-
-    def __unicode__(self):
-        return 'Change Proposal: %s (content: "%s")' % (self.action, self.content_short())
-
-    def content_short(self):
-        return trim(self.content, 30)
-
-
-MOTION = {
-    'TALK': 1,
-    'REPLY': 2,
-    'CLARIFY': 3,
-    'POINT': 4,
-}
-
-
-def get_power(user, issue):
-    power = 1
-    bases = [issue, issue.polity]
-    bases.extend(issue.topics.all())
-
-    # print "Getting power for user %s on issue %s" % (user, issue)
-    delegations = Delegate.objects.filter(delegate=user, base_issue__in=bases)
-    for i in delegations:
-        power += get_power(i.user, issue)
-    return power
-
-
-def get_issue_power(issue, user):
-    return get_power(user, issue)
-
-
-# TODO: Why are these set here? Fix later..?
-Issue.get_power = get_issue_power
-User.get_power = get_power
-
-
-class Election(NameSlugBase):
-    """
-    An election is different from an issue vote; it's a vote
-    on people. Users, specifically.
-    """
-
-    VOTING_SYSTEMS = (
-        ('schulze', 'Schulze'),
-    )
-
-    polity = models.ForeignKey(Polity)
-    voting_system = models.CharField(max_length=30, verbose_name=_('Voting system'), choices=VOTING_SYSTEMS)
-    deadline_candidacy = models.DateTimeField(verbose_name=_('Deadline for candidacy'))
-    deadline_votes = models.DateTimeField(verbose_name=_('Deadline for votes'))
-
-    # Sometimes elections may depend on a user having been the organization's member for an X amount of time
-    # This optional field lets the vote counter disregard members who are too new.
-    deadline_joined_org = models.DateTimeField(null=True, blank=True, verbose_name=_('Membership deadline'))
-    is_processed = models.BooleanField(default=False)
-
-    instructions = models.TextField(null=True, blank=True, verbose_name=_('Instructions'))
-
-    # An election can only be processed once, since votes are deleted during the process
-    class AlreadyProcessedException(Exception):
-        def __init__(self, message):
-            super(Election.AlreadyProcessedException, self).__init__(message)
-
-    def process(self):
-        if self.electionvote_set.count() == 0:
-            raise Election.AlreadyProcessedException('Cannot process election %s (no ElectionVote found)' % self)
-
-        ordered_candidates = self.get_ordered_candidates_from_votes()
-        vote_count = self.electionvote_set.values('user').distinct().count()
-
-        try:
-            election_result = ElectionResult.objects.get(election=self)
-        except ElectionResult.DoesNotExist:
-            election_result = ElectionResult.objects.create(election=self, vote_count=vote_count)
-
-        election_result.rows.all().delete()
-        order = 0
-        for candidate in ordered_candidates:
-            order = order + 1
-            election_result_row = ElectionResultRow()
-            election_result_row.election_result = election_result
-            election_result_row.candidate = candidate
-            election_result_row.order = order
-            election_result_row.save()
-
-        self.electionvote_set.all().delete()
-
-        self.is_processed = True
-        self.save()
-
-    def get_ordered_candidates_from_votes(self):
-        if self.deadline_joined_org:
-            votes = ElectionVote.objects.select_related('candidate__user').filter(election=self, user__userprofile__joined_org__lt = self.deadline_joined_org)
-        else:
-            votes = ElectionVote.objects.select_related('candidate__user').filter(election=self)
-        candidates = Candidate.objects.select_related('user').filter(election=self)
-        votemap = {}
-        for vote in votes:
-            if not votemap.has_key(vote.user_id):
-                votemap[vote.user_id] = []
-            votemap[vote.user_id].append(vote)
-
-        manger = []
-        for user_id in votemap:
-            manger.append([(v.value, v.candidate) for v in votemap[user_id]])
-
-        preference = schulze.rank_votes(manger, candidates)
-        strongest_paths = schulze.compute_strongest_paths(preference, candidates)
-
-        ordered_candidates = schulze.get_ordered_voting_results(strongest_paths)
-        return ordered_candidates
-
-    def export_openstv_ballot(self):
-        return ""
-
-    def __unicode__(self):
-        return self.name
-
-    def is_open(self):
-        return not self.is_closed()
-
-    def is_voting(self):
-        if not self.deadline_candidacy or not self.deadline_votes:
-            return False
-
-        if datetime.now() > self.deadline_candidacy and datetime.now() < self.deadline_votes:
-            return True
-
-        return False
-
-    def is_closed(self):
-        if not self.deadline_votes:
-            return False
-
-        if datetime.now() > self.deadline_votes:
-            return True
-
-        return False
-
-    def get_candidates(self):
-        ctx = {}
-        ctx["count"] = self.candidate_set.count()
-        ctx["users"] = [{"username": x.user.username} for x in self.candidate_set.all()]
-        return ctx
-
-    def get_unchosen_candidates(self, user):
-        if not user.is_authenticated():
-            return Candidate.objects.filter(election=self)
-        # votes = []
-        votes = ElectionVote.objects.filter(election=self, user=user)
-        votedcands = [x.candidate.id for x in votes]
-        if len(votedcands) != 0:
-            candidates = Candidate.objects.filter(election=self).exclude(id__in=votedcands).order_by('?')
-        else:
-            candidates = Candidate.objects.filter(election=self).order_by('?')
-
-        return candidates
-
-    def get_vote_count(self):
-        if self.is_processed:
-            return self.result.vote_count
-        else:
-            return self.electionvote_set.values("user").distinct().count()
-
-    def get_vote(self, user):
-        votes = []
-        if not user.is_anonymous():
-            votes = ElectionVote.objects.filter(election=self, user=user).order_by("value")
-        return [x.candidate for x in votes]
-
-    def get_ballots(self):
-        ballot_box = []
-        for voter in self.electionvote_set.values("user").distinct():
-            user = User.objects.get(pk=voter["user"])
-            ballot = []
-            for vote in user.electionvote_set.filter(election=self).order_by('value'):
-                ballot.append(vote.candidate.user.username)
-            ballot_box.append(ballot)
-        random.shuffle(ballot_box)
-        return ballot_box
-
-
-class Candidate(models.Model):
-    user = models.ForeignKey(settings.AUTH_USER_MODEL)
-    election = models.ForeignKey(Election)
-
-    def __unicode__(self):
-        return str(self.user.username)
-
-class ElectionVote(models.Model):
-    election = models.ForeignKey(Election)
-    user = models.ForeignKey(settings.AUTH_USER_MODEL)
-    candidate = models.ForeignKey(Candidate)
-    value = models.IntegerField()
-
-    class Meta:
-        unique_together = (('election', 'user', 'candidate'),
-                    ('election', 'user', 'value'))
-
-    def __unicode__(self):
-        return u'In %s, user %s voted for %s for seat %d' % (self.election, self.user, self.candidate, self.value)
-
-
-class ElectionResult(models.Model):
-    election = models.OneToOneField('Election', related_name='result')
-    vote_count = models.IntegerField()
-
-
-class ElectionResultRow(models.Model):
-    election_result = models.ForeignKey('ElectionResult', related_name='rows')
-    candidate = models.ForeignKey('Candidate')
-    order = models.IntegerField()
-
-    class Meta:
-        ordering = ['order']
diff --git a/wasa2il/core/saml.py b/wasa2il/core/saml.py
deleted file mode 100644
index 495bc8ee..00000000
--- a/wasa2il/core/saml.py
+++ /dev/null
@@ -1,52 +0,0 @@
-
-import os
-import binascii
-
-from lxml import etree
-from StringIO import StringIO
-from suds.client import Client
-
-from django.conf import settings
-from django.contrib.auth import login
-from django.contrib.auth.models import User
-from django.http import HttpResponseRedirect
-
-
-class SamlException(Exception):
-    pass
-
-
-def get_saml(request, token):
-    # Fetch SAML info
-    AI = settings.SAML_1['AUTH']
-    client = Client(AI['wsdl'], username=AI['login'], password=AI['password'])
-    ipaddr = request.META.get('REMOTE_ADDR')
-    result = client.service.generateSAMLFromToken(token, ipaddr)
-
-    if result['status']['message'] != 'Success':
-        raise SamlException('%s' % result['status']['message'])
-
-    return result
-
-
-def parse_saml(saml):
-    # Parse the SAML and retrieve user info
-    tree = etree.parse(StringIO(saml))
-    namespaces = {'saml': 'urn:oasis:names:tc:SAML:1.0:assertion'}
-    name = tree.xpath('/saml:Assertion/saml:AttributeStatement/saml:Subject/saml:NameIdentifier[@NameQualifier="Full Name"]/text()', namespaces=namespaces)[0]
-    ssn = tree.xpath('/saml:Assertion/saml:AttributeStatement/saml:Attribute[@AttributeName="SSN"]/saml:AttributeValue/text()', namespaces=namespaces)[0]
-    return name, ssn
-
-
-def authenticate(request, redirect_url):
-    user = request.user
-    token = request.GET.get('token')
-
-    if not token:
-        return HttpResponseRedirect(redirect_url)
-
-    result = get_saml(request, token)
-    name, ssn = parse_saml(result['saml'])
-
-    return { 'ssn': ssn, 'name': name }
-
diff --git a/wasa2il/core/static/README b/wasa2il/core/static/README
deleted file mode 100755
index 8836fac6..00000000
--- a/wasa2il/core/static/README
+++ /dev/null
@@ -1 +0,0 @@
-Here be all the static files!
diff --git a/wasa2il/core/static/SIL Open Font License 1.1.txt b/wasa2il/core/static/SIL Open Font License 1.1.txt
deleted file mode 100755
index e4b0c4ff..00000000
--- a/wasa2il/core/static/SIL Open Font License 1.1.txt	
+++ /dev/null
@@ -1,91 +0,0 @@
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
-
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded, 
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting -- in part or in whole -- any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
\ No newline at end of file
diff --git a/wasa2il/core/static/css/League-Gothic-fontfacekit.zip b/wasa2il/core/static/css/League-Gothic-fontfacekit.zip
deleted file mode 100755
index ce63429e..00000000
Binary files a/wasa2il/core/static/css/League-Gothic-fontfacekit.zip and /dev/null differ
diff --git a/wasa2il/core/static/css/League_Gothic-webfont.eot b/wasa2il/core/static/css/League_Gothic-webfont.eot
deleted file mode 100755
index d6d203de..00000000
Binary files a/wasa2il/core/static/css/League_Gothic-webfont.eot and /dev/null differ
diff --git a/wasa2il/core/static/css/League_Gothic-webfont.svg b/wasa2il/core/static/css/League_Gothic-webfont.svg
deleted file mode 100755
index 15c4d173..00000000
--- a/wasa2il/core/static/css/League_Gothic-webfont.svg
+++ /dev/null
@@ -1,230 +0,0 @@
-
-
-
-
-This is a custom SVG webfont generated by Font Squirrel.
-Copyright   : Generated in 2009 by FontLab Studio Copyright info pending
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 
\ No newline at end of file
diff --git a/wasa2il/core/static/css/League_Gothic-webfont.ttf b/wasa2il/core/static/css/League_Gothic-webfont.ttf
deleted file mode 100755
index 193f3769..00000000
Binary files a/wasa2il/core/static/css/League_Gothic-webfont.ttf and /dev/null differ
diff --git a/wasa2il/core/static/css/League_Gothic-webfont.woff b/wasa2il/core/static/css/League_Gothic-webfont.woff
deleted file mode 100755
index 7496c875..00000000
Binary files a/wasa2il/core/static/css/League_Gothic-webfont.woff and /dev/null differ
diff --git a/wasa2il/core/static/css/application.css b/wasa2il/core/static/css/application.css
deleted file mode 100755
index 27bf61e1..00000000
--- a/wasa2il/core/static/css/application.css
+++ /dev/null
@@ -1,356 +0,0 @@
-/* css for timepicker */
-.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; }
-.ui-timepicker-div dl { text-align: left; }
-.ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; }
-.ui-timepicker-div dl dd { margin: 0 10px 10px 65px; }
-.ui-timepicker-div td { font-size: 90%; }
-.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; }
-
-.ui-timepicker-rtl{ direction: rtl; }
-.ui-timepicker-rtl dl { text-align: right; }
-.ui-timepicker-rtl dl dd { margin: 0 65px 10px 10px; }
-
-.search-form {
-	margin-top: 5px;
-}
-
-.img-negpad {
-	margin-top:	-5px;
-	margin-bottom:	-5px;
-}
-
-@font-face {
-    font-family: 'LeagueGothicRegular';
-    src: url('League_Gothic-webfont.eot');
-    src: url('League_Gothic-webfont.eot?#iefix') format('embedded-opentype'),
-         url('League_Gothic-webfont.woff') format('woff'),
-         url('League_Gothic-webfont.ttf') format('truetype'),
-         url('League_Gothic-webfont.svg#LeagueGothicRegular') format('svg');
-    font-weight: normal;
-    font-style: normal;
-}
-
-h1 { font-family: 'LeagueGothicRegular'; font-weight: normal; font-size: 400%; }
-h2 { font-family: 'LeagueGothicRegular'; font-weight: normal; font-size: 300%; }
-h3 { font-family: 'LeagueGothicRegular'; font-weight: normal; font-size: 250%; }
-a {
-	color: #644f80;
-}
-a:hover {
-	color: #47385b;
-}
-
-textarea {
-     width: 100%;
-     -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-     -moz-box-sizing: border-box;    /* Firefox, other Gecko */
-     box-sizing: border-box;         /* Opera/IE 8+ */
-}
-
-.right { text-align: right !important; }
-.date { text-align: right !important; }
-
-
-#statements_declarations ol li:before {
-	content: value(statement-counter);
-}
-
-#statements_declarations ol {
-	/* list-style-type:        lower-alpha; */
-	counter-reset: 			statement-counter -1;
-}
-
-.candidates {
-	min-height:			50px;
-}
-
-.candidates p {
-	padding:			10px;
-	border:				1px solid #ddd;
-}
-
-.candidates li {
-	border-bottom:		1px solid #ddd;
-	padding-bottom:		3px;
-	font-size:			15pt;
-}
-
-.candidates li div {
-	display:			block;
-    height: 50px;
-}
-
-.candidates li div:hover {
-	text-decoration:		none;
-}
-
-#candidates li {
-	display:			block;
-}
-
-.vote-button {
-    float: right;
-    margin-top: 12px;
-}
-
-.vote-image {
-    height: 50px;
-    width: 50px;
-}
-
-#votes {
-	background: 		#fff;
-	list-style-type:			decimal;
-}
-
-#votes li {
-	display:			list-item;
-}
-
-
-.memberstatusbox {
-	padding:			5px;
-	margin:				2px;
-	margin-right:			10px;
-	background:			#eee;
-	border:				1px solid #ddd;
-	border-radius:			3px;
-}
-
-.statementset ol {
-	margin-top: 5px;
-	margin-bottom: 5px;
-}
-.statementset#statements_references ol {
-	list-style-type: square;
-}
-.statementset#statements_assumptions ol {
-	list-style-type: upper-roman;
-}
-.statementset.empty-statementset ol {
-	list-style-type: none !important;
-	color: #999;
-	font-style: italic;
-}
-
-.document-fragment {
-	border: 1px solid #eee;
-	padding: 3px;
-	margin: 1em 3px;
-	border-radius: 3px;
-}
-
-.statement_subheading {
-	/*list-style-type:		none;*/
-	counter-increment:		statement-counter;
-	font-size:				150%;
-	margin-top:				6px;
-	margin-bottom:			2px;
-}
-
-.state_buttons {
-	display:                none;
-}
-
-.statementset li {
-	padding: 5px;
-	margin: 3px;
-	border-radius: 5px;
-}
-
-.statementset li:hover {
-	background:		#eee;
-}
-
-.statementset .mover {
-	display: none;
-}
-
-.statementset li:hover .mover {
-	display: block;
-	float: right;
-	cursor: move;
-}
-
-.statementset li:hover > .state_buttons {
-	display:                block;
-	position:				absolute;
-	padding-left:			50px;
-}
-
-.helptext {
-	font-size:	8pt;
-	color:	#888;
-}
-
-ul.nobullets {
-	list-style-type: none;
-}
-
-.thumbnail {
-	float:		left;
-	position:	relative;
-	width:		80px;
-	height:		80px;
-	display:	inline-block;
-	margin:		3px;
-	overflow:	hidden;
-}
-
-.thumbnail.member {
-}
-.thumbnail.member > img {
-	position: absolute;
-	z-index: -1;
-}
-.thumbnail.member .title {
-	position: absolute;
-	margin-right: auto;
-	margin-left: auto;
-	width: 80px;
-	bottom: 0;
-	text-align: center;
-	background: rgba(255,255,255, 0.5);
-}
-
-.mainaction {
-	display:	block;
-	background:	#ccf;
-	border:		1px solid #7768cc;
-	border-radius:	15px;
-	padding:	30px;
-	color:		#000;
-	min-height:	100px;
-}
-
-.mainaction:hover {
-	text-decoration: none;
-	background:	#eef;
-}
-
-
-footer {
-	font-size:	10pt;
-	margin-top:	30px;
-}
-
-
-.subnav {
-	background:	#ccf;
-	border-radius:	5px;
-	padding-left:	10px;
-	padding-right:	10px;
-	margin-bottom:	20px;
-}
-
-
-.subnav > .nav {
-	margin-bottom:	0px;
-}
-
-
-.icon-grey { color: #c0d0c0 }
-
-#legal-text { font-family: serif; font-size: 12px; border: 1px #e0e0e0 solid; padding: 12px 24px 12px 24px; margin-bottom: 10px; text-align: justify; }
-#legal-text h1 { font-family: inherit; font-size: 28px; font-weight: bold; margin: 4px 0px 4px 4px; }
-#legal-text h2 { font-family: inherit; font-size: 18px; font-weight: bold; margin: 4px 0px 4px 4px; }
-#legal-text ol { margin: 12px 0px 0px 24px; }
-#legal-text li { padding: 0px 0px 0px 0px; margin: 0px 0px 18px 0px; }
-
-#legal-text-editor { width: 100%; height: 400px; font-family: Courier New; }
-#legal-text-editor-preview { width: 100%; }
-
-#legal-text-diff ins { background-color: #aaffaa !important; }
-#legal-text-diff del { background-color: #ffaaaa !important; }
-
-.legal-text-container #siblings-container { float: right; border-bottom: 1px solid #e0e0e0; border-left: 1px solid #e0e0e0; background: #f8f8f8; padding: 0px 0px 0px 12px; }
-.legal-text-container #siblings { margin: 0px; }
-
-#diff-content { overflow: inherit; white-space: pre-wrap; }
-
-#propose-change .comments { width: 100%; }
-#propose-change .comments textarea { width: 100%; }
-
-.instructions img.screenshot { border: 1px solid #a0a0a0; display: block; max-width: 800px; margin-top: 48px; margin-bottom: 48px; }
-.instructions p, .instructions ul { margin-top: 16px; margin-bottom: 16px; max-width: 800px; }
-.instructions ul li { padding-bottom: 12px; }
-.instructions .instructions-nav p { margin: 0px; }
-.instructions .instructions-nav label { margin: 0px; padding: 0px; float: left; width: 100px; clear: both; }
-
-.profile-document-data { margin-top: 12px; }
-.profile-document-data p.document { font-weight: bold; font-size: 18px; }
-.profile-document-data p.documentcontent { padding-left: 18px; }
-.profile-document-data p.documentcontent small { padding-left: 8px; }
-.profile .img-polaroid { float: right; border: 1px solid #606060; margin-left: 16px; margin-bottom: 16px; }
-.profile .tab-content { padding-top: 16px; text-align: justify; }
-
-#newissues_list .issue-status { white-space: nowrap; }
-
-.comment {
-	background: #f7f7f7;
-	margin:		2px;
-	padding:	2px;
-	padding-left: 5px;
-	padding-right: 5px;
-	border-radius:	3px;
-}
-
-.comment_created_by {
-	font-size:	9pt;
-	font-weight: bold;
-	display:	inline;
-	margin-right:	7px;
-}
-
-.comment_created {
-	font-size:	8pt;
-	color:		#888;
-}
-
-.comment_content {
-	display:	inline;
-}
-
-
-ul.errorlist {
-	color: #ff0000;
-	list-style: none;
-	margin-left: 4px;
-}
-#content {
-	visibility: hidden;
-	-webkit-box-shadow: 3px 3px 13px rgba(50, 50, 50, 0.75);
-	-moz-box-shadow:    3px 3px 13px rgba(50, 50, 50, 0.75);
-	box-shadow:         3px 3px 13px rgba(50, 50, 50, 0.75);
-	margin-top: 2em;
-	margin-bottom: 2em;
-	border: 1px solid transparent;
-	border-radius: 5px;
-    height: 350px;
-}
-
-.document {
-	position: relative;
-}
-
-#content_org {
-	display: none;
-}
-#content_diff {
-	display: none;
-}
-
-#content_diff ins {
-	background-color: #aaffaa !important;
-}
-#content_diff del {
-	background-color: #ffaaaa !important;
-}
-
-#versions .current {
-	font-weight: bold;
-}
-
-.glyphicon { cursor: pointer; }
-.dropdown-menu { cursor: pointer; }
-
diff --git a/wasa2il/core/static/demo.html b/wasa2il/core/static/demo.html
deleted file mode 100755
index d60201b3..00000000
--- a/wasa2il/core/static/demo.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-	
-
-	Font Face Demo
-	
-	
-
-
-
-	
-

Font-face Demo for the League Gothic Font

- - - -

League Gothic Regular - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- -
- - diff --git a/wasa2il/core/static/img/pplogo.png b/wasa2il/core/static/img/pplogo.png deleted file mode 100755 index daca2d01..00000000 Binary files a/wasa2il/core/static/img/pplogo.png and /dev/null differ diff --git a/wasa2il/core/static/js/csrf.js b/wasa2il/core/static/js/csrf.js deleted file mode 100755 index ad150b88..00000000 --- a/wasa2il/core/static/js/csrf.js +++ /dev/null @@ -1,14 +0,0 @@ -$(function () { - function csrfSafeMethod(method) { - // these HTTP methods do not require CSRF protection - return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); - } - $.ajaxSetup({ - crossDomain: false, // obviates need for sameOrigin test - beforeSend: function(xhr, settings) { - if (!csrfSafeMethod(settings.type)) { - xhr.setRequestHeader("X-CSRFToken", $.cookie('csrftoken')); - } - } - }); -}); diff --git a/wasa2il/core/static/js/document.js b/wasa2il/core/static/js/document.js deleted file mode 100755 index c5950565..00000000 --- a/wasa2il/core/static/js/document.js +++ /dev/null @@ -1,17 +0,0 @@ - -var file = {'name': 'foobar.txt', defaultContent: 'Hello World'}; - -$(function () { - - var content = $('#content'), - content_org = $('#content_org'), - editor = undefined, - previewer = undefined, - line_height = undefined, - opts = { - container: content[0], - basePath: '/static', - file: file - }; - -}); diff --git a/wasa2il/core/static/js/jquery-1.11.3.min.js b/wasa2il/core/static/js/jquery-1.11.3.min.js deleted file mode 100644 index 0f60b7bd..00000000 --- a/wasa2il/core/static/js/jquery-1.11.3.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! jQuery v1.11.3 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1; - -return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ha=/^\s+/,ia=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ja=/<([\w:]+)/,ka=/\s*$/g,ra={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?""!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ua(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m("