Skip to content

Commit b6b94f6

Browse files
authored
Merge branch 'master' into PopularVoteContest
2 parents 8b42dfe + 3135f48 commit b6b94f6

File tree

7 files changed

+416
-232
lines changed

7 files changed

+416
-232
lines changed

lib/cadet/assessments/assessments.ex

Lines changed: 217 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,92 +1324,229 @@ defmodule Cadet.Assessments do
13241324

13251325
@doc """
13261326
Function returning submissions under a grader. This function returns only the
1327-
fields that are exposed in the /grading endpoint. The reason we select only
1328-
those fields is to reduce the memory usage especially when the number of
1329-
submissions is large i.e. > 25000 submissions.
1327+
fields that are exposed in the /grading endpoint.
13301328
1331-
The input parameters are the user and group_only. group_only is used to check
1332-
whether only the groups under the grader should be returned. The parameter is
1333-
a boolean which is false by default.
1329+
The input parameters are the user and query parameters. Query parameters are
1330+
used to filter the submissions.
13341331
1335-
The return value is {:ok, submissions} if no errors, else it is {:error,
1336-
{:forbidden, "Forbidden."}}
1332+
The group parameter is used to check whether only the groups under the grader
1333+
should be returned. If pageSize and offset are not provided, the default
1334+
values are 10 and 0 respectively.
1335+
1336+
The return value is
1337+
{:ok, %{"count": count, "data": submissions}} if no errors,
1338+
else it is {:error, {:forbidden, "Forbidden."}}
13371339
"""
1338-
@spec all_submissions_by_grader_for_index(CourseRegistration.t()) ::
1339-
{:ok, %{:assessments => [any()], :submissions => [any()], :users => [any()]}}
1340-
def all_submissions_by_grader_for_index(
1340+
1341+
# We bypass Ecto here and use a raw query to generate JSON directly from
1342+
# PostgreSQL, because doing it in Elixir/Erlang is too inefficient.
1343+
@spec submissions_by_grader_for_index(CourseRegistration.t()) ::
1344+
{:ok,
1345+
%{
1346+
:count => integer,
1347+
:data => %{:assessments => [any()], :submissions => [any()], :users => [any()]}
1348+
}}
1349+
def submissions_by_grader_for_index(
13411350
grader = %CourseRegistration{course_id: course_id},
1342-
group_only \\ false,
1343-
_ungraded_only \\ false
1351+
params \\ %{
1352+
"group" => "false",
1353+
"notFullyGraded" => "true",
1354+
"pageSize" => "10",
1355+
"offset" => "0"
1356+
}
13441357
) do
1345-
show_all = not group_only
1346-
1347-
group_filter =
1348-
if show_all,
1349-
do: "",
1350-
else:
1351-
"AND s.student_id IN (SELECT cr.id FROM course_registrations AS cr INNER JOIN groups AS g ON cr.group_id = g.id WHERE g.leader_id = #{grader.id}) OR s.student_id = #{grader.id}"
1352-
1353-
# TODO: Restore ungraded filtering
1354-
# ... or more likely, decouple email logic from this function
1355-
# ungraded_where =
1356-
# if ungraded_only,
1357-
# do: "where s.\"gradedCount\" < assts.\"questionCount\"",
1358-
# else: ""
1359-
1360-
# We bypass Ecto here and use a raw query to generate JSON directly from
1361-
# PostgreSQL, because doing it in Elixir/Erlang is too inefficient.
1362-
1363-
submissions =
1364-
case Repo.query("""
1365-
SELECT
1366-
s.id,
1367-
s.status,
1368-
s.unsubmitted_at,
1369-
s.unsubmitted_by_id,
1370-
s_ans.xp,
1371-
s_ans.xp_adjustment,
1372-
s.xp_bonus,
1373-
s_ans.graded_count,
1374-
s.student_id,
1375-
s.assessment_id
1376-
FROM
1377-
submissions AS s
1378-
LEFT JOIN (
1379-
SELECT
1380-
ans.submission_id,
1381-
SUM(ans.xp) AS xp,
1382-
SUM(ans.xp_adjustment) AS xp_adjustment,
1383-
COUNT(ans.id) FILTER (
1384-
WHERE
1385-
ans.grader_id IS NOT NULL
1386-
) AS graded_count
1387-
FROM
1388-
answers AS ans
1389-
GROUP BY
1390-
ans.submission_id
1391-
) AS s_ans ON s_ans.submission_id = s.id
1392-
WHERE
1393-
s.assessment_id IN (
1394-
SELECT
1395-
id
1396-
FROM
1397-
assessments
1398-
WHERE
1399-
assessments.course_id = #{course_id}
1400-
) #{group_filter};
1401-
""") do
1402-
{:ok, %{columns: columns, rows: result}} ->
1403-
result
1404-
|> Enum.map(
1405-
&(columns
1406-
|> Enum.map(fn c -> String.to_atom(c) end)
1407-
|> Enum.zip(&1)
1408-
|> Enum.into(%{}))
1409-
)
1410-
end
1358+
submission_answers_query =
1359+
from(ans in Answer,
1360+
group_by: ans.submission_id,
1361+
select: %{
1362+
submission_id: ans.submission_id,
1363+
xp: sum(ans.xp),
1364+
xp_adjustment: sum(ans.xp_adjustment),
1365+
graded_count: filter(count(ans.id), not is_nil(ans.grader_id))
1366+
}
1367+
)
1368+
1369+
question_answers_query =
1370+
from(q in Question,
1371+
group_by: q.assessment_id,
1372+
join: a in Assessment,
1373+
on: q.assessment_id == a.id,
1374+
select: %{
1375+
assessment_id: q.assessment_id,
1376+
question_count: count(q.id)
1377+
}
1378+
)
1379+
1380+
query =
1381+
from(s in Submission,
1382+
left_join: ans in subquery(submission_answers_query),
1383+
on: ans.submission_id == s.id,
1384+
as: :ans,
1385+
left_join: asst in subquery(question_answers_query),
1386+
on: asst.assessment_id == s.assessment_id,
1387+
as: :asst,
1388+
where: ^build_user_filter(params),
1389+
where: s.assessment_id in subquery(build_assessment_filter(params, course_id)),
1390+
where: s.assessment_id in subquery(build_assessment_config_filter(params)),
1391+
where: ^build_submission_filter(params),
1392+
where: ^build_course_registration_filter(params, grader),
1393+
order_by: [desc: s.inserted_at],
1394+
limit: ^elem(Integer.parse(Map.get(params, "pageSize", "10")), 0),
1395+
offset: ^elem(Integer.parse(Map.get(params, "offset", "0")), 0),
1396+
select: %{
1397+
id: s.id,
1398+
status: s.status,
1399+
xp_bonus: s.xp_bonus,
1400+
unsubmitted_at: s.unsubmitted_at,
1401+
unsubmitted_by_id: s.unsubmitted_by_id,
1402+
student_id: s.student_id,
1403+
assessment_id: s.assessment_id,
1404+
xp: ans.xp,
1405+
xp_adjustment: ans.xp_adjustment,
1406+
graded_count: ans.graded_count,
1407+
question_count: asst.question_count
1408+
}
1409+
)
1410+
1411+
submissions = Repo.all(query)
1412+
1413+
count_query =
1414+
from(s in Submission,
1415+
left_join: ans in subquery(submission_answers_query),
1416+
on: ans.submission_id == s.id,
1417+
as: :ans,
1418+
left_join: asst in subquery(question_answers_query),
1419+
on: asst.assessment_id == s.assessment_id,
1420+
as: :asst,
1421+
where: s.assessment_id in subquery(build_assessment_filter(params, course_id)),
1422+
where: s.assessment_id in subquery(build_assessment_config_filter(params)),
1423+
where: ^build_user_filter(params),
1424+
where: ^build_submission_filter(params),
1425+
where: ^build_course_registration_filter(params, grader),
1426+
select: count(s.id)
1427+
)
1428+
1429+
count = Repo.one(count_query)
1430+
1431+
{:ok, %{count: count, data: generate_grading_summary_view_model(submissions, course_id)}}
1432+
end
14111433

1412-
{:ok, generate_grading_summary_view_model(submissions, course_id)}
1434+
defp build_assessment_filter(params, course_id) do
1435+
assessments_filters =
1436+
Enum.reduce(params, dynamic(true), fn
1437+
{"title", value}, dynamic ->
1438+
dynamic([assessment], ^dynamic and ilike(assessment.title, ^"%#{value}%"))
1439+
1440+
{_, _}, dynamic ->
1441+
dynamic
1442+
end)
1443+
1444+
from(a in Assessment,
1445+
where: a.course_id == ^course_id,
1446+
where: ^assessments_filters,
1447+
select: a.id
1448+
)
1449+
end
1450+
1451+
defp build_submission_filter(params) do
1452+
Enum.reduce(params, dynamic(true), fn
1453+
{"status", value}, dynamic ->
1454+
dynamic([submission], ^dynamic and submission.status == ^value)
1455+
1456+
{"notFullyGraded", "true"}, dynamic ->
1457+
dynamic([ans: ans, asst: asst], ^dynamic and asst.question_count > ans.graded_count)
1458+
1459+
{_, _}, dynamic ->
1460+
dynamic
1461+
end)
1462+
end
1463+
1464+
defp build_course_registration_filter(params, grader) do
1465+
Enum.reduce(params, dynamic(true), fn
1466+
{"group", "true"}, dynamic ->
1467+
dynamic(
1468+
[submission],
1469+
(^dynamic and
1470+
submission.student_id in subquery(
1471+
from(cr in CourseRegistration,
1472+
join: g in Group,
1473+
on: cr.group_id == g.id,
1474+
where: g.leader_id == ^grader.id,
1475+
select: cr.id
1476+
)
1477+
)) or submission.student_id == ^grader.id
1478+
)
1479+
1480+
{"groupName", value}, dynamic ->
1481+
dynamic(
1482+
[submission],
1483+
^dynamic and
1484+
submission.student_id in subquery(
1485+
from(cr in CourseRegistration,
1486+
join: g in Group,
1487+
on: cr.group_id == g.id,
1488+
where: g.name == ^value,
1489+
select: cr.id
1490+
)
1491+
)
1492+
)
1493+
1494+
{_, _}, dynamic ->
1495+
dynamic
1496+
end)
1497+
end
1498+
1499+
defp build_user_filter(params) do
1500+
Enum.reduce(params, dynamic(true), fn
1501+
{"name", value}, dynamic ->
1502+
dynamic(
1503+
[submission],
1504+
^dynamic and
1505+
submission.student_id in subquery(
1506+
from(user in User,
1507+
where: ilike(user.name, ^"%#{value}%"),
1508+
select: user.id
1509+
)
1510+
)
1511+
)
1512+
1513+
{"username", value}, dynamic ->
1514+
dynamic(
1515+
[submission],
1516+
^dynamic and
1517+
submission.student_id in subquery(
1518+
from(user in User,
1519+
where: ilike(user.username, ^"%#{value}%"),
1520+
select: user.id
1521+
)
1522+
)
1523+
)
1524+
1525+
{_, _}, dynamic ->
1526+
dynamic
1527+
end)
1528+
end
1529+
1530+
defp build_assessment_config_filter(params) do
1531+
assessment_config_filters =
1532+
Enum.reduce(params, dynamic(true), fn
1533+
{"type", value}, dynamic ->
1534+
dynamic([assessment_config: config], ^dynamic and config.type == ^value)
1535+
1536+
{"isManuallyGraded", value}, dynamic ->
1537+
dynamic([assessment_config: config], ^dynamic and config.is_manually_graded == ^value)
1538+
1539+
{_, _}, dynamic ->
1540+
dynamic
1541+
end)
1542+
1543+
from(a in Assessment,
1544+
inner_join: config in AssessmentConfig,
1545+
on: a.config_id == config.id,
1546+
as: :assessment_config,
1547+
where: ^assessment_config_filters,
1548+
select: a.id
1549+
)
14131550
end
14141551

14151552
defp generate_grading_summary_view_model(submissions, course_id) do

lib/cadet/workers/NotificationWorker.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,11 @@ defmodule Cadet.Workers.NotificationWorker do
7373
for avenger_cr <- avengers_crs do
7474
avenger = Cadet.Accounts.get_user(avenger_cr.user_id)
7575

76-
{:ok, %{submissions: ungraded_submissions}} =
77-
Cadet.Assessments.all_submissions_by_grader_for_index(avenger_cr, true, true)
76+
{:ok, %{data: %{submissions: ungraded_submissions}}} =
77+
Cadet.Assessments.submissions_by_grader_for_index(avenger_cr, %{
78+
"group" => "true",
79+
"notFullyGraded" => "true"
80+
})
7881

7982
if Enum.count(ungraded_submissions) < ungraded_threshold do
8083
IO.puts("[AVENGER_BACKLOG] below threshold!")

lib/cadet_web/admin_controllers/admin_grading_controller.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ defmodule CadetWeb.AdminGradingController do
44

55
alias Cadet.Assessments
66

7-
def index(conn, %{"group" => group}) when group in ["true", "false"] do
7+
def index(conn, %{"group" => group} = params)
8+
when group in ["true", "false"] do
89
course_reg = conn.assigns[:course_reg]
910

10-
group = String.to_atom(group)
11-
12-
case Assessments.all_submissions_by_grader_for_index(course_reg, group) do
11+
case Assessments.submissions_by_grader_for_index(course_reg, params) do
1312
{:ok, view_model} ->
1413
conn
1514
|> put_status(:ok)

lib/cadet_web/admin_views/admin_grading_view.ex

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,36 @@ defmodule CadetWeb.AdminGradingView do
2525
end
2626

2727
def render("gradingsummaries.json", %{
28-
users: users,
29-
assessments: assessments,
30-
submissions: submissions
28+
count: count,
29+
data: %{
30+
users: users,
31+
assessments: assessments,
32+
submissions: submissions
33+
}
3134
}) do
32-
for submission <- submissions do
33-
user = users |> Enum.find(&(&1.id == submission.student_id))
34-
assessment = assessments |> Enum.find(&(&1.id == submission.assessment_id))
35+
%{
36+
count: count,
37+
data:
38+
for submission <- submissions do
39+
user = users |> Enum.find(&(&1.id == submission.student_id))
40+
assessment = assessments |> Enum.find(&(&1.id == submission.assessment_id))
3541

36-
render(
37-
CadetWeb.AdminGradingView,
38-
"gradingsummary.json",
39-
%{
40-
user: user,
41-
assessment: assessment,
42-
submission: submission,
43-
unsubmitter:
44-
case submission.unsubmitted_by_id do
45-
nil -> nil
46-
_ -> users |> Enum.find(&(&1.id == submission.unsubmitted_by_id))
47-
end
48-
}
49-
)
50-
end
42+
render(
43+
CadetWeb.AdminGradingView,
44+
"gradingsummary.json",
45+
%{
46+
user: user,
47+
assessment: assessment,
48+
submission: submission,
49+
unsubmitter:
50+
case submission.unsubmitted_by_id do
51+
nil -> nil
52+
_ -> users |> Enum.find(&(&1.id == submission.unsubmitted_by_id))
53+
end
54+
}
55+
)
56+
end
57+
}
5158
end
5259

5360
def render("gradingsummary.json", %{

0 commit comments

Comments
 (0)