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. -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](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. ![Cog](/media/cog.png?raw=true) @@ -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: -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](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=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + @@ -41,6 +41,12 @@ {% block end %} {% endblock %} + diff --git a/cog/templates/messages/email_message.html b/cog/templates/messages/email_message.html new file mode 100644 index 0000000..8d7f3dd --- /dev/null +++ b/cog/templates/messages/email_message.html @@ -0,0 +1,13 @@ +{% extends "includes/email_base.html" %} +{% block title %}Your hardware checkout is ready{% endblock %} +{% block big_header %}Yay!{% endblock %} +{% block header %}Your hardware checkout is ready!{% endblock %} +{% block content %} +Hi {{user.first_name}},
+Your requested items are now available! Please pick them up as soon as possible. +
    + {% for request_item in request_items %} +
  • {{request_item.entry.name}}
  • + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/cog/templates/messages/slack_message.html b/cog/templates/messages/slack_message.html new file mode 100644 index 0000000..17ceb9e --- /dev/null +++ b/cog/templates/messages/slack_message.html @@ -0,0 +1,7 @@ +Hi {{user.first_name}}, your requested items are ready! Please pick them up as soon as possible. + +{% for request_item in request_items %} +- {{request_item.entry.name}} +{% endfor %} + +This slack bot cannot receive messages, so please do not respond to this message; if you need help, please contact an organizer or email hello@treehacks.com. \ No newline at end of file diff --git a/hardwarecheckout/templates/pages/admin.html b/cog/templates/pages/admin.html similarity index 94% rename from hardwarecheckout/templates/pages/admin.html rename to cog/templates/pages/admin.html index 7457371..a1e6a41 100644 --- a/hardwarecheckout/templates/pages/admin.html +++ b/cog/templates/pages/admin.html @@ -55,6 +55,6 @@

{{ item.name }}

{% endblock %} {% block script %} - + {% endblock %} \ No newline at end of file diff --git a/hardwarecheckout/templates/pages/inventory.html b/cog/templates/pages/inventory.html similarity index 64% rename from hardwarecheckout/templates/pages/inventory.html rename to cog/templates/pages/inventory.html index a5d58a3..e2b6884 100644 --- a/hardwarecheckout/templates/pages/inventory.html +++ b/cog/templates/pages/inventory.html @@ -54,6 +54,18 @@
+
+
+ Instructions +
+

+

    +
  1. Request the checkout required hardware items you want to borrow. We will send you a Slack message when your hardware is ready for pickup, so you can wait at your hacking spot.
  2. +
  3. Apply for any lottery items you would like to hack on. We will start running the lotteries on Friday at 10:30 PM, and will text you if you get the chance to hack on these items.
  4. +
  5. Pick up no checkout required items at any time from the hardware lab.
  6. +
+

+
{% if lottery_items %}

Lottery Required

{{ config['LOTTERY_TEXT'] }} @@ -74,20 +86,34 @@

No Checkout Required

{{ accordion_list(free_items, "free_accordion") }} {% endif %} + + {% if mlh_items %} +

MLH Items

+ {{ config['MLH_TEXT'] }} + + {{ accordion_list(mlh_items, "mlh_accordion") }} + {% endif %}
-
-
-

My Requests

-
- {{ display_requests(requests, RequestStatus, admin=False, time=False) }} -
+
+
+

My Requests

+
+ {{ display_requests(requests, RequestStatus, admin=False, time=False) }} +
+
+ requested items + +
{% endblock %} {% block script %} - + {% endblock %} diff --git a/hardwarecheckout/templates/pages/item.html b/cog/templates/pages/item.html similarity index 90% rename from hardwarecheckout/templates/pages/item.html rename to cog/templates/pages/item.html index aa9c21d..53e4459 100644 --- a/hardwarecheckout/templates/pages/item.html +++ b/cog/templates/pages/item.html @@ -18,8 +18,7 @@
-{{ confirmation("delete-conf", "Are you sure you want to delete this item?", "This cannot be undone!", - None, item.id, "delete-item") }} +{{ confirmation("delete-conf", "Are you sure you want to delete this item?", "This cannot be undone!", None, item.id, "delete-item") }} {{ item_form("Update Item", url_for('inventory_update', id=item.id), item=item) }} @@ -49,8 +48,7 @@
- {{ confirmation("return-notice", "Be sure to return ID to user!", "They've returned their last item", - None, None, "return-notice-btn") }} +{{ confirmation("return-notice", "Be sure to return ID to user!", "They've returned their last item", True, None, "return-notice-btn") }}

Requests

{{ display_requests(requests, RequestStatus, user.is_admin, show_item_name=False, show_proposal=is_lottery) }} diff --git a/cog/templates/pages/login.html b/cog/templates/pages/login.html new file mode 100644 index 0000000..a17fdba --- /dev/null +++ b/cog/templates/pages/login.html @@ -0,0 +1,58 @@ +{% extends "includes/wrapper.html" %} +{% block title %} Login {% endblock %} + +{% block content %} +
+
+

{{ config["HACKATHON_NAME"] }} Hardware

+ Please wait... +
+
+{% endblock %} + +{% block end %} + + + +{% endblock %} \ No newline at end of file diff --git a/hardwarecheckout/templates/pages/user.html b/cog/templates/pages/user.html similarity index 68% rename from hardwarecheckout/templates/pages/user.html rename to cog/templates/pages/user.html index aed7850..4876614 100644 --- a/hardwarecheckout/templates/pages/user.html +++ b/cog/templates/pages/user.html @@ -23,18 +23,7 @@

Info

- -
-
- -
-
- -
-
- +
diff --git a/hardwarecheckout/templates/pages/users.html b/cog/templates/pages/users.html similarity index 66% rename from hardwarecheckout/templates/pages/users.html rename to cog/templates/pages/users.html index 000c82e..53c3792 100644 --- a/hardwarecheckout/templates/pages/users.html +++ b/cog/templates/pages/users.html @@ -5,11 +5,11 @@ {% block title %} Users {% endblock %} {% block panel %} - {{ display_users(users) }} + {{ display_users(users, admin = user.is_admin) }} {% endblock %} {% block script %} - + {% endblock %} diff --git a/cog/tools/email.py b/cog/tools/email.py new file mode 100644 index 0000000..14b2238 --- /dev/null +++ b/cog/tools/email.py @@ -0,0 +1,52 @@ +import smtplib +import email.utils +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import html2text +from cog.config import EMAIL_SENDER, EMAIL_SENDER_NAME, SMTP_USERNAME, SMTP_PASSWORD, SMTP_HOST, SMTP_PORT + +def send_email(recipient, subject, body): + """ + Send email. Returns true if success and false on error. + recipient - recipient email address. + subject - subject of email. + body - HTML body of email. + """ + # The HTML body of the email. + BODY_HTML = body + + # The email body for recipients with non-HTML email clients. + BODY_TEXT = html2text.html2text(body) + + # Create message container - the correct MIME type is multipart/alternative. + msg = MIMEMultipart('alternative') + msg['Subject'] = subject + msg['From'] = email.utils.formataddr((EMAIL_SENDER_NAME, EMAIL_SENDER)) + msg['To'] = recipient + + # Record the MIME types of both parts - text/plain and text/html. + part1 = MIMEText(BODY_TEXT, 'plain') + part2 = MIMEText(BODY_HTML, 'html') + + # Attach parts into message container. + # According to RFC 2046, the last part of a multipart message, in this case + # the HTML message, is best and preferred. + msg.attach(part1) + msg.attach(part2) + + # Try to send the message. + try: + server = smtplib.SMTP(SMTP_HOST, SMTP_PORT) + server.ehlo() + server.starttls() + #stmplib docs recommend calling ehlo() before & after starttls() + server.ehlo() + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.sendmail(EMAIL_SENDER, recipient, msg.as_string()) + server.close() + # Display an error message if something goes wrong. + except Exception as e: + print ("Email error: ", e) + return False + else: + return True \ No newline at end of file diff --git a/cog/tools/slack.py b/cog/tools/slack.py new file mode 100644 index 0000000..78f5918 --- /dev/null +++ b/cog/tools/slack.py @@ -0,0 +1,35 @@ +import slack + +from cog.config import SLACK_OAUTH_ACCESS_TOKEN + +client = slack.WebClient(token=SLACK_OAUTH_ACCESS_TOKEN) + +def send_slack(email, message): + """Send a slack message *message* to *email*. + Returns true if successful, false if not.""" + try: + response = client.users_lookupByEmail( + email=email + ) + if not response["ok"]: + return False + user_id = response["user"]["id"] + + response = client.conversations_open( + return_im=False, + users=[user_id] + ) + if not response["ok"]: + return False + dm_channel_id = response["channel"]["id"] + + response = client.chat_postMessage( + channel=dm_channel_id, + text=message, + mrkdwn=True) + if not response["ok"]: + return False + except slack.errors.SlackApiError as e: + print("Slack error: ", e) + return False + return True \ No newline at end of file diff --git a/hardwarecheckout/utils.py b/cog/utils.py similarity index 73% rename from hardwarecheckout/utils.py rename to cog/utils.py index 38f13e9..7df6a37 100644 --- a/hardwarecheckout/utils.py +++ b/cog/utils.py @@ -2,9 +2,10 @@ from jose import jws from jose.exceptions import JWSError import os -from hardwarecheckout.config import * -from hardwarecheckout.constants import * -from hardwarecheckout.models.user import * +import requests +from cog.config import * +from cog.constants import * +from cog.models.user import * from flask import (redirect, request, jsonify, url_for) from functools import wraps from datetime import datetime @@ -15,7 +16,7 @@ def gen_uuid(): def verify_token(token): try: - return jws.verify(token, SECRET, algorithms=['HS256']) + return jws.verify(token, SECRET, algorithms=['HS256']).decode('utf-8') except JWSError: return None @@ -28,15 +29,24 @@ def safe_redirect(endpoint, request): ), 401 return redirect(url_for(endpoint)) +def get_profile_from_jwt(jwt): + try: + r = requests.get(os.getenv("ENDPOINT_URL") + "/user_profile", headers={"Authorization": jwt}) + profile = r.json() + return profile, profile["id"] + except Exception as e: + print(e) + return None, None + def requires_auth(): def decorator(f): @wraps(f) def decorated(*args, **kwargs): if 'jwt' in request.cookies: - quill_id = verify_token(request.cookies['jwt']) - if not quill_id: + profile, hackerapi_id = get_profile_from_jwt(request.cookies['jwt']) + if not hackerapi_id: return safe_redirect('login_page', request) - user = User.query.filter_by(quill_id=quill_id).first() + user = User.query.filter_by(hackerapi_id=hackerapi_id).first() # if no user found for auth token, log them out (clears token) if user == None: @@ -55,12 +65,12 @@ def decorator(f): @wraps(f) def decorated(*args, **kwargs): if 'jwt' in request.cookies: - quill_id = verify_token(request.cookies['jwt']) - if not quill_id: + profile, hackerapi_id = get_profile_from_jwt(request.cookies['jwt']) + if not hackerapi_id: f.__globals__['user'] = None return f(*args, **kwargs) - user = User.query.filter_by(quill_id=quill_id).first() + user = User.query.filter_by(hackerapi_id=hackerapi_id).first() # if no user found for auth token, log them out (clears token) if user == None: @@ -80,10 +90,10 @@ def decorator(f): @wraps(f) def decorated(*args, **kwargs): if 'jwt' in request.cookies: - quill_id = verify_token(request.cookies['jwt']) - if not quill_id: + profile, hackerapi_id = get_profile_from_jwt(request.cookies['jwt']) + if not hackerapi_id: return safe_redirect('login_page', request) - user = User.query.filter_by(quill_id=quill_id).first() + user = User.query.filter_by(hackerapi_id=hackerapi_id).first() # if no user found for auth token, log them out (clears token) if user == None: @@ -116,4 +126,4 @@ def read_csv(csv_text): ret = [parsed[0]] for p in parsed: ret.append(p) - return {"header": ret[0], "data": ret[2:]} + return {"header": ret[0], "data": ret[2:]} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..14254a0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3" +services: + dev_db: + image: postgres + ports: + - 5432 + environment: + - PG_PASSWORD=password + cog: + container_name: cog + image: cog:latest + build: + context: . + environment: + - LOGIN_URL=${LOGIN_URL} + - ENDPOINT_URL=${ENDPOINT_URL} + - DATABASE_URL=${DATABASE_URL} + - SECRET=${SECRET} + - SLACK_OAUTH_ACCESS_TOKEN=${SLACK_OAUTH_ACCESS_TOKEN} + - SMTP_USERNAME=${SMTP_USERNAME} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - FLASK_DEBUG=1 + - PORT=8000 + ports: + - 127.0.0.1:8000:8000 + depends_on: + - dev_db + volumes: + - .:/cog \ No newline at end of file diff --git a/hardwarecheckout/controllers/__init__.py b/hardwarecheckout/controllers/__init__.py deleted file mode 100644 index 91b892e..0000000 --- a/hardwarecheckout/controllers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -import hardwarecheckout.controllers.default -import hardwarecheckout.controllers.inventory -import hardwarecheckout.controllers.request -import hardwarecheckout.controllers.user -import hardwarecheckout.controllers.login diff --git a/hardwarecheckout/controllers/login.py b/hardwarecheckout/controllers/login.py deleted file mode 100644 index 51e22a9..0000000 --- a/hardwarecheckout/controllers/login.py +++ /dev/null @@ -1,68 +0,0 @@ -from hardwarecheckout import app -from hardwarecheckout import config -from hardwarecheckout.models.user import * -from hardwarecheckout.utils import verify_token -import requests -import datetime -import json -from urlparse import urljoin -from hardwarecheckout.forms.login_form import LoginForm -from flask import ( - redirect, - render_template, - request, - url_for -) - -@app.route('/login') -def login_page(): - """If not logged in render login page, otherwise redirect to inventory""" - if 'jwt' in request.cookies: - try: - decode_token(request.cookies['jwt']) - return redirect('/inventory') - except Exception as e: - pass - - return render_template('pages/login.html') - -@app.route('/login', methods=['POST']) -def login_handler(): - """Log user in""" - form = LoginForm(request.form) - if form.validate(): - url = urljoin(config.QUILL_URL, '/auth/login') - r = requests.post(url, data={'email':request.form['email'], 'password':request.form['password']}) - try: - r = json.loads(r.text) - except ValueError as e: - return render_template('pages/login.html', error=[str(e)]) - - if 'message' in r: - return render_template('pages/login.html', error=[r['message']]) - - quill_id = verify_token(r['token']) - if not quill_id: - return render_template('pages/login.html', error=['Invalid token returned by registration']) - - if User.query.filter_by(quill_id=quill_id).count() == 0: - user = User(quill_id, request.form['email'], r['user']['admin']) - db.session.add(user) - db.session.commit() - - response = app.make_response(redirect('/inventory')) - response.set_cookie('jwt', r['token']) - return response - - errors = [] - for field, error in form.errors.items(): - errors.append(field + ": " + "\n".join(error) + "\n") - - return render_template('pages/login.html', error=errors) - -@app.route('/logout') -def logout(): - """Log user out""" - response = app.make_response(redirect('/')) - response.set_cookie('jwt', '') - return response \ No newline at end of file diff --git a/hardwarecheckout/controllers/user.py b/hardwarecheckout/controllers/user.py deleted file mode 100644 index 4bb774a..0000000 --- a/hardwarecheckout/controllers/user.py +++ /dev/null @@ -1,89 +0,0 @@ -from hardwarecheckout import app - -import os - -from hardwarecheckout.utils import requires_auth, requires_admin -from hardwarecheckout.models.user import User -from hardwarecheckout.models.request import Request, RequestStatus -from hardwarecheckout.models import db - -from hardwarecheckout.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/hardwarecheckout/forms/__init__.py b/hardwarecheckout/forms/__init__.py deleted file mode 100644 index b35fa6f..0000000 --- a/hardwarecheckout/forms/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -import hardwarecheckout.forms.inventory_form -import hardwarecheckout.forms.inventory_update_form -import hardwarecheckout.forms.inventory_import_form -import hardwarecheckout.forms.login_form -import hardwarecheckout.forms.user_update_form \ No newline at end of file diff --git a/hardwarecheckout/forms/login_form.py b/hardwarecheckout/forms/login_form.py deleted file mode 100644 index 4a63b72..0000000 --- a/hardwarecheckout/forms/login_form.py +++ /dev/null @@ -1,5 +0,0 @@ -from wtforms import Form, StringField, PasswordField, FileField, validators - -class LoginForm(Form): - email = StringField('email_address', [validators.input_required(), validators.Email()]) - password = PasswordField('password', [validators.input_required()]) \ No newline at end of file diff --git a/hardwarecheckout/static/css/app.css b/hardwarecheckout/static/css/app.css deleted file mode 100644 index 98e1bbf..0000000 --- a/hardwarecheckout/static/css/app.css +++ /dev/null @@ -1,29 +0,0 @@ -#login-wrapper { - max-width: 450px; - margin-top: 35px; } - -#login-form { - text-align: left; - vertical-align: middle; } - -.footer { - color: #777; - padding-bottom: 1em; - padding-top: 3em; - text-align: center; } - -/* 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 */ - -.add-subitems-container .field, .list-subitems-container .field { - display: inline-block; -} \ No newline at end of file diff --git a/hardwarecheckout/static/css/app.css.map b/hardwarecheckout/static/css/app.css.map deleted file mode 100644 index 0e44d45..0000000 --- a/hardwarecheckout/static/css/app.css.map +++ /dev/null @@ -1,7 +0,0 @@ -{ -"version": 3, -"mappings": "AAAA,cAAe;EACX,SAAS,EAAE,KAAK;EAChB,UAAU,EAAE,IAAI;;AAGpB,WAAY;EACR,UAAU,EAAE,IAAI;EAChB,cAAc,EAAE,MAAM;;AAG1B,OAAQ;EACJ,KAAK,EAAE,IAAI;EACX,cAAc,EAAE,GAAG;EACnB,WAAW,EAAE,GAAG;EAChB,UAAU,EAAE,MAAM;;AAKtB,YAAY;AACZ,yCAA0C;EACxC;;;;;4BAK2B;IACzB,OAAO,EAAE,eAAe", -"sources": ["../sass/app.scss"], -"names": [], -"file": "app.css" -} \ No newline at end of file diff --git a/hardwarecheckout/static/favicon.png b/hardwarecheckout/static/favicon.png deleted file mode 100644 index b30f186..0000000 Binary files a/hardwarecheckout/static/favicon.png and /dev/null differ diff --git a/hardwarecheckout/static/images/default.png b/hardwarecheckout/static/images/default.png deleted file mode 100644 index 252c707..0000000 Binary files a/hardwarecheckout/static/images/default.png and /dev/null differ diff --git a/hardwarecheckout/static/scripts/admin.js b/hardwarecheckout/static/scripts/admin.js deleted file mode 100644 index abeaced..0000000 --- a/hardwarecheckout/static/scripts/admin.js +++ /dev/null @@ -1,77 +0,0 @@ -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) { - }, - onFailure: function(err) { - console.log(err); - alert(err.message) - } - }); - $('.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/templates/includes/nav.html b/hardwarecheckout/templates/includes/nav.html deleted file mode 100644 index 8abb484..0000000 --- a/hardwarecheckout/templates/includes/nav.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "includes/wrapper.html" %} -{% block head %} -{% endblock %} -{% block content %} - - -
-

{% block title %}{% endblock %}

- {% block panel %} - {% endblock %} -
- -{% endblock %} - -{% block end %} -{% block script %} -{% endblock %} -{% endblock %} - diff --git a/hardwarecheckout/templates/pages/login.html b/hardwarecheckout/templates/pages/login.html deleted file mode 100644 index f3bc072..0000000 --- a/hardwarecheckout/templates/pages/login.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "includes/wrapper.html" %} -{% block title %} Login {% endblock %} - -{% block content %} -
-
-

{{ config["HACKATHON_NAME"] }} Hardware

- Please login using your {{ config["QUILL_URL_READABLE"] }} credentials - -
- - -
-
- - -
- - - {% if error %} -
Login Errors: - {% for item in error %} -
{{item}}
- {% endfor %} -
- {% endif %} -
-
-{% endblock %} - diff --git a/initialize.py b/initialize.py index 745cf3f..5d9dc6e 100644 --- a/initialize.py +++ b/initialize.py @@ -1,5 +1,10 @@ -if __name__ == '__main__': - from hardwarecheckout.models import db + +def rebuild(): + from cog.models import db db.reflect() db.drop_all() db.create_all() + + +if __name__ == '__main__': + rebuild() \ No newline at end of file diff --git a/kubernetes/deployment.template.yaml b/kubernetes/deployment.template.yaml new file mode 100644 index 0000000..02c61c3 --- /dev/null +++ b/kubernetes/deployment.template.yaml @@ -0,0 +1,77 @@ +apiVersion: v1 +kind: Service +metadata: + name: cog-hackthenorth-com + labels: + app: cog-hackthenorth-com +spec: + type: NodePort + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: cog-hackthenorth-com + selector: + app: cog-hackthenorth-com +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cog-hackthenorth-com + labels: + app: cog-hackthenorth-com +spec: + replicas: 1 + selector: + matchLabels: + app: cog-hackthenorth-com + template: + metadata: + labels: + app: cog-hackthenorth-com + spec: + volumes: + - name: "$CREDENTIALS_SECRET" + secret: + secretName: "$CREDENTIALS_SECRET" + containers: + # [START proxy_container] + - name: cloudsql-proxy + image: gcr.io/cloudsql-docker/gce-proxy:1.11 + command: ["/cloud_sql_proxy", + "-instances=htn-techyon:northamerica-northeast1:main=tcp:5432", + "-credential_file=/secrets/cloudsql/credentials.json"] + # [START cloudsql_security_context] + securityContext: + runAsUser: 2 # non-root user + allowPrivilegeEscalation: false + # [END cloudsql_security_context] + volumeMounts: + - name: "$CREDENTIALS_SECRET" + mountPath: /secrets/cloudsql + readOnly: true + resources: + limits: + cpu: 25m + memory: "0.05G" + # [END proxy_container] + - name: cog-hackthenorth-com + image: gcr.io/htn-techyon/cog-hackthenorth-com + ports: + - containerPort: 80 + env: + - name: ENV + value: production + - name: DB_PASS + valueFrom: + secretKeyRef: + name: "$DB_SECRET" + key: password + - name: DATABASE_URL + value: postgres://postgres:$(DB_PASS)@localhost/cog + - name: SECRET + value: "$ENV_SECRET" + resources: + requests: + cpu: 30m + memory: "0.15G" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 16fb224..3266ab3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,25 @@ +aiohttp==3.6.2 astroid==1.5.3 +async-timeout==3.0.1 +attrs==19.3.0 Babel==2.4.0 backports.functools-lru-cache==1.4 beautifulsoup4==4.6.0 blinker==1.4 +boto3==1.11.12 +botocore==1.14.12 bs4==0.0.1 certifi==2017.4.17 cffi==1.10.0 chardet==3.0.4 click==6.7 configparser==3.5.0 -cryptography==1.7.2 decorator==4.1.2 +dnspython==1.16.0 +docutils==0.15.2 ecdsa==0.13 enum34==1.1.6 -eventlet +eventlet==0.25.1 Flask==0.12.2 Flask-Admin==1.5.0 Flask-Markdown==0.3 @@ -21,39 +27,48 @@ Flask-SocketIO==2.9.1 Flask-SQLAlchemy==2.2 Flask-SSLify==0.1.5 future==0.16.0 +html2text==2020.1.16 gevent==1.2.2 gevent-websocket==0.10.1 greenlet==0.4.12 gunicorn==19.7.1 idna==2.5 +idna-ssl==1.1.0 infinity==1.4 intervals==0.8.0 ipaddress==1.0.18 isort==4.2.15 itsdangerous==0.24 Jinja2==2.9.6 +jmespath==0.9.4 lazy-object-proxy==1.3.1 Markdown==2.6.9 MarkupSafe==1.0 mccabe==0.6.1 +monotonic==1.5 +multidict==4.7.4 phonenumbers==8.8.1 -psycopg2==2.7.1 +psycopg2==2.7.3.2 py==1.4.34 pyasn1==0.2.3 pycodestyle==2.3.1 pycparser==2.18 pycrypto==2.6.1 pytest==3.2.3 +python-dateutil==2.8.1 python-engineio==1.7.0 python-jose==1.3.2 python-socketio==1.8.0 pytz==2017.2 requests==2.18.1 +s3transfer==0.3.3 singledispatch==3.4.0.3 six==1.10.0 +slackclient==2.5.0 SQLAlchemy==1.1.11 SQLAlchemy-Utils==0.32.16 typing==3.5.3.0 +typing-extensions==3.7.4.1 urllib3==1.21.1 validators==0.12.0 Werkzeug==0.12.2 @@ -61,3 +76,4 @@ wrapt==1.10.11 WTForms==2.0 WTForms-Alchemy==0.15.0 WTForms-Components==0.10.3 +yarl==1.4.2 diff --git a/runserver.py b/runserver.py index 7440841..62dfb7a 100644 --- a/runserver.py +++ b/runserver.py @@ -1,9 +1,11 @@ -from hardwarecheckout import app, socketio +from cog import app# , socketio +from initialize import rebuild import sys +import os if __name__ == '__main__': - try: - port = int(sys.argv[1]) - except (IndexError, ValueError): - port = 5000 - socketio.run(app, host='0.0.0.0', port=port) + if os.getenv('ENV', '') == 'test': + rebuild() + port = int(os.getenv("PORT", "80")) + debug = os.getenv("FLASK_DEBUG") == "1" + app.run(host='0.0.0.0', port=port, debug=debug) diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index 5385682..0000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-2.7.14 diff --git a/sample.env b/sample.env new file mode 100644 index 0000000..36e8c5b --- /dev/null +++ b/sample.env @@ -0,0 +1,9 @@ +LOGIN_URL=https://login.dev.treehacks.com +ENDPOINT_URL=https://root.dev.treehacks.com/api +DATABASE_URL=postgres://postgres:password@dev_db/postgres +SECRET=randomAF)(DHASI) +SLACK_OAUTH_ACCESS_TOKEN=none +SMTP_USERNAME=abc +SMTP_PASSWORD=defg +DEBUG=True +FORCE_SSL=False \ No newline at end of file diff --git a/semantic.json b/semantic.json index 7f010b7..a951659 100644 --- a/semantic.json +++ b/semantic.json @@ -1,5 +1,5 @@ { - "base": "hardwarecheckout/static/vendor/semantic/", + "base": "cog/static/vendor/semantic/", "paths": { "source": { "config": "src/theme.config", diff --git a/skaffold.yaml b/skaffold.yaml new file mode 100644 index 0000000..59546c0 --- /dev/null +++ b/skaffold.yaml @@ -0,0 +1,10 @@ +apiVersion: skaffold/v1alpha5 +kind: Config +build: + artifacts: + - image: gcr.io/htn-techyon/cog-hackthenorth-com + context: ./ +deploy: + kubectl: + manifests: + - ./kubernetes/deployment.yaml \ No newline at end of file diff --git a/tests/test_all.py b/tests/test_all.py index 96d7d99..9db071c 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -3,8 +3,8 @@ TEST_DB="sqlite:///test_db" os.environ['DATABASE_URL'] = TEST_DB # sets DB URL for this process -import hardwarecheckout -from hardwarecheckout.config import SECRET +import cog +from cog.config import SECRET import unittest from jose import jws import tempfile @@ -12,27 +12,27 @@ import string from utils import * -from hardwarecheckout.models import db -from hardwarecheckout.models.user import User -from hardwarecheckout.models.inventory_entry import InventoryEntry -from hardwarecheckout.models.inventory_entry import ItemType -from hardwarecheckout.models.request import Request -from hardwarecheckout.models.request import RequestStatus -from hardwarecheckout.models.request_item import RequestItem +from cog.models import db +from cog.models.user import User +from cog.models.inventory_entry import InventoryEntry +from cog.models.inventory_entry import ItemType +from cog.models.request import Request +from cog.models.request import RequestStatus +from cog.models.request_item import RequestItem from flask import url_for, json import pytest @pytest.fixture def app(): - hardwarecheckout.app.config['TESTING'] = True - hardwarecheckout.app.config['DEBUG'] = False - app = hardwarecheckout.app.test_client() - with hardwarecheckout.app.app_context(): + cog.app.config['TESTING'] = True + cog.app.config['DEBUG'] = False + app = cog.app.test_client() + with cog.app.app_context(): db.drop_all() db.create_all() - db.app = hardwarecheckout.app - db.init_app(hardwarecheckout.app) - ctx = hardwarecheckout.app.test_request_context() + db.app = cog.app + db.init_app(cog.app) + ctx = cog.app.test_request_context() ctx.push() yield app ctx.pop() @@ -41,12 +41,12 @@ def app(): @pytest.fixture def user(app): - quill_id = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10)) - token = jws.sign(quill_id, SECRET, algorithm='HS256') - user = User(quill_id, 'alyssap@hacker.org', False) + hackerapi_id = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10)) + token = jws.sign(hackerapi_id, SECRET, algorithm='HS256') + user = User(hackerapi_id, 'alyssap@hacker.org', False) db.session.add(user) db.session.commit() - app.set_cookie('localhost:8000', 'jwt', token) + app.set_cookie('localhost:80', 'jwt', token) return user @@ -65,7 +65,7 @@ def item(app): return item def test_home(app): - with captured_templates(hardwarecheckout.app) as templates: + with captured_templates(cog.app) as templates: rv = app.get('/', follow_redirects = True) assert rv.status_code == 200 assert len(templates) == 1 @@ -75,24 +75,24 @@ def test_home(app): assert len(context['checkout_items']) == 0 assert len(context['free_items']) == 0 -def test_quill_login(app): - with captured_templates(hardwarecheckout.app) as templates: - rv = quill_login(app, 'admin@example.com', 'party') +def test_hackerapi_login(app): + with captured_templates(cog.app) as templates: + rv = hackerapi_login(app, 'admin@example.com', 'party') assert rv.status_code == 200 assert len(templates) == 1 template, context = templates[0] assert template.name == 'pages/inventory.html' - with captured_templates(hardwarecheckout.app) as templates: - rv = quill_login(app, 'admin@example.com', 'prty') + with captured_templates(cog.app) as templates: + rv = hackerapi_login(app, 'admin@example.com', 'prty') assert rv.status_code == 200 assert len(templates) == 1 template, context = templates[0] assert template.name == 'pages/login.html' assert "That's not the right password." in context['error'] - with captured_templates(hardwarecheckout.app) as templates: - rv = quill_login(app, 'admin@example.co', 'party') + with captured_templates(cog.app) as templates: + rv = hackerapi_login(app, 'admin@example.co', 'party') assert rv.status_code == 200 assert len(templates) == 1 template, context = templates[0] @@ -122,7 +122,7 @@ def test_add_delete_item(app, admin): # TODO: add back check def test_view_request(app, admin): - with captured_templates(hardwarecheckout.app) as templates: + with captured_templates(cog.app) as templates: rv = app.get('/request', follow_redirects=True) template, context = templates[0] assert rv.status_code == 200 @@ -145,7 +145,7 @@ def test_run_lottery(app, admin, item): .join(Request).filter_by(status=RequestStatus.APPROVED).count() == 3 def test_run_all_lotteries(app, admin): - for _ in xrange(3): + for _ in range(3): item = InventoryEntry('Item' + str(_), 'Wow lick my socks', 'http://test.co', 'Item', [], '', 3, item_type=ItemType.LOTTERY) db.session.add(item) @@ -170,7 +170,7 @@ def quantities_correct(expected_counts): expected_counts - a list of tuples with form (inventory_entry_name, count) """ - with captured_templates(hardwarecheckout.app) as templates: + with captured_templates(cog.app) as templates: rv = app.get('/', follow_redirects=True) counts = templates[0][1]["counts"] for (name, num) in expected_counts: diff --git a/tests/utils.py b/tests/utils.py index 576bf05..38614ab 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,7 @@ from contextlib import contextmanager from flask import template_rendered -def quill_login(app, username, password): +def hackerapi_login(app, username, password): return app.post('/login', data=dict( email=username, password=password @@ -27,12 +27,12 @@ def add_item(app, quantity=5, lottery=True, checkout=True): return app.post('/inventory/add', data=data, follow_redirects=True) -def update_user(app): - return app.post('/user/1/update', data=dict( - location='A5', - phone='617-555-0123', - name='Alyssa' - ), follow_redirects=True) +# def update_user(app): +# return app.post('/user/1/update', data=dict( +# location='A5', +# phone='617-555-0123', +# name='Alyssa' +# ), follow_redirects=True) def request_item(app, id): return app.post('/request/submit', data=dict(