From 9c9702cd37f2c529fd1ee5df5ecd1722c112e5c8 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 27 Mar 2020 14:42:16 +0800 Subject: [PATCH 01/60] Update user.ex --- lib/cadet/accounts/user.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index 26738d354..15379e257 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -14,6 +14,7 @@ defmodule Cadet.Accounts.User do field(:name, :string) field(:role, Role) field(:nusnet_id, :string) + field(:collectibles, :map) belongs_to(:group, Group) timestamps() end From 82605a92bd72e50f301f33315275de9346092156 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 27 Mar 2020 14:43:53 +0800 Subject: [PATCH 02/60] Add files via upload --- priv/repo/migrations/20200320041525_add_collectibles.exs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 priv/repo/migrations/20200320041525_add_collectibles.exs diff --git a/priv/repo/migrations/20200320041525_add_collectibles.exs b/priv/repo/migrations/20200320041525_add_collectibles.exs new file mode 100644 index 000000000..042bd5156 --- /dev/null +++ b/priv/repo/migrations/20200320041525_add_collectibles.exs @@ -0,0 +1,9 @@ +defmodule Cadet.Repo.Migrations.AddCollectibles do + use Ecto.Migration + + def change do + alter table(:users) do + add( :collectibles, :map, default: %{}) + end + end +end From 2acbf8ab5d81adbe7f329979f03ad0cd12ce4f69 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 27 Mar 2020 14:44:52 +0800 Subject: [PATCH 03/60] Update user_controller.ex --- lib/cadet_web/controllers/user_controller.ex | 40 +++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index e43e9d9a8..25974367a 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -7,6 +7,7 @@ defmodule CadetWeb.UserController do use PhoenixSwagger import Cadet.Assessments + import Cadet.Collectibles def index(conn, _) do user = conn.assigns.current_user @@ -15,6 +16,8 @@ defmodule CadetWeb.UserController do story = user_current_story(user) xp = user_total_xp(user) + collectibles = user_collectibles(user) + render( conn, "index.json", @@ -22,7 +25,28 @@ defmodule CadetWeb.UserController do grade: grade, max_grade: max_grade, story: story, - xp: xp + xp: xp, + collectibles: collectibles, + ) + end + + def addCollectibles(conn, pic_nickname, pic_name) do + user = conn.assigns.current_user + grade = user_total_grade(user) + max_grade = user_max_grade(user) + story = user_current_story(user) + xp = user_total_xp(user) + collectibles = add_user_collectibles(user, pic_nickname, pic_name) + + render( + conn, + "index.json", + user: user, + grade: grade, + max_grade: max_grade, + story: story, + xp: xp, + collectibles: collectibles, ) end @@ -39,6 +63,15 @@ defmodule CadetWeb.UserController do response(401, "Unauthorised") end + swagger_path :addCollectibles do + post("/user") + summary("add one collectible to the user") + security([%{JWT: []}]) + response(200, "OK") + response(400, "Invalid parameters") + response(401, "Unauthorised") + end + def swagger_definitions do %{ UserInfo: @@ -73,6 +106,11 @@ defmodule CadetWeb.UserController do :integer, "Amount of xp. Only provided for 'Student'." <> "Value will be 0 for non-students." ) + + collectibles( + :map, + "Collectibles users obtained in story." <> " Value will be empty map for non-students." + ) end end, UserStory: From faca0cada9b57369cf9f357bdd566f4beac47147 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 27 Mar 2020 14:46:01 +0800 Subject: [PATCH 04/60] Update router.ex --- lib/cadet_web/router.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1f37b5c3e..addae7ec7 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -50,6 +50,7 @@ defmodule CadetWeb.Router do post("/notification/acknowledge", NotificationController, :acknowledge) get("/user", UserController, :index) + post("/user", UserController, :addCollectibles) post("/chat/token", ChatController, :index) post("/chat/notify", ChatController, :notify) From bed98a9f75169590c4b0e748b183a36b73848465 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 27 Mar 2020 14:46:59 +0800 Subject: [PATCH 05/60] Update user_view.ex --- lib/cadet_web/views/user_view.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index f8b3e711b..2af4dce39 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -1,7 +1,7 @@ defmodule CadetWeb.UserView do use CadetWeb, :view - def render("index.json", %{user: user, grade: grade, max_grade: max_grade, xp: xp, story: story}) do + def render("index.json", %{user: user, grade: grade, max_grade: max_grade, xp: xp, story: story, collectibles: collectibles}) do %{ name: user.name, role: user.role, @@ -12,7 +12,8 @@ defmodule CadetWeb.UserView do transform_map_for_view(story, %{ story: :story, playStory: :play_story? - }) + }), + collectibles: collectibles } end end From 495a9158969c7a2b91d937ccbe401bd5b393cbe4 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 27 Mar 2020 14:48:37 +0800 Subject: [PATCH 06/60] Create collectibles.ex --- lib/cadet/accounts/collectibles.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lib/cadet/accounts/collectibles.ex diff --git a/lib/cadet/accounts/collectibles.ex b/lib/cadet/accounts/collectibles.ex new file mode 100644 index 000000000..8fffb95a4 --- /dev/null +++ b/lib/cadet/accounts/collectibles.ex @@ -0,0 +1,12 @@ +defmodule Cadet.Collectibles do + import Ecto.Query + alias Cadet.Accounts.{Notifications, User} + def add_user_collectibles(user = %User{}, pic_nickname, pic_name) do + # add the collectibles to user.collectibles, and return user.collectibles + Map.put(user.collectibles, pic_nickname, pic_name) + end + def user_collectibles(user = %User{}) do + # simply return the collectibles of the user, within a single map + user.collectibles + end +end From 517dca7ba2c00d16cfc154d240a034dc4f6a2b22 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Sun, 29 Mar 2020 19:41:21 +0800 Subject: [PATCH 07/60] Update collectibles.ex --- lib/cadet/accounts/collectibles.ex | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/cadet/accounts/collectibles.ex b/lib/cadet/accounts/collectibles.ex index 8fffb95a4..b4aedda45 100644 --- a/lib/cadet/accounts/collectibles.ex +++ b/lib/cadet/accounts/collectibles.ex @@ -1,12 +1,24 @@ defmodule Cadet.Collectibles do import Ecto.Query - alias Cadet.Accounts.{Notifications, User} - def add_user_collectibles(user = %User{}, pic_nickname, pic_name) do - # add the collectibles to user.collectibles, and return user.collectibles - Map.put(user.collectibles, pic_nickname, pic_name) - end - def user_collectibles(user = %User{}) do + alias Cadet.Accounts.User + alias Ecto.Multi + + def user_collectibles(user) do # simply return the collectibles of the user, within a single map user.collectibles end + + ''' + @spec update_collectibles( + string(), + string(), + %User{} + ) :: + {:ok, nil} + | {:error, {:unauthorized | :bad_request | :internal_server_error, String.t()}} + ''' + + def update_grading_info(pic_nickname, pic_name, user) do + + end end From bcf34a9bb8ff463818fe9fec0d8880b699dcf9e5 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Sun, 29 Mar 2020 19:42:32 +0800 Subject: [PATCH 08/60] Update router.ex --- lib/cadet_web/router.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index addae7ec7..21e00a6c5 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -50,8 +50,8 @@ defmodule CadetWeb.Router do post("/notification/acknowledge", NotificationController, :acknowledge) get("/user", UserController, :index) - post("/user", UserController, :addCollectibles) - + post("/user", UserController, :collectiblesUpdate) + post("/chat/token", ChatController, :index) post("/chat/notify", ChatController, :notify) end From b22dc988c6aab69ee3053ba4592fbb356367bf78 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Sun, 29 Mar 2020 19:44:32 +0800 Subject: [PATCH 09/60] Update user_controller.ex --- lib/cadet_web/controllers/user_controller.ex | 46 +++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 25974367a..e320b39c9 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -6,6 +6,8 @@ defmodule CadetWeb.UserController do use CadetWeb, :controller use PhoenixSwagger + import Ecto.Changeset + import Cadet.Assessments import Cadet.Collectibles @@ -15,7 +17,6 @@ defmodule CadetWeb.UserController do max_grade = user_max_grade(user) story = user_current_story(user) xp = user_total_xp(user) - collectibles = user_collectibles(user) render( @@ -28,26 +29,7 @@ defmodule CadetWeb.UserController do xp: xp, collectibles: collectibles, ) - end - def addCollectibles(conn, pic_nickname, pic_name) do - user = conn.assigns.current_user - grade = user_total_grade(user) - max_grade = user_max_grade(user) - story = user_current_story(user) - xp = user_total_xp(user) - collectibles = add_user_collectibles(user, pic_nickname, pic_name) - - render( - conn, - "index.json", - user: user, - grade: grade, - max_grade: max_grade, - story: story, - xp: xp, - collectibles: collectibles, - ) end swagger_path :index do @@ -63,15 +45,37 @@ defmodule CadetWeb.UserController do response(401, "Unauthorised") end - swagger_path :addCollectibles do + def collectiblesUpdate(conn, %{"picnickname" => pic_nickname, "picname" => pic_name}) do + user = conn.assigns[:current_user] + case Collectibles.update_collectibles(pic_nickname, pic_name, user) do + {:ok, _} -> + text(conn, "OK") + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + + swagger_path :collectiblesUpdate do post("/user") summary("add one collectible to the user") security([%{JWT: []}]) + consumes("application/json") + produces("application/json") + parameters do + picNickname(:path, :string, "picture nickname", required: true) + questionId(:path, :string, "picture name", required: true) + end response(200, "OK") response(400, "Invalid parameters") response(401, "Unauthorised") end + + + + def swagger_definitions do %{ UserInfo: From d2fc579c5cfd59b11db838252a3fa738242fc0c1 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Mon, 30 Mar 2020 20:40:03 +0800 Subject: [PATCH 10/60] Update collectibles.ex --- lib/cadet/accounts/collectibles.ex | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/cadet/accounts/collectibles.ex b/lib/cadet/accounts/collectibles.ex index b4aedda45..3d74a234b 100644 --- a/lib/cadet/accounts/collectibles.ex +++ b/lib/cadet/accounts/collectibles.ex @@ -2,23 +2,25 @@ defmodule Cadet.Collectibles do import Ecto.Query alias Cadet.Accounts.User alias Ecto.Multi - + import Cadet.Repo + import Ecto.Repo + + # simply return the collectibles of the user, within a single map def user_collectibles(user) do - # simply return the collectibles of the user, within a single map user.collectibles end - ''' - @spec update_collectibles( - string(), - string(), - %User{} - ) :: - {:ok, nil} - | {:error, {:unauthorized | :bad_request | :internal_server_error, String.t()}} - ''' - - def update_grading_info(pic_nickname, pic_name, user) do + def update_collectibles(pic_nickname, pic_name, user) do + changeset = + Ecto.Changeset.cast(user, %{collectibles: Map.put(user_collectibles(user), pic_nickname, pic_name)},[:collectibles]) + Cadet.Repo.update!(changeset) + end + # should be idle since we are not going to delete students' collectibles + # but provide the function delete_collectibles here for future extension + def delete_all_collectibles(user) do + changeset = + Ecto.Changeset.cast(user, %{collectibles: %{}},[:collectibles]) + Cadet.Repo.update!(changeset) end end From 709e02a6cb550bf607bbce2f47cffd6789b0a0eb Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 2 Apr 2020 20:44:08 +0800 Subject: [PATCH 11/60] Add a game_states column to the database. --- .../20200402094115_add_user_game_states.exs | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 priv/repo/migrations/20200402094115_add_user_game_states.exs diff --git a/priv/repo/migrations/20200402094115_add_user_game_states.exs b/priv/repo/migrations/20200402094115_add_user_game_states.exs new file mode 100644 index 000000000..0d2e7bbc4 --- /dev/null +++ b/priv/repo/migrations/20200402094115_add_user_game_states.exs @@ -0,0 +1,11 @@ +defmodule Cadet.Repo.Migrations.AddUserGameStates do + use Ecto.Migration + + def change do + alter table(:users) do + add(:game_states, :map, default: %{collectibles: %{}, save_data: %{ + action_sequence: [], start_location: "" + }}) + end + end +end From d052ce4e967ffdc86d681635fa78c571e59d3c0f Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 2 Apr 2020 20:45:00 +0800 Subject: [PATCH 12/60] Delete original collectibles. --- priv/repo/migrations/20200320041525_add_collectibles.exs | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 priv/repo/migrations/20200320041525_add_collectibles.exs diff --git a/priv/repo/migrations/20200320041525_add_collectibles.exs b/priv/repo/migrations/20200320041525_add_collectibles.exs deleted file mode 100644 index 042bd5156..000000000 --- a/priv/repo/migrations/20200320041525_add_collectibles.exs +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Cadet.Repo.Migrations.AddCollectibles do - use Ecto.Migration - - def change do - alter table(:users) do - add( :collectibles, :map, default: %{}) - end - end -end From 16323c944542802165c3b3d10a7ea10cb8bb3bdb Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 2 Apr 2020 20:46:32 +0800 Subject: [PATCH 13/60] added trivial change to maitain consitency --- lib/cadet_web/views/user_view.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 2af4dce39..71856bd00 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -1,7 +1,7 @@ defmodule CadetWeb.UserView do use CadetWeb, :view - def render("index.json", %{user: user, grade: grade, max_grade: max_grade, xp: xp, story: story, collectibles: collectibles}) do + def render("index.json", %{user: user, grade: grade, max_grade: max_grade, xp: xp, story: story, game_states: game_states}) do %{ name: user.name, role: user.role, @@ -13,7 +13,7 @@ defmodule CadetWeb.UserView do story: :story, playStory: :play_story? }), - collectibles: collectibles + game_states: game_states } end end From 18375b52a85c1f6e619462b9322a3efca39a5a61 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 2 Apr 2020 20:47:58 +0800 Subject: [PATCH 14/60] changed some post actions to put actions --- lib/cadet_web/router.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 21e00a6c5..fddf6366a 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -50,8 +50,9 @@ defmodule CadetWeb.Router do post("/notification/acknowledge", NotificationController, :acknowledge) get("/user", UserController, :index) - post("/user", UserController, :collectiblesUpdate) - + put("/user/collectibles", UserController, :collectible_update) + put("/user/game_states", UserController, :save_data_update) + post("/chat/token", ChatController, :index) post("/chat/notify", ChatController, :notify) end From 9635a2bfede9ddd96e2638ec27c633bf0b8e630a Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 2 Apr 2020 20:49:32 +0800 Subject: [PATCH 15/60] updated to game_states version Updated to game_states version and added some to-dos for the save_data part --- lib/cadet_web/controllers/user_controller.ex | 36 +++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index e320b39c9..7037532f3 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -6,10 +6,11 @@ defmodule CadetWeb.UserController do use CadetWeb, :controller use PhoenixSwagger - import Ecto.Changeset + # import Ecto.Changeset import Cadet.Assessments - import Cadet.Collectibles + import Cadet.GameStates + import Ecto.Repo def index(conn, _) do user = conn.assigns.current_user @@ -17,8 +18,7 @@ defmodule CadetWeb.UserController do max_grade = user_max_grade(user) story = user_current_story(user) xp = user_total_xp(user) - collectibles = user_collectibles(user) - + game_states = user_game_states(user) render( conn, "index.json", @@ -27,9 +27,8 @@ defmodule CadetWeb.UserController do max_grade: max_grade, story: story, xp: xp, - collectibles: collectibles, + game_states: game_states ) - end swagger_path :index do @@ -45,9 +44,14 @@ defmodule CadetWeb.UserController do response(401, "Unauthorised") end - def collectiblesUpdate(conn, %{"picnickname" => pic_nickname, "picname" => pic_name}) do + def collectibles_update(conn, %{"picnickname" => pic_nickname, "picname" => pic_name}) do user = conn.assigns[:current_user] - case Collectibles.update_collectibles(pic_nickname, pic_name, user) do + Cadet.GameStates.update_collectibles(pic_nickname, pic_name, user) + ''' + + # Error reporting part, to be further implemented. + + case Cadet.GameStates.update_collectibles(pic_nickname, pic_name, user) do {:ok, _} -> text(conn, "OK") {:error, {status, message}} -> @@ -55,10 +59,11 @@ defmodule CadetWeb.UserController do |> put_status(status) |> text(message) end + ''' end - swagger_path :collectiblesUpdate do - post("/user") + swagger_path :collectibles_update do + put("/user/collectibles") summary("add one collectible to the user") security([%{JWT: []}]) consumes("application/json") @@ -72,9 +77,16 @@ defmodule CadetWeb.UserController do response(401, "Unauthorised") end + ''' + # to do + def save_data_update do + end + swagger_path :save_data_update do + end + ''' def swagger_definitions do %{ @@ -111,9 +123,9 @@ defmodule CadetWeb.UserController do "Amount of xp. Only provided for 'Student'." <> "Value will be 0 for non-students." ) - collectibles( + game_states( :map, - "Collectibles users obtained in story." <> " Value will be empty map for non-students." + "States for user's game, including users' collectibles and save data." <> " Value will be a map of empty maps for non-students." ) end end, From e92b5c0d69ad588c4240adf325a3bec1d1c406ba Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 2 Apr 2020 20:51:13 +0800 Subject: [PATCH 16/60] added one line to be consistent with migration --- lib/cadet/accounts/user.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index 15379e257..2016bbfd1 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -14,7 +14,7 @@ defmodule Cadet.Accounts.User do field(:name, :string) field(:role, Role) field(:nusnet_id, :string) - field(:collectibles, :map) + field(:game_states, :map) belongs_to(:group, Group) timestamps() end From 210cff66537d6320efec881df9098ce5e5f2a602 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 2 Apr 2020 20:54:04 +0800 Subject: [PATCH 17/60] make changes to model the behavior of game_states changed from the previous collectibles version to game_states version. add a few to-dos for futrue implementation of save_data related functions. Also noted that the error handling part is not implemented yet, which is also a to-do. --- lib/cadet/accounts/collectibles.ex | 26 -------------- lib/cadet/accounts/game_states.ex | 57 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 26 deletions(-) delete mode 100644 lib/cadet/accounts/collectibles.ex create mode 100644 lib/cadet/accounts/game_states.ex diff --git a/lib/cadet/accounts/collectibles.ex b/lib/cadet/accounts/collectibles.ex deleted file mode 100644 index 3d74a234b..000000000 --- a/lib/cadet/accounts/collectibles.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule Cadet.Collectibles do - import Ecto.Query - alias Cadet.Accounts.User - alias Ecto.Multi - import Cadet.Repo - import Ecto.Repo - - # simply return the collectibles of the user, within a single map - def user_collectibles(user) do - user.collectibles - end - - def update_collectibles(pic_nickname, pic_name, user) do - changeset = - Ecto.Changeset.cast(user, %{collectibles: Map.put(user_collectibles(user), pic_nickname, pic_name)},[:collectibles]) - Cadet.Repo.update!(changeset) - end - - # should be idle since we are not going to delete students' collectibles - # but provide the function delete_collectibles here for future extension - def delete_all_collectibles(user) do - changeset = - Ecto.Changeset.cast(user, %{collectibles: %{}},[:collectibles]) - Cadet.Repo.update!(changeset) - end -end diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex new file mode 100644 index 000000000..1896dfaff --- /dev/null +++ b/lib/cadet/accounts/game_states.ex @@ -0,0 +1,57 @@ +defmodule Cadet.GameStates do + import Ecto.Repo + + # simply return the game states of the user + def user_game_states(user) do + user.game_states + end + + # simply return the collectibles of the user, within a single map + def user_collectibles(user) do + user.game_states["collectibles"] + end + + # simply return the collectibles of the user, within a single map + def user_save_data(user) do + user.game_states["save_data"] + end + + def update_collectibles(pic_nickname, pic_name, user) do + # to do + changeset = + Ecto.Changeset.cast(user, %{game_states: %{collectibles: Map.put(user_collectibles(user), pic_nickname, pic_name), + save_data: user_save_data(user)}},[:game_states]) + Cadet.Repo.update!(changeset) + # really simple error handling action, to be further implemented + ''' + with {:ok, _} <- Repo.update(changeset) do + {:ok, nil} + else + {:error, _} -> + {:error, {:internal_server_error, "Please try again later."}} + end + ''' + end + + # should be idle since we are not going to delete students' collectibles + # but provide the function delete_collectibles here for future extension + def delete_all_collectibles(user) do + changeset = + Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, save_data: user_save_data(user)}},[:game_states]) + Cadet.Repo.update!(changeset) + end + + ''' + # to implement when needed + + def update_save_data() do + ... # to do the actual implementation + end + + def delete_all_save_data() do + ... # to do the actual implementation + end + + ''' + +end From 431a0587161a9080c05201232ee3b6e87aab311a Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Mon, 6 Apr 2020 21:22:00 +0800 Subject: [PATCH 18/60] Added GroundControl --- lib/cadet/assessments/assessments.ex | 185 ++++++++++++++---- lib/cadet/jobs/xml_parser.ex | 119 ++++++----- .../controllers/assessments_controller.ex | 72 ++++++- lib/cadet_web/router.ex | 4 + lib/cadet_web/views/assessments_view.ex | 3 +- 5 files changed, 292 insertions(+), 91 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 59965c8a9..e39f66572 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -16,11 +16,56 @@ defmodule Cadet.Assessments do @xp_early_submission_max_bonus 100 @xp_bonus_assessment_type ~w(mission sidequest)a @submit_answer_roles ~w(student)a + @change_dates_assessment_role ~w(staff admin)a + @delete_assessment_role ~w(staff admin)a + @publish_assessment_role ~w(staff admin)a @unsubmit_assessment_role ~w(staff admin)a @grading_roles ~w()a @see_all_submissions_roles ~w(staff admin)a @open_all_assessment_roles ~w(staff admin)a + def change_dates_assessment(_user = %User{role: role}, id, close_at, open_at) do + if role in @change_dates_assessment_role do + assessment = Repo.get(Assessment, id) + previous_open_time = assessment.open_at + cond do + Timex.before?(close_at, open_at) -> + {:error, {:bad_request, "New end date should occur after new opening date"}} + + Timex.before?(close_at, Timex.now()) -> + {:error, {:bad_request, "New end date should occur after current time"}} + + Timex.equal?(previous_open_time, open_at) or Timex.after?(previous_open_time, Timex.now()) -> + update_assessment(id, %{close_at: close_at, open_at: open_at}) + + Timex.before?(open_at, Timex.now()) -> + {:error, {:bad_request, "New Opening date should occur after current time"}} + + true -> + {:error, {:forbidden, "Assessment is already opened"}} + end + else + {:error, {:forbidden, "User is not permitted to edit"}} + end + end + + def toggle_publish_assessment(_publisher = %User{role: role}, id, bool) do + if role in @publish_assessment_role do + update_assessment(id, %{is_published: bool}) + else + {:error, {:forbidden, "User is not permitted to publish"}} + end + end + + def delete_assessment(_deleter = %User{role: role}, id) do + if role in @delete_assessment_role do + assessment = Repo.get(Assessment, id) + Repo.delete(assessment) + else + {:error, {:forbidden, "User is not permitted to delete"}} + end + end + @spec user_total_xp(%User{}) :: integer() def user_total_xp(%User{id: user_id}) when is_ecto_id(user_id) do total_xp_bonus = @@ -163,11 +208,18 @@ defmodule Cadet.Assessments do def assessment_with_questions_and_answers(id, user = %User{}, password) when is_ecto_id(id) do + role = user.role assessment = - Assessment - |> where(id: ^id) - |> where(is_published: true) - |> Repo.one() + if role in @open_all_assessment_roles do + Assessment + |> where(id: ^id) + |> Repo.one() + else + Assessment + |> where(id: ^id) + |> where(is_published: true) + |> Repo.one() + end if assessment do assessment_with_questions_and_answers(assessment, user, password) @@ -206,11 +258,7 @@ defmodule Cadet.Assessments do assessment_with_questions_and_answers(id, user, nil) end - @doc """ - Returns a list of assessments with all fields and an indicator showing whether it has been attempted - by the supplied user - """ - def all_published_assessments(user = %User{}) do + def all_assessments(user = %User{}) do assessments = Query.all_assessments_with_max_xp_and_grade() |> subquery() @@ -240,7 +288,7 @@ defmodule Cadet.Assessments do question_count: q_count.count, graded_count: a_count.count }) - |> where(is_published: true) + |> filter_published_assessments(user) |> order_by(:open_at) |> Repo.all() |> Enum.map(fn assessment = %Assessment{} -> @@ -259,6 +307,14 @@ defmodule Cadet.Assessments do {:ok, assessments} end + def filter_published_assessments(assessments, user) do + role = user.role + case role do + :student -> where(assessments, is_published: true) + _ -> assessments + end + end + defp build_grading_status(submission_status, a_type, q_count, g_count) do case a_type do type when type in [:mission, :sidequest] -> @@ -283,53 +339,108 @@ defmodule Cadet.Assessments do @doc """ The main function that inserts or updates assessments from the XML Parser """ - @spec insert_or_update_assessments_and_questions(map(), [map()]) :: + @spec insert_or_update_assessments_and_questions(map(), [map()], boolean()) :: {:ok, any()} | {:error, Ecto.Multi.name(), any(), %{optional(Ecto.Multi.name()) => any()}} - def insert_or_update_assessments_and_questions(assessment_params, questions_params) do + def insert_or_update_assessments_and_questions(assessment_params, questions_params, force_update) do assessment_multi = Multi.insert_or_update( Multi.new(), :assessment, - insert_or_update_assessment_changeset(assessment_params) + insert_or_update_assessment_changeset(assessment_params, force_update) ) - questions_params - |> Enum.with_index(1) - |> Enum.reduce(assessment_multi, fn {question_params, index}, multi -> - Multi.run(multi, String.to_atom("question#{index}"), fn _repo, - %{assessment: %Assessment{id: id}} -> - question_params - |> Map.put(:display_order, index) - |> build_question_changeset_for_assessment_id(id) - |> Repo.insert() + if force_update and check_question_count(assessment_multi, questions_params) do + {:error, "Question count is different"} + else + questions_params + |> Enum.with_index(1) + |> Enum.reduce(assessment_multi, fn {question_params, index}, multi -> + Multi.run(multi, String.to_atom("question#{index}"), fn _repo, + %{assessment: %Assessment{id: id}} -> + question_exists = + Repo.exists?(where(Question, [q], q.assessment_id == ^id and q.display_order == ^index)) + if !force_update or !question_exists do + question_params + |> Map.put(:display_order, index) + |> build_question_changeset_for_assessment_id(id) + |> Repo.insert() + else + params = + (if !question_params.max_xp do + question_params + |> Map.put(:max_xp, 0) + else + question_params + end) + |> Map.put(:display_order, index) + + %{id: question_id} = + where(Question, [q], q.display_order == ^index and q.assessment_id == ^id) + |> Repo.one() + + changeset = Question.changeset(%Question{assessment_id: id, id: question_id}, params) + Repo.update(changeset) + end + end) end) - end) - |> Repo.transaction() + |> Repo.transaction() + end end - @spec insert_or_update_assessment_changeset(map()) :: Ecto.Changeset.t() - defp insert_or_update_assessment_changeset(params = %{number: number}) do + defp check_question_count(assessment_multi, questions_params) do + assessment_id = + (assessment_multi.operations + |> List.first() + |> elem(1) + |> elem(1)).data.id + + if !assessment_id do + false + else + existing_questions_count = + where(Question, [q], q.assessment_id == ^assessment_id) + |> Repo.all() + |> Enum.count + + new_questions_count = Enum.count(questions_params) + + existing_questions_count != new_questions_count + end + end + + @spec insert_or_update_assessment_changeset(map(), boolean()) :: Ecto.Changeset.t() + defp insert_or_update_assessment_changeset(params = %{number: number}, force_update) do Assessment |> where(number: ^number) |> Repo.one() |> case do nil -> Assessment.changeset(%Assessment{}, params) - assessment -> - if Timex.after?(assessment.open_at, Timex.now()) do - # Delete all existing questions - %{id: assessment_id} = assessment + cond do + Timex.after?(assessment.open_at, Timex.now()) -> + # Delete all existing questions + %{id: assessment_id} = assessment - Question - |> where(assessment_id: ^assessment_id) - |> Repo.delete_all() + Question + |> where(assessment_id: ^assessment_id) + |> Repo.delete_all() - Assessment.changeset(assessment, params) - else - # if the assessment is already open, don't mess with it - create_invalid_changeset_with_error(:assessment, "is already open") + Assessment.changeset(assessment, params) + + force_update -> + # Maintain the same open/close date when force updating an assessment + new_params = + params + |> Map.delete(:open_at) + |> Map.delete(:close_at) + + Assessment.changeset(assessment, new_params) + + true -> + # if the assessment is already open, don't mess with it + create_invalid_changeset_with_error(:assessment, "is already open") end end end diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index c4da6a4d3..87e80aed7 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -77,14 +77,15 @@ defmodule Cadet.Updater.XMLParser do end end - @spec parse_xml(String.t()) :: :ok | :error - def parse_xml(xml) do + @spec parse_xml(String.t(), boolean()) :: :ok | {:ok, String.t} | {:error, {atom(), String.t}} + def parse_xml(xml, force_update \\ false) do with {:ok, assessment_params} <- process_assessment(xml), {:ok, questions_params} <- process_questions(xml), {:ok, %{assessment: assessment}} <- Assessments.insert_or_update_assessments_and_questions( assessment_params, - questions_params + questions_params, + force_update ) do Logger.info( "Created/updated assessment with id: #{assessment.id}, with #{length(questions_params)} questions." @@ -92,25 +93,49 @@ defmodule Cadet.Updater.XMLParser do :ok else - :error -> - :error - {:error, stage, %{errors: [assessment: {"is already open", []}]}, _} when is_atom(stage) -> Logger.warn("Assessment already open, ignoring...") - :ok + {:ok, "Assessment already open, ignoring..."} + + {:error, errmsg} -> + log_and_return_badrequest(errmsg) {:error, stage, changeset, _} when is_atom(stage) -> log_error_bad_changeset(changeset, stage) - :error + changeset_error = + changeset + |> Map.get(:errors) + |> extract_changeset_error_message + error_message = "Invalid #{stage} changeset " <> changeset_error + log_and_return_badrequest(error_message) end catch # the :erlsom library used by SweetXml will exit if XML is invalid - :exit, _ -> - :error + :exit, parse_error -> + error_message = + parse_error + |> nested_tuple_to_list() + |> List.flatten() + |> Enum.reduce("", fn x, acc -> acc <> to_string(x) <> " " end) + {:error, {:bad_request, "Invalid XML " <> error_message}} + end + + defp extract_changeset_error_message(errors_list) do + errors_list + |> Enum.map(fn {field, {error, _}} -> to_string(field) <> " " <> error end) + |> List.foldr("", fn x, acc -> acc <> x <> " " end) end - @spec process_assessment(String.t()) :: {:ok, map()} | :error + @spec process_assessment(String.t()) :: {:ok, map()} | {:error, String.t} defp process_assessment(xml) do + open_at = + Timex.now() + |> Timex.beginning_of_day() + |> Timex.shift(days: 3) + |> Timex.shift(hours: 4) + + close_at = Timex.shift(open_at, days: 7) + assessment_params = xml |> xpath( @@ -118,8 +143,6 @@ defmodule Cadet.Updater.XMLParser do access: ~x"./@access"s |> transform_by(&process_access/1), type: ~x"./@kind"s |> transform_by(&change_quest_to_sidequest/1), title: ~x"./@title"s, - open_at: ~x"./@startdate"s |> transform_by(&Timex.parse!(&1, "{ISO:Extended}")), - close_at: ~x"./@duedate"s |> transform_by(&Timex.parse!(&1, "{ISO:Extended}")), number: ~x"./@number"s, story: ~x"./@story"s, cover_picture: ~x"./@coverimage"s, @@ -128,7 +151,9 @@ defmodule Cadet.Updater.XMLParser do summary_long: ~x"./TEXT/text()" |> transform_by(&process_charlist/1), password: ~x"//PASSWORD/text()"so |> transform_by(&process_charlist/1) ) - |> Map.put(:is_published, true) + |> Map.put(:is_published, false) + |> Map.put(:open_at, open_at) + |> Map.put(:close_at, close_at) if assessment_params.access === "public" do Map.put(assessment_params, :password, nil) @@ -138,21 +163,12 @@ defmodule Cadet.Updater.XMLParser do Map.put(assessment_params, :password, "") end - if verify_has_time_offset(assessment_params) do - {:ok, assessment_params} - else - Logger.error("Time does not have offset specified.") - :error - end + {:ok, assessment_params} + rescue - e in Timex.Parse.ParseError -> - Logger.error("Time does not conform to ISO8601 DateTime: #{e.message}") - :error - # This error is raised by xpath/3 when TASK does not exist (hence is equal to nil) Protocol.UndefinedError -> - Logger.error("Missing TASK") - :error + {:error, "Missing TASK"} end def process_access("private") do @@ -172,17 +188,7 @@ defmodule Cadet.Updater.XMLParser do type end - @spec verify_has_time_offset(%{ - :open_at => DateTime.t() | NaiveDateTime.t(), - :close_at => DateTime.t() | NaiveDateTime.t(), - optional(atom()) => any() - }) :: boolean() - defp verify_has_time_offset(%{open_at: open_at, close_at: close_at}) do - # Timex.parse!/2 returns NaiveDateTime when offset is not specified, or DateTime otherwise. - open_at.__struct__ != NaiveDateTime and close_at.__struct__ != NaiveDateTime - end - - @spec process_questions(String.t()) :: {:ok, [map()]} | :error + @spec process_questions(String.t()) :: {:ok, [map()]} | {:error, String.t} defp process_questions(xml) do default_library = xpath(xml, ~x"//TASK/DEPLOYMENT"e) default_grading_library = xpath(xml, ~x"//TASK/GRADERDEPLOYMENT"e) @@ -206,16 +212,16 @@ defmodule Cadet.Updater.XMLParser do question else {:no_missing_attr?, false} -> - Logger.error("Missing attribute(s) on PROBLEM") - :error - - :error -> - :error + {:error, "Missing attribute(s) on PROBLEM"} + + {:error, errmsg} -> + {:error, errmsg} end end) - if Enum.any?(questions_params, &(&1 == :error)) do - :error + if Enum.any?(questions_params, &(!is_map(&1))) do + error = Enum.find(questions_params, &(!is_map(&1))) + error else {:ok, questions_params} end @@ -228,7 +234,7 @@ defmodule Cadet.Updater.XMLParser do Logger.error("Changeset: #{inspect(changeset, pretty: true)}") end - @spec process_question_by_question_type(map()) :: map() | :error + @spec process_question_by_question_type(map()) :: map() | {:error, String.t} defp process_question_by_question_type(question) do question[:entity] |> process_question_entity_by_type(question[:type]) @@ -236,8 +242,8 @@ defmodule Cadet.Updater.XMLParser do question_map when is_map(question_map) -> Map.put(question, :question, question_map) - :error -> - :error + {:error, errmsg} -> + {:error, errmsg} end end @@ -288,11 +294,10 @@ defmodule Cadet.Updater.XMLParser do end defp process_question_entity_by_type(_, _) do - Logger.error("Invalid question type.") - :error + {:error, "Invalid question type"} end - @spec process_question_library(map(), any(), any()) :: map() | :error + @spec process_question_library(map(), any(), any()) :: map() | {:error, String.t} defp process_question_library(question, default_library, default_grading_library) do library = xpath(question[:entity], ~x"./DEPLOYMENT"o) || default_library @@ -304,8 +309,7 @@ defmodule Cadet.Updater.XMLParser do |> Map.put(:library, process_question_library(library)) |> Map.put(:grading_library, process_question_library(grading_library)) else - Logger.error("Missing DEPLOYMENT") - :error + {:error, "Missing DEPLOYMENT"} end end @@ -350,4 +354,15 @@ defmodule Cadet.Updater.XMLParser do |> to_string() |> String.trim() end + + defp log_and_return_badrequest(error_message) do + Logger.error(error_message) + {:error, {:bad_request, error_message}} + end + + defp nested_tuple_to_list(tuple) when is_tuple(tuple) do + tuple |> Tuple.to_list |> Enum.map(&nested_tuple_to_list/1) + end + + defp nested_tuple_to_list(x), do: x end diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index c6ba44fe9..fd23306b4 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -4,6 +4,7 @@ defmodule CadetWeb.AssessmentsController do use PhoenixSwagger alias Cadet.Assessments + import Cadet.Updater.XMLParser, only: [parse_xml: 2] def submit(conn, %{"assessmentid" => assessment_id}) when is_ecto_id(assessment_id) do case Assessments.finalise_submission(assessment_id, conn.assigns.current_user) do @@ -19,7 +20,7 @@ defmodule CadetWeb.AssessmentsController do def index(conn, _) do user = conn.assigns[:current_user] - {:ok, assessments} = Assessments.all_published_assessments(user) + {:ok, assessments} = Assessments.all_assessments(user) render(conn, "index.json", assessments: assessments) end @@ -34,6 +35,75 @@ defmodule CadetWeb.AssessmentsController do end end + def update(conn, %{"id" => id, "bool" => bool}) do + result = Assessments.toggle_publish_assessment(conn.assigns.current_user, id, bool) + + case result do + {:ok, _nil} -> + send_resp(conn, 200, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + + def update(conn, %{"id" => id, "closeAt" => close_at, "openAt" => open_at}) do + formatted_close_date = elem(DateTime.from_iso8601(close_at), 1) + formatted_open_date = elem(DateTime.from_iso8601(open_at), 1) + result = Assessments.change_dates_assessment(conn.assigns.current_user, id, formatted_close_date, formatted_open_date) + + case result do + {:ok, _nil} -> + send_resp(conn, 200, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + + def delete(conn, %{"id" => id}) do + result = Assessments.delete_assessment(conn.assigns.current_user, id) + + case result do + {:ok, _nil} -> + send_resp(conn, 200, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + + def create(conn, %{"assessment" => assessment, "forceUpdate" => force_update}) do + file = assessment["file"].path + |> File.read!() + result = + case force_update do + "true" -> parse_xml(file, true) + "false" -> parse_xml(file, false) + end + + case result do + :ok -> + if (force_update == "true") do + send_resp(conn, 200, "Force Update OK") + else + send_resp(conn, 200, "OK") + end + + {:ok, warning_message} -> + send_resp(conn, 200, warning_message) + + {:error, {status, message}} -> + send_resp(conn, status, message) + end + end + swagger_path :submit do post("/assessments/{assessmentId}/submit") summary("Finalise submission for an assessment") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1f37b5c3e..be2e2369c 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -37,7 +37,11 @@ defmodule CadetWeb.Router do resources("/sourcecast", SourcecastController, only: [:create, :delete]) get("/assessments", AssessmentsController, :index) + post("/assessments", AssessmentsController, :create) + delete("/assessments/:id", AssessmentsController, :delete) post("/assessments/:id", AssessmentsController, :show) + post("/assessments/publish/:id", AssessmentsController, :update) + post("/assessments/update/:id", AssessmentsController, :update) post("/assessments/:assessmentid/submit", AssessmentsController, :submit) post("/assessments/question/:questionid/submit", AnswerController, :submit) diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index 4918e1693..e8b1dfea9 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -26,7 +26,8 @@ defmodule CadetWeb.AssessmentsView do xp: &(&1.xp || 0), grade: &(&1.grade || 0), coverImage: :cover_picture, - private: &password_protected?(&1.password) + private: &password_protected?(&1.password), + isPublished: :is_published }) end From 5a865d9c082d394540c52be36b1ce8498a934e5f Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Tue, 7 Apr 2020 21:47:10 +0800 Subject: [PATCH 19/60] Minor edit --- lib/cadet/assessments/assessments.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index e39f66572..c314c6e69 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -258,6 +258,10 @@ defmodule Cadet.Assessments do assessment_with_questions_and_answers(id, user, nil) end + @doc """ + Returns a list of assessments with all fields and an indicator showing whether it has been attempted + by the supplied user + """ def all_assessments(user = %User{}) do assessments = Query.all_assessments_with_max_xp_and_grade() From 0218305362200d8050833feec08f5aa5884e25b8 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 10 Apr 2020 13:50:49 +0800 Subject: [PATCH 20/60] Added few functions regarding game data --- lib/cadet/accounts/game_states.ex | 51 ++++++++++++++++--------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex index 1896dfaff..eb2bbab43 100644 --- a/lib/cadet/accounts/game_states.ex +++ b/lib/cadet/accounts/game_states.ex @@ -1,6 +1,9 @@ defmodule Cadet.GameStates do import Ecto.Repo + # currently in this module no error handling function + # has been implemented yet + # simply return the game states of the user def user_game_states(user) do user.game_states @@ -17,41 +20,41 @@ defmodule Cadet.GameStates do end def update_collectibles(pic_nickname, pic_name, user) do - # to do changeset = Ecto.Changeset.cast(user, %{game_states: %{collectibles: Map.put(user_collectibles(user), pic_nickname, pic_name), save_data: user_save_data(user)}},[:game_states]) Cadet.Repo.update!(changeset) - # really simple error handling action, to be further implemented - ''' - with {:ok, _} <- Repo.update(changeset) do - {:ok, nil} - else - {:error, _} -> - {:error, {:internal_server_error, "Please try again later."}} - end - ''' - end - - # should be idle since we are not going to delete students' collectibles - # but provide the function delete_collectibles here for future extension - def delete_all_collectibles(user) do + end + + def update_save_data(action_sequence, start_location, user) do changeset = - Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, save_data: user_save_data(user)}},[:game_states]) + Ecto.Changeset.cast(user, %{game_states: %{collectibles: user_collectibles(user), + save_data: %{ + action_sequence: action_sequence, + start_location: start_location + }}},[:game_states]) Cadet.Repo.update!(changeset) end - ''' - # to implement when needed - - def update_save_data() do - ... # to do the actual implementation + # functions below are for debugging and testing purposes + def clear_up(user) do + changeset = + Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, + save_data: %{action_sequence: [], start_location: ""}}},[:game_states]) + Cadet.Repo.update!(changeset) end - def delete_all_save_data() do - ... # to do the actual implementation + def delete_all_collectibles(user) do + changeset = + Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, save_data: user_save_data(user)}},[:game_states]) + Cadet.Repo.update!(changeset) end - ''' + def delete_save_data(user) do + changeset = + Ecto.Changeset.cast(user, %{game_states: %{collectibles: user_collectibles(user), + save_data: %{action_sequence: [], start_location: ""}}},[:game_states]) + Cadet.Repo.update!(changeset) + end end From 14f50257215bbdb2e9f606ff56b702301ba17798 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 10 Apr 2020 13:51:41 +0800 Subject: [PATCH 21/60] Added some routes regarding game states. --- lib/cadet_web/router.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index fddf6366a..9030ada18 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -51,7 +51,8 @@ defmodule CadetWeb.Router do get("/user", UserController, :index) put("/user/collectibles", UserController, :collectible_update) - put("/user/game_states", UserController, :save_data_update) + put("/user/save_data/update", UserController, :save_data_update) + put("/user/save_data/clear_up", UserController, :save_data_clear_up) post("/chat/token", ChatController, :index) post("/chat/notify", ChatController, :notify) From 5bb40e9d28002b5ceb015eec1f155da5c98cc139 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 10 Apr 2020 13:53:39 +0800 Subject: [PATCH 22/60] Added some functios regarding game states. --- lib/cadet_web/controllers/user_controller.ex | 53 +++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 7037532f3..b0a4c4b02 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -47,19 +47,6 @@ defmodule CadetWeb.UserController do def collectibles_update(conn, %{"picnickname" => pic_nickname, "picname" => pic_name}) do user = conn.assigns[:current_user] Cadet.GameStates.update_collectibles(pic_nickname, pic_name, user) - ''' - - # Error reporting part, to be further implemented. - - case Cadet.GameStates.update_collectibles(pic_nickname, pic_name, user) do - {:ok, _} -> - text(conn, "OK") - {:error, {status, message}} -> - conn - |> put_status(status) - |> text(message) - end - ''' end swagger_path :collectibles_update do @@ -77,16 +64,41 @@ defmodule CadetWeb.UserController do response(401, "Unauthorised") end - ''' - # to do - def save_data_update do - + def save_data_update(conn, %{[] => action_sequence, "" => start_location}) do + user = conn.assigns[:current_user] + Cadet.GameStates.update_save_data(action_sequence, start_location, user) end swagger_path :save_data_update do + put("/user/save_data/update") + summary("update users' game data saved") + security([%{JWT: []}]) + consumes("application/json") + produces("application/json") + parameters do + action(:path, :array, "action sequence", required: true) + startLocation(:path, :string, "start location", required: true) + end + response(200, "OK") + response(400, "Invalid parameters") + response(401, "Unauthorised") + end + def game_state_clear_up(conn, _params) do + user = conn.assigns[:current_user] + Cadet.GameStates.clear_up(user) end - ''' + + swagger_path :save_data_clear_up do + put("/user/save_data/clear_up") + summary("clear up users' game data saved") + security([%{JWT: []}]) + consumes("application/json") + produces("application/json") + response(200, "OK") + response(401, "Unauthorised") + end + def swagger_definitions do %{ @@ -125,7 +137,10 @@ defmodule CadetWeb.UserController do game_states( :map, - "States for user's game, including users' collectibles and save data." <> " Value will be a map of empty maps for non-students." + "States for user's game, including users' collectibles and save data.\n" + <> "collectibles is a map, and save data is a map with elements action sequence and start location\n" + <> "action sequence is an array of string, and start location is a string.\n" + <> " Value will be a map of empty maps for non-students." ) end end, From 26ac37ceddbb56ed3d9d6d48413b40322946beb9 Mon Sep 17 00:00:00 2001 From: jiachen Date: Wed, 29 Jan 2020 22:07:49 +0800 Subject: [PATCH 23/60] Update README.md (#570) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f8cbb9cc..6d94816f7 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,12 @@ $ cp config/secrets.exs.example config/secrets.exs $ vim config/secrets.exs ``` - A valid `luminus_api_key`, `luminus_client_id`, `luminus_client_secret` and - `luminus_redirect_url` are required for the application to properly authenticate with LumiNUS. + `luminus_redirect_url` are required for the application to properly authenticate with LumiNUS.\* - A valid `cs1101s_repository`, `cs1101s_rsa_key` is required for the application to run with the `--updater` flag. Otherwise, the default values will suffice. - A valid `instance_id`, `key_id` and `key_secret` are required to use ChatKit's services. Otherwise, the placeholder values can be left as they are. + + \*If you require access to Luminus keys please email Prof Henz at henz@comp.nus.edu.sg to request for a with the email subject heading "Request for Luminus API Keys". 2. Install Elixir dependencies ```bash From 20538687585dafa5751b7b9990fbf3a3f2cf5f3d Mon Sep 17 00:00:00 2001 From: travisryte Date: Thu, 2 Apr 2020 01:52:01 +0800 Subject: [PATCH 24/60] Update to allow for xml files --- lib/cadet/course/materialUpload.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet/course/materialUpload.ex b/lib/cadet/course/materialUpload.ex index e1d0374b5..a817eae74 100644 --- a/lib/cadet/course/materialUpload.ex +++ b/lib/cadet/course/materialUpload.ex @@ -5,7 +5,7 @@ defmodule Cadet.Course.MaterialUpload do use Arc.Definition use Arc.Ecto.Definition - @extension_whitelist ~w(.doc .docx .jpg .pdf .png .ppt .pptx .txt .xls .xlsx) + @extension_whitelist ~w(.doc .docx .jpg .pdf .png .ppt .pptx .txt .xls .xlsx .xml) @versions [:original] def bucket, do: :cadet |> Application.fetch_env!(:uploader) |> Keyword.get(:materials_bucket) From ac472d310283496d1e2acdbe8574c2c536a73533 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:05:02 +0800 Subject: [PATCH 25/60] Create add_game_states --- priv/repo/migrations/add_game_states | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 priv/repo/migrations/add_game_states diff --git a/priv/repo/migrations/add_game_states b/priv/repo/migrations/add_game_states new file mode 100644 index 000000000..182be52ba --- /dev/null +++ b/priv/repo/migrations/add_game_states @@ -0,0 +1,8 @@ +defmodule Cadet.Repo.Migrations.AddGameStates do + use Ecto.Migration + def change do + alter table(:users) do + add(:game_states, :map, default: %{collectibles: %{}, completed_quests: []}) + end + end +end From 39791c3c80954ed1525e1290074f11dfcd169f2f Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:05:33 +0800 Subject: [PATCH 26/60] Update router.ex --- lib/cadet_web/router.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 9030ada18..dd9bde768 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -49,10 +49,11 @@ defmodule CadetWeb.Router do get("/notification", NotificationController, :index) post("/notification/acknowledge", NotificationController, :acknowledge) + get("/user", UserController, :index) - put("/user/collectibles", UserController, :collectible_update) - put("/user/save_data/update", UserController, :save_data_update) - put("/user/save_data/clear_up", UserController, :save_data_clear_up) + put("/user/game_states/clear", UserController, :clear_up_game_states) + put("/user/game_states/save", UserController, :update_game_states) + post("/chat/token", ChatController, :index) post("/chat/notify", ChatController, :notify) From 6b421900ff495ed2e12662b9cbc8b644070b26a6 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:06:07 +0800 Subject: [PATCH 27/60] Update user_controller.ex --- lib/cadet_web/controllers/user_controller.ex | 58 ++++++-------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index b0a4c4b02..f6ec22a27 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -5,9 +5,6 @@ defmodule CadetWeb.UserController do use CadetWeb, :controller use PhoenixSwagger - - # import Ecto.Changeset - import Cadet.Assessments import Cadet.GameStates import Ecto.Repo @@ -18,6 +15,7 @@ defmodule CadetWeb.UserController do max_grade = user_max_grade(user) story = user_current_story(user) xp = user_total_xp(user) + update(user, %{ collectibles: %{haha: "haha"}, completed_quests: ["haha"]}) game_states = user_game_states(user) render( conn, @@ -33,64 +31,42 @@ defmodule CadetWeb.UserController do swagger_path :index do get("/user") - summary("Get the name and role of a user") - security([%{JWT: []}]) - produces("application/json") - response(200, "OK", Schema.ref(:UserInfo)) response(401, "Unauthorised") end - def collectibles_update(conn, %{"picnickname" => pic_nickname, "picname" => pic_name}) do + def update_game_states(conn, new_game_states) do user = conn.assigns[:current_user] - Cadet.GameStates.update_collectibles(pic_nickname, pic_name, user) + Cadet.GameStates.update(new_game_states, user) end - swagger_path :collectibles_update do - put("/user/collectibles") - summary("add one collectible to the user") - security([%{JWT: []}]) - consumes("application/json") - produces("application/json") - parameters do - picNickname(:path, :string, "picture nickname", required: true) - questionId(:path, :string, "picture name", required: true) - end - response(200, "OK") - response(400, "Invalid parameters") - response(401, "Unauthorised") - end - - def save_data_update(conn, %{[] => action_sequence, "" => start_location}) do - user = conn.assigns[:current_user] - Cadet.GameStates.update_save_data(action_sequence, start_location, user) - end - swagger_path :save_data_update do - put("/user/save_data/update") - summary("update users' game data saved") + swagger_path :update_game_states do + put("/user/game_states/save") + summary("update user's game states") security([%{JWT: []}]) consumes("application/json") produces("application/json") + parameters do - action(:path, :array, "action sequence", required: true) - startLocation(:path, :string, "start location", required: true) + pic_nicname(:path, :map, "new game states", required: true) end response(200, "OK") response(400, "Invalid parameters") response(401, "Unauthorised") end - def game_state_clear_up(conn, _params) do + + def clear_up_game_states(conn, _) do user = conn.assigns[:current_user] - Cadet.GameStates.clear_up(user) + Cadet.GameStates.clear(user) end - swagger_path :save_data_clear_up do - put("/user/save_data/clear_up") + swagger_path :clear_up_game_states do + put("/user/game_states/clear") summary("clear up users' game data saved") security([%{JWT: []}]) consumes("application/json") @@ -137,10 +113,10 @@ defmodule CadetWeb.UserController do game_states( :map, - "States for user's game, including users' collectibles and save data.\n" - <> "collectibles is a map, and save data is a map with elements action sequence and start location\n" - <> "action sequence is an array of string, and start location is a string.\n" - <> " Value will be a map of empty maps for non-students." + "States for user's game, including users' collectibles and completed quests.\n" + <> "Collectibles are a map.\n" + <> "Completed quests are an array of strings" + ) end end, From 3f2bcb399d1b23ad9fe9ce199f1e8a9ed92b1cb3 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 10 Apr 2020 16:06:57 +0800 Subject: [PATCH 28/60] Update game_states.ex --- lib/cadet/accounts/game_states.ex | 40 +++++-------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex index eb2bbab43..baeb5bf91 100644 --- a/lib/cadet/accounts/game_states.ex +++ b/lib/cadet/accounts/game_states.ex @@ -4,57 +4,29 @@ defmodule Cadet.GameStates do # currently in this module no error handling function # has been implemented yet - # simply return the game states of the user def user_game_states(user) do user.game_states end - # simply return the collectibles of the user, within a single map def user_collectibles(user) do user.game_states["collectibles"] end - # simply return the collectibles of the user, within a single map def user_save_data(user) do - user.game_states["save_data"] + user.game_states["completed_quests"] end - def update_collectibles(pic_nickname, pic_name, user) do + def update(user, new_game_states) do changeset = - Ecto.Changeset.cast(user, %{game_states: %{collectibles: Map.put(user_collectibles(user), pic_nickname, pic_name), - save_data: user_save_data(user)}},[:game_states]) - Cadet.Repo.update!(changeset) - end - - def update_save_data(action_sequence, start_location, user) do - changeset = - Ecto.Changeset.cast(user, %{game_states: %{collectibles: user_collectibles(user), - save_data: %{ - action_sequence: action_sequence, - start_location: start_location - }}},[:game_states]) + Ecto.Changeset.cast(user, %{game_states: + new_game_states},[:game_states]) Cadet.Repo.update!(changeset) end - # functions below are for debugging and testing purposes - def clear_up(user) do + def clear(user) do changeset = Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, - save_data: %{action_sequence: [], start_location: ""}}},[:game_states]) - Cadet.Repo.update!(changeset) - end - - def delete_all_collectibles(user) do - changeset = - Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, save_data: user_save_data(user)}},[:game_states]) - Cadet.Repo.update!(changeset) - end - - def delete_save_data(user) do - changeset = - Ecto.Changeset.cast(user, %{game_states: %{collectibles: user_collectibles(user), - save_data: %{action_sequence: [], start_location: ""}}},[:game_states]) + completed_quests: []}},[:game_states]) Cadet.Repo.update!(changeset) end - end From 71f1ac971ed2ec5c6f53502b3da11dc96b08eb61 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 10 Apr 2020 17:07:44 +0800 Subject: [PATCH 29/60] fixed a consistency problem --- lib/cadet_web/controllers/user_controller.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index f6ec22a27..43f95c0c3 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -41,6 +41,7 @@ defmodule CadetWeb.UserController do def update_game_states(conn, new_game_states) do user = conn.assigns[:current_user] Cadet.GameStates.update(new_game_states, user) + conn end @@ -63,6 +64,7 @@ defmodule CadetWeb.UserController do def clear_up_game_states(conn, _) do user = conn.assigns[:current_user] Cadet.GameStates.clear(user) + conn end swagger_path :clear_up_game_states do From fbdf59220cf73ff8f1b7ede2e70dd2b481adcfe1 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Fri, 10 Apr 2020 17:15:53 +0800 Subject: [PATCH 30/60] Update user_controller.ex --- lib/cadet_web/controllers/user_controller.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 43f95c0c3..f6f8ea6c1 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -15,7 +15,7 @@ defmodule CadetWeb.UserController do max_grade = user_max_grade(user) story = user_current_story(user) xp = user_total_xp(user) - update(user, %{ collectibles: %{haha: "haha"}, completed_quests: ["haha"]}) + game_states = user_game_states(user) render( conn, @@ -40,7 +40,7 @@ defmodule CadetWeb.UserController do def update_game_states(conn, new_game_states) do user = conn.assigns[:current_user] - Cadet.GameStates.update(new_game_states, user) + Cadet.GameStates.update(user, new_game_states) conn end @@ -53,7 +53,7 @@ defmodule CadetWeb.UserController do produces("application/json") parameters do - pic_nicname(:path, :map, "new game states", required: true) + new_game_states(:path, :map, "new game states", required: true) end response(200, "OK") response(400, "Invalid parameters") @@ -116,8 +116,8 @@ defmodule CadetWeb.UserController do game_states( :map, "States for user's game, including users' collectibles and completed quests.\n" - <> "Collectibles are a map.\n" - <> "Completed quests are an array of strings" + <> "Collectibles is a map.\n" + <> "Completed quests is an array of strings" ) end From c0c73c97744381b2b886ee414d4433a615121542 Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Wed, 15 Apr 2020 18:13:17 +0800 Subject: [PATCH 31/60] Changed variable naming --- lib/cadet/assessments/assessments.ex | 30 +++++++++---------- .../controllers/assessments_controller.ex | 6 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index c314c6e69..ac1082149 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -29,7 +29,7 @@ defmodule Cadet.Assessments do assessment = Repo.get(Assessment, id) previous_open_time = assessment.open_at cond do - Timex.before?(close_at, open_at) -> + Timex.before?(close_at, open_at) -> {:error, {:bad_request, "New end date should occur after new opening date"}} Timex.before?(close_at, Timex.now()) -> @@ -40,7 +40,7 @@ defmodule Cadet.Assessments do Timex.before?(open_at, Timex.now()) -> {:error, {:bad_request, "New Opening date should occur after current time"}} - + true -> {:error, {:forbidden, "Assessment is already opened"}} end @@ -49,9 +49,9 @@ defmodule Cadet.Assessments do end end - def toggle_publish_assessment(_publisher = %User{role: role}, id, bool) do + def toggle_publish_assessment(_publisher = %User{role: role}, id, toggle_publish_to) do if role in @publish_assessment_role do - update_assessment(id, %{is_published: bool}) + update_assessment(id, %{is_published: toggle_publish_to}) else {:error, {:forbidden, "User is not permitted to publish"}} end @@ -311,7 +311,7 @@ defmodule Cadet.Assessments do {:ok, assessments} end - def filter_published_assessments(assessments, user) do + def filter_published_assessments(assessments, user) do role = user.role case role do :student -> where(assessments, is_published: true) @@ -362,16 +362,16 @@ defmodule Cadet.Assessments do |> Enum.reduce(assessment_multi, fn {question_params, index}, multi -> Multi.run(multi, String.to_atom("question#{index}"), fn _repo, %{assessment: %Assessment{id: id}} -> - question_exists = + question_exists = Repo.exists?(where(Question, [q], q.assessment_id == ^id and q.display_order == ^index)) - if !force_update or !question_exists do + if !force_update or !question_exists do question_params |> Map.put(:display_order, index) |> build_question_changeset_for_assessment_id(id) |> Repo.insert() else - params = - (if !question_params.max_xp do + params = + (if !question_params.max_xp do question_params |> Map.put(:max_xp, 0) else @@ -379,7 +379,7 @@ defmodule Cadet.Assessments do end) |> Map.put(:display_order, index) - %{id: question_id} = + %{id: question_id} = where(Question, [q], q.display_order == ^index and q.assessment_id == ^id) |> Repo.one() @@ -399,10 +399,10 @@ defmodule Cadet.Assessments do |> elem(1) |> elem(1)).data.id - if !assessment_id do - false + if !assessment_id do + false else - existing_questions_count = + existing_questions_count = where(Question, [q], q.assessment_id == ^assessment_id) |> Repo.all() |> Enum.count @@ -410,7 +410,7 @@ defmodule Cadet.Assessments do new_questions_count = Enum.count(questions_params) existing_questions_count != new_questions_count - end + end end @spec insert_or_update_assessment_changeset(map(), boolean()) :: Ecto.Changeset.t() @@ -435,7 +435,7 @@ defmodule Cadet.Assessments do force_update -> # Maintain the same open/close date when force updating an assessment - new_params = + new_params = params |> Map.delete(:open_at) |> Map.delete(:close_at) diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index fd23306b4..b4a6fcaca 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -35,8 +35,8 @@ defmodule CadetWeb.AssessmentsController do end end - def update(conn, %{"id" => id, "bool" => bool}) do - result = Assessments.toggle_publish_assessment(conn.assigns.current_user, id, bool) + def update(conn, %{"id" => id, "togglePublishTo" => toggle_publish_to}) do + result = Assessments.toggle_publish_assessment(conn.assigns.current_user, id, toggle_publish_to) case result do {:ok, _nil} -> @@ -82,7 +82,7 @@ defmodule CadetWeb.AssessmentsController do def create(conn, %{"assessment" => assessment, "forceUpdate" => force_update}) do file = assessment["file"].path |> File.read!() - result = + result = case force_update do "true" -> parse_xml(file, true) "false" -> parse_xml(file, false) From e3eddd690bf1c1d93c10bcd6ca86686218c219e9 Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Wed, 15 Apr 2020 20:50:49 +0800 Subject: [PATCH 32/60] Fixed bug when force updating assessments that are not opened. Added requirement that all question types remain the same when force updating --- lib/cadet/assessments/assessments.ex | 39 ++++++++++++++++++---------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index ac1082149..6fc510994 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -354,7 +354,7 @@ defmodule Cadet.Assessments do insert_or_update_assessment_changeset(assessment_params, force_update) ) - if force_update and check_question_count(assessment_multi, questions_params) do + if force_update and invalid_force_update(assessment_multi, questions_params) do {:error, "Question count is different"} else questions_params @@ -364,6 +364,7 @@ defmodule Cadet.Assessments do %{assessment: %Assessment{id: id}} -> question_exists = Repo.exists?(where(Question, [q], q.assessment_id == ^id and q.display_order == ^index)) + # the !question_exists check allows for force updating of brand new assessments if !force_update or !question_exists do question_params |> Map.put(:display_order, index) @@ -379,12 +380,16 @@ defmodule Cadet.Assessments do end) |> Map.put(:display_order, index) - %{id: question_id} = + %{id: question_id, type: type} = where(Question, [q], q.display_order == ^index and q.assessment_id == ^id) |> Repo.one() - changeset = Question.changeset(%Question{assessment_id: id, id: question_id}, params) - Repo.update(changeset) + if question_params.type != Atom.to_string(type) do + {:error, create_invalid_changeset_with_error(:question, "Question types should remain the same")} + else + changeset = Question.changeset(%Question{assessment_id: id, id: question_id}, params) + Repo.update(changeset) + end end end) end) @@ -392,24 +397,31 @@ defmodule Cadet.Assessments do end end - defp check_question_count(assessment_multi, questions_params) do + # Function that checks if the force update is invalid. The force update is only invalid + # if the new question count is different from the old question count. + defp invalid_force_update(assessment_multi, questions_params) do assessment_id = (assessment_multi.operations |> List.first() |> elem(1) |> elem(1)).data.id + # check if assessment already exists if !assessment_id do false else - existing_questions_count = - where(Question, [q], q.assessment_id == ^assessment_id) - |> Repo.all() - |> Enum.count - - new_questions_count = Enum.count(questions_params) - - existing_questions_count != new_questions_count + open_date = Repo.get(Assessment, assessment_id).open_at + # check if assessment is already opened + if Timex.after?(open_date, Timex.now()) do + false + else + existing_questions_count = + where(Question, [q], q.assessment_id == ^assessment_id) + |> Repo.all() + |> Enum.count + new_questions_count = Enum.count(questions_params) + existing_questions_count != new_questions_count + end end end @@ -439,6 +451,7 @@ defmodule Cadet.Assessments do params |> Map.delete(:open_at) |> Map.delete(:close_at) + |> Map.delete(:is_published) Assessment.changeset(assessment, new_params) From 72d9744ace7b3b7eaaa691b695c3cef7cc9a5953 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 02:45:56 +0800 Subject: [PATCH 33/60] Delete 20200402094115_add_user_game_states.exs --- .../20200402094115_add_user_game_states.exs | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 priv/repo/migrations/20200402094115_add_user_game_states.exs diff --git a/priv/repo/migrations/20200402094115_add_user_game_states.exs b/priv/repo/migrations/20200402094115_add_user_game_states.exs deleted file mode 100644 index 0d2e7bbc4..000000000 --- a/priv/repo/migrations/20200402094115_add_user_game_states.exs +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Cadet.Repo.Migrations.AddUserGameStates do - use Ecto.Migration - - def change do - alter table(:users) do - add(:game_states, :map, default: %{collectibles: %{}, save_data: %{ - action_sequence: [], start_location: "" - }}) - end - end -end From 9d482c1971ed865cf8fc000b1568f5363f7e85ac Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 02:52:17 +0800 Subject: [PATCH 34/60] Delete add_game_states --- priv/repo/migrations/add_game_states | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 priv/repo/migrations/add_game_states diff --git a/priv/repo/migrations/add_game_states b/priv/repo/migrations/add_game_states deleted file mode 100644 index 182be52ba..000000000 --- a/priv/repo/migrations/add_game_states +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Cadet.Repo.Migrations.AddGameStates do - use Ecto.Migration - def change do - alter table(:users) do - add(:game_states, :map, default: %{collectibles: %{}, completed_quests: []}) - end - end -end From 53141eaad94b72cd57a2e764689801fc6e528f6b Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 02:52:51 +0800 Subject: [PATCH 35/60] Add files via upload --- priv/repo/migrations/20200410074625_add_game_states.exs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 priv/repo/migrations/20200410074625_add_game_states.exs diff --git a/priv/repo/migrations/20200410074625_add_game_states.exs b/priv/repo/migrations/20200410074625_add_game_states.exs new file mode 100644 index 000000000..182be52ba --- /dev/null +++ b/priv/repo/migrations/20200410074625_add_game_states.exs @@ -0,0 +1,8 @@ +defmodule Cadet.Repo.Migrations.AddGameStates do + use Ecto.Migration + def change do + alter table(:users) do + add(:game_states, :map, default: %{collectibles: %{}, completed_quests: []}) + end + end +end From 8c956cfd8e9f1452f44869adfcc46c1fb5cbfbc3 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 02:55:42 +0800 Subject: [PATCH 36/60] Update user_view.ex --- lib/cadet_web/views/user_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 71856bd00..0c4ddfe61 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -13,7 +13,7 @@ defmodule CadetWeb.UserView do story: :story, playStory: :play_story? }), - game_states: game_states + gameStates: game_states } end end From 34c4567e84df5bb475c6220f1ed39404f3b21b55 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 03:43:00 +0800 Subject: [PATCH 37/60] Fixed no status codes error Also, satisfied frontend's need to make gameStates an object to input. Maintained consistency with other parts of the backend. --- lib/cadet_web/controllers/user_controller.ex | 34 +++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index f6f8ea6c1..645281944 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -15,7 +15,6 @@ defmodule CadetWeb.UserController do max_grade = user_max_grade(user) story = user_current_story(user) xp = user_total_xp(user) - game_states = user_game_states(user) render( conn, @@ -38,33 +37,44 @@ defmodule CadetWeb.UserController do response(401, "Unauthorised") end - def update_game_states(conn, new_game_states) do + def update_game_states(conn, %{"gameStates" => new_game_states}) do user = conn.assigns[:current_user] - Cadet.GameStates.update(user, new_game_states) - conn + case Cadet.GameStates.update(user, new_game_states) do + {:ok, nil} -> + text(conn, "OK") + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end end - swagger_path :update_game_states do put("/user/game_states/save") summary("update user's game states") security([%{JWT: []}]) consumes("application/json") produces("application/json") - parameters do new_game_states(:path, :map, "new game states", required: true) end - response(200, "OK") + response(200, "OK", Schema.ref(:UserInfo)) + response(201, "Created") + response(204, "No Content") response(400, "Invalid parameters") response(401, "Unauthorised") end - def clear_up_game_states(conn, _) do user = conn.assigns[:current_user] - Cadet.GameStates.clear(user) - conn + case Cadet.GameStates.clear(user) do + {:ok, nil} -> + text(conn, "OK") + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end end swagger_path :clear_up_game_states do @@ -73,7 +83,9 @@ defmodule CadetWeb.UserController do security([%{JWT: []}]) consumes("application/json") produces("application/json") - response(200, "OK") + response(200, "OK", Schema.ref(:UserInfo)) + response(201, "Created") + response(204, "No Content") response(401, "Unauthorised") end From 0f59ab05d18d5f02ee29c41845ca68de9761df9d Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 03:44:25 +0800 Subject: [PATCH 38/60] Solved no status codes error --- lib/cadet/accounts/game_states.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex index baeb5bf91..fad7cf409 100644 --- a/lib/cadet/accounts/game_states.ex +++ b/lib/cadet/accounts/game_states.ex @@ -21,6 +21,7 @@ defmodule Cadet.GameStates do Ecto.Changeset.cast(user, %{game_states: new_game_states},[:game_states]) Cadet.Repo.update!(changeset) + {:ok, nil} end def clear(user) do @@ -28,5 +29,6 @@ defmodule Cadet.GameStates do Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, completed_quests: []}},[:game_states]) Cadet.Repo.update!(changeset) + {:ok, nil} end end From efbc305a198320298e9fbb5bceab938a6d2b4bad Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 11:19:24 +0800 Subject: [PATCH 39/60] Update game_states.ex --- lib/cadet/accounts/game_states.ex | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex index fad7cf409..82f9705ba 100644 --- a/lib/cadet/accounts/game_states.ex +++ b/lib/cadet/accounts/game_states.ex @@ -8,6 +8,7 @@ defmodule Cadet.GameStates do user.game_states end + @spec user_collectibles(atom | %{game_states: nil | maybe_improper_list | map}) :: any def user_collectibles(user) do user.game_states["collectibles"] end @@ -17,18 +18,26 @@ defmodule Cadet.GameStates do end def update(user, new_game_states) do - changeset = - Ecto.Changeset.cast(user, %{game_states: - new_game_states},[:game_states]) - Cadet.Repo.update!(changeset) - {:ok, nil} + if user.role == "student" do + changeset = + Ecto.Changeset.cast(user, %{game_states: + new_game_states},[:game_states]) + Cadet.Repo.update!(changeset) + {:ok, nil} + else + {:error, {:forbidden, "Please try again later."}} + end end def clear(user) do - changeset = - Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, - completed_quests: []}},[:game_states]) - Cadet.Repo.update!(changeset) - {:ok, nil} + if user.role == "student" do + changeset = + Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, + completed_quests: []}},[:game_states]) + Cadet.Repo.update!(changeset) + {:ok, nil} + else + {:error, {:forbidden, "Please try again later."}} + end end end From 3d5e37d9efe4f011e197fccbe17a502665c2bc65 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 11:19:56 +0800 Subject: [PATCH 40/60] Added status codes information --- lib/cadet/accounts/game_states.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex index 82f9705ba..ca469ec15 100644 --- a/lib/cadet/accounts/game_states.ex +++ b/lib/cadet/accounts/game_states.ex @@ -8,7 +8,6 @@ defmodule Cadet.GameStates do user.game_states end - @spec user_collectibles(atom | %{game_states: nil | maybe_improper_list | map}) :: any def user_collectibles(user) do user.game_states["collectibles"] end From 3ca9dc6a04e5ba08dc11e1b24aee4fc1e561c5bb Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 11:23:20 +0800 Subject: [PATCH 41/60] Added status codes information --- lib/cadet_web/controllers/user_controller.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 645281944..65760ba74 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -42,6 +42,7 @@ defmodule CadetWeb.UserController do case Cadet.GameStates.update(user, new_game_states) do {:ok, nil} -> text(conn, "OK") + {:error, {status, message}} -> conn |> put_status(status) From 3c3c780b8210c3f19fc25eec713715167f2456af Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 16 Apr 2020 11:44:32 +0800 Subject: [PATCH 42/60] Add function to differentiate students from others --- lib/cadet/accounts/game_states.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex index ca469ec15..704444cd4 100644 --- a/lib/cadet/accounts/game_states.ex +++ b/lib/cadet/accounts/game_states.ex @@ -17,7 +17,7 @@ defmodule Cadet.GameStates do end def update(user, new_game_states) do - if user.role == "student" do + if user.role == :student do changeset = Ecto.Changeset.cast(user, %{game_states: new_game_states},[:game_states]) @@ -29,7 +29,7 @@ defmodule Cadet.GameStates do end def clear(user) do - if user.role == "student" do + if user.role == :student do changeset = Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, completed_quests: []}},[:game_states]) From b39ad61974ff04229da5177c66c4df43b007557b Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Sun, 19 Apr 2020 22:15:26 +0800 Subject: [PATCH 43/60] Added swagger paths --- lib/cadet/assessments/assessments.ex | 2 +- lib/cadet/jobs/xml_parser.ex | 20 ++-- .../controllers/assessments_controller.ex | 107 +++++++++++++++--- lib/cadet_web/router.ex | 2 +- 4 files changed, 101 insertions(+), 30 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 6fc510994..ea6488a95 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -42,7 +42,7 @@ defmodule Cadet.Assessments do {:error, {:bad_request, "New Opening date should occur after current time"}} true -> - {:error, {:forbidden, "Assessment is already opened"}} + {:error, {:unauthorized, "Assessment is already opened"}} end else {:error, {:forbidden, "User is not permitted to edit"}} diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index 87e80aed7..239d60ce5 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -102,33 +102,33 @@ defmodule Cadet.Updater.XMLParser do {:error, stage, changeset, _} when is_atom(stage) -> log_error_bad_changeset(changeset, stage) - changeset_error = + changeset_error = changeset |> Map.get(:errors) |> extract_changeset_error_message - error_message = "Invalid #{stage} changeset " <> changeset_error + error_message = "Invalid #{stage} changeset #{changeset_error}" log_and_return_badrequest(error_message) end catch # the :erlsom library used by SweetXml will exit if XML is invalid :exit, parse_error -> - error_message = + error_message = parse_error |> nested_tuple_to_list() |> List.flatten() - |> Enum.reduce("", fn x, acc -> acc <> to_string(x) <> " " end) - {:error, {:bad_request, "Invalid XML " <> error_message}} + |> Enum.reduce("", fn x, acc -> "#{acc <> to_string(x)} " end) + {:error, {:bad_request, "Invalid XML #{error_message}"}} end defp extract_changeset_error_message(errors_list) do errors_list - |> Enum.map(fn {field, {error, _}} -> to_string(field) <> " " <> error end) - |> List.foldr("", fn x, acc -> acc <> x <> " " end) + |> Enum.map(fn {field, {error, _}} -> "#{to_string(field)} #{error}" end) + |> List.foldr("", fn x, acc -> "#{acc <> x} " end) end @spec process_assessment(String.t()) :: {:ok, map()} | {:error, String.t} defp process_assessment(xml) do - open_at = + open_at = Timex.now() |> Timex.beginning_of_day() |> Timex.shift(days: 3) @@ -164,7 +164,7 @@ defmodule Cadet.Updater.XMLParser do end {:ok, assessment_params} - + rescue # This error is raised by xpath/3 when TASK does not exist (hence is equal to nil) Protocol.UndefinedError -> @@ -213,7 +213,7 @@ defmodule Cadet.Updater.XMLParser do else {:no_missing_attr?, false} -> {:error, "Missing attribute(s) on PROBLEM"} - + {:error, errmsg} -> {:error, errmsg} end diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index b4a6fcaca..b521cf854 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -35,7 +35,7 @@ defmodule CadetWeb.AssessmentsController do end end - def update(conn, %{"id" => id, "togglePublishTo" => toggle_publish_to}) do + def publish(conn, %{"id" => id, "togglePublishTo" => toggle_publish_to}) do result = Assessments.toggle_publish_assessment(conn.assigns.current_user, id, toggle_publish_to) case result do @@ -80,27 +80,32 @@ defmodule CadetWeb.AssessmentsController do end def create(conn, %{"assessment" => assessment, "forceUpdate" => force_update}) do - file = assessment["file"].path + role = conn.assigns[:current_user].role + if role == :student do + send_resp(conn, :forbidden, "User not allowed to create") + else + file = assessment["file"].path |> File.read!() - result = - case force_update do - "true" -> parse_xml(file, true) - "false" -> parse_xml(file, false) - end - - case result do - :ok -> - if (force_update == "true") do - send_resp(conn, 200, "Force Update OK") - else - send_resp(conn, 200, "OK") + result = + case force_update do + "true" -> parse_xml(file, true) + "false" -> parse_xml(file, false) end - {:ok, warning_message} -> - send_resp(conn, 200, warning_message) + case result do + :ok -> + if (force_update == "true") do + send_resp(conn, 200, "Force Update OK") + else + send_resp(conn, 200, "OK") + end - {:error, {status, message}} -> - send_resp(conn, status, message) + {:ok, warning_message} -> + send_resp(conn, 200, warning_message) + + {:error, {status, message}} -> + send_resp(conn, status, message) + end end end @@ -153,6 +158,72 @@ defmodule CadetWeb.AssessmentsController do response(403, "Password incorrect") end + swagger_path :create do + post("/assessments") + + summary("Creates a new assessment or updates an existing assessment") + + security([%{JWT: []}]) + + parameters do + assessment(:body, :file, "assessment to create or update", required: true) + forceUpdate(:body, :boolean, "force update", required: true) + end + + response(200, "OK") + response(400, "XML parse error") + response(403, "User not allowed to create") + end + + swagger_path :delete do + PhoenixSwagger.Path.delete("/assessments/:id") + + summary("Deletes an assessment") + + security([%{JWT: []}]) + + parameters do + assessmentId(:path, :integer, "assessment id", required: true) + end + + response(200, "OK") + response(403, "User is not permitted to delete") + end + + swagger_path :publish do + post("/assessments/publish/:id") + + summary("Toggles an assessment between published and unpublished") + + security([%{JWT: []}]) + + parameters do + assessmentId(:path, :integer, "assessment id", required: true) + togglePublishTo(:body, :boolean, "toggles assessment publish state", required: true) + end + + response(200, "OK") + response(403, "User is not permitted to publish") + end + + swagger_path :update do + post("/assessments/update/:id") + + summary("Changes the open/close date of an assessment") + + security([%{JWT: []}]) + + parameters do + assessmentId(:path, :integer, "assessment id", required: true) + closeAt(:body, :string, "open date", required: true) + openAt(:body, :string, "close date", required: true) + end + + response(200, "OK") + response(401, "Assessment is already opened") + response(403, "User is not permitted to edit") + end + def swagger_definitions do %{ AssessmentsList: diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index be2e2369c..5de07ee45 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -40,7 +40,7 @@ defmodule CadetWeb.Router do post("/assessments", AssessmentsController, :create) delete("/assessments/:id", AssessmentsController, :delete) post("/assessments/:id", AssessmentsController, :show) - post("/assessments/publish/:id", AssessmentsController, :update) + post("/assessments/publish/:id", AssessmentsController, :publish) post("/assessments/update/:id", AssessmentsController, :update) post("/assessments/:assessmentid/submit", AssessmentsController, :submit) post("/assessments/question/:questionid/submit", AnswerController, :submit) From 1a077575335a37ca702e2eb7275a5637c69afe97 Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Sun, 19 Apr 2020 22:22:55 +0800 Subject: [PATCH 44/60] Fixed format --- lib/cadet/assessments/assessments.ex | 38 ++++++++++++++----- lib/cadet/jobs/xml_parser.ex | 17 +++++---- .../controllers/assessments_controller.ex | 21 +++++++--- 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index ea6488a95..7ef40e078 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -28,6 +28,7 @@ defmodule Cadet.Assessments do if role in @change_dates_assessment_role do assessment = Repo.get(Assessment, id) previous_open_time = assessment.open_at + cond do Timex.before?(close_at, open_at) -> {:error, {:bad_request, "New end date should occur after new opening date"}} @@ -209,6 +210,7 @@ defmodule Cadet.Assessments do def assessment_with_questions_and_answers(id, user = %User{}, password) when is_ecto_id(id) do role = user.role + assessment = if role in @open_all_assessment_roles do Assessment @@ -313,6 +315,7 @@ defmodule Cadet.Assessments do def filter_published_assessments(assessments, user) do role = user.role + case role do :student -> where(assessments, is_published: true) _ -> assessments @@ -346,7 +349,11 @@ defmodule Cadet.Assessments do @spec insert_or_update_assessments_and_questions(map(), [map()], boolean()) :: {:ok, any()} | {:error, Ecto.Multi.name(), any(), %{optional(Ecto.Multi.name()) => any()}} - def insert_or_update_assessments_and_questions(assessment_params, questions_params, force_update) do + def insert_or_update_assessments_and_questions( + assessment_params, + questions_params, + force_update + ) do assessment_multi = Multi.insert_or_update( Multi.new(), @@ -363,7 +370,10 @@ defmodule Cadet.Assessments do Multi.run(multi, String.to_atom("question#{index}"), fn _repo, %{assessment: %Assessment{id: id}} -> question_exists = - Repo.exists?(where(Question, [q], q.assessment_id == ^id and q.display_order == ^index)) + Repo.exists?( + where(Question, [q], q.assessment_id == ^id and q.display_order == ^index) + ) + # the !question_exists check allows for force updating of brand new assessments if !force_update or !question_exists do question_params @@ -372,12 +382,12 @@ defmodule Cadet.Assessments do |> Repo.insert() else params = - (if !question_params.max_xp do + if !question_params.max_xp do question_params |> Map.put(:max_xp, 0) else question_params - end) + end |> Map.put(:display_order, index) %{id: question_id, type: type} = @@ -385,9 +395,15 @@ defmodule Cadet.Assessments do |> Repo.one() if question_params.type != Atom.to_string(type) do - {:error, create_invalid_changeset_with_error(:question, "Question types should remain the same")} + {:error, + create_invalid_changeset_with_error( + :question, + "Question types should remain the same" + )} else - changeset = Question.changeset(%Question{assessment_id: id, id: question_id}, params) + changeset = + Question.changeset(%Question{assessment_id: id, id: question_id}, params) + Repo.update(changeset) end end @@ -402,9 +418,9 @@ defmodule Cadet.Assessments do defp invalid_force_update(assessment_multi, questions_params) do assessment_id = (assessment_multi.operations - |> List.first() - |> elem(1) - |> elem(1)).data.id + |> List.first() + |> elem(1) + |> elem(1)).data.id # check if assessment already exists if !assessment_id do @@ -418,7 +434,8 @@ defmodule Cadet.Assessments do existing_questions_count = where(Question, [q], q.assessment_id == ^assessment_id) |> Repo.all() - |> Enum.count + |> Enum.count() + new_questions_count = Enum.count(questions_params) existing_questions_count != new_questions_count end @@ -433,6 +450,7 @@ defmodule Cadet.Assessments do |> case do nil -> Assessment.changeset(%Assessment{}, params) + assessment -> cond do Timex.after?(assessment.open_at, Timex.now()) -> diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index 239d60ce5..849606f45 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -77,7 +77,8 @@ defmodule Cadet.Updater.XMLParser do end end - @spec parse_xml(String.t(), boolean()) :: :ok | {:ok, String.t} | {:error, {atom(), String.t}} + @spec parse_xml(String.t(), boolean()) :: + :ok | {:ok, String.t()} | {:error, {atom(), String.t()}} def parse_xml(xml, force_update \\ false) do with {:ok, assessment_params} <- process_assessment(xml), {:ok, questions_params} <- process_questions(xml), @@ -102,10 +103,12 @@ defmodule Cadet.Updater.XMLParser do {:error, stage, changeset, _} when is_atom(stage) -> log_error_bad_changeset(changeset, stage) + changeset_error = changeset |> Map.get(:errors) |> extract_changeset_error_message + error_message = "Invalid #{stage} changeset #{changeset_error}" log_and_return_badrequest(error_message) end @@ -117,6 +120,7 @@ defmodule Cadet.Updater.XMLParser do |> nested_tuple_to_list() |> List.flatten() |> Enum.reduce("", fn x, acc -> "#{acc <> to_string(x)} " end) + {:error, {:bad_request, "Invalid XML #{error_message}"}} end @@ -126,7 +130,7 @@ defmodule Cadet.Updater.XMLParser do |> List.foldr("", fn x, acc -> "#{acc <> x} " end) end - @spec process_assessment(String.t()) :: {:ok, map()} | {:error, String.t} + @spec process_assessment(String.t()) :: {:ok, map()} | {:error, String.t()} defp process_assessment(xml) do open_at = Timex.now() @@ -164,7 +168,6 @@ defmodule Cadet.Updater.XMLParser do end {:ok, assessment_params} - rescue # This error is raised by xpath/3 when TASK does not exist (hence is equal to nil) Protocol.UndefinedError -> @@ -188,7 +191,7 @@ defmodule Cadet.Updater.XMLParser do type end - @spec process_questions(String.t()) :: {:ok, [map()]} | {:error, String.t} + @spec process_questions(String.t()) :: {:ok, [map()]} | {:error, String.t()} defp process_questions(xml) do default_library = xpath(xml, ~x"//TASK/DEPLOYMENT"e) default_grading_library = xpath(xml, ~x"//TASK/GRADERDEPLOYMENT"e) @@ -234,7 +237,7 @@ defmodule Cadet.Updater.XMLParser do Logger.error("Changeset: #{inspect(changeset, pretty: true)}") end - @spec process_question_by_question_type(map()) :: map() | {:error, String.t} + @spec process_question_by_question_type(map()) :: map() | {:error, String.t()} defp process_question_by_question_type(question) do question[:entity] |> process_question_entity_by_type(question[:type]) @@ -297,7 +300,7 @@ defmodule Cadet.Updater.XMLParser do {:error, "Invalid question type"} end - @spec process_question_library(map(), any(), any()) :: map() | {:error, String.t} + @spec process_question_library(map(), any(), any()) :: map() | {:error, String.t()} defp process_question_library(question, default_library, default_grading_library) do library = xpath(question[:entity], ~x"./DEPLOYMENT"o) || default_library @@ -361,7 +364,7 @@ defmodule Cadet.Updater.XMLParser do end defp nested_tuple_to_list(tuple) when is_tuple(tuple) do - tuple |> Tuple.to_list |> Enum.map(&nested_tuple_to_list/1) + tuple |> Tuple.to_list() |> Enum.map(&nested_tuple_to_list/1) end defp nested_tuple_to_list(x), do: x diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index b521cf854..b0b355afe 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -36,7 +36,8 @@ defmodule CadetWeb.AssessmentsController do end def publish(conn, %{"id" => id, "togglePublishTo" => toggle_publish_to}) do - result = Assessments.toggle_publish_assessment(conn.assigns.current_user, id, toggle_publish_to) + result = + Assessments.toggle_publish_assessment(conn.assigns.current_user, id, toggle_publish_to) case result do {:ok, _nil} -> @@ -52,7 +53,14 @@ defmodule CadetWeb.AssessmentsController do def update(conn, %{"id" => id, "closeAt" => close_at, "openAt" => open_at}) do formatted_close_date = elem(DateTime.from_iso8601(close_at), 1) formatted_open_date = elem(DateTime.from_iso8601(open_at), 1) - result = Assessments.change_dates_assessment(conn.assigns.current_user, id, formatted_close_date, formatted_open_date) + + result = + Assessments.change_dates_assessment( + conn.assigns.current_user, + id, + formatted_close_date, + formatted_open_date + ) case result do {:ok, _nil} -> @@ -81,11 +89,14 @@ defmodule CadetWeb.AssessmentsController do def create(conn, %{"assessment" => assessment, "forceUpdate" => force_update}) do role = conn.assigns[:current_user].role + if role == :student do send_resp(conn, :forbidden, "User not allowed to create") else - file = assessment["file"].path - |> File.read!() + file = + assessment["file"].path + |> File.read!() + result = case force_update do "true" -> parse_xml(file, true) @@ -94,7 +105,7 @@ defmodule CadetWeb.AssessmentsController do case result do :ok -> - if (force_update == "true") do + if force_update == "true" do send_resp(conn, 200, "Force Update OK") else send_resp(conn, 200, "OK") From 7663bf20453e69b470d2557f6d0d8250ba38a383 Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Mon, 20 Apr 2020 02:26:25 +0800 Subject: [PATCH 45/60] Modified AssessmentsController tests to fit with changes to how the is_published parameter is now handled --- .../assessments_controller_test.exs | 224 ++++++++++++------ 1 file changed, 151 insertions(+), 73 deletions(-) diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index a9cd2e7b1..35ddbe2f8 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -64,7 +64,8 @@ defmodule CadetWeb.AssessmentsControllerTest do "maxXp" => 4500, "status" => get_assessment_status(user, &1), "gradingStatus" => "excluded", - "private" => false + "private" => false, + "isPublished" => &1.is_published } ) @@ -80,7 +81,7 @@ defmodule CadetWeb.AssessmentsControllerTest do end end - test "does not render unpublished assessments", %{ + test "render password protected assessments properly", %{ conn: conn, users: users, assessments: assessments @@ -90,74 +91,73 @@ defmodule CadetWeb.AssessmentsControllerTest do {:ok, _} = mission.assessment - |> Assessment.changeset(%{is_published: false}) + |> Assessment.changeset(%{password: "mysupersecretpassword"}) |> Repo.update() - expected = - assessments - |> Map.delete(:mission) - |> Map.values() - |> Enum.map(fn a -> a.assessment end) - |> Enum.sort(&open_at_asc_comparator/2) - |> Enum.map( - &%{ - "id" => &1.id, - "title" => &1.title, - "shortSummary" => &1.summary_short, - "story" => &1.story, - "number" => &1.number, - "reading" => &1.reading, - "openAt" => format_datetime(&1.open_at), - "closeAt" => format_datetime(&1.close_at), - "type" => "#{&1.type}", - "coverImage" => &1.cover_picture, - "maxGrade" => 720, - "maxXp" => 4500, - "status" => get_assessment_status(user, &1), - "gradingStatus" => "excluded", - "private" => false - } - ) - resp = conn |> sign_in(user) |> get(build_url()) |> json_response(200) - |> Enum.map(&Map.delete(&1, "xp")) - |> Enum.map(&Map.delete(&1, "grade")) + |> Enum.find(&(&1["type"] == "mission")) + |> Map.get("private") - assert expected == resp + assert resp == true end end + end - test "render password protected assessments properly", %{ + describe "GET /, student only" do + test "does not render unpublished assessments", %{ conn: conn, - users: users, + users: %{student: student}, assessments: assessments } do - for {_role, user} <- users do - mission = assessments.mission + mission = assessments.mission - {:ok, _} = - mission.assessment - |> Assessment.changeset(%{password: "mysupersecretpassword"}) - |> Repo.update() + {:ok, _} = + mission.assessment + |> Assessment.changeset(%{is_published: false}) + |> Repo.update() - resp = - conn - |> sign_in(user) - |> get(build_url()) - |> json_response(200) - |> Enum.find(&(&1["type"] == "mission")) - |> Map.get("private") + expected = + assessments + |> Map.delete(:mission) + |> Map.values() + |> Enum.map(fn a -> a.assessment end) + |> Enum.sort(&open_at_asc_comparator/2) + |> Enum.map( + &%{ + "id" => &1.id, + "title" => &1.title, + "shortSummary" => &1.summary_short, + "story" => &1.story, + "number" => &1.number, + "reading" => &1.reading, + "openAt" => format_datetime(&1.open_at), + "closeAt" => format_datetime(&1.close_at), + "type" => "#{&1.type}", + "coverImage" => &1.cover_picture, + "maxGrade" => 720, + "maxXp" => 4500, + "status" => get_assessment_status(student, &1), + "gradingStatus" => "excluded", + "private" => false, + "isPublished" => &1.is_published + } + ) - assert resp == true - end + resp = + conn + |> sign_in(student) + |> get(build_url()) + |> json_response(200) + |> Enum.map(&Map.delete(&1, "xp")) + |> Enum.map(&Map.delete(&1, "grade")) + + assert expected == resp end - end - describe "GET /, student only" do test "renders student submission status in overview", %{ conn: conn, users: %{student: student}, @@ -220,6 +220,65 @@ defmodule CadetWeb.AssessmentsControllerTest do end end + describe "GET /, non-students" do + test "renders unpublished assessments", %{ + conn: conn, + users: users, + assessments: assessments + } do + for role <- ~w(staff admin)a do + user = Map.get(users, role) + mission = assessments.mission + + {:ok, _} = + mission.assessment + |> Assessment.changeset(%{is_published: false}) + |> Repo.update() + + resp = + conn + |> sign_in(user) + |> get(build_url()) + |> json_response(200) + |> Enum.map(&Map.delete(&1, "xp")) + |> Enum.map(&Map.delete(&1, "grade")) + + expected = + assessments + |> Map.values() + |> Enum.map(fn a -> a.assessment end) + |> Enum.sort(&open_at_asc_comparator/2) + |> Enum.map( + &%{ + "id" => &1.id, + "title" => &1.title, + "shortSummary" => &1.summary_short, + "story" => &1.story, + "number" => &1.number, + "reading" => &1.reading, + "openAt" => format_datetime(&1.open_at), + "closeAt" => format_datetime(&1.close_at), + "type" => "#{&1.type}", + "coverImage" => &1.cover_picture, + "maxGrade" => 720, + "maxXp" => 4500, + "status" => get_assessment_status(user, &1), + "gradingStatus" => "excluded", + "private" => false, + "isPublished" => + if &1.type == :mission do + false + else + &1.is_published + end + } + ) + + assert expected == resp + end + end + end + describe "POST /assessment_id, all roles" do test "it renders assessment details", %{ conn: conn, @@ -499,28 +558,6 @@ defmodule CadetWeb.AssessmentsControllerTest do end end end - - test "it does not permit access to unpublished assessments", %{ - conn: conn, - users: users, - assessments: %{mission: mission} - } do - for role <- Role.__enum_map__() do - user = Map.get(users, role) - - {:ok, _} = - mission.assessment - |> Assessment.changeset(%{is_published: false}) - |> Repo.update() - - conn = - conn - |> sign_in(user) - |> post(build_url(mission.assessment.id)) - - assert response(conn, 400) == "Assessment not found" - end - end end describe "POST /assessment_id, student" do @@ -601,6 +638,24 @@ defmodule CadetWeb.AssessmentsControllerTest do assert response(conn, 401) == "Assessment not open" end + + test "it does not permit access to unpublished assessments", %{ + conn: conn, + users: %{student: student}, + assessments: %{mission: mission} + } do + {:ok, _} = + mission.assessment + |> Assessment.changeset(%{is_published: false}) + |> Repo.update() + + conn = + conn + |> sign_in(student) + |> post(build_url(mission.assessment.id)) + + assert response(conn, 400) == "Assessment not found" + end end describe "POST /assessment_id, non-students" do @@ -650,6 +705,29 @@ defmodule CadetWeb.AssessmentsControllerTest do assert resp["id"] == mission.assessment.id end end + + test "it permits access to unpublished assessments", %{ + conn: conn, + users: users, + assessments: %{mission: mission} + } do + for role <- ~w(staff admin)a do + user = Map.get(users, role) + + {:ok, _} = + mission.assessment + |> Assessment.changeset(%{is_published: false}) + |> Repo.update() + + resp = + conn + |> sign_in(user) + |> post(build_url(mission.assessment.id)) + |> json_response(200) + + assert resp["id"] == mission.assessment.id + end + end end describe "POST /assessment_id/submit unauthenticated" do From 159a2d3512875140354f9f2b84fb725ac5a005b7 Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Mon, 20 Apr 2020 13:39:55 +0800 Subject: [PATCH 46/60] Updated XML parsers tests --- lib/cadet/jobs/xml_parser.ex | 19 ++++-- test/cadet/updater/xml_parser_test.exs | 93 +++++++++++++------------- 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index 849606f45..38e415b26 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -68,6 +68,10 @@ defmodule Cadet.Updater.XMLParser do Logger.error(error_message) Sentry.capture_message(error_message) :error + + {:error, {_status, error_message}} -> + Sentry.capture_message(error_message) + :error end end |> Enum.any?(&(&1 == :error)) @@ -98,8 +102,8 @@ defmodule Cadet.Updater.XMLParser do Logger.warn("Assessment already open, ignoring...") {:ok, "Assessment already open, ignoring..."} - {:error, errmsg} -> - log_and_return_badrequest(errmsg) + {:error, error_message} -> + log_and_return_badrequest(error_message) {:error, stage, changeset, _} when is_atom(stage) -> log_error_bad_changeset(changeset, stage) @@ -115,6 +119,7 @@ defmodule Cadet.Updater.XMLParser do catch # the :erlsom library used by SweetXml will exit if XML is invalid :exit, parse_error -> + # error info is stored in multiple nested tuples error_message = parse_error |> nested_tuple_to_list() @@ -217,8 +222,8 @@ defmodule Cadet.Updater.XMLParser do {:no_missing_attr?, false} -> {:error, "Missing attribute(s) on PROBLEM"} - {:error, errmsg} -> - {:error, errmsg} + {:error, error_message} -> + {:error, error_message} end end) @@ -245,8 +250,8 @@ defmodule Cadet.Updater.XMLParser do question_map when is_map(question_map) -> Map.put(question, :question, question_map) - {:error, errmsg} -> - {:error, errmsg} + {:error, error_message} -> + {:error, error_message} end end @@ -297,7 +302,7 @@ defmodule Cadet.Updater.XMLParser do end defp process_question_entity_by_type(_, _) do - {:error, "Invalid question type"} + {:error, "Invalid question type."} end @spec process_question_library(map(), any(), any()) :: map() | {:error, String.t()} diff --git a/test/cadet/updater/xml_parser_test.exs b/test/cadet/updater/xml_parser_test.exs index da175973a..2725128ff 100644 --- a/test/cadet/updater/xml_parser_test.exs +++ b/test/cadet/updater/xml_parser_test.exs @@ -10,7 +10,6 @@ defmodule Cadet.Updater.XMLParserTest do @local_name "test/fixtures/local_repo" # @locations %{mission: "missions", sidequest: "quests", path: "paths", contest: "contests"} - @time_fields ~w(open_at close_at)a setup do File.rm_rf!(@local_name) @@ -50,8 +49,22 @@ defmodule Cadet.Updater.XMLParserTest do |> where(number: ^number) |> Repo.one() + open_at = + Timex.now() + |> Timex.beginning_of_day() + |> Timex.shift(days: 3) + |> Timex.shift(hours: 4) + + close_at = Timex.shift(open_at, days: 7) + + expected_assesment = + assessment + |> Map.put(:open_at, open_at) + |> Map.put(:close_at, close_at) + |> Map.put(:is_published, false) + assert_map_keys( - Map.from_struct(assessment), + Map.from_struct(expected_assesment), Map.from_struct(assessment_db), ~w(title is_published type summary_short summary_long open_at close_at)a ++ ~w(number story reading password)a @@ -97,51 +110,17 @@ defmodule Cadet.Updater.XMLParserTest do end end - test "open and close dates not in ISO8601 DateTime", %{ - assessments: assessments, - questions: questions - } do - date_strings = - Enum.map( - ~w({ISO:Basic} {ISOdate} {RFC822} {RFC1123} {ANSIC} {UNIX}), - &{&1, Timex.format!(Timex.now(), &1)} - ) - - for assessment <- assessments, - {date_format_string, date_string} <- date_strings, - time_field <- @time_fields do - assessment_wrong_date_format = %{assessment | time_field => date_string} - - xml = XMLGenerator.generate_xml_for(assessment_wrong_date_format, questions) - - assert capture_log(fn -> - assert( - XMLParser.parse_xml(xml) == :error, - inspect({date_format_string, date_string}, pretty: true) - ) - end) =~ "Time does not conform to ISO8601 DateTime" - end - end - - test "open and close time without offset", %{assessments: assessments, questions: questions} do - datetime_string = Timex.format!(Timex.now(), "{YYYY}-{0M}-{0D}T{h24}:{m}:{s}") - - for assessment <- assessments, - time_field <- @time_fields do - assessment_time_without_offset = %{assessment | time_field => datetime_string} - xml = XMLGenerator.generate_xml_for(assessment_time_without_offset, questions) - - assert capture_log(fn -> assert XMLParser.parse_xml(xml) == :error end) =~ - "Time does not have offset specified." - end - end - test "PROBLEM with missing type", %{assessments: assessments, questions: questions} do for assessment <- assessments do xml = XMLGenerator.generate_xml_for(assessment, questions, problem_permit_keys: ~w(maxgrade)a) - assert capture_log(fn -> assert(XMLParser.parse_xml(xml) == :error) end) =~ + assert capture_log(fn -> + assert( + XMLParser.parse_xml(xml) == + {:error, {:bad_request, "Missing attribute(s) on PROBLEM"}} + ) + end) =~ "Missing attribute(s) on PROBLEM" end end @@ -150,7 +129,12 @@ defmodule Cadet.Updater.XMLParserTest do for assessment <- assessments do xml = XMLGenerator.generate_xml_for(assessment, questions, problem_permit_keys: ~w(type)a) - assert capture_log(fn -> assert(XMLParser.parse_xml(xml) == :error) end) =~ + assert capture_log(fn -> + assert( + XMLParser.parse_xml(xml) == + {:error, {:bad_request, "Missing attribute(s) on PROBLEM"}} + ) + end) =~ "Missing attribute(s) on PROBLEM" end end @@ -159,7 +143,11 @@ defmodule Cadet.Updater.XMLParserTest do for assessment <- assessments do xml = XMLGenerator.generate_xml_for(assessment, questions, override_type: "anu") - assert capture_log(fn -> assert(XMLParser.parse_xml(xml) == :error) end) =~ + assert capture_log(fn -> + assert( + XMLParser.parse_xml(xml) == {:error, {:bad_request, "Invalid question type."}} + ) + end) =~ "Invalid question type." end end @@ -171,7 +159,10 @@ defmodule Cadet.Updater.XMLParserTest do xml = XMLGenerator.generate_xml_for(assessment, questions_without_content) - assert capture_log(fn -> assert(XMLParser.parse_xml(xml) == :error) end) =~ + # the error message can be quite convoluted + assert capture_log(fn -> + assert({:error, {:bad_request, _error_message}} = XMLParser.parse_xml(xml)) + end) =~ ~r/Invalid \b.*\b changeset\./ end end @@ -180,7 +171,11 @@ defmodule Cadet.Updater.XMLParserTest do for assessment <- assessments do xml = XMLGenerator.generate_xml_for(assessment, questions, no_deployment: true) - assert capture_log(fn -> assert(XMLParser.parse_xml(xml) == :error) end) =~ + assert capture_log(fn -> + assert( + XMLParser.parse_xml(xml) == {:error, {:bad_request, "Missing DEPLOYMENT"}} + ) + end) =~ "Missing DEPLOYMENT" end end @@ -200,7 +195,9 @@ defmodule Cadet.Updater.XMLParserTest do xml = XMLGenerator.generate_xml_for(assessment, questions) - assert capture_log(fn -> assert XMLParser.parse_xml(xml) == :ok end) =~ + assert capture_log(fn -> + assert XMLParser.parse_xml(xml) == {:ok, "Assessment already open, ignoring..."} + end) =~ "Assessment already open, ignoring..." end end @@ -308,7 +305,7 @@ defmodule Cadet.Updater.XMLParserTest do """) assert capture_log(fn -> - XMLParser.parse_and_insert(path) == {:error, "Error processing XML files."} + XMLParser.parse_and_insert(path) == {:error, {:bad_request, "Missing TASK"}} end) =~ "Missing TASK" end end From c7f01c20e9f907ded1b9643f7d719b8e1205c448 Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Mon, 20 Apr 2020 13:48:59 +0800 Subject: [PATCH 47/60] Changed assessment deletion such that associated submissions are also deleted --- lib/cadet/assessments/assessments.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 7ef40e078..dbbfe7922 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -61,6 +61,11 @@ defmodule Cadet.Assessments do def delete_assessment(_deleter = %User{role: role}, id) do if role in @delete_assessment_role do assessment = Repo.get(Assessment, id) + + Submission + |> where(assessment_id: ^id) + |> Repo.delete_all() + Repo.delete(assessment) else {:error, {:forbidden, "User is not permitted to delete"}} From 5d4e6d86becf73a90da22bc8ea9a441c6a00132f Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Mon, 20 Apr 2020 14:11:52 +0800 Subject: [PATCH 48/60] Minor formatting change --- lib/cadet/assessments/assessments.ex | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index dbbfe7922..b04b58b26 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -387,16 +387,13 @@ defmodule Cadet.Assessments do |> Repo.insert() else params = - if !question_params.max_xp do - question_params - |> Map.put(:max_xp, 0) - else - question_params - end + question_params + |> Map.put_new(:max_xp, 0) |> Map.put(:display_order, index) %{id: question_id, type: type} = - where(Question, [q], q.display_order == ^index and q.assessment_id == ^id) + Question + |> where([q], q.display_order == ^index and q.assessment_id == ^id) |> Repo.one() if question_params.type != Atom.to_string(type) do @@ -427,23 +424,23 @@ defmodule Cadet.Assessments do |> elem(1) |> elem(1)).data.id - # check if assessment already exists - if !assessment_id do - false - else + if assessment_id do open_date = Repo.get(Assessment, assessment_id).open_at # check if assessment is already opened if Timex.after?(open_date, Timex.now()) do false else existing_questions_count = - where(Question, [q], q.assessment_id == ^assessment_id) + Question + |> where([q], q.assessment_id == ^assessment_id) |> Repo.all() |> Enum.count() new_questions_count = Enum.count(questions_params) existing_questions_count != new_questions_count end + else + false end end From ae7beac0adc728d5f994b2d3ddc3bb221e73e81b Mon Sep 17 00:00:00 2001 From: ScrubWzz Date: Mon, 20 Apr 2020 18:33:39 +0800 Subject: [PATCH 49/60] Updated AssessmentOverview schema --- lib/cadet_web/controllers/assessments_controller.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index b0b355afe..f0209760a 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -293,6 +293,8 @@ defmodule CadetWeb.AssessmentsController do coverImage(:string, "The URL to the cover picture", required: true) private(:boolean, "Is this an private assessment?", required: true) + + isPublished(:boolean, "Is the assessment published?", required: true) end end, Assessment: From 6597a3e4c25daae3d281c4a91b6e5cc2d8419c86 Mon Sep 17 00:00:00 2001 From: travisryte Date: Wed, 22 Apr 2020 05:21:32 +0800 Subject: [PATCH 50/60] Ran Mix Format --- lib/cadet/accounts/game_states.ex | 10 +++++----- lib/cadet_web/controllers/user_controller.ex | 14 +++++++++----- lib/cadet_web/router.ex | 2 -- lib/cadet_web/views/user_view.ex | 9 ++++++++- .../migrations/20200410074625_add_game_states.exs | 1 + test/cadet/updater/xml_parser_test.exs | 1 + 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex index 704444cd4..bec02ded3 100644 --- a/lib/cadet/accounts/game_states.ex +++ b/lib/cadet/accounts/game_states.ex @@ -18,9 +18,7 @@ defmodule Cadet.GameStates do def update(user, new_game_states) do if user.role == :student do - changeset = - Ecto.Changeset.cast(user, %{game_states: - new_game_states},[:game_states]) + changeset = Ecto.Changeset.cast(user, %{game_states: new_game_states}, [:game_states]) Cadet.Repo.update!(changeset) {:ok, nil} else @@ -31,8 +29,10 @@ defmodule Cadet.GameStates do def clear(user) do if user.role == :student do changeset = - Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, - completed_quests: []}},[:game_states]) + Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, completed_quests: []}}, [ + :game_states + ]) + Cadet.Repo.update!(changeset) {:ok, nil} else diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 65760ba74..ad42f5de2 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -16,6 +16,7 @@ defmodule CadetWeb.UserController do story = user_current_story(user) xp = user_total_xp(user) game_states = user_game_states(user) + render( conn, "index.json", @@ -39,6 +40,7 @@ defmodule CadetWeb.UserController do def update_game_states(conn, %{"gameStates" => new_game_states}) do user = conn.assigns[:current_user] + case Cadet.GameStates.update(user, new_game_states) do {:ok, nil} -> text(conn, "OK") @@ -56,9 +58,11 @@ defmodule CadetWeb.UserController do security([%{JWT: []}]) consumes("application/json") produces("application/json") + parameters do new_game_states(:path, :map, "new game states", required: true) end + response(200, "OK", Schema.ref(:UserInfo)) response(201, "Created") response(204, "No Content") @@ -68,9 +72,11 @@ defmodule CadetWeb.UserController do def clear_up_game_states(conn, _) do user = conn.assigns[:current_user] + case Cadet.GameStates.clear(user) do {:ok, nil} -> text(conn, "OK") + {:error, {status, message}} -> conn |> put_status(status) @@ -90,7 +96,6 @@ defmodule CadetWeb.UserController do response(401, "Unauthorised") end - def swagger_definitions do %{ UserInfo: @@ -128,10 +133,9 @@ defmodule CadetWeb.UserController do game_states( :map, - "States for user's game, including users' collectibles and completed quests.\n" - <> "Collectibles is a map.\n" - <> "Completed quests is an array of strings" - + "States for user's game, including users' collectibles and completed quests.\n" <> + "Collectibles is a map.\n" <> + "Completed quests is an array of strings" ) end end, diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 3d3d42cf0..e3698822f 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -53,12 +53,10 @@ defmodule CadetWeb.Router do get("/notification", NotificationController, :index) post("/notification/acknowledge", NotificationController, :acknowledge) - get("/user", UserController, :index) put("/user/game_states/clear", UserController, :clear_up_game_states) put("/user/game_states/save", UserController, :update_game_states) - post("/chat/token", ChatController, :index) post("/chat/notify", ChatController, :notify) end diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 0c4ddfe61..e6839f7db 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -1,7 +1,14 @@ defmodule CadetWeb.UserView do use CadetWeb, :view - def render("index.json", %{user: user, grade: grade, max_grade: max_grade, xp: xp, story: story, game_states: game_states}) do + def render("index.json", %{ + user: user, + grade: grade, + max_grade: max_grade, + xp: xp, + story: story, + game_states: game_states + }) do %{ name: user.name, role: user.role, diff --git a/priv/repo/migrations/20200410074625_add_game_states.exs b/priv/repo/migrations/20200410074625_add_game_states.exs index 182be52ba..5d66a3f82 100644 --- a/priv/repo/migrations/20200410074625_add_game_states.exs +++ b/priv/repo/migrations/20200410074625_add_game_states.exs @@ -1,5 +1,6 @@ defmodule Cadet.Repo.Migrations.AddGameStates do use Ecto.Migration + def change do alter table(:users) do add(:game_states, :map, default: %{collectibles: %{}, completed_quests: []}) diff --git a/test/cadet/updater/xml_parser_test.exs b/test/cadet/updater/xml_parser_test.exs index 2725128ff..f348be93a 100644 --- a/test/cadet/updater/xml_parser_test.exs +++ b/test/cadet/updater/xml_parser_test.exs @@ -9,6 +9,7 @@ defmodule Cadet.Updater.XMLParserTest do import ExUnit.CaptureLog @local_name "test/fixtures/local_repo" + # @locations %{mission: "missions", sidequest: "quests", path: "paths", contest: "contests"} setup do From 45448ba10b5ff83bc1cbab15f0da3f9a963e48a6 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Wed, 22 Apr 2020 18:07:27 +0800 Subject: [PATCH 51/60] Modified user_controller_test.exs Modified this to fit collectible system. --- test/cadet_web/controllers/user_controller_test.exs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 8495073ad..db745fe94 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -61,7 +61,8 @@ defmodule CadetWeb.UserControllerTest do "role" => "#{user.role}", "xp" => 110, "grade" => 40, - "maxGrade" => question.max_grade + "maxGrade" => question.max_grade, + "gameStates" => %{"collectibles" => %{}, "completed_quests" => []} } assert expected == resp @@ -212,7 +213,8 @@ defmodule CadetWeb.UserControllerTest do "role" => "#{user.role}", "grade" => 0, "maxGrade" => 0, - "xp" => 0 + "xp" => 0, + "gameStates" => %{"collectibles" => %{}, "completed_quests" => []} } assert expected == resp From a47cbabb00b3effa0d8f43ead84a53dacf818e82 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Wed, 22 Apr 2020 18:09:24 +0800 Subject: [PATCH 52/60] Update accounts_test.exs Modified this to fit our collectible system. --- test/cadet/accounts/accounts_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index 2c264e3c8..9a7155ea3 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -106,7 +106,7 @@ defmodule Cadet.AccountsTest do describe "sign in using nusnet_id" do test "unregistered user" do use_cassette "accounts/sign_in#1" do - {:ok, _} = Accounts.sign_in("e012345", "TOM", @token) + {:ok, user} = Accounts.sign_in("e012345", "TOM", @token) assert Repo.one(Query.nusnet_id("e012345")).uid == "e012345" end end From 3ff9e52c8b133529decce95f3b5d9db3ac75a305 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Wed, 22 Apr 2020 18:11:28 +0800 Subject: [PATCH 53/60] Minor change to user.ex This change is to fit the testing format. --- lib/cadet/accounts/user.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index 2016bbfd1..5e41662ed 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -14,7 +14,7 @@ defmodule Cadet.Accounts.User do field(:name, :string) field(:role, Role) field(:nusnet_id, :string) - field(:game_states, :map) + field(:game_states, :map, default: %{"collectibles" => %{}, "completed_quests" => []}) belongs_to(:group, Group) timestamps() end From cca29eac0937f4f53c1030f8045470ae5583392f Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 23 Apr 2020 00:26:32 +0800 Subject: [PATCH 54/60] Eliminated one compile warning --- test/cadet/accounts/accounts_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index 9a7155ea3..1326fa943 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -106,7 +106,7 @@ defmodule Cadet.AccountsTest do describe "sign in using nusnet_id" do test "unregistered user" do use_cassette "accounts/sign_in#1" do - {:ok, user} = Accounts.sign_in("e012345", "TOM", @token) + {:ok, _user} = Accounts.sign_in("e012345", "TOM", @token) assert Repo.one(Query.nusnet_id("e012345")).uid == "e012345" end end From 0d59384d5889a92f55af803f2396bdb0e056a5f7 Mon Sep 17 00:00:00 2001 From: Cheng20010201 <53768585+Cheng20010201@users.noreply.github.com> Date: Thu, 23 Apr 2020 00:29:16 +0800 Subject: [PATCH 55/60] Added tests on collectible system --- .../controllers/user_controller_test.exs | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index db745fe94..b62a11a45 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -224,6 +224,189 @@ defmodule CadetWeb.UserControllerTest do conn = get(conn, "/v1/user", nil) assert response(conn, 401) =~ "Unauthorised" end + + @tag authenticate: :student + test "success, student adding collectibles", %{conn: conn} do + user = conn.assigns.current_user + new_game_states = %{ + "completed_quests" => ["haha"], + "collectibles" => %{ + "HAHA" => "HAHA.png" + } + } + Cadet.GameStates.update(user, new_game_states) + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert new_game_states == resp["gameStates"] + end + + @tag authenticate: :student + test "success, student deleting collectibles", %{conn: conn} do + user = conn.assigns.current_user + new_game_states = %{ + "completed_quests" => ["haha"], + "collectibles" => %{ + "HAHA" => "HAHA.png" + } + } + Cadet.GameStates.update(user, new_game_states) + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert new_game_states == resp["gameStates"] + + Cadet.GameStates.clear(user) + resp_2 = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp_2["gameStates"] + end + + @tag authenticate: :student + test "success, student retrieving collectibles", %{conn: conn} do + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp["gameStates"] + end + + + @tag authenticate: :staff + test "forbidden, staff adding collectibles", %{conn: conn} do + user = conn.assigns.current_user + new_game_states = %{ + "completed_quests" => ["haha"], + "collectibles" => %{ + "HAHA" => "HAHA.png" + } + } + + assert Cadet.GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp["gameStates"] + end + + @tag authenticate: :staff + test "forbidden, staff deleting collectibles", %{conn: conn} do + user = conn.assigns.current_user + new_game_states = %{ + "completed_quests" => ["haha"], + "collectibles" => %{ + "HAHA" => "HAHA.png" + } + } + assert Cadet.GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp["gameStates"] + + assert Cadet.GameStates.clear(user) == {:error, {:forbidden, "Please try again later."}} + resp_2 = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp_2["gameStates"] + end + + @tag authenticate: :staff + test "success, staff retrieving collectibles", %{conn: conn} do + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp["gameStates"] + end + + @tag authenticate: :admin + test "forbidden, admin adding collectibles", %{conn: conn} do + user = conn.assigns.current_user + new_game_states = %{ + "completed_quests" => ["haha"], + "collectibles" => %{ + "HAHA" => "HAHA.png" + } + } + + assert Cadet.GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp["gameStates"] + end + + @tag authenticate: :admin + test "forbidden, admin deleting collectibles", %{conn: conn} do + user = conn.assigns.current_user + new_game_states = %{ + "completed_quests" => ["haha"], + "collectibles" => %{ + "HAHA" => "HAHA.png" + } + } + assert Cadet.GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp["gameStates"] + + assert Cadet.GameStates.clear(user) == {:error, {:forbidden, "Please try again later."}} + resp_2 = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp_2["gameStates"] + end + + @tag authenticate: :admin + test "success, admin retrieving collectibles", %{conn: conn} do + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert %{ + "completed_quests" => [], + "collectibles" => %{} + } == resp["gameStates"] + end end defp build_assessments_starting_at(time) do From 9e7f141a05696a4c81936fec45866d5a18826cc0 Mon Sep 17 00:00:00 2001 From: Yuc Sun Date: Thu, 23 Apr 2020 19:15:20 +0800 Subject: [PATCH 56/60] Modified Module Name --- lib/cadet/accounts/game_states.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex index bec02ded3..21022cb83 100644 --- a/lib/cadet/accounts/game_states.ex +++ b/lib/cadet/accounts/game_states.ex @@ -1,4 +1,4 @@ -defmodule Cadet.GameStates do +defmodule Cadet.Accounts.GameStates do import Ecto.Repo # currently in this module no error handling function From 1525c256adb832ceb01465fea464838bd44dae27 Mon Sep 17 00:00:00 2001 From: Yuc Sun Date: Thu, 23 Apr 2020 19:54:21 +0800 Subject: [PATCH 57/60] Some format change to adhere to backend style --- lib/cadet/accounts/game_states.ex | 20 ++- lib/cadet_web/controllers/user_controller.ex | 50 +++--- mix.lock | 162 +++++++++--------- .../controllers/user_controller_test.exs | 19 +- 4 files changed, 127 insertions(+), 124 deletions(-) diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex index 21022cb83..ff1b68df4 100644 --- a/lib/cadet/accounts/game_states.ex +++ b/lib/cadet/accounts/game_states.ex @@ -1,6 +1,8 @@ defmodule Cadet.Accounts.GameStates do - import Ecto.Repo + use Cadet, :context + alias Cadet.Accounts.User + @update_gamestate_roles ~w(student)a # currently in this module no error handling function # has been implemented yet @@ -16,24 +18,24 @@ defmodule Cadet.Accounts.GameStates do user.game_states["completed_quests"] end - def update(user, new_game_states) do - if user.role == :student do - changeset = Ecto.Changeset.cast(user, %{game_states: new_game_states}, [:game_states]) - Cadet.Repo.update!(changeset) + def update(user = %User{role: role}, new_game_states) do + if role in @update_gamestate_roles do + changeset = cast(user, %{game_states: new_game_states}, [:game_states]) + Repo.update!(changeset) {:ok, nil} else {:error, {:forbidden, "Please try again later."}} end end - def clear(user) do - if user.role == :student do + def clear(user = %User{role: role}) do + if role in @update_gamestate_roles do changeset = - Ecto.Changeset.cast(user, %{game_states: %{collectibles: %{}, completed_quests: []}}, [ + cast(user, %{game_states: %{collectibles: %{}, completed_quests: []}}, [ :game_states ]) - Cadet.Repo.update!(changeset) + Repo.update!(changeset) {:ok, nil} else {:error, {:forbidden, "Please try again later."}} diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index ad42f5de2..7da52b040 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -6,8 +6,7 @@ defmodule CadetWeb.UserController do use CadetWeb, :controller use PhoenixSwagger import Cadet.Assessments - import Cadet.GameStates - import Ecto.Repo + import Cadet.Accounts.GameStates def index(conn, _) do user = conn.assigns.current_user @@ -29,19 +28,24 @@ defmodule CadetWeb.UserController do ) end - swagger_path :index do - get("/user") - summary("Get the name and role of a user") - security([%{JWT: []}]) - produces("application/json") - response(200, "OK", Schema.ref(:UserInfo)) - response(401, "Unauthorised") + def update_game_states(conn, %{"gameStates" => new_game_states}) do + user = conn.assigns[:current_user] + + case update(user, new_game_states) do + {:ok, nil} -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end end - def update_game_states(conn, %{"gameStates" => new_game_states}) do + def clear_up_game_states(conn, _) do user = conn.assigns[:current_user] - case Cadet.GameStates.update(user, new_game_states) do + case clear(user) do {:ok, nil} -> text(conn, "OK") @@ -52,6 +56,16 @@ defmodule CadetWeb.UserController do end end + swagger_path :index do + get("/user") + summary("Get the name and role of a user") + security([%{JWT: []}]) + produces("application/json") + response(200, "OK", Schema.ref(:UserInfo)) + response(401, "Unauthorised") + end + + swagger_path :update_game_states do put("/user/game_states/save") summary("update user's game states") @@ -70,20 +84,6 @@ defmodule CadetWeb.UserController do response(401, "Unauthorised") end - def clear_up_game_states(conn, _) do - user = conn.assigns[:current_user] - - case Cadet.GameStates.clear(user) do - {:ok, nil} -> - text(conn, "OK") - - {:error, {status, message}} -> - conn - |> put_status(status) - |> text(message) - end - end - swagger_path :clear_up_game_states do put("/user/game_states/clear") summary("clear up users' game data saved") diff --git a/mix.lock b/mix.lock index 8464c591d..c2c0df0ff 100644 --- a/mix.lock +++ b/mix.lock @@ -1,85 +1,85 @@ %{ - "arc": {:hex, :arc, "0.11.0", "ac7a0cc03035317b6fef9fe94c97d7d9bd183a3e7ce1606aa0c175cfa8d1ba6d", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.0", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, - "arc_ecto": {:hex, :arc_ecto, "0.11.2", "bd9b0c78ec7e09749c47e7e57a52076b5e0c3b9fd19be55f043b3445690ad95b", [:mix], [{:arc, "~> 0.11.0", [hex: :arc, repo: "hexpm", optional: false]}, {:ecto, ">= 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm"}, - "artificery": {:hex, :artificery, "0.4.2", "3ded6e29e13113af52811c72f414d1e88f711410cac1b619ab3a2666bbd7efd4", [:mix], [], "hexpm"}, - "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, - "blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm"}, - "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, - "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, - "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, - "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, - "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [], [], "hexpm"}, - "credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "crontab": {:hex, :crontab, "1.1.5", "2c9439506ceb0e9045de75879e994b88d6f0be88bfe017d58cb356c66c4a5482", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, - "csv": {:hex, :csv, "2.3.1", "9ce11eff5a74a07baf3787b2b19dd798724d29a9c3a492a41df39f6af686da0e", [:mix], [{:parallel_stream, "~> 1.0.4", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm"}, - "db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, - "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, - "distillery": {:hex, :distillery, "2.0.14", "25fc1cdad06282334dbf4a11b6e869cc002855c4e11825157498491df2eed594", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"}, - "ecto": {:hex, :ecto, "3.1.7", "fa21d06ef56cdc2fdaa62574e8c3ba34a2751d44ea34c30bc65f0728421043e5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, - "ecto_enum": {:hex, :ecto_enum, "1.3.2", "659f7251b6a201a236db9dceef0f713319f095a23ad1d8718efd7a3d3ef3e21b", [:mix], [{:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, - "ecto_sql": {:hex, :ecto_sql, "3.1.6", "1e80e30d16138a729c717f73dcb938590bcdb3a4502f3012414d0cbb261045d8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0 or ~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, - "erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm"}, - "ex_aws": {:hex, :ex_aws, "2.0.2", "8df2f96f58624a399abe5a0ce26db648ee848aca6393b9c65c939ece9ac07bfa", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, - "ex_aws_kms": {:hex, :ex_aws_kms, "2.0.0", "35221e9b306c2b80be692eb006e9e6fc3d7c35da010e4595c6f27e9686e15092", [:mix], [{:ex_aws, "~> 2.0.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_aws_lambda": {:hex, :ex_aws_lambda, "2.0.1", "d1904b9705244f2c56ab6284ac00af802b8b0b5531559ab58f64c1a9710f3c22", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.2", "c0258bbdfea55de4f98f0b2f0ca61fe402cc696f573815134beb1866e778f47b", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, - "ex_json_schema": {:hex, :ex_json_schema, "0.7.3", "3289bf2edf57eb1ae0d5af35bc6d6c37d7e6d935f72e0120c7f0704510956049", [:mix], [], "hexpm"}, - "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"}, - "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm"}, - "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.12.1", "a553c59f6850d0aff3770e4729515762ba7c8e41eedde03208182a8dc9d0ce07", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, - "exvcr": {:hex, :exvcr, "0.11.0", "59d5c11c9022852e9265d223fbde38c512cc350404f695a7b838cd7fb8dabed8", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, - "faker": {:hex, :faker, "0.13.0", "8abcb996f010ccd6c85588c89fc047f11134e04da019b70252f95431d721a3dc", [:mix], [], "hexpm"}, - "floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm"}, - "gen_stage": {:hex, :gen_stage, "0.14.1", "9d46723fda072d4f4bb31a102560013f7960f5d80ea44dcb96fd6304ed61e7a4", [:mix], [], "hexpm"}, - "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, - "gettext": {:hex, :gettext, "0.17.1", "8baab33482df4907b3eae22f719da492cee3981a26e649b9c2be1c0192616962", [:mix], [], "hexpm"}, - "git_hooks": {:hex, :git_hooks, "0.3.2", "62fe1a3c518cef263d462ac19ea69041dfeafc095f388f6b11ba02dd3cd5c778", [:mix], [{:blankable, "~> 1.0.0", [hex: :blankable, repo: "hexpm", optional: false]}, {:recase, "~> 0.6.0", [hex: :recase, repo: "hexpm", optional: false]}], "hexpm"}, - "guardian": {:hex, :guardian, "2.0.0", "5d3e537832b7cf35c8674da92457b7be671666a2eff4bf0f2ccfcfb3a8c67a0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, - "guardian_db": {:hex, :guardian_db, "2.0.2", "6247303fda5ed90e19ea1d2e4c5a65b13f58cc12810f95f71b6ffb50ef2d057f", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm"}, - "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, - "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, - "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, - "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, - "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, - "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"}, - "memento": {:hex, :memento, "0.3.1", "b2909390820550d8b90b68ec96f9e15ff8a45a28b6f97fa4a62ef50e87c2f9d9", [:mix], [], "hexpm"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, + "arc": {:hex, :arc, "0.11.0", "ac7a0cc03035317b6fef9fe94c97d7d9bd183a3e7ce1606aa0c175cfa8d1ba6d", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.0", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "e91a8bd676fca716f6e46275ae81fb96c0bbc7a9d5b96cac511ae190588eddd0"}, + "arc_ecto": {:hex, :arc_ecto, "0.11.2", "bd9b0c78ec7e09749c47e7e57a52076b5e0c3b9fd19be55f043b3445690ad95b", [:mix], [{:arc, "~> 0.11.0", [hex: :arc, repo: "hexpm", optional: false]}, {:ecto, ">= 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "58b40610d9f85665e0454f20fbfc3a071aabb77526191d089c4ef542d8eda6ae"}, + "artificery": {:hex, :artificery, "0.4.2", "3ded6e29e13113af52811c72f414d1e88f711410cac1b619ab3a2666bbd7efd4", [:mix], [], "hexpm", "514586f4312ef3709a3ccbd8e55f69455add235c1729656687bb781d10d0afdb"}, + "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm", "fab09b20e3f5db886725544cbcf875b8e73ec93363954eb8a1a9ed834aa8c1f9"}, + "blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm", "7cf11aac0e44f4eedbee0c15c1d37d94c090cb72a8d9fddf9f7aec30f9278899"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, + "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, + "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, + "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"}, + "credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0bbd3222607ccaaac5c0340f7f525c627ae4d7aee6c8c8c108922620c5b6446"}, + "crontab": {:hex, :crontab, "1.1.5", "2c9439506ceb0e9045de75879e994b88d6f0be88bfe017d58cb356c66c4a5482", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "be38e047e84bfcee40b60c365fe086dc8b3419efdf7baef9fcca6285bbcf9d00"}, + "csv": {:hex, :csv, "2.3.1", "9ce11eff5a74a07baf3787b2b19dd798724d29a9c3a492a41df39f6af686da0e", [:mix], [{:parallel_stream, "~> 1.0.4", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "86626e1c89a4ad9a96d0d9c638f9e88c2346b89b4ba1611988594ebe72b5d5ee"}, + "db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "5a0e8c1c722dbcd31c0cbd1906b1d1074c863d335c295e4b994849b65a1fbe47"}, + "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm", "52694ef56e60108e5012f8af9673874c66ed58ac1c4fae9b5b7ded31786663f5"}, + "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "506294d6c543e4e5282d4852aead19ace8a35bedeb043f9256a06a6336827122"}, + "distillery": {:hex, :distillery, "2.0.14", "25fc1cdad06282334dbf4a11b6e869cc002855c4e11825157498491df2eed594", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "1bc4861534891c1e144271a5b566d61128ccdc64c7920fd97fb1be5062142e5f"}, + "ecto": {:hex, :ecto, "3.1.7", "fa21d06ef56cdc2fdaa62574e8c3ba34a2751d44ea34c30bc65f0728421043e5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "fd0f11a8454e490ae11b6f69aa1ed9e0352641242d014cc3d2f420d7743f6966"}, + "ecto_enum": {:hex, :ecto_enum, "1.3.2", "659f7251b6a201a236db9dceef0f713319f095a23ad1d8718efd7a3d3ef3e21b", [:mix], [{:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "7d111aef5601596f1c6da3fd1de784cb7ed7f8eca3f8af631df04f11fc8f248f"}, + "ecto_sql": {:hex, :ecto_sql, "3.1.6", "1e80e30d16138a729c717f73dcb938590bcdb3a4502f3012414d0cbb261045d8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0 or ~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cdb6a76a6d88b256fd1bfc37da66cfc96f0935591c5114c1123b04c150828b69"}, + "erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm", "756d3e19b056339af674b715fdd752c5dac468cf9d0e2d1a03abf4574e99fbf8"}, + "ex_aws": {:hex, :ex_aws, "2.0.2", "8df2f96f58624a399abe5a0ce26db648ee848aca6393b9c65c939ece9ac07bfa", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm", "e32830626ef09d1ed843d686f31b6b226cabc001792c5a81d3ae9d52e9877644"}, + "ex_aws_kms": {:hex, :ex_aws_kms, "2.0.0", "35221e9b306c2b80be692eb006e9e6fc3d7c35da010e4595c6f27e9686e15092", [:mix], [{:ex_aws, "~> 2.0.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm", "fc36f2d0024ad849df27816a53ffc8625c3c093b4f92ee5fb91f6210fdf31b5b"}, + "ex_aws_lambda": {:hex, :ex_aws_lambda, "2.0.1", "d1904b9705244f2c56ab6284ac00af802b8b0b5531559ab58f64c1a9710f3c22", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm", "8f372547b5f69eee0dcbbba0ab1fe93d3e90807d652fffe285270f474c7dded9"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.2", "c0258bbdfea55de4f98f0b2f0ca61fe402cc696f573815134beb1866e778f47b", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "0569f5b211b1a3b12b705fe2a9d0e237eb1360b9d76298028df2346cad13097a"}, + "ex_json_schema": {:hex, :ex_json_schema, "0.7.3", "3289bf2edf57eb1ae0d5af35bc6d6c37d7e6d935f72e0120c7f0704510956049", [:mix], [], "hexpm", "d5389c44e2804d4e6cada6f4a99d68f9d2bc0e38c2e0fd21383f1878425bd5a9"}, + "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "b84f6af156264530b312a8ab98ac6088f6b77ae5fe2058305c81434aa01fbaf9"}, + "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, + "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, + "excoveralls": {:hex, :excoveralls, "0.12.1", "a553c59f6850d0aff3770e4729515762ba7c8e41eedde03208182a8dc9d0ce07", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "5c1f717066a299b1b732249e736c5da96bb4120d1e55dc2e6f442d251e18a812"}, + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, + "exvcr": {:hex, :exvcr, "0.11.0", "59d5c11c9022852e9265d223fbde38c512cc350404f695a7b838cd7fb8dabed8", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "17722e01511015cf3b46735850665425bb64264e304e6946a420040cff787ef7"}, + "faker": {:hex, :faker, "0.13.0", "8abcb996f010ccd6c85588c89fc047f11134e04da019b70252f95431d721a3dc", [:mix], [], "hexpm", "b0016680cae6776e3d1caa34d70438acc09c11c003e80fd3d44f79ec7370be00"}, + "floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e680b5ef0b61ce02faa7137db8d1714903a5552be4c89fb57293b8770e7f49c2"}, + "gen_stage": {:hex, :gen_stage, "0.14.1", "9d46723fda072d4f4bb31a102560013f7960f5d80ea44dcb96fd6304ed61e7a4", [:mix], [], "hexpm", "967dd5f2469ba77a8a54eef247c0f08a022f89b627a5b121b18cd224a513042f"}, + "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm", "5cacd405e72b2609a7e1f891bddb80c53d0b3b7b0036d1648e7382ca108c41c8"}, + "gettext": {:hex, :gettext, "0.17.1", "8baab33482df4907b3eae22f719da492cee3981a26e649b9c2be1c0192616962", [:mix], [], "hexpm", "f7d97341e536f95b96eef2988d6d4230f7262cf239cda0e2e63123ee0b717222"}, + "git_hooks": {:hex, :git_hooks, "0.3.2", "62fe1a3c518cef263d462ac19ea69041dfeafc095f388f6b11ba02dd3cd5c778", [:mix], [{:blankable, "~> 1.0.0", [hex: :blankable, repo: "hexpm", optional: false]}, {:recase, "~> 0.6.0", [hex: :recase, repo: "hexpm", optional: false]}], "hexpm", "072886de7e330b9ea8cad2a6aba30585fec1b58c9ca7c7c70ffc8c1ba44c691e"}, + "guardian": {:hex, :guardian, "2.0.0", "5d3e537832b7cf35c8674da92457b7be671666a2eff4bf0f2ccfcfb3a8c67a0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "6804b9eea4a30cab82bf51f1ae7ae333980b3bdcc6535b018242c4737e41e042"}, + "guardian_db": {:hex, :guardian_db, "2.0.2", "6247303fda5ed90e19ea1d2e4c5a65b13f58cc12810f95f71b6ffb50ef2d057f", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "540450c31de312bdcc4620eca6e95a947db08932750d580c78ad763ec3d25a32"}, + "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, + "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm", "3e3d7156a272950373ce5a4018b1490bea26676f8d6a7d409f6fac8568b8cb9a"}, + "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, + "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, + "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"}, + "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm", "6429c4fee52b2dda7861ee19a4f09c8c1ffa213bee3a1ec187828fde95d447ed"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, + "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm", "1feaf05ee886815ad047cad7ede17d6910710986148ae09cf73eee2989717b81"}, + "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"}, + "memento": {:hex, :memento, "0.3.1", "b2909390820550d8b90b68ec96f9e15ff8a45a28b6f97fa4a62ef50e87c2f9d9", [:mix], [], "hexpm", "ff8fc66255d21dcd539c5d77a0b5458715bf3efec91b389dd06017bbb4e2e916"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, - "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, - "parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, - "phoenix": {:hex, :phoenix, "1.4.11", "d112c862f6959f98e6e915c3b76c7a87ca3efd075850c8daa7c3c7a609014b0d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, - "phoenix_swagger": {:hex, :phoenix_swagger, "0.8.1", "af7fc985804145e17df316bb988db86d43401af3cff2f5f7ef6c21d22af5086c", [:mix], [{:ex_json_schema, "~> 0.5", [hex: :ex_json_schema, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "a280d1f7b6f4bbcbd9282616e57502721781c66ee5b540720efabeaf627cc7eb"}, + "parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm", "639b2e8749e11b87b9eb42f2ad325d161c170b39b288ac8d04c4f31f8f0823eb"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "phoenix": {:hex, :phoenix, "1.4.11", "d112c862f6959f98e6e915c3b76c7a87ca3efd075850c8daa7c3c7a609014b0d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef19d737ca23b66f7333eaa873cbfc5e6fa6427ef5a0ffd358de1ba8e1a4b2f4"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"}, + "phoenix_swagger": {:hex, :phoenix_swagger, "0.8.1", "af7fc985804145e17df316bb988db86d43401af3cff2f5f7ef6c21d22af5086c", [:mix], [{:ex_json_schema, "~> 0.5", [hex: :ex_json_schema, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "54f76fff87d797dee86ced3f17b9206383517868c06ad90f239a305e1d0dd6dc"}, + "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "164baaeb382d19beee0ec484492aa82a9c8685770aee33b24ec727a0971b34d0"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6cd8ddd1bd1fbfa54d3fc61d4719c2057dae67615395d58d40437a919a46f132"}, + "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, - "postgrex": {:hex, :postgrex, "0.15.1", "23ce3417de70f4c0e9e7419ad85bdabcc6860a6925fe2c6f3b1b5b1e8e47bf2f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, - "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"}, - "que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm"}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, - "recase": {:hex, :recase, "0.6.0", "1dd2dd2f4e06603b74977630e739f08b7fedbb9420cc14de353666c2fc8b99f4", [:mix], [], "hexpm"}, - "sentry": {:hex, :sentry, "7.2.0", "37a367ae58b112cc548e17aa8640e5e329eb1d19b71bc368fcb7ccad919d5dac", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, - "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"}, - "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"}, - "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"}, - "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, - "tzdata": {:hex, :tzdata, "1.0.0", "fe4da40f76348d2ca0d16491196089fe75f57d6164e2a0ef8adf2804b9a2b3fa", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, - "xml_builder": {:hex, :xml_builder, "2.1.2", "90cb9ad382958934c78c6ddfbe6d385a8ce147d84b61cbfa83ec93a169d0feab", [:mix], [], "hexpm"}, + "postgrex": {:hex, :postgrex, "0.15.1", "23ce3417de70f4c0e9e7419ad85bdabcc6860a6925fe2c6f3b1b5b1e8e47bf2f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "12cd418e207b8ed787dfe0f520fccd6c001f58d9108233feae7df36462593d1f"}, + "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm", "6de553ba9ac0668d3728b699d5065543f3e40c854154017461ee8c09038752da"}, + "que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm", "a737b365253e75dbd24b2d51acc1d851049e87baae08cd0c94e2bc5cd65088d5"}, + "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, + "recase": {:hex, :recase, "0.6.0", "1dd2dd2f4e06603b74977630e739f08b7fedbb9420cc14de353666c2fc8b99f4", [:mix], [], "hexpm", "8712e318420a228eb2e6366ada230148ed3a4316a798319edd5512f64d78c990"}, + "sentry": {:hex, :sentry, "7.2.0", "37a367ae58b112cc548e17aa8640e5e329eb1d19b71bc368fcb7ccad919d5dac", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "0bea1c16bc9d26173847e43535750d0da90a718b27b2781d648980c53ed20baf"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, + "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm", "94884f84783fc1ba027aba8fe8a7dae4aad78c98e9f9c76667ec3471585c08c6"}, + "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"}, + "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, + "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, + "tzdata": {:hex, :tzdata, "1.0.0", "fe4da40f76348d2ca0d16491196089fe75f57d6164e2a0ef8adf2804b9a2b3fa", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa97f8b3ea669720a380e5167585bbc30d6e3b061ee51de338e52d629fa38b5a"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, + "xml_builder": {:hex, :xml_builder, "2.1.2", "90cb9ad382958934c78c6ddfbe6d385a8ce147d84b61cbfa83ec93a169d0feab", [:mix], [], "hexpm", "b89046041da2fbc1d51d31493ba31b9d5fc6223c93384bf513a1a9e1df9ec081"}, } diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index b62a11a45..24a59e66c 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -6,6 +6,7 @@ defmodule CadetWeb.UserControllerTest do alias Cadet.Repo alias CadetWeb.UserController alias Cadet.Assessments.{Assessment, AssessmentType, Submission} + alias Cadet.Accounts.GameStates test "swagger" do assert is_map(UserController.swagger_definitions()) @@ -234,7 +235,7 @@ defmodule CadetWeb.UserControllerTest do "HAHA" => "HAHA.png" } } - Cadet.GameStates.update(user, new_game_states) + GameStates.update(user, new_game_states) resp = conn |> get("/v1/user") @@ -251,14 +252,14 @@ defmodule CadetWeb.UserControllerTest do "HAHA" => "HAHA.png" } } - Cadet.GameStates.update(user, new_game_states) + GameStates.update(user, new_game_states) resp = conn |> get("/v1/user") |> json_response(200) assert new_game_states == resp["gameStates"] - Cadet.GameStates.clear(user) + GameStates.clear(user) resp_2 = conn |> get("/v1/user") @@ -292,7 +293,7 @@ defmodule CadetWeb.UserControllerTest do } } - assert Cadet.GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} + assert GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} resp = conn |> get("/v1/user") @@ -312,7 +313,7 @@ defmodule CadetWeb.UserControllerTest do "HAHA" => "HAHA.png" } } - assert Cadet.GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} + assert GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} resp = conn |> get("/v1/user") @@ -322,7 +323,7 @@ defmodule CadetWeb.UserControllerTest do "collectibles" => %{} } == resp["gameStates"] - assert Cadet.GameStates.clear(user) == {:error, {:forbidden, "Please try again later."}} + assert GameStates.clear(user) == {:error, {:forbidden, "Please try again later."}} resp_2 = conn |> get("/v1/user") @@ -355,7 +356,7 @@ defmodule CadetWeb.UserControllerTest do } } - assert Cadet.GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} + assert GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} resp = conn |> get("/v1/user") @@ -375,7 +376,7 @@ defmodule CadetWeb.UserControllerTest do "HAHA" => "HAHA.png" } } - assert Cadet.GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} + assert GameStates.update(user, new_game_states) == {:error, {:forbidden, "Please try again later."}} resp = conn |> get("/v1/user") @@ -385,7 +386,7 @@ defmodule CadetWeb.UserControllerTest do "collectibles" => %{} } == resp["gameStates"] - assert Cadet.GameStates.clear(user) == {:error, {:forbidden, "Please try again later."}} + assert GameStates.clear(user) == {:error, {:forbidden, "Please try again later."}} resp_2 = conn |> get("/v1/user") From b71612ae29b438d359c7f15aae894e13b52e3cb1 Mon Sep 17 00:00:00 2001 From: Yuc Sun Date: Wed, 29 Apr 2020 19:44:01 +0800 Subject: [PATCH 58/60] fix issues proposed by reviewer --- config/secrets.exs.example | 28 -------------------- lib/cadet_web/controllers/user_controller.ex | 20 +++++--------- 2 files changed, 6 insertions(+), 42 deletions(-) delete mode 100644 config/secrets.exs.example diff --git a/config/secrets.exs.example b/config/secrets.exs.example deleted file mode 100644 index 6ed2bf1c1..000000000 --- a/config/secrets.exs.example +++ /dev/null @@ -1,28 +0,0 @@ -use Mix.Config - -config :cadet, - luminus: [ - api_key: "API_KEY", - client_id: "CLIENT_ID", - client_secret: "CLIENT_SECRET", - redirect_url: "REDIRECT_URL" - ], - updater: [ - cs1101s_repository: "git@github.com:cs1101s/cs1101s.git", - cs1101s_rsa_key: "/home/user/.ssh/cs1101s" - ], - autograder: [ - lambda_name: "autograderLambdaName" - ], - chat: [ - instance_id: "CHATKIT_INSTANCE_ID", - key_id: "CHATKIT_KEY_ID", - key_secret: "CHATKIT_KEY_SECRET" - ], - uploader: [ - materials_bucket: "env-cadet-materials", - sourcecasts_bucket: "env-cadet-sourcecasts" - ] - -config :sentry, - dsn: "https://public_key/sentry.io/somethingsomething" diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 7da52b040..5f4525322 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -71,29 +71,21 @@ defmodule CadetWeb.UserController do summary("update user's game states") security([%{JWT: []}]) consumes("application/json") - produces("application/json") - + parameters do - new_game_states(:path, :map, "new game states", required: true) + new_game_states(:body, :map, "new game states", required: true) end - response(200, "OK", Schema.ref(:UserInfo)) - response(201, "Created") - response(204, "No Content") - response(400, "Invalid parameters") - response(401, "Unauthorised") + response(200, "OK") + response(403, "Please try again later.") end swagger_path :clear_up_game_states do put("/user/game_states/clear") summary("clear up users' game data saved") security([%{JWT: []}]) - consumes("application/json") - produces("application/json") - response(200, "OK", Schema.ref(:UserInfo)) - response(201, "Created") - response(204, "No Content") - response(401, "Unauthorised") + response(200, "OK") + response(403, "Please try again later.") end def swagger_definitions do From f2e3b3fa9dc8569d16a014e5840ca3d163d3f1ed Mon Sep 17 00:00:00 2001 From: Yuc Sun Date: Wed, 29 Apr 2020 19:46:42 +0800 Subject: [PATCH 59/60] fix issues proposed --- config/secrets.exs.example | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 config/secrets.exs.example diff --git a/config/secrets.exs.example b/config/secrets.exs.example new file mode 100644 index 000000000..6ed2bf1c1 --- /dev/null +++ b/config/secrets.exs.example @@ -0,0 +1,28 @@ +use Mix.Config + +config :cadet, + luminus: [ + api_key: "API_KEY", + client_id: "CLIENT_ID", + client_secret: "CLIENT_SECRET", + redirect_url: "REDIRECT_URL" + ], + updater: [ + cs1101s_repository: "git@github.com:cs1101s/cs1101s.git", + cs1101s_rsa_key: "/home/user/.ssh/cs1101s" + ], + autograder: [ + lambda_name: "autograderLambdaName" + ], + chat: [ + instance_id: "CHATKIT_INSTANCE_ID", + key_id: "CHATKIT_KEY_ID", + key_secret: "CHATKIT_KEY_SECRET" + ], + uploader: [ + materials_bucket: "env-cadet-materials", + sourcecasts_bucket: "env-cadet-sourcecasts" + ] + +config :sentry, + dsn: "https://public_key/sentry.io/somethingsomething" From a46af782fec8866b86a232a0bc27f4cec0b2269c Mon Sep 17 00:00:00 2001 From: Yuc Sun Date: Wed, 29 Apr 2020 20:00:49 +0800 Subject: [PATCH 60/60] modified positioning of test cases --- .../controllers/user_controller_test.exs | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 24a59e66c..1a85691db 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -226,6 +226,28 @@ defmodule CadetWeb.UserControllerTest do assert response(conn, 401) =~ "Unauthorised" end + defp build_assessments_starting_at(time) do + type_order_map = + AssessmentType.__enum_map__() + |> Enum.with_index() + |> Enum.reduce(%{}, fn {type, idx}, acc -> Map.put(acc, type, idx) end) + + AssessmentType.__enum_map__() + |> Enum.map( + &build(:assessment, %{ + type: &1, + is_published: true, + open_at: time, + close_at: Timex.shift(time, days: 10) + }) + ) + |> Enum.shuffle() + |> Enum.map(&insert(&1)) + |> Enum.sort(&(type_order_map[&1.type] < type_order_map[&2.type])) + end + end + + describe "PUT /user" do @tag authenticate: :student test "success, student adding collectibles", %{conn: conn} do user = conn.assigns.current_user @@ -409,24 +431,4 @@ defmodule CadetWeb.UserControllerTest do } == resp["gameStates"] end end - - defp build_assessments_starting_at(time) do - type_order_map = - AssessmentType.__enum_map__() - |> Enum.with_index() - |> Enum.reduce(%{}, fn {type, idx}, acc -> Map.put(acc, type, idx) end) - - AssessmentType.__enum_map__() - |> Enum.map( - &build(:assessment, %{ - type: &1, - is_published: true, - open_at: time, - close_at: Timex.shift(time, days: 10) - }) - ) - |> Enum.shuffle() - |> Enum.map(&insert(&1)) - |> Enum.sort(&(type_order_map[&1.type] < type_order_map[&2.type])) - end end