aboutsummaryrefslogtreecommitdiffstats
path: root/pysite
diff options
context:
space:
mode:
Diffstat (limited to 'pysite')
-rw-r--r--pysite/constants.py3
-rw-r--r--pysite/decorators.py4
-rw-r--r--pysite/migrations/tables/code_jam_teams/__init__.py0
-rw-r--r--pysite/migrations/tables/code_jam_teams/v1.py13
-rw-r--r--pysite/migrations/tables/code_jam_teams/v2.py13
-rw-r--r--pysite/migrations/tables/code_jams/v2.py10
-rw-r--r--pysite/tables.py12
-rw-r--r--pysite/views/api/bot/user.py100
-rw-r--r--pysite/views/api/bot/user_complete.py143
-rw-r--r--pysite/views/main/jams/index.py11
-rw-r--r--pysite/views/main/jams/jam_team_list.py42
-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/staff/jams/actions.py27
-rw-r--r--pysite/views/staff/jams/edit_info.py2
-rw-r--r--pysite/views/staff/jams/teams/view.py14
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")
}
)