diff options
| author | 2018-06-17 21:31:00 +0000 | |
|---|---|---|
| committer | 2018-06-17 21:31:00 +0000 | |
| commit | dc2a5013fb93141f50dc92b4b8cc4d6004b8e814 (patch) | |
| tree | 07a3c381d9b58890c14049f8a23e8154b33e0003 /pysite | |
| parent | [Wiki] Quick-fix for broken editor (diff) | |
Backend for team assignment
Diffstat (limited to 'pysite')
| -rw-r--r-- | pysite/tables.py | 6 | ||||
| -rw-r--r-- | pysite/utils/words.py | 73 | ||||
| -rw-r--r-- | pysite/views/api/bot/user.py | 24 | ||||
| -rw-r--r-- | pysite/views/staff/jams/actions.py | 229 | ||||
| -rw-r--r-- | pysite/views/staff/jams/teams/__init__.py | 0 | ||||
| -rw-r--r-- | pysite/views/staff/jams/teams/view.py | 106 | 
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 +        ) | 
