diff options
author | 2018-08-07 15:09:08 +0100 | |
---|---|---|
committer | 2018-08-07 15:09:16 +0100 | |
commit | af54db6c136138c66cf5ca72419989525a0baa5c (patch) | |
tree | 8519aeab8d45277c51797c7dc23aacf3b56ed1bb /pysite/views | |
parent | A wizard is never late, nor is he early. (diff) |
Initial project layout for django
Diffstat (limited to 'pysite/views')
114 files changed, 0 insertions, 5407 deletions
diff --git a/pysite/views/__init__.py b/pysite/views/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/__init__.py +++ /dev/null diff --git a/pysite/views/api/__init__.py b/pysite/views/api/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/api/__init__.py +++ /dev/null diff --git a/pysite/views/api/bot/__init__.py b/pysite/views/api/bot/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/api/bot/__init__.py +++ /dev/null diff --git a/pysite/views/api/bot/bigbrother.py b/pysite/views/api/bot/bigbrother.py deleted file mode 100644 index 89697811..00000000 --- a/pysite/views/api/bot/bigbrother.py +++ /dev/null @@ -1,118 +0,0 @@ -import json - -from flask import jsonify -from schema import And, Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - - -GET_SCHEMA = Schema({ - # This is passed as a GET parameter, so it has to be a string - Optional('user_id'): And(str, str.isnumeric, error="`user_id` must be a numeric string") -}) - -POST_SCHEMA = Schema({ - 'user_id': And(str, str.isnumeric, error="`user_id` must be a numeric string"), - 'channel_id': And(str, str.isnumeric, error="`channel_id` must be a numeric string") -}) - -DELETE_SCHEMA = Schema({ - 'user_id': And(str, str.isnumeric, error="`user_id` must be a numeric string") -}) - - -NOT_A_NUMBER_JSON = json.dumps({ - 'error_message': "The given `user_id` parameter is not a valid number" -}) -NOT_FOUND_JSON = json.dumps({ - 'error_message': "No entry for the requested user ID could be found." -}) - - -class BigBrotherView(APIView, DBMixin): - path = '/bot/bigbrother' - name = 'bot.bigbrother' - table_name = 'watched_users' - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params): - """ - Without query parameters, returns a list of all monitored users. - A parameter `user_id` can be specified to return a single entry, - or a dictionary with the string field 'error_message' that tells why it failed. - - If the returned status is 200, has got either a list of entries - or a single object (see above). - - If the returned status is 400, the `user_id` parameter was incorrectly specified. - If the returned status is 404, the given `user_id` could not be found. - See the 'error_message' field in the JSON response for more information. - - The user ID must be provided as query parameter. - API key must be provided as header. - """ - - user_id = params.get('user_id') - if user_id is not None: - data = self.db.get(self.table_name, user_id) - if data is None: - return NOT_FOUND_JSON, 404 - return jsonify(data) - - else: - data = self.db.pluck(self.table_name, ('user_id', 'channel_id')) or [] - return jsonify(data) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - """ - Adds a new entry to the database. - Entries take the following form: - { - "user_id": ..., # The user ID of the user being monitored, as a string. - "channel_id": ... # The channel ID that the user's messages will be relayed to, as a string. - } - - If an entry for the given `user_id` already exists, it will be updated with the new channel ID. - - Returns 204 (ok, empty response) on success. - - Data must be provided as JSON. - API key must be provided as header. - """ - - self.db.insert( - self.table_name, - { - 'user_id': data['user_id'], - 'channel_id': data['channel_id'] - }, - conflict='update' - ) - - return '', 204 - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.params) - def delete(self, params): - """ - Removes an entry for the given `user_id`. - - Returns 204 (ok, empty response) on success. - Returns 400 if the given `user_id` is invalid. - - The user ID must be provided as query parameter. - API key must be provided as header. - """ - - self.db.delete( - self.table_name, - params['user_id'] - ) - - return '', 204 diff --git a/pysite/views/api/bot/clean.py b/pysite/views/api/bot/clean.py deleted file mode 100644 index 82d1e735..00000000 --- a/pysite/views/api/bot/clean.py +++ /dev/null @@ -1,48 +0,0 @@ -from flask import jsonify -from schema import Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -POST_SCHEMA = Schema({ - 'log_data': [ - { - "author": str, - "user_id": str, - "content": str, - "role_id": str, - "timestamp": str, - "embeds": object, - "attachments": [str], - } - ] -}) - - -class CleanView(APIView, DBMixin): - path = '/bot/clean' - name = 'bot.clean' - table_name = 'clean_logs' - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - """ - Receive some log_data from a bulk deletion, - and store it in the database. - - Returns an ID which can be used to get the data - from the /bot/clean_logs/<id> endpoint. - """ - - # Insert and return the id to use for GET - insert = self.db.insert( - self.table_name, - { - "log_data": data["log_data"] - } - ) - - return jsonify({"log_id": insert['generated_keys'][0]}) diff --git a/pysite/views/api/bot/doc.py b/pysite/views/api/bot/doc.py deleted file mode 100644 index c1d6020c..00000000 --- a/pysite/views/api/bot/doc.py +++ /dev/null @@ -1,98 +0,0 @@ -from flask import jsonify -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - - -GET_SCHEMA = Schema([ - { - Optional("package"): str - } -]) - -POST_SCHEMA = Schema([ - { - "package": str, - "base_url": str, - "inventory_url": str - } -]) - -DELETE_SCHEMA = Schema([ - { - "package": str - } -]) - - -class DocView(APIView, DBMixin): - path = "/bot/docs" - name = "bot.docs" - table_name = "pydoc_links" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - """ - Fetches documentation metadata from the database. - - - If `package` parameters are provided, fetch metadata - for the given packages, or `[]` if none matched. - - - If `package` is not provided, return all - packages known to the database. - - Data must be provided as params. - API key must be provided as header. - """ - - if params: - packages = (param['package'] for param in params if 'package' in param) - data = self.db.get_all(self.table_name, *packages, index='package') or [] - else: - data = self.db.pluck(self.table_name, ("package", "base_url", "inventory_url")) or [] - - return jsonify(data) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, json_data): - """ - Adds one or more new documentation metadata objects. - - If the `package` passed in the data - already exists, it will be updated instead. - - Data must be provided as JSON. - API key must be provided as header. - """ - - packages_to_insert = ( - { - "package": json_object["package"], - "base_url": json_object["base_url"], - "inventory_url": json_object["inventory_url"] - } for json_object in json_data - ) - - self.db.insert(self.table_name, *packages_to_insert, conflict="update") - return jsonify({"success": True}) - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json) - def delete(self, json_data): - """ - Deletes a documentation metadata object. - Expects the `package` to be deleted to - be specified as a request parameter. - - Data must be provided as params. - API key must be provided as header. - """ - - packages = (json_object["package"]for json_object in json_data) - changes = self.db.delete(self.table_name, *packages, return_changes=True) - return jsonify(changes) diff --git a/pysite/views/api/bot/hiphopify.py b/pysite/views/api/bot/hiphopify.py deleted file mode 100644 index ce4dfa4a..00000000 --- a/pysite/views/api/bot/hiphopify.py +++ /dev/null @@ -1,170 +0,0 @@ -import logging - -from flask import jsonify -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin -from pysite.utils.time import is_expired, parse_duration - -log = logging.getLogger(__name__) - -GET_SCHEMA = Schema({ - "user_id": str -}) - -POST_SCHEMA = Schema({ - "user_id": str, - "duration": str, - Optional("forced_nick"): str -}) - -DELETE_SCHEMA = Schema({ - "user_id": str -}) - - -class HiphopifyView(APIView, DBMixin): - path = "/bot/hiphopify" - name = "bot.hiphopify" - prison_table = "hiphopify" - name_table = "hiphopify_namelist" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - """ - Check if the user is currently in hiphop-prison. - - If user is currently servin' his sentence in the big house, - return the name stored in the forced_nick column of prison_table. - - If user cannot be found in prison, or - if his sentence has expired, return nothing. - - Data must be provided as params. - API key must be provided as header. - """ - - user_id = params.get("user_id") - - log.debug(f"Checking if user ({user_id}) is permitted to change their nickname.") - data = self.db.get(self.prison_table, user_id) or {} - - if data and data.get("end_timestamp"): - log.trace("User exists in the prison_table.") - end_time = data.get("end_timestamp") - if is_expired(end_time): - log.trace("...But their sentence has already expired.") - data = {} # Return nothing if the sentence has expired. - - return jsonify(data) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, json_data): - """ - Imprisons a user in hiphop-prison. - - If a forced_nick was provided by the caller, the method will force - this nick. If not, a random hiphop nick will be selected from the - name_table. - - Data must be provided as JSON. - API key must be provided as header. - """ - - user_id = json_data.get("user_id") - duration = json_data.get("duration") - forced_nick = json_data.get("forced_nick") - - log.debug(f"Attempting to imprison user ({user_id}).") - - # Get random name and picture if no forced_nick was provided. - if not forced_nick: - log.trace("No forced_nick provided. Fetching a random rapper name and image.") - rapper_data = self.db.sample(self.name_table, 1)[0] - forced_nick = rapper_data.get('name') - - # If forced nick was provided, try to look up the forced_nick in the database. - # If a match cannot be found, just default to Lil' Jon for the image. - else: - log.trace(f"Forced nick provided ({forced_nick}). Trying to match it with the database.") - rapper_data = ( - self.db.get(self.name_table, forced_nick) - or self.db.get(self.name_table, "Lil' Joseph") - ) - - image_url = rapper_data.get('image_url') - log.trace(f"Using the nickname {forced_nick} and the image_url {image_url}.") - - # Convert duration to valid timestamp - try: - log.trace("Parsing the duration and converting it to a timestamp") - end_timestamp = parse_duration(duration) - except ValueError: - log.warning(f"The duration could not be parsed, or was invalid. The duration was '{duration}'.") - return jsonify({ - "success": False, - "error_message": "Invalid duration" - }) - - log.debug("Everything seems to be in order, inserting the data into the prison_table.") - self.db.insert( - self.prison_table, - { - "user_id": user_id, - "end_timestamp": end_timestamp, - "forced_nick": forced_nick - }, - conflict="update" # If it exists, update it. - ) - - return jsonify({ - "success": True, - "end_timestamp": end_timestamp, - "forced_nick": forced_nick, - "image_url": image_url - }) - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json) - def delete(self, json_data): - """ - Releases a user from hiphop-prison. - - Data must be provided as JSON. - API key must be provided as header. - """ - - user_id = json_data.get("user_id") - - log.debug(f"Attempting to release user ({user_id}) from hiphop-prison.") - prisoner_data = self.db.get(self.prison_table, user_id) - sentence_expired = None - - log.trace(f"Checking if the user ({user_id}) is currently in hiphop-prison.") - if prisoner_data and prisoner_data.get("end_timestamp"): - sentence_expired = is_expired(prisoner_data['end_timestamp']) - - if prisoner_data and not sentence_expired: - log.debug("User is currently in hiphop-prison. Deleting the record and releasing the prisoner.") - self.db.delete( - self.prison_table, - user_id - ) - return jsonify({"success": True}) - elif not prisoner_data: - log.warning(f"User ({user_id}) is not currently in hiphop-prison.") - return jsonify({ - "success": False, - "error_message": "User is not currently in hiphop-prison!" - }) - elif sentence_expired: - log.warning(f"User ({user_id}) was in hiphop-prison, but has already been released.") - return jsonify({ - "success": False, - "error_message": "User has already been released from hiphop-prison!" - }) diff --git a/pysite/views/api/bot/infractions.py b/pysite/views/api/bot/infractions.py deleted file mode 100644 index eee40b82..00000000 --- a/pysite/views/api/bot/infractions.py +++ /dev/null @@ -1,572 +0,0 @@ -""" -INFRACTIONS API - -"GET" endpoints in this API may take the following optional parameters, depending on the endpoint: - - active: filters infractions that are active (true), expired (false), or either (not present/any) - - expand: expands the result data with the information about the users (slower) - - dangling: filters infractions that are active, or inactive infractions that have not been closed manually. - - search: filters the "reason" field to match the given RE2 query. - -Infraction Schema: - This schema is used when an infraction's data is returned. - - Root object: - "id" (str): the UUID of the infraction. - "inserted_at" (str): the date and time of the creation of this infraction (RFC1123 format). - "expires_at" (str): the date and time of the expiration of this infraction (RC1123 format), may be null. - The significance of this field being null depends on the type of infraction. Duration-based infractions - have a "null" expiration if they are permanent. Other infraction types do not have expirations. - "active" (bool): whether the infraction is still active. Note that the check for expiration of - duration-based infractions is done by the API, so you should check for expiration using this "active" field. - "user" (object): the user to which the infraction was applied. - "user_id" (str): the Discord ID of the user. - "username" (optional str): the username of the user. This field is only present if the query was expanded. - "discriminator" (optional int): the username discriminator of the user. This field is only present if the - query was expanded. - "avatar" (optional str): the avatar URL of the user. This field is only present if the query was expanded. - "actor" (object): the user which applied the infraction. - This object uses the same schema as the "user" field. - "type" (str): the type of the infraction. - "reason" (str): the reason for the infraction. - - -Endpoints: - - GET /bot/infractions - Gets a list of all infractions, regardless of type or user. - Parameters: "active", "expand", "dangling", "search". - This endpoint returns an array of infraction objects. - - GET /bot/infractions/user/<user_id> - Gets a list of all infractions for a user. - Parameters: "active", "expand", "search". - This endpoint returns an array of infraction objects. - - GET /bot/infractions/type/<type> - Gets a list of all infractions of the given type (ban, mute, etc.) - Parameters: "active", "expand", "search". - This endpoint returns an array of infraction objects. - - GET /bot/infractions/user/<user_id>/<type> - Gets a list of all infractions of the given type for a user. - Parameters: "active", "expand", "search". - This endpoint returns an array of infraction objects. - - GET /bot/infractions/user/<user_id>/<type>/current - Gets the active infraction (if any) of the given type for a user. - Parameters: "expand". - This endpoint returns an object with the "infraction" key, which is either set to null (no infraction) - or the query's corresponding infraction. It will not return an infraction if the type of the infraction - isn't duration-based (e.g. kick, warning, etc.) - - GET /bot/infractions/id/<infraction_id> - Gets the infraction (if any) for the given ID. - Parameters: "expand". - This endpoint returns an object with the "infraction" key, which is either set to null (no infraction) - or the infraction corresponding to the ID. - - POST /bot/infractions - Creates an infraction for a user. - Parameters (JSON payload): - "type" (str): the type of the infraction (must be a valid infraction type). - "reason" (str): the reason of the infraction. - "user_id" (str): the Discord ID of the user who is being given the infraction. - "actor_id" (str): the Discord ID of the user who submitted the infraction. - "duration" (optional str): the duration of the infraction. This is ignored for infractions - which are not duration-based. For other infraction types, omitting this field may imply permanence. - "expand" (optional bool): whether to expand the infraction user data once the infraction is inserted and returned. - - PATCH /bot/infractions - Updates an infractions. - Parameters (JSON payload): - "id" (str): the ID of the infraction to update. - "reason" (optional str): if provided, the new reason for the infraction. - "duration" (optional str): if provided, updates the expiration of the infraction to the time of UPDATING - plus the duration. If set to null, the expiration is also set to null (may imply permanence). - "active" (optional bool): if provided, activates or deactivates the infraction. This does not do anything - if the infraction isn't duration-based, or if the infraction has already expired. This marks the infraction - as closed. - "expand" (optional bool): whether to expand the infraction user data once the infraction is updated and returned. -""" - -import datetime -from typing import NamedTuple - -import rethinkdb -from flask import jsonify -from schema import Optional, Or, Schema - -from pysite.base_route import APIView -from pysite.constants import ErrorCodes, ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin -from pysite.utils.time import parse_duration - - -class InfractionType(NamedTuple): - timed_infraction: bool # whether the infraction is active until it expires. - - -RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" -EXCLUDED_FIELDS = "user_id", "actor_id", "closed", "_timed" -INFRACTION_ORDER = rethinkdb.desc("active"), rethinkdb.desc("inserted_at") - -INFRACTION_TYPES = { - "warning": InfractionType(timed_infraction=False), - "mute": InfractionType(timed_infraction=True), - "ban": InfractionType(timed_infraction=True), - "kick": InfractionType(timed_infraction=False), - "superstar": InfractionType(timed_infraction=True) # hiphopify -} - -GET_SCHEMA = Schema({ - Optional("active"): str, - Optional("expand"): str, - Optional("dangling"): str, - Optional("search"): str -}) - -GET_ACTIVE_SCHEMA = Schema({ - Optional("expand"): str -}) - -CREATE_INFRACTION_SCHEMA = Schema({ - "type": lambda tp: tp in INFRACTION_TYPES, - "reason": Or(str, None), - "user_id": str, # Discord user ID - "actor_id": str, # Discord user ID - Optional("duration"): str, # If not provided, may imply permanence depending on the infraction - Optional("expand"): bool -}) - -UPDATE_INFRACTION_SCHEMA = Schema({ - "id": str, - Optional("reason"): Or(str, None), - Optional("duration"): Or(str, None), - Optional("active"): bool -}) - -IMPORT_INFRACTIONS_SCHEMA = Schema([ - { - "id": str, - "active": bool, - "actor": { - "id": str - }, - "created_at": str, - "expires_at": Or(str, None), - "reason": Or(str, None), - "type": { - "name": str - }, - "user": { - "id": str - } - } -], ignore_extra_keys=True) - - -class InfractionsView(APIView, DBMixin): - path = "/bot/infractions" - name = "bot.infractions" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params: dict = None): - if "dangling" in params: - return _infraction_list_filtered(self, params, {"_timed": True, "closed": False}) - else: - return _infraction_list_filtered(self, params, {}) - - @api_key - @api_params(schema=CREATE_INFRACTION_SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - deactivate_infraction_query = None - - infraction_type = data["type"] - user_id = data["user_id"] - actor_id = data["actor_id"] - reason = data["reason"] - duration_str = data.get("duration") - expand = data.get("expand") - expires_at = None - inserted_at = datetime.datetime.now(tz=datetime.timezone.utc) - - if infraction_type not in INFRACTION_TYPES: - return self.error(ErrorCodes.incorrect_parameters, "Invalid infraction type.") - - # check if the user already has an active infraction of this type - # if so, we need to disable that infraction and create a new infraction - if INFRACTION_TYPES[infraction_type].timed_infraction: - active_infraction_query = \ - self.db.query(self.table_name).merge(_merge_active_check()) \ - .filter({"user_id": user_id, "type": infraction_type, "active": True}) \ - .limit(1).nth(0).default(None) - - active_infraction = self.db.run(active_infraction_query) - if active_infraction: - deactivate_infraction_query = \ - self.db.query(self.table_name) \ - .get(active_infraction["id"]) \ - .update({"active": False, "closed": True}) - - if duration_str: - try: - expires_at = parse_duration(duration_str) - except ValueError: - return self.error( - ErrorCodes.incorrect_parameters, - "Invalid duration format." - ) - - infraction_insert_doc = { - "actor_id": actor_id, - "user_id": user_id, - "type": infraction_type, - "reason": reason, - "inserted_at": inserted_at, - "expires_at": expires_at - } - - infraction_id = self.db.insert(self.table_name, infraction_insert_doc)["generated_keys"][0] - - if deactivate_infraction_query: - self.db.run(deactivate_infraction_query) - - query = self.db.query(self.table_name).get(infraction_id) \ - .merge(_merge_expand_users(self, expand)) \ - .merge(_merge_active_check()) \ - .without(*EXCLUDED_FIELDS).default(None) - return jsonify({ - "infraction": self.db.run(query) - }) - - @api_key - @api_params(schema=UPDATE_INFRACTION_SCHEMA, validation_type=ValidationTypes.json) - def patch(self, data): - expand = data.get("expand") - update_collection = { - "id": data["id"] - } - - if "reason" in data: - update_collection["reason"] = data["reason"] - - if "active" in data: - update_collection["active"] = data["active"] - update_collection["closed"] = not data["active"] - - if "duration" in data: - duration_str = data["duration"] - if duration_str is None: - update_collection["expires_at"] = None - else: - try: - update_collection["expires_at"] = parse_duration(duration_str) - except ValueError: - return self.error( - ErrorCodes.incorrect_parameters, - "Invalid duration format." - ) - - query_update = self.db.query(self.table_name).update(update_collection) - result_update = self.db.run(query_update) - - if not result_update["replaced"]: - return jsonify({ - "success": False, - "error_message": "Unknown infraction / nothing was changed." - }) - - # return the updated infraction - query = self.db.query(self.table_name).get(data["id"]) \ - .merge(_merge_expand_users(self, expand)) \ - .merge(_merge_active_check()) \ - .without(*EXCLUDED_FIELDS).default(None) - infraction = self.db.run(query) - - return jsonify({ - "infraction": infraction, - "success": True - }) - - -class InfractionById(APIView, DBMixin): - path = "/bot/infractions/id/<string:infraction_id>" - name = "bot.infractions.id" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_ACTIVE_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, infraction_id): - params = params or {} - expand = parse_bool(params.get("expand"), default=False) - - query = self.db.query(self.table_name).get(infraction_id) \ - .merge(_merge_expand_users(self, expand)) \ - .merge(_merge_active_check()) \ - .without(*EXCLUDED_FIELDS).default(None) - return jsonify({ - "infraction": self.db.run(query) - }) - - -class ListInfractionsByUserView(APIView, DBMixin): - path = "/bot/infractions/user/<string:user_id>" - name = "bot.infractions.user" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, user_id): - return _infraction_list_filtered(self, params, { - "user_id": user_id - }) - - -class ListInfractionsByTypeView(APIView, DBMixin): - path = "/bot/infractions/type/<string:type>" - name = "bot.infractions.type" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, type): - return _infraction_list_filtered(self, params, { - "type": type - }) - - -class ListInfractionsByTypeAndUserView(APIView, DBMixin): - path = "/bot/infractions/user/<string:user_id>/<string:type>" - name = "bot.infractions.user.type" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, user_id, type): - return _infraction_list_filtered(self, params, { - "user_id": user_id, - "type": type - }) - - -class CurrentInfractionByTypeAndUserView(APIView, DBMixin): - path = "/bot/infractions/user/<string:user_id>/<string:infraction_type>/current" - name = "bot.infractions.user.type.current" - table_name = "bot_infractions" - - @api_key - @api_params(schema=GET_ACTIVE_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params, user_id, infraction_type): - params = params or {} - expand = parse_bool(params.get("expand"), default=False) - - query_filter = { - "user_id": user_id, - "type": infraction_type - } - query = _merged_query(self, expand, query_filter).filter({ - "active": True - }).order_by(rethinkdb.desc("data")).limit(1).nth(0).default(None) - return jsonify({ - "infraction": self.db.run(query) - }) - - -class ImportRowboatInfractionsView(APIView, DBMixin): - path = "/bot/infractions/import" - name = "bot.infractions.import" - table_name = "bot_infractions" - - @api_key - @api_params(schema=IMPORT_INFRACTIONS_SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - # keep track of the un-bans, to apply after the import is complete. - unbans = [] - infractions = [] - - # previously imported infractions - imported_infractions = self.db.run( - self.db.query(self.table_name).filter( - lambda row: row.has_fields("legacy_rowboat_id") - ).fold([], lambda acc, row: acc.append(row["legacy_rowboat_id"])).coerce_to("array") - ) - - for rowboat_infraction_data in data: - legacy_rowboat_id = rowboat_infraction_data["id"] - if legacy_rowboat_id in imported_infractions: - continue - infraction_type = rowboat_infraction_data["type"]["name"] - if infraction_type == "unban": - unbans.append(rowboat_infraction_data) - continue - # adjust infraction types - if infraction_type == "tempmute": - infraction_type = "mute" - if infraction_type == "tempban": - infraction_type = "ban" - if infraction_type not in INFRACTION_TYPES: - # unknown infraction type - continue - active = rowboat_infraction_data["active"] - reason = rowboat_infraction_data["reason"] or "<No reason>" - user_id = rowboat_infraction_data["user"]["id"] - actor_id = rowboat_infraction_data["actor"]["id"] - inserted_at_str = rowboat_infraction_data["created_at"] - try: - inserted_at = parse_rfc1123(inserted_at_str) - except ValueError: - continue - expires_at_str = rowboat_infraction_data["expires_at"] - if expires_at_str is not None: - try: - expires_at = parse_rfc1123(expires_at_str) - except ValueError: - continue - else: - expires_at = None - infractions.append({ - "legacy_rowboat_id": legacy_rowboat_id, - "active": active, - "reason": reason, - "user_id": user_id, - "actor_id": actor_id, - "inserted_at": inserted_at, - "expires_at": expires_at, - "type": infraction_type - }) - - insertion_query = self.db.query(self.table_name).insert(infractions) - inserted_count = self.db.run(insertion_query)["inserted"] - - # apply unbans - for unban_data in unbans: - inserted_at_str = unban_data["created_at"] - user_id = unban_data["user"]["id"] - try: - inserted_at = parse_rfc1123(inserted_at_str) - except ValueError: - continue - self.db.run( - self.db.query(self.table_name).filter( - lambda row: (row["user_id"].eq(user_id)) & - (row["type"].eq("ban")) & - (row["inserted_at"] < inserted_at) - ).pluck("id").merge(lambda row: { - "active": False - }).coerce_to("array").for_each(lambda doc: self.db.query(self.table_name).get(doc["id"]).update(doc)) - ) - - return jsonify({ - "success": True, - "inserted_count": inserted_count - }) - - -def _infraction_list_filtered(view, params=None, query_filter=None): - params = params or {} - query_filter = query_filter or {} - active = parse_bool(params.get("active")) - expand = parse_bool(params.get("expand"), default=False) - search = params.get("search") - - if active is not None: - query_filter["active"] = active - - query = _merged_query(view, expand, query_filter) - - if search is not None: - query = query.filter( - lambda row: rethinkdb.branch( - row["reason"].eq(None), - False, - row["reason"].match(search) - ) - ) - - query = query.order_by(*INFRACTION_ORDER) - - return jsonify(view.db.run(query.coerce_to("array"))) - - -def _merged_query(view, expand, query_filter): - return view.db.query(view.table_name).merge(_merge_active_check()).filter(query_filter) \ - .merge(_merge_expand_users(view, expand)).without(*EXCLUDED_FIELDS) - - -def _merge_active_check(): - # Checks if the "closed" field has been set to true (manual infraction removal). - # If not, the "active" field is set to whether the infraction has expired. - def _merge(row): - return { - "active": - rethinkdb.branch( - _is_timed_infraction(row["type"]), - rethinkdb.branch( - (row["closed"].default(False).eq(True)) | (row["active"].default(True).eq(False)), - False, - rethinkdb.branch( - row["expires_at"].eq(None), - True, - row["expires_at"] > rethinkdb.now() - ) - ), - False - ), - "closed": row["closed"].default(False), - "_timed": _is_timed_infraction(row["type"]) - } - - return _merge - - -def _merge_expand_users(view, expand): - def _do_expand(user_id): - if not user_id: - return None - # Expands the user information, if it is in the database. - - if expand: - return view.db.query("users").get(user_id).default({ - "user_id": user_id - }) - - return { - "user_id": user_id - } - - def _merge(row): - return { - "user": _do_expand(row["user_id"].default(None)), - "actor": _do_expand(row["actor_id"].default(None)) - } - - return _merge - - -def _is_timed_infraction(type_var): - # this method generates an ReQL expression to check if the given type - # is a "timed infraction" (i.e it can expire or be permanent) - - timed_infractions = filter(lambda key: INFRACTION_TYPES[key].timed_infraction, INFRACTION_TYPES.keys()) - expr = rethinkdb.expr(False) - for infra_type in timed_infractions: - expr = expr | type_var.eq(infra_type) - return expr - - -def parse_rfc1123(time_str): - return datetime.datetime.strptime(time_str, RFC1123_FORMAT).replace(tzinfo=datetime.timezone.utc) - - -def parse_bool(a_string, default=None): - # Not present, null or any: returns default (defaults to None) - # false, no, or 0: returns False - # anything else: True - if a_string is None or a_string == "null" or a_string == "any": - return default - if a_string.lower() == "false" or a_string.lower() == "no" or a_string == "0": - return False - return True diff --git a/pysite/views/api/bot/off_topic_names.py b/pysite/views/api/bot/off_topic_names.py deleted file mode 100644 index 1c75428e..00000000 --- a/pysite/views/api/bot/off_topic_names.py +++ /dev/null @@ -1,108 +0,0 @@ -import random - -from flask import jsonify, request -from schema import And, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - - -OFF_TOPIC_NAME = And( - str, - len, - lambda name: all(c.isalnum() or c == '-' for c in name), - str.islower, - lambda name: len(name) <= 96, - error=( - "The channel name must be a non-blank string consisting only of" - " lowercase regular characters and '-' with a maximum length of 96" - ) -) - -DELETE_SCHEMA = Schema({ - 'name': OFF_TOPIC_NAME -}) - -POST_SCHEMA = Schema({ - 'name': OFF_TOPIC_NAME -}) - - -class OffTopicNamesView(APIView, DBMixin): - path = "/bot/off-topic-names" - name = "bot.off_topic_names" - table_name = "off_topic_names" - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.params) - def delete(self, params): - """ - Removes a single off-topic name from the database. - Returns the result of the deletion call. - - API key must be provided as header. - Name to delete must be provided as the `name` query argument. - """ - - result = self.db.delete( - self.table_name, - params['name'], - return_changes=True - ) - - return jsonify(result) - - @api_key - def get(self): - """ - Fetch all known off-topic channel names from the database. - Returns a list of strings, the strings being the off-topic names. - - If the query argument `random_items` is provided (a non-negative integer), - then this view will return `random_items` random names from the database - instead of returning all items at once. - - API key must be provided as header. - """ - - names = [ - entry['name'] for entry in self.db.get_all(self.table_name) - ] - - if 'random_items' in request.args: - random_count = request.args['random_items'] - if not random_count.isdigit(): - response = {'message': "`random_items` must be a valid integer"} - return jsonify(response), 400 - - samples = random.sample(names, int(random_count)) - return jsonify(samples) - - return jsonify(names) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.params) - def post(self, data): - """ - Add a new off-topic channel name to the database. - Expects the new channel's name as the `name` argument. - The name must consist only of alphanumeric characters or minus signs, - and must not be empty or exceed 96 characters. - - Data must be provided as params. - API key must be provided as header. - """ - - if self.db.get(self.table_name, data['name']) is not None: - response = { - 'message': "An entry with the given name already exists" - } - return jsonify(response), 400 - - self.db.insert( - self.table_name, - {'name': data['name']} - ) - return jsonify({'message': 'ok'}) diff --git a/pysite/views/api/bot/settings.py b/pysite/views/api/bot/settings.py deleted file mode 100644 index a633a68a..00000000 --- a/pysite/views/api/bot/settings.py +++ /dev/null @@ -1,56 +0,0 @@ -from flask import jsonify -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -# todo: type safety -SETTINGS_KEYS_DEFAULTS = { - "defcon_enabled": False, - "defcon_days": 1 -} - -GET_SCHEMA = Schema({ - Optional("keys"): str -}) - - -def settings_schema(): - schema_dict = {Optional(key): type(SETTINGS_KEYS_DEFAULTS[key]) for key in SETTINGS_KEYS_DEFAULTS.keys()} - return Schema(schema_dict) - - -class ServerSettingsView(APIView, DBMixin): - path = "/bot/settings" - name = "bot.settings" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - keys_raw = None - if params: - keys_raw = params.get("keys") - - keys = filter(lambda key: key in SETTINGS_KEYS_DEFAULTS, - keys_raw.split(",")) if keys_raw else SETTINGS_KEYS_DEFAULTS.keys() - - result = {key: (self.db.get("bot_settings", key) or {}).get("value") or SETTINGS_KEYS_DEFAULTS[key] for key in - keys} - return jsonify(result) - - @api_key - @api_params(schema=settings_schema(), validation_type=ValidationTypes.json) - def put(self, json_data): - # update in database - - for key, value in json_data.items(): - self.db.insert("bot_settings", { - "key": key, - "value": value - }, conflict="update") - - return jsonify({ - "success": True - }) diff --git a/pysite/views/api/bot/snake_cog/__init__.py b/pysite/views/api/bot/snake_cog/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/api/bot/snake_cog/__init__.py +++ /dev/null diff --git a/pysite/views/api/bot/snake_cog/snake_facts.py b/pysite/views/api/bot/snake_cog/snake_facts.py deleted file mode 100644 index 4e8c8a5d..00000000 --- a/pysite/views/api/bot/snake_cog/snake_facts.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from flask import jsonify - -from pysite.base_route import APIView -from pysite.decorators import api_key -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - - -class SnakeFactsView(APIView, DBMixin): - path = "/bot/snake_facts" - name = "bot.snake_facts" - table = "snake_facts" - - @api_key - def get(self): - """ - Returns a random fact from the snake_facts table. - - API key must be provided as header. - """ - - log.trace("Fetching a random fact from the snake_facts database") - question = self.db.sample(self.table, 1)[0]["fact"] - - return jsonify(question) diff --git a/pysite/views/api/bot/snake_cog/snake_idioms.py b/pysite/views/api/bot/snake_cog/snake_idioms.py deleted file mode 100644 index 9d879871..00000000 --- a/pysite/views/api/bot/snake_cog/snake_idioms.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from flask import jsonify - -from pysite.base_route import APIView -from pysite.decorators import api_key -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - - -class SnakeIdiomView(APIView, DBMixin): - path = "/bot/snake_idioms" - name = "bot.snake_idioms" - table = "snake_idioms" - - @api_key - def get(self): - """ - Returns a random idiom from the snake_idioms table. - - API key must be provided as header. - """ - - log.trace("Fetching a random idiom from the snake_idioms database") - question = self.db.sample(self.table, 1)[0]["idiom"] - - return jsonify(question) diff --git a/pysite/views/api/bot/snake_cog/snake_names.py b/pysite/views/api/bot/snake_cog/snake_names.py deleted file mode 100644 index d9e0c6b8..00000000 --- a/pysite/views/api/bot/snake_cog/snake_names.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging - -from flask import jsonify -from schema import Optional, Schema - - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - -GET_SCHEMA = Schema([ - { - Optional("get_all"): str - } -]) - - -class SnakeNamesView(APIView, DBMixin): - path = "/bot/snake_names" - name = "bot.snake_names" - table = "snake_names" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - """ - Returns all snake names random name from the snake_names table. - - API key must be provided as header. - """ - - get_all = None - - if params: - get_all = params[0].get("get_all") - - if get_all: - log.trace("Returning all snake names from the snake_names table") - snake_names = self.db.get_all(self.table) - - else: - log.trace("Fetching a single random snake name from the snake_names table") - snake_names = self.db.sample(self.table, 1)[0] - - return jsonify(snake_names) diff --git a/pysite/views/api/bot/snake_cog/snake_quiz.py b/pysite/views/api/bot/snake_cog/snake_quiz.py deleted file mode 100644 index 359077d7..00000000 --- a/pysite/views/api/bot/snake_cog/snake_quiz.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from flask import jsonify - -from pysite.base_route import APIView -from pysite.decorators import api_key -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - - -class SnakeQuizView(APIView, DBMixin): - path = "/bot/snake_quiz" - name = "bot.snake_quiz" - table = "snake_quiz" - - @api_key - def get(self): - """ - Returns a random question from the snake_quiz table. - - API key must be provided as header. - """ - - log.trace("Fetching a random question from the snake_quiz database") - question = self.db.sample(self.table, 1)[0] - - return jsonify(question) diff --git a/pysite/views/api/bot/snake_cog/special_snakes.py b/pysite/views/api/bot/snake_cog/special_snakes.py deleted file mode 100644 index 294c16c9..00000000 --- a/pysite/views/api/bot/snake_cog/special_snakes.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from flask import jsonify - -from pysite.base_route import APIView -from pysite.decorators import api_key -from pysite.mixins import DBMixin - -log = logging.getLogger(__name__) - - -class SpecialSnakesView(APIView, DBMixin): - path = "/bot/special_snakes" - name = "bot.special_snakes" - table = "special_snakes" - - @api_key - def get(self): - """ - Returns all special snake objects from the database - - API key must be provided as header. - """ - - log.trace("Returning all special snakes in the database") - snake_names = self.db.get_all(self.table) - - return jsonify(snake_names) diff --git a/pysite/views/api/bot/tags.py b/pysite/views/api/bot/tags.py deleted file mode 100644 index 4394c224..00000000 --- a/pysite/views/api/bot/tags.py +++ /dev/null @@ -1,107 +0,0 @@ -from flask import jsonify -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -GET_SCHEMA = Schema({ - Optional("tag_name"): str -}) - -POST_SCHEMA = Schema({ - "tag_name": str, - "tag_content": str -}) - -DELETE_SCHEMA = Schema({ - "tag_name": str -}) - - -class TagsView(APIView, DBMixin): - path = "/bot/tags" - name = "bot.tags" - table_name = "tags" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, params=None): - """ - Fetches tags from the database. - - - If tag_name is provided, it fetches - that specific tag. - - - If tag_category is provided, it fetches - all tags in that category. - - - If nothing is provided, it will - fetch a list of all tag_names. - - Data must be provided as params. - API key must be provided as header. - """ - - tag_name = None - - if params: - tag_name = params.get("tag_name") - - if tag_name: - data = self.db.get(self.table_name, tag_name) or {} - else: - data = self.db.pluck(self.table_name, "tag_name") or [] - - return jsonify(data) - - @api_key - @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json) - def post(self, json_data): - """ - If the tag_name doesn't exist, this - saves a new tag in the database. - - If the tag_name already exists, - this will edit the existing tag. - - Data must be provided as JSON. - API key must be provided as header. - """ - - tag_name = json_data.get("tag_name") - tag_content = json_data.get("tag_content") - - self.db.insert( - self.table_name, - { - "tag_name": tag_name, - "tag_content": tag_content - }, - conflict="update" # If it exists, update it. - ) - - return jsonify({"success": True}) - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json) - def delete(self, data): - """ - Deletes a tag from the database. - - Data must be provided as JSON. - API key must be provided as header. - """ - - tag_name = data.get("tag_name") - tag_exists = self.db.get(self.table_name, tag_name) - - if tag_exists: - self.db.delete( - self.table_name, - tag_name - ) - return jsonify({"success": True}) - - return jsonify({"success": False}) diff --git a/pysite/views/api/bot/user.py b/pysite/views/api/bot/user.py deleted file mode 100644 index a3a0c7a8..00000000 --- a/pysite/views/api/bot/user.py +++ /dev/null @@ -1,166 +0,0 @@ -import logging - -import rethinkdb -from flask import jsonify, request -from schema import Optional, Schema - -from pysite.base_route import APIView -from pysite.constants import ErrorCodes, ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - -SCHEMA = Schema([ - { - "avatar": str, - "discriminator": str, - "roles": [str], - "user_id": str, - "username": str - } -]) - -GET_SCHEMA = Schema([ - { - "user_id": str - } -]) - -DELETE_SCHEMA = Schema([ - { - "user_id": str, - - Optional("avatar"): str, - Optional("discriminator"): str, - Optional("roles"): [str], - Optional("username"): str - } -]) - -BANNABLE_STATES = ("preparing", "running") - - -class UserView(APIView, DBMixin): - path = "/bot/users" - name = "bot.users" - - chunks_table = "member_chunks" - infractions_table = "code_jam_infractions" - jams_table = "code_jams" - oauth_table_name = "oauth_data" - participants_table = "code_jam_participants" - responses_table = "code_jam_responses" - table_name = "users" - teams_table = "code_jam_teams" - - @api_key - @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params) - def get(self, data): - logging.getLogger(__name__).debug(f"Size of request: {len(request.data)} bytes") - - if not data: - return self.error(ErrorCodes.bad_data_format, "No user IDs supplied") - - data = [x["user_id"] for x in data] - - result = self.db.run( - self.db.query(self.table_name) - .filter(lambda document: rethinkdb.expr(data).contains(document["user_id"])), - coerce=list - ) - - return jsonify({"data": result}) # pragma: no cover - - @api_key - @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - logging.getLogger(__name__).debug(f"Size of request: {len(request.data)} bytes") - - if not data: - return self.error(ErrorCodes.bad_data_format, "No users supplied") - - self.db.insert(self.chunks_table, {"chunk": data}) - - return jsonify({"success": True}) # pragma: no cover - - @api_key - @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) - def put(self, data): - changes = self.db.insert( - self.table_name, *data, - conflict="update" - ) - - return jsonify(changes) # pragma: no cover - - @api_key - @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json) - def delete(self, data): - user_ids = [user["user_id"] for user in data] - - changes = {} - - # changes = self.db.run( - # self.db.query(self.table_name) - # .get_all(*user_ids) - # .delete() - # ) - - oauth_deletions = self.db.run( - self.db.query(self.oauth_table_name) - .get_all(*user_ids, index="snowflake") - .delete() - ).get("deleted", 0) - - profile_deletions = self.db.run( - 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 - - teams = self.db.run( - self.db.query(self.teams_table).filter(lambda row: row["members"].contains(user_id)), - coerce=list - ) - - for team in teams: - team["members"].remove(user_id) - - self.db.insert(self.teams_table, team, conflict="replace", durability="soft") - - self.db.sync(self.teams_table) - - 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/api/bot/user_complete.py b/pysite/views/api/bot/user_complete.py deleted file mode 100644 index 877eee34..00000000 --- a/pysite/views/api/bot/user_complete.py +++ /dev/null @@ -1,143 +0,0 @@ -import logging - -from flask import jsonify, request - -from pysite.base_route import APIView -from pysite.constants import ErrorCodes, ValidationTypes -from pysite.decorators import api_key, api_params -from pysite.mixins import DBMixin - - -BANNABLE_STATES = ("preparing", "running") - -log = logging.getLogger(__name__) - - -class UserView(APIView, DBMixin): - path = "/bot/users/complete" - name = "bot.users.complete" - - chunks_table = "member_chunks" - infractions_table = "code_jam_infractions" - jams_table = "code_jams" - oauth_table_name = "oauth_data" - participants_table = "code_jam_participants" - responses_table = "code_jam_responses" - table_name = "users" - teams_table = "code_jam_teams" - - @api_key - @api_params(validation_type=ValidationTypes.none) - def post(self, _): - log.debug(f"Size of request: {len(request.data)} bytes") - - documents = self.db.get_all(self.chunks_table) - chunks = [] - - for doc in documents: - log.info(f"Got member chunk with {len(doc['chunk'])} users") - chunks.append(doc["chunk"]) - - self.db.delete(self.chunks_table, doc["id"], durability="soft") - self.db.sync(self.chunks_table) - - log.info(f"Got {len(chunks)} member chunks") - - data = [] - - for chunk in chunks: - data += chunk - - log.info(f"Got {len(data)} members") - - if not data: - return self.error(ErrorCodes.bad_data_format, "No users supplied") - - deletions = 0 - oauth_deletions = 0 - profile_deletions = 0 - response_deletions = 0 - bans = 0 - - user_ids = [user["user_id"] for user in data] - - all_users = self.db.run(self.db.query(self.table_name), coerce=list) - - for user in all_users: - if user["user_id"] not in user_ids: - self.db.delete(self.table_name, user["user_id"], durability="soft") - deletions += 1 - - all_oauth_data = self.db.run(self.db.query(self.oauth_table_name), coerce=list) - - for item in all_oauth_data: - if item["snowflake"] not in user_ids: - 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 - - teams = self.db.run( - self.db.query(self.teams_table).filter(lambda row: row["members"].contains(user_id)), - coerce=list - ) - - for team in teams: - team["members"].remove(user_id) - - self.db.insert(self.teams_table, team, conflict="replace", durability="soft") - - 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 - - changes = self.db.insert( - self.table_name, *data, - conflict="update", - 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) - self.db.sync(self.teams_table) - - 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 diff --git a/pysite/views/api/error_view.py b/pysite/views/api/error_view.py deleted file mode 100644 index 89b4d6ad..00000000 --- a/pysite/views/api/error_view.py +++ /dev/null @@ -1,40 +0,0 @@ -from flask import jsonify -from werkzeug.exceptions import HTTPException - -from pysite.base_route import ErrorView - - -class APIErrorView(ErrorView): - name = "api.error_all" - error_code = range(400, 600) - register_on_app = False - - def __init__(self): - - # Direct errors for all methods at self.return_error - methods = [ - 'get', 'post', 'put', - 'delete', 'patch', 'connect', - 'options', 'trace' - ] - - for method in methods: - setattr(self, method, self.return_error) - - def return_error(self, error: HTTPException): - """ - Return a basic JSON object representing the HTTP error, - as well as propagating its status code - """ - - message = str(error) - code = 500 - - if isinstance(error, HTTPException): - message = error.description - code = error.code - - return jsonify({ - "error_code": -1, - "error_message": message - }), code diff --git a/pysite/views/api/healthcheck.py b/pysite/views/api/healthcheck.py deleted file mode 100644 index c873d674..00000000 --- a/pysite/views/api/healthcheck.py +++ /dev/null @@ -1,11 +0,0 @@ -from flask import jsonify - -from pysite.base_route import APIView - - -class HealthCheckView(APIView): - path = "/healthcheck" - name = "api.healthcheck" - - def get(self): - return jsonify({"status": "ok"}) diff --git a/pysite/views/api/index.py b/pysite/views/api/index.py deleted file mode 100644 index 5111162c..00000000 --- a/pysite/views/api/index.py +++ /dev/null @@ -1,10 +0,0 @@ -from pysite.base_route import APIView -from pysite.constants import ErrorCodes - - -class IndexView(APIView): - path = "/" - name = "api.index" - - def get(self): - return self.error(ErrorCodes.unknown_route) diff --git a/pysite/views/api/robots_txt.py b/pysite/views/api/robots_txt.py deleted file mode 100644 index d4406d54..00000000 --- a/pysite/views/api/robots_txt.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class RobotsTXT(RouteView): - path = "/robots.txt" - name = "robots_txt" - - def get(self): - return Response( - self.render( - "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True), rules={"*": ["/"]} - ), content_type="text/plain" - ) diff --git a/pysite/views/api/sitemap_xml.py b/pysite/views/api/sitemap_xml.py deleted file mode 100644 index 26a786b0..00000000 --- a/pysite/views/api/sitemap_xml.py +++ /dev/null @@ -1,11 +0,0 @@ -from flask import Response - -from pysite.base_route import RouteView - - -class SitemapXML(RouteView): - path = "/sitemap.xml" - name = "sitemap_xml" - - def get(self): - return Response(self.render("sitemap.xml", urls=[]), content_type="application/xml") diff --git a/pysite/views/error_handlers/http_4xx.py b/pysite/views/error_handlers/http_4xx.py deleted file mode 100644 index 731204f9..00000000 --- a/pysite/views/error_handlers/http_4xx.py +++ /dev/null @@ -1,31 +0,0 @@ -from flask import request -from werkzeug.exceptions import HTTPException - -from pysite.base_route import ErrorView -from pysite.constants import ERROR_DESCRIPTIONS - - -class Error400View(ErrorView): - name = "errors.4xx" - error_code = range(400, 430) - - def __init__(self): - # Direct errors for all methods at self.return_error - methods = [ - 'get', 'post', 'put', - 'delete', 'patch', 'connect', - 'options', 'trace' - ] - - for method in methods: - setattr(self, method, self.error) - - def error(self, error: HTTPException): - error_desc = ERROR_DESCRIPTIONS.get(error.code, "We're not really sure what happened there, please try again.") - - return self.render( - "errors/error.html", code=error.code, req=request, error_title=error_desc, - error_message=f"{error_desc} If you believe we have made a mistake, please " - "<a href='https://gitlab.com/python-discord/projects/site/issues'>" - "open an issue on our GitLab</a>." - ), error.code diff --git a/pysite/views/error_handlers/http_5xx.py b/pysite/views/error_handlers/http_5xx.py deleted file mode 100644 index 489eb5e5..00000000 --- a/pysite/views/error_handlers/http_5xx.py +++ /dev/null @@ -1,41 +0,0 @@ -from flask import request -from werkzeug.exceptions import HTTPException, InternalServerError - -from pysite.base_route import ErrorView -from pysite.constants import ERROR_DESCRIPTIONS - - -class Error500View(ErrorView): - name = "errors.5xx" - error_code = range(500, 600) - - def __init__(self): - - # Direct errors for all methods at self.return_error - methods = [ - 'get', 'post', 'put', - 'delete', 'patch', 'connect', - 'options', 'trace' - ] - - for method in methods: - setattr(self, method, self.error) - - def error(self, error: HTTPException): - - # We were sometimes recieving errors from RethinkDB, which were not originating from Werkzeug. - # To fix this, this section checks whether they have a code (which werkzeug adds) and if not - # change the error to a Werkzeug InternalServerError. - - if not hasattr(error, "code"): - error = InternalServerError() - - error_desc = ERROR_DESCRIPTIONS.get(error.code, "We're not really sure what happened there, please try again.") - - return self.render( - "errors/error.html", code=error.code, req=request, error_title=error_desc, - error_message="An error occurred while processing this request, please try " - "again later. If you believe we have made a mistake, please " - "<a href='https://gitlab.com/python-discord/projects/site/issues'>file an issue on our" - " GitLab</a>." - ), error.code diff --git a/pysite/views/main/__init__.py b/pysite/views/main/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/__init__.py +++ /dev/null diff --git a/pysite/views/main/abort.py b/pysite/views/main/abort.py deleted file mode 100644 index ecfe8f91..00000000 --- a/pysite/views/main/abort.py +++ /dev/null @@ -1,11 +0,0 @@ -from werkzeug.exceptions import InternalServerError - -from pysite.base_route import RouteView - - -class EasterEgg500(RouteView): - path = "/500" - name = "500" - - def get(self): - raise InternalServerError diff --git a/pysite/views/main/about/__init__.py b/pysite/views/main/about/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/about/__init__.py +++ /dev/null diff --git a/pysite/views/main/about/channels.py b/pysite/views/main/about/channels.py deleted file mode 100644 index 2e5496f9..00000000 --- a/pysite/views/main/about/channels.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class ChannelsView(TemplateView): - path = "/about/channels" - name = "about.channels" - template = "main/about/channels.html" diff --git a/pysite/views/main/about/index.py b/pysite/views/main/about/index.py deleted file mode 100644 index 6f5ef1c8..00000000 --- a/pysite/views/main/about/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class IndexView(TemplateView): - path = "/about/" - name = "about.index" - template = "main/about/index.html" diff --git a/pysite/views/main/about/partners.py b/pysite/views/main/about/partners.py deleted file mode 100644 index 4fe321a5..00000000 --- a/pysite/views/main/about/partners.py +++ /dev/null @@ -1,19 +0,0 @@ -import json -from logging import getLogger - -from pysite.base_route import RouteView - -try: - with open("static/partners.json") as fh: - partners = json.load(fh) -except Exception: - getLogger("Partners").exception("Failed to load partners.json") - categories = None - - -class PartnersView(RouteView): - path = "/about/partners" - name = "about.partners" - - def get(self): - return self.render("main/about/partners.html", partners=partners) diff --git a/pysite/views/main/about/privacy.py b/pysite/views/main/about/privacy.py deleted file mode 100644 index a08aa22b..00000000 --- a/pysite/views/main/about/privacy.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class PrivacyView(TemplateView): - path = "/about/privacy" - name = "about.privacy" - template = "main/about/privacy.html" diff --git a/pysite/views/main/about/rules.py b/pysite/views/main/about/rules.py deleted file mode 100644 index a40110a1..00000000 --- a/pysite/views/main/about/rules.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class RulesView(TemplateView): - path = "/about/rules" - name = "about.rules" - template = "main/about/rules.html" diff --git a/pysite/views/main/auth/__init__.py b/pysite/views/main/auth/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/auth/__init__.py +++ /dev/null diff --git a/pysite/views/main/auth/done.py b/pysite/views/main/auth/done.py deleted file mode 100644 index 6e892906..00000000 --- a/pysite/views/main/auth/done.py +++ /dev/null @@ -1,18 +0,0 @@ -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/bot/cleanlog.py b/pysite/views/main/bot/cleanlog.py deleted file mode 100644 index 9c719b3e..00000000 --- a/pysite/views/main/bot/cleanlog.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES, DEVELOPERS_ROLE, ROLE_COLORS -from pysite.decorators import require_roles -from pysite.mixins import DBMixin, OAuthMixin - -log = logging.getLogger(__name__) - - -class CleanLogView(RouteView, DBMixin, OAuthMixin): - path = "/bot/clean_logs/<log_id>" - name = "bot.clean_logs" - - table_name = "clean_logs" - template = "main/bot/clean_logs.html" - - @require_roles(ALL_STAFF_ROLES) - def get(self, log_id): - """ - Get the requested clean log and spit it out - in a beautiful template. - """ - - data = self.db.get(self.table_name, log_id) - - if data is None: - return "ID could not be found in the database", 404 - - messages = data["log_data"] - - for message in messages: - message['color'] = ROLE_COLORS.get(message['role_id'], ROLE_COLORS[DEVELOPERS_ROLE]) - - return self.render(self.template, messages=messages) diff --git a/pysite/views/main/error.py b/pysite/views/main/error.py deleted file mode 100644 index 07286eb4..00000000 --- a/pysite/views/main/error.py +++ /dev/null @@ -1,14 +0,0 @@ -from flask import abort - -from pysite.base_route import RouteView - - -class ErrorView(RouteView): - path = "/error/<int:code>" - name = "error" - - def get(self, code): - try: - return abort(code) - except LookupError: - return abort(500) diff --git a/pysite/views/main/index.py b/pysite/views/main/index.py deleted file mode 100644 index 874961bb..00000000 --- a/pysite/views/main/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class IndexView(TemplateView): - path = "/" - name = "index" - template = "main/index.html" diff --git a/pysite/views/main/info/__init__.py b/pysite/views/main/info/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/info/__init__.py +++ /dev/null diff --git a/pysite/views/main/info/faq.py b/pysite/views/main/info/faq.py deleted file mode 100644 index 8878e180..00000000 --- a/pysite/views/main/info/faq.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class IndexView(TemplateView): - path = "/info/faq" - name = "info.faq" - template = "main/info/faq.html" diff --git a/pysite/views/main/info/help.py b/pysite/views/main/info/help.py deleted file mode 100644 index 6a82a9ed..00000000 --- a/pysite/views/main/info/help.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class HelpView(TemplateView): - path = "/info/help" - name = "info.help" - template = "main/info/help.html" diff --git a/pysite/views/main/info/index.py b/pysite/views/main/info/index.py deleted file mode 100644 index 97678ee4..00000000 --- a/pysite/views/main/info/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class IndexView(TemplateView): - path = "/info/" - name = "info.index" - template = "main/info/index.html" diff --git a/pysite/views/main/info/jams.py b/pysite/views/main/info/jams.py deleted file mode 100644 index b654ec1d..00000000 --- a/pysite/views/main/info/jams.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import RedirectView - - -class JamsView(RedirectView): - path = "/info/jams" - name = "info.jams" - page = "main.jams.index" diff --git a/pysite/views/main/info/resources.py b/pysite/views/main/info/resources.py deleted file mode 100644 index 541b9ba1..00000000 --- a/pysite/views/main/info/resources.py +++ /dev/null @@ -1,58 +0,0 @@ -import json -from logging import getLogger - -from pysite.base_route import RouteView - -ICON_STYLES = { - "branding": "fab", - "regular": "far", - "solid": "fas", - "light": "fal" -} - -logger = getLogger("Resources") - -try: - with open("static/resources.json") as fh: - categories = json.load(fh) - - for category, items in categories.items(): - to_remove = [] - - for name, resource in items["resources"].items(): - for url_obj in resource["urls"]: - icon = url_obj["icon"].lower() - - if "/" not in icon: - to_remove.append(name) - logger.error( - f"Resource {name} in category {category} has an invalid icon. Icons should be of the" - f"form `style/name`." - ) - continue - - style, icon_name = icon.split("/") - - if style not in ICON_STYLES: - to_remove.append(name) - logger.error( - f"Resource {name} in category {category} has an invalid icon style. Icon style must " - f"be one of {', '.join(ICON_STYLES.keys())}." - ) - continue - - url_obj["classes"] = f"{ICON_STYLES[style]} fa-{icon_name}" - - for name in to_remove: - del items["resources"][name] -except Exception: - getLogger("Resources").exception("Failed to load resources.json") - categories = None - - -class ResourcesView(RouteView): - path = "/info/resources" - name = "info.resources" - - def get(self): - return self.render("main/info/resources.html", categories=categories) diff --git a/pysite/views/main/jams/__init__.py b/pysite/views/main/jams/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/jams/__init__.py +++ /dev/null diff --git a/pysite/views/main/jams/index.py b/pysite/views/main/jams/index.py deleted file mode 100644 index 0cd9a287..00000000 --- a/pysite/views/main/jams/index.py +++ /dev/null @@ -1,52 +0,0 @@ -import rethinkdb - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin - - -class JamsIndexView(RouteView, DBMixin): - path = "/jams" - name = "jams.index" - table_name = "code_jams" - - teams_table = "code_jam_teams" - - def get(self): - query = ( - self.db.query(self.table_name) - .filter(rethinkdb.row["state"] != "planning") - .merge( - lambda jam_obj: { - "teams": - self.db.query(self.teams_table) - .filter(lambda team_row: jam_obj["teams"].contains(team_row["id"])) - .pluck(["id"]) - .coerce_to("array") - } - ) - .order_by(rethinkdb.desc("number")) - .limit(5) - ) - - jams = self.db.run(query, coerce=list) - for jam in jams: - if "winning_team" in jam and jam["winning_team"]: - jam["winning_team"] = self.db.get(self.teams_table, jam["winning_team"]) - else: - jam["winning_team"] = None - pass - return self.render("main/jams/index.html", jams=jams, has_applied_to_jam=self.has_applied_to_jam) - - def get_jam_response(self, jam, user_id): - query = self.db.query("code_jam_responses").filter({"jam": jam, "snowflake": user_id}) - result = self.db.run(query, coerce=list) - - if result: - return result[0] - return None - - def has_applied_to_jam(self, jam): - # whether the user has applied to this jam - if not self.logged_in: - return False - return self.get_jam_response(jam, self.user_data["user_id"]) diff --git a/pysite/views/main/jams/info.py b/pysite/views/main/jams/info.py deleted file mode 100644 index fd4615e9..00000000 --- a/pysite/views/main/jams/info.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class JamsInfoView(TemplateView): - path = "/jams/info" - name = "jams.info" - template = "main/jams/info.html" diff --git a/pysite/views/main/jams/jam_team_list.py b/pysite/views/main/jams/jam_team_list.py deleted file mode 100644 index 452a073f..00000000 --- a/pysite/views/main/jams/jam_team_list.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging - -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin, OAuthMixin - -log = logging.getLogger(__name__) - - -class JamsTeamListView(RouteView, DBMixin, OAuthMixin): - path = "/jams/teams/<int:jam_id>" - name = "jams.jam_team_list" - - table_name = "code_jam_teams" - jams_table = "code_jams" - - def get(self, jam_id): - jam_obj = self.db.get(self.jams_table, jam_id) - if not jam_obj: - raise NotFound() - - # Get all the participants of this jam - # Note: the group function will return a dict with user_ids as keys, however each element will be an array - participants_query = self.db.query("users").get_all(*jam_obj["participants"], index="user_id").group("user_id") - participants = self.db.run(participants_query) - - # Get all the teams, leaving the team members as only an array of IDs - query = self.db.query(self.table_name).get_all(self.table_name, *jam_obj["teams"]).pluck( - ["id", "name", "members", "repo"]).coerce_to("array") - jam_obj["teams"] = self.db.run(query) - - # Populate each team's members using the previously queried participant list - for team in jam_obj["teams"]: - team["members"] = [participants[user_id][0] for user_id in team["members"]] - - return self.render( - "main/jams/team_list.html", - jam=jam_obj, - teams=jam_obj["teams"], - member_ids=self.member_ids - ) - - def member_ids(self, members): - return [member["user_id"] for member in members] diff --git a/pysite/views/main/jams/join.py b/pysite/views/main/jams/join.py deleted file mode 100644 index 4db59630..00000000 --- a/pysite/views/main/jams/join.py +++ /dev/null @@ -1,247 +0,0 @@ -import datetime -from email.utils import parseaddr - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import BotEventTypes, CHANNEL_JAM_LOGS -from pysite.decorators import csrf -from pysite.mixins import DBMixin, OAuthMixin, RMQMixin - - -class JamsJoinView(RouteView, DBMixin, OAuthMixin, RMQMixin): - path = "/jams/join/<int:jam>" - name = "jams.join" - - table_name = "code_jams" - forms_table = "code_jam_forms" - questions_table = "code_jam_questions" - responses_table = "code_jam_responses" - participants_table = "code_jam_participants" - infractions_table = "code_jam_infractions" - - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not self.user_data: - return self.redirect_login(jam=jam) - - infractions = self.get_infractions(self.user_data["user_id"]) - - for infraction in infractions: - if infraction["number"] == -1: # Indefinite ban - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - if infraction["number"]: # Got some jams left - if jam not in infraction["decremented_for"]: - # Make sure they haven't already tried to apply for this jam - infraction["number"] -= 1 - infraction["decremented_for"].append(jam) - - self.db.insert(self.infractions_table, infraction, conflict="replace") - - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - if jam in infraction["decremented_for"]: - # They already tried to apply for this jam - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - participant = self.db.get(self.participants_table, self.user_data["user_id"]) - - if not participant: - return redirect(url_for("main.jams.profile", form=jam)) - - if self.get_response(jam, self.user_data["user_id"]): - return self.render("main/jams/already.html", jam=jam_obj) - - form_obj = self.db.get(self.forms_table, jam) - questions = [] - - if form_obj: - for question in form_obj["questions"]: - questions.append(self.db.get(self.questions_table, question)) - - return self.render( - "main/jams/join.html", jam=jam_obj, form=form_obj, - questions=questions, question_ids=[q["id"] for q in questions] - ) - - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not self.user_data: - return self.redirect_login(jam=jam) - - infractions = self.get_infractions(self.user_data["user_id"]) - - for infraction in infractions: - if infraction["number"] == -1: # Indefinite ban - self.log_banned(infraction["number"], infraction["reason"]) - return self.render("main/jams/banned.html", infraction=infraction) - - if infraction["number"]: # Got some jams left - if jam not in infraction["decremented_for"]: - # Make sure they haven't already tried to apply for this jam - infraction["number"] -= 1 - infraction["decremented_for"].append(jam) - - self.db.insert(self.infractions_table, infraction, conflict="replace") - - self.log_banned(infraction["number"], infraction["reason"]) - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - if jam in infraction["decremented_for"]: - # They already tried to apply for this jam - self.log_banned(infraction["number"], infraction["reason"]) - return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj) - - participant = self.db.get(self.participants_table, self.user_data["user_id"]) - - if not participant: - return redirect(url_for("main.jams.profile")) - - if self.get_response(jam, self.user_data["user_id"]): - return self.render("main/jams/already.html", jam=jam_obj) - - form_obj = self.db.get(self.forms_table, jam) - - if not form_obj: - return NotFound() - - questions = [] - - for question in form_obj["questions"]: - questions.append(self.db.get(self.questions_table, question)) - - answers = [] - - for question in questions: - value = request.form.get(question["id"]) - answer = {"question": question["id"]} - - if not question["optional"] and value is None: - return BadRequest() - - if question["type"] == "checkbox": - if value == "on": - answer["value"] = True - elif not question["optional"]: - return BadRequest() - else: - answer["value"] = False - - elif question["type"] == "email": - if value: - address = parseaddr(value) - - if address == ("", ""): - return BadRequest() - - answer["value"] = value - - elif question["type"] in ["number", "range", "slider"]: - if value is not None: - value = int(value) - - if value > int(question["data"]["max"]) or value < int(question["data"]["min"]): - return BadRequest() - - answer["value"] = value - - elif question["type"] == "radio": - if value: - if value not in question["data"]["options"]: - return BadRequest() - - answer["value"] = value - - elif question["type"] in ["text", "textarea"]: - answer["value"] = value - - answers.append(answer) - - user_id = self.user_data["user_id"] - - response = { - "snowflake": user_id, - "jam": jam, - "approved": False, - "answers": answers - } - - self.db.insert(self.responses_table, response) - self.log_success() - - return self.render("main/jams/thanks.html", jam=jam_obj) - - def get_response(self, jam, user_id): - query = self.db.query(self.responses_table).filter({"jam": jam, "snowflake": user_id}) - result = self.db.run(query, coerce=list) - - if result: - return result[0] - return None - - def get_infractions(self, user_id): - query = self.db.query(self.infractions_table).filter({"participant": user_id}) - return self.db.run(query, coerce=list) - - def log_banned(self, number, reason): - user_data = self.user_data - - user_id = user_data["user_id"] - username = user_data["username"] - discriminator = user_data["discriminator"] - - message = f"Failed code jam signup from banned user: {user_id} ({username}#{discriminator})\n\n" - - if number == -1: - message += f"This user has been banned indefinitely. Reason: '{reason}'" - elif number < 1: - message += f"This application has expired the infraction. Reason: '{reason}'" - else: - message += f"This user has {number} more applications left before they're unbanned. Reason: '{reason}'" - - self.rmq_bot_event( - BotEventTypes.mod_log, - { - "level": "warning", "title": "Code Jams: Applications", - "message": message - } - ) - - def log_success(self): - user_data = self.user_data - - user_id = user_data["user_id"] - username = user_data["username"] - discriminator = user_data["discriminator"] - - self.rmq_bot_event( - BotEventTypes.mod_log, - { - "level": "info", "title": "Code Jams: Applications", - "message": f"Successful code jam signup from user: {user_id} " - f"({username}#{discriminator})" - } - ) - - self.rmq_bot_event( - BotEventTypes.send_embed, - { - "target": CHANNEL_JAM_LOGS, - "title": "Code Jams: Applications", - "description": f"Successful code jam signup from user: {user_id} " - f"({username}#{discriminator})", - "colour": 0x2ecc71, # Green from d.py - "timestamp": datetime.datetime.now().isoformat() - } - ) diff --git a/pysite/views/main/jams/profile.py b/pysite/views/main/jams/profile.py deleted file mode 100644 index e918c135..00000000 --- a/pysite/views/main/jams/profile.py +++ /dev/null @@ -1,71 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest - -from pysite.base_route import RouteView -from pysite.decorators import csrf -from pysite.mixins import DBMixin, OAuthMixin - - -class JamsProfileView(RouteView, DBMixin, OAuthMixin): - path = "/jams/profile" - name = "jams.profile" - - table_name = "code_jam_participants" - - def get(self): - if not self.user_data: - 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") - - if form: - try: - form = int(form) - except ValueError: - pass # Someone trying to have some fun I guess - - return self.render( - "main/jams/profile.html", participant=participant, form=form, existing=existing - ) - - @csrf - def post(self): - if not self.user_data: - return self.redirect_login() - - participant = self.db.get(self.table_name, self.user_data["user_id"]) - - if not participant: - participant = {"id": self.user_data["user_id"]} - - gitlab_username = request.form.get("gitlab_username") - timezone = request.form.get("timezone") - - if not gitlab_username or not timezone: - return BadRequest() - - participant["gitlab_username"] = gitlab_username - participant["timezone"] = timezone - - self.db.insert(self.table_name, participant, conflict="replace") - - form = request.args.get("form") - - if form: - try: - form = int(form) - except ValueError: - pass # Someone trying to have some fun I guess - else: - return redirect(url_for("main.jams.join", jam=form)) - - return self.render( - "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 deleted file mode 100644 index 277426b5..00000000 --- a/pysite/views/main/jams/retract.py +++ /dev/null @@ -1,83 +0,0 @@ -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/jams/team_edit_repo.py b/pysite/views/main/jams/team_edit_repo.py deleted file mode 100644 index 03e752bc..00000000 --- a/pysite/views/main/jams/team_edit_repo.py +++ /dev/null @@ -1,151 +0,0 @@ -import logging -import re -from urllib.parse import quote - -import requests -from flask import jsonify, request -from rethinkdb import ReqlNonExistenceError -from urllib3.util import parse_url -from werkzeug.exceptions import NotFound, Unauthorized - -from pysite.base_route import APIView -from pysite.constants import ErrorCodes, GITLAB_ACCESS_TOKEN -from pysite.decorators import csrf -from pysite.mixins import DBMixin, OAuthMixin - -log = logging.getLogger(__name__) - - -class JamsTeamEditRepo(APIView, DBMixin, OAuthMixin): - path = "/jams/teams/<string:team_id>/edit_repo" - name = "jams.team.edit_repo" - - table_name = "code_jam_teams" - jams_table = "code_jams" - - gitlab_projects_api_endpoint = "https://gitlab.com/api/v4/projects/{0}" - - @csrf - def post(self, team_id): - if not self.user_data: - return self.redirect_login() - - try: - query = self.db.query(self.table_name).get(team_id).merge( - lambda team: { - "jam": self.db.query("code_jams").get(team["jam"]) - } - ) - - team = self.db.run(query) - except ReqlNonExistenceError: - log.exception("Failed RethinkDB query") - raise NotFound() - - # Only team members can use this route - if not self.user_data["user_id"] in team["members"]: - raise Unauthorized() - - repo_url = request.form.get("repo_url").strip() - - # Check if repo is a valid GitLab repo URI - url = parse_url(repo_url) - - if url.host != "gitlab.com" or url.path is None: - return self.error( - ErrorCodes.incorrect_parameters, - "Not a GitLab repository." - ) - - project_path = url.path.strip("/") # /user/repository/ --> user/repository - if len(project_path.split("/")) < 2: - return self.error( - ErrorCodes.incorrect_parameters, - "Not a valid repository." - ) - - word_regex = re.compile("^[\-\.\w]+$") # Alphanumerical, underscores, periods, and dashes - for segment in project_path.split("/"): - if not word_regex.fullmatch(segment): - return self.error( - ErrorCodes.incorrect_parameters, - "Not a valid repository." - ) - - project_path_encoded = quote(project_path, safe='') # Replaces / with %2F, etc. - - # If validation returns something else than True, abort - validation = self.validate_project(team, project_path_encoded) - if validation is not True: - return validation - - # Update the team repo - # Note: the team repo is only stored using its path (e.g. user/repository) - team_obj = self.db.get(self.table_name, team_id) - team_obj["repo"] = project_path - self.db.insert(self.table_name, team_obj, conflict="update") - - return jsonify( - { - "project_path": project_path - } - ) - - def validate_project(self, team, project_path): - # Check on GitLab if the project exists - # NB: certain fields (such as "forked_from_project") need an access token - # to be visible. Set the GITLAB_ACCESS_TOKEN env variable to solve this - query_response = self.request_project(project_path) - - if query_response.status_code != 200: - return self.error( - ErrorCodes.incorrect_parameters, - "Not a valid repository." - ) - - # Check if the jam's base repo has been set by staff - # If not, just ignore the fork check and proceed - if "repo" not in team["jam"]: - return True - jam_repo = team["jam"]["repo"] - - # Check if the provided repo is a forked repo - project_data = query_response.json() - if "forked_from_project" not in project_data: - return self.error( - ErrorCodes.incorrect_parameters, - "This repository is not a fork of the jam's repository." - ) - - # Check if the provided repo is forking the base repo - forked_from_project = project_data["forked_from_project"] - - # The jam repo is stored in full (e.g. https://gitlab.com/user/repository) - jam_repo_path = quote(parse_url(jam_repo).path.strip("/"), safe='') - - # Get info about the code jam repo - jam_repo_response = self.request_project(jam_repo_path) - - # Something went wrong, fail silently - if jam_repo_response.status_code != 200: - return True - - # Check if the IDs for the code jam repo and the fork source match - jam_repo_data = jam_repo_response.json() - if jam_repo_data["id"] != forked_from_project["id"]: - return self.error( - ErrorCodes.incorrect_parameters, - "This repository is not a fork of the jam's repository." - ) - - # All good - return True - - def request_project(self, project_path): - # Request the project details using a private access token - return requests.get( - self.gitlab_projects_api_endpoint.format(project_path), - params={ - "private_token": GITLAB_ACCESS_TOKEN - } - ) diff --git a/pysite/views/main/jams/team_view.py b/pysite/views/main/jams/team_view.py deleted file mode 100644 index 6b5d86ce..00000000 --- a/pysite/views/main/jams/team_view.py +++ /dev/null @@ -1,53 +0,0 @@ -import datetime -import logging - -from rethinkdb import ReqlNonExistenceError -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin, OAuthMixin - -log = logging.getLogger(__name__) - - -class JamsTeamView(RouteView, DBMixin, OAuthMixin): - path = "/jams/team/<string:team_id>" - name = "jams.team_view" - - table_name = "code_jam_teams" - - def get(self, team_id: str): - try: - query = self.db.query(self.table_name).get(team_id).merge( - lambda team: { - "members": - self.db.query("users") - .filter(lambda user: team["members"].contains(user["user_id"])) - .merge( - lambda user: { - "gitlab_username": self.db.query("code_jam_participants").filter( - {"id": user["user_id"]} - ).coerce_to("array")[0]["gitlab_username"] - } - ).coerce_to("array"), - "jam": self.db.query("code_jams").get(team["jam"]) - } - ) - - team = self.db.run(query) - except ReqlNonExistenceError: - log.exception("Failed RethinkDB query") - raise NotFound() - - # check if the current user is a member of this team - # (this is for edition privileges) - is_own_team = self.logged_in and self.user_data["user_id"] in [member["user_id"] for member in team["members"]] - - return self.render( - "main/jams/team_view.html", - team=team, is_own_team=is_own_team, day_delta=self.day_delta - ) - - def day_delta(self, date, delta): - # util to add or subtract days from a date - return date + datetime.timedelta(days=delta) diff --git a/pysite/views/main/jams/user_team_list.py b/pysite/views/main/jams/user_team_list.py deleted file mode 100644 index 226cc4b0..00000000 --- a/pysite/views/main/jams/user_team_list.py +++ /dev/null @@ -1,37 +0,0 @@ -import rethinkdb - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin, OAuthMixin - - -class JamsUserTeamListView(RouteView, DBMixin, OAuthMixin): - path = "/jams/my_teams" - name = "jams.user_team_list" - - def get(self): - # list teams a user is (or was) a part of - if not self.user_data: - return self.redirect_login() - - query = self.db.query("code_jam_teams").filter( - lambda team: team["members"].contains(self.user_data["user_id"]) - ).merge( - lambda team: { - "members": - self.db.query("users") - .filter(lambda user: team["members"].contains(user["user_id"])) - .merge(lambda user: { - "gitlab_username": - self.db.query("code_jam_participants").filter({"id": user["user_id"]}) - .coerce_to("array")[0]["gitlab_username"] - }).coerce_to("array"), - "jam": self.db.query("code_jams").get(team["jam"]) - } - ).order_by(rethinkdb.desc("jam.number")) - teams = self.db.run(query) - - return self.render( - "main/jams/team_list.html", - user_teams=True, - teams=teams - ) diff --git a/pysite/views/main/logout.py b/pysite/views/main/logout.py deleted file mode 100644 index 64326371..00000000 --- a/pysite/views/main/logout.py +++ /dev/null @@ -1,16 +0,0 @@ -from flask import redirect, session, url_for - -from pysite.base_route import RouteView - - -class LogoutView(RouteView): - path = "/auth/logout" - name = "logout" - - def get(self): - if self.logged_in: - # remove user's session - del session["session_id"] - self.oauth.logout() - - return redirect(url_for("main.index")) diff --git a/pysite/views/main/redirects/__init__.py b/pysite/views/main/redirects/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/main/redirects/__init__.py +++ /dev/null diff --git a/pysite/views/main/redirects/github.py b/pysite/views/main/redirects/github.py deleted file mode 100644 index 9e9c0cb8..00000000 --- a/pysite/views/main/redirects/github.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class GitHubView(RedirectView): - path = "/github" - name = "github" - page = "https://gitlab.com/python-discord/" - code = 302 diff --git a/pysite/views/main/redirects/gitlab.py b/pysite/views/main/redirects/gitlab.py deleted file mode 100644 index 4b2b60b4..00000000 --- a/pysite/views/main/redirects/gitlab.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class GitLabView(RedirectView): - path = "/gitlab" - name = "gitlab" - page = "https://gitlab.com/python-discord/" - code = 302 diff --git a/pysite/views/main/redirects/invite.py b/pysite/views/main/redirects/invite.py deleted file mode 100644 index 72e0d144..00000000 --- a/pysite/views/main/redirects/invite.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class InviteView(RedirectView): - path = "/invite" - name = "invite" - page = "https://discord.gg/8NWhsvT" - code = 302 diff --git a/pysite/views/main/redirects/stats.py b/pysite/views/main/redirects/stats.py deleted file mode 100644 index 57a56b3d..00000000 --- a/pysite/views/main/redirects/stats.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class StatsView(RedirectView): - path = "/stats" - name = "stats" - page = "https://p.datadoghq.com/sb/ac8680a8c-c01b556f01b96622fd4f57545b81d568" - code = 302 diff --git a/pysite/views/main/robots_txt.py b/pysite/views/main/robots_txt.py deleted file mode 100644 index 308fe2a2..00000000 --- a/pysite/views/main/robots_txt.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class RobotsTXT(RouteView): - path = "/robots.txt" - name = "robots_txt" - - def get(self): - return Response( - self.render( - "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True) - ), content_type="text/plain" - ) diff --git a/pysite/views/main/sitemap_xml.py b/pysite/views/main/sitemap_xml.py deleted file mode 100644 index 98893c21..00000000 --- a/pysite/views/main/sitemap_xml.py +++ /dev/null @@ -1,69 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class SitemapXML(RouteView): - path = "/sitemap.xml" - name = "sitemap_xml" - - def get(self): - urls = [ - { - "type": "url", - "url": url_for("main.index", _external=True), - "priority": 1.0, # Max priority - - "images": [ - { - "caption": "Python Discord Logo", - "url": url_for("static", filename="logos/logo_discord.png", _external=True) - }, - { - "caption": "Python Discord Banner", - "url": url_for("static", filename="logos/logo_banner.png", _external=True) - } - ] - }, - - { - "type": "url", - "url": url_for("main.jams.index", _external=True), - "priority": 0.9 # Above normal priority - }, - - { - "type": "url", - "url": url_for("main.about.privacy", _external=True), - "priority": 0.8 # Above normal priority - }, - { - "type": "url", - "url": url_for("main.about.rules", _external=True), - "priority": 0.8 # Above normal priority - }, - - { - "type": "url", - "url": url_for("main.info.help", _external=True), - "priority": 0.7 # Above normal priority - }, - { - "type": "url", - "url": url_for("main.info.faq", _external=True), - "priority": 0.7 # Above normal priority - }, - { - "type": "url", - "url": url_for("main.info.resources", _external=True), - "priority": 0.7 # Above normal priority - }, - - { - "type": "url", - "url": url_for("main.about.partners", _external=True), - "priority": 0.6 # Normal priority - }, - ] - - return Response(self.render("sitemap.xml", urls=urls), content_type="application/xml") diff --git a/pysite/views/main/ws_test.py b/pysite/views/main/ws_test.py deleted file mode 100644 index a0b6215f..00000000 --- a/pysite/views/main/ws_test.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -from pysite.base_route import RouteView - - -class WSTest(RouteView): - path = "/ws_test" - name = "ws_test" - - def get(self): - return self.render( - "main/ws_test.html", - server_name=os.environ.get("SERVER_NAME", "localhost") - ) diff --git a/pysite/views/main/ws_test_rst.py b/pysite/views/main/ws_test_rst.py deleted file mode 100644 index e80acc55..00000000 --- a/pysite/views/main/ws_test_rst.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -from pysite.base_route import RouteView - - -class WSTest(RouteView): - path = "/ws_test_rst" - name = "ws_test_rst" - - def get(self): - return self.render( - "main/ws_test_rst.html", - server_name=os.environ.get("SERVER_NAME", "localhost") - ) diff --git a/pysite/views/staff/__init__.py b/pysite/views/staff/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/__init__.py +++ /dev/null diff --git a/pysite/views/staff/index.py b/pysite/views/staff/index.py deleted file mode 100644 index a090ebdd..00000000 --- a/pysite/views/staff/index.py +++ /dev/null @@ -1,31 +0,0 @@ -from pprint import pformat - -from flask import current_app - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES, DEBUG_MODE, TABLE_MANAGER_ROLES -from pysite.decorators import require_roles - - -class StaffView(RouteView): - path = "/" - name = "index" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - return self.render( - "staff/index.html", manager=self.is_table_editor(), - app_config=pformat(current_app.config, indent=4, width=120) - ) - - def is_table_editor(self): - if DEBUG_MODE: - return True - - data = self.user_data - - for role in TABLE_MANAGER_ROLES: - if role in data.get("roles", []): - return True - - return False diff --git a/pysite/views/staff/jams/__init__.py b/pysite/views/staff/jams/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/jams/__init__.py +++ /dev/null diff --git a/pysite/views/staff/jams/actions.py b/pysite/views/staff/jams/actions.py deleted file mode 100644 index dfcbf2de..00000000 --- a/pysite/views/staff/jams/actions.py +++ /dev/null @@ -1,597 +0,0 @@ -from flask import jsonify, request -from rethinkdb import ReqlNonExistenceError - -from pysite.base_route import APIView -from pysite.constants import ALL_STAFF_ROLES, BotEventTypes, CHANNEL_JAM_LOGS, ErrorCodes, JAMMERS_ROLE -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin, RMQMixin -from pysite.utils.words import get_word_pairs - -GET_ACTIONS = ("questions",) -POST_ACTIONS = ( - "associate_question", "disassociate_question", "infraction", "questions", "state", "approve_application", - "unapprove_application", "create_team", "generate_teams", "set_team_member", - "reroll_team", "set_winning_team", "unset_winning_team" -) -DELETE_ACTIONS = ("infraction", "question", "team") - -KEYS = ("action",) -QUESTION_KEYS = ("optional", "title", "type") - - -class ActionView(APIView, DBMixin, RMQMixin): - path = "/jams/action" - name = "jams.action" - - table_name = "code_jams" - forms_table = "code_jam_forms" - infractions_table = "code_jam_infractions" - questions_table = "code_jam_questions" - responses_table = "code_jam_responses" - teams_table = "code_jam_teams" - users_table = "users" - - @csrf - @require_roles(*ALL_STAFF_ROLES) - def get(self): - action = request.args.get("action") - - if action not in GET_ACTIONS: - return self.error(ErrorCodes.incorrect_parameters) - - if action == "questions": - questions = self.db.get_all(self.questions_table) - - return jsonify({"questions": questions}) - - @csrf - @require_roles(*ALL_STAFF_ROLES) - def post(self): - if request.is_json: - data = request.get_json(force=True) - action = data["action"] if "action" in data else None - else: - action = request.form.get("action") - - if action not in POST_ACTIONS: - return self.error(ErrorCodes.incorrect_parameters) - - if action == "associate_question": - form = int(request.form.get("form")) - question = request.form.get("question") - - form_obj = self.db.get(self.forms_table, form) - - if not form_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown form: {form}") - - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown question: {question}") - - if question_obj["id"] not in form_obj["questions"]: - form_obj["questions"].append(question_obj["id"]) - self.db.insert(self.forms_table, form_obj, conflict="replace") - - return jsonify({"question": question_obj}) - else: - return self.error( - ErrorCodes.incorrect_parameters, - f"Question {question} already associated with form {form}" - ) - - if action == "disassociate_question": - form = int(request.form.get("form")) - question = request.form.get("question") - - form_obj = self.db.get(self.forms_table, form) - - if not form_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown form: {form}") - - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown question: {question}") - - if question_obj["id"] in form_obj["questions"]: - form_obj["questions"].remove(question_obj["id"]) - self.db.insert(self.forms_table, form_obj, conflict="replace") - - return jsonify({"question": question_obj}) - else: - return self.error( - ErrorCodes.incorrect_parameters, - f"Question {question} not already associated with form {form}" - ) - - if action == "state": - jam = int(request.form.get("jam")) - state = request.form.get("state") - - if not all((jam, state)): - return self.error(ErrorCodes.incorrect_parameters) - - jam_obj = self.db.get(self.table_name, jam) - jam_obj["state"] = state - self.db.insert(self.table_name, jam_obj, conflict="update") - - return jsonify({}) - - if action == "questions": - data = request.get_json(force=True) - - for key in QUESTION_KEYS: - if key not in data: - return self.error(ErrorCodes.incorrect_parameters, f"Missing key: {key}") - - title = data["title"] - optional = data["optional"] - question_type = data["type"] - question_data = data.get("data", {}) - - if question_type in ["number", "range", "slider"]: - if "max" not in question_data or "min" not in question_data: - return self.error( - ErrorCodes.incorrect_parameters, f"{question_type} questions must have both max and min values" - ) - - result = self.db.insert( - self.questions_table, - { - "title": title, - "optional": optional, - "type": question_type, - "data": { - "max": question_data["max"], - "min": question_data["min"] - } - }, - conflict="error" - ) - elif question_type == "radio": - if "options" not in question_data: - return self.error( - ErrorCodes.incorrect_parameters, f"{question_type} questions must have both options" - ) - - result = self.db.insert( - self.questions_table, - { - "title": title, - "optional": optional, - "type": question_type, - "data": { - "options": question_data["options"] - } - }, - conflict="error" - ) - else: - result = self.db.insert( - self.questions_table, - { # No extra data for other types of question - "title": title, - "optional": optional, - "type": question_type - }, - conflict="error" - ) - - return jsonify({"id": result["generated_keys"][0]}) - - if action == "infraction": - participant = request.form.get("participant") - reason = request.form.get("reason") - - if not participant or not reason or "number" not in request.form: - return self.error( - ErrorCodes.incorrect_parameters, "Infractions must have a participant, reason and number" - ) - - number = int(request.form.get("number")) - - result = self.db.insert(self.infractions_table, { - "participant": participant, - "reason": reason, - "number": number, - "decremented_for": [] - }) - - return jsonify({"id": result["generated_keys"][0]}) - - if action == "create_team": - jam = request.form.get("jam", type=int) - - if not jam: - return self.error( - ErrorCodes.incorrect_parameters, "Jam number required" - ) - - jam_data = self.db.get(self.table_name, jam) - - if not jam_data: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown jam number" - ) - - word_pairs = get_word_pairs() - adjective, noun = list(word_pairs)[0] - - team = { - "name": f"{adjective} {noun}".title(), - "members": [], - "jam": jam - } - - result = self.db.insert(self.teams_table, team) - team["id"] = result["generated_keys"][0] - - jam_obj = self.db.get(self.table_name, jam) - jam_obj["teams"].append(team["id"]) - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return jsonify({"team": team}) - - if action == "generate_teams": - jam = request.form.get("jam", type=int) - - if not jam: - return self.error( - ErrorCodes.incorrect_parameters, "Jam number required" - ) - - try: - query = self.db.query(self.table_name).get(jam).merge( - lambda jam_obj: { - "participants": - self.db.query(self.responses_table) - .filter({"jam": jam_obj["number"], "approved": True}) - .eq_join("snowflake", self.db.query(self.users_table)) - .without({"left": ["snowflake", "answers"]}) - .zip() - .order_by("username") - .coerce_to("array"), - "teams": - self.db.query(self.teams_table) - .outer_join(self.db.query(self.table_name), - lambda team_row, jams_row: jams_row["teams"].contains(team_row["id"])) - .pluck({"left": ["id", "name", "members"]}) - .zip() - .coerce_to("array") - } - ) - - jam_data = self.db.run(query) - except ReqlNonExistenceError: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown jam number" - ) - - if jam_data["teams"]: - return self.error( - ErrorCodes.incorrect_parameters, "Jam already has teams" - ) - - num_participants = len(jam_data["participants"]) - num_teams = num_participants // 3 - - if num_participants % 3: - num_teams += 1 - - word_pairs = get_word_pairs(num_teams) - teams = [] - - for adjective, noun in word_pairs: - team = { - "name": f"{adjective} {noun}".title(), - "members": [] - } - - result = self.db.insert(self.teams_table, team, durability="soft") - team["id"] = result["generated_keys"][0] - teams.append(team) - - self.db.sync(self.teams_table) - - jam_obj = self.db.get(self.table_name, jam) - jam_obj["teams"] = [team["id"] for team in teams] - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return jsonify({"teams": teams}) - - if action == "set_team_member": - jam = request.form.get("jam", type=int) - member = request.form.get("member") - team = request.form.get("team") - - if not jam: - return self.error( - ErrorCodes.incorrect_parameters, "Jam number required" - ) - - if not member: - return self.error( - ErrorCodes.incorrect_parameters, "Member ID required" - ) - - if not team: - return self.error( - ErrorCodes.incorrect_parameters, "Team ID required" - ) - - try: - query = self.db.query(self.table_name).get(jam).merge( - lambda jam_obj: { - "participants": - self.db.query(self.responses_table) - .filter({"jam": jam_obj["number"], "approved": True}) - .eq_join("snowflake", self.db.query(self.users_table)) - .without({"left": ["snowflake", "answers"]}) - .zip() - .order_by("username") - .coerce_to("array"), - "teams": - self.db.query(self.teams_table) - .filter(lambda team_row: jam_obj["teams"].contains(team_row["id"])) - .pluck(["id", "name", "members", "jam"]) - .coerce_to("array") - } - ) - - jam_data = self.db.run(query) - except ReqlNonExistenceError: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown jam number" - ) - - if not jam_data["teams"]: - return self.error( - ErrorCodes.incorrect_parameters, "Jam has no teams" - ) - - team_obj = self.db.get(self.teams_table, team) - - if not team_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown team ID" - ) - - for jam_team_obj in jam_data["teams"]: - if jam_team_obj["id"] == team: - if member not in jam_team_obj["members"]: - jam_team_obj["members"].append(member) - - self.db.insert(self.teams_table, jam_team_obj, conflict="replace") - else: - if member in jam_team_obj["members"]: - jam_team_obj["members"].remove(member) - - self.db.insert(self.teams_table, jam_team_obj, conflict="replace") - - return jsonify({"result": True}) - - if action == "reroll_team": - team = request.form.get("team") - - if not team: - return self.error( - ErrorCodes.incorrect_parameters, "Team ID required" - ) - - team_obj = self.db.get(self.teams_table, team) - - if not team_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown team ID" - ) - - word_pairs = get_word_pairs() - adjective, noun = list(word_pairs)[0] - - team_obj["name"] = f"{adjective} {noun}".title() - - self.db.insert(self.teams_table, team_obj, conflict="replace") - - return jsonify({"name": team_obj["name"]}) - - if action == "set_winning_team": - team = request.form.get("team") - - if not team: - return self.error( - ErrorCodes.incorrect_parameters, "Team ID required" - ) - - team_obj = self.db.get(self.teams_table, team) - - if not team_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown team ID" - ) - - jam_number = team_obj["jam"] - jam_obj = self.db.get(self.table_name, jam_number) - jam_obj["winning_team"] = team - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return jsonify({"result": "success"}) - - if action == "unset_winning_team": - jam = request.form.get("jam", type=int) - - if not jam: - return self.error( - ErrorCodes.incorrect_parameters, "Jam number required" - ) - - jam_obj = self.db.get(self.table_name, jam) - if not jam_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown jam number" - ) - - jam_obj["winning_team"] = None - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return jsonify({"result": "success"}) - - if action == "approve_application": - app = request.form.get("id") - - if not app: - return self.error( - ErrorCodes.incorrect_parameters, "Application ID required" - ) - - app_obj = self.db.get(self.responses_table, app) - - if not app_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown application ID" - ) - - app_obj["approved"] = True - - self.db.insert(self.responses_table, app_obj, conflict="replace") - - jam_obj = self.db.get(self.table_name, app_obj["jam"]) - - snowflake = app_obj["snowflake"] - participants = jam_obj.get("participants", []) - - if snowflake not in participants: - participants.append(snowflake) - jam_obj["participants"] = participants - self.db.insert(self.table_name, jam_obj, conflict="replace") - - self.rmq_bot_event( - BotEventTypes.add_role, - { - "reason": "Code jam application approved", - "role_id": JAMMERS_ROLE, - "target": snowflake, - } - ) - - self.rmq_bot_event( - BotEventTypes.send_message, - { - "message": f"Congratulations <@{snowflake}> - you've been approved, " - f"and we've assigned you the Jammer role!", - "target": CHANNEL_JAM_LOGS, - } - ) - - return jsonify({"result": "success"}) - - if action == "unapprove_application": - app = request.form.get("id") - - if not app: - return self.error( - ErrorCodes.incorrect_parameters, "Application ID required" - ) - - app_obj = self.db.get(self.responses_table, app) - - if not app_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown application ID" - ) - - app_obj["approved"] = False - - self.db.insert(self.responses_table, app_obj, conflict="replace") - - jam_obj = self.db.get(self.table_name, app_obj["jam"]) - - snowflake = app_obj["snowflake"] - participants = jam_obj.get("participants", []) - - if snowflake in participants: - participants.remove(snowflake) - jam_obj["participants"] = participants - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - self.rmq_bot_event( - BotEventTypes.remove_role, - { - "reason": "Code jam application unapproved", - "role_id": JAMMERS_ROLE, - "target": snowflake, - } - ) - - return jsonify({"result": "success"}) - - @csrf - @require_roles(*ALL_STAFF_ROLES) - def delete(self): - action = request.form.get("action") - - if action not in DELETE_ACTIONS: - return self.error(ErrorCodes.incorrect_parameters) - - if action == "question": - question = request.form.get("id") - - if not question: - return self.error(ErrorCodes.incorrect_parameters, f"Missing key: id") - - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown question: {question}") - - self.db.delete(self.questions_table, question) - - for form_obj in self.db.get_all(self.forms_table): - if question in form_obj["questions"]: - form_obj["questions"].remove(question) - self.db.insert(self.forms_table, form_obj, conflict="replace") - - return jsonify({"id": question}) - - if action == "infraction": - infraction = request.form.get("id") - - if not infraction: - return self.error(ErrorCodes.incorrect_parameters, "Missing key id") - - infraction_obj = self.db.get(self.infractions_table, infraction) - - if not infraction_obj: - return self.error(ErrorCodes.incorrect_parameters, f"Unknown infraction: {infraction}") - - self.db.delete(self.infractions_table, infraction) - - return jsonify({"id": infraction_obj["id"]}) - - if action == "team": - team = request.form.get("team") - - if not team: - return self.error( - ErrorCodes.incorrect_parameters, "Team ID required" - ) - - team_obj = self.db.get(self.teams_table, team) - - if not team_obj: - return self.error( - ErrorCodes.incorrect_parameters, "Unknown team ID" - ) - - jam_obj = self.db.get(self.table_name, team_obj["jam"]) - if jam_obj: - jam_obj["teams"].remove(team) - self.db.insert(self.table_name, jam_obj, conflict="update") - - self.db.delete(self.teams_table, team) - - return jsonify({"result": True}) diff --git a/pysite/views/staff/jams/create.py b/pysite/views/staff/jams/create.py deleted file mode 100644 index ef61cbef..00000000 --- a/pysite/views/staff/jams/create.py +++ /dev/null @@ -1,61 +0,0 @@ -import datetime - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/create" - name = "jams.create" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - number = self.get_next_number() - return self.render("staff/jams/create.html", number=number) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self): - data = {} - - for key in REQUIRED_KEYS: - arg = request.form.get(key) - - if not arg: - return BadRequest() - - data[key] = arg - - data["state"] = "planning" - data["number"] = self.get_next_number() - - # Convert given datetime strings into actual objects, adding timezones to keep rethinkdb happy - date_start = datetime.datetime.strptime(data["date_start"], "%Y-%m-%d %H:%M") - date_start = date_start.replace(tzinfo=datetime.timezone.utc) - - date_end = datetime.datetime.strptime(data["date_end"], "%Y-%m-%d %H:%M") - date_end = date_end.replace(tzinfo=datetime.timezone.utc) - - data["date_start"] = date_start - data["date_end"] = date_end - - self.db.insert(self.table_name, data) - - return redirect(url_for("staff.jams.index")) - - def get_next_number(self) -> int: - count = self.db.run(self.table.count(), coerce=int) - - if count: - max_num = self.db.run(self.table.max("number"))["number"] - - return max_num + 1 - return 1 diff --git a/pysite/views/staff/jams/edit_basics.py b/pysite/views/staff/jams/edit_basics.py deleted file mode 100644 index 462cba14..00000000 --- a/pysite/views/staff/jams/edit_basics.py +++ /dev/null @@ -1,55 +0,0 @@ -import datetime - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/<int:jam>/edit/basics" - name = "jams.edit.basics" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - return self.render("staff/jams/edit_basics.html", jam=jam_obj) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - for key in REQUIRED_KEYS: - arg = request.form.get(key) - - if not arg: - return BadRequest() - - jam_obj[key] = arg - - # Convert given datetime strings into actual objects, adding timezones to keep rethinkdb happy - date_start = datetime.datetime.strptime(jam_obj["date_start"], "%Y-%m-%d %H:%M") - date_start = date_start.replace(tzinfo=datetime.timezone.utc) - - date_end = datetime.datetime.strptime(jam_obj["date_end"], "%Y-%m-%d %H:%M") - date_end = date_end.replace(tzinfo=datetime.timezone.utc) - - jam_obj["date_start"] = date_start - jam_obj["date_end"] = date_end - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return redirect(url_for("staff.jams.index")) diff --git a/pysite/views/staff/jams/edit_ending.py b/pysite/views/staff/jams/edit_ending.py deleted file mode 100644 index 43a36ebc..00000000 --- a/pysite/views/staff/jams/edit_ending.py +++ /dev/null @@ -1,54 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin -from pysite.rst import render - -REQUIRED_KEYS = ["end_rst"] -ALLOWED_STATES = ["judging", "finished"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/<int:jam>/edit/ending" - name = "jams.edit.ending" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not jam_obj["state"] in ALLOWED_STATES: - return BadRequest() - - return self.render("staff/jams/edit_ending.html", jam=jam_obj) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not jam_obj["state"] in ALLOWED_STATES: - return BadRequest() - - for key in REQUIRED_KEYS: - arg = request.form.get(key) - - if not arg: - return BadRequest() - - jam_obj[key] = arg - - jam_obj["end_html"] = render(jam_obj["end_rst"], link_headers=False)["html"] - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return redirect(url_for("staff.jams.index")) diff --git a/pysite/views/staff/jams/edit_info.py b/pysite/views/staff/jams/edit_info.py deleted file mode 100644 index 4944ae67..00000000 --- a/pysite/views/staff/jams/edit_info.py +++ /dev/null @@ -1,55 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin -from pysite.rst import render - -REQUIRED_KEYS = ["info_rst", "repo", "task_rst", "theme"] -ALLOWED_STATES = ["planning", "announced", "preparing", "finished"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/<int:jam>/edit/info" - name = "jams.edit.info" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not jam_obj["state"] in ALLOWED_STATES: - return BadRequest() - - return self.render("staff/jams/edit_info.html", jam=jam_obj) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - if not jam_obj["state"] in ALLOWED_STATES: - return BadRequest() - - for key in REQUIRED_KEYS: - arg = request.form.get(key) - - if not arg: - return BadRequest() - - jam_obj[key] = arg - - jam_obj["task_html"] = render(jam_obj["task_rst"], link_headers=False)["html"] - jam_obj["info_html"] = render(jam_obj["info_rst"], link_headers=False)["html"] - - self.db.insert(self.table_name, jam_obj, conflict="replace") - - return redirect(url_for("staff.jams.index")) diff --git a/pysite/views/staff/jams/forms/__init__.py b/pysite/views/staff/jams/forms/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/jams/forms/__init__.py +++ /dev/null diff --git a/pysite/views/staff/jams/forms/preamble_edit.py b/pysite/views/staff/jams/forms/preamble_edit.py deleted file mode 100644 index 59b4678b..00000000 --- a/pysite/views/staff/jams/forms/preamble_edit.py +++ /dev/null @@ -1,45 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin -from pysite.rst import render - - -class StaffView(RouteView, DBMixin): - path = "/jams/form/<int:jam>/preamble" - name = "jams.forms.preamble.edit" - - table_name = "code_jam_forms" - jams_table = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.jams_table, jam) - - if not jam_obj: - return NotFound() - - form_obj = self.db.get(self.table_name, jam) - return self.render("staff/jams/forms/preamble_edit.html", jam=jam_obj, form=form_obj) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - form_obj = self.db.get(self.table_name, jam) - - preamble_rst = request.form.get("preamble_rst") - - form_obj["preamble_rst"] = preamble_rst - form_obj["preamble_html"] = render(preamble_rst, link_headers=False)["html"] - - self.db.insert(self.table_name, form_obj, conflict="replace") - - return redirect(url_for("staff.jams.forms.view", jam=jam)) diff --git a/pysite/views/staff/jams/forms/questions_edit.py b/pysite/views/staff/jams/forms/questions_edit.py deleted file mode 100644 index d46c4ef3..00000000 --- a/pysite/views/staff/jams/forms/questions_edit.py +++ /dev/null @@ -1,75 +0,0 @@ -import json - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/forms/questions/<question>" - name = "jams.forms.questions.edit" - - questions_table = "code_jam_questions" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, question): - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return NotFound() - - question_obj["data"] = question_obj.get("data", {}) - - return self.render( - "staff/jams/forms/questions_edit.html", question=question_obj - ) - - @require_roles(*ALL_STAFF_ROLES) - @csrf - def post(self, question): - question_obj = self.db.get(self.questions_table, question) - - if not question_obj: - return NotFound() - - title = request.form.get("title") - optional = request.form.get("optional") - question_type = request.form.get("type") - - if not title or not optional or not question_type: - return BadRequest() - - question_obj["title"] = title - question_obj["optional"] = optional == "optional" - question_obj["type"] = question_type - - if question_type == "radio": - options = request.form.get("options") - - if not options: - return BadRequest() - - options = json.loads(options)["options"] # No choice this time - question_obj["data"] = {"options": options} - - elif question_type in ("number", "range", "slider"): - question_min = request.form.get("min") - question_max = request.form.get("max") - - if question_min is None or question_max is None: - return BadRequest() - - question_obj["data"] = { - "min": question_min, - "max": question_max - } - - self.db.insert(self.questions_table, question_obj, conflict="replace") - - return redirect(url_for("staff.jams.forms.questions")) diff --git a/pysite/views/staff/jams/forms/questions_view.py b/pysite/views/staff/jams/forms/questions_view.py deleted file mode 100644 index 50ad009e..00000000 --- a/pysite/views/staff/jams/forms/questions_view.py +++ /dev/null @@ -1,22 +0,0 @@ -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/forms/questions" - name = "jams.forms.questions" - - questions_table = "code_jam_questions" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - questions = self.db.get_all(self.questions_table) - - return self.render( - "staff/jams/forms/questions_view.html", questions=questions, - question_ids=[q["id"] for q in questions] - ) diff --git a/pysite/views/staff/jams/forms/view.py b/pysite/views/staff/jams/forms/view.py deleted file mode 100644 index 8d4e16ad..00000000 --- a/pysite/views/staff/jams/forms/view.py +++ /dev/null @@ -1,46 +0,0 @@ -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/forms/<int:jam>" - name = "jams.forms.view" - - table_name = "code_jams" - forms_table = "code_jam_forms" - questions_table = "code_jam_questions" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam): - jam_obj = self.db.get(self.table_name, jam) - - if not jam_obj: - return NotFound() - - form_obj = self.db.get(self.forms_table, jam) - - if not form_obj: - form_obj = { - "number": jam, - "questions": [], - "preamble_rst": "", - "preamble_html": "" - } - - self.db.insert(self.forms_table, form_obj) - - if form_obj["questions"]: - questions = self.db.get_all(self.questions_table, *[q for q in form_obj["questions"]]) - else: - questions = [] - - return self.render( - "staff/jams/forms/view.html", jam=jam_obj, form=form_obj, - questions=questions, question_ids=[q["id"] for q in questions] - ) diff --git a/pysite/views/staff/jams/index.py b/pysite/views/staff/jams/index.py deleted file mode 100644 index 40a8387c..00000000 --- a/pysite/views/staff/jams/index.py +++ /dev/null @@ -1,15 +0,0 @@ -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES, JAM_STATES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - - -class StaffView(RouteView, DBMixin): - path = "/jams" - name = "jams.index" - table_name = "code_jams" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - jams = self.db.get_all(self.table_name) - return self.render("staff/jams/index.html", jams=jams, states=JAM_STATES) diff --git a/pysite/views/staff/jams/infractions/__init__.py b/pysite/views/staff/jams/infractions/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/jams/infractions/__init__.py +++ /dev/null diff --git a/pysite/views/staff/jams/infractions/view.py b/pysite/views/staff/jams/infractions/view.py deleted file mode 100644 index 235f99ac..00000000 --- a/pysite/views/staff/jams/infractions/view.py +++ /dev/null @@ -1,29 +0,0 @@ -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] - - -class StaffView(RouteView, DBMixin): - path = "/jams/infractions" - name = "jams.infractions" - - table_name = "code_jam_infractions" - users_table = "users" - - @require_roles(*ALL_STAFF_ROLES) - def get(self): - infractions = self.db.get_all(self.table_name) - - for document in infractions: - user_obj = self.db.get(self.users_table, document["participant"]) - - if user_obj: - document["participant"] = user_obj - - return self.render( - "staff/jams/infractions/view.html", infractions=infractions, - infraction_ids=[i["id"] for i in infractions] - ) diff --git a/pysite/views/staff/jams/participants.py b/pysite/views/staff/jams/participants.py deleted file mode 100644 index 52f9bdec..00000000 --- a/pysite/views/staff/jams/participants.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging - -from rethinkdb import ReqlNonExistenceError -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ["title", "date_start", "date_end"] -log = logging.getLogger(__name__) - - -class StaffView(RouteView, DBMixin): - path = "/jams/participants/<int:jam>" - name = "jams.participants" - - forms_table = "code_jam_forms" - participants_table = "code_jam_participants" - questions_table = "code_jam_questions" - responses_table = "code_jam_responses" - table_name = "code_jams" - users_table = "users" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam: int): - try: - query = self.db.query(self.table_name).get(jam).merge( - lambda jam_obj: { - "participants": - self.db.query(self.responses_table) - .filter({"jam": jam_obj["number"]}) - .eq_join("snowflake", self.db.query(self.users_table)) - .without({"left": "snowflake"}) - .zip() - .coerce_to("array") - } - ) - - jam_data = self.db.run(query) - except ReqlNonExistenceError: - log.exception("Failed RethinkDB query") - raise NotFound() - - form_obj = self.db.get(self.forms_table, jam) - questions = {} - - if form_obj: - for question in form_obj["questions"]: - questions[question] = self.db.get(self.questions_table, question) - - return self.render( - "staff/jams/participants.html", - jam=jam_data, form=form_obj, questions=questions - ) diff --git a/pysite/views/staff/jams/teams/__init__.py b/pysite/views/staff/jams/teams/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/jams/teams/__init__.py +++ /dev/null diff --git a/pysite/views/staff/jams/teams/view.py b/pysite/views/staff/jams/teams/view.py deleted file mode 100644 index 662cc084..00000000 --- a/pysite/views/staff/jams/teams/view.py +++ /dev/null @@ -1,102 +0,0 @@ -import logging - -from rethinkdb import ReqlNonExistenceError -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import ALL_STAFF_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin - -REQUIRED_KEYS = ("title", "date_start", "date_end") -log = logging.getLogger(__name__) - - -class StaffView(RouteView, DBMixin): - path = "/jams/teams/<int:jam>" - name = "jams.teams" - - table_name = "code_jam_teams" - - forms_table = "code_jam_forms" - jams_table = "code_jams" - participants_table = "code_jam_participants" - questions_table = "code_jam_questions" - responses_table = "code_jam_responses" - users_table = "users" - - @require_roles(*ALL_STAFF_ROLES) - def get(self, jam: int): - try: - query = self.db.query(self.jams_table).get(jam).merge( - # Merge the jam document with a custom document defined below - lambda jam_obj: { # The lambda lets us manipulate the jam document server-side - "participants": - # Query the responses table - self.db.query(self.responses_table) - # Filter: approved responses for this jam only # noqa: E131 - .filter({"jam": jam_obj["number"], "approved": True}) - # Join each response document with documents from the user table that match the user that - # created this response - this is the efficient way to do things, inner/outer joins - # are slower as they only support explicit predicates - .eq_join("snowflake", self.db.query(self.users_table)) - # Remove the user ID from the left side (the response document) - .without({"left": ["snowflake"]}) - .zip() # Combine the left and right documents together - .order_by("username") # Reorder the documents by username - .coerce_to("array"), # Coerce the document stream into an array - "profiles": - # Query the responses table (again) - # We do this because RethinkDB just returns empty lists if you join on another join - self.db.query(self.responses_table) - # Filter: approved responses for this jam only # noqa: E131 - .filter({"jam": jam_obj["number"], "approved": True}) - # Join each response document with documents from the participant profiles table - # this time - .eq_join("snowflake", self.db.query(self.participants_table)) - # Remove the user ID and answers from the left side (the response document) - .without({"left": ["snowflake", "answers"]}) - .zip() # Combine the left and right documents together - .order_by("username") # Reorder the documents by username - .coerce_to("array"), # Coerce the document stream into an array - "form": self.db.query(self.forms_table).get(jam), # Just get the correct form object - "teams": - self.db.query(self.table_name) - .filter(lambda team_row: jam_obj["teams"].contains(team_row["id"])) - .pluck(["id", "name", "members"]) - .coerce_to("array") - } - ) - - jam_data = self.db.run(query) - except ReqlNonExistenceError: - log.exception("Failed RethinkDB query") - raise NotFound() - - questions = {} - - for question in jam_data["form"]["questions"]: - questions[question] = self.db.get(self.questions_table, question) - - teams = {} - participants = {} - assigned = [] - - for team in jam_data["teams"]: - teams[team["id"]] = team - - for member in team["members"]: - assigned.append(member) - - for user in jam_data["participants"]: - participants[user["user_id"]] = user - - for profile in jam_data["profiles"]: - participants[profile["id"]]["profile"] = profile - - return self.render( - "staff/jams/teams/view.html", - jam=jam_data, teams=teams, - participants=participants, assigned=assigned, - questions=questions - ) diff --git a/pysite/views/staff/render.py b/pysite/views/staff/render.py deleted file mode 100644 index 0152e568..00000000 --- a/pysite/views/staff/render.py +++ /dev/null @@ -1,62 +0,0 @@ -import re - -from docutils.utils import SystemMessage -from flask import jsonify -from schema import Schema - -from pysite.base_route import APIView -from pysite.constants import EDITOR_ROLES, ValidationTypes -from pysite.decorators import api_params, csrf, require_roles -from pysite.rst import render - -SCHEMA = Schema([{ - "data": str -}]) - -MESSAGE_REGEX = re.compile(r"<string>:(\d+): \([A-Z]+/\d\) (.*)") - - -class RenderView(APIView): - path = "/render" # "path" means that it accepts slashes - name = "render" - - @csrf - @require_roles(*EDITOR_ROLES) - @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - if not len(data): - return jsonify({"error": "No data!"}) - - data = data[0]["data"] - try: - html = render(data, link_headers=False)["html"] - - return jsonify({"data": html}) - except SystemMessage as e: - lines = str(e) - data = { - "error": lines, - "error_lines": [] - } - - if "\n" in lines: - lines = lines.split("\n") - else: - lines = [lines] - - for message in lines: - match = MESSAGE_REGEX.match(message) - - if match: - data["error_lines"].append( - { - "row": int(match.group(1)) - 3, - "column": 0, - "type": "error", - "text": match.group(2) - } - ) - - return jsonify(data) - except Exception as e: - return jsonify({"error": str(e)}) diff --git a/pysite/views/staff/robots_txt.py b/pysite/views/staff/robots_txt.py deleted file mode 100644 index 308fe2a2..00000000 --- a/pysite/views/staff/robots_txt.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class RobotsTXT(RouteView): - path = "/robots.txt" - name = "robots_txt" - - def get(self): - return Response( - self.render( - "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True) - ), content_type="text/plain" - ) diff --git a/pysite/views/staff/sitemap_xml.py b/pysite/views/staff/sitemap_xml.py deleted file mode 100644 index 26a786b0..00000000 --- a/pysite/views/staff/sitemap_xml.py +++ /dev/null @@ -1,11 +0,0 @@ -from flask import Response - -from pysite.base_route import RouteView - - -class SitemapXML(RouteView): - path = "/sitemap.xml" - name = "sitemap_xml" - - def get(self): - return Response(self.render("sitemap.xml", urls=[]), content_type="application/xml") diff --git a/pysite/views/staff/tables/__init__.py b/pysite/views/staff/tables/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/staff/tables/__init__.py +++ /dev/null diff --git a/pysite/views/staff/tables/edit.py b/pysite/views/staff/tables/edit.py deleted file mode 100644 index 7de63ad2..00000000 --- a/pysite/views/staff/tables/edit.py +++ /dev/null @@ -1,110 +0,0 @@ -import json - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import TABLE_MANAGER_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin -from pysite.tables import TABLES - - -class TableEditView(RouteView, DBMixin): - path = "/tables/<table>/edit" - name = "tables.edit" - - @require_roles(*TABLE_MANAGER_ROLES) - def get(self, table): - obj = TABLES.get(table) - - if not obj: - # Unknown table - raise NotFound() - - if obj.locked: - return redirect(url_for("staff.tables.table", table=table, page=1), code=303) - - key = request.args.get("key") - - old_primary = None - - if key: - db_obj = self.db.get(table, key) - old_primary = key # Provide the current document's primary key, in case it's modified - - document = json.dumps( # Editor uses JSON - db_obj, - indent=4 - ) - else: - document = json.dumps( # Generate default document from key schema - {k: "" for k in obj.keys}, - indent=4 - ) - - return self.render( - "staff/tables/edit.html", table=table, primary_key=obj.primary_key, - document=document, old_primary=old_primary - ) - - @require_roles(*TABLE_MANAGER_ROLES) - @csrf - def post(self, table): - obj = TABLES.get(table) - - if not obj: - # Unknown table - raise NotFound() - - if obj.locked: - raise BadRequest() - - data = request.form.get("json") - old_primary = request.form.get("old_primary") - - if not data: - # No data given (for some reason) - document = json.dumps( - {k: "" for k in obj.keys}, - indent=4 - ) - - return self.render( - "staff/tables/edit.html", table=table, primary_key=obj.primary_key, document=document, - message="Please provide some data to save", old_primary=old_primary - ) - - try: - data = json.loads(data) - except json.JSONDecodeError as e: - # Invalid JSON - return self.render( - "staff/tables/edit.html", table=table, primary_key=obj.primary_key, document=data, - message=f"Invalid JSON, please try again: {e}", old_primary=old_primary - ) - - if not data[obj.primary_key]: - # No primary key value provided - return self.render( - "staff/tables/edit.html", table=table, primary_key=obj.primary_key, document=data, - message=f"Please provide a value for the primary key: {obj.primary_key}", old_primary=old_primary - ) - - if old_primary is None: - self.db.insert( # This is a new object, so just insert it - table, data - ) - elif old_primary == data[obj.primary_key]: - self.db.insert( # This is an update without a primary key change, replace the whole document - table, data, conflict="replace" - ) - else: - self.db.delete( # This is a primary key change, so we need to remove the old object - table, old_primary - ) - self.db.insert( - table, data, - ) - - return redirect(url_for("staff.tables.table", table=table, page=1), code=303) diff --git a/pysite/views/staff/tables/index.py b/pysite/views/staff/tables/index.py deleted file mode 100644 index 0d84aeb4..00000000 --- a/pysite/views/staff/tables/index.py +++ /dev/null @@ -1,13 +0,0 @@ -from pysite.base_route import RouteView -from pysite.constants import TABLE_MANAGER_ROLES -from pysite.decorators import require_roles -from pysite.tables import TABLES - - -class TablesView(RouteView): - path = "/tables" - name = "tables.index" - - @require_roles(*TABLE_MANAGER_ROLES) - def get(self): - return self.render("staff/tables/index.html", tables=TABLES) diff --git a/pysite/views/staff/tables/table.py b/pysite/views/staff/tables/table.py deleted file mode 100644 index f47d7793..00000000 --- a/pysite/views/staff/tables/table.py +++ /dev/null @@ -1,63 +0,0 @@ -from math import ceil - -from flask import request -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import TABLE_MANAGER_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin -from pysite.tables import TABLES - - -class TableView(RouteView, DBMixin): - path = "/tables/<table>/<page>" - name = "tables.table" - - @require_roles(*TABLE_MANAGER_ROLES) - def get(self, table, page): - search = request.args.get("search") - search_key = request.args.get("search-key") - - pages = page - obj = TABLES.get(table) - - if not obj: - return NotFound() - - if search: - new_search = f"(?i){search}" # Case-insensitive search - search_key = search_key or obj.primary_key - - query = self.db.query(table).filter(lambda d: d[search_key].match(new_search)) - else: - query = self.db.query(table) - - if page != "all": - try: - page = int(page) - except ValueError: - # Not an integer - return BadRequest() - - count = self.db.run(query.count(), coerce=int) - pages = max(ceil(count / 10), 1) # Pages if we have 10 documents per page, always at least one - - if page < 1 or page > pages: - # If the page is too small or too big, well, that's an error - return BadRequest() - - documents = self.db.run( # Get only the documents for this page - query.skip((page - 1) * 10).limit(10), - coerce=list - ) - else: - documents = self.db.run(query, coerce=list) - - documents = [dict(sorted(d.items())) for d in documents] - - return self.render( - "staff/tables/table.html", - table=table, documents=documents, table_obj=obj, - page=page, pages=pages, search=search, search_key=search_key - ) diff --git a/pysite/views/staff/tables/table_bare.py b/pysite/views/staff/tables/table_bare.py deleted file mode 100644 index abd6cb19..00000000 --- a/pysite/views/staff/tables/table_bare.py +++ /dev/null @@ -1,30 +0,0 @@ -from flask import redirect, request, url_for -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import TABLE_MANAGER_ROLES -from pysite.decorators import require_roles -from pysite.mixins import DBMixin -from pysite.tables import TABLES - - -class TableView(RouteView, DBMixin): - path = "/tables/<table>" - name = "tables.table_bare" - - @require_roles(*TABLE_MANAGER_ROLES) - def get(self, table): - if table not in TABLES: - raise NotFound() - - search = request.args.get("search") - - args = { - "table": table, - "page": 1 - } - - if search is not None: - args["search"] = search - - return redirect(url_for("staff.tables.table", **args)) diff --git a/pysite/views/tests/__init__.py b/pysite/views/tests/__init__.py deleted file mode 100644 index adfc1286..00000000 --- a/pysite/views/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep diff --git a/pysite/views/tests/index.py b/pysite/views/tests/index.py deleted file mode 100644 index f99e3f3c..00000000 --- a/pysite/views/tests/index.py +++ /dev/null @@ -1,23 +0,0 @@ -from flask import jsonify -from schema import Schema - -from pysite.base_route import APIView -from pysite.constants import ValidationTypes -from pysite.decorators import api_params - -LIST_SCHEMA = Schema([{"test": str}]) -DICT_SCHEMA = Schema({"segfault": str}) - - -class TestParamsView(APIView): - path = "/testparams" - name = "testparams" - - @api_params(schema=DICT_SCHEMA, validation_type=ValidationTypes.params) - def get(self, data): - return jsonify(data) - - @api_params(schema=LIST_SCHEMA, validation_type=ValidationTypes.params) - def post(self, data): - jsonified = jsonify(data) - return jsonified diff --git a/pysite/views/wiki/__init__.py b/pysite/views/wiki/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/wiki/__init__.py +++ /dev/null diff --git a/pysite/views/wiki/delete.py b/pysite/views/wiki/delete.py deleted file mode 100644 index 728570a9..00000000 --- a/pysite/views/wiki/delete.py +++ /dev/null @@ -1,64 +0,0 @@ -import datetime - -from flask import redirect, url_for -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import BotEventTypes, CHANNEL_MOD_LOG, EDITOR_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin, RMQMixin - - -class DeleteView(RouteView, DBMixin, RMQMixin): - path = "/delete/<path:page>" # "path" means that it accepts slashes - name = "delete" - table_name = "wiki" - revision_table_name = "wiki_revisions" - - @require_roles(*EDITOR_ROLES) - def get(self, page): - obj = self.db.get(self.table_name, page) - - if obj: - title = obj.get("title", "") - - if obj.get("lock_expiry") and obj.get("lock_user") != self.user_data.get("user_id"): - lock_time = datetime.datetime.fromtimestamp(obj["lock_expiry"]) - if datetime.datetime.utcnow() < lock_time: - return self.render("wiki/page_in_use.html", page=page) - - return self.render("wiki/page_delete.html", page=page, title=title, can_edit=True) - else: - raise NotFound() - - @require_roles(*EDITOR_ROLES) - @csrf - def post(self, page): - obj = self.db.get(self.table_name, page) - - if not obj: - raise NotFound() - - self.db.delete(self.table_name, page) - self.db.delete(self.revision_table_name, page) - - revisions = self.db.filter(self.revision_table_name, lambda revision: revision["slug"] == page) - - for revision in revisions: - self.db.delete(self.revision_table_name, revision["id"]) - - self.audit_log(obj) - - return redirect(url_for("wiki.page", page="home"), code=303) # Redirect, ensuring a GET - - def audit_log(self, obj): - self.rmq_bot_event( - BotEventTypes.send_embed, - { - "target": CHANNEL_MOD_LOG, - "title": f"Page Deletion", - "description": f"**{obj['title']}** was deleted by **{self.user_data.get('username')}**", - "colour": 0x3F8DD7, # Light blue - "timestamp": datetime.datetime.now().isoformat() - } - ) diff --git a/pysite/views/wiki/edit.py b/pysite/views/wiki/edit.py deleted file mode 100644 index 949c9942..00000000 --- a/pysite/views/wiki/edit.py +++ /dev/null @@ -1,149 +0,0 @@ -import datetime -import html -import re - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest - -from pysite.base_route import RouteView -from pysite.constants import BotEventTypes, CHANNEL_MOD_LOG, DEBUG_MODE, EDITOR_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin, RMQMixin -from pysite.rst import render - -STRIP_REGEX = re.compile(r"<[^<]+?>") - - -class EditView(RouteView, DBMixin, RMQMixin): - path = "/edit/<path:page>" # "path" means that it accepts slashes - name = "edit" - table_name = "wiki" - revision_table_name = "wiki_revisions" - - @require_roles(*EDITOR_ROLES) - def get(self, page): - rst = "" - title = "" - preview = "<p>Preview will appear here.</p>" - - obj = self.db.get(self.table_name, page) - - if obj: - rst = obj.get("rst", "") - title = obj.get("title", "") - preview = obj.get("html", preview) - - if obj.get("lock_expiry") and obj.get("lock_user") != self.user_data.get("user_id"): - lock_time = datetime.datetime.fromtimestamp(obj["lock_expiry"]) - if datetime.datetime.utcnow() < lock_time: - return self.render("wiki/page_in_use.html", page=page, can_edit=True) - - lock_expiry = datetime.datetime.utcnow() + datetime.timedelta(minutes=5) - - # There are a couple of cases where we will not need to lock a page. One of these is if the application is - # current set to debug mode. The other of these cases is if the page is empty, because if the page is empty - # we will only have a partially filled out page if the user quits before saving. - if obj: - if not DEBUG_MODE and obj.get("rst"): - self.db.insert( - self.table_name, - { - "slug": page, - "lock_expiry": lock_expiry.timestamp(), - "lock_user": self.user_data.get("user_id") - }, - conflict="update" - ) - - return self.render("wiki/page_edit.html", page=page, rst=rst, title=title, preview=preview, can_edit=True) - - @require_roles(*EDITOR_ROLES) - @csrf - def post(self, page): - rst = request.form.get("rst") - title = request.form["title"] - - if not rst or not rst.strip(): - raise BadRequest() - - if not title or not title.strip(): - raise BadRequest() - - rendered = render(rst) - - obj = { - "slug": page, - "title": request.form["title"], - "rst": rst, - "html": rendered["html"], - "text": html.unescape(STRIP_REGEX.sub("", rendered["html"]).strip()), - "headers": rendered["headers"] - } - - self.db.insert( - self.table_name, - obj, - conflict="replace" - ) - - if not DEBUG_MODE: - # Add the post to the revisions table - revision_payload = { - "slug": page, - "post": obj, - "date": datetime.datetime.utcnow().timestamp(), - "user": self.user_data.get("user_id") - } - - del revision_payload["post"]["slug"] - - current_revisions = self.db.filter(self.revision_table_name, lambda rev: rev["slug"] == page) - sorted_revisions = sorted(current_revisions, key=lambda rev: rev["date"], reverse=True) - - if len(sorted_revisions) > 0: - old_rev = sorted_revisions[0] - else: - old_rev = None - - new_rev = self.db.insert(self.revision_table_name, revision_payload)["generated_keys"][0] - - self.audit_log(page, new_rev, old_rev, obj) - - return redirect(url_for("wiki.page", page=page), code=303) # Redirect, ensuring a GET - - @require_roles(*EDITOR_ROLES) - @csrf - def patch(self, page): - current = self.db.get(self.table_name, page) - if not current: - return "", 404 - - if current.get("lock_expiry"): # If there is a lock present - - # If user patching is not the user with the lock end here - if current["lock_user"] != self.user_data.get("user_id"): - return "", 400 - new_lock = datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # New lock time, 5 minutes in future - self.db.insert(self.table_name, { - "slug": page, - "lock_expiry": new_lock.timestamp() - }, conflict="update") # Update with new lock time - return "", 204 - - def audit_log(self, page, new_id, old_data, new_data): - if not old_data: - link = f"https://wiki.pythondiscord.com/source/{page}" - else: - link = f"https://wiki.pythondiscord.com/history/compare/{old_data['id']}/{new_id}" - - self.rmq_bot_event( - BotEventTypes.send_embed, - { - "target": CHANNEL_MOD_LOG, - "title": "Page Edit", - "description": f"**{new_data['title']}** edited by **{self.user_data.get('username')}**. " - f"[View the diff here]({link})", - "colour": 0x3F8DD7, # Light blue - "timestamp": datetime.datetime.now().isoformat() - } - ) diff --git a/pysite/views/wiki/history/compare.py b/pysite/views/wiki/history/compare.py deleted file mode 100644 index 6411ab30..00000000 --- a/pysite/views/wiki/history/compare.py +++ /dev/null @@ -1,70 +0,0 @@ -import difflib - -from pygments import highlight -from pygments.formatters import HtmlFormatter -from pygments.lexers import DiffLexer -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import DEBUG_MODE, EDITOR_ROLES -from pysite.mixins import DBMixin - - -class CompareView(RouteView, DBMixin): - path = "/history/compare/<string:first_rev>/<string:second_rev>" - name = "history.compare" - - table_name = "wiki_revisions" - table_primary_key = "id" - - def get(self, first_rev, second_rev): - before = self.db.get(self.table_name, first_rev) - after = self.db.get(self.table_name, second_rev) - - if not (before and after): - raise NotFound() - - if before["date"] > after["date"]: # Check whether the before was created after the after - raise BadRequest() - - if before["id"] == after["id"]: # The same revision has been requested - raise BadRequest() - - before_text = before["post"]["rst"] - after_text = after["post"]["rst"] - - if not before_text.endswith("\n"): - before_text += "\n" - - if not after_text.endswith("\n"): - after_text += "\n" - - before_text = before_text.splitlines(keepends=True) - after_text = after_text.splitlines(keepends=True) - - if not before["slug"] == after["slug"]: - raise BadRequest() # The revisions are not from the same post - - diff = difflib.unified_diff(before_text, after_text, fromfile=f"{first_rev}.rst", tofile=f"{second_rev}.rst") - diff = "".join(diff) - diff = highlight(diff, DiffLexer(), HtmlFormatter()) - return self.render("wiki/compare_revision.html", - title=after["post"]["title"], - page=before["slug"], - diff=diff, - slug=before["slug"], - can_edit=self.is_staff()) - - def is_staff(self): - if DEBUG_MODE: - return True - if not self.logged_in: - return False - - roles = self.user_data.get("roles", []) - - for role in roles: - if role in EDITOR_ROLES: - return True - - return False diff --git a/pysite/views/wiki/history/show.py b/pysite/views/wiki/history/show.py deleted file mode 100644 index 00a1dc27..00000000 --- a/pysite/views/wiki/history/show.py +++ /dev/null @@ -1,41 +0,0 @@ -import datetime - -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import DEBUG_MODE, EDITOR_ROLES -from pysite.mixins import DBMixin - - -class RevisionsListView(RouteView, DBMixin): - path = "/history/show/<path:page>" - name = "history.show" - - table_name = "wiki_revisions" - table_primary_key = "id" - - def get(self, page): - results = self.db.filter(self.table_name, lambda revision: revision["slug"] == page) - if len(results) == 0: - raise NotFound() - - for result in results: - ts = datetime.datetime.fromtimestamp(result["date"]) - result["pretty_time"] = ts.strftime("%d %b %Y") - - results = sorted(results, key=lambda revision: revision["date"], reverse=True) - return self.render("wiki/revision_list.html", page=page, revisions=results, can_edit=self.is_staff()), 200 - - def is_staff(self): - if DEBUG_MODE: - return True - if not self.logged_in: - return False - - roles = self.user_data.get("roles", []) - - for role in roles: - if role in EDITOR_ROLES: - return True - - return False diff --git a/pysite/views/wiki/index.py b/pysite/views/wiki/index.py deleted file mode 100644 index 53a4d269..00000000 --- a/pysite/views/wiki/index.py +++ /dev/null @@ -1,8 +0,0 @@ -from pysite.base_route import RedirectView - - -class WikiView(RedirectView): - path = "/" - name = "index" - page = "wiki.page" - kwargs = {"page": "home"} diff --git a/pysite/views/wiki/move.py b/pysite/views/wiki/move.py deleted file mode 100644 index 095a1fdb..00000000 --- a/pysite/views/wiki/move.py +++ /dev/null @@ -1,84 +0,0 @@ -import datetime - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest, NotFound - -from pysite.base_route import RouteView -from pysite.constants import BotEventTypes, CHANNEL_MOD_LOG, EDITOR_ROLES -from pysite.decorators import csrf, require_roles -from pysite.mixins import DBMixin, RMQMixin - - -class MoveView(RouteView, DBMixin, RMQMixin): - path = "/move/<path:page>" # "path" means that it accepts slashes - name = "move" - table_name = "wiki" - revision_table_name = "wiki_revisions" - - @require_roles(*EDITOR_ROLES) - def get(self, page): - obj = self.db.get(self.table_name, page) - - if obj: - title = obj.get("title", "") - - if obj.get("lock_expiry") and obj.get("lock_user") != self.user_data.get("user_id"): - lock_time = datetime.datetime.fromtimestamp(obj["lock_expiry"]) - if datetime.datetime.utcnow() < lock_time: - return self.render("wiki/page_in_use.html", page=page, can_edit=True) - - return self.render("wiki/page_move.html", page=page, title=title, can_edit=True) - else: - raise NotFound() - - @require_roles(*EDITOR_ROLES) - @csrf - def post(self, page): - location = request.form.get("location") - - if not location or not location.strip(): - raise BadRequest() - - obj = self.db.get(self.table_name, page) - - if not obj: - raise NotFound() - - title = obj.get("title", "") - other_obj = self.db.get(self.table_name, location) - - if other_obj: - return self.render( - "wiki/page_move.html", page=page, title=title, - message=f"There's already a page at {location} - please pick a different location" - ) - - self.db.delete(self.table_name, page) - - # Move all revisions for the old slug to the new slug. - revisions = self.db.filter(self.revision_table_name, lambda revision: revision["slug"] == obj["slug"]) - - for revision in revisions: - revision["slug"] = location - self.db.insert(self.revision_table_name, revision, conflict="update") - - obj["slug"] = location - - self.db.insert(self.table_name, obj, conflict="update") - - self.audit_log(obj) - - return redirect(url_for("wiki.page", page=location), code=303) # Redirect, ensuring a GET - - def audit_log(self, obj): - self.rmq_bot_event( - BotEventTypes.send_embed, - { - "target": CHANNEL_MOD_LOG, - "title": "Wiki Page Move", - "description": f"**{obj['title']}** was moved by **{self.user_data.get('username')}** to " - f"**{obj['slug']}**", - "colour": 0x3F8DD7, # Light blue - "timestamp": datetime.datetime.now().isoformat() - } - ) diff --git a/pysite/views/wiki/page.py b/pysite/views/wiki/page.py deleted file mode 100644 index 26edfcc4..00000000 --- a/pysite/views/wiki/page.py +++ /dev/null @@ -1,36 +0,0 @@ -from flask import redirect, url_for -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import DEBUG_MODE, EDITOR_ROLES -from pysite.mixins import DBMixin - - -class PageView(RouteView, DBMixin): - path = "/wiki/<path:page>" # "path" means that it accepts slashes - name = "page" - table_name = "wiki" - - def get(self, page): - obj = self.db.get(self.table_name, page) - - if obj is None: - if self.is_staff(): - return redirect(url_for("wiki.edit", page=page)) - - raise NotFound() - return self.render("wiki/page_view.html", page=page, data=obj, can_edit=self.is_staff()) - - def is_staff(self): - if DEBUG_MODE: - return True - if not self.logged_in: - return False - - roles = self.user_data.get("roles", []) - - for role in roles: - if role in EDITOR_ROLES: - return True - - return False diff --git a/pysite/views/wiki/render.py b/pysite/views/wiki/render.py deleted file mode 100644 index 39bdd133..00000000 --- a/pysite/views/wiki/render.py +++ /dev/null @@ -1,62 +0,0 @@ -import re - -from docutils.utils import SystemMessage -from flask import jsonify -from schema import Schema - -from pysite.base_route import APIView -from pysite.constants import EDITOR_ROLES, ValidationTypes -from pysite.decorators import api_params, csrf, require_roles -from pysite.rst import render - -SCHEMA = Schema([{ - "data": str -}]) - -MESSAGE_REGEX = re.compile(r"<string>:(\d+): \([A-Z]+/\d\) (.*)", flags=re.S) - - -class RenderView(APIView): - path = "/render" # "path" means that it accepts slashes - name = "render" - - @csrf - @require_roles(*EDITOR_ROLES) - @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) - def post(self, data): - if not len(data): - return jsonify({"error": "No data!"}) - - data = data[0]["data"] - try: - html = render(data)["html"] - - return jsonify({"data": html}) - except SystemMessage as e: - lines = str(e) - data = { - "error": lines, - "error_lines": [] - } - - if "\n" in lines: - lines = lines.split("\n") - else: - lines = [lines] - - for message in lines: - match = MESSAGE_REGEX.match(message) - - if match: - data["error_lines"].append( - { - "row": int(match.group(1)) - 3, - "column": 0, - "type": "error", - "text": match.group(2) - } - ) - - return jsonify(data) - except Exception as e: - return jsonify({"error": str(e)}) diff --git a/pysite/views/wiki/robots_txt.py b/pysite/views/wiki/robots_txt.py deleted file mode 100644 index 308fe2a2..00000000 --- a/pysite/views/wiki/robots_txt.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView - - -class RobotsTXT(RouteView): - path = "/robots.txt" - name = "robots_txt" - - def get(self): - return Response( - self.render( - "robots.txt", sitemap_url=url_for("api.sitemap_xml", _external=True) - ), content_type="text/plain" - ) diff --git a/pysite/views/wiki/search.py b/pysite/views/wiki/search.py deleted file mode 100644 index 369da943..00000000 --- a/pysite/views/wiki/search.py +++ /dev/null @@ -1,66 +0,0 @@ -import html -import re - -from flask import redirect, request, url_for -from werkzeug.exceptions import BadRequest - -from pysite.base_route import RouteView -from pysite.decorators import csrf -from pysite.mixins import DBMixin - -STRIP_REGEX = re.compile(r"<[^<]+?>") - - -class SearchView(RouteView, DBMixin): - path = "/search" # "path" means that it accepts slashes - name = "search" - table_name = "wiki" - revision_table_name = "wiki_revisions" - - def get(self): - return self.render("wiki/search.html") - - @csrf - def post(self): - given_query = request.form.get("query") - - if not given_query or not given_query.strip(): - raise BadRequest() - - query = f"({re.escape(given_query)})" - - pages = self.db.filter( - self.table_name, - lambda doc: doc["text"].match(f"(?i){query}") - ) - - if len(pages) == 1: - slug = pages[0]["slug"] - return redirect(url_for("wiki.page", page=slug), code=303) - - for obj in pages: - text = obj["text"] - - matches = re.finditer(query, text, flags=re.IGNORECASE) - snippets = [] - - for match in matches: - start = match.start() - 50 - - if start < 0: - start = 0 - - end = match.end() + 50 - - if end > len(text): - end = len(text) - - match_text = text[start:end] - match_text = re.sub(query, r"<strong>\1</strong>", html.escape(match_text), flags=re.IGNORECASE) - - snippets.append(match_text.replace("\n", "<br />")) - - obj["matches"] = snippets - - pages = sorted(pages, key=lambda d: d["title"]) - return self.render("wiki/search_results.html", pages=pages, query=given_query) diff --git a/pysite/views/wiki/sitemap_xml.py b/pysite/views/wiki/sitemap_xml.py deleted file mode 100644 index 9b7f0980..00000000 --- a/pysite/views/wiki/sitemap_xml.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import Response, url_for - -from pysite.base_route import RouteView -from pysite.mixins import DBMixin - - -class SitemapXML(RouteView, DBMixin): - path = "/sitemap.xml" - name = "sitemap_xml" - table_name = "wiki" - - def get(self): - urls = [] - - for page in self.db.get_all(self.table_name): - urls.append({ - "change_frequency": "weekly", - "type": "url", - "url": url_for("wiki.page", page=page["slug"], _external=True) - }) - - return Response(self.render("sitemap.xml", urls=urls), content_type="application/xml") diff --git a/pysite/views/wiki/source.py b/pysite/views/wiki/source.py deleted file mode 100644 index 83674447..00000000 --- a/pysite/views/wiki/source.py +++ /dev/null @@ -1,42 +0,0 @@ -from flask import redirect, url_for -from pygments import highlight -from pygments.formatters.html import HtmlFormatter -from pygments.lexers import get_lexer_by_name -from werkzeug.exceptions import NotFound - -from pysite.base_route import RouteView -from pysite.constants import DEBUG_MODE, EDITOR_ROLES -from pysite.mixins import DBMixin - - -class PageView(RouteView, DBMixin): - path = "/source/<path:page>" # "path" means that it accepts slashes - name = "source" - table_name = "wiki" - - def get(self, page): - obj = self.db.get(self.table_name, page) - - if obj is None: - if self.is_staff(): - return redirect(url_for("wiki.edit", page=page, can_edit=False)) - - raise NotFound() - - rst = obj["rst"] - rst = highlight(rst, get_lexer_by_name("rst"), HtmlFormatter(preclass="code", linenos="inline")) - return self.render("wiki/page_source.html", page=page, data=obj, rst=rst, can_edit=self.is_staff()) - - def is_staff(self): - if DEBUG_MODE: - return True - if not self.logged_in: - return False - - roles = self.user_data.get("roles", []) - - for role in roles: - if role in EDITOR_ROLES: - return True - - return False diff --git a/pysite/views/wiki/special/__init__.py b/pysite/views/wiki/special/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/wiki/special/__init__.py +++ /dev/null diff --git a/pysite/views/wiki/special/all_pages.py b/pysite/views/wiki/special/all_pages.py deleted file mode 100644 index d2e02a72..00000000 --- a/pysite/views/wiki/special/all_pages.py +++ /dev/null @@ -1,27 +0,0 @@ -from pysite.base_route import RouteView -from pysite.mixins import DBMixin - - -class PageView(RouteView, DBMixin): - path = "/special/all_pages" - name = "special.all_pages" - table_name = "wiki" - - def get(self): - pages = self.db.pluck(self.table_name, "title", "slug") - pages = sorted(pages, key=lambda d: d.get("title", "No Title")) - - letters = {} - - for page in pages: - if "title" not in page: - page["title"] = "No Title" - - letter = page["title"][0].upper() - - if letter not in letters: - letters[letter] = [] - - letters[letter].append(page) - - return self.render("wiki/special_all.html", letters=letters) diff --git a/pysite/views/wiki/special/index.py b/pysite/views/wiki/special/index.py deleted file mode 100644 index ccfc7a5a..00000000 --- a/pysite/views/wiki/special/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from pysite.base_route import TemplateView - - -class PageView(TemplateView): - path = "/special" - name = "special" - template = "wiki/special.html" diff --git a/pysite/views/ws/__init__.py b/pysite/views/ws/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pysite/views/ws/__init__.py +++ /dev/null diff --git a/pysite/views/ws/bot.py b/pysite/views/ws/bot.py deleted file mode 100644 index 816e7579..00000000 --- a/pysite/views/ws/bot.py +++ /dev/null @@ -1,56 +0,0 @@ -import json -import logging - -from geventwebsocket.websocket import WebSocket - -from pysite.constants import BOT_API_KEY -from pysite.mixins import DBMixin -from pysite.websockets import WS - - -class BotWebsocket(WS, DBMixin): - path = "/bot" - name = "ws.bot" - table_name = "bot_events" - - do_changefeed = True - - def __init__(self, socket: WebSocket): - super().__init__(socket) - self.log = logging.getLogger() - - def on_open(self): - self.log.debug("Bot | WS opened.") - - def on_message(self, message): - self.log.debug(f"Bot | Message: {message}") - - try: - message = json.loads(message) - except json.JSONDecodeError: - self.send_json({"error": "Message was not valid JSON"}) - return self.socket.close() - - action = message["action"] - - if action == "login": - if message["key"] != BOT_API_KEY: - return self.socket.close() - - self.do_changefeed = True - - for document in self.db.changes(self.table_name, include_initial=True, include_types=True): - if not self.do_changefeed: - break - - if document["type"] not in ["add", "initial"]: - continue - - self.send_json({"action": "event", "event": document["new_val"]}) - self.db.delete(self.table_name, document["id"]) - - self.send_json({"error": f"Unknown action: {action}"}) - - def on_close(self): - self.log.debug("Bot | WS closed.") - self.do_changefeed = False diff --git a/pysite/views/ws/echo.py b/pysite/views/ws/echo.py deleted file mode 100644 index b6f11168..00000000 --- a/pysite/views/ws/echo.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging - -from geventwebsocket.websocket import WebSocket - -from pysite.websockets import WS - - -class EchoWebsocket(WS): - path = "/echo" - name = "ws.echo" - - def __init__(self, socket: WebSocket): - super().__init__(socket) - self.log = logging.getLogger() - - def on_open(self): - self.log.debug("Echo | WS opened.") - self.send("Hey, welcome!") - - def on_message(self, message): - self.log.debug(f"Echo | Message: {message}") - self.send(message) - - def on_close(self): - self.log.debug("Echo | WS closed.") diff --git a/pysite/views/ws/rst.py b/pysite/views/ws/rst.py deleted file mode 100644 index f2b2db24..00000000 --- a/pysite/views/ws/rst.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging - -from geventwebsocket.websocket import WebSocket - -from pysite.rst import render -from pysite.websockets import WS - - -class RSTWebsocket(WS): - path = "/rst" - name = "ws.rst" - - def __init__(self, socket: WebSocket): - super().__init__(socket) - self.log = logging.getLogger() - - def on_open(self): - self.log.debug("RST | WS opened.") - self.send("Hey, welcome!") - - def on_message(self, message): - self.log.debug(f"RST | Message: {message}") - - try: - data = render(message)["html"] - except Exception as e: - self.log.exception("Parsing error") - data = str(e) - - self.send(data) - - def on_close(self): - self.log.debug("RST | WS closed.") |