diff options
author | 2018-05-20 23:29:17 +0100 | |
---|---|---|
committer | 2018-05-20 23:29:17 +0100 | |
commit | 449d52caf4010ed112f1928bf6b5234bcfb9a339 (patch) | |
tree | 3ce59258a68fcb4174610b157f3a3ae9c50be02a | |
parent | Tests directory (#73) (diff) |
Privacy/Usability updates (#75)
* Use less intrusive oauth scopes, add login redirect method
* Remove debugging prints, add missing __init__
* Work towards new privacy policy
* Fix judging state icons on code jam management page
* Jammer profile retraction and punishments based on jam status
* Linting
* [Jams] Deny profile saving for users < 13 years, and finish removal page
* Fix tests
* Clean up and address Volcyy's review
* Add proper login redirection to require_roles decorator
* Fix template is_staff() and add staff link to navigation
* Address lemon's review
* Linting
* Privacy page formatting
* Privacy page formatting
35 files changed, 621 insertions, 148 deletions
diff --git a/app_test.py b/app_test.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/app_test.py diff --git a/pysite/base_route.py b/pysite/base_route.py index e6bd00ad..bb50afd9 100644 --- a/pysite/base_route.py +++ b/pysite/base_route.py @@ -2,15 +2,15 @@ from collections import Iterable from datetime import datetime from typing import Any -from flask import Blueprint, Response, jsonify, redirect, render_template, url_for +from flask import Blueprint, Response, jsonify, redirect, render_template, session, url_for from flask.views import MethodView from werkzeug.exceptions import default_exceptions -from pysite.constants import DEBUG_MODE, ErrorCodes -from pysite.mixins import OauthMixin +from pysite.constants import ALL_STAFF_ROLES, DEBUG_MODE, ErrorCodes +from pysite.mixins import OAuthMixin -class BaseView(MethodView, OauthMixin): +class BaseView(MethodView, OAuthMixin): """ Base view class with functions and attributes that should be common to all view classes. @@ -52,10 +52,26 @@ class BaseView(MethodView, OauthMixin): context["current_page"] = self.name context["view"] = self context["logged_in"] = self.logged_in + context["user"] = self.user_data context["static_file"] = self._static_file context["debug"] = DEBUG_MODE context["format_datetime"] = lambda dt: dt.strftime("%b %d %Y, %H:%M") if isinstance(dt, datetime) else dt + def is_staff(): + if DEBUG_MODE: + return True + + if not self.logged_in: + return False + + for role in ALL_STAFF_ROLES: + if role in self.user_data.get("roles", []): + return True + + return False + + context["is_staff"] = is_staff + return render_template(template_names, **context) def _static_file(self, filename): @@ -103,6 +119,14 @@ class RouteView(BaseView): cls.name = f"{blueprint.name}.{cls.name}" # Add blueprint to page name + def redirect_login(self, **kwargs): + session["redirect_target"] = { + "url": self.name, + "kwargs": kwargs + } + + return redirect(url_for("discord.login")) + class APIView(RouteView): """ diff --git a/pysite/constants.py b/pysite/constants.py index be1bd9f8..f95e076f 100644 --- a/pysite/constants.py +++ b/pysite/constants.py @@ -39,7 +39,7 @@ DISCORD_OAUTH_REDIRECT = "/auth/discord" DISCORD_OAUTH_AUTHORIZED = "/auth/discord/authorized" DISCORD_OAUTH_ID = environ.get('DISCORD_OAUTH_ID', '') DISCORD_OAUTH_SECRET = environ.get('DISCORD_OAUTH_SECRET', '') -DISCORD_OAUTH_SCOPE = 'identify email guilds.join' +DISCORD_OAUTH_SCOPE = 'identify' OAUTH_DATABASE = "oauth_data" PREFERRED_URL_SCHEME = environ.get("PREFERRED_URL_SCHEME", "http") diff --git a/pysite/decorators.py b/pysite/decorators.py index 16d555f0..705c519e 100644 --- a/pysite/decorators.py +++ b/pysite/decorators.py @@ -1,11 +1,11 @@ from functools import wraps from json import JSONDecodeError -from flask import redirect, request, url_for +from flask import request from schema import Schema, SchemaError from werkzeug.exceptions import Forbidden -from pysite.base_route import APIView, BaseView +from pysite.base_route import APIView, RouteView from pysite.constants import BOT_API_KEY, CSRF, DEBUG_MODE, ErrorCodes, ValidationTypes @@ -27,21 +27,22 @@ def require_roles(*roles: int): def inner_decorator(f): @wraps(f) - def inner(self: BaseView, *args, **kwargs): + def inner(self: RouteView, *args, **kwargs): data = self.user_data + print(kwargs) if DEBUG_MODE: return f(self, *args, **kwargs) elif data: for role in roles: - if DEBUG_MODE or role in data.get("roles", []): + if role in data.get("roles", []): return f(self, *args, **kwargs) if isinstance(self, APIView): return self.error(ErrorCodes.unauthorized) raise Forbidden() - return redirect(url_for("discord.login")) + return self.redirect_login(**kwargs) return inner @@ -78,7 +79,7 @@ def api_params(schema: Schema, validation_type: ValidationTypes = ValidationType def inner_decorator(f): @wraps(f) - def inner(self: BaseView, *args, **kwargs): + def inner(self: APIView, *args, **kwargs): if validation_type == ValidationTypes.json: try: if not request.is_json: diff --git a/pysite/migrations/tables/oauth_data/v1.py b/pysite/migrations/tables/oauth_data/v1.py index dc7417bb..9ace6bf9 100644 --- a/pysite/migrations/tables/oauth_data/v1.py +++ b/pysite/migrations/tables/oauth_data/v1.py @@ -2,6 +2,10 @@ from rethinkdb import ReqlOpFailedError def run(db, table, table_obj): + """ + Create a secondary index on the "snowflake" key, so we can easily get documents by matching that key + """ + try: db.run(db.query(table).index_create("snowflake")) db.run(db.query(table).index_wait("snowflake")) diff --git a/pysite/migrations/tables/users/__init__.py b/pysite/migrations/tables/users/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pysite/migrations/tables/users/__init__.py diff --git a/pysite/migrations/tables/users/v1.py b/pysite/migrations/tables/users/v1.py new file mode 100644 index 00000000..9ba70142 --- /dev/null +++ b/pysite/migrations/tables/users/v1.py @@ -0,0 +1,11 @@ +def run(db, table, table_obj): + """ + Remove stored email addresses from every user document + """ + + for document in db.get_all(table): + if "email" in document: + del document["email"] + + db.insert(table, document, conflict="update", durability="soft") + db.sync(table) diff --git a/pysite/migrations/tables/wiki/v1.py b/pysite/migrations/tables/wiki/v1.py index a5282f28..22670342 100644 --- a/pysite/migrations/tables/wiki/v1.py +++ b/pysite/migrations/tables/wiki/v1.py @@ -1,4 +1,8 @@ def run(db, table, table_obj): + """ + Ensure that there are no wiki articles that don't have titles + """ + for document in db.pluck(table, table_obj.primary_key, "title"): if not document.get("title"): document["title"] = "No Title" diff --git a/pysite/mixins.py b/pysite/mixins.py index 6e5032ab..d0e822bf 100644 --- a/pysite/mixins.py +++ b/pysite/mixins.py @@ -4,7 +4,7 @@ from flask import Blueprint from rethinkdb.ast import Table from pysite.database import RethinkDB -from pysite.oauth import OauthBackend +from pysite.oauth import OAuthBackend class DBMixin: @@ -58,7 +58,7 @@ class DBMixin: return self._db() -class OauthMixin: +class OAuthMixin: """ Mixin for the classes that need access to a logged in user's information. This class should be used to grant route's access to user information, such as name, email, id, ect. @@ -80,11 +80,11 @@ class OauthMixin: user_data returns None, if the user isn't logged in. - * oauth (OauthBackend): The instance of pysite.oauth.OauthBackend, connected to the RouteManager. + * oauth (OAuthBackend): The instance of pysite.oauth.OAuthBackend, connected to the RouteManager. """ @classmethod - def setup(cls: "OauthMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): + def setup(cls: "OAuthMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): if hasattr(super(), "setup"): super().setup(manager, blueprint) # pragma: no cover @@ -99,5 +99,5 @@ class OauthMixin: return self.oauth.user_data() @property - def oauth(self) -> OauthBackend: + def oauth(self) -> OAuthBackend: return self._oauth() diff --git a/pysite/oauth.py b/pysite/oauth.py index d025ea37..86e7cdde 100644 --- a/pysite/oauth.py +++ b/pysite/oauth.py @@ -8,7 +8,7 @@ from flask_dance.contrib.discord import discord from pysite.constants import DISCORD_API_ENDPOINT, OAUTH_DATABASE -class OauthBackend(BaseBackend): +class OAuthBackend(BaseBackend): """ This is the backend for the oauth @@ -34,7 +34,6 @@ class OauthBackend(BaseBackend): pass def set(self, blueprint, token): - user = self.get_user() sess_id = str(uuid5(uuid4(), self.key)) self.add_user(token, user, sess_id) @@ -62,8 +61,7 @@ class OauthBackend(BaseBackend): { "user_id": user_data["id"], "username": user_data["username"], - "discriminator": user_data["discriminator"], - "email": user_data["email"] + "discriminator": user_data["discriminator"] }, conflict="update" ) @@ -85,3 +83,4 @@ class OauthBackend(BaseBackend): sess_id = session.get("session_id") if sess_id and self.db.get(OAUTH_DATABASE, sess_id): # If user exists in db, self.db.delete(OAUTH_DATABASE, sess_id) # remove them (at least, their session) + session.clear() diff --git a/pysite/route_manager.py b/pysite/route_manager.py index eacd74b4..c899cf02 100644 --- a/pysite/route_manager.py +++ b/pysite/route_manager.py @@ -13,7 +13,7 @@ from pysite.constants import ( CSRF, DEBUG_MODE, DISCORD_OAUTH_AUTHORIZED, DISCORD_OAUTH_ID, DISCORD_OAUTH_REDIRECT, DISCORD_OAUTH_SCOPE, DISCORD_OAUTH_SECRET, PREFERRED_URL_SCHEME) from pysite.database import RethinkDB -from pysite.oauth import OauthBackend +from pysite.oauth import OAuthBackend from pysite.websockets import WS TEMPLATES_PATH = "../templates" @@ -51,14 +51,14 @@ class RouteManager: CSRF.init_app(self.app) # Set up CSRF protection # Load the oauth blueprint - self.oauth_backend = OauthBackend(self) + self.oauth_backend = OAuthBackend(self) self.oauth_blueprint = make_discord_blueprint( DISCORD_OAUTH_ID, DISCORD_OAUTH_SECRET, DISCORD_OAUTH_SCOPE, - '/', login_url=DISCORD_OAUTH_REDIRECT, authorized_url=DISCORD_OAUTH_AUTHORIZED, + redirect_to="main.auth.done", backend=self.oauth_backend ) self.log.debug(f"Loading Blueprint: {self.oauth_blueprint.name}") diff --git a/pysite/tables.py b/pysite/tables.py index a9a0dc88..be43c588 100644 --- a/pysite/tables.py +++ b/pysite/tables.py @@ -190,8 +190,7 @@ TABLES = { "user_id", "roles", "username", - "discriminator", - "email" + "discriminator" ]) ), diff --git a/pysite/views/api/bot/user.py b/pysite/views/api/bot/user.py index a353ccfe..8c5d8f77 100644 --- a/pysite/views/api/bot/user.py +++ b/pysite/views/api/bot/user.py @@ -26,6 +26,8 @@ DELETE_SCHEMA = Schema([ } ]) +BANNABLE_STATES = ("preparing", "running") + class UserView(APIView, DBMixin): path = "/bot/users" @@ -33,6 +35,9 @@ class UserView(APIView, DBMixin): table_name = "users" oauth_table_name = "oauth_data" participants_table = "code_jam_participants" + infractions_table = "code_jam_infractions" + jams_table = "code_jams" + responses_table = "code_jam_responses" @api_key @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) @@ -42,6 +47,8 @@ class UserView(APIView, DBMixin): deletions = 0 oauth_deletions = 0 profile_deletions = 0 + response_deletions = 0 + bans = 0 user_ids = [user["user_id"] for user in data] @@ -56,10 +63,42 @@ class UserView(APIView, DBMixin): for item in all_oauth_data: if item["snowflake"] not in user_ids: - self.db.delete(self.oauth_table_name, item["id"], durability="soft") - self.db.delete(self.participants_table, item["id"], durability="soft") - oauth_deletions += 1 - profile_deletions += 1 + user_id = item["snowflake"] + + oauth_deletions += self.db.delete( + self.oauth_table_name, item["id"], durability="soft", return_changes=True + ).get("deleted", 0) + profile_deletions += self.db.delete( + self.participants_table, user_id, durability="soft", return_changes=True + ).get("deleted", 0) + + banned = False + responses = self.db.run( + self.db.query(self.responses_table).filter({"snowflake": user_id}), + coerce=list + ) + + for response in responses: + jam = response["jam"] + jam_obj = self.db.get(self.jams_table, jam) + + if jam_obj: + if jam_obj["state"] in BANNABLE_STATES: + banned = True + + self.db.delete(self.responses_table, response["id"], durability="soft") + response_deletions += 1 + + if banned: + self.db.insert( + self.infractions_table, { + "participant": user_id, + "reason": "Automatic ban: Removed jammer profile in the middle of a code jam", + "number": -1, + "decremented_for": [] + }, durability="soft" + ) + bans += 1 del user_ids @@ -69,11 +108,17 @@ class UserView(APIView, DBMixin): durability="soft" ) + self.db.sync(self.infractions_table) + self.db.sync(self.oauth_table_name) + self.db.sync(self.participants_table) + self.db.sync(self.responses_table) self.db.sync(self.table_name) changes["deleted"] = deletions changes["deleted_oauth"] = oauth_deletions changes["deleted_jam_profiles"] = profile_deletions + changes["deleted_responses"] = response_deletions + changes["jam_bans"] = bans return jsonify(changes) # pragma: no cover @@ -108,9 +153,40 @@ class UserView(APIView, DBMixin): self.db.query(self.participants_table) .get_all(*user_ids) .delete() - ) + ).get("deleted", 0) + + bans = 0 + response_deletions = 0 + + for user_id in user_ids: + banned = False + responses = self.db.run(self.db.query(self.responses_table).filter({"snowflake": user_id}), coerce=list) + + for response in responses: + jam = response["jam"] + jam_obj = self.db.get(self.jams_table, jam) + + if jam_obj: + if jam_obj["state"] in BANNABLE_STATES: + banned = True + + self.db.delete(self.responses_table, response["id"]) + response_deletions += 1 + + if banned: + self.db.insert( + self.infractions_table, { + "participant": user_id, + "reason": "Automatic ban: Removed jammer profile in the middle of a code jam", + "number": -1, + "decremented_for": [] + } + ) + bans += 1 changes["deleted_oauth"] = oauth_deletions changes["deleted_jam_profiles"] = profile_deletions + changes["deleted_responses"] = response_deletions + changes["jam_bans"] = bans return jsonify(changes) # pragma: no cover diff --git a/pysite/views/main/auth/__init__.py b/pysite/views/main/auth/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pysite/views/main/auth/__init__.py diff --git a/pysite/views/main/auth/done.py b/pysite/views/main/auth/done.py new file mode 100644 index 00000000..6e892906 --- /dev/null +++ b/pysite/views/main/auth/done.py @@ -0,0 +1,18 @@ +from flask import redirect, session, url_for + +from pysite.base_route import RouteView + + +class AuthDoneView(RouteView): + path = "/auth/done" + name = "auth.done" + + def get(self): + if self.logged_in: + target = session.get("redirect_target") + + if target: + del session["redirect_target"] + return redirect(url_for(target["url"], **target.get("kwargs", {}))) + + return redirect(url_for("main.index")) diff --git a/pysite/views/main/jams/index.py b/pysite/views/main/jams/index.py index 6d066117..8d34fa50 100644 --- a/pysite/views/main/jams/index.py +++ b/pysite/views/main/jams/index.py @@ -16,6 +16,6 @@ class JamsIndexView(RouteView, DBMixin): .order_by(rethinkdb.desc("number")) .limit(5) ) + jams = self.db.run(query, coerce=list) - print(jams) return self.render("main/jams/index.html", jams=jams) diff --git a/pysite/views/main/jams/join.py b/pysite/views/main/jams/join.py index 24931f72..c4011170 100644 --- a/pysite/views/main/jams/join.py +++ b/pysite/views/main/jams/join.py @@ -5,10 +5,10 @@ from werkzeug.exceptions import BadRequest, NotFound from pysite.base_route import RouteView from pysite.decorators import csrf -from pysite.mixins import DBMixin, OauthMixin +from pysite.mixins import DBMixin, OAuthMixin -class JamsJoinView(RouteView, DBMixin, OauthMixin): +class JamsJoinView(RouteView, DBMixin, OAuthMixin): path = "/jams/join/<int:jam>" name = "jams.join" @@ -26,7 +26,7 @@ class JamsJoinView(RouteView, DBMixin, OauthMixin): return NotFound() if not self.user_data: - return redirect(url_for("discord.login")) + return self.redirect_login(jam=jam) infractions = self.get_infractions(self.user_data["user_id"]) @@ -76,7 +76,7 @@ class JamsJoinView(RouteView, DBMixin, OauthMixin): return NotFound() if not self.user_data: - return redirect(url_for("discord.login")) + return self.redirect_login(jam=jam) infractions = self.get_infractions(self.user_data["user_id"]) diff --git a/pysite/views/main/jams/profile.py b/pysite/views/main/jams/profile.py index ce8dfdf1..d8a663f7 100644 --- a/pysite/views/main/jams/profile.py +++ b/pysite/views/main/jams/profile.py @@ -5,10 +5,10 @@ from werkzeug.exceptions import BadRequest from pysite.base_route import RouteView from pysite.decorators import csrf -from pysite.mixins import DBMixin, OauthMixin +from pysite.mixins import DBMixin, OAuthMixin -class JamsProfileView(RouteView, DBMixin, OauthMixin): +class JamsProfileView(RouteView, DBMixin, OAuthMixin): path = "/jams/profile" name = "jams.profile" @@ -16,12 +16,14 @@ class JamsProfileView(RouteView, DBMixin, OauthMixin): def get(self): if not self.user_data: - return redirect(url_for("discord.login")) + return self.redirect_login() participant = self.db.get(self.table_name, self.user_data["user_id"]) + existing = True if not participant: participant = {"id": self.user_data["user_id"]} + existing = False form = request.args.get("form") @@ -32,13 +34,13 @@ class JamsProfileView(RouteView, DBMixin, OauthMixin): pass # Someone trying to have some fun I guess return self.render( - "main/jams/profile.html", participant=participant, form=form + "main/jams/profile.html", participant=participant, form=form, existing=existing ) @csrf def post(self): if not self.user_data: - return redirect(url_for("discord.login")) + return self.redirect_login() participant = self.db.get(self.table_name, self.user_data["user_id"]) @@ -56,6 +58,12 @@ class JamsProfileView(RouteView, DBMixin, OauthMixin): dob = datetime.datetime.strptime(dob, "%Y-%m-%d") dob = dob.replace(tzinfo=datetime.timezone.utc) + now = datetime.datetime.now(tz=datetime.timezone.utc) + then = now.replace(year=now.year - 13) + + if then < dob: + raise BadRequest() # They're too young, but this is validated on the form + participant["dob"] = dob participant["github_username"] = github_username participant["timezone"] = timezone @@ -73,5 +81,5 @@ class JamsProfileView(RouteView, DBMixin, OauthMixin): return redirect(url_for("main.jams.join", jam=form)) return self.render( - "main/jams/profile.html", participant=participant, done=True + "main/jams/profile.html", participant=participant, done=True, existing=True ) diff --git a/pysite/views/main/jams/retract.py b/pysite/views/main/jams/retract.py new file mode 100644 index 00000000..277426b5 --- /dev/null +++ b/pysite/views/main/jams/retract.py @@ -0,0 +1,83 @@ +from werkzeug.exceptions import BadRequest + +from pysite.base_route import RouteView +from pysite.decorators import csrf +from pysite.mixins import DBMixin, OAuthMixin + +BANNABLE_STATES = ("preparing", "running") + + +class JamsProfileView(RouteView, DBMixin, OAuthMixin): + path = "/jams/retract" + name = "jams.retract" + + table_name = "code_jam_participants" + infractions_table = "code_jam_infractions" + jams_table = "code_jams" + responses_table = "code_jam_responses" + + def get(self): + if not self.user_data: + return self.redirect_login() + + user_id = self.user_data["user_id"] + participant = self.db.get(self.table_name, user_id) + + banned = False + + if participant: + responses = self.db.run(self.db.query(self.responses_table).filter({"snowflake": user_id}), coerce=list) + + for response in responses: + jam = response["jam"] + jam_obj = self.db.get(self.jams_table, jam) + + if jam_obj: + if jam_obj["state"] in BANNABLE_STATES: + banned = True + break + + return self.render( + "main/jams/retract.html", participant=participant, banned=banned + ) + + @csrf + def post(self): + if not self.user_data: + return self.redirect_login() + + user_id = self.user_data["user_id"] + participant = self.db.get(self.table_name, user_id) + + if not participant: + return BadRequest() + + banned = False + + responses = self.db.run(self.db.query(self.responses_table).filter({"snowflake": user_id}), coerce=list) + + for response in responses: + jam = response["jam"] + jam_obj = self.db.get(self.jams_table, jam) + + if jam_obj: + if jam_obj["state"] in BANNABLE_STATES: + banned = True + + self.db.delete(self.responses_table, response["id"]) + + self.db.delete(self.table_name, participant["id"]) + + if banned: + self.db.insert( + self.infractions_table, { + "participant": user_id, + "reason": "Automatic ban: Removed jammer profile in the middle of a code jam", + "number": -1, + "decremented_for": [] + } + ) + + return self.render( + "main/jams/retracted.html", participant=participant, banned=banned + ) diff --git a/pysite/views/main/logout.py b/pysite/views/main/logout.py index 2461450d..64326371 100644 --- a/pysite/views/main/logout.py +++ b/pysite/views/main/logout.py @@ -1,4 +1,4 @@ -from flask import redirect, session +from flask import redirect, session, url_for from pysite.base_route import RouteView @@ -12,4 +12,5 @@ class LogoutView(RouteView): # remove user's session del session["session_id"] self.oauth.logout() - return redirect("/") + + return redirect(url_for("main.index")) diff --git a/pysite/views/staff/jams/actions.py b/pysite/views/staff/jams/actions.py index 1af215a5..059f8969 100644 --- a/pysite/views/staff/jams/actions.py +++ b/pysite/views/staff/jams/actions.py @@ -33,7 +33,6 @@ class ActionView(APIView, DBMixin): if action == "questions": questions = self.db.get_all(self.questions_table) - print(questions) return jsonify({"questions": questions}) @csrf diff --git a/pysite/views/staff/jams/edit_ending.py b/pysite/views/staff/jams/edit_ending.py index c4dcfcb3..43a36ebc 100644 --- a/pysite/views/staff/jams/edit_ending.py +++ b/pysite/views/staff/jams/edit_ending.py @@ -39,7 +39,6 @@ class StaffView(RouteView, DBMixin): if not jam_obj["state"] in ALLOWED_STATES: return BadRequest() - print(request.form) for key in REQUIRED_KEYS: arg = request.form.get(key) diff --git a/pysite/views/staff/jams/edit_info.py b/pysite/views/staff/jams/edit_info.py index 7d4401f0..ad0d3d41 100644 --- a/pysite/views/staff/jams/edit_info.py +++ b/pysite/views/staff/jams/edit_info.py @@ -39,7 +39,6 @@ class StaffView(RouteView, DBMixin): if not jam_obj["state"] in ALLOWED_STATES: return BadRequest() - print(request.form) for key in REQUIRED_KEYS: arg = request.form.get(key) diff --git a/pysite/views/staff/jams/forms/questions_edit.py b/pysite/views/staff/jams/forms/questions_edit.py index 4de06793..d46c4ef3 100644 --- a/pysite/views/staff/jams/forms/questions_edit.py +++ b/pysite/views/staff/jams/forms/questions_edit.py @@ -42,8 +42,6 @@ class StaffView(RouteView, DBMixin): optional = request.form.get("optional") question_type = request.form.get("type") - print(question_type) - if not title or not optional or not question_type: return BadRequest() diff --git a/pysite/views/staff/render.py b/pysite/views/staff/render.py index 00c9a9f3..0152e568 100644 --- a/pysite/views/staff/render.py +++ b/pysite/views/staff/render.py @@ -57,7 +57,6 @@ class RenderView(APIView): } ) - print(data) return jsonify(data) except Exception as e: return jsonify({"error": str(e)}) diff --git a/pysite/views/wiki/render.py b/pysite/views/wiki/render.py index b08f54dd..40e5d3f4 100644 --- a/pysite/views/wiki/render.py +++ b/pysite/views/wiki/render.py @@ -57,7 +57,6 @@ class RenderView(APIView): } ) - print(data) return jsonify(data) except Exception as e: return jsonify({"error": str(e)}) diff --git a/static/style.css b/static/style.css index 1f3f9f58..7fa51f0c 100644 --- a/static/style.css +++ b/static/style.css @@ -190,4 +190,12 @@ div.danger-input * { transition: color 0.5s ease, border-color 0.5s ease; +} + +table.table-bordered { + border: 1px solid rgb(229, 229, 229) !important; +} + +tr.thick-bottom-border { + border-bottom: 3px solid rgb(229, 229, 229) !important; }
\ No newline at end of file diff --git a/templates/main/about/privacy.html b/templates/main/about/privacy.html index 870b75a8..92a5eb73 100644 --- a/templates/main/about/privacy.html +++ b/templates/main/about/privacy.html @@ -20,119 +20,216 @@ <p> We take every step to ensure that your data is used ethically and that includes making sure that you know exactly what data we collect, and what we do with it. That means that instead of a - bunch of legal mumbo-jumbo, we've provided this information in an easy, human-readable form below. + bunch of legalese, we've provided this information in an easy, human-readable form below. </p> - <h1 class="uk-article-title hover-title" id="data-collected"> - What We Collect + <p> + Please note that we are a completely non-profit community. We have no interest in selling your + data, or shipping it off to third parties. Our community is entirely volunteer-run - it does + not have any form of monetary income whatsoever - and we believe that this is how it should be. + </p> + + <h3>Data collection</h3> + + <table class="uk-table uk-table-divider uk-table-striped uk-table-small table-bordered"> + <thead> + <tr class="thick-bottom-border"> + <th>What we collect</th> + <th class="uk-table-shrink">When</th> + <th style="max-width: 30rem;">What it's used for</th> + <th>Who can access it</th> + </tr> + </thead> + <tbody> + <tr> + <td>Discord user ID</td> + <td class="uk-table-shrink"><strong>self.accept()</strong> run on Discord</td> + <td style="max-width: 30rem;">Statistics, data association (infractions, code jam applications, etc)</td> + <td>Administrative staff</td> + </tr> + <tr> + <td>Discord username and discriminator</td> + <td class="uk-table-shrink"><strong>self.accept()</strong> run on Discord</td> + <td style="max-width: 30rem;">Display purposes (alongside ID in staff areas, public profiles)</td> + <td>Public, for code jam team listings and winner info</td> + </tr> + <tr> + <td>Assigned roles on Discord</td> + <td class="uk-table-shrink"><strong>self.accept()</strong> run on Discord</td> + <td style="max-width: 30rem;">Access control for the site</td> + <td>Administrative staff</td> + </tr> + <tr class="thick-bottom-border"> + <td>Messages sent on Discord</td> + <td class="uk-table-shrink"><strong>self.accept()</strong> run on Discord</td> + <td style="max-width: 30rem;"> + Stored in memory by the bot for processing temporarily, no message content reaches + the database unless you're using a bot command that interfaces with the site - May be + temporarily written to a log file for debugging purposes + </td> + <td>N/A</td> + </tr> + + <tr class="thick-bottom-border"> + <td>OAuth access and refresh token</td> + <td class="uk-table-shrink">Discord login on site</td> + <td style="max-width: 30rem;">Used to find your Discord user ID when you log in</td> + <td>Administrative staff</td> + </tr> + + <tr> + <td>Date of birth</td> + <td class="uk-table-shrink">Code jam profile </td> + <td style="max-width: 30rem;">Age verification and a factor in code jam team match-ups; only stored if you're over 13</td> + <td>Administrative staff</td> + </tr> + <tr> + <td>GitHub username</td> + <td class="uk-table-shrink">Code jam profile</td> + <td style="max-width: 30rem;">Used to identify you on GitHub as part of a code jam team</td> + <td>Public, for code jam team listings</td> + </tr> + <tr> + <td>Timezone</td> + <td class="uk-table-shrink">Code jam profile</td> + <td style="max-width: 30rem;">A factor in code jam team match-ups</td> + <td>Administrative staff</td> + </tr> + </tbody> + </table> - <a href="#data-collected" class="uk-text-primary" title="Permanent link to this header"> + <h1 class="uk-article-title hover-title" id="consent"> + Collecting consent + + <a href="#consent" class="uk-text-primary" title="Permanent link to this header"> <i class="fas fa-paragraph" data-fa-transform="shrink-8"></i> </a> </h1> - <p class="uk-article-meta"> - Cherry-picking from the firehose of data + + <p> + If you joined the community on or before the <strong>20th of May, 2018</strong>, you will have seen an announcement about our + privacy policy on the Discord server. You will have had the opportunity to leave the server if + you weren't happy with it. If you decided to stay, then we will consider you to have accepted + our use of your data, as detailed on this page. </p> <p> - During your time on the discord server, we collect... + If you joined the community after the <strong>20th of May, 2018</strong>, you will have been greeted with the + <code>#checkpoint</code> channel. In this channel, you must run the <code>self.accept()</code> + command to signify that you accept both our rules and this privacy policy. This will also have been + detailed in a message in that channel. </p> - - <ul> - <li>Your Discord user ID</li> - <li>Your Discord username and discriminator</li> - <li>The list of roles you're assigned on Discord</li> - <li>Any messages you send on the server</li> - </ul> - <p> - Should you click the login button on the site, we additionally collect... + Please note that your acceptance of this privacy policy is retroactive, and you agree that any + revisions to it will apply when they are published. We will attempt to keep everyone updated on + changes to this policy via the usual announcement channels - if at any point you are not happy with + a change to the privacy policy, please bring it up with a member of staff. If we're unable to + solve your issue in a satisfactory way, you may remove your data as detailed below. </p> - <ul> - <li>Your email address, supplied by Discord</li> - <li>An access token and refresh token</li> - </ul> + <h1 class="uk-article-title hover-title" id="removal"> + Data removal + + <a href="#removal" class="uk-text-primary" title="Permanent link to this header"> + <i class="fas fa-paragraph" data-fa-transform="shrink-8"></i> + </a> + </h1> <p> - Should you set up your code jam profile, we additionally collect... + If you'd like to remove your data from our servers, there are two options available to you. </p> - <ul> - <li>Your date of birth</li> - <li>Your GitHub username</li> - <li>Your timezone</li> - </ul> + <div class="uk-grid uk-grid-match" uk-grid> + <div class="uk-width-1-2@m"> + <div class="uk-card uk-card-default uk-card-small"> + <div class="uk-card-header"> + <h3 class="uk-card-title">Complete data removal</h3> + </div> + + <div class="uk-card-body"> + <p> + If you'd like to remove all of your personal data from our servers, all you need to do + is leave the Discord server. As much of the data we collect is necessary for running + our community, we are unable to offer you community membership with zero data collection. + </p> + <p> + Once you've left the Discord server, your data is removed automatically. Please note that + for the sake of data integrity and moderation purposes, we do not remove your Discord + user ID from our database - but we do anonymize your data as far as possible. + </p> + <p> + As with deleting your code jam profile directly, you will be issued an automatic ban + from future code jams if you have applied for or are currently taking part in a + code jam. + </p> + </div> + </div> + </div> + <div class="uk-width-1-2@m"> + <div class="uk-card uk-card-default uk-card-small"> + <div class="uk-card-header"> + <h3 class="uk-card-title">Code jam profile removal</h3> + </div> - <h1 class="uk-article-title hover-title" id="usage"> - How We Use Your Data + <div class="uk-card-body"> + <p> + If you've provided us with a code jam profile in the past and would like to remove + it, you may do so by heading to the + <a href="{{ url_for("main.jams.profile") }}">"My Profile" page</a>, + where you will find a button that will remove your profile. + </p> + <p> + Please note that this is a nuclear option. If you have applied for or are currently + taking part in a code jam, this will void your application and you will receive an + automatic ban from future code jams until you've contacted us about it. + </p> + </div> + </div> + </div> + </div> + + <h1 class="uk-article-title hover-title" id="gdpr"> + GDPR compliance - <a href="#usage" class="uk-text-primary" title="Permanent link to this header"> + <a href="#gdpr" class="uk-text-primary" title="Permanent link to this header"> <i class="fas fa-paragraph" data-fa-transform="shrink-8"></i> </a> </h1> <p class="uk-article-meta"> - Keeping secrets + Keeping your data under your control </p> <p> - We use your data for the daily maintainance of the server and website. In short: We only collect - what we need. To explain this in more detail: + Under the terms specified above, we do aim to comply with GDPR. While we do not currently have + an automated way for users to export the data they've provided to us, we're happy to do this + manually or answer any other GDPR- or privacy-related queries you may have. Feel free to contact + our GDPR officer on Discord (<code>gdude#2002</code>), or any other member of the administrative + staff. + </p> + <p> + We are currently working on an automated way to get all of your data in both a human-readable + and machine-readable format. Keep your eye on the usual announcements channels for more information + on that, as it happens. </p> - <ul> - <li> - Your Discord account details are useful for statistics - However, the storage of your assigned - roles in our database also allows our site to use a Discord account-based authorization system. - That means that all we have to do is assign the correct roles to you on Discord to give you - access to the relevant parts of the site. - </li> - <li> - While we currently do not use your email address for anything, we do have projects in the works - that will make use of it. Don't worry, your email address will never leave our network, and we - won't send anything to you without your consent! - </li> - <li> - Your messages are not stored in our database - they're stored temporarily in memory during - processing, and may also be written to a logfile. - </li> - <li> - We are a completely non-profit community. We have no interest in selling your data, or - shipping it off to third parties. Our community is entirely volunteer-run and it does not have - any form of monetary income whatsoever - and we believe that this is how it should be. - </li> - </ul> - <h1 class="uk-article-title hover-title" id="gdpr"> - GDPR + <h1 class="uk-article-title hover-title" id="changelog"> + Changelog - <a href="#gdpr" class="uk-text-primary" title="Permanent link to this header"> + <a href="#changelog" class="uk-text-primary" title="Permanent link to this header"> <i class="fas fa-paragraph" data-fa-transform="shrink-8"></i> </a> </h1> <p class="uk-article-meta"> - Keeping your data under your control + Accountability, for the masses </p> - <p> - The data we collect is required for the daily operation of this website, our bot and the Discord - server. That said, we intend to fully comply with GDPR. Here's how we do this, and how you can - contact us with any questions you have: - </p> - <ul> - <li> - When you join the server, we require that you accept our rules and terms by running a command - in the <code>#checkpoint</code> channel. In doing so, you agree that you will abide by our rules - - and you also agree to our data collection and usage policies (as detailed above). - </li> - <li> - Should you change your mind at any point, we cannot selectively remove data and keep your - membership on the server. If you'd like us to remove your data, there's only one thing you need - to do: Leave the Discord server. In doing that, all of your data will automatically be removed - from our systems. It's that simple! - </li> + <ul class="uk-list uk-list-divider"> <li> - If you'd like a copy of the data we have belonging to you or you have any other questions about - our data and GDPR handling, feel free to send a message to our GDPR officer on Discord, - <code>gdude#2002</code> - or any other admin if he's not around. + <h4>May 20th, 2018</h4> + <p> + Completed the first version of our privacy policy. We also updated our OAuth scopes for + Discord logins - we no longer collect your email, or get the access to join you to servers + automatically. All collected emails have also been removed from the database. + </p> </li> </ul> </article> diff --git a/templates/main/jams/profile.html b/templates/main/jams/profile.html index efa0e274..cf2088c7 100644 --- a/templates/main/jams/profile.html +++ b/templates/main/jams/profile.html @@ -67,6 +67,17 @@ <button type="submit" class="uk-button uk-button-primary" id="submit"> <i class="uk-icon fa-fw far fa-check"></i> Save </button> + + {% if existing %} + <a class="uk-button uk-button-danger" href="{{ url_for("main.jams.retract") }}"> + <i class="uk-icon fa-fw fas fa-bomb"></i> Delete + </a> + {% else %} + <a class="uk-button uk-button-default uk-text-muted uk-link-muted" style="cursor: default !important" + uk-tooltip="title: You can't delete your profile because you haven't submitted one yet!; pos: bottom"> + <i class="uk-icon fa-fw fas fa-bomb"></i> Delete + </a> + {% endif %} </div> </form> </div> @@ -75,19 +86,47 @@ <script type="application/javascript"> - const date = flatpickr("#dob", {enableTime: false, altInput: true}); + const date = flatpickr("#dob", { + enableTime: false, altInput: true, altInputClass: "date-picker", + onChange: function() { + let dob = moment(dob_input.value); + + if (!dob.isBefore(earliest_dob)) { + UIkit.notification({ + "message": "You must be aged 13 or older to participate in our code jams.", + "status": "danger", + "pos": "top-center", + "timeout": 5000, + }); + + dob_output.classList.add("uk-form-danger"); + submit_button.disabled = true; + } else { + dob_output.classList.remove("uk-form-danger"); + } + } + }); + const tz = moment().format("Z"); const dob_input = document.getElementById("dob"); + const dob_output = document.getElementsByClassName("date-picker")[0]; const github_input = document.getElementById("github_username"); const tz_input = document.getElementById("timezone"); const submit_button = document.getElementById("submit"); + const earliest_dob = moment().subtract(13, "years"); function checkInputs() { if (dob_input.value.length < 1) return submit_button.disabled = true; + let dob = moment(dob_input.value); + + if (!dob.isBefore(earliest_dob)) { + return submit_button.disabled = true; + } + if (github_input.value.length < 1) return submit_button.disabled = true; diff --git a/templates/main/jams/retract.html b/templates/main/jams/retract.html new file mode 100644 index 00000000..bbe3bdae --- /dev/null +++ b/templates/main/jams/retract.html @@ -0,0 +1,61 @@ +{% extends "main/base.html" %} +{% block title %}Code Jams | Already applied{% endblock %} +{% block og_title %}Code Jams | Already applied{% endblock %} + +{% block content %} +<div class="uk-section"> + <div class="uk-container uk-container-small"> + <h1 class="uk-header uk-article-title"> + Code Jams: Retract Profile + </h1> + + {% if participant %} + <p> + Are you sure you'd like to retract your code jam profile? + </p> + + {% if banned %} + <p> + Retracting your code jam profile will remove your date of birth, GitHub username and timezone from our + database. If you're entirely sure that you'd like to remove your profile, please click on the "Remove" button below. + </p> + + <p> + As you are currently taking part in a code jam, + <strong class="uk-text-danger">this will void your application and you will receive an automatic ban from future code jams</strong> + until you've contacted us about it. + </p> + {% else %} + <p> + Retracting your code jam profile will remove your date of birth, GitHub username and timezone from our + database. If you're entirely sure that you'd like to remove your profile, please click on the "Remove" button below. + </p> + + <p> + As you are not currently taking part in an ongoing code jam, + <strong class="uk-text-primary">you will not be banned from future code jams</strong>. + </p> + {% endif %} + + <form action="{{ url_for("main.jams.retract") }}" method="post" class="uk-form uk-text-center" uk-form> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> + + <a class="uk-button uk-button-primary" href="{{ url_for("main.jams.profile") }}"> + <i class="uk-icon fa-fw far fa-arrow-left"></i> Cancel + </a> + <button class="uk-button uk-button-danger" type="submit"> + <i class="uk-icon fa-fw fas fa-bomb"></i> Remove + </button> + </form> + {% else %} + <p class="uk-alert uk-alert-danger"> + You can't delete your profile - you haven't submitted one to us yet! + </p> + + <a class="uk-button uk-button-secondary uk-width-1-1" href="{{ url_for("main.jams.profile") }}"> + <i class="uk-icon fa-fw far fa-arrow-left"></i> Back + </a> + {% endif %} + </div> +</div> +{% endblock %} diff --git a/templates/main/jams/retracted.html b/templates/main/jams/retracted.html new file mode 100644 index 00000000..b67b6497 --- /dev/null +++ b/templates/main/jams/retracted.html @@ -0,0 +1,31 @@ +{% extends "main/base.html" %} +{% block title %}Code Jams | Already applied{% endblock %} +{% block og_title %}Code Jams | Already applied{% endblock %} + +{% block content %} +<div class="uk-section"> + <div class="uk-container uk-container-small"> + <h1 class="uk-header uk-article-title"> + Code Jams: Profile Retracted + </h1> + + {% if banned %} + <p> + Your code jam profile has been deleted. As you were participating in an ongoing code jam, you have + been issued with an automatic ban from future code jams. If you'd like to join a code jam in the + future, please contact us directly and we'll try to resolve the situation with you. Thanks for your + interest in our code jams regardless! + </p> + {% else %} + <p> + Your code jam profile has been deleted. you were not participating in an ongoing code jam, no further + action is required by you. Thanks for your interest in our code jams regardless! + </p> + {% endif %} + + <a class="uk-button uk-button-secondary uk-width-1-1" href="{{ url_for("main.jams.index") }}"> + <i class="uk-icon fa-fw far fa-arrow-left"></i> Back to code jams + </a> + </div> +</div> +{% endblock %} diff --git a/templates/main/navigation.html b/templates/main/navigation.html index 3130747b..ea5bac3e 100644 --- a/templates/main/navigation.html +++ b/templates/main/navigation.html @@ -26,6 +26,14 @@ {% endif %} <li><a href="{{ url_for('main.invite') }}"><i class="uk-icon fab fa-discord fa-fw"></i> Discord</a></li> + + {% if is_staff() %} + {% if current_page.startswith("staff.") %} + <li class="uk-active"><a href="{{ url_for('staff.index') }}"><i class="uk-icon fas fa-wrench fa-fw"></i> Staff</a></li> + {% else %} + <li class=""><a href="{{ url_for('staff.index') }}"><i class="uk-icon fas fa-wrench fa-fw"></i> Staff</a></li> + {% endif %} + {% endif %} </ul> <ul class="uk-navbar-nav"> <li> @@ -47,6 +55,14 @@ {% endif %} <li class="uk-nav-item uk-hidden@m"><a href="{{ url_for('main.invite') }}"><i class="uk-icon fab fa-discord fa-fw"></i> Discord</a></li> + + {% if is_staff() %} + {% if current_page.startswith("staff.") %} + <li class="uk-nav-item uk-active uk-hidden@m"><a href="{{ url_for('staff.index') }}"><i class="uk-icon fas fa-wrench fa-fw"></i> Staff</a></li> + {% else %} + <li class="uk-nav-item uk-hidden@m"><a href="{{ url_for('staff.index') }}"><i class="uk-icon fas fa-wrench fa-fw"></i> Staff</a></li> + {% endif %} + {% endif %} <li class="uk-nav-divider uk-hidden@m"></li> {% if not debug %} diff --git a/templates/staff/jams/index.html b/templates/staff/jams/index.html index dea7429c..b5734b51 100644 --- a/templates/staff/jams/index.html +++ b/templates/staff/jams/index.html @@ -24,45 +24,45 @@ <span class="uk-align-right"> {% if jam.state == "planning" %} - <i class="uk-icon uk-text-muted fa-fw far fa-edit state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-planning"></i> + <i class="uk-icon uk-text-muted fa-fw far fa-edit state-{{ jam.number }}" title="State: Planning" id="state-{{ jam.number }}-planning"></i> {% else %} - <i class="uk-icon uk-text-muted fa-fw far fa-edit state-{{ jam.number }}" style="display: none;" title="State: {{ jam.state }}" id="state-{{ jam.number }}-planning"></i> + <i class="uk-icon uk-text-muted fa-fw far fa-edit state-{{ jam.number }}" style="display: none;" title="State: Planning" id="state-{{ jam.number }}-planning"></i> {% endif %} {% if jam.state == "announced" %} - <i class="uk-icon uk-text-primary fa-fw far fa-bullhorn state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-announced"></i> + <i class="uk-icon uk-text-primary fa-fw far fa-bullhorn state-{{ jam.number }}" title="State: Announced" id="state-{{ jam.number }}-announced"></i> {% else %} - <i class="uk-icon uk-text-primary fa-fw far fa-bullhorn state-{{ jam.number }}" hidden="hidden" title="State: {{ jam.state }}" id="state-{{ jam.number }}-announced"></i> + <i class="uk-icon uk-text-primary fa-fw far fa-bullhorn state-{{ jam.number }}" hidden="hidden" title="State: Announced" id="state-{{ jam.number }}-announced"></i> {% endif %} {% if jam.state == "preparing" %} - <i class="uk-icon uk-text-muted fa-fw far fa-pause state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-preparing"></i> + <i class="uk-icon uk-text-muted fa-fw fal fa-ellipsis-h-alt state-{{ jam.number }}" title="State: Preparing" id="state-{{ jam.number }}-preparing"></i> {% else %} - <i class="uk-icon uk-text-muted fa-fw far fa-pause state-{{ jam.number }}" hidden="hidden" title="State: {{ jam.state }}" id="state-{{ jam.number }}-preparing"></i> + <i class="uk-icon uk-text-muted fa-fw fal fa-ellipsis-h-alt state-{{ jam.number }}" hidden="hidden" title="State: Preparing" id="state-{{ jam.number }}-preparing"></i> {% endif %} {% if jam.state == "running" %} - <i class="uk-icon uk-text-success fa-fw far fa-play state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-running"></i> + <i class="uk-icon uk-text-success fa-fw far fa-play state-{{ jam.number }}" title="State: Running" id="state-{{ jam.number }}-running"></i> {% else %} - <i class="uk-icon uk-text-success fa-fw far fa-play state-{{ jam.number }}" hidden="hidden" title="State: {{ jam.state }}" id="state-{{ jam.number }}-running"></i> + <i class="uk-icon uk-text-success fa-fw far fa-play state-{{ jam.number }}" hidden="hidden" title="State: Running" id="state-{{ jam.number }}-running"></i> {% endif %} {% if jam.state == "judging" %} - <i class="uk-icon uk-text-primary fa-fw far fa-balance-scale state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-judging"></i> + <i class="uk-icon uk-text-primary fa-fw far fa-balance-scale state-{{ jam.number }}" title="State: Judging" id="state-{{ jam.number }}-judging"></i> {% else %} - <i class="uk-icon uk-text-primary fa-fw far fa-balance-scale state-{{ jam.number }}" hidden="hidden" title="State: {{ jam.state }}" id="state-{{ jam.number }}-judging"></i> + <i class="uk-icon uk-text-primary fa-fw far fa-balance-scale state-{{ jam.number }}" hidden="hidden" title="State: Judging" id="state-{{ jam.number }}-judging"></i> {% endif %} {% if jam.state == "finished" %} - <i class="uk-icon uk-text-success fa-fw far fa-check-square state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-finished"></i> + <i class="uk-icon uk-text-success fa-fw far fa-check-square state-{{ jam.number }}" title="State: Finished" id="state-{{ jam.number }}-finished"></i> {% else %} - <i class="uk-icon uk-text-success fa-fw far fa-check-square state-{{ jam.number }}" hidden="hidden" title="State: {{ jam.state }}" id="state-{{ jam.number }}-finished"></i> + <i class="uk-icon uk-text-success fa-fw far fa-check-square state-{{ jam.number }}" hidden="hidden" title="State: Finished" id="state-{{ jam.number }}-finished"></i> {% endif %} {% if not jam.state in states %} - <i class="uk-icon uk-text-danger fa-fw far fa-question-square" title="Unknown state: {{ jam.state }}" id="state-{{ jam.number }}-unknown"></i> + <i class="uk-icon uk-text-danger fa-fw far fa-question-square" title="Unknown state" id="state-{{ jam.number }}-unknown"></i> {% else %} - <i class="uk-icon uk-text-danger fa-fw far fa-question-square" hidden="hidden" title="Unknown state: {{ jam.state }}" id="state-{{ jam.number }}-unknown"></i> + <i class="uk-icon uk-text-danger fa-fw far fa-question-square" hidden="hidden" title="Unknown state" id="state-{{ jam.number }}-unknown"></i> {% endif %} </span> </h2> @@ -218,7 +218,7 @@ document.getElementById("jam-" + jam + "-button-ending").setAttribute("hidden", "hidden"); break; case "judging": - document.getElementById("state-" + jam + "-finished").removeAttribute("hidden"); + document.getElementById("state-" + jam + "-judging").removeAttribute("hidden"); document.getElementById("jam-" + jam + "-button-info").setAttribute("hidden", "hidden"); document.getElementById("jam-" + jam + "-button-ending").removeAttribute("hidden"); diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 58118ae0..27e61b22 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -27,7 +27,7 @@ class MixinTests(SiteTest): from flask import Blueprint from pysite.route_manager import RouteView - from pysite.oauth import OauthBackend + from pysite.oauth import OAuthBackend class TestRoute(RouteView): name = "test" @@ -35,7 +35,7 @@ class MixinTests(SiteTest): tr = TestRoute() tr.setup(manager, Blueprint("test", "test_name")) - self.assertIsInstance(tr.oauth, OauthBackend) + self.assertIsInstance(tr.oauth, OAuthBackend) def test_user_data_property(self): """ Make sure the user_data property works""" diff --git a/tests/test_oauth_backend.py b/tests/test_oauth_backend.py index 58e40c25..473f5c54 100644 --- a/tests/test_oauth_backend.py +++ b/tests/test_oauth_backend.py @@ -1,6 +1,6 @@ from tests import SiteTest, manager -class TestOauthBackend(SiteTest): +class TestOAuthBackend(SiteTest): """ Test cases for the oauth.py file """ def test_get(self): @@ -23,7 +23,7 @@ class TestOauthBackend(SiteTest): sess_id = "hey bro wazup" fake_token = {"access_token": "access_token", "id": sess_id, "refresh_token": "refresh_token", "expires_at": 5} - fake_user = {"id": 1235678987654321, "username": "Zwacky", "discriminator": "#6660", "email": "[email protected]"} + fake_user = {"id": 1235678987654321, "username": "Zwacky", "discriminator": "#6660"} manager.db.conn = manager.db.get_connection() manager.oauth_backend.add_user(fake_token, fake_user, sess_id) |