aboutsummaryrefslogtreecommitdiffstats
path: root/pysite/views
diff options
context:
space:
mode:
authorGravatar Gareth Coles <[email protected]>2018-08-07 15:09:08 +0100
committerGravatar Gareth Coles <[email protected]>2018-08-07 15:09:16 +0100
commitaf54db6c136138c66cf5ca72419989525a0baa5c (patch)
tree8519aeab8d45277c51797c7dc23aacf3b56ed1bb /pysite/views
parentA wizard is never late, nor is he early. (diff)
Initial project layout for django
Diffstat (limited to 'pysite/views')
-rw-r--r--pysite/views/__init__.py0
-rw-r--r--pysite/views/api/__init__.py0
-rw-r--r--pysite/views/api/bot/__init__.py0
-rw-r--r--pysite/views/api/bot/bigbrother.py118
-rw-r--r--pysite/views/api/bot/clean.py48
-rw-r--r--pysite/views/api/bot/doc.py98
-rw-r--r--pysite/views/api/bot/hiphopify.py170
-rw-r--r--pysite/views/api/bot/infractions.py572
-rw-r--r--pysite/views/api/bot/off_topic_names.py108
-rw-r--r--pysite/views/api/bot/settings.py56
-rw-r--r--pysite/views/api/bot/snake_cog/__init__.py0
-rw-r--r--pysite/views/api/bot/snake_cog/snake_facts.py28
-rw-r--r--pysite/views/api/bot/snake_cog/snake_idioms.py28
-rw-r--r--pysite/views/api/bot/snake_cog/snake_names.py48
-rw-r--r--pysite/views/api/bot/snake_cog/snake_quiz.py28
-rw-r--r--pysite/views/api/bot/snake_cog/special_snakes.py28
-rw-r--r--pysite/views/api/bot/tags.py107
-rw-r--r--pysite/views/api/bot/user.py166
-rw-r--r--pysite/views/api/bot/user_complete.py143
-rw-r--r--pysite/views/api/error_view.py40
-rw-r--r--pysite/views/api/healthcheck.py11
-rw-r--r--pysite/views/api/index.py10
-rw-r--r--pysite/views/api/robots_txt.py15
-rw-r--r--pysite/views/api/sitemap_xml.py11
-rw-r--r--pysite/views/error_handlers/http_4xx.py31
-rw-r--r--pysite/views/error_handlers/http_5xx.py41
-rw-r--r--pysite/views/main/__init__.py0
-rw-r--r--pysite/views/main/abort.py11
-rw-r--r--pysite/views/main/about/__init__.py0
-rw-r--r--pysite/views/main/about/channels.py7
-rw-r--r--pysite/views/main/about/index.py7
-rw-r--r--pysite/views/main/about/partners.py19
-rw-r--r--pysite/views/main/about/privacy.py7
-rw-r--r--pysite/views/main/about/rules.py7
-rw-r--r--pysite/views/main/auth/__init__.py0
-rw-r--r--pysite/views/main/auth/done.py18
-rw-r--r--pysite/views/main/bot/cleanlog.py35
-rw-r--r--pysite/views/main/error.py14
-rw-r--r--pysite/views/main/index.py7
-rw-r--r--pysite/views/main/info/__init__.py0
-rw-r--r--pysite/views/main/info/faq.py7
-rw-r--r--pysite/views/main/info/help.py7
-rw-r--r--pysite/views/main/info/index.py7
-rw-r--r--pysite/views/main/info/jams.py7
-rw-r--r--pysite/views/main/info/resources.py58
-rw-r--r--pysite/views/main/jams/__init__.py0
-rw-r--r--pysite/views/main/jams/index.py52
-rw-r--r--pysite/views/main/jams/info.py7
-rw-r--r--pysite/views/main/jams/jam_team_list.py45
-rw-r--r--pysite/views/main/jams/join.py247
-rw-r--r--pysite/views/main/jams/profile.py71
-rw-r--r--pysite/views/main/jams/retract.py83
-rw-r--r--pysite/views/main/jams/team_edit_repo.py151
-rw-r--r--pysite/views/main/jams/team_view.py53
-rw-r--r--pysite/views/main/jams/user_team_list.py37
-rw-r--r--pysite/views/main/logout.py16
-rw-r--r--pysite/views/main/redirects/__init__.py0
-rw-r--r--pysite/views/main/redirects/github.py8
-rw-r--r--pysite/views/main/redirects/gitlab.py8
-rw-r--r--pysite/views/main/redirects/invite.py8
-rw-r--r--pysite/views/main/redirects/stats.py8
-rw-r--r--pysite/views/main/robots_txt.py15
-rw-r--r--pysite/views/main/sitemap_xml.py69
-rw-r--r--pysite/views/main/ws_test.py14
-rw-r--r--pysite/views/main/ws_test_rst.py14
-rw-r--r--pysite/views/staff/__init__.py0
-rw-r--r--pysite/views/staff/index.py31
-rw-r--r--pysite/views/staff/jams/__init__.py0
-rw-r--r--pysite/views/staff/jams/actions.py597
-rw-r--r--pysite/views/staff/jams/create.py61
-rw-r--r--pysite/views/staff/jams/edit_basics.py55
-rw-r--r--pysite/views/staff/jams/edit_ending.py54
-rw-r--r--pysite/views/staff/jams/edit_info.py55
-rw-r--r--pysite/views/staff/jams/forms/__init__.py0
-rw-r--r--pysite/views/staff/jams/forms/preamble_edit.py45
-rw-r--r--pysite/views/staff/jams/forms/questions_edit.py75
-rw-r--r--pysite/views/staff/jams/forms/questions_view.py22
-rw-r--r--pysite/views/staff/jams/forms/view.py46
-rw-r--r--pysite/views/staff/jams/index.py15
-rw-r--r--pysite/views/staff/jams/infractions/__init__.py0
-rw-r--r--pysite/views/staff/jams/infractions/view.py29
-rw-r--r--pysite/views/staff/jams/participants.py56
-rw-r--r--pysite/views/staff/jams/teams/__init__.py0
-rw-r--r--pysite/views/staff/jams/teams/view.py102
-rw-r--r--pysite/views/staff/render.py62
-rw-r--r--pysite/views/staff/robots_txt.py15
-rw-r--r--pysite/views/staff/sitemap_xml.py11
-rw-r--r--pysite/views/staff/tables/__init__.py0
-rw-r--r--pysite/views/staff/tables/edit.py110
-rw-r--r--pysite/views/staff/tables/index.py13
-rw-r--r--pysite/views/staff/tables/table.py63
-rw-r--r--pysite/views/staff/tables/table_bare.py30
-rw-r--r--pysite/views/tests/__init__.py1
-rw-r--r--pysite/views/tests/index.py23
-rw-r--r--pysite/views/wiki/__init__.py0
-rw-r--r--pysite/views/wiki/delete.py64
-rw-r--r--pysite/views/wiki/edit.py149
-rw-r--r--pysite/views/wiki/history/compare.py70
-rw-r--r--pysite/views/wiki/history/show.py41
-rw-r--r--pysite/views/wiki/index.py8
-rw-r--r--pysite/views/wiki/move.py84
-rw-r--r--pysite/views/wiki/page.py36
-rw-r--r--pysite/views/wiki/render.py62
-rw-r--r--pysite/views/wiki/robots_txt.py15
-rw-r--r--pysite/views/wiki/search.py66
-rw-r--r--pysite/views/wiki/sitemap_xml.py22
-rw-r--r--pysite/views/wiki/source.py42
-rw-r--r--pysite/views/wiki/special/__init__.py0
-rw-r--r--pysite/views/wiki/special/all_pages.py27
-rw-r--r--pysite/views/wiki/special/index.py7
-rw-r--r--pysite/views/ws/__init__.py0
-rw-r--r--pysite/views/ws/bot.py56
-rw-r--r--pysite/views/ws/echo.py25
-rw-r--r--pysite/views/ws/rst.py33
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.")