diff --git a/lib/cadet/accounts/game_states.ex b/lib/cadet/accounts/game_states.ex new file mode 100644 index 000000000..ff1b68df4 --- /dev/null +++ b/lib/cadet/accounts/game_states.ex @@ -0,0 +1,44 @@ +defmodule Cadet.Accounts.GameStates do + 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 + + def user_game_states(user) do + user.game_states + end + + def user_collectibles(user) do + user.game_states["collectibles"] + end + + def user_save_data(user) do + user.game_states["completed_quests"] + end + + 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 = %User{role: role}) do + if role in @update_gamestate_roles do + changeset = + cast(user, %{game_states: %{collectibles: %{}, completed_quests: []}}, [ + :game_states + ]) + + Repo.update!(changeset) + {:ok, nil} + else + {:error, {:forbidden, "Please try again later."}} + end + end +end diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index 26738d354..5e41662ed 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(:game_states, :map, default: %{"collectibles" => %{}, "completed_quests" => []}) belongs_to(:group, Group) timestamps() end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 59965c8a9..b04b58b26 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -16,11 +16,62 @@ 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, {:unauthorized, "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, toggle_publish_to) do + if role in @publish_assessment_role do + update_assessment(id, %{is_published: toggle_publish_to}) + 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) + + Submission + |> where(assessment_id: ^id) + |> Repo.delete_all() + + 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 +214,19 @@ 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) @@ -210,7 +269,7 @@ defmodule Cadet.Assessments do 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 +299,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 +318,15 @@ 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,33 +351,101 @@ 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 invalid_force_update(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) + ) + + # 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) + |> build_question_changeset_for_assessment_id(id) + |> Repo.insert() + else + params = + question_params + |> Map.put_new(:max_xp, 0) + |> Map.put(:display_order, index) + + %{id: question_id, type: type} = + Question + |> where([q], q.display_order == ^index and q.assessment_id == ^id) + |> Repo.one() + + 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) - 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 + # 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 + + 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 = + 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 + + @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() @@ -318,18 +454,30 @@ defmodule Cadet.Assessments do 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) + |> Map.delete(:is_published) + + 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/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) diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index c4da6a4d3..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)) @@ -77,14 +81,16 @@ 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 +98,53 @@ 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, error_message} -> + log_and_return_badrequest(error_message) {: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 info is stored in multiple nested tuples + 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 - @spec process_assessment(String.t()) :: {:ok, map()} | :error + 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, 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 +152,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 +160,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 +172,11 @@ 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 +196,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 +220,16 @@ defmodule Cadet.Updater.XMLParser do question else {:no_missing_attr?, false} -> - Logger.error("Missing attribute(s) on PROBLEM") - :error + {:error, "Missing attribute(s) on PROBLEM"} - :error -> - :error + {:error, error_message} -> + {:error, error_message} 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 +242,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 +250,8 @@ defmodule Cadet.Updater.XMLParser do question_map when is_map(question_map) -> Map.put(question, :question, question_map) - :error -> - :error + {:error, error_message} -> + {:error, error_message} end end @@ -288,11 +302,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 +317,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 +362,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..f0209760a 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,91 @@ defmodule CadetWeb.AssessmentsController do end end + 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 + {: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 + 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") + end + + {:ok, warning_message} -> + send_resp(conn, 200, warning_message) + + {:error, {status, message}} -> + send_resp(conn, status, message) + end + end + end + swagger_path :submit do post("/assessments/{assessmentId}/submit") summary("Finalise submission for an assessment") @@ -83,6 +169,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: @@ -141,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: diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index e43e9d9a8..5f4525322 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -5,8 +5,8 @@ defmodule CadetWeb.UserController do use CadetWeb, :controller use PhoenixSwagger - import Cadet.Assessments + import Cadet.Accounts.GameStates def index(conn, _) do user = conn.assigns.current_user @@ -14,6 +14,7 @@ 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, @@ -22,23 +23,71 @@ defmodule CadetWeb.UserController do grade: grade, max_grade: max_grade, story: story, - xp: xp + xp: xp, + game_states: game_states ) end + 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 clear_up_game_states(conn, _) do + user = conn.assigns[:current_user] + + case clear(user) do + {:ok, nil} -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + 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") + security([%{JWT: []}]) + consumes("application/json") + + parameters do + new_game_states(:body, :map, "new game states", required: true) + end + + 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: []}]) + response(200, "OK") + response(403, "Please try again later.") + end + def swagger_definitions do %{ UserInfo: @@ -73,6 +122,13 @@ defmodule CadetWeb.UserController do :integer, "Amount of xp. Only provided for 'Student'." <> "Value will be 0 for non-students." ) + + 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" + ) end end, UserStory: diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1f37b5c3e..e3698822f 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, :publish) + post("/assessments/update/:id", AssessmentsController, :update) post("/assessments/:assessmentid/submit", AssessmentsController, :submit) post("/assessments/question/:questionid/submit", AnswerController, :submit) @@ -50,6 +54,8 @@ defmodule CadetWeb.Router do 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) 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 diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index f8b3e711b..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}) 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, @@ -12,7 +19,8 @@ defmodule CadetWeb.UserView do transform_map_for_view(story, %{ story: :story, playStory: :play_story? - }) + }), + gameStates: game_states } end end 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/priv/repo/migrations/20200410074625_add_game_states.exs b/priv/repo/migrations/20200410074625_add_game_states.exs new file mode 100644 index 000000000..5d66a3f82 --- /dev/null +++ b/priv/repo/migrations/20200410074625_add_game_states.exs @@ -0,0 +1,9 @@ +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 diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index 2c264e3c8..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, _} = 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 diff --git a/test/cadet/updater/xml_parser_test.exs b/test/cadet/updater/xml_parser_test.exs index da175973a..f348be93a 100644 --- a/test/cadet/updater/xml_parser_test.exs +++ b/test/cadet/updater/xml_parser_test.exs @@ -9,8 +9,8 @@ defmodule Cadet.Updater.XMLParserTest do import ExUnit.CaptureLog @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 +50,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 +111,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 +130,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 +144,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 +160,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 +172,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 +196,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 +306,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 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 diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 8495073ad..1a85691db 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()) @@ -61,7 +62,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 +214,8 @@ defmodule CadetWeb.UserControllerTest do "role" => "#{user.role}", "grade" => 0, "maxGrade" => 0, - "xp" => 0 + "xp" => 0, + "gameStates" => %{"collectibles" => %{}, "completed_quests" => []} } assert expected == resp @@ -222,25 +225,210 @@ defmodule CadetWeb.UserControllerTest do conn = get(conn, "/v1/user", nil) assert response(conn, 401) =~ "Unauthorised" end - end - defp build_assessments_starting_at(time) do - type_order_map = + 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.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])) + |> 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 + new_game_states = %{ + "completed_quests" => ["haha"], + "collectibles" => %{ + "HAHA" => "HAHA.png" + } + } + 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" + } + } + GameStates.update(user, new_game_states) + resp = + conn + |> get("/v1/user") + |> json_response(200) + assert new_game_states == resp["gameStates"] + + 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 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 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 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 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 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 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 end