aboutsummaryrefslogtreecommitdiffstats
path: root/pysite
diff options
context:
space:
mode:
Diffstat (limited to 'pysite')
-rw-r--r--pysite/tables.py6
-rw-r--r--pysite/utils/words.py73
-rw-r--r--pysite/views/api/bot/user.py24
-rw-r--r--pysite/views/staff/jams/actions.py229
-rw-r--r--pysite/views/staff/jams/teams/__init__.py0
-rw-r--r--pysite/views/staff/jams/teams/view.py106
6 files changed, 428 insertions, 10 deletions
diff --git a/pysite/tables.py b/pysite/tables.py
index ad864fee..cf38a698 100644
--- a/pysite/tables.py
+++ b/pysite/tables.py
@@ -49,10 +49,10 @@ TABLES = {
"state", # str
"task_html", # str
"task_rst", # str
- "teams", # list[int]
+ "teams", # list[str]
"theme", # str
"title", # str
- "winners" # list[int]
+ "winners" # list[str]
])
),
@@ -93,7 +93,7 @@ TABLES = {
keys=sorted([
"id", # uuid
"name", # str
- "members" # list[int]
+ "members" # list[str]
])
),
diff --git a/pysite/utils/words.py b/pysite/utils/words.py
new file mode 100644
index 00000000..0959ec3e
--- /dev/null
+++ b/pysite/utils/words.py
@@ -0,0 +1,73 @@
+import random
+from typing import Iterator, List, Tuple
+
+adjectives = (
+ "abortive", "abounding", "abrasive", "absent", "acceptable", "adamant", "adhesive", "adjoining", "aggressive",
+ "alike", "alleged", "aloof", "ambitious", "amused", "aspiring", "available", "awake", "axiomatic", "barbarous",
+ "bashful", "beautiful", "befitting", "beneficial", "blushing", "boundless", "brawny", "certain", "childlike",
+ "cluttered", "courageous", "crooked", "damp", "deadpan", "debonair", "decorous", "defiant", "delirious",
+ "detailed", "disturbed", "divergent", "drab", "dramatic", "drunk", "electric", "enormous", "erect", "evanescent",
+ "excellent", "exultant", "faded", "famous", "far-flung", "fascinated", "faulty", "festive", "fine", "fixed",
+ "flaky", "flat", "fluttering", "foregoing", "frail", "fresh", "frightened", "funny", "furtive", "gainful", "glib",
+ "godly", "half", "hallowed", "handsome", "hard", "heavenly", "hesitant", "high", "honorable", "hot", "hungry",
+ "hurt", "hushed", "hypnotic", "ill-fated", "illegal", "important", "incompetent", "inconclusive", "infamous",
+ "innocent", "insidious", "instinctive", "jazzy", "jumbled", "kind", "knowing", "late", "laughable", "lean",
+ "loving", "madly", "majestic", "married", "materialistic", "measly", "mighty", "misty", "murky", "mushy",
+ "mysterious", "needy", "next", "nice", "nondescript", "nutritious", "omniscient", "ossified", "overconfident",
+ "panoramic", "parallel", "parched", "pastoral", "plant", "possible", "pricey", "prickly", "private", "productive",
+ "pumped", "purple", "purring", "quixotic", "rabid", "rare", "real", "receptive", "resolute", "right", "rightful",
+ "ritzy", "rough", "ruddy", "rude", "salty", "sassy", "satisfying", "scandalous", "sedate", "selective", "separate",
+ "shrill", "sincere", "slow", "small", "smooth", "sordid", "sour", "spicy", "spiky", "spiteful", "spooky" "spotty",
+ "steady", "subdued", "successful", "supreme", "sweltering", "synonymous", "talented", "tasty", "teeny", "telling",
+ "temporary", "tender", "tense", "tenuous", "thinkable", "thoughtless", "tiny", "tough", "trashy", "two",
+ "uncovered", "uninterested", "unruly", "unsuitable", "used", "useful", "vagabond", "verdant", "vivacious",
+ "voiceless", "waggish", "wasteful", "wealthy", "whole", "wise", "woebegone", "workable", "wrong", "young",
+)
+
+nouns = (
+ "actions", "activities", "additions", "advertisements", "afterthoughts", "airplanes", "amounts", "angles", "ants",
+ "baskets", "baths", "battles", "bees", "beginners", "behaviors", "beliefs", "bells", "berries", "bikes",
+ "birthdays", "bits", "boats", "boys", "breaths", "bubbles", "bulbs", "bursts", "butter", "cables", "camps", "cans",
+ "captions", "cars", "carpenters", "cats", "cemeteries", "changes", "channels", "chickens", "classes", "clubs",
+ "committees", "covers", "cracks", "crates", "crayons", "crowds", "decisions", "degrees", "details", "directions",
+ "dresses", "drops", "dusts", "errors", "examples", "expansions", "falls", "fangs", "feelings", "firemen",
+ "flowers", "fog", "feet", "fowls", "frogs", "glasses", "gloves", "grandmothers", "grounds", "guns", "haircuts",
+ "halls", "harmonies", "hats", "hopes", "horns", "horses", "ideas", "inks", "insects", "interests", "inventions",
+ "irons", "islands", "jails", "jeans", "jellyfish", "laborers", "lakes", "letters", "lockets", "matches", "measures",
+ "mice", "milk", "motions", "moves", "nerves", "numbers", "pans", "pancakes", "persons", "pets", "pickles", "pies",
+ "pizzas", "plantations", "plastics", "ploughs", "pockets", "potatoes", "powders", "properties", "reactions",
+ "regrets", "riddles", "rivers", "rocks", "sails", "scales", "scarecrows", "scarves", "scenes", "schools",
+ "sciences", "shakes", "shapes", "shirts", "silvers", "sinks", "snakes", "sneezes", "sofas", "songs", "sounds",
+ "spades", "sparks", "stages", "stamps", "stars", "stations", "stews", "stomachs", "suggestions", "suits", "swings",
+ "tables", "tents", "territories", "tests", "textures", "things", "thoughts", "threads", "tigers", "toads", "toes",
+ "tomatoes", "trains", "treatments", "troubles", "tubs", "turkeys", "umbrellas", "uncles", "vacations", "veils",
+ "voices", "volcanoes", "volleyballs", "walls", "wars", "waters", "waves", "wilderness", "women", "words", "works",
+ "worms", "wounds", "writings", "yams", "yards", "yarns", "zebras"
+)
+
+
+def get_adjectives(num: int = 1) -> List[str]:
+ """
+ Get a list of random, unique adjectives
+ """
+
+ return random.sample(adjectives, num)
+
+
+def get_nouns(num: int = 1) -> List[str]:
+ """
+ Get a list of random, unique nouns
+ """
+
+ return random.sample(nouns, num)
+
+
+def get_word_pairs(num: int = 1) -> Iterator[Tuple[str, str]]:
+ """
+ Get an iterator over random, unique (adjective, noun) pairs
+ """
+
+ return zip(
+ get_adjectives(num),
+ get_nouns(num)
+ )
diff --git a/pysite/views/api/bot/user.py b/pysite/views/api/bot/user.py
index d9efb829..ced39306 100644
--- a/pysite/views/api/bot/user.py
+++ b/pysite/views/api/bot/user.py
@@ -41,6 +41,7 @@ class UserView(APIView, DBMixin):
infractions_table = "code_jam_infractions"
jams_table = "code_jams"
responses_table = "code_jam_responses"
+ teams_table = "code_jam_teams"
@api_key
@api_params(schema=SCHEMA, validation_type=ValidationTypes.json)
@@ -92,6 +93,16 @@ class UserView(APIView, DBMixin):
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, {
@@ -116,6 +127,7 @@ class UserView(APIView, DBMixin):
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
@@ -176,6 +188,18 @@ class UserView(APIView, DBMixin):
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, {
diff --git a/pysite/views/staff/jams/actions.py b/pysite/views/staff/jams/actions.py
index 24b31ce9..3cacbb50 100644
--- a/pysite/views/staff/jams/actions.py
+++ b/pysite/views/staff/jams/actions.py
@@ -1,19 +1,22 @@
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 = [
+GET_ACTIONS = ("questions",)
+POST_ACTIONS = (
"associate_question", "disassociate_question", "infraction", "questions", "state", "approve_application",
- "unapprove_application"
-]
-DELETE_ACTIONS = ["infraction", "question"]
-KEYS = ["action"]
+ "unapprove_application", "create_team", "generate_teams", "set_team_member",
+ "reroll_team"
+)
+DELETE_ACTIONS = ("infraction", "question", "team")
-QUESTION_KEYS = ["optional", "title", "type"]
+KEYS = ("action",)
+QUESTION_KEYS = ("optional", "title", "type")
class ActionView(APIView, DBMixin, RMQMixin):
@@ -25,6 +28,8 @@ class ActionView(APIView, DBMixin, RMQMixin):
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)
@@ -192,6 +197,204 @@ class ActionView(APIView, DBMixin, RMQMixin):
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": []
+ }
+
+ 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)
+ .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 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 == "approve_application":
app = request.form.get("id")
@@ -324,3 +527,15 @@ class ActionView(APIView, DBMixin, RMQMixin):
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"
+ )
+
+ self.db.delete(self.teams_table, team)
+
+ return jsonify({"result": True})
diff --git a/pysite/views/staff/jams/teams/__init__.py b/pysite/views/staff/jams/teams/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pysite/views/staff/jams/teams/__init__.py
diff --git a/pysite/views/staff/jams/teams/view.py b/pysite/views/staff/jams/teams/view.py
new file mode 100644
index 00000000..c68da63e
--- /dev/null
+++ b/pysite/views/staff/jams/teams/view.py
@@ -0,0 +1,106 @@
+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")
+
+
+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":
+ # 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
+ }
+ )
+
+ jam_data = self.db.run(query)
+ except ReqlNonExistenceError:
+ 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
+ )