Add order and score for course problems (#124)
* Add order and grade for course problems * Fix delete problems bug
This commit is contained in:
parent
67888bcd27
commit
c833dc06d9
10 changed files with 483 additions and 102 deletions
|
@ -564,6 +564,11 @@ urlpatterns = [
|
||||||
course.CourseStudentResults.as_view(),
|
course.CourseStudentResults.as_view(),
|
||||||
name="course_grades",
|
name="course_grades",
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r"^/grades/lesson/(?P<id>\d+)$",
|
||||||
|
course.CourseStudentResultsLesson.as_view(),
|
||||||
|
name="course_grades_lesson",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
44
judge/migrations/0192_course_lesson_problem.py
Normal file
44
judge/migrations/0192_course_lesson_problem.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Generated by Django 3.2.21 on 2024-09-02 05:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0191_deprecate_old_org_image"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CourseLessonProblem",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("order", models.IntegerField(default=0, verbose_name="order")),
|
||||||
|
("score", models.IntegerField(default=0, verbose_name="score")),
|
||||||
|
(
|
||||||
|
"lesson",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="lesson_problems",
|
||||||
|
to="judge.courselesson",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"problem",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="judge.problem"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -61,7 +61,7 @@ from judge.models.ticket import Ticket, TicketMessage
|
||||||
from judge.models.volunteer import VolunteerProblemVote
|
from judge.models.volunteer import VolunteerProblemVote
|
||||||
from judge.models.pagevote import PageVote, PageVoteVoter
|
from judge.models.pagevote import PageVote, PageVoteVoter
|
||||||
from judge.models.bookmark import BookMark, MakeBookMark
|
from judge.models.bookmark import BookMark, MakeBookMark
|
||||||
from judge.models.course import Course, CourseRole, CourseLesson
|
from judge.models.course import Course, CourseRole, CourseLesson, CourseLessonProblem
|
||||||
from judge.models.notification import Notification, NotificationProfile
|
from judge.models.notification import Notification, NotificationProfile
|
||||||
from judge.models.test_formatter import TestFormatterModel
|
from judge.models.test_formatter import TestFormatterModel
|
||||||
|
|
||||||
|
|
|
@ -165,3 +165,12 @@ class CourseLesson(models.Model):
|
||||||
problems = models.ManyToManyField(Problem, verbose_name=_("problem"), blank=True)
|
problems = models.ManyToManyField(Problem, verbose_name=_("problem"), blank=True)
|
||||||
order = models.IntegerField(verbose_name=_("order"), default=0)
|
order = models.IntegerField(verbose_name=_("order"), default=0)
|
||||||
points = models.IntegerField(verbose_name=_("points"))
|
points = models.IntegerField(verbose_name=_("points"))
|
||||||
|
|
||||||
|
|
||||||
|
class CourseLessonProblem(models.Model):
|
||||||
|
lesson = models.ForeignKey(
|
||||||
|
CourseLesson, on_delete=models.CASCADE, related_name="lesson_problems"
|
||||||
|
)
|
||||||
|
problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
|
||||||
|
order = models.IntegerField(verbose_name=_("order"), default=0)
|
||||||
|
score = models.IntegerField(verbose_name=_("score"), default=0)
|
||||||
|
|
|
@ -4,16 +4,32 @@ from django.views.generic import ListView, DetailView, View
|
||||||
from django.utils.translation import gettext, gettext_lazy as _
|
from django.utils.translation import gettext, gettext_lazy as _
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import inlineformset_factory
|
from django.forms import (
|
||||||
|
inlineformset_factory,
|
||||||
|
ModelForm,
|
||||||
|
modelformset_factory,
|
||||||
|
BaseModelFormSet,
|
||||||
|
)
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.db.models import Max, F
|
from django.db.models import Max, F
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from judge.models import Course, CourseLesson, Submission, Profile, CourseRole
|
from judge.models import (
|
||||||
|
Course,
|
||||||
|
CourseLesson,
|
||||||
|
Submission,
|
||||||
|
Profile,
|
||||||
|
CourseRole,
|
||||||
|
CourseLessonProblem,
|
||||||
|
)
|
||||||
from judge.models.course import RoleInCourse
|
from judge.models.course import RoleInCourse
|
||||||
from judge.widgets import HeavyPreviewPageDownWidget, HeavySelect2MultipleWidget
|
from judge.widgets import (
|
||||||
|
HeavyPreviewPageDownWidget,
|
||||||
|
HeavySelect2MultipleWidget,
|
||||||
|
HeavySelect2Widget,
|
||||||
|
)
|
||||||
from judge.utils.problems import (
|
from judge.utils.problems import (
|
||||||
user_attempted_ids,
|
user_attempted_ids,
|
||||||
user_completed_ids,
|
user_completed_ids,
|
||||||
|
@ -36,32 +52,35 @@ def max_case_points_per_problem(profile, problems):
|
||||||
|
|
||||||
def calculate_lessons_progress(profile, lessons):
|
def calculate_lessons_progress(profile, lessons):
|
||||||
res = {}
|
res = {}
|
||||||
total_achieved_points = 0
|
total_achieved_points = total_lesson_points = 0
|
||||||
total_points = 0
|
|
||||||
for lesson in lessons:
|
for lesson in lessons:
|
||||||
problems = list(lesson.problems.all())
|
problems = lesson.lesson_problems.values_list("problem", flat=True)
|
||||||
if not problems:
|
|
||||||
res[lesson.id] = {"achieved_points": 0, "percentage": 0}
|
|
||||||
total_points += lesson.points
|
|
||||||
continue
|
|
||||||
problem_points = max_case_points_per_problem(profile, problems)
|
problem_points = max_case_points_per_problem(profile, problems)
|
||||||
num_problems = len(problems)
|
achieved_points = total_points = 0
|
||||||
percentage = 0
|
|
||||||
for val in problem_points.values():
|
for lesson_problem in lesson.lesson_problems.all():
|
||||||
if val["case_total"] > 0:
|
val = problem_points.get(lesson_problem.problem.id)
|
||||||
score = val["case_points"] / val["case_total"]
|
if val and val["case_total"]:
|
||||||
percentage += score / num_problems
|
achieved_points += (
|
||||||
|
val["case_points"] / val["case_total"] * lesson_problem.score
|
||||||
|
)
|
||||||
|
total_points += lesson_problem.score
|
||||||
|
|
||||||
res[lesson.id] = {
|
res[lesson.id] = {
|
||||||
"achieved_points": percentage * lesson.points,
|
"achieved_points": achieved_points,
|
||||||
"percentage": percentage * 100,
|
"total_points": total_points,
|
||||||
|
"percentage": achieved_points / total_points * 100 if total_points else 0,
|
||||||
}
|
}
|
||||||
total_achieved_points += percentage * lesson.points
|
if total_points:
|
||||||
total_points += lesson.points
|
total_achieved_points += achieved_points / total_points * lesson.points
|
||||||
|
total_lesson_points += lesson.points
|
||||||
|
|
||||||
res["total"] = {
|
res["total"] = {
|
||||||
"achieved_points": total_achieved_points,
|
"achieved_points": total_achieved_points,
|
||||||
"total_points": total_points,
|
"total_points": total_lesson_points,
|
||||||
"percentage": total_achieved_points / total_points * 100 if total_points else 0,
|
"percentage": total_achieved_points / total_lesson_points * 100
|
||||||
|
if total_lesson_points
|
||||||
|
else 0,
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -157,12 +176,13 @@ class CourseLessonDetail(CourseDetailMixin, DetailView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(CourseLessonDetail, self).get_context_data(**kwargs)
|
context = super(CourseLessonDetail, self).get_context_data(**kwargs)
|
||||||
profile = self.get_profile()
|
profile = self.get_profile()
|
||||||
|
context["profile"] = profile
|
||||||
context["title"] = self.lesson.title
|
context["title"] = self.lesson.title
|
||||||
context["lesson"] = self.lesson
|
context["lesson"] = self.lesson
|
||||||
context["completed_problem_ids"] = user_completed_ids(profile)
|
context["completed_problem_ids"] = user_completed_ids(profile)
|
||||||
context["attempted_problems"] = user_attempted_ids(profile)
|
context["attempted_problems"] = user_attempted_ids(profile)
|
||||||
context["problem_points"] = max_case_points_per_problem(
|
context["problem_points"] = max_case_points_per_problem(
|
||||||
profile, self.lesson.problems.all()
|
profile, self.lesson.lesson_problems.values_list("problem", flat=True)
|
||||||
)
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -183,20 +203,65 @@ CourseLessonFormSet = inlineformset_factory(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CourseLessonProblemForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CourseLessonProblem
|
||||||
|
fields = ["order", "problem", "score", "lesson"]
|
||||||
|
widgets = {
|
||||||
|
"problem": HeavySelect2Widget(
|
||||||
|
data_view="problem_select2", attrs={"style": "width: 100%"}
|
||||||
|
),
|
||||||
|
"lesson": forms.HiddenInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CourseLessonProblemFormSet = modelformset_factory(
|
||||||
|
CourseLessonProblem, form=CourseLessonProblemForm, extra=5, can_delete=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EditCourseLessonsView(CourseEditableMixin, FormView):
|
class EditCourseLessonsView(CourseEditableMixin, FormView):
|
||||||
template_name = "course/edit_lesson.html"
|
template_name = "course/edit_lesson.html"
|
||||||
form_class = CourseLessonFormSet
|
form_class = CourseLessonFormSet
|
||||||
|
|
||||||
|
def get_problem_formset(self, post=False, lesson=None):
|
||||||
|
formset = CourseLessonProblemFormSet(
|
||||||
|
data=self.request.POST if post else None,
|
||||||
|
prefix=f"problems_{lesson.id}" if lesson else "problems",
|
||||||
|
queryset=CourseLessonProblem.objects.filter(lesson=lesson).order_by(
|
||||||
|
"order"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if lesson:
|
||||||
|
for form in formset:
|
||||||
|
form.fields["lesson"].initial = lesson
|
||||||
|
return formset
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(EditCourseLessonsView, self).get_context_data(**kwargs)
|
context = super(EditCourseLessonsView, self).get_context_data(**kwargs)
|
||||||
if self.request.method == "POST":
|
if self.request.method == "POST":
|
||||||
context["formset"] = self.form_class(
|
context["formset"] = self.form_class(
|
||||||
self.request.POST, self.request.FILES, instance=self.course
|
self.request.POST, self.request.FILES, instance=self.course
|
||||||
)
|
)
|
||||||
|
context["problem_formsets"] = {
|
||||||
|
lesson.instance.id: self.get_problem_formset(
|
||||||
|
post=True, lesson=lesson.instance
|
||||||
|
)
|
||||||
|
for lesson in context["formset"].forms
|
||||||
|
if lesson.instance.id
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
context["formset"] = self.form_class(
|
context["formset"] = self.form_class(
|
||||||
instance=self.course, queryset=self.course.lessons.order_by("order")
|
instance=self.course, queryset=self.course.lessons.order_by("order")
|
||||||
)
|
)
|
||||||
|
context["problem_formsets"] = {
|
||||||
|
lesson.instance.id: self.get_problem_formset(
|
||||||
|
post=False, lesson=lesson.instance
|
||||||
|
)
|
||||||
|
for lesson in context["formset"].forms
|
||||||
|
if lesson.instance.id
|
||||||
|
}
|
||||||
|
|
||||||
context["title"] = _("Edit lessons for %(course_name)s") % {
|
context["title"] = _("Edit lessons for %(course_name)s") % {
|
||||||
"course_name": self.course.name
|
"course_name": self.course.name
|
||||||
}
|
}
|
||||||
|
@ -213,8 +278,22 @@ class EditCourseLessonsView(CourseEditableMixin, FormView):
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
formset = self.form_class(request.POST, instance=self.course)
|
formset = self.form_class(request.POST, instance=self.course)
|
||||||
|
problem_formsets = [
|
||||||
|
self.get_problem_formset(post=True, lesson=lesson.instance)
|
||||||
|
for lesson in formset.forms
|
||||||
|
if lesson.instance.id
|
||||||
|
]
|
||||||
|
for pf in problem_formsets:
|
||||||
|
if not pf.is_valid():
|
||||||
|
return self.form_invalid(pf)
|
||||||
|
|
||||||
if formset.is_valid():
|
if formset.is_valid():
|
||||||
formset.save()
|
formset.save()
|
||||||
|
for problem_formset in problem_formsets:
|
||||||
|
problem_formset.save()
|
||||||
|
for obj in problem_formset.deleted_objects:
|
||||||
|
if obj.pk is not None:
|
||||||
|
obj.delete()
|
||||||
return self.form_valid(formset)
|
return self.form_valid(formset)
|
||||||
else:
|
else:
|
||||||
return self.form_invalid(formset)
|
return self.form_invalid(formset)
|
||||||
|
@ -239,7 +318,10 @@ class CourseStudentResults(CourseEditableMixin, DetailView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(CourseStudentResults, self).get_context_data(**kwargs)
|
context = super(CourseStudentResults, self).get_context_data(**kwargs)
|
||||||
context["title"] = mark_safe(
|
context["title"] = _("Grades in %(course_name)s</a>") % {
|
||||||
|
"course_name": self.course.name,
|
||||||
|
}
|
||||||
|
context["content_title"] = mark_safe(
|
||||||
_("Grades in <a href='%(url)s'>%(course_name)s</a>")
|
_("Grades in <a href='%(url)s'>%(course_name)s</a>")
|
||||||
% {
|
% {
|
||||||
"course_name": self.course.name,
|
"course_name": self.course.name,
|
||||||
|
@ -249,3 +331,61 @@ class CourseStudentResults(CourseEditableMixin, DetailView):
|
||||||
context["page_type"] = "grades"
|
context["page_type"] = "grades"
|
||||||
context["grades"] = self.get_grades()
|
context["grades"] = self.get_grades()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CourseStudentResultsLesson(CourseEditableMixin, DetailView):
|
||||||
|
model = CourseLesson
|
||||||
|
template_name = "course/grades_lesson.html"
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
try:
|
||||||
|
self.lesson = CourseLesson.objects.get(
|
||||||
|
course=self.course, id=self.kwargs["id"]
|
||||||
|
)
|
||||||
|
return self.lesson
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
def get_lesson_grades(self):
|
||||||
|
students = self.course.get_students()
|
||||||
|
students.sort(key=lambda u: u.username.lower())
|
||||||
|
problems = self.lesson.lesson_problems.values_list("problem", flat=True)
|
||||||
|
lesson_problems = self.lesson.lesson_problems.all()
|
||||||
|
grades = {}
|
||||||
|
for s in students:
|
||||||
|
grades[s] = problem_points = max_case_points_per_problem(s, problems)
|
||||||
|
achieved_points = total_points = 0
|
||||||
|
for lesson_problem in lesson_problems:
|
||||||
|
val = problem_points.get(lesson_problem.problem.id)
|
||||||
|
if val and val["case_total"]:
|
||||||
|
achieved_points += (
|
||||||
|
val["case_points"] / val["case_total"] * lesson_problem.score
|
||||||
|
)
|
||||||
|
total_points += lesson_problem.score
|
||||||
|
grades[s]["total"] = {
|
||||||
|
"achieved_points": achieved_points,
|
||||||
|
"total_points": total_points,
|
||||||
|
"percentage": achieved_points / total_points * 100
|
||||||
|
if total_points
|
||||||
|
else 0,
|
||||||
|
}
|
||||||
|
return grades
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CourseStudentResultsLesson, self).get_context_data(**kwargs)
|
||||||
|
context["lesson"] = self.lesson
|
||||||
|
context["title"] = _("Grades of %(lesson_name)s</a> in %(course_name)s</a>") % {
|
||||||
|
"course_name": self.course.name,
|
||||||
|
"lesson_name": self.lesson.title,
|
||||||
|
}
|
||||||
|
context["content_title"] = mark_safe(
|
||||||
|
_("Grades of %(lesson_name)s</a> in <a href='%(url)s'>%(course_name)s</a>")
|
||||||
|
% {
|
||||||
|
"course_name": self.course.name,
|
||||||
|
"lesson_name": self.lesson.title,
|
||||||
|
"url": self.course.get_absolute_url(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context["page_type"] = "grades"
|
||||||
|
context["grades"] = self.get_lesson_grades()
|
||||||
|
return context
|
||||||
|
|
|
@ -15,7 +15,11 @@
|
||||||
<div class="lesson-title">
|
<div class="lesson-title">
|
||||||
{{ lesson.title }}
|
{{ lesson.title }}
|
||||||
<div class="lesson-points">
|
<div class="lesson-points">
|
||||||
{{progress['achieved_points'] | floatformat(1)}} / {{lesson.points}}
|
{% if progress['total_points'] %}
|
||||||
|
{{(progress['achieved_points'] / progress['total_points'] * lesson.points) | floatformat(1)}} / {{lesson.points}}
|
||||||
|
{% else %}
|
||||||
|
0 / {{lesson.points}}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-container">
|
<div class="progress-container">
|
||||||
|
@ -30,7 +34,6 @@
|
||||||
{% set achieved_points = total_progress['achieved_points'] %}
|
{% set achieved_points = total_progress['achieved_points'] %}
|
||||||
{% set total_points = total_progress['total_points'] %}
|
{% set total_points = total_progress['total_points'] %}
|
||||||
{% set percentage = total_progress['percentage'] %}
|
{% set percentage = total_progress['percentage'] %}
|
||||||
|
|
||||||
{{_("Total achieved points")}}:
|
{{_("Total achieved points")}}:
|
||||||
<span style="float: right; font-weight: normal;">
|
<span style="float: right; font-weight: normal;">
|
||||||
{{ achieved_points | floatformat(2) }} / {{ total_points }} ({{percentage|floatformat(1)}}%)
|
{{ achieved_points | floatformat(2) }} / {{ total_points }} ({{percentage|floatformat(1)}}%)
|
||||||
|
|
|
@ -1,67 +1,109 @@
|
||||||
{% extends "course/base.html" %}
|
{% extends "course/base.html" %}
|
||||||
|
|
||||||
{% block two_col_media %}
|
{% block two_col_media %}
|
||||||
{{ form.media.css }}
|
{{ form.media.css }}
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
.field-order, .field-title, .field-points {
|
.field-order, .field-title, .field-points {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
.form-header {
|
.form-header {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_media %}
|
{% block js_media %}
|
||||||
{{ form.media.js }}
|
{{ form.media.js }}
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
$(function() {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
if ('DjangoPagedown' in window) {
|
if ('DjangoPagedown' in window) {
|
||||||
DjangoPagedown.init();
|
DjangoPagedown.init();
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block middle_content %}
|
{% block middle_content %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
{% for form in formset %}
|
|
||||||
<h3 class="toggle {{'open' if form.errors else 'closed'}} form-header"><i class="fa fa-chevron-right fa-fw"></i>
|
{% for lesson_form in formset %}
|
||||||
{% if form.title.value() %}
|
{% set ns = namespace(problem_formset_has_error=false) %}
|
||||||
{{form.order.value()}}. {{form.title.value()}}
|
|
||||||
{% else %}
|
{% if lesson_form.instance.id %}
|
||||||
+ {{_("Add new")}}
|
{% set problem_formset = problem_formsets[lesson_form.instance.id] %}
|
||||||
{% endif %}
|
{% for form in problem_formset %}
|
||||||
</h3>
|
{% if form.errors %}
|
||||||
<div class="toggled" style="{{'display: none;' if not form.errors}} margin-bottom: 1em">
|
{% set ns.problem_formset_has_error = true %}
|
||||||
{{form.id}}
|
{% break %}
|
||||||
{% if form.errors %}
|
{% endif %}
|
||||||
<div class="alert alert-danger alert-dismissable">
|
{% endfor %}
|
||||||
<a href="#" class="close">x</a>
|
{% endif %}
|
||||||
{{_("Please fix below errors")}}
|
<h3 class="toggle {{'open' if lesson_form.errors or ns.problem_formset_has_error else 'closed'}} form-header">
|
||||||
</div>
|
<i class="fa fa-chevron-right fa-fw"></i>
|
||||||
{% endif %}
|
{% if lesson_form.title.value() %}
|
||||||
{% for field in form %}
|
{{lesson_form.order.value()}}. {{lesson_form.title.value()}}
|
||||||
{% if not field.is_hidden %}
|
{% else %}
|
||||||
<div style="margin-bottom: 1em;">
|
+ {{_("Add new")}}
|
||||||
{{ field.errors }}
|
{% endif %}
|
||||||
<label for="{{field.id_for_label }}"><b>{{ field.label }}{% if field.field.required %}<span class="red"> * </span>{% endif %}:</b> </label>
|
</h3>
|
||||||
<div class="org-field-wrapper field-{{field.name}}" id="org-field-wrapper-{{field.html_name}}">
|
<div class="toggled" style="{{'display: none;' if not lesson_form.errors and not ns.problem_formset_has_error}} margin-bottom: 1em">
|
||||||
{{ field }}
|
{{lesson_form.id}}
|
||||||
</div>
|
{% if lesson_form.errors %}
|
||||||
{% if field.help_text %}
|
<div class="alert alert-danger alert-dismissable">
|
||||||
<small class="org-help-text"><i class="fa fa-exclamation-circle"></i> {{ field.help_text|safe }}</small>
|
<a href="#" class="close">x</a>
|
||||||
{% endif %}
|
{{_("Please fix below errors")}}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% for field in lesson_form %}
|
||||||
<hr/>
|
{% if not field.is_hidden %}
|
||||||
</div>
|
<div style="margin-bottom: 1em;">
|
||||||
{% endfor %}
|
{{ field.errors }}
|
||||||
<input type="submit" value="{{_('Save')}}" style="float: right">
|
<label for="{{field.id_for_label }}"><b>{{ field.label }}{% if field.field.required %}<span class="red"> * </span>{% endif %}:</b> </label>
|
||||||
</form>
|
<div class="org-field-wrapper field-{{field.name}}" id="org-field-wrapper-{{field.html_name}}">
|
||||||
|
{{ field }}
|
||||||
|
</div>
|
||||||
|
{% if field.help_text %}
|
||||||
|
<small class="org-help-text"><i class="fa fa-exclamation-circle"></i> {{ field.help_text|safe }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Problems Table -->
|
||||||
|
{% if problem_formset %}
|
||||||
|
{{ problem_formset.management_form }}
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% for field in problem_formset.forms.0 %}
|
||||||
|
{% if not field.is_hidden %}
|
||||||
|
<th class="problems-{{field.name}}">
|
||||||
|
{{field.label}}
|
||||||
|
</th>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for form in problem_formset %}
|
||||||
|
<tr>
|
||||||
|
{% for field in form %}
|
||||||
|
<td class="problems-{{field.name}}" title="{{ field.help_text|safe if field.help_text }}" style="{{ 'display:none' if field.is_hidden }}">
|
||||||
|
{{field}}<div class="red">{{field.errors}}</div>
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<input type="submit" value="{{_('Save')}}" style="float: right">
|
||||||
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -74,8 +74,8 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block middle_content %}
|
{% block middle_content %}
|
||||||
<center><h2>{{title}}</h2></center>
|
<center><h2>{{content_title}}</h2></center>
|
||||||
{% set lessons = course.lessons.all() %}
|
{% set lessons = course.lessons.order_by('order') %}
|
||||||
{{_("Sort by")}}:
|
{{_("Sort by")}}:
|
||||||
<select id="sortSelect">
|
<select id="sortSelect">
|
||||||
<option value="username">{{_("Username")}}</option>
|
<option value="username">{{_("Username")}}</option>
|
||||||
|
@ -89,7 +89,7 @@
|
||||||
{% if grades|length > 0 %}
|
{% if grades|length > 0 %}
|
||||||
{% for lesson in lessons %}
|
{% for lesson in lessons %}
|
||||||
<th class="points">
|
<th class="points">
|
||||||
<a href="{{url('course_lesson_detail', course.slug, lesson.id)}}">
|
<a href="{{url('course_grades_lesson', course.slug, lesson.id)}}">
|
||||||
{{ lesson.title }}
|
{{ lesson.title }}
|
||||||
<div class="point-denominator">{{lesson.points}}</div>
|
<div class="point-denominator">{{lesson.points}}</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -111,9 +111,14 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{% for lesson in lessons %}
|
{% for lesson in lessons %}
|
||||||
|
{% set val = grade.get(lesson.id) %}
|
||||||
<td class="partial-score">
|
<td class="partial-score">
|
||||||
<a href="{{url('course_lesson_detail', course.slug, lesson.id)}}?user={{student.username}}">
|
<a href="{{url('course_lesson_detail', course.slug, lesson.id)}}?user={{student.username}}">
|
||||||
{{ grade[lesson.id]['percentage'] | floatformat(0) }}%
|
{% if val and val['total_points'] %}
|
||||||
|
{{ (val['achieved_points'] / val['total_points'] * lesson.points) | floatformat(0) }}
|
||||||
|
{% else %}
|
||||||
|
0
|
||||||
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
132
templates/course/grades_lesson.html
Normal file
132
templates/course/grades_lesson.html
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
{% extends "course/base.html" %}
|
||||||
|
|
||||||
|
{% block two_col_media %}
|
||||||
|
<style type="text/css">
|
||||||
|
table {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
height: 2.5em;
|
||||||
|
}
|
||||||
|
.user-name {
|
||||||
|
padding-left: 1em !important;
|
||||||
|
}
|
||||||
|
#search-input {
|
||||||
|
float: right;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_media %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
var $searchInput = $("#search-input");
|
||||||
|
var $usersTable = $("#users-table");
|
||||||
|
var tableBorderColor = $('#users-table td').css('border-bottom-color');
|
||||||
|
|
||||||
|
$searchInput.on("keyup", function() {
|
||||||
|
var value = $(this).val().toLowerCase();
|
||||||
|
$("#users-table tbody tr").filter(function() {
|
||||||
|
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1)
|
||||||
|
});
|
||||||
|
|
||||||
|
if(value) {
|
||||||
|
$('#users-table').css('border-bottom', '1px solid ' + tableBorderColor);
|
||||||
|
} else {
|
||||||
|
$('#users-table').css('border-bottom', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#sortSelect').select2({
|
||||||
|
minimumResultsForSearch: -1,
|
||||||
|
width: "10em",
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#sortSelect').on('change', function() {
|
||||||
|
var rows = $('#users-table tbody tr').get();
|
||||||
|
var sortBy = $(this).val();
|
||||||
|
rows.sort(function(a, b) {
|
||||||
|
var keyA = $(a).find(sortBy === 'username' ? '.user-name' : 'td:last-child').text().trim();
|
||||||
|
var keyB = $(b).find(sortBy === 'username' ? '.user-name' : 'td:last-child').text().trim();
|
||||||
|
|
||||||
|
if(sortBy === 'total') {
|
||||||
|
// Convert percentage string to number for comparison
|
||||||
|
keyA = -parseFloat(keyA.replace('%', ''));
|
||||||
|
keyB = -parseFloat(keyB.replace('%', ''));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
keyA = keyA.toLowerCase();
|
||||||
|
keyB = keyB.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(keyA < keyB) return -1;
|
||||||
|
if(keyA > keyB) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
$.each(rows, function(index, row) {
|
||||||
|
$('#users-table tbody').append(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block middle_content %}
|
||||||
|
<center><h2>{{content_title}}</h2></center>
|
||||||
|
{% set lesson_problems = lesson.lesson_problems.order_by('order') %}
|
||||||
|
{{_("Sort by")}}:
|
||||||
|
<select id="sortSelect">
|
||||||
|
<option value="username">{{_("Username")}}</option>
|
||||||
|
<option value="total">{{_("Score")}}</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="search-input" placeholder="{{_('Search')}}" autofocus>
|
||||||
|
<table class="table striped" id="users-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{_('Student')}}</th>
|
||||||
|
{% if grades|length > 0 %}
|
||||||
|
{% for lesson_problem in lesson_problems %}
|
||||||
|
<th class="points">
|
||||||
|
<a href="{{url('problem_detail', lesson_problem.problem.code)}}">
|
||||||
|
{{ lesson_problem.problem.name }}
|
||||||
|
<div class="point-denominator">{{lesson_problem.score}}</div>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
<th>{{_('Total')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for student, grade in grades.items() %}
|
||||||
|
<tr>
|
||||||
|
<td class="user-name">
|
||||||
|
<div>
|
||||||
|
{{link_user(student)}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{student.first_name}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% for lesson_problem in lesson_problems %}
|
||||||
|
{% set val = grade.get(lesson_problem.problem.id) %}
|
||||||
|
<td class="partial-score">
|
||||||
|
<a href="{{url('user_submissions', lesson_problem.problem.code, student.username)}}">
|
||||||
|
{% if val and val['case_total'] %}
|
||||||
|
{{ (val['case_points'] / val['case_total'] * lesson_problem.score) | floatformat(0) }}
|
||||||
|
{% else %}
|
||||||
|
0
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
<td style="font-weight: bold">
|
||||||
|
{{ grade['total']['percentage'] | floatformat(0) }}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
|
@ -6,14 +6,15 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block middle_content %}
|
{% block middle_content %}
|
||||||
<center><h2>{{title}}</h2></center>
|
<center><h2>{{title}} - {{profile.username}}</h2></center>
|
||||||
<h3 class="course-content-title">{{_("Content")}}</h3>
|
<h3 class="course-content-title">{{_("Content")}}</h3>
|
||||||
<div>
|
<div>
|
||||||
{{ lesson.content|markdown|reference|str|safe }}
|
{{ lesson.content|markdown|reference|str|safe }}
|
||||||
</div>
|
</div>
|
||||||
<h3 class="course-content-title">{{_("Problems")}}</h3>
|
<h3 class="course-content-title">{{_("Problems")}}</h3>
|
||||||
<ul class="course-problem-list">
|
<ul class="course-problem-list">
|
||||||
{% for problem in lesson.problems.all() %}
|
{% for lesson_problem in lesson.lesson_problems.order_by('order') %}
|
||||||
|
{% set problem = lesson_problem.problem %}
|
||||||
<a href="{{url('problem_detail', problem.code)}}">
|
<a href="{{url('problem_detail', problem.code)}}">
|
||||||
<li>
|
<li>
|
||||||
{% if problem.id in completed_problem_ids %}
|
{% if problem.id in completed_problem_ids %}
|
||||||
|
@ -26,10 +27,10 @@
|
||||||
<span class="problem-name">{{problem.name}}</span>
|
<span class="problem-name">{{problem.name}}</span>
|
||||||
{% set pp = problem_points[problem.id] %}
|
{% set pp = problem_points[problem.id] %}
|
||||||
<span class="score">
|
<span class="score">
|
||||||
{% if pp %}
|
{% if pp and pp.case_total %}
|
||||||
{{pp.case_points|floatformat(1)}} / {{pp.case_total|floatformat(0)}}
|
{{(pp.case_points / pp.case_total * lesson_problem.score) |floatformat(1)}} / {{lesson_problem.score|floatformat(0)}}
|
||||||
{% else %}
|
{% else %}
|
||||||
0
|
0 / {{lesson_problem.score|floatformat(0)}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in a new issue