diff options
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.")  |