Skip to content

Commit 6da808e

Browse files
committed
Add separate visibility to problems
Tests are missing, but existing ones were adapted to the new functionality.
1 parent 9f7a0b2 commit 6da808e

File tree

15 files changed

+120
-33
lines changed

15 files changed

+120
-33
lines changed

web/attempts/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class AttemptAdmin(SimpleHistoryAdmin):
2424
"part__problem__problem_set__course__institution",
2525
"part__problem__problem_set__course",
2626
"part__problem__problem_set",
27+
"part__problem__visible",
2728
)
2829
search_fields = (
2930
"part__pk",

web/attempts/tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class AttemptSubmitTestCase(TestCase):
1111
def setUp(self):
1212
course = baker.make("courses.Course")
1313
problem_set = baker.make("courses.ProblemSet", course=course, visible=True)
14-
problem = baker.make("problems.Problem", problem_set=problem_set)
14+
problem = baker.make("problems.Problem", problem_set=problem_set, visible=True)
1515
self.part1 = baker.make("problems.Part", problem=problem, secret='["1"]')
1616
self.part2 = baker.make("problems.Part", problem=problem, secret='["1", "2"]')
1717
self.part3 = baker.make(

web/courses/models.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def recent_problem_sets(self, n=3):
4646
return self.problem_sets.reverse().filter(visible=True)[:n]
4747

4848
def user_attempts(self, user):
49+
"""This function ignores problem visibility, because it assumes it is only called from problems/models.py:marking_file() by a teacher user."""
4950
attempts = {}
5051
for attempt in user.attempts.filter(part__problem__problem_set__course=self):
5152
attempts[attempt.part_id] = attempt
@@ -108,7 +109,7 @@ def annotate_for_teacher(self):
108109
student_count = len(students)
109110

110111
part_sets = Part.objects.filter(
111-
problem__problem_set__in=self.annotated_problem_sets
112+
problem__problem_set__in=self.annotated_problem_sets, problem__visible=True
112113
)
113114
parts_count = (
114115
part_sets.values("problem__problem_set_id")
@@ -123,6 +124,7 @@ def annotate_for_teacher(self):
123124
attempts_full = Attempt.objects.filter(
124125
user__in=students,
125126
part__problem__problem_set__in=self.annotated_problem_sets,
127+
part__problem__visible=True,
126128
)
127129
attempts = attempts_full.values("valid", "part__problem__problem_set_id")
128130
attempts_dict = {}
@@ -197,8 +199,12 @@ def observed_students(self):
197199
def student_success(self):
198200
students = self.observed_students()
199201
problem_sets = self.problem_sets.filter(visible=True)
200-
part_count = Part.objects.filter(problem__problem_set__in=problem_sets).count()
201-
attempts = Attempt.objects.filter(part__problem__problem_set__in=problem_sets)
202+
part_count = Part.objects.filter(
203+
problem__problem_set__in=problem_sets, problem__visible=True
204+
).count()
205+
attempts = Attempt.objects.filter(
206+
part__problem__problem_set__in=problem_sets, part__problem__visible=True
207+
)
202208
valid_attempts = (
203209
attempts.filter(valid=True).values("user").annotate(Count("user"))
204210
)
@@ -253,9 +259,8 @@ def student_success_by_problem_set(self):
253259
"problems", "problems__parts"
254260
):
255261
different_subtasks = 0
256-
for problem in problem_set.problems.all():
257-
for part in problem.parts.all():
258-
different_subtasks += 1
262+
for problem in problem_set.problems.filter(visible=True):
263+
different_subtasks += problem.parts.count()
259264

260265
# In case there are no parts, we do not want to divide by 0
261266
if different_subtasks == 0:
@@ -268,7 +273,9 @@ def student_success_by_problem_set(self):
268273
] # Valid, invalid
269274

270275
attempts = Attempt.objects.filter(
271-
part__problem__problem_set=problem_set, user__in=students
276+
part__problem__problem_set=problem_set,
277+
user__in=students,
278+
part__problem__visible=True,
272279
)
273280
for attempt in attempts:
274281
if attempt.valid:
@@ -312,13 +319,13 @@ def student_success_by_problemset_grouped_by_groups(self):
312319
groups = self.groups.all()
313320
student_success = self.student_success_by_problem_set()
314321

315-
student_sucess_by_groups = {}
322+
student_success_by_groups = {}
316323
for group in groups:
317-
student_sucess_by_groups[group] = {}
324+
student_success_by_groups[group] = {}
318325
for student in group.students.all():
319-
student_sucess_by_groups[group][student] = student_success[student]
326+
student_success_by_groups[group][student] = student_success[student]
320327

321-
return student_sucess_by_groups
328+
return student_success_by_groups
322329

323330

324331
class StudentEnrollment(models.Model):
@@ -395,7 +402,13 @@ def get_absolute_url(self):
395402
return reverse("problem_set_detail", args=[str(self.pk)])
396403

397404
def attempts_archive(self, user):
398-
files = [problem.attempt_file(user) for problem in self.problems.all()]
405+
if user.can_edit_problem_set(self):
406+
files = [problem.attempt_file(user) for problem in self.problems.all()]
407+
else:
408+
files = [
409+
problem.attempt_file(user)
410+
for problem in self.problems.filter(visible=True)
411+
]
399412
archive_name = slugify(self.title)
400413
return archive_name, files
401414

@@ -433,33 +446,33 @@ def results_archive(self, user):
433446
attempt_dict[user_id] = user_attempts
434447
users = User.objects.filter(id__in=user_ids)
435448

436-
archive_name = "{0}-results".format(slugify(self.title))
449+
archive_name = f"{slugify(self.title)}-results"
437450
files = []
438451

439452
bare_files = {}
440453
for problem in self.problems.all():
441454
folder = slugify(problem.title)
442455
for user in users.all():
443456
filename, contents = problem.marking_file(user)
444-
files.append(("{0}/{1}".format(folder, filename), contents))
457+
files.append((f"{folder}/{filename}", contents))
445458
filename, contents = problem.bare_file(user)
446459
bare_files[filename] = bare_files.get(filename, "") + contents + "\n\n"
447460

448461
for filename, contents in bare_files.items():
449-
files.append(("bare/{0}".format(filename), contents))
462+
files.append((f"bare/{filename}", contents))
450463

451464
for user, history in self.attempt_history().items():
452465
username = user.get_full_name() or user.username
453466
problem_slug = slugify(username).replace("-", "_")
454467
extension = "py"
455-
filename = "{0}.{1}".format(problem_slug, extension)
468+
filename = f"{problem_slug}.{extension}"
456469
contents = render_to_string(
457-
"history.{0}".format(extension),
470+
f"history.{extension}",
458471
{
459472
"history": history,
460473
},
461474
)
462-
files.append(("history/{0}".format(filename), contents))
475+
files.append((f"history/{filename}", contents))
463476

464477
users = []
465478
for user in User.objects.filter(id__in=user_ids).order_by("last_name"):
@@ -524,6 +537,7 @@ def student_statistics(self):
524537
"title": problem.title,
525538
"pk": problem.pk,
526539
"parts": parts,
540+
"visible": problem.visible,
527541
}
528542
)
529543
return statistics
@@ -532,10 +546,13 @@ def valid_percentage(self, user):
532546
"""
533547
Returns the percentage of parts (rounded to the nearest integer)
534548
of parts in this problem set for which the given user has a valid attempt.
549+
Doesn't count parts of problems that have visible set to False.
535550
"""
536-
number_of_all_parts = Part.objects.filter(problem__problem_set=self).count()
551+
number_of_all_parts = Part.objects.filter(
552+
problem__problem_set=self, problem__visible=True
553+
).count()
537554
number_of_valid_parts = user.attempts.filter(
538-
valid=True, part__problem__problem_set=self
555+
valid=True, part__problem__problem_set=self, part__problem__visible=True
539556
).count()
540557
if number_of_all_parts == 0:
541558
return None

web/courses/views.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ def problem_set_detail(request, problem_set_pk):
102102
user_attempts = request.user.attempts.filter(
103103
part__problem__problem_set__id=problem_set_pk
104104
)
105+
student_statistics = problem_set.student_statistics()
106+
if not request.user.is_teacher(problem_set.course):
107+
user_attempts = user_attempts.filter(part__problem__visible=True)
108+
student_statistics = filter(lambda p: p["visible"], student_statistics)
105109
valid_parts_ids = user_attempts.filter(valid=True).values_list("part_id", flat=True)
106110
invalid_parts_ids = user_attempts.filter(valid=False).values_list(
107111
"part_id", flat=True
@@ -115,14 +119,14 @@ def problem_set_detail(request, problem_set_pk):
115119
"valid_parts_ids": valid_parts_ids,
116120
"invalid_parts_ids": invalid_parts_ids,
117121
"show_teacher_forms": request.user.can_edit_problem_set(problem_set),
118-
"student_statistics": problem_set.student_statistics(),
122+
"student_statistics": student_statistics,
119123
},
120124
)
121125

122126

123127
@login_required
124128
def course_detail(request, course_pk):
125-
"""Show a list of all problems in a problem set."""
129+
"""Show a list of all problem sets in a course."""
126130
course = get_object_or_404(Course, pk=course_pk)
127131
verify(request.user.can_view_course(course))
128132
if request.user.can_edit_course(course):

web/problems/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class ProblemAdmin(SimpleHistoryAdmin):
1616
"course",
1717
"problem_set",
1818
"description",
19+
"visible",
1920
)
2021
list_display_links = ("title",)
2122
ordering = (
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.1.2 on 2023-02-18 21:42
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("problems", "0003_alter_part_problem_alter_problem_problem_set"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="historicalproblem",
15+
name="visible",
16+
field=models.BooleanField(default=True, verbose_name="Visible"),
17+
),
18+
migrations.AddField(
19+
model_name="problem",
20+
name="visible",
21+
field=models.BooleanField(default=True, verbose_name="Visible"),
22+
),
23+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.1.2 on 2023-02-18 21:42
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("problems", "0004_historicalproblem_visible_problem_visible"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="historicalproblem",
15+
name="visible",
16+
field=models.BooleanField(default=False, verbose_name="Visible"),
17+
),
18+
migrations.AlterField(
19+
model_name="problem",
20+
name="visible",
21+
field=models.BooleanField(default=False, verbose_name="Visible"),
22+
),
23+
]

web/problems/models.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.template.defaultfilters import slugify
88
from django.template.loader import render_to_string
99
from django.urls import reverse
10+
from django.utils.translation import gettext_lazy as _
1011
from rest_framework.authtoken.models import Token
1112
from simple_history.models import HistoricalRecords
1213
from taggit.managers import TaggableManager
@@ -20,6 +21,7 @@ class Problem(OrderWithRespectToMixin, models.Model):
2021
problem_set = models.ForeignKey(
2122
"courses.ProblemSet", on_delete=models.CASCADE, related_name="problems"
2223
)
24+
visible = models.BooleanField(default=False, verbose_name=_("Visible"))
2325
history = HistoricalRecords()
2426
tags = TaggableManager(blank=True)
2527
language = models.CharField(
@@ -48,7 +50,7 @@ def get_absolute_url(self):
4850
return "{}#{}".format(self.problem_set.get_absolute_url(), self.anchor())
4951

5052
def anchor(self):
51-
return "problem-{}".format(self.pk)
53+
return f"problem-{self.pk}"
5254

5355
def user_attempts(self, user):
5456
return user.attempts.filter(part__problem=self)
@@ -99,6 +101,9 @@ def solution_file(self):
99101
return filename, contents
100102

101103
def marking_file(self, user):
104+
"""This function ignores problem visibility because it assumes its
105+
called only from courses/models.py:results_archive() by a teacher user
106+
"""
102107
attempts = {attempt.part.id: attempt for attempt in self.user_attempts(user)}
103108
parts = [(part, attempts.get(part.id)) for part in self.parts.all()]
104109
username = user.get_full_name() or user.username
@@ -116,6 +121,9 @@ def marking_file(self, user):
116121
return filename, contents
117122

118123
def bare_file(self, user):
124+
"""This function ignores problem visibility because it assumes its
125+
called only from courses/models.py:results_archive() by a teacher user
126+
"""
119127
attempts = {attempt.part.id: attempt for attempt in self.user_attempts(user)}
120128
parts = [(part, attempts.get(part.id)) for part in self.parts.all()]
121129
username = user.get_full_name() or user.username
@@ -133,6 +141,9 @@ def bare_file(self, user):
133141
return filename, contents
134142

135143
def edit_file(self, user):
144+
"""This function ignores problem visibility because it assumes its
145+
called only from courses/models.py:edit_archive() by a teacher user
146+
"""
136147
authentication_token = Token.objects.get(user=user)
137148
url = settings.SUBMISSION_URL + reverse("problems-submit")
138149
problem_slug = slugify(self.title).replace("-", "_")

web/problems/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class ProblemCreate(CreateView):
5252
"""
5353

5454
model = Problem
55-
fields = ["title", "description", "language"]
55+
fields = ["title", "description", "language", "visible"]
5656

5757
def get_context_data(self, **kwargs):
5858
context = super(ProblemCreate, self).get_context_data(**kwargs)

web/templates/courses/coursegroup_form.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
1313
<h4 class="modal-title">
1414
{% if form.instance.pk %}
15-
{# Translators: Title of editing form for problems sets. #}
15+
{# Translators: Title of editing form for problem sets. #}
1616
{% trans "Edit course group" %} <em>{{ form.instance.title }}</em>
1717
{% else %}
1818
{# Translators: Title of 'add new problem set' form. #}

0 commit comments

Comments
 (0)