aboutsummaryrefslogtreecommitdiffstats
path: root/pysite
diff options
context:
space:
mode:
authorGravatar Gareth Coles <[email protected]>2018-06-23 19:00:07 +0000
committerGravatar Gareth Coles <[email protected]>2018-06-23 19:00:07 +0000
commit90b9ebfe6b59a69e9783259a70e7927d989604f8 (patch)
treea6b796051cb7b7811ce8528b2b4501278422591e /pysite
parentUpdate Font-Awesome Pro to 5.1.0 (diff)
parentAdd some documentation to the team_edit_repo route (diff)
Merge branch 'teams-user-frontend' into 'master'
User frontend for teams See merge request python-discord/projects/site!9
Diffstat (limited to 'pysite')
-rw-r--r--pysite/constants.py2
-rw-r--r--pysite/tables.py3
-rw-r--r--pysite/views/main/jams/index.py11
-rw-r--r--pysite/views/main/jams/jam_team_list.py50
-rw-r--r--pysite/views/main/jams/team_edit_repo.py154
-rw-r--r--pysite/views/main/jams/team_view.py51
-rw-r--r--pysite/views/main/jams/user_team_list.py40
-rw-r--r--pysite/views/staff/jams/actions.py6
-rw-r--r--pysite/views/staff/jams/teams/view.py14
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")
}
)