diff --git a/.gitignore b/.gitignore
index aa226fc..8d7afe7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,5 +15,5 @@ node_modules/
.DS_Store
__pycache__
.cache/
-Cog
.vscode/
+.env
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 420b7dc..8c050c4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,64 +1,4 @@
Contributing
============
-All kinds of contributions to Cog are greatly appreciated. For someone
-unfamiliar with the code base, the most efficient way to contribute is usually
-to submit a [feature request](#feature-requests) or [bug report](#bug-reports).
-
-If you want to dive into the source code, you can submit a [patch](#patches) as
-well. Working on [existing issues][issues] is super helpful!
-
-Feature Requests
-----------------
-
-Do you have an idea for an awesome new feature for Cog? Please [submit a
-feature request][issue]. It's great to hear about new ideas.
-
-If you are inclined to do so, you're welcome to [fork][fork] Cog, work on
-implementing the feature yourself, and submit a patch. In this case, it's
-*highly recommended* that you first [open an issue][issue] describing your
-enhancement to get early feedback on the new feature that you are implementing.
-This will help avoid wasted efforts and ensure that your work is incorporated
-into the code base.
-
-Bug Reports
------------
-
-Did something go wrong with Cog? Sorry about that! Bug reports are greatly
-appreciated!
-
-When you [submit a bug report][issue], please include relevant information such
-as Cog version, operating system, configuration, error messages, and steps to
-reproduce the bug. The more details you can include, the easier it is to find
-and fix the bug.
-
-Patches
--------
-
-Want to hack on Cog? Awesome!
-
-If there are [open issues][issues], you're more than welcome to work on those -
-this is probably the best way to contribute to Cog. If you have your own
-ideas, that's great too! In that case, before working on substantial changes to
-the code base, it is *highly recommended* that you first [open an issue][issue]
-describing what you intend to work on.
-
-Patches should be submitted as Github pull requests.
-
-Any changes to the code base should follow the style and coding conventions
-used in the rest of the project. The version history should be clean, and
-commit messages should be descriptive. Please run the included tests to ensure
-that nothing has broken, and, if applicable, we recommend writing tests for
-any new features you add!
-
----
-
-If you have any questions about anything, feel free to [ask][email]!
-
-_Thanks to Anish Athalye for letting us borrow this contribution guide from [Gavel](https://github.com/anishathalye/gavel)._
-
-[issue]: https://github.com/techx/cog/issues/new
-[issues]: https://github.com/techx/cog/issues
-[fork]: https://github.com/techx/cog/fork
-[email]: mailto:me@noahmoroze.com
-[gavel]: https://github.com/anishathalye/gavel
\ No newline at end of file
+Contributions are not welcome at this time. If you would like to contribute, please visit the [original cog repo](https://github.com/techx/cog).
\ No newline at end of file
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 2f665cd..66d6fdc 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -1,27 +1,18 @@
# Development
## Setup
-- Set up a Python2
-[virtualenv](http://python-guide-pt-br.readthedocs.io/en/latest/dev/virtualenvs/)
-to manage Python dependencies
-- Source your virtualenv
-- Run `pip install -r requirements.txt` to install all dependencies
-- Install [PostgreSQL](https://www.postgresql.org/download/) to run a database locally
- - If you're using Mac, install *Postgres.app* from
- [here](https://www.postgresql.org/download/)
-- Set three environment variables:
- - `DATABASE_URL` points to the URL of a development database,
-which has to be set up using Postgres on your system. A sample `DATABASE_URL`
-could look like `postgres://username:password@localhost/cog`.
- - `QUILL` is the URL to your Quill instance for auth.
- - `SECRET` needs to be the same JWT secret used in your Quill instance.
-- Run `python initialize.py`
- - This initializes the database - run it if you make any changes to the models and
- are fine with overwriting data.
+- Install Docker
+- Install Docker Compose
+- Copy `sample.env` to `.env` and enter in the proper values
+- `make migrate` to initialize and set up the db
+
+## Build
+- If you need to rebuild (in case you change the Dockerfile), run `make build`
## Running
-- Run `make run`
-- The site will be visible at `localhost:8000`
+- Run `make start`
+- The site will be visible at `localhost:80`
+- `make logs` for a live stream of logs.
-## Tests
-- Run `make test` to run all tests
+## Destroying
+- Run `make stop` to destroy
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index ff58fb9..e3eb8d6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
-FROM python:2
+FROM python:3.6.3
-ARG APP_PATH=/hardware-checkout
+ARG APP_PATH=/cog
WORKDIR $APP_PATH
@@ -9,6 +9,6 @@ RUN pip install -r requirements.txt
ADD . $APP_PATH
-EXPOSE 8000
-CMD ["gunicorn", "--bind", ":8000", "-k", "geventwebsocket.gunicorn.workers.GeventWebSocketWorker", "hardwarecheckout:app"]
-
+EXPOSE $FLASK_RUN_PORT
+CMD ["python", "runserver.py"]
+# CMD ["flask", "run"]
diff --git a/HEROKU.md b/HEROKU.md
new file mode 100644
index 0000000..50382ba
--- /dev/null
+++ b/HEROKU.md
@@ -0,0 +1,2 @@
+# Deploying on Heroku
+After deploying on Heroku, you should go into the console and then run `python initialize.py` to properly set up the database. (WARNING: doing this will clear all existing data in the database)
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 9056773..5391247 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,21 @@
-test:
- python -m pytest tests/
+build:
+ docker-compose build
-PORT = 8000
-run:
- gunicorn --bind 0.0.0.0:$(PORT) -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker hardwarecheckout:app
+sass:
+ cd cog/static && sass --watch sass/app.scss:css/app.css
+start:
+ docker-compose up -d
+ @echo "cog listening on port 8000, postgres on 5432"
+ @echo "run 'make logs' to watch logs"
+stop:
+ docker-compose down
+
+# watch the logs from cog
+logs:
+ docker-compose logs -f -t cog
+
+# run all the migrations
+migrate:
+ docker-compose run cog python initialize.py
+ # db/containers still running
\ No newline at end of file
diff --git a/Procfile b/Procfile
index 0de4e17..06a286c 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1 @@
-web: python runserver.py $PORT
\ No newline at end of file
+web: python runserver.py
\ No newline at end of file
diff --git a/README.md b/README.md
index b370610..a23c204 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,13 @@
-# Cog
+**THIS IS A FORKED, IN-DEVELOPMENT VERSION OF COG**
+
+If you're looking for a working copy of the original cog, please visit https://github.com/techx/cog.
-[](https://heroku.com/deploy?template=https://github.com/techx/cog)
+---
+
+# Cog
Cog is a hardware checkout system for hackathons, originally written for use
-at HackMIT and MakeMIT.
+at HackMIT and MakeMIT, now forked by Hack the North.

@@ -29,39 +33,20 @@ View, approve, and fulfill item requests in real-time as they come in. As
soon as an organizer approves a request, hackers can see that their item is
ready to be picked up.
-### Quill-Integrated Login
-Users login using credentials from an associated
-[Quill](https://github.com/techx/quill) instance, forgoing the need to create
-an additional account.
-
## Deployment & Configuration
-The easiest way to deploy Cog is to smash this Deploy to Heroku button right
-here:
-[](https://heroku.com/deploy?template=https://github.com/techx/cog)
-
-If you're interested in deploying on other infrastructure, that should be
-doable as well. Cog is written in Python 2, and all dependencies can easily
-be installed using Pip via `requirements.txt`. Cog uses PostgreSQL as a
-database. Deployments of Cog generally use Gunicorn as a web server
-(alongside gevent or eventlet for handling websockets). The exception to this
-is Cog's default Heroku configuration which uses the built in
-[Flask-SocketIO](https://flask-socketio.readthedocs.io/en/latest/) web server
-due to performance issues using Gunicorn on Heroku.
+Cog is written in Python 3 (at least our fork partially is) and uses PostgreSQL as a
+database. Hack the North's fork of Cog is configured with `docker-compose` for local development and deploys to Kubernetes through Skaffold.
A myriad of configuration options are available to be tweaked in
-[`config.py`](hardwarecheckout/config.py). Alternatively, all values set in
+[`config.py`](cog/config.py). Alternatively, all values set in
this file can be set as environment variables of the same name - environment
variable values will take precedence over the value specified in `config.py`.
Sensible defaults are in place for all of the event logistical settings, but
we recommend playing around with them a bit. At the bare minimum you
-should change the `HACKATHON_NAME` and set your `QUILL` and `SECRET` env
-variables to match the associated Quill instance.
+should change the `HACKATHON_NAME` and set your `SECRET` env
+variables.
-We strongly recommend deploying Cog and experimenting with/testing your
-desired configuration options **in advance** of your event to ensure it
-behaves in a manner consistent with the logistical organization of your
-event.
### Adding Hardware via Google Sheets
While you can add individual items one-by-one, we recommend creating a
@@ -80,7 +65,7 @@ Branding can easily be customized using Semantic UI
[themes](https://semantic-ui.com/usage/theming.html).
While Cog mostly uses default Semantic UI styling, a minimal amount of custom
-CSS lives in `hardwarecheckout/static/sass/app.scss`. In order to rebuild the
+CSS lives in `cog/static/sass/app.scss`. In order to rebuild the
CSS when the Sass is changed, install [Sass](https://sass-lang.com/) and run
`sass --watch sass:css` in the `/static` directory.
@@ -88,6 +73,8 @@ CSS when the Sass is changed, install [Sass](https://sass-lang.com/) and run
[users list](https://github.com/techx/cog/wiki/Cog-Users)!*
## Development
+**Here be dragons**
+
Interested in hacking on Cog? Check out the [development guide](DEVELOPMENT.md)
for some steps to get you started.
@@ -97,6 +84,9 @@ contributions - have a look at our [contributing guide](CONTRIBUTING.md) for
information on how you can get involved.
## Acknowledgements
+
+**Pre-fork acknowledgements**
+
Thanks to the following folks for their contributions to Cog pre-open
sourcing:
- [Ethan Weber](https://github.com/ethanweber) and [Albert
diff --git a/app.json b/app.json
index db73e2c..1428af7 100644
--- a/app.json
+++ b/app.json
@@ -3,11 +3,8 @@
"description": "A hackathon hardware check-out system by HackMIT",
"repository": "https://github.com/techx/cog",
"env": {
- "QUILL": {
- "description": "URL of Quill instance (for auth integration)"
- },
"SECRET": {
- "description": "Random Secret for JWTs - must match Quill secret"
+ "description": "Random Secret for JWTs"
},
"DEBUG": {
"description": "Toggle Flask debug mode. Should always be False in production",
@@ -59,17 +56,22 @@
},
"LOTTERY_TEXT": {
"description": "The info text underneath the 'Lottery Required' section",
- "value": "We have a limited quantity of these items. Please fill out a brief proposal describing your project idea, and we'll randomly accept as many requests as we can 30 minutes after hacking starts.",
+ "value": "We have a limited quantity of these items. Please fill out a brief proposal describing your project idea by 12:30. If you are randomly selected to hack on one of these items, we will call you to the desk by text.",
"required": false
},
"CHECKOUT_TEXT": {
"description": "The info text underneath the 'Checkout Required' section",
- "value": "Click to request any of these items, and your request will be approved when we have one available. Keep in mind we will ask to hold on to a form of ID until the item is returned.",
+ "value": "Click to request any of these items. We will text you when your hardware is ready for pickup. Keep in mind we will ask to hold on to a form of ID until the item is returned.",
"required": false
},
"FREE_TEXT": {
"description": "The info text underneath the 'No Checkout Required' section",
- "value": "Just come to the hardware desk and ask for any of these items!",
+ "value": "Pick these up from the tool shop at any time. Please don't take more than you need, and return the items at the end of the event!",
+ "required": false
+ },
+ "MLH_TEXT": {
+ "description": "The info text underneath the 'MLH Item' section",
+ "value": "If you would like to sign out any of these items, request them through the MLH portal, then wait in the MLH line.",
"required": false
}
},
diff --git a/hardwarecheckout/__init__.py b/cog/__init__.py
similarity index 63%
rename from hardwarecheckout/__init__.py
rename to cog/__init__.py
index a0d8967..b0bb0c4 100644
--- a/hardwarecheckout/__init__.py
+++ b/cog/__init__.py
@@ -1,18 +1,40 @@
import pytz
import os
+import random
+import string
-from flask import Flask
-from flask_socketio import SocketIO
-from urlparse import urlsplit
+from flask import Flask, session, request, abort
+# from flask_socketio import SocketIO
+from urllib.parse import urlsplit
from flaskext.markdown import Markdown
-from hardwarecheckout.utils import display_date, deltatimeformat
-from hardwarecheckout.models.socket import Socket
+from cog.utils import display_date, deltatimeformat
+# from cog.models.socket import Socket
from flask_sslify import SSLify
app = Flask(__name__)
-import hardwarecheckout.config as config
+app.config['SQLALCHEMY_POOL_SIZE'] = 2
+
+import cog.config as config
+
+
+app.secret_key = config.SECRET
+
+@app.before_request
+def csrf_protect():
+ if request.method == "POST":
+ token = session['_csrf_token']
+ if not token or (token != request.form.get('_csrf_token') and token != request.headers.get('x-csrftoken')):
+ abort(403)
+
+def generate_csrf_token():
+ if '_csrf_token' not in session:
+ # generate random CSRF token
+ session['_csrf_token'] = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(32)])
+ return session['_csrf_token']
+
+app.jinja_env.globals['csrf_token'] = generate_csrf_token
def get_conf_bool(variable):
val = os.environ.get(variable, getattr(config, variable))
@@ -39,8 +61,6 @@ def set_conf_int(app, variable):
set_conf_str(app, 'HACKATHON_NAME')
app.config['APP_NAME'] = app.config['HACKATHON_NAME'] + ' Hardware Checkout'
-app.config['QUILL_URL'] = config.QUILL_URL
-app.config['QUILL_URL_READABLE'] = urlsplit(app.config['QUILL_URL']).netloc
# Debug
app.config['TEMPLATES_AUTO_RELOAD'] = True
@@ -61,9 +81,10 @@ def set_conf_int(app, variable):
set_conf_str(app, 'LOTTERY_TEXT')
set_conf_str(app, 'CHECKOUT_TEXT')
set_conf_str(app, 'FREE_TEXT')
+set_conf_str(app, 'MLH_TEXT')
set_conf_int(app, 'LOTTERY_CHAR_LIMIT')
-from hardwarecheckout.models import db
+from cog.models import db
db.app = app
db.init_app(app)
@@ -73,14 +94,15 @@ def set_conf_int(app, variable):
if get_conf_bool("FORCE_SSL"):
SSLify(app)
-socketio = SocketIO()
-socketio.init_app(app)
+# socketio = SocketIO()
+# socketio.init_app(app)
-import hardwarecheckout.controllers # registers controllers
+import cog.controllers # registers controllers
+import cog.filters
# delete stale sockets from previous open sessions
try:
- Socket.query.delete()
+ # Socket.query.delete()
db.session.commit()
except:
# exception if DB not yet initialized
diff --git a/hardwarecheckout/config.py b/cog/config.py
similarity index 59%
rename from hardwarecheckout/config.py
rename to cog/config.py
index ff8a679..a9f7e51 100644
--- a/hardwarecheckout/config.py
+++ b/cog/config.py
@@ -7,12 +7,9 @@
# Postgres SQL Connection String.
# Note: This is the default env variable name
# used for Heroku postgres deploys.
-DB_URI = os.environ['DATABASE_URL']
+DB_URI = os.environ['DATABASE_URL']
-# URL of Quill instance (for auth integration)
-QUILL_URL = os.environ['QUILL']
-
-# Random Secret for JWTs - must match Quill secret
+# Random Secret for sessions
SECRET = os.environ['SECRET']
@@ -23,6 +20,9 @@
## Deploy settings ##
+# HackerAPI event slug
+EVENT_SLUG = 'hackthenorth2019'
+
# Enable/disable Flask debug mode. Should be set to
# False on production deploys of Cog.
DEBUG = True
@@ -36,7 +36,7 @@
## Metadata ##
-HACKATHON_NAME = "HackMIT"
+HACKATHON_NAME = "Hack Better"
## Event logistical settings ##
@@ -71,15 +71,31 @@
LOTTERY_MULTIPLE_SUBMISSIONS = False
# The info text underneath the 'Lottery Required' section
-LOTTERY_TEXT = """We have a limited quantity of these items. Please fill out a
-brief proposal describing your project idea, and we'll randomly accept as many
-requests as we can 30 minutes after hacking starts."""
+LOTTERY_TEXT = """We have a limited quantity of these items.
+Please fill out a brief proposal describing your project idea by Friday at 10:30.
+If you are randomly selected to hack on one of these items, we will call you to the desk by Slack message."""
# The info text underneath the 'Checkout Required' section
-CHECKOUT_TEXT = """Click to request any of these items, and your request will
-be approved when we have one available. Keep in mind we will ask to hold on
-to a form of ID until the item is returned."""
+CHECKOUT_TEXT = """Click to request any of these items.
+We will text you when your hardware is ready for pickup.
+Keep in mind we will ask to hold on to a form of ID until the item is returned."""
# The info text underneath the 'No Checkout Required' section
-FREE_TEXT = """Just come to the hardware desk and ask for any of these
-items!"""
\ No newline at end of file
+FREE_TEXT = """Pick these up from the tool shop at any time.
+Please don't take more than you need, and return the items at the end of the event!"""
+
+MLH_TEXT = """If you would like to sign out any of these items, request them through the MLH portal, then wait in the MLH line."""
+
+## Email notification settings ##
+
+EMAIL_SENDER = 'hello@treehacks.com'
+EMAIL_SENDER_NAME = 'TreeHacks'
+SMTP_USERNAME = os.getenv('SMTP_USERNAME')
+SMTP_PASSWORD = os.getenv('SMTP_PASSWORD')
+SMTP_HOST = 'email-smtp.us-east-1.amazonaws.com'
+SMTP_PORT = 587
+EMAIL_SUBJECT = "TreeHacks - Your hardware checkout is ready"
+
+## Slack notification settings ##
+
+SLACK_OAUTH_ACCESS_TOKEN = os.getenv('SLACK_OAUTH_ACCESS_TOKEN')
\ No newline at end of file
diff --git a/hardwarecheckout/constants.py b/cog/constants.py
similarity index 100%
rename from hardwarecheckout/constants.py
rename to cog/constants.py
diff --git a/cog/controllers/__init__.py b/cog/controllers/__init__.py
new file mode 100644
index 0000000..65fb5d9
--- /dev/null
+++ b/cog/controllers/__init__.py
@@ -0,0 +1,5 @@
+import cog.controllers.default
+import cog.controllers.inventory
+import cog.controllers.request
+import cog.controllers.user
+import cog.controllers.login
diff --git a/hardwarecheckout/controllers/default.py b/cog/controllers/default.py
similarity index 81%
rename from hardwarecheckout/controllers/default.py
rename to cog/controllers/default.py
index 42a050c..1eb4f5d 100644
--- a/hardwarecheckout/controllers/default.py
+++ b/cog/controllers/default.py
@@ -1,4 +1,4 @@
-from hardwarecheckout import app
+from cog import app
import os
@@ -8,7 +8,7 @@
redirect,
render_template
)
-from hardwarecheckout.utils import requires_auth
+from cog.utils import requires_auth
@app.route('/favicon.ico')
def favicon():
diff --git a/hardwarecheckout/controllers/inventory.py b/cog/controllers/inventory.py
similarity index 90%
rename from hardwarecheckout/controllers/inventory.py
rename to cog/controllers/inventory.py
index 35c7ab4..9cf5ab0 100644
--- a/hardwarecheckout/controllers/inventory.py
+++ b/cog/controllers/inventory.py
@@ -1,30 +1,28 @@
-from hardwarecheckout import app
-from hardwarecheckout.models import db
+from cog import app
+from cog.models import db
-from hardwarecheckout.controllers.request import request_update
+from cog.controllers.request import request_update
-from hardwarecheckout.models.item import Item
-from hardwarecheckout.models.inventory_entry import InventoryEntry
-from hardwarecheckout.models.inventory_entry import ItemType
-from hardwarecheckout.models.user import User
-from hardwarecheckout.models.request import Request, RequestStatus
-from hardwarecheckout.models.request_item import RequestItem
+from cog.models.item import Item
+from cog.models.inventory_entry import InventoryEntry
+from cog.models.inventory_entry import ItemType
+from cog.models.user import User
+from cog.models.request import Request, RequestStatus
+from cog.models.request_item import RequestItem
-from hardwarecheckout.forms.inventory_form import InventoryForm
-from hardwarecheckout.forms.inventory_update_form import InventoryUpdateForm
-from hardwarecheckout.forms.inventory_import_form import InventoryImportForm
+from cog.forms.inventory_form import InventoryForm
+from cog.forms.inventory_update_form import InventoryUpdateForm
+from cog.forms.inventory_import_form import InventoryImportForm
-from hardwarecheckout.utils import requires_auth, requires_admin, auth_optional
+from cog.utils import requires_auth, requires_admin, auth_optional
-from hardwarecheckout.sheets_csv import get_csv, SheetsImportError
+from cog.sheets_csv import get_csv, SheetsImportError
from flask import jsonify
from werkzeug.datastructures import MultiDict
-from sqlalchemy import func
+from sqlalchemy import func, and_
-import urllib2
-import urlparse
import requests
from bs4 import BeautifulSoup
@@ -51,6 +49,8 @@ def inventory():
item_type = ItemType.CHECKOUT)
free_query = InventoryEntry.query.filter_by(
item_type = ItemType.FREE)
+ mlh_query = InventoryEntry.query.filter_by(
+ item_type = ItemType.MLH)
# total number of items that exist by id
total_item_quants = db.session.query(Item.entry_id, func.count(Item.entry_id))\
@@ -82,6 +82,7 @@ def inventory():
lottery_items = lottery_query.all(),
checkout_items = checkout_query.all(),
free_items = free_query.all(),
+ mlh_items = mlh_query.all(),
counts = counts,
requests = requests,
RequestStatus=RequestStatus, user=user)
@@ -90,6 +91,7 @@ def inventory():
lottery_items = lottery_query.filter_by(is_visible = True).all(),
checkout_items = checkout_query.filter_by(is_visible = True).all(),
free_items = free_query.filter_by(is_visible = True).all(),
+ mlh_items = mlh_query.all(),
counts = counts,
requests = requests,
RequestStatus=RequestStatus, user=user)
@@ -233,6 +235,8 @@ def create_item(form):
item_type = ItemType.CHECKOUT
elif form.item_type.data == 'lottery':
item_type = ItemType.LOTTERY
+ elif form.item_type.data == 'mlh':
+ item_type = ItemType.MLH
image = url_for('static', filename='images/default.png')
if form.image.data != '': image = form.image.data
@@ -348,6 +352,8 @@ def inventory_update(id):
item_type = ItemType.CHECKOUT
elif form.item_type.data == 'lottery':
item_type = ItemType.LOTTERY
+ elif form.item_type.data == 'mlh':
+ item_type = ItemType.MLH
item.item_type = item_type
@@ -409,6 +415,15 @@ def inventory_return(id):
success=False,
message='No user for item.'
)
+
+ request = Request.query.filter(and_(
+ Request.items.any(entry_id=item.entry_id),
+ Request.status == RequestStatus.FULFILLED,
+ Request.user_id == user.id
+ )).first()
+
+ if request == None:
+ request.status = RequestStatus.RETURNED
item.user = None
return_id = False
diff --git a/cog/controllers/login.py b/cog/controllers/login.py
new file mode 100644
index 0000000..54882bc
--- /dev/null
+++ b/cog/controllers/login.py
@@ -0,0 +1,86 @@
+from cog import app
+from cog.config import SECRET, EVENT_SLUG
+from cog.models.user import *
+from cog.utils import get_profile_from_jwt
+import requests
+import datetime
+import json
+from urllib.parse import urljoin
+from flask import (
+ redirect,
+ render_template,
+ request,
+ url_for
+)
+import os
+
+def check_role(roles, role):
+ return any(
+ d['event_slug'] == EVENT_SLUG and
+ d['role'] == role
+ for d in roles
+ )
+
+def get_hacker(token, is_organizer):
+ if is_organizer == False:
+ req = requests.get('https://hackerapi.com/v2/events/hackthenorth2019/applications/me?token=' + token)
+ if req.ok:
+ r = json.loads(req.text)
+ return r
+ return dict()
+
+COOKIE_NAME = '__hackerapi-token-client-only__'
+
+@app.route('/login', methods=['GET', 'POST'])
+def login_page():
+ if request.method == 'GET':
+ response = app.make_response(render_template('pages/login.html'))
+ return response
+ # POST
+
+ jwt = request.headers.get('Authorization')
+ # Attempt to grab the user details
+ profile, _ = get_profile_from_jwt(jwt)
+ if not profile:
+ return 'unauthorized jwt', 401
+ is_organizer = "admin" in profile.get("groups", []) or "hardware_admin" in profile.get("groups", [])
+
+ if not is_organizer and profile.get("status") != "admission_confirmed":
+ return 'user is not admin or ADMISSION_CONFIRMED status', 403
+
+ hackerapi_id = profile["id"]
+
+ first_name = profile.get("first_name", "")
+ last_name = profile.get("last_name", "")
+ name = first_name + " " + last_name
+ email = profile.get("email")
+
+ user = User.query.filter_by(hackerapi_id=hackerapi_id).first()
+
+ if user == None:
+ user = User(hackerapi_id, email, name, None, is_organizer)
+ user.first_name = first_name
+ user.last_name = last_name
+ db.session.add(user)
+ else:
+ if name != '':
+ user.name = name
+ user.email = email
+ user.is_organizer = is_organizer
+
+ db.session.commit()
+
+ response = app.make_response("")
+ response.set_cookie('jwt', jwt)
+ return response
+
+ # Send user to error page.
+ # response = app.make_response(render_template('pages/login.html?error=1'))
+ # return response
+
+
+@app.route('/logout')
+def logout():
+ """Log user out"""
+ response = app.make_response(redirect(os.getenv("LOGIN_URL") + "/logout"))
+ return response
\ No newline at end of file
diff --git a/hardwarecheckout/controllers/request.py b/cog/controllers/request.py
similarity index 72%
rename from hardwarecheckout/controllers/request.py
rename to cog/controllers/request.py
index fa96978..679c1fe 100644
--- a/hardwarecheckout/controllers/request.py
+++ b/cog/controllers/request.py
@@ -1,16 +1,17 @@
-from hardwarecheckout import app
-from hardwarecheckout import socketio
-from hardwarecheckout.models import db
-
-from hardwarecheckout.models.request import Request, RequestStatus
-from hardwarecheckout.models.inventory_entry import InventoryEntry
-from hardwarecheckout.models.inventory_entry import ItemType
-from hardwarecheckout.models.user import User
-from hardwarecheckout.models.item import Item
-from hardwarecheckout.models.request_item import RequestItem
-from hardwarecheckout.models.socket import Socket
-
-from hardwarecheckout.utils import requires_auth, requires_admin, verify_token
+from cog import app
+# from cog import socketio
+from cog.models import db
+from cog.config import EMAIL_SUBJECT
+from cog.models.request import Request, RequestStatus
+from cog.models.inventory_entry import InventoryEntry
+from cog.models.inventory_entry import ItemType
+from cog.models.user import User
+from cog.models.item import Item
+from cog.models.request_item import RequestItem
+# from cog.models.socket import Socket
+from cog.tools.email import send_email
+from cog.tools.slack import send_slack
+from cog.utils import requires_auth, requires_admin, verify_token
from sqlalchemy import event
from flask import (
@@ -18,6 +19,7 @@
request,
redirect,
render_template,
+ render_template_string,
jsonify
)
@@ -37,12 +39,6 @@ def get_requests():
@requires_auth()
def request_submit():
"""Submits new request"""
- if not (user.location and user.phone):
- return jsonify(
- success=False,
- message="""Please fill out your user info before
- requesting items!"""
- )
proposal = request.form.get('proposal', '')
requested_quantity = int(request.form.get('quantity', 1))
@@ -91,7 +87,7 @@ def request_submit():
message='Out of stock!'
)
- for _ in xrange(requested_quantity):
+ for _ in range(requested_quantity):
item = RequestItem(
InventoryEntry.query.get(request.form['item_id']),
1)
@@ -148,16 +144,27 @@ def request_approve(id):
quantity = request_item.quantity
# get items of proper type
- for _ in xrange(quantity):
+ for _ in range(quantity):
if entry.quantity < quantity:
return jsonify(
success=False,
message='Out of stock!'
)
request_update(id, RequestStatus.APPROVED)
- return jsonify(
- success=True,
- )
+ ctx = {
+ "request_items": r.items,
+ "user": user
+ }
+ notification_sent = send_slack(user.email, render_template("messages/slack_message.html", **ctx)) or send_email(user.email, render_template_string(EMAIL_SUBJECT, **ctx), render_template("messages/email_message.html", **ctx))
+ if notification_sent:
+ return jsonify(
+ success=True
+ )
+ else:
+ return jsonify(
+ success=True,
+ message="Request was approved, but notification failed. You will need to manually notify the user."
+ )
@app.route('/request//fulfill', methods=['POST'])
@requires_admin()
@@ -184,7 +191,7 @@ def request_fulfill(id):
quantity = request_item.quantity
# get items of proper type
- for _ in xrange(quantity):
+ for _ in range(quantity):
item = Item.query.filter_by(entry_id = entry.id, user = None).first()
if item == None:
return jsonify(
@@ -213,51 +220,51 @@ def request_deny(id):
success=True,
)
-@socketio.on('connect', namespace='/admin')
-def authenticate_admin_conection():
- """Callback when client connects to /admin namespace, returns True
- if admin and False otherwise
- """
- if 'jwt' in request.cookies:
- quill_id = verify_token(request.cookies['jwt'])
- if not quill_id:
- return False
- user = User.query.filter_by(quill_id=quill_id).first()
-
- if user == None or not user.is_admin:
- return False
-
- return True
- else:
- return False
-
-@socketio.on('connect', namespace='/user')
-def authenticate_user_conection():
- """Callback when client connects to /user namespace, returns True
- if logged in and False otherwise
- """
- if 'jwt' in request.cookies:
- quill_id = verify_token(request.cookies['jwt'])
- if not quill_id:
- return False
- user = User.query.filter_by(quill_id=quill_id).first()
-
- if user == None:
- return False
-
- socket = Socket(request.sid, user)
- db.session.add(socket)
- db.session.commit()
- return True
- else:
- return False
-
-@socketio.on('disconnect', namespace='/user')
-def user_disconnect():
- """Delete user's socket when they disconnect"""
- socket = Socket.query.get(request.sid)
- db.session.delete(socket)
- db.session.commit()
+# @socketio.on('connect', namespace='/admin')
+# def authenticate_admin_conection():
+# """Callback when client connects to /admin namespace, returns True
+# if admin and False otherwise
+# """
+# if 'jwt' in request.cookies:
+# hackerapi_id = verify_token(request.cookies['jwt'])
+# if not hackerapi_id:
+# return False
+# user = User.query.filter_by(hackerapi_id=hackerapi_id).first()
+
+# if user == None or not user.is_admin:
+# return False
+
+# return True
+# else:
+# return False
+
+# @socketio.on('connect', namespace='/user')
+# def authenticate_user_conection():
+# """Callback when client connects to /user namespace, returns True
+# if logged in and False otherwise
+# """
+# if 'jwt' in request.cookies:
+# hackerapi_id = verify_token(request.cookies['jwt'])
+# if not hackerapi_id:
+# return False
+# user = User.query.filter_by(hackerapi_id=hackerapi_id).first()
+
+# if user == None:
+# return False
+
+# socket = Socket(request.sid, user)
+# db.session.add(socket)
+# db.session.commit()
+# return True
+# else:
+# return False
+
+# @socketio.on('disconnect', namespace='/user')
+# def user_disconnect():
+# """Delete user's socket when they disconnect"""
+# socket = Socket.query.get(request.sid)
+# db.session.delete(socket)
+# db.session.commit()
def on_request_insert(mapper, connection, target):
"""Callback for when new request is inserted into DB"""
@@ -270,7 +277,7 @@ def on_request_update(mapper, connection, target):
def request_change_handler(target):
"""Handler that sends updated HTML for rendering requests"""
user = target.user
- sockets = Socket.query.filter_by(user=user).all()
+ # sockets = Socket.query.filter_by(user=user).all()
requests = Request.query.filter(Request.user == user, Request.status.in_(
[RequestStatus.APPROVED, RequestStatus.SUBMITTED, RequestStatus.DENIED])).all()
@@ -281,10 +288,10 @@ def request_change_handler(target):
admin = False,
time = False)
- for socket in sockets:
- socketio.emit('update', {
- 'requests': requests_html,
- }, namespace='/user', room=socket.sid)
+ # for socket in sockets:
+ # socketio.emit('update', {
+ # 'requests': requests_html,
+ # }, namespace='/user', room=socket.sid)
# TODO: add check if at least one admin is connected
approved_requests = render_template('includes/macros/display_requests.html',
@@ -313,11 +320,11 @@ def request_change_handler(target):
}
)
- socketio.emit('update', {
- 'approved_requests': approved_requests,
- 'submitted_requests': submitted_requests,
- 'lottery_quantities': lottery_quantities
- }, namespace='/admin')
+ # socketio.emit('update', {
+ # 'approved_requests': approved_requests,
+ # 'submitted_requests': submitted_requests,
+ # 'lottery_quantities': lottery_quantities
+ # }, namespace='/admin')
# listeners for change to Request database
event.listen(RequestItem, 'after_insert', on_request_insert)
diff --git a/cog/controllers/user.py b/cog/controllers/user.py
new file mode 100644
index 0000000..e01ee09
--- /dev/null
+++ b/cog/controllers/user.py
@@ -0,0 +1,89 @@
+from cog import app
+
+import os
+
+from cog.utils import requires_auth, requires_admin
+from cog.models.user import User
+from cog.models.request import Request, RequestStatus
+from cog.models import db
+
+from cog.forms.user_update_form import UserUpdateForm
+
+from flask import (
+ jsonify,
+ send_from_directory,
+ request,
+ redirect,
+ render_template
+)
+
+@app.route('/user')
+@requires_auth()
+def get_user():
+ # render user page, with options to change settings etc
+ return render_template('pages/user.html',
+ requests = Request.query.filter_by(user_id = user.id).order_by(Request.timestamp.desc()).all(),
+ user = user,
+ isme = True,
+ target = user,
+ RequestStatus = RequestStatus)
+
+@app.route('/user/')
+@requires_auth()
+def user_items(id):
+ # display items signed out by user
+ # only works for user if they match id, works for admins
+ is_me = (user.id == id)
+ target = User.query.get(id)
+ return render_template('pages/user.html',
+ requests = Request.query.filter_by(user_id = target.id).order_by(Request.timestamp.desc()).all(),
+ user = user,
+ target = target,
+ RequestStatus = RequestStatus,
+ isme = is_me,
+ items = target.items)
+
+# @app.route('/user//update', methods=['POST'])
+# @requires_auth()
+# def user_update(id):
+# # update user settings
+# if user.is_admin or user.id == id:
+# user_to_change = User.query.get(id)
+# form = UserUpdateForm(request.form)
+# if form.validate():
+# if form.location.data:
+# user_to_change.location = form.location.data
+# if form.phone.data:
+# user_to_change.phone = form.phone.data.national_number
+# if form.name.data:
+# user_to_change.name = form.name.data
+# db.session.commit()
+# return jsonify(
+# success=True
+# )
+
+# error_msg = '\n'.join([key.title() + ': ' + ', '.join(value) for key, value in form.errors.items()])
+
+# return jsonify(
+# success=False,
+# message=error_msg,
+# user={
+# 'phone': user_to_change.phone,
+# 'name': user_to_change.name,
+# 'location': user_to_change.location
+# }
+# )
+
+# else:
+# return jsonify(
+# success=False,
+# message='Forbidden'
+# ), 403
+
+@app.route('/users')
+@requires_admin()
+def get_users():
+ # render list of all users
+ return render_template('pages/users.html',
+ user = user,
+ users = User.query.all())
\ No newline at end of file
diff --git a/cog/filters.py b/cog/filters.py
new file mode 100644
index 0000000..54780fb
--- /dev/null
+++ b/cog/filters.py
@@ -0,0 +1,6 @@
+import os
+from cog import app
+
+@app.template_filter()
+def env_override(value, key):
+ return os.getenv(key, value)
\ No newline at end of file
diff --git a/cog/forms/__init__.py b/cog/forms/__init__.py
new file mode 100644
index 0000000..d642fae
--- /dev/null
+++ b/cog/forms/__init__.py
@@ -0,0 +1,4 @@
+import cog.forms.inventory_form
+import cog.forms.inventory_update_form
+import cog.forms.inventory_import_form
+import cog.forms.user_update_form
\ No newline at end of file
diff --git a/hardwarecheckout/forms/inventory_form.py b/cog/forms/inventory_form.py
similarity index 70%
rename from hardwarecheckout/forms/inventory_form.py
rename to cog/forms/inventory_form.py
index 6d07ef7..7abe1d2 100644
--- a/hardwarecheckout/forms/inventory_form.py
+++ b/cog/forms/inventory_form.py
@@ -1,9 +1,9 @@
from wtforms import Form, StringField, BooleanField, IntegerField, SelectField, validators
-from hardwarecheckout.forms.inventory_update_form import InventoryUpdateForm
-from hardwarecheckout.models.inventory_entry import ItemType
+from cog.forms.inventory_update_form import InventoryUpdateForm
+from cog.models.inventory_entry import ItemType
def validate_quantity(form, field):
- return field.data != None or form.item_type == ItemType.FREE
+ return field.data != None or form.item_type == ItemType.FREE or form.item_type == ItemType.MLH
class InventoryForm(InventoryUpdateForm):
quantity = IntegerField('quantity', [validators.Optional(), validators.NumberRange(min=0), validate_quantity])
diff --git a/hardwarecheckout/forms/inventory_import_form.py b/cog/forms/inventory_import_form.py
similarity index 100%
rename from hardwarecheckout/forms/inventory_import_form.py
rename to cog/forms/inventory_import_form.py
diff --git a/hardwarecheckout/forms/inventory_update_form.py b/cog/forms/inventory_update_form.py
similarity index 88%
rename from hardwarecheckout/forms/inventory_update_form.py
rename to cog/forms/inventory_update_form.py
index 330ec5c..0b1bb64 100644
--- a/hardwarecheckout/forms/inventory_update_form.py
+++ b/cog/forms/inventory_update_form.py
@@ -1,5 +1,5 @@
from wtforms import Form, StringField, BooleanField, IntegerField, SelectField, validators
-from hardwarecheckout.models.inventory_entry import ItemType
+from cog.models.inventory_entry import ItemType
def validate_image(form, field):
if field.data == field.default:
@@ -14,6 +14,6 @@ class InventoryUpdateForm(Form):
category = StringField('category', [validators.input_required()])
image = StringField('image', [validators.Optional(), validate_image], default='/static/images/default.png')
item_type = SelectField('item_type', [validators.input_required()],
- choices=[('free', 'Free to Take'), ('checkout', 'Requires Checkout'), ('lottery', 'Requires Lottery')])
+ choices=[('free', 'Free to Take'), ('checkout', 'Requires Checkout'), ('lottery', 'Requires Lottery'), ('mlh', 'MLH Item')])
visible = BooleanField('visible')
diff --git a/hardwarecheckout/forms/user_update_form.py b/cog/forms/user_update_form.py
similarity index 100%
rename from hardwarecheckout/forms/user_update_form.py
rename to cog/forms/user_update_form.py
diff --git a/hardwarecheckout/models/__init__.py b/cog/models/__init__.py
similarity index 70%
rename from hardwarecheckout/models/__init__.py
rename to cog/models/__init__.py
index 6a168ca..d5d5558 100644
--- a/hardwarecheckout/models/__init__.py
+++ b/cog/models/__init__.py
@@ -2,7 +2,7 @@
class SerializableAlchemy(SQLAlchemy):
def apply_driver_hacks(self, app, info, options):
- if not 'isolation_level' in options:
- options['isolation_level'] = 'SERIALIZABLE'
+ # if not 'isolation_level' in options:
+ # options['isolation_level'] = 'SERIALIZABLE'
return super(SerializableAlchemy, self).apply_driver_hacks(app, info, options)
db = SerializableAlchemy()
diff --git a/hardwarecheckout/models/inventory_entry.py b/cog/models/inventory_entry.py
similarity index 80%
rename from hardwarecheckout/models/inventory_entry.py
rename to cog/models/inventory_entry.py
index 2df623b..fa7685c 100644
--- a/hardwarecheckout/models/inventory_entry.py
+++ b/cog/models/inventory_entry.py
@@ -1,16 +1,17 @@
-from hardwarecheckout.models import db
+from cog.models import db
from sqlalchemy.dialects import postgresql
from sqlalchemy.sql import func
-from hardwarecheckout.models.item import Item
-from hardwarecheckout.models.request_item import RequestItem
-import hardwarecheckout.models.request
+from cog.models.item import Item
+from cog.models.request_item import RequestItem
+import cog.models.request
import enum
class ItemType(enum.Enum):
LOTTERY = 0
CHECKOUT = 1
FREE = 2
+ MLH = 3
class InventoryEntry(db.Model):
id = db.Column(db.Integer, primary_key=True)
@@ -42,7 +43,7 @@ def __init__(self, name, description, link, category, tags,
self.image_src = image
self.items = []
- for i in xrange(int(qty)):
+ for i in range(int(qty)):
self.items.append(
Item(self, self.name + " " + str(i+1)))
@@ -57,10 +58,11 @@ def quantity(self):
"""Returns quantity of items that have not been 'claimed' by a request"""
requests = RequestItem.query \
.filter_by(entry_id=self.id) \
- .join(hardwarecheckout.models.request.Request) \
- .filter_by(status=hardwarecheckout.models.request.RequestStatus.APPROVED) \
+ .join(cog.models.request.Request) \
+ .filter_by(status=cog.models.request.RequestStatus.APPROVED) \
.with_entities(func.sum(RequestItem.quantity)).scalar()
if not requests: requests = 0
+
return Item.query.filter_by(entry_id = self.id, user = None).count() - requests
@property
@@ -68,8 +70,8 @@ def submitted_request_quantity(self):
"""Returns number of submitted requests for this entry"""
requests = RequestItem.query \
.filter_by(entry_id=self.id) \
- .join(hardwarecheckout.models.request.Request) \
- .filter_by(status=hardwarecheckout.models.request.RequestStatus.SUBMITTED).count()
+ .join(cog.models.request.Request) \
+ .filter_by(status=cog.models.request.RequestStatus.SUBMITTED).count()
return requests
@property
@@ -80,4 +82,8 @@ def requires_checkout(self):
def requires_lottery(self):
return self.item_type == ItemType.LOTTERY
+ @property
+ def requires_mlh(self):
+ return self.item_type == ItemType.MLH
+
def __str__(self): return str(self.name) + " [" + str(self.id) + "]"
\ No newline at end of file
diff --git a/hardwarecheckout/models/item.py b/cog/models/item.py
similarity index 90%
rename from hardwarecheckout/models/item.py
rename to cog/models/item.py
index fb2bbf8..fce1388 100644
--- a/hardwarecheckout/models/item.py
+++ b/cog/models/item.py
@@ -1,4 +1,4 @@
-from hardwarecheckout.models import db
+from cog.models import db
class Item(db.Model):
id = db.Column(db.Integer, primary_key=True)
diff --git a/hardwarecheckout/models/request.py b/cog/models/request.py
similarity index 91%
rename from hardwarecheckout/models/request.py
rename to cog/models/request.py
index 74f34d4..bd735f8 100644
--- a/hardwarecheckout/models/request.py
+++ b/cog/models/request.py
@@ -1,6 +1,6 @@
-from hardwarecheckout.models import db
-from hardwarecheckout.models.user import User
-from hardwarecheckout.models.inventory_entry import ItemType
+from cog.models import db
+from cog.models.user import User
+from cog.models.inventory_entry import ItemType
from datetime import datetime
import enum
@@ -10,6 +10,7 @@ class RequestStatus(enum.Enum):
FULFILLED = 2
DENIED = 3
CANCELLED = 4
+ RETURNED = 5
def __str__(self):
return self.name
diff --git a/hardwarecheckout/models/request_item.py b/cog/models/request_item.py
similarity index 92%
rename from hardwarecheckout/models/request_item.py
rename to cog/models/request_item.py
index f16189d..9ce9477 100644
--- a/hardwarecheckout/models/request_item.py
+++ b/cog/models/request_item.py
@@ -1,4 +1,4 @@
-from hardwarecheckout.models import db
+from cog.models import db
class RequestItem(db.Model):
entry_id = db.Column(db.Integer, db.ForeignKey('inventory_entry.id'), primary_key=True)
diff --git a/hardwarecheckout/models/socket.py b/cog/models/socket.py
similarity index 87%
rename from hardwarecheckout/models/socket.py
rename to cog/models/socket.py
index 6f662f1..5ad5ef0 100644
--- a/hardwarecheckout/models/socket.py
+++ b/cog/models/socket.py
@@ -1,4 +1,4 @@
-from hardwarecheckout.models import db
+from cog.models import db
class Socket(db.Model):
sid = db.Column(db.String(), primary_key=True)
diff --git a/hardwarecheckout/models/user.py b/cog/models/user.py
similarity index 65%
rename from hardwarecheckout/models/user.py
rename to cog/models/user.py
index fb8bcb8..c3f930d 100644
--- a/hardwarecheckout/models/user.py
+++ b/cog/models/user.py
@@ -1,27 +1,29 @@
-from hardwarecheckout.models import db
+from cog.models import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
- quill_id = db.Column(db.String(), unique=True)
+ hackerapi_id = db.Column(db.String(), unique=True)
is_admin = db.Column(db.Boolean)
location = db.Column(db.String(120))
name = db.Column(db.String())
+ first_name = db.Column(db.String())
+ last_name = db.Column(db.String())
phone = db.Column(db.String(255))
email = db.Column(db.String())
notifications = db.Column(db.Boolean)
have_their_id = db.Column(db.Boolean)
requests = db.relationship('Request', back_populates='user')
- sockets = db.relationship('Socket', back_populates='user')
+ # sockets = db.relationship('Socket', back_populates='user')
items = db.relationship('Item', backref='user')
- def __init__(self, quill_id, email, is_admin):
- self.quill_id = quill_id
+ def __init__(self, hackerapi_id, email, name, phone, is_admin):
+ self.hackerapi_id = hackerapi_id
self.email = email
self.is_admin = is_admin
- self.name = ''
- self.location = ''
- self.phone = ''
+ self.name = name
+ self.phone = phone
+ self.location = ''
self.notifications = False
self.have_their_id = False
diff --git a/hardwarecheckout/sheets_csv.py b/cog/sheets_csv.py
similarity index 100%
rename from hardwarecheckout/sheets_csv.py
rename to cog/sheets_csv.py
diff --git a/cog/static/css.map b/cog/static/css.map
new file mode 100644
index 0000000..a29f3dc
--- /dev/null
+++ b/cog/static/css.map
@@ -0,0 +1 @@
+{"version":3,"sourceRoot":"","sources":["sass/nav.scss","sass/_vars.scss","sass/app.scss"],"names":[],"mappings":"AACA;EACE,YCFa;;;ACEf;EACI;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAKJ;AACA;EACE;AAAA;AAAA;AAAA;AAAA;AAAA;IAME","file":"css"}
\ No newline at end of file
diff --git a/cog/static/css/app.css b/cog/static/css/app.css
new file mode 100644
index 0000000..9ad4875
--- /dev/null
+++ b/cog/static/css/app.css
@@ -0,0 +1,243 @@
+#navbar {
+ background: #183249;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ color: white;
+ z-index: 20;
+}
+#navbar #primary-menu {
+ border-style: none;
+}
+@media (min-width: 768px) {
+ #navbar #primary-menu {
+ margin: 0 -1rem;
+ }
+}
+@media (max-width: 767px) {
+ #navbar #primary-menu {
+ justify-content: center;
+ background: #183249;
+ position: absolute;
+ top: 40px;
+ left: 0;
+ width: 100vw;
+ box-sizing: border-box;
+ overflow: auto;
+ height: calc(100vh - 40px);
+ display: none;
+ }
+ #navbar #primary-menu .item {
+ align-self: center;
+ display: block;
+ text-align: center;
+ }
+ #navbar #primary-menu .item .icon {
+ display: none;
+ }
+ #navbar #primary-menu .right.menu:before {
+ content: "";
+ display: block;
+ margin: 1rem auto;
+ width: 50px;
+ border-top: 1px solid rgba(255, 255, 255, 0.5);
+ }
+ #navbar #primary-menu.visible {
+ display: flex;
+ }
+}
+#navbar #primary-menu .item {
+ padding: 1rem;
+}
+#navbar #mobile-menu {
+ position: relative;
+ display: none;
+ margin: 0 -1em;
+ border: none;
+ justify-content: space-between;
+ flex-direction: row;
+}
+#navbar #mobile-menu:after {
+ content: none;
+}
+@media (max-width: 767px) {
+ #navbar #mobile-menu {
+ display: flex;
+ }
+}
+
+#body #checkout_accordion .title {
+ border-bottom: 1px solid #CCC;
+ position: sticky;
+ top: 40px;
+ z-index: 5;
+}
+#body #inventory-requests-prompt {
+ display: none;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ z-index: 31;
+ justify-content: space-between;
+ padding: 0.5em;
+ align-items: center;
+ background: #FFF;
+ border-top: 1px solid #CCC;
+}
+#body #inventory-requests-prompt > span {
+ margin-left: 1em;
+}
+#body #inventory-requests-prompt #inventory-requests-toggle .icon {
+ margin: 0;
+}
+#body #inventory-requests-prompt #inventory-requests-toggle .down {
+ display: none;
+}
+#body #inventory-requests-prompt #inventory-requests-toggle.visible .down {
+ display: block;
+}
+#body #inventory-requests-prompt #inventory-requests-toggle.visible .up {
+ display: none;
+}
+#body #inventory-requests {
+ margin-top: -7em;
+}
+#body .inventory-item.row {
+ border-bottom: 1px solid #EAEAEA;
+ margin-top: 1em;
+}
+@media (max-width: 767px) {
+ #body .inventory-item.row .inventory-item-content, #body .inventory-item.row .ui.items:not(.unstackable) > .item {
+ flex-direction: row !important;
+ margin: 0 !important;
+ }
+ #body .inventory-item.row .inventory-item-content .ui.image, #body .inventory-item.row .ui.items:not(.unstackable) > .item .ui.image {
+ max-width: none !important;
+ max-height: none !important;
+ min-width: none !important;
+ min-height: none !important;
+ height: 48px !important;
+ width: 48px !important;
+ overflow: hidden;
+ }
+ #body .inventory-item.row .inventory-item-content .ui.image img, #body .inventory-item.row .ui.items:not(.unstackable) > .item .ui.image img {
+ display: block;
+ max-height: 100% !important;
+ margin: auto;
+ max-width: 100% !important;
+ }
+ #body .inventory-item.row .inventory-item-content > .content, #body .inventory-item.row .ui.items:not(.unstackable) > .item > .content {
+ padding-left: 1em !important;
+ padding-top: 0 !important;
+ }
+ #body .inventory-item.row > .column.right {
+ display: flex !important;
+ align-items: center;
+ justify-content: space-between;
+ flex-direction: row !important;
+ padding-top: 0 !important;
+ }
+ #body .inventory-item.row > .column.right br {
+ display: none;
+ }
+ #body #checkout_accordion .title {
+ margin: 0 -1em;
+ padding: 1em;
+ }
+ #body #inventory {
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ }
+ #body #inventory-requests-prompt {
+ display: flex;
+ }
+ #body #inventory-requests {
+ position: fixed;
+ margin-top: 0;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh !important;
+ overflow: auto;
+ background: rgba(0, 0, 0, 0.9);
+ backdrop-filter: blur(3px);
+ color: #FFF;
+ z-index: 30;
+ display: none;
+ }
+ #body #inventory-requests.visible {
+ display: block;
+ }
+ #body #inventory-requests .table {
+ display: table !important;
+ }
+ #body #inventory-requests .table tbody {
+ display: table-row-group !important;
+ }
+ #body #inventory-requests .table tbody tr {
+ display: table-row !important;
+ }
+ #body #inventory-requests .table tbody tr > td {
+ padding: 0.75em !important;
+ display: table-cell !important;
+ }
+}
+
+@media (max-width: 767px) {
+ body .modals .ui.modal, body .modals .ui.scrolling.modal, .modals.dimmer .ui.modal, .modals.dimmer .ui.scrolling.modal {
+ width: 100%;
+ margin: 0 !important;
+ margin-bottom: 0 !important;
+ margin-top: 0 !important;
+ bottom: 0;
+ max-height: 100%;
+ overflow: auto;
+ top: auto;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ left: 0;
+ }
+}
+
+body {
+ padding-top: 60px;
+}
+
+#login-wrapper {
+ max-width: 450px;
+ margin-top: 35px;
+}
+
+#login-form {
+ text-align: left;
+ vertical-align: middle;
+}
+
+.ui.select {
+ border: 1px solid #CCC;
+ border-right-style: none;
+ display: block;
+ background: transparent;
+ border-radius: 3px 0 0 3px;
+}
+
+.footer {
+ color: #777;
+ padding: 3em 0;
+}
+
+/* Mobile */
+@media only screen and (max-width: 767px) {
+ [class*="mobile hidden"],
+[class*="tablet only"]:not(.mobile),
+[class*="computer only"]:not(.mobile),
+[class*="large screen only"]:not(.mobile),
+[class*="widescreen only"]:not(.mobile),
+[class*="or lower hidden"] {
+ display: none !important;
+ }
+}
+
+/*# sourceMappingURL=app.css.map */
diff --git a/cog/static/css/app.css.map b/cog/static/css/app.css.map
new file mode 100644
index 0000000..e2e9281
--- /dev/null
+++ b/cog/static/css/app.css.map
@@ -0,0 +1 @@
+{"version":3,"sourceRoot":"","sources":["../sass/nav.scss","../sass/_vars.scss","../sass/inventory.scss","../sass/modals.scss","../sass/app.scss"],"names":[],"mappings":"AACA;EACE,YCFa;EDGb;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;;AACA;EAFF;IAGI;;;AAEF;EALF;IAqBI;IACA,YC/BS;IDgCT;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAxBA;IACE;IACA;IACA;;EACA;IACE;;EAGJ;IACE;IACA;IACA;IACA;IACA;;EAYF;IACE;;;AAGJ;EACE;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;;AAEF;EAVF;IAWI;;;;AE1DF;EACI;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;AAGA;EACI;;AAEJ;EACI;;AAGA;EACI;;AAEJ;EACI;;AAKhB;EACI;;AAEJ;EACI;EACA;;AAEJ;EAEQ;IACI;IACA;;EACA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;;EACA;IACI;IACA;IACA;IACA;;EAGR;IACI;IACA;;EAGR;IACI;IACA;IACA;IACA;IACA;;EACA;IACI;;EAIZ;IACI;IACA;;EAEJ;IACI;IACA;;EAEJ;IACI;;EAEJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EACA;IACI;;EAEJ;IACI;;EACA;IACI;;EACA;IACI;;EACA;IACI;IACA;;;;ACnHxB;EADF;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;;ACTN;EACE;;;AAGF;EACI;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACI;EACA;;;AAKJ;AACA;EACE;AAAA;AAAA;AAAA;AAAA;AAAA;IAME","file":"app.css"}
\ No newline at end of file
diff --git a/cog/static/favicon.ico b/cog/static/favicon.ico
new file mode 100644
index 0000000..a63d21c
Binary files /dev/null and b/cog/static/favicon.ico differ
diff --git a/cog/static/images/default.png b/cog/static/images/default.png
new file mode 100644
index 0000000..d597571
Binary files /dev/null and b/cog/static/images/default.png differ
diff --git a/cog/static/sass/_vars.scss b/cog/static/sass/_vars.scss
new file mode 100644
index 0000000..466ecd8
--- /dev/null
+++ b/cog/static/sass/_vars.scss
@@ -0,0 +1 @@
+$darkest_blue: #183249;
\ No newline at end of file
diff --git a/hardwarecheckout/static/sass/app.scss b/cog/static/sass/app.scss
similarity index 67%
rename from hardwarecheckout/static/sass/app.scss
rename to cog/static/sass/app.scss
index c63a45d..9e67443 100644
--- a/hardwarecheckout/static/sass/app.scss
+++ b/cog/static/sass/app.scss
@@ -1,3 +1,11 @@
+@import './nav.scss';
+@import './inventory.scss';
+@import './modals.scss';
+
+body {
+ padding-top: 60px;
+}
+
#login-wrapper {
max-width: 450px;
margin-top: 35px;
@@ -8,11 +16,17 @@
vertical-align: middle;
}
+.ui.select {
+ border: 1px solid #CCC;
+ border-right-style: none;
+ display: block;
+ background: transparent;
+ border-radius: 3px 0 0 3px;
+}
+
.footer {
color: #777;
- padding-bottom: 1em;
- padding-top: 3em;
- text-align: center;
+ padding: 3em 0;
}
diff --git a/cog/static/sass/inventory.scss b/cog/static/sass/inventory.scss
new file mode 100644
index 0000000..5d8b8f5
--- /dev/null
+++ b/cog/static/sass/inventory.scss
@@ -0,0 +1,123 @@
+#body {
+ #checkout_accordion .title {
+ top: 40px;
+ z-index: 5;
+ }
+ #inventory-requests-prompt {
+ display: none;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ z-index: 31;
+ justify-content: space-between;
+ padding: 0.5em;
+ align-items: center;
+ background: #FFF;
+ border-top: 1px solid #CCC;
+ > span {
+ margin-left: 1em;
+ }
+ #inventory-requests-toggle {
+ .icon {
+ margin: 0;
+ }
+ .down {
+ display: none;
+ }
+ &.visible {
+ .down {
+ display: block;
+ }
+ .up {
+ display: none;
+ }
+ }
+ }
+ }
+ #inventory-requests {
+ margin-top: -7em;
+ }
+ .inventory-item.row {
+ border-bottom: 1px solid #EAEAEA;
+ margin-top: 1em;
+ }
+ @media(max-width: 767px) {
+ .inventory-item.row {
+ .inventory-item-content, .ui.items:not(.unstackable)>.item {
+ flex-direction: row !important;
+ margin: 0 !important;
+ .ui.image {
+ max-width: none !important;
+ max-height: none !important;
+ min-width: none !important;
+ min-height: none !important;
+ height: 48px !important;
+ width: 48px !important;
+ overflow: hidden;
+ img {
+ display: block;
+ max-height: 100% !important;
+ margin: auto;
+ max-width: 100% !important;
+ }
+ }
+ >.content {
+ padding-left: 1em !important;
+ padding-top: 0 !important;
+ }
+ }
+ >.column.right {
+ display: flex !important;
+ align-items: center;
+ justify-content: space-between;
+ flex-direction: row !important;
+ padding-top: 0 !important;
+ br {
+ display: none;
+ }
+ }
+ }
+ #checkout_accordion .title {
+ margin: 0 -1em;
+ padding: 1em;
+ }
+ #inventory {
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ }
+ #inventory-requests-prompt {
+ display: flex;
+ }
+ #inventory-requests {
+ position: fixed;
+ margin-top: 0;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh !important;
+ overflow: auto;
+ background: rgba(0, 0, 0, 0.9);
+ backdrop-filter: blur(3px);
+ color: #FFF;
+ z-index: 30;
+ display: none;
+ &.visible {
+ display: block;
+ }
+ .table {
+ display: table !important;
+ tbody {
+ display: table-row-group !important;
+ tr {
+ display: table-row !important;
+ > td {
+ padding: 0.75em !important;
+ display: table-cell !important;
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/cog/static/sass/modals.scss b/cog/static/sass/modals.scss
new file mode 100644
index 0000000..84ca64b
--- /dev/null
+++ b/cog/static/sass/modals.scss
@@ -0,0 +1,17 @@
+body .modals, .modals.dimmer {
+ .ui.modal, .ui.scrolling.modal {
+ @media(max-width: 767px) {
+ width: 100%;
+ margin: 0 !important;
+ margin-bottom: 0 !important;
+ margin-top: 0 !important;
+ bottom: 0;
+ max-height: 100%;
+ overflow: auto;
+ top: auto;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ left: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/cog/static/sass/nav.scss b/cog/static/sass/nav.scss
new file mode 100644
index 0000000..7ff12f9
--- /dev/null
+++ b/cog/static/sass/nav.scss
@@ -0,0 +1,63 @@
+@import './_vars';
+#navbar {
+ background: $darkest_blue;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ color: white;
+ z-index: 20;
+ #primary-menu {
+ border-style: none;
+ @media(min-width: 768px) {
+ margin: 0 -1rem;
+ }
+ @media(max-width: 767px) {
+ .item {
+ align-self: center;
+ display: block;
+ text-align: center;
+ .icon {
+ display: none;
+ }
+ }
+ .right.menu:before {
+ content: '';
+ display: block;
+ margin: 1rem auto;
+ width: 50px;
+ border-top: 1px solid rgba(white, 0.5);
+ }
+ justify-content: center;
+ background: $darkest_blue;
+ position: absolute;
+ top: 40px;
+ left: 0;
+ width: 100vw;
+ box-sizing: border-box;
+ overflow: auto;
+ height: calc(100vh - 40px);
+ display: none;
+ &.visible {
+ display: flex;
+ }
+ }
+ .item {
+ padding: 1rem;
+ }
+ }
+ #mobile-menu {
+ position: relative;
+ display: none;
+ margin: 0 -1em;
+ border: none;
+ justify-content: space-between;
+ flex-direction: row;
+ &:after {
+ content: none;
+ }
+ @media(max-width: 767px) {
+ display: flex;
+ }
+ }
+}
\ No newline at end of file
diff --git a/cog/static/scripts/admin.js b/cog/static/scripts/admin.js
new file mode 100644
index 0000000..53f6e92
--- /dev/null
+++ b/cog/static/scripts/admin.js
@@ -0,0 +1,82 @@
+// var socket = io.connect(location.protocol + '//' + document.domain + ':'
+// + location.port + '/admin');
+// socket.on('connect', function() {
+// console.log('Socket connected!')
+// socket.emit('', {data: 'I\'m connected!'});
+// });
+
+// socket.on('update', function(data) {
+// if (data.approved_requests) {
+// $('#approved_requests').fadeOut(250, function() {
+// $(this).html(data.approved_requests)
+// .fadeIn(250, init_request_actions);
+// });
+// }
+// if (data.submitted_requests) {
+// $('#submitted_requests').fadeOut(250, function() {
+// $(this).html(data.submitted_requests)
+// .fadeIn(250, init_request_actions);
+// });
+// }
+// if (data.lottery_quantities) {
+// q = data.lottery_quantities;
+// for (var i = 0; i < data.lottery_quantities.length; i++) {
+// q = data.lottery_quantities[i];
+// var div = $('div[data-item-id='+q['id']+']');
+// div.find('.item-quantity').html(q['available']);
+// div.find('.submitted-quantity').html(q['submitted']);
+// }
+// }
+// });
+
+function init_request_actions() {
+ $('.request-action').api({
+ method: 'POST',
+ onSuccess: function(response) {
+ if (response.message) {
+ alert(message);
+ }
+ window.location.reload();
+ },
+ onFailure: function(err) {
+ console.log(err);
+ alert(err.message);
+ window.location.reload();
+ }
+ });
+ $('.id-fulfill').api({
+ action: 'fulfill request',
+ method: 'POST',
+ serializeForm: true,
+ beforeSend: function(settings) {
+ settings.data.collected_id = $(this).data('collected-id');
+ return settings;
+ },
+ onSuccess: function(response) {
+ }
+ });
+}
+
+$(document).ready(function() {
+ init_request_actions();
+ $('.run-lottery.button').api({
+ method: 'POST',
+ onSuccess: function(response) {
+ // window.location.reload();
+ },
+ onFailure: function(err) {
+ console.log("ERROR!");
+ console.log(err);
+ },
+ onError: function(err) {
+ console.log("ERROR!");
+ console.log(err);
+ }
+ });
+
+ $('.run-all-lottery.button').api({
+ method: 'POST',
+ onSuccess: function(response) {
+ },
+ });
+});
diff --git a/hardwarecheckout/static/scripts/api.js b/cog/static/scripts/api.js
similarity index 100%
rename from hardwarecheckout/static/scripts/api.js
rename to cog/static/scripts/api.js
diff --git a/hardwarecheckout/static/scripts/inventory.js b/cog/static/scripts/inventory.js
similarity index 78%
rename from hardwarecheckout/static/scripts/inventory.js
rename to cog/static/scripts/inventory.js
index cb0f14b..8dc1083 100644
--- a/hardwarecheckout/static/scripts/inventory.js
+++ b/cog/static/scripts/inventory.js
@@ -2,12 +2,18 @@ function init_request_actions() {
$('.request-action').api({
method: 'POST',
onSuccess: function(response) {
+ if (response.message) {
+ alert(message);
+ }
+ window.location.reload();
},
onFailure: function(err) {
console.log(err);
- alert(err.message)
+ alert(err.message);
+ window.location.reload();
}
});
+ $('#inventory-requests-count').text($('#my_requests tbody>tr').length);
}
$(document).ready(function() {
@@ -106,6 +112,9 @@ $(document).ready(function() {
settings.data.item_id = $(this).data('item-id');
return settings;
},
+ onSuccess: function(response) {
+ window.location.reload();
+ },
onError: function(error, element, xhr) {
// handle redirects
var json = xhr.responseJSON;
@@ -136,19 +145,24 @@ $(document).ready(function() {
$('select.dropdown').dropdown();
- var socket = io.connect(location.protocol + '//' + document.domain + ':'
- + location.port + '/user');
- socket.on('connect', function() {
- console.log('Socket connected!')
- socket.emit('', {data: 'I\'m connected!'});
+ $('#inventory-requests-toggle').click(() => {
+ $('#inventory-requests-toggle, #inventory-requests').toggleClass('visible');
});
+
+ // var socket = io.connect(location.protocol + '//' + document.domain + ':'
+ // + location.port + '/user');
+ // socket.on('connect', function() {
+ // console.log('Socket connected!')
+ // socket.emit('', {data: 'I\'m connected!'});
+ // });
- socket.on('update', function(data) {
- if (data.requests) {
- $('#my_requests').fadeOut(100, function() {
- $(this).html(data.requests)
- .fadeIn(100, init_request_actions);
- });
- }
- });
+ // socket.on('update', function(data) {
+ // if (data.requests) {
+ // $('#my_requests').fadeOut(100, function() {
+ // $(this).html(data.requests)
+ // .fadeIn(100, init_request_actions);
+ // $('#inventory-requests-count').text($('#my_requests tbody>tr').length);
+ // });
+ // }
+ // });
});
\ No newline at end of file
diff --git a/hardwarecheckout/static/scripts/item.js b/cog/static/scripts/item.js
similarity index 95%
rename from hardwarecheckout/static/scripts/item.js
rename to cog/static/scripts/item.js
index b0f2731..18def0d 100644
--- a/hardwarecheckout/static/scripts/item.js
+++ b/cog/static/scripts/item.js
@@ -73,11 +73,15 @@ $(document).ready(function() {
$('.request-action').api({
method: 'POST',
onSuccess: function(response) {
+ if (response.message) {
+ alert(message);
+ }
window.location.reload();
},
onFailure: function(err) {
console.log(err);
- alert(err.message)
+ alert(err.message);
+ window.location.reload();
}
});
diff --git a/hardwarecheckout/static/scripts/main.js b/cog/static/scripts/main.js
similarity index 58%
rename from hardwarecheckout/static/scripts/main.js
rename to cog/static/scripts/main.js
index b7fb393..7e64405 100644
--- a/hardwarecheckout/static/scripts/main.js
+++ b/cog/static/scripts/main.js
@@ -3,4 +3,10 @@ $.fn.filterByData = function(prop, val) {
return this.filter(
function() { return $(this).data(prop)==val; }
);
-}
\ No newline at end of file
+}
+
+$(function() {
+ $('#toggle-mobile').click(() => {
+ $('#primary-menu').toggleClass('visible');
+ });
+});
\ No newline at end of file
diff --git a/hardwarecheckout/static/scripts/user.js b/cog/static/scripts/user.js
similarity index 93%
rename from hardwarecheckout/static/scripts/user.js
rename to cog/static/scripts/user.js
index 242f392..24802dc 100644
--- a/hardwarecheckout/static/scripts/user.js
+++ b/cog/static/scripts/user.js
@@ -52,11 +52,15 @@ $(document).ready(function() {
$('.request-action').api({
method: 'POST',
onSuccess: function(response) {
+ if (response.message) {
+ alert(message);
+ }
window.location.reload();
},
onFailure: function(err) {
console.log(err);
- alert(err.message)
+ alert(err.message);
+ window.location.reload();
}
});
diff --git a/hardwarecheckout/static/scripts/users.js b/cog/static/scripts/users.js
similarity index 100%
rename from hardwarecheckout/static/scripts/users.js
rename to cog/static/scripts/users.js
diff --git a/cog/static/vendor/jquery/jquery-3.1.1.min.js b/cog/static/vendor/jquery/jquery-3.1.1.min.js
new file mode 100644
index 0000000..4c5be4c
--- /dev/null
+++ b/cog/static/vendor/jquery/jquery-3.1.1.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v3.1.1 | (c) jQuery Foundation | 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.1.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,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(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}):C.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(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.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)),B.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)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={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 J(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 J(a,"nextSibling")},prev:function(a){return J(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 a.contentDocument||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&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(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 M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(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!==N&&(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:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).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&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),
+a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=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)S(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(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.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 V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.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,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.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 qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(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)xa(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=va;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=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,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(K)||[""],j=b.length;while(j--)h=ta.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=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.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)&&V.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=(V.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,za=/
+
@@ -41,6 +41,12 @@
{% block end %}
{% endblock %}
+