diff --git a/.credo.exs b/.credo.exs index 9828a7ad2..a3658e4fd 100644 --- a/.credo.exs +++ b/.credo.exs @@ -83,7 +83,7 @@ {Credo.Check.Readability.ModuleNames}, {Credo.Check.Readability.ParenthesesOnZeroArityDefs}, {Credo.Check.Readability.ParenthesesInCondition}, - {Credo.Check.Readability.PredicateFunctionNames}, + {Credo.Check.Readability.PredicateFunctionNames, exit_status: 0}, {Credo.Check.Readability.PreferImplicitTry}, {Credo.Check.Readability.RedundantBlankLines}, {Credo.Check.Readability.StringSigils}, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 908421a25..3c27c7962 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,11 +23,11 @@ jobs: env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ELIXIR_VERSION: 1.13.4 - OTP_VERSION: 25.3.2 + ELIXIR_VERSION: 1.18.3 + OTP_VERSION: 27.3.3 services: postgres: - image: postgres:14.2 + image: postgres:17.4 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres diff --git a/README.md b/README.md index 3cb9ec70b..d8d70bee3 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ Cadet is the web application powering Source Academy. ### System requirements -1. Elixir 1.13.3+ (current version: 1.13.4) -2. Erlang/OTP 23.2.1+ (current version: 25.3.2) -3. PostgreSQL 12+ (tested to be working up to 14.5) +1. Elixir 1.18+ (current version: 1.18.3) +2. Erlang/OTP 27+ (current version: 27.3.3) +3. PostgreSQL 12+ (tested to be working up to 17) It is probably okay to use a different version of PostgreSQL or Erlang/OTP, but using a different version of Elixir may result in differences in e.g. `mix format`. diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 79979555a..f25aa58bd 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -941,7 +941,7 @@ defmodule Cadet.Assessments do raw_answer, force_submit ) do - with {:ok, team} <- find_team(question.assessment.id, cr_id), + with {:ok, _team} <- find_team(question.assessment.id, cr_id), {:ok, submission} <- find_or_create_submission(cr, question.assessment), {:status, true} <- {:status, force_submit or submission.status != :submitted}, {:ok, _answer} <- insert_or_update_answer(submission, question, raw_answer, cr_id) do @@ -2689,7 +2689,7 @@ defmodule Cadet.Assessments do def has_last_modified_answer?( question = %Question{}, - cr = %CourseRegistration{id: cr_id}, + cr = %CourseRegistration{id: _cr_id}, last_modified_at, force_submit ) do @@ -2700,15 +2700,6 @@ defmodule Cadet.Assessments do else {:status, _} -> {:error, {:forbidden, "Assessment submission already finalised"}} - - {:error, :race_condition} -> - {:error, {:internal_server_error, "Please try again later."}} - - {:error, :invalid_vote} -> - {:error, {:bad_request, "Invalid vote! Vote is not saved."}} - - _ -> - {:error, {:bad_request, "Missing or invalid parameter(s)"}} end end diff --git a/lib/cadet/devices/devices.ex b/lib/cadet/devices/devices.ex index 7862b8792..daaf117be 100644 --- a/lib/cadet/devices/devices.ex +++ b/lib/cadet/devices/devices.ex @@ -72,7 +72,11 @@ defmodule Cadet.Devices do with {:ok, device} <- maybe_insert_device(type, secret), {:ok, registration} <- %DeviceRegistration{} - |> DeviceRegistration.changeset(%{user_id: user_id, device_id: device.id, title: title}) + |> DeviceRegistration.changeset(%{ + user_id: user_id, + device_id: device.id, + title: title + }) |> Repo.insert() do {:ok, registration |> Repo.preload(:device)} end diff --git a/lib/cadet_web/admin_controllers/admin_assets_controller.ex b/lib/cadet_web/admin_controllers/admin_assets_controller.ex index 3316cfffa..4ff25d0bd 100644 --- a/lib/cadet_web/admin_controllers/admin_assets_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_assets_controller.ex @@ -22,10 +22,15 @@ defmodule CadetWeb.AdminAssetsController do case Assets.delete_object(Courses.assets_prefix(course_reg.course), foldername, filename) do {:error, {status, message}} -> conn |> put_status(status) |> text(message) - _ -> conn |> put_status(204) |> text('') + _ -> conn |> put_status(204) |> text("") end end + # Ignore the dialyzer warning, just ctrl click the + # `Assets.upload_to_s3` function to see the type, + # it clearly returns a string URL + @dialyzer {:no_match, upload: 2} + def upload(conn, %{ "upload" => upload_params, "filename" => filename, @@ -96,7 +101,9 @@ defmodule CadetWeb.AdminAssetsController do parameters do folderName(:path, :string, "Folder name", required: true) - fileName(:path, :string, "File path in folder, which may contain subfolders", required: true) + fileName(:path, :string, "File path in folder, which may contain subfolders", + required: true + ) end security([%{JWT: []}]) @@ -115,7 +122,9 @@ defmodule CadetWeb.AdminAssetsController do parameters do folderName(:path, :string, "Folder name", required: true) - fileName(:path, :string, "File path in folder, which may contain subfolders", required: true) + fileName(:path, :string, "File path in folder, which may contain subfolders", + required: true + ) end security([%{JWT: []}]) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 7220a4d80..bdda2c868 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -143,7 +143,11 @@ defmodule CadetWeb.AdminCoursesController do title("AdminSublanguage") properties do - chapter(:integer, "Chapter number from 1 to 4", required: true, minimum: 1, maximum: 4) + chapter(:integer, "Chapter number from 1 to 4", + required: true, + minimum: 1, + maximum: 4 + ) variant(Schema.ref(:SourceVariant), "Variant name", required: true) end diff --git a/lib/cadet_web/admin_controllers/admin_grading_controller.ex b/lib/cadet_web/admin_controllers/admin_grading_controller.ex index aa93cd30f..9e7507bd7 100644 --- a/lib/cadet_web/admin_controllers/admin_grading_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_grading_controller.ex @@ -378,7 +378,9 @@ defmodule CadetWeb.AdminGradingController do required: true ) - student(Schema.ref(:StudentInfo), "Student who created the submission", required: true) + student(Schema.ref(:StudentInfo), "Student who created the submission", + required: true + ) unsubmittedBy(Schema.ref(:GraderInfo)) unsubmittedAt(:string, "Last unsubmitted at", format: "date-time", required: false) diff --git a/lib/cadet_web/admin_views/admin_assessments_view.ex b/lib/cadet_web/admin_views/admin_assessments_view.ex index 33af48629..00bc81849 100644 --- a/lib/cadet_web/admin_views/admin_assessments_view.ex +++ b/lib/cadet_web/admin_views/admin_assessments_view.ex @@ -64,7 +64,9 @@ defmodule CadetWeb.AdminAssessmentsView do end def render("leaderboard.json", %{leaderboard: leaderboard}) do - render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json", as: :contestEntry) + render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json", + as: :contestEntry + ) end def render("contestEntry.json", %{contestEntry: contestEntry}) do diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index 7542c25c9..bee2efbc9 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -68,7 +68,9 @@ defmodule CadetWeb.AssessmentsView do end def render("leaderboard.json", %{leaderboard: leaderboard}) do - render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json", as: :contestEntry) + render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json", + as: :contestEntry + ) end def render("contestEntry.json", %{contestEntry: contestEntry}) do diff --git a/mix.lock b/mix.lock index 4a5b66a3c..64591ae8c 100644 --- a/mix.lock +++ b/mix.lock @@ -17,7 +17,7 @@ "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"}, - "credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"}, "csv": {:hex, :csv, "3.2.2", "452f96414b39a176b7c390af6d8b78f15130dc6167fe3b836729131f515d843e", [:mix], [], "hexpm", "cbf256ff74a3fa01d9ec420d07b19c90d410ed9fe5b6d6e1bc7662edf35bc574"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, @@ -46,7 +46,7 @@ "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"}, "exvcr": {:hex, :exvcr, "0.16.0", "11579f43c88ae81f57c82ce4f09e3ebda4c40117c859ed39e61a653c3a0b4ff4", [:mix], [{:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.9", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "8f576af22369942f7a1482baff1f31e2f45983cf6fac45d49d2bd2e84b4d5be8"}, "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "gettext": {:hex, :gettext, "0.22.2", "6bfca374de34ecc913a28ba391ca184d88d77810a3e427afa8454a71a51341ac", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "8a2d389673aea82d7eae387e6a2ccc12660610080ae7beb19452cfdc1ec30f60"}, diff --git a/test/cadet/jobs/log_test.exs b/test/cadet/jobs/log_test.exs index 8a7e67c89..7d859df83 100644 --- a/test/cadet/jobs/log_test.exs +++ b/test/cadet/jobs/log_test.exs @@ -20,7 +20,10 @@ defmodule Cadet.Jobs.LogEntryTest do test "returns true (job runs) when log entry old enough" do %LogEntry{ name: @name, - last_run: Timex.subtract(DateTime.truncate(Timex.now(), :second), Duration.from_hours(25)) + last_run: + Timex.now() + |> Timex.subtract(Duration.from_hours(25)) + |> DateTime.truncate(:second) } |> Repo.insert!() @@ -37,7 +40,10 @@ defmodule Cadet.Jobs.LogEntryTest do test "returns false (job does not run) when log entry too recent" do %LogEntry{ name: @name, - last_run: Timex.subtract(DateTime.truncate(Timex.now(), :second), Duration.from_hours(23)) + last_run: + Timex.now() + |> Timex.subtract(Duration.from_hours(23)) + |> DateTime.truncate(:second) } |> Repo.insert!() diff --git a/test/cadet_web/admin_controllers/admin_sourcecast_controller_test.exs b/test/cadet_web/admin_controllers/admin_sourcecast_controller_test.exs index b38657283..84000e85f 100644 --- a/test/cadet_web/admin_controllers/admin_sourcecast_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_sourcecast_controller_test.exs @@ -180,7 +180,7 @@ defmodule CadetWeb.AdminSourcecastControllerTest do conn = post(conn, build_url(course_id), %{"sourcecast" => %{}}) assert response(conn, 400) =~ - "audio can't be blank\nplaybackData can't be blank\ntitle can't be blank" + "title can't be blank\naudio can't be blank\nplaybackData can't be blank" end end diff --git a/test/cadet_web/admin_controllers/admin_stories_controller_test.exs b/test/cadet_web/admin_controllers/admin_stories_controller_test.exs index 700adb570..4af5102c8 100644 --- a/test/cadet_web/admin_controllers/admin_stories_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_stories_controller_test.exs @@ -122,7 +122,7 @@ defmodule CadetWeb.AdminStoriesControllerTest do conn = post(conn, build_url(course_id), %{"story" => %{}}) assert response(conn, 400) == - "close_at can't be blank\nfilenames can't be blank\nopen_at can't be blank\ntitle can't be blank" + "title can't be blank\nclose_at can't be blank\nopen_at can't be blank\nfilenames can't be blank" end end diff --git a/test/cadet_web/helpers/controller_helper_test.exs b/test/cadet_web/helpers/controller_helper_test.exs index e158aecaa..f8f430643 100644 --- a/test/cadet_web/helpers/controller_helper_test.exs +++ b/test/cadet_web/helpers/controller_helper_test.exs @@ -16,12 +16,14 @@ defmodule CadetWeb.ControllerHelperTest do assert_called(Conn.send_resp(:conn, :no_content, "")) end - test_with_mock "sends 204 with {:ok} and empty string", Conn, send_resp: fn _, _, _ -> nil end do + test_with_mock "sends 204 with {:ok} and empty string", Conn, + send_resp: fn _, _, _ -> nil end do handle_standard_result({:ok, nil}, :conn, "") assert_called(Conn.send_resp(:conn, :no_content, "")) end - test_with_mock "sends 204 with :ok and empty string", Conn, send_resp: fn _, _, _ -> nil end do + test_with_mock "sends 204 with :ok and empty string", Conn, + send_resp: fn _, _, _ -> nil end do handle_standard_result(:ok, :conn, "") assert_called(Conn.send_resp(:conn, :no_content, "")) end