diff options
Diffstat (limited to '')
| -rw-r--r-- | pysite/constants.py | 2 | ||||
| -rw-r--r-- | pysite/tables.py | 3 | ||||
| -rw-r--r-- | pysite/views/main/jams/index.py | 11 | ||||
| -rw-r--r-- | pysite/views/main/jams/jam_team_list.py | 50 | ||||
| -rw-r--r-- | pysite/views/main/jams/team_edit_repo.py | 154 | ||||
| -rw-r--r-- | pysite/views/main/jams/team_view.py | 51 | ||||
| -rw-r--r-- | pysite/views/main/jams/user_team_list.py | 40 | ||||
| -rw-r--r-- | pysite/views/staff/jams/actions.py | 6 | ||||
| -rw-r--r-- | pysite/views/staff/jams/teams/view.py | 14 | 
9 files changed, 315 insertions, 16 deletions
| diff --git a/pysite/constants.py b/pysite/constants.py index 016ab9c5..fa5dbab2 100644 --- a/pysite/constants.py +++ b/pysite/constants.py @@ -53,6 +53,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/tables.py b/pysite/tables.py index cf38a698..191b52bd 100644 --- a/pysite/tables.py +++ b/pysite/tables.py @@ -93,7 +93,8 @@ TABLES = {          keys=sorted([              "id",  # uuid              "name",  # str -            "members"  # list[str] +            "members",  # list[str] +            "repo"  # str          ])      ), diff --git a/pysite/views/main/jams/index.py b/pysite/views/main/jams/index.py index 8d34fa50..abe2859f 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..0eb43a5a --- /dev/null +++ b/pysite/views/main/jams/jam_team_list.py @@ -0,0 +1,50 @@ +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 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): +        try: +            query = self.db.query(self.jams_table).get(jam_id).merge( +                lambda jam_obj: { +                    "teams": +                        self.db.query(self.table_name) +                            .filter(lambda team_row: jam_obj["teams"].contains(team_row["id"])) +                            .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_data = self.db.run(query) +        except ReqlNonExistenceError: +            log.exception("Failed RethinkDB query") +            raise NotFound() + +        return self.render( +            "main/jams/team_list.html", +            jam=jam_data, +            teams=jam_data["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..03f23f17 --- /dev/null +++ b/pysite/views/main/jams/team_edit_repo.py @@ -0,0 +1,154 @@ +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").filter( +                            lambda jam: jam["teams"].contains(team["id"]) +                        ).coerce_to("array")[0] +                } +            ) + +            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..2d99828c --- /dev/null +++ b/pysite/views/main/jams/team_view.py @@ -0,0 +1,51 @@ +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").filter( +                            lambda jam: jam["teams"].contains(team["id"]) +                        ).coerce_to("array")[0] +                } +            ) + +            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 +        ) 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..272c0a74 --- /dev/null +++ b/pysite/views/main/jams/user_team_list.py @@ -0,0 +1,40 @@ +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").filter( +                        lambda jam: jam["teams"].contains(team["id"]) +                    ).coerce_to("array")[0] +            } +        ).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 de9e2d2c..3f8b4c20 100644 --- a/pysite/views/staff/jams/actions.py +++ b/pysite/views/staff/jams/actions.py @@ -335,10 +335,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"])                                  .coerce_to("array")                      }                  ) 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")                  }              ) | 
