diff options
Diffstat (limited to 'pysite')
-rw-r--r-- | pysite/constants.py | 3 | ||||
-rw-r--r-- | pysite/decorators.py | 4 | ||||
-rw-r--r-- | pysite/migrations/tables/code_jam_teams/__init__.py | 0 | ||||
-rw-r--r-- | pysite/migrations/tables/code_jam_teams/v1.py | 13 | ||||
-rw-r--r-- | pysite/migrations/tables/code_jam_teams/v2.py | 13 | ||||
-rw-r--r-- | pysite/migrations/tables/code_jams/v2.py | 10 | ||||
-rw-r--r-- | pysite/tables.py | 12 | ||||
-rw-r--r-- | pysite/views/api/bot/user.py | 100 | ||||
-rw-r--r-- | pysite/views/api/bot/user_complete.py | 143 | ||||
-rw-r--r-- | pysite/views/main/jams/index.py | 11 | ||||
-rw-r--r-- | pysite/views/main/jams/jam_team_list.py | 42 | ||||
-rw-r--r-- | pysite/views/main/jams/team_edit_repo.py | 151 | ||||
-rw-r--r-- | pysite/views/main/jams/team_view.py | 53 | ||||
-rw-r--r-- | pysite/views/main/jams/user_team_list.py | 37 | ||||
-rw-r--r-- | pysite/views/staff/jams/actions.py | 27 | ||||
-rw-r--r-- | pysite/views/staff/jams/edit_info.py | 2 | ||||
-rw-r--r-- | pysite/views/staff/jams/teams/view.py | 14 |
17 files changed, 525 insertions, 110 deletions
diff --git a/pysite/constants.py b/pysite/constants.py index 016ab9c5..e6d618c0 100644 --- a/pysite/constants.py +++ b/pysite/constants.py @@ -14,6 +14,7 @@ class ErrorCodes(IntEnum): class ValidationTypes(Enum): json = "json" + none = "none" params = "params" @@ -53,6 +54,8 @@ DISCORD_OAUTH_SECRET = environ.get('DISCORD_OAUTH_SECRET', '') DISCORD_OAUTH_SCOPE = 'identify' OAUTH_DATABASE = "oauth_data" +GITLAB_ACCESS_TOKEN = environ.get("GITLAB_ACCESS_TOKEN", '') + PREFERRED_URL_SCHEME = environ.get("PREFERRED_URL_SCHEME", "http") ERROR_DESCRIPTIONS = { diff --git a/pysite/decorators.py b/pysite/decorators.py index 1d840ac7..de914c6f 100644 --- a/pysite/decorators.py +++ b/pysite/decorators.py @@ -65,7 +65,7 @@ def api_key(f): def api_params( - schema: Schema, + schema: Schema = None, validation_type: ValidationTypes = ValidationTypes.json, allow_duplicate_params: bool = False): """ @@ -133,6 +133,8 @@ def api_params( if len(value) > 1: raise BadRequest("This view does not allow duplicate query arguments") data = request.args.to_dict() + elif validation_type == ValidationTypes.none: + return f(self, None, *args, **kwargs) else: raise ValueError(f"Unknown validation type: {validation_type}") # pragma: no cover diff --git a/pysite/migrations/tables/code_jam_teams/__init__.py b/pysite/migrations/tables/code_jam_teams/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pysite/migrations/tables/code_jam_teams/__init__.py diff --git a/pysite/migrations/tables/code_jam_teams/v1.py b/pysite/migrations/tables/code_jam_teams/v1.py new file mode 100644 index 00000000..165d3100 --- /dev/null +++ b/pysite/migrations/tables/code_jam_teams/v1.py @@ -0,0 +1,13 @@ +def run(db, table, table_obj): + """ + Associate the ID of each team's code jam (team -> jam) + """ + + for document in db.get_all(table): + if "jam" not in document: + # find the code jam containing this team + for jam in db.get_all("code_jams"): + if document["id"] in jam["teams"]: + document["jam"] = jam["number"] + db.insert(table, document, conflict="update", durability="soft") + db.sync(table) diff --git a/pysite/migrations/tables/code_jam_teams/v2.py b/pysite/migrations/tables/code_jam_teams/v2.py new file mode 100644 index 00000000..c6d7c972 --- /dev/null +++ b/pysite/migrations/tables/code_jam_teams/v2.py @@ -0,0 +1,13 @@ +def run(db, table, table_obj): + """ + Associate the ID of each team's code jam (team -> jam) - again + """ + + for document in db.get_all(table): + if "jam" not in document: + # find the code jam containing this team + for jam in db.get_all("code_jams"): + if document["id"] in jam["teams"]: + document["jam"] = jam["number"] + db.insert(table, document, conflict="update", durability="soft") + db.sync(table) diff --git a/pysite/migrations/tables/code_jams/v2.py b/pysite/migrations/tables/code_jams/v2.py new file mode 100644 index 00000000..df4752c8 --- /dev/null +++ b/pysite/migrations/tables/code_jams/v2.py @@ -0,0 +1,10 @@ +def run(db, table, table_obj): + """ + Clean list of teams from teams that do not exist anymore. + """ + for document in db.get_all(table): + for team_id in document["teams"]: + if db.get("code_jam_teams", team_id) is None: + document["teams"].remove(team_id) + db.insert(table, document, conflict="update", durability="soft") + db.sync(table) diff --git a/pysite/tables.py b/pysite/tables.py index cf38a698..de9499e8 100644 --- a/pysite/tables.py +++ b/pysite/tables.py @@ -93,7 +93,9 @@ TABLES = { keys=sorted([ "id", # uuid "name", # str - "members" # list[str] + "members", # list[str] + "repo", # str + "jam" # int ]) ), @@ -117,6 +119,14 @@ TABLES = { ]) ), + "member_chunks": Table( + primary_key="id", + keys=sorted([ + "id", # str + "chunk", # list + ]) + ), + "oauth_data": Table( # OAuth login information primary_key="id", keys=sorted([ diff --git a/pysite/views/api/bot/user.py b/pysite/views/api/bot/user.py index ced39306..189dd1f8 100644 --- a/pysite/views/api/bot/user.py +++ b/pysite/views/api/bot/user.py @@ -4,7 +4,7 @@ from flask import jsonify, request from schema import Optional, Schema from pysite.base_route import APIView -from pysite.constants import ValidationTypes +from pysite.constants import ErrorCodes, ValidationTypes from pysite.decorators import api_key, api_params from pysite.mixins import DBMixin @@ -35,12 +35,14 @@ BANNABLE_STATES = ("preparing", "running") class UserView(APIView, DBMixin): path = "/bot/users" name = "bot.users" - table_name = "users" - oauth_table_name = "oauth_data" - participants_table = "code_jam_participants" + + 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 @@ -48,94 +50,12 @@ class UserView(APIView, DBMixin): def post(self, data): logging.getLogger(__name__).debug(f"Size of request: {len(request.data)} bytes") - 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 not data: + return self.error(ErrorCodes.bad_data_format, "No users supplied") - if jam_obj: - if jam_obj["state"] in BANNABLE_STATES: - banned = True + self.db.insert(self.chunks_table, {"chunk": data}) - 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 + return jsonify({"success": True}) # pragma: no cover @api_key @api_params(schema=SCHEMA, validation_type=ValidationTypes.json) diff --git a/pysite/views/api/bot/user_complete.py b/pysite/views/api/bot/user_complete.py new file mode 100644 index 00000000..877eee34 --- /dev/null +++ b/pysite/views/api/bot/user_complete.py @@ -0,0 +1,143 @@ +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/main/jams/index.py b/pysite/views/main/jams/index.py index dd75c3a9..652779b2 100644 --- a/pysite/views/main/jams/index.py +++ b/pysite/views/main/jams/index.py @@ -9,10 +9,21 @@ class JamsIndexView(RouteView, DBMixin): 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) ) diff --git a/pysite/views/main/jams/jam_team_list.py b/pysite/views/main/jams/jam_team_list.py new file mode 100644 index 00000000..49587e36 --- /dev/null +++ b/pysite/views/main/jams/jam_team_list.py @@ -0,0 +1,42 @@ +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() + + query = self.db.query(self.table_name).get_all(self.table_name, *jam_obj["teams"]).pluck( + ["id", "name", "members", "repo"]).merge( + lambda team: { + "members": + self.db.query("users") + .filter(lambda user: team["members"].contains(user["user_id"])) + .coerce_to("array") + }).coerce_to("array") + + jam_obj["teams"] = self.db.run(query) + + 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/team_edit_repo.py b/pysite/views/main/jams/team_edit_repo.py new file mode 100644 index 00000000..03e752bc --- /dev/null +++ b/pysite/views/main/jams/team_edit_repo.py @@ -0,0 +1,151 @@ +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 new file mode 100644 index 00000000..6b5d86ce --- /dev/null +++ b/pysite/views/main/jams/team_view.py @@ -0,0 +1,53 @@ +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 new file mode 100644 index 00000000..226cc4b0 --- /dev/null +++ b/pysite/views/main/jams/user_team_list.py @@ -0,0 +1,37 @@ +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/staff/jams/actions.py b/pysite/views/staff/jams/actions.py index 3cacbb50..761ba7d6 100644 --- a/pysite/views/staff/jams/actions.py +++ b/pysite/views/staff/jams/actions.py @@ -47,7 +47,11 @@ class ActionView(APIView, DBMixin, RMQMixin): @csrf @require_roles(*ALL_STAFF_ROLES) def post(self): - action = request.form.get("action") + 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) @@ -217,7 +221,8 @@ class ActionView(APIView, DBMixin, RMQMixin): team = { "name": f"{adjective} {noun}".title(), - "members": [] + "members": [], + "jam": jam } result = self.db.insert(self.teams_table, team) @@ -331,10 +336,8 @@ class ActionView(APIView, DBMixin, RMQMixin): .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() + .filter(lambda team_row: jam_obj["teams"].contains(team_row["id"])) + .pluck(["id", "name", "members", "jam"]) .coerce_to("array") } ) @@ -536,6 +539,18 @@ class ActionView(APIView, DBMixin, RMQMixin): 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/edit_info.py b/pysite/views/staff/jams/edit_info.py index ad0d3d41..4944ae67 100644 --- a/pysite/views/staff/jams/edit_info.py +++ b/pysite/views/staff/jams/edit_info.py @@ -8,7 +8,7 @@ from pysite.mixins import DBMixin from pysite.rst import render REQUIRED_KEYS = ["info_rst", "repo", "task_rst", "theme"] -ALLOWED_STATES = ["planning", "announced", "finished"] +ALLOWED_STATES = ["planning", "announced", "preparing", "finished"] class StaffView(RouteView, DBMixin): diff --git a/pysite/views/staff/jams/teams/view.py b/pysite/views/staff/jams/teams/view.py index 6b30f9a2..662cc084 100644 --- a/pysite/views/staff/jams/teams/view.py +++ b/pysite/views/staff/jams/teams/view.py @@ -61,18 +61,10 @@ class StaffView(RouteView, DBMixin): .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": - # Query the teams table self.db.query(self.table_name) - # Join the teams table against the jams table, to find all of the teams for this - # specific jam - we can't simply filter because of the one-to-many relationship, - # so we must use an inner join with a predicate function. This function is still - # run on the server, however - .inner_join(self.db.query(self.jams_table), - lambda team_row, jams_row: jams_row["teams"].contains(team_row["id"])) - # Only take the ID, name and members of each team, discard everything else - .pluck({"left": ["id", "name", "members"]}) - .zip() # Combine the left and right documents together - .coerce_to("array") # Coerce the document stream into an array + .filter(lambda team_row: jam_obj["teams"].contains(team_row["id"])) + .pluck(["id", "name", "members"]) + .coerce_to("array") } ) |