2023-01-31 15:50:52 +00:00
|
|
|
from django.core.validators import RegexValidator
|
|
|
|
from django.db import models
|
|
|
|
from django.utils.translation import gettext, gettext_lazy as _
|
2024-02-19 23:00:44 +00:00
|
|
|
from django.urls import reverse
|
|
|
|
from django.db.models import Q
|
2023-01-31 15:50:52 +00:00
|
|
|
|
2024-10-02 20:06:33 +00:00
|
|
|
from judge.models import BlogPost, Problem, Contest
|
2023-01-31 15:50:52 +00:00
|
|
|
from judge.models.profile import Organization, Profile
|
|
|
|
|
|
|
|
|
2024-02-19 23:00:44 +00:00
|
|
|
class RoleInCourse(models.TextChoices):
|
|
|
|
STUDENT = "ST", _("Student")
|
|
|
|
ASSISTANT = "AS", _("Assistant")
|
|
|
|
TEACHER = "TE", _("Teacher")
|
|
|
|
|
|
|
|
|
|
|
|
EDITABLE_ROLES = (RoleInCourse.TEACHER, RoleInCourse.ASSISTANT)
|
2023-01-31 15:50:52 +00:00
|
|
|
|
2023-01-31 15:54:03 +00:00
|
|
|
|
2023-01-31 15:50:52 +00:00
|
|
|
class Course(models.Model):
|
2023-01-31 15:54:03 +00:00
|
|
|
name = models.CharField(
|
|
|
|
max_length=128,
|
|
|
|
verbose_name=_("course name"),
|
|
|
|
)
|
2024-02-19 23:00:44 +00:00
|
|
|
about = models.TextField(verbose_name=_("course description"))
|
2023-01-31 15:54:03 +00:00
|
|
|
is_public = models.BooleanField(
|
|
|
|
verbose_name=_("publicly visible"),
|
|
|
|
default=False,
|
|
|
|
)
|
2023-01-31 15:50:52 +00:00
|
|
|
organizations = models.ManyToManyField(
|
|
|
|
Organization,
|
|
|
|
blank=True,
|
|
|
|
verbose_name=_("organizations"),
|
|
|
|
help_text=_("If private, only these organizations may see the course"),
|
|
|
|
)
|
|
|
|
slug = models.SlugField(
|
|
|
|
max_length=128,
|
|
|
|
verbose_name=_("course slug"),
|
|
|
|
help_text=_("Course name shown in URL"),
|
|
|
|
unique=True,
|
|
|
|
validators=[
|
|
|
|
RegexValidator("^[-a-zA-Z0-9]+$", _("Only alphanumeric and hyphens"))
|
|
|
|
],
|
|
|
|
)
|
2023-01-31 15:54:03 +00:00
|
|
|
is_open = models.BooleanField(
|
|
|
|
verbose_name=_("public registration"),
|
|
|
|
default=False,
|
|
|
|
)
|
2023-01-31 15:50:52 +00:00
|
|
|
image_url = models.CharField(
|
2023-01-31 15:54:03 +00:00
|
|
|
verbose_name=_("course image"),
|
|
|
|
default="",
|
|
|
|
max_length=150,
|
2023-01-31 15:50:52 +00:00
|
|
|
blank=True,
|
|
|
|
)
|
2023-01-31 15:54:03 +00:00
|
|
|
|
2023-01-31 15:50:52 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
2024-02-19 23:00:44 +00:00
|
|
|
def get_absolute_url(self):
|
|
|
|
return reverse("course_detail", args=(self.slug,))
|
|
|
|
|
2023-01-31 15:50:52 +00:00
|
|
|
@classmethod
|
2024-02-19 23:00:44 +00:00
|
|
|
def is_editable_by(cls, course, profile):
|
|
|
|
try:
|
|
|
|
course_role = CourseRole.objects.get(course=course, user=profile)
|
|
|
|
return course_role.role in EDITABLE_ROLES
|
|
|
|
except CourseRole.DoesNotExist:
|
|
|
|
return False
|
2023-01-31 15:50:52 +00:00
|
|
|
|
|
|
|
@classmethod
|
2023-07-06 15:39:16 +00:00
|
|
|
def is_accessible_by(cls, course, profile):
|
2024-02-19 23:00:44 +00:00
|
|
|
if not profile:
|
|
|
|
return False
|
|
|
|
try:
|
|
|
|
course_role = CourseRole.objects.get(course=course, user=profile)
|
|
|
|
if course_role.course.is_public:
|
|
|
|
return True
|
|
|
|
return course_role.role in EDITABLE_ROLES
|
|
|
|
except CourseRole.DoesNotExist:
|
2023-01-31 15:50:52 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
@classmethod
|
2024-02-19 23:00:44 +00:00
|
|
|
def get_accessible_courses(cls, profile):
|
|
|
|
return Course.objects.filter(
|
|
|
|
Q(is_public=True) | Q(courserole__role__in=EDITABLE_ROLES),
|
|
|
|
courserole__user=profile,
|
|
|
|
).distinct()
|
2023-01-31 15:54:03 +00:00
|
|
|
|
2024-02-19 23:00:44 +00:00
|
|
|
def _get_users_by_role(self, role):
|
|
|
|
course_roles = CourseRole.objects.filter(course=self, role=role).select_related(
|
|
|
|
"user"
|
|
|
|
)
|
|
|
|
return [course_role.user for course_role in course_roles]
|
2023-01-31 15:54:03 +00:00
|
|
|
|
2024-02-19 23:00:44 +00:00
|
|
|
def get_students(self):
|
|
|
|
return self._get_users_by_role(RoleInCourse.STUDENT)
|
|
|
|
|
|
|
|
def get_assistants(self):
|
|
|
|
return self._get_users_by_role(RoleInCourse.ASSISTANT)
|
|
|
|
|
|
|
|
def get_teachers(self):
|
|
|
|
return self._get_users_by_role(RoleInCourse.TEACHER)
|
2023-01-31 15:50:52 +00:00
|
|
|
|
|
|
|
@classmethod
|
2023-07-06 15:39:16 +00:00
|
|
|
def add_student(cls, course, profiles):
|
2023-01-31 15:50:52 +00:00
|
|
|
for profile in profiles:
|
2023-01-31 15:54:03 +00:00
|
|
|
CourseRole.make_role(course=course, user=profile, role="ST")
|
|
|
|
|
|
|
|
@classmethod
|
2023-07-06 15:39:16 +00:00
|
|
|
def add_teachers(cls, course, profiles):
|
2023-01-31 15:50:52 +00:00
|
|
|
for profile in profiles:
|
2023-01-31 15:54:03 +00:00
|
|
|
CourseRole.make_role(course=course, user=profile, role="TE")
|
2023-01-31 15:50:52 +00:00
|
|
|
|
2023-01-31 15:54:03 +00:00
|
|
|
@classmethod
|
2023-07-06 15:39:16 +00:00
|
|
|
def add_assistants(cls, course, profiles):
|
2023-01-31 15:50:52 +00:00
|
|
|
for profile in profiles:
|
2023-01-31 15:54:03 +00:00
|
|
|
CourseRole.make_role(course=course, user=profile, role="AS")
|
|
|
|
|
|
|
|
|
2023-01-31 15:50:52 +00:00
|
|
|
class CourseRole(models.Model):
|
2024-02-19 23:00:44 +00:00
|
|
|
course = models.ForeignKey(
|
2023-01-31 15:50:52 +00:00
|
|
|
Course,
|
|
|
|
verbose_name=_("course"),
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
db_index=True,
|
|
|
|
)
|
|
|
|
user = models.ForeignKey(
|
|
|
|
Profile,
|
|
|
|
verbose_name=_("user"),
|
|
|
|
on_delete=models.CASCADE,
|
2024-02-19 23:00:44 +00:00
|
|
|
related_name="course_roles",
|
2023-01-31 15:50:52 +00:00
|
|
|
)
|
2023-01-31 15:54:03 +00:00
|
|
|
|
2023-01-31 15:50:52 +00:00
|
|
|
role = models.CharField(
|
|
|
|
max_length=2,
|
|
|
|
choices=RoleInCourse.choices,
|
|
|
|
default=RoleInCourse.STUDENT,
|
|
|
|
)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def make_role(self, course, user, role):
|
|
|
|
userqueryset = CourseRole.objects.filter(course=course, user=user)
|
|
|
|
if userqueryset.exists():
|
|
|
|
userqueryset[0].role = role
|
|
|
|
else:
|
|
|
|
couresrole = CourseRole()
|
|
|
|
couresrole.course = course
|
|
|
|
couresrole.user = user
|
|
|
|
couresrole.role = role
|
|
|
|
couresrole.save()
|
|
|
|
|
2024-02-19 23:00:44 +00:00
|
|
|
class Meta:
|
|
|
|
unique_together = ("course", "user")
|
2023-01-31 15:54:03 +00:00
|
|
|
|
2023-01-31 15:50:52 +00:00
|
|
|
|
2024-02-19 23:00:44 +00:00
|
|
|
class CourseLesson(models.Model):
|
|
|
|
course = models.ForeignKey(
|
2023-01-31 15:50:52 +00:00
|
|
|
Course,
|
|
|
|
verbose_name=_("course"),
|
2024-02-19 23:00:44 +00:00
|
|
|
related_name="lessons",
|
2023-01-31 15:50:52 +00:00
|
|
|
on_delete=models.CASCADE,
|
2023-01-31 15:54:03 +00:00
|
|
|
)
|
2024-10-02 20:06:33 +00:00
|
|
|
title = models.TextField(verbose_name=_("lesson title"))
|
|
|
|
content = models.TextField(verbose_name=_("lesson content"))
|
2024-02-19 23:00:44 +00:00
|
|
|
order = models.IntegerField(verbose_name=_("order"), default=0)
|
|
|
|
points = models.IntegerField(verbose_name=_("points"))
|
2024-10-02 20:06:33 +00:00
|
|
|
is_visible = models.BooleanField(verbose_name=_("publicly visible"), default=True)
|
2024-09-03 14:26:20 +00:00
|
|
|
|
2024-09-03 14:51:38 +00:00
|
|
|
def get_absolute_url(self):
|
|
|
|
return reverse(
|
|
|
|
"course_lesson_detail",
|
|
|
|
args=(
|
|
|
|
self.course.slug,
|
|
|
|
self.id,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2024-09-03 14:26:20 +00:00
|
|
|
|
|
|
|
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)
|
2024-10-02 20:06:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CourseContest(models.Model):
|
|
|
|
course = models.ForeignKey(
|
|
|
|
Course, on_delete=models.CASCADE, related_name="contests"
|
|
|
|
)
|
|
|
|
contest = models.ForeignKey(
|
|
|
|
Contest, unique=True, on_delete=models.CASCADE, related_name="course"
|
|
|
|
)
|
|
|
|
order = models.IntegerField(verbose_name=_("order"), default=0)
|
|
|
|
points = models.IntegerField(verbose_name=_("points"))
|
|
|
|
|
|
|
|
def get_course_of_contest(contest):
|
|
|
|
course_contest = contest.course.get()
|
|
|
|
course = course_contest.course
|
|
|
|
return course
|