From 449d52caf4010ed112f1928bf6b5234bcfb9a339 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Sun, 20 May 2018 23:29:17 +0100 Subject: 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 --- app_test.py | 0 pysite/base_route.py | 32 ++- pysite/constants.py | 2 +- pysite/decorators.py | 13 +- pysite/migrations/tables/oauth_data/v1.py | 4 + pysite/migrations/tables/users/__init__.py | 0 pysite/migrations/tables/users/v1.py | 11 ++ pysite/migrations/tables/wiki/v1.py | 4 + pysite/mixins.py | 10 +- pysite/oauth.py | 7 +- pysite/route_manager.py | 6 +- pysite/tables.py | 3 +- pysite/views/api/bot/user.py | 86 +++++++- pysite/views/main/auth/__init__.py | 0 pysite/views/main/auth/done.py | 18 ++ pysite/views/main/jams/index.py | 2 +- pysite/views/main/jams/join.py | 8 +- pysite/views/main/jams/profile.py | 20 +- pysite/views/main/jams/retract.py | 83 ++++++++ pysite/views/main/logout.py | 5 +- pysite/views/staff/jams/actions.py | 1 - pysite/views/staff/jams/edit_ending.py | 1 - pysite/views/staff/jams/edit_info.py | 1 - pysite/views/staff/jams/forms/questions_edit.py | 2 - pysite/views/staff/render.py | 1 - pysite/views/wiki/render.py | 1 - static/style.css | 8 + templates/main/about/privacy.html | 253 ++++++++++++++++-------- templates/main/jams/profile.html | 41 +++- templates/main/jams/retract.html | 61 ++++++ templates/main/jams/retracted.html | 31 +++ templates/main/navigation.html | 16 ++ templates/staff/jams/index.html | 30 +-- tests/test_mixins.py | 4 +- tests/test_oauth_backend.py | 4 +- 35 files changed, 621 insertions(+), 148 deletions(-) create mode 100644 app_test.py create mode 100644 pysite/migrations/tables/users/__init__.py create mode 100644 pysite/migrations/tables/users/v1.py create mode 100644 pysite/views/main/auth/__init__.py create mode 100644 pysite/views/main/auth/done.py create mode 100644 pysite/views/main/jams/retract.py create mode 100644 templates/main/jams/retract.html create mode 100644 templates/main/jams/retracted.html diff --git a/app_test.py b/app_test.py new file mode 100644 index 00000000..e69de29b 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 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 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/" 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 @@

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.

-

- What We Collect +

+ 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. +

+ +

Data collection

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
What we collectWhenWhat it's used forWho can access it
Discord user IDself.accept() run on DiscordStatistics, data association (infractions, code jam applications, etc)Administrative staff
Discord username and discriminatorself.accept() run on DiscordDisplay purposes (alongside ID in staff areas, public profiles)Public, for code jam team listings and winner info
Assigned roles on Discordself.accept() run on DiscordAccess control for the siteAdministrative staff
Messages sent on Discordself.accept() run on Discord + 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 + N/A
OAuth access and refresh tokenDiscord login on siteUsed to find your Discord user ID when you log inAdministrative staff
Date of birthCode jam profile Age verification and a factor in code jam team match-ups; only stored if you're over 13Administrative staff
GitHub usernameCode jam profileUsed to identify you on GitHub as part of a code jam teamPublic, for code jam team listings
TimezoneCode jam profileA factor in code jam team match-upsAdministrative staff
- +

+ Collecting consent + +

-

+ If you joined the community on or before the 20th of May, 2018, 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.

- During your time on the discord server, we collect... + If you joined the community after the 20th of May, 2018, you will have been greeted with the + #checkpoint channel. In this channel, you must run the self.accept() + 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.

- - -

- 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.

- +

+ Data removal + + + + +

- 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.

- +
+
+
+
+

Complete data removal

+
+ +
+

+ 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. +

+

+ 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. +

+

+ 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. +

+
+
+
+
+
+
+

Code jam profile removal

+
-

- How We Use Your Data +
+

+ 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 + "My Profile" page, + where you will find a button that will remove your profile. +

+

+ 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. +

+
+

+
+
+ +

+ GDPR compliance - +

- 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 (gdude#2002), or any other member of the administrative + staff. +

+

+ 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.

- -

- GDPR +

+ Changelog - +

-

- 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: -

-