Skip to content

Commit 8f8b93a

Browse files
authored
Merge branch 'master' into feat/add-AI-generated-comments-grading
2 parents 81e5bf7 + 009532a commit 8f8b93a

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
@@ -941,7 +941,7 @@ defmodule Cadet.Assessments do
941941
raw_answer,
942942
force_submit
943943
) do
944-
with {:ok, team} <- find_team(question.assessment.id, cr_id),
944+
with {:ok, _team} <- find_team(question.assessment.id, cr_id),
945945
{:ok, submission} <- find_or_create_submission(cr, question.assessment),
946946
{:status, true} <- {:status, force_submit or submission.status != :submitted},
947947
{:ok, _answer} <- insert_or_update_answer(submission, question, raw_answer, cr_id) do
@@ -1055,7 +1055,7 @@ defmodule Cadet.Assessments do
10551055

10561056
# Begin autograding job
10571057
GradingJob.force_grade_individual_submission(updated_submission)
1058-
update_xp_bonus(submission)
1058+
update_xp_bonus(updated_submission)
10591059

10601060
{:ok, nil}
10611061
else
@@ -1294,6 +1294,8 @@ defmodule Cadet.Assessments do
12941294
|> Submission.changeset(%{is_grading_published: true})
12951295
|> Repo.update()
12961296

1297+
update_xp_bonus(submission)
1298+
12971299
Notifications.write_notification_when_published(
12981300
submission.id,
12991301
:published_grading
@@ -1493,23 +1495,18 @@ defmodule Cadet.Assessments do
14931495
Answer
14941496
|> where(submission_id: ^submission_id)
14951497
|> order_by(:question_id)
1496-
|> group_by([a], a.id)
14971498
|> select([a], %{
1498-
# grouping by submission, so s.xp_bonus will be the same, but we need an
1499-
# aggregate function
1500-
total_xp: sum(a.xp) + sum(a.xp_adjustment)
1499+
total_xp: a.xp + a.xp_adjustment
15011500
})
15021501

15031502
total =
15041503
ans_xp
15051504
|> subquery
15061505
|> select([a], %{
1507-
total_xp: sum(a.total_xp)
1506+
total_xp: coalesce(sum(a.total_xp), 0)
15081507
})
15091508
|> Repo.one()
15101509

1511-
xp = decimal_to_integer(total.total_xp)
1512-
15131510
cur_time =
15141511
if submission.submitted_at == nil do
15151512
Timex.now()
@@ -1518,7 +1515,7 @@ defmodule Cadet.Assessments do
15181515
end
15191516

15201517
xp_bonus =
1521-
if xp <= 0 do
1518+
if total.total_xp <= 0 do
15221519
0
15231520
else
15241521
if Timex.before?(cur_time, Timex.shift(assessment.open_at, hours: early_hours)) do
@@ -2699,7 +2696,7 @@ defmodule Cadet.Assessments do
26992696

27002697
def has_last_modified_answer?(
27012698
question = %Question{},
2702-
cr = %CourseRegistration{id: cr_id},
2699+
cr = %CourseRegistration{id: _cr_id},
27032700
last_modified_at,
27042701
force_submit
27052702
) do
@@ -2710,15 +2707,6 @@ defmodule Cadet.Assessments do
27102707
else
27112708
{:status, _} ->
27122709
{:error, {:forbidden, "Assessment submission already finalised"}}
2713-
2714-
{:error, :race_condition} ->
2715-
{:error, {:internal_server_error, "Please try again later."}}
2716-
2717-
{:error, :invalid_vote} ->
2718-
{:error, {:bad_request, "Invalid vote! Vote is not saved."}}
2719-
2720-
_ ->
2721-
{:error, {:bad_request, "Missing or invalid parameter(s)"}}
27222710
end
27232711
end
27242712

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
@@ -145,7 +145,11 @@ defmodule CadetWeb.AdminCoursesController do
145145
title("AdminSublanguage")
146146

147147
properties do
148-
chapter(:integer, "Chapter number from 1 to 4", required: true, minimum: 1, maximum: 4)
148+
chapter(:integer, "Chapter number from 1 to 4",
149+
required: true,
150+
minimum: 1,
151+
maximum: 4
152+
)
149153

150154
variant(Schema.ref(:SourceVariant), "Variant name", required: true)
151155
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)