Skip to content

Commit 87cc6a0

Browse files
authored
Merge branch 'master' into leaderboard
2 parents 0a38978 + 009532a commit 87cc6a0

26 files changed

+329
-86
lines changed

.credo.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
{Credo.Check.Readability.ModuleNames},
8484
{Credo.Check.Readability.ParenthesesOnZeroArityDefs},
8585
{Credo.Check.Readability.ParenthesesInCondition},
86-
{Credo.Check.Readability.PredicateFunctionNames},
86+
{Credo.Check.Readability.PredicateFunctionNames, exit_status: 0},
8787
{Credo.Check.Readability.PreferImplicitTry},
8888
{Credo.Check.Readability.RedundantBlankLines},
8989
{Credo.Check.Readability.StringSigils},

.github/workflows/cd.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ on:
2121
jobs:
2222
ci:
2323
name: Build release
24-
runs-on: ubuntu-20.04
24+
runs-on: ubuntu-latest
2525
env:
2626
MIX_ENV: prod
2727
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28-
ELIXIR_VERSION: 1.13.4
29-
OTP_VERSION: 25.3.2
28+
ELIXIR_VERSION: 1.18.3
29+
OTP_VERSION: 27.3.3
3030
steps:
3131
- uses: rlespinasse/github-slug-action@v3.x
3232
- uses: actions/checkout@v4

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ on:
1919
jobs:
2020
ci:
2121
name: Run CI
22-
runs-on: ubuntu-20.04
22+
runs-on: ubuntu-latest
2323
env:
2424
MIX_ENV: test
2525
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26-
ELIXIR_VERSION: 1.13.4
27-
OTP_VERSION: 25.3.2
26+
ELIXIR_VERSION: 1.18.3
27+
OTP_VERSION: 27.3.3
2828
services:
2929
postgres:
30-
image: postgres:14.2
30+
image: postgres:17.4
3131
env:
3232
POSTGRES_USER: postgres
3333
POSTGRES_PASSWORD: postgres

README.md

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,22 @@ Cadet is the web application powering Source Academy.
1414

1515
### System requirements
1616

17-
1. Elixir 1.13.3+ (current version: 1.13.4)
18-
2. Erlang/OTP 23.2.1+ (current version: 25.3.2)
19-
3. PostgreSQL 12+ (tested to be working up to 14.5)
17+
1. Elixir 1.18+ (current version: 1.18.3)
18+
2. Erlang/OTP 27+ (current version: 27.3.3)
19+
3. PostgreSQL 12+ (tested to be working up to 17)
2020

2121
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`.
2222

23+
> ## Setting up PostgreSQL
24+
>
25+
> The simplest way to get started is to use Docker. Simply [install Docker](https://docs.docker.com/get-docker/) and run the following command:
26+
>
27+
> ```bash
28+
> $ docker run --name sa-backend-db -e POSTGRES_HOST_AUTH_METHOD=trust -e -p 5432:5432 -d postgres
29+
> ```
30+
>
31+
> This configures PostgreSQL on port 5432. You can then connect to the database using `localhost:5432` as the host and `postgres` as the username. Note: `-e POSTGRES_HOST_AUTH_METHOD=trust` is used to disable password authentication for local development; since we are only accesing the database locally from our own machine, it is safe to do so.
32+
2333
### Setting up your local development environment
2434
2535
1. Set up the development secrets (replace the values appropriately)
@@ -29,8 +39,6 @@ It is probably okay to use a different version of PostgreSQL or Erlang/OTP, but
2939
$ vim config/dev.secrets.exs
3040
```
3141
32-
- To use NUSNET authentication, specify the NUS ADFS OAuth2 URL. (Ask for it.) Note that the frontend will supply the ADFS client ID and redirect URL (so you will need that too, but not here).
33-
3442
2. Install Elixir dependencies
3543

3644
```bash
@@ -49,19 +57,6 @@ It is probably okay to use a different version of PostgreSQL or Erlang/OTP, but
4957
$ mix ecto.setup
5058
```
5159

52-
If you encounter error message about invalid password for the user "postgres".
53-
You should reset the "postgres" password:
54-
55-
```bash
56-
$ sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
57-
```
58-
59-
and restart postgres service:
60-
61-
```bash
62-
$ sudo service postgresql restart
63-
```
64-
6560
By default, the database is populated with 10 students and 5 assessments. Each student will have a submission to the corresponding submission. This can be changed in `priv/repo/seeds.exs` with the variables `number_of_students`, `number_of_assessments` and `number_of_questions`. Save the changes and run:
6661

6762
```bash

config/config.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ config :cadet, Cadet.Jobs.Scheduler,
2525
# Compute rolling leaderboard every 2 hours
2626
{"0 */2 * * *", {Cadet.Assessments, :update_rolling_contest_leaderboards, []}},
2727
# Collate contest entries that close in the previous day at 00:01
28-
{"1 0 * * *", {Cadet.Assessments, :update_final_contest_entries, []}}
28+
{"1 0 * * *", {Cadet.Assessments, :update_final_contest_entries, []}},
29+
# Clean up expired exchange tokens at 00:01
30+
{"1 0 * * *", {Cadet.TokenExchange, :delete_expired, []}}
2931
]
3032

3133
# Configures the endpoint

config/dev.secrets.exs.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ config :cadet,
3636
# %{
3737
# assertion_extractor: Cadet.Auth.Providers.NusstuAssertionExtractor,
3838
# client_redirect_url: "http://cadet.frontend:8000/login/callback"
39+
# vscode_redirect_url_prefix: "vscode://source-academy.source-academy/sso",
40+
# client_post_exchange_redirect_url: "http://cadet.frontend:8000/login/vscode_callback",
3941
# }},
4042

4143
"test" =>

lib/cadet/assessments/assessments.ex

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,7 +1068,7 @@ defmodule Cadet.Assessments do
10681068
raw_answer,
10691069
force_submit
10701070
) do
1071-
with {:ok, team} <- find_team(question.assessment.id, cr_id),
1071+
with {:ok, _team} <- find_team(question.assessment.id, cr_id),
10721072
{:ok, submission} <- find_or_create_submission(cr, question.assessment),
10731073
{:status, true} <- {:status, force_submit or submission.status != :submitted},
10741074
{:ok, _answer} <- insert_or_update_answer(submission, question, raw_answer, cr_id) do
@@ -1182,7 +1182,7 @@ defmodule Cadet.Assessments do
11821182

11831183
# Begin autograding job
11841184
GradingJob.force_grade_individual_submission(updated_submission)
1185-
update_xp_bonus(submission)
1185+
update_xp_bonus(updated_submission)
11861186

11871187
{:ok, nil}
11881188
else
@@ -1421,6 +1421,8 @@ defmodule Cadet.Assessments do
14211421
|> Submission.changeset(%{is_grading_published: true})
14221422
|> Repo.update()
14231423

1424+
update_xp_bonus(submission)
1425+
14241426
Notifications.write_notification_when_published(
14251427
submission.id,
14261428
:published_grading
@@ -1620,23 +1622,18 @@ defmodule Cadet.Assessments do
16201622
Answer
16211623
|> where(submission_id: ^submission_id)
16221624
|> order_by(:question_id)
1623-
|> group_by([a], a.id)
16241625
|> select([a], %{
1625-
# grouping by submission, so s.xp_bonus will be the same, but we need an
1626-
# aggregate function
1627-
total_xp: sum(a.xp) + sum(a.xp_adjustment)
1626+
total_xp: a.xp + a.xp_adjustment
16281627
})
16291628

16301629
total =
16311630
ans_xp
16321631
|> subquery
16331632
|> select([a], %{
1634-
total_xp: sum(a.total_xp)
1633+
total_xp: coalesce(sum(a.total_xp), 0)
16351634
})
16361635
|> Repo.one()
16371636

1638-
xp = decimal_to_integer(total.total_xp)
1639-
16401637
cur_time =
16411638
if submission.submitted_at == nil do
16421639
Timex.now()
@@ -1645,7 +1642,7 @@ defmodule Cadet.Assessments do
16451642
end
16461643

16471644
xp_bonus =
1648-
if xp <= 0 do
1645+
if total.total_xp <= 0 do
16491646
0
16501647
else
16511648
if Timex.before?(cur_time, Timex.shift(assessment.open_at, hours: early_hours)) do
@@ -3006,7 +3003,7 @@ defmodule Cadet.Assessments do
30063003

30073004
def has_last_modified_answer?(
30083005
question = %Question{},
3009-
cr = %CourseRegistration{id: cr_id},
3006+
cr = %CourseRegistration{id: _cr_id},
30103007
last_modified_at,
30113008
force_submit
30123009
) do
@@ -3017,15 +3014,6 @@ defmodule Cadet.Assessments do
30173014
else
30183015
{:status, _} ->
30193016
{:error, {:forbidden, "Assessment submission already finalised"}}
3020-
3021-
{:error, :race_condition} ->
3022-
{:error, {:internal_server_error, "Please try again later."}}
3023-
3024-
{:error, :invalid_vote} ->
3025-
{:error, {:bad_request, "Invalid vote! Vote is not saved."}}
3026-
3027-
_ ->
3028-
{:error, {:bad_request, "Missing or invalid parameter(s)"}}
30293017
end
30303018
end
30313019

lib/cadet/code_exchange.ex

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
defmodule Cadet.TokenExchange do
2+
@moduledoc """
3+
The TokenExchange entity stores short-lived codes to be exchanged for long-lived auth tokens.
4+
"""
5+
use Cadet, :model
6+
7+
import Ecto.Query
8+
9+
alias Cadet.Repo
10+
alias Cadet.Accounts.User
11+
12+
@primary_key {:code, :string, []}
13+
schema "token_exchange" do
14+
field(:generated_at, :utc_datetime_usec)
15+
field(:expires_at, :utc_datetime_usec)
16+
17+
belongs_to(:user, User)
18+
19+
timestamps()
20+
end
21+
22+
@required_fields ~w(code generated_at expires_at user_id)a
23+
24+
def get_by_code(code) do
25+
case Repo.get_by(__MODULE__, code: code) do
26+
nil ->
27+
{:error, "Not found"}
28+
29+
struct ->
30+
if Timex.before?(struct.expires_at, Timex.now()) do
31+
{:error, "Expired"}
32+
else
33+
struct = Repo.preload(struct, :user)
34+
Repo.delete(struct)
35+
{:ok, struct}
36+
end
37+
end
38+
end
39+
40+
def delete_expired do
41+
now = Timex.now()
42+
43+
Repo.delete_all(from(c in __MODULE__, where: c.expires_at < ^now))
44+
end
45+
46+
def changeset(struct, attrs) do
47+
struct
48+
|> cast(attrs, @required_fields)
49+
|> validate_required(@required_fields)
50+
end
51+
52+
def insert(attrs) do
53+
changeset =
54+
%__MODULE__{}
55+
|> changeset(attrs)
56+
57+
changeset
58+
|> Repo.insert()
59+
end
60+
end

lib/cadet/devices/devices.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ defmodule Cadet.Devices do
7272
with {:ok, device} <- maybe_insert_device(type, secret),
7373
{:ok, registration} <-
7474
%DeviceRegistration{}
75-
|> DeviceRegistration.changeset(%{user_id: user_id, device_id: device.id, title: title})
75+
|> DeviceRegistration.changeset(%{
76+
user_id: user_id,
77+
device_id: device.id,
78+
title: title
79+
})
7680
|> Repo.insert() do
7781
{:ok, registration |> Repo.preload(:device)}
7882
end

lib/cadet_web/admin_controllers/admin_assets_controller.ex

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ defmodule CadetWeb.AdminAssetsController do
2222

2323
case Assets.delete_object(Courses.assets_prefix(course_reg.course), foldername, filename) do
2424
{:error, {status, message}} -> conn |> put_status(status) |> text(message)
25-
_ -> conn |> put_status(204) |> text('')
25+
_ -> conn |> put_status(204) |> text("")
2626
end
2727
end
2828

29+
# Ignore the dialyzer warning, just ctrl click the
30+
# `Assets.upload_to_s3` function to see the type,
31+
# it clearly returns a string URL
32+
@dialyzer {:no_match, upload: 2}
33+
2934
def upload(conn, %{
3035
"upload" => upload_params,
3136
"filename" => filename,
@@ -96,7 +101,9 @@ defmodule CadetWeb.AdminAssetsController do
96101
parameters do
97102
folderName(:path, :string, "Folder name", required: true)
98103

99-
fileName(:path, :string, "File path in folder, which may contain subfolders", required: true)
104+
fileName(:path, :string, "File path in folder, which may contain subfolders",
105+
required: true
106+
)
100107
end
101108

102109
security([%{JWT: []}])
@@ -115,7 +122,9 @@ defmodule CadetWeb.AdminAssetsController do
115122
parameters do
116123
folderName(:path, :string, "Folder name", required: true)
117124

118-
fileName(:path, :string, "File path in folder, which may contain subfolders", required: true)
125+
fileName(:path, :string, "File path in folder, which may contain subfolders",
126+
required: true
127+
)
119128
end
120129

121130
security([%{JWT: []}])

lib/cadet_web/admin_controllers/admin_courses_controller.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ defmodule CadetWeb.AdminCoursesController do
147147
title("AdminSublanguage")
148148

149149
properties do
150-
chapter(:integer, "Chapter number from 1 to 4", required: true, minimum: 1, maximum: 4)
150+
chapter(:integer, "Chapter number from 1 to 4",
151+
required: true,
152+
minimum: 1,
153+
maximum: 4
154+
)
151155

152156
variant(Schema.ref(:SourceVariant), "Variant name", required: true)
153157
end

lib/cadet_web/admin_controllers/admin_grading_controller.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,9 @@ defmodule CadetWeb.AdminGradingController do
378378
required: true
379379
)
380380

381-
student(Schema.ref(:StudentInfo), "Student who created the submission", required: true)
381+
student(Schema.ref(:StudentInfo), "Student who created the submission",
382+
required: true
383+
)
382384

383385
unsubmittedBy(Schema.ref(:GraderInfo))
384386
unsubmittedAt(:string, "Last unsubmitted at", format: "date-time", required: false)

lib/cadet_web/admin_views/admin_assessments_view.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ defmodule CadetWeb.AdminAssessmentsView do
6464
end
6565

6666
def render("leaderboard.json", %{leaderboard: leaderboard}) do
67-
render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json", as: :contestEntry)
67+
render_many(leaderboard, CadetWeb.AdminAssessmentsView, "contestEntry.json",
68+
as: :contestEntry
69+
)
6870
end
6971

7072
def render("contestEntry.json", %{contestEntry: contestEntry}) do

0 commit comments

Comments
 (0)