Skip to content

Commit 8f7bec5

Browse files
authored
Merge branch 'master' into No-GC-For-Staff
2 parents 9686184 + d7c147f commit 8f7bec5

File tree

8 files changed

+215
-44
lines changed

8 files changed

+215
-44
lines changed

lib/cadet/accounts/teams.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ defmodule Cadet.Accounts.Teams do
99
import Ecto.{Changeset, Query}
1010

1111
alias Cadet.Repo
12-
alias Cadet.Accounts.{Team, TeamMember, CourseRegistration, Notification}
13-
alias Cadet.Assessments.{Answer, Assessment, Submission}
12+
alias Cadet.Accounts.{Team, TeamMember, Notification}
13+
alias Cadet.Assessments.{Answer, Submission}
1414

1515
@doc """
1616
Creates a new team and assigns an assessment and team members to it.

lib/cadet/assessments/assessments.ex

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -966,7 +966,17 @@ defmodule Cadet.Assessments do
966966
end
967967
end
968968

969-
defp find_teams(cr_id) do
969+
def is_team_assessment?(assessment_id) when is_ecto_id(assessment_id) do
970+
max_team_size =
971+
Assessment
972+
|> where(id: ^assessment_id)
973+
|> select([a], a.max_team_size)
974+
|> Repo.one()
975+
976+
max_team_size > 1
977+
end
978+
979+
defp find_teams(cr_id) when is_ecto_id(cr_id) do
970980
query =
971981
from(t in Team,
972982
join: tm in assoc(t, :team_members),
@@ -986,28 +996,14 @@ defmodule Cadet.Assessments do
986996
limit: 1
987997
)
988998

989-
assessment_team_size =
990-
Map.get(
991-
Repo.one(
992-
from(a in Assessment,
993-
where: a.id == ^assessment_id,
994-
select: %{max_team_size: a.max_team_size}
995-
)
996-
),
997-
:max_team_size,
998-
0
999-
)
1000-
1001-
case assessment_team_size > 1 do
1002-
true ->
1003-
case Repo.one(query) do
1004-
nil -> {:error, :team_not_found}
1005-
team -> {:ok, team}
1006-
end
1007-
999+
if is_team_assessment?(assessment_id) do
1000+
case Repo.one(query) do
1001+
nil -> {:error, :team_not_found}
1002+
team -> {:ok, team}
1003+
end
1004+
else
10081005
# team is nil for individual assessments
1009-
false ->
1010-
{:ok, nil}
1006+
{:ok, nil}
10111007
end
10121008
end
10131009

lib/cadet/assessments/submission.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ defmodule Cadet.Assessments.Submission do
4343
|> validate_xor_relationship
4444
|> validate_required(@required_fields)
4545
|> foreign_key_constraint(:student_id)
46+
|> foreign_key_constraint(:team_id)
4647
|> foreign_key_constraint(:assessment_id)
4748
|> foreign_key_constraint(:unsubmitted_by_id)
4849
end

lib/cadet/jobs/autograder/grading_job.ex

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Cadet.Autograder.GradingJob do
99

1010
require Logger
1111

12+
alias Cadet.Accounts.{Team, TeamMember}
1213
alias Cadet.Assessments
1314
alias Cadet.Assessments.{Answer, Assessment, Question, Submission, SubmissionVotes}
1415
alias Cadet.Autograder.Utilities
@@ -102,13 +103,68 @@ defmodule Cadet.Autograder.GradingJob do
102103
end
103104

104105
defp insert_empty_submission(%{student_id: student_id, assessment: assessment}) do
105-
%Submission{}
106-
|> Submission.changeset(%{
107-
student_id: student_id,
108-
assessment: assessment,
109-
status: :submitted
110-
})
111-
|> Repo.insert!()
106+
if Assessments.is_team_assessment?(assessment.id) do
107+
# Get current team if any
108+
team =
109+
Team
110+
|> where(assessment_id: ^assessment.id)
111+
|> join(:inner, [t], tm in assoc(t, :team_members))
112+
|> where([_, tm], tm.student_id == ^student_id)
113+
|> Repo.one()
114+
115+
team =
116+
if team do
117+
team
118+
else
119+
# Student is not in any team
120+
# Create new team just for the student
121+
team =
122+
%Team{}
123+
|> Team.changeset(%{
124+
assessment_id: assessment.id
125+
})
126+
|> Repo.insert!()
127+
128+
%TeamMember{}
129+
|> TeamMember.changeset(%{
130+
team_id: team.id,
131+
student_id: student_id
132+
})
133+
|> Repo.insert!()
134+
135+
team
136+
end
137+
138+
find_or_create_team_submission(team.id, assessment)
139+
else
140+
# Individual assessment
141+
%Submission{}
142+
|> Submission.changeset(%{
143+
student_id: student_id,
144+
assessment: assessment,
145+
status: :submitted
146+
})
147+
|> Repo.insert!()
148+
end
149+
end
150+
151+
defp find_or_create_team_submission(team_id, assessment) when is_ecto_id(team_id) do
152+
submission =
153+
Submission
154+
|> where(team_id: ^team_id, assessment_id: ^assessment.id)
155+
|> Repo.one()
156+
157+
if submission do
158+
submission
159+
else
160+
%Submission{}
161+
|> Submission.changeset(%{
162+
team_id: team_id,
163+
assessment: assessment,
164+
status: :submitted
165+
})
166+
|> Repo.insert!()
167+
end
112168
end
113169

114170
defp update_submission_status_to_submitted(submission = %Submission{status: status}) do

lib/cadet/jobs/autograder/utilities.ex

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ defmodule Cadet.Autograder.Utilities do
88

99
import Ecto.Query
1010

11-
alias Cadet.Accounts.CourseRegistration
11+
alias Cadet.Accounts.{CourseRegistration, TeamMember}
12+
13+
alias Cadet.Assessments
1214
alias Cadet.Assessments.{Answer, Assessment, Question, Submission}
1315

1416
def dispatch_programming_answer(answer = %Answer{}, question = %Question{}, overwrite \\ false) do
@@ -25,7 +27,37 @@ defmodule Cadet.Autograder.Utilities do
2527
})
2628
end
2729

28-
def fetch_submissions(assessment_id, course_id) when is_ecto_id(assessment_id) do
30+
def fetch_submissions(assessment_id, course_id)
31+
when is_ecto_id(assessment_id) and is_ecto_id(course_id) do
32+
if Assessments.is_team_assessment?(assessment_id) do
33+
fetch_team_submissions(assessment_id, course_id)
34+
else
35+
fetch_student_submissions(assessment_id, course_id)
36+
end
37+
end
38+
39+
defp fetch_team_submissions(assessment_id, course_id)
40+
when is_ecto_id(assessment_id) and is_ecto_id(course_id) do
41+
CourseRegistration
42+
|> where(role: "student", course_id: ^course_id)
43+
|> join(
44+
:left,
45+
[cr],
46+
tm in TeamMember,
47+
on: cr.id == tm.student_id
48+
)
49+
|> join(
50+
:left,
51+
[cr, tm],
52+
s in Submission,
53+
on: tm.team_id == s.team_id and s.assessment_id == ^assessment_id
54+
)
55+
|> select([cr, tm, s], %{student_id: cr.id, submission: s})
56+
|> Repo.all()
57+
end
58+
59+
defp fetch_student_submissions(assessment_id, course_id)
60+
when is_ecto_id(assessment_id) and is_ecto_id(course_id) do
2961
CourseRegistration
3062
|> where(role: "student", course_id: ^course_id)
3163
|> join(

mix.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"},
55
"bamboo": {:hex, :bamboo, "2.3.1", "85029339f01c3dd59071cfd2b7b18e826aa7fc172cc324d75603bb99d95b6544", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "895b2993ed195b2b0fa79c0d5a1d36aa529e817b6df257e4a10745459048d505"},
66
"bamboo_phoenix": {:hex, :bamboo_phoenix, "1.0.0", "f3cc591ffb163ed0bf935d256f1f4645cd870cf436545601215745fb9cc9953f", [:mix], [{:bamboo, ">= 2.0.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.3.0", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "6db88fbb26019c84a47994bb2bd879c0887c29ce6c559bc6385fd54eb8b37dee"},
7-
"bamboo_ses": {:hex, :bamboo_ses, "0.4.4", "143afad1cffe07db9c68592d41a0685a2f950d3c31369f9da6030773a61b7566", [:mix], [{:bamboo, "~> 2.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:ex_aws, "~> 2.4", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 1.2.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "91740b6b1ab432ca2e41274d097bcf2239149d797acb2b1c4b0d07318bf8aa7e"},
7+
"bamboo_ses": {:hex, :bamboo_ses, "0.4.5", "f974bc8321b1520ff042c3415b1f296a5fb3f77c8240bc6721ac73554cf6fbd2", [:mix], [{:bamboo, "~> 2.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:ex_aws, "~> 2.4", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 1.2.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ea3e82b35a7a255690753824392e8eb25f5bf884cfec416deb9a81bbeb1b503b"},
88
"blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm", "7cf11aac0e44f4eedbee0c15c1d37d94c090cb72a8d9fddf9f7aec30f9278899"},
99
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
1010
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
@@ -31,7 +31,7 @@
3131
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
3232
"esaml": {:hex, :esaml, "4.6.0", "8fb5a3a0d56ccfce3e081a2f72b29058511047a2abbafb64cb6f595bf7465124", [:rebar3], [{:cowboy, "< 3.0.0", [hex: :cowboy, repo: "hexpm", optional: false]}], "hexpm", "d34d0b259cd8ac8215fd2c333fac9dbbb91b5f5da5a9304508612ff3ac0afa7a"},
3333
"ex2ms": {:hex, :ex2ms, "1.7.0", "45b9f523d0b777667ded60070d82d871a37e294f0b6c5b8eca86771f00f82ee1", [:mix], [], "hexpm", "2589eee51f81f1b1caa6d08c990b1ad409215fe6f64c73f73c67d36ed10be827"},
34-
"ex_aws": {:hex, :ex_aws, "2.5.5", "5dc378eff99c3c46c917b7a96a75ad0d4c300ab7250df668d0819bcd18c0213d", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ed7ee39c56012c56600e021953c6487ecce9c49320ec3b4655a15d785f221ca6"},
34+
"ex_aws": {:hex, :ex_aws, "2.5.6", "6f642e0f82eff10a9b470044f084b81a791cf15b393d647ea5f3e65da2794e3d", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c69eec59e31fdd89d0beeb1d97e16518dd1b23ad95b3d5c9f1dcfec23d97f960"},
3535
"ex_aws_lambda": {:hex, :ex_aws_lambda, "2.1.0", "f28bffae6dde34ba17ef815ef50a82a1e7f3af26d36fe31dbda3a7657e0de449", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm", "25608630b8b45fe22b0237696662f88be724bc89f77ee42708a5871511f531a0"},
3636
"ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [: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", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"},
3737
"ex_aws_secretsmanager": {:hex, :ex_aws_secretsmanager, "2.0.0", "deff8c12335f0160882afeb9687e55a97fddcd7d9a82fc3a6fbb270797374773", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm", "8b2838af536c32263ff797012b29e87bad73ef34f43cfa60ebca8e84576f6d45"},
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule Cadet.Repo.Migrations.CreateTeamsSubmissionConstraint do
2+
use Ecto.Migration
3+
4+
def change do
5+
create(index(:submissions, :team_id))
6+
create(unique_index(:submissions, [:assessment_id, :team_id]))
7+
end
8+
end

test/cadet/jobs/autograder/utilities_test.exs

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,58 @@ defmodule Cadet.Autograder.UtilitiesTest do
7272
setup do
7373
course = insert(:course)
7474
config = insert(:assessment_config, %{course: course})
75-
assessment = insert(:assessment, %{is_published: true, course: course, config: config})
75+
76+
# Individual assessment
77+
assessment =
78+
insert(:assessment, %{
79+
is_published: true,
80+
course: course,
81+
config: config,
82+
max_team_size: 1
83+
})
84+
7685
students = insert_list(5, :course_registration, %{role: :student, course: course})
7786
insert(:course_registration, %{course: build(:course), role: :student})
78-
%{students: students, assessment: assessment}
87+
88+
# Team assessment
89+
team_assessment =
90+
insert(:assessment, %{
91+
is_published: true,
92+
course: course,
93+
config: config,
94+
max_team_size: 2
95+
})
96+
97+
team_member1 = insert(:team_member, %{student: Enum.at(students, 0)})
98+
team_member2 = insert(:team_member, %{student: Enum.at(students, 1)})
99+
100+
team1 =
101+
insert(:team, %{assessment: team_assessment, team_members: [team_member1, team_member2]})
102+
103+
team_member3 = insert(:team_member, %{student: Enum.at(students, 2)})
104+
team_member4 = insert(:team_member, %{student: Enum.at(students, 3)})
105+
106+
team2 =
107+
insert(:team, %{assessment: team_assessment, team_members: [team_member3, team_member4]})
108+
109+
%{
110+
individual: %{students: students, assessment: assessment},
111+
team: %{
112+
teams: [team1, team2],
113+
assessment: team_assessment,
114+
teamless: [Enum.at(students, 4)]
115+
}
116+
}
79117
end
80118

81-
test "it returns list of students with matching submissions", %{
82-
students: students,
83-
assessment: assessment
119+
test "it returns list of students with matching submissions for individual assessments", %{
120+
individual: %{students: students, assessment: assessment}
84121
} do
85122
submissions =
86-
Enum.map(students, &insert(:submission, %{assessment: assessment, student: &1}))
123+
Enum.map(
124+
students,
125+
&insert(:submission, %{assessment: assessment, student: &1, team: nil})
126+
)
87127

88128
expected = Enum.map(submissions, &%{student_id: &1.student_id, submission_id: &1.id})
89129

@@ -95,9 +135,8 @@ defmodule Cadet.Autograder.UtilitiesTest do
95135
assert results == expected
96136
end
97137

98-
test "it returns list of students with without matching submissions", %{
99-
students: students,
100-
assessment: assessment
138+
test "it returns list of students without matching submissions for individual assessments", %{
139+
individual: %{students: students, assessment: assessment}
101140
} do
102141
expected_student_ids = Enum.map(students, & &1.id)
103142

@@ -108,6 +147,45 @@ defmodule Cadet.Autograder.UtilitiesTest do
108147

109148
assert results |> Enum.map(& &1.submission) |> Enum.uniq() == [nil]
110149
end
150+
151+
test "it returns list of students both with and without matching submissions for team assessments",
152+
%{
153+
team: %{teams: teams, assessment: assessment, teamless: teamless}
154+
} do
155+
submissions =
156+
Enum.map(
157+
teams,
158+
&%{
159+
team_id: &1.id,
160+
submission: insert(:submission, %{assessment: assessment, team: &1, student: nil})
161+
}
162+
)
163+
164+
expected =
165+
teams
166+
|> Enum.flat_map(& &1.team_members)
167+
|> Enum.map(
168+
&%{
169+
student_id: &1.student_id,
170+
submission_id:
171+
Enum.find(submissions, fn s -> s.team_id == &1.team_id end).submission.id
172+
}
173+
)
174+
175+
expected = expected ++ Enum.map(teamless, &%{student_id: &1.id, submission_id: nil})
176+
177+
results =
178+
assessment.id
179+
|> Utilities.fetch_submissions(assessment.course_id)
180+
|> Enum.map(
181+
&%{
182+
student_id: &1.student_id,
183+
submission_id: if(&1.submission, do: &1.submission.id, else: nil)
184+
}
185+
)
186+
187+
assert results == expected
188+
end
111189
end
112190

113191
defp get_assessments_ids(assessments) do

0 commit comments

Comments
 (0)