skillbox/server/books/models/module.py

189 lines
6.2 KiB
Python

from django.db import models
from django import forms
from django.utils import timezone
from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList
from wagtail.fields import RichTextField
from wagtail.admin.forms import WagtailAdminPageForm
from django.utils.translation import gettext as _
from core.constants import DEFAULT_RICH_TEXT_FEATURES
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
from users.models import SchoolClass
EXACT = "exact"
FILTER_ATTRIBUTE_TYPE = (("all", "All"), (EXACT, "Exact"))
class ModuleLevel(models.Model):
name = models.CharField(max_length=255, unique=True)
filter_attribute_type = models.CharField(
max_length=16, choices=FILTER_ATTRIBUTE_TYPE, default=EXACT
)
def __str__(self):
return self.name
class Meta:
verbose_name_plural = _("module Levels")
verbose_name = _("module level")
class ModuleCategory(models.Model):
class Meta:
verbose_name = _("module type")
verbose_name_plural = _("module types")
ordering = ("name",)
name = models.CharField(max_length=255)
filter_attribute_type = models.CharField(
max_length=16, choices=FILTER_ATTRIBUTE_TYPE, default=EXACT
)
def __str__(self):
return f"{self.name}"
class ModulePageForm(WagtailAdminPageForm):
def clean(self):
cleaned_data = super().clean()
if "slug" in self.cleaned_data:
page_slug = cleaned_data["slug"]
if not Module._slug_is_available(page_slug, self.instance):
self.add_error(
"slug",
forms.ValidationError(
_("The slug '%(page_slug)s' is already in use")
% {"page_slug": page_slug}
),
)
return cleaned_data
class Module(StrictHierarchyPage):
class Meta:
verbose_name = "Modul"
verbose_name_plural = "Module"
meta_title = models.CharField(max_length=255, help_text="e.g. 'Intro' or 'Modul 1'")
level = models.ForeignKey(
ModuleLevel, on_delete=models.SET_NULL, blank=True, null=True
)
category = models.ForeignKey(
ModuleCategory, on_delete=models.SET_NULL, blank=True, null=True
)
hero_image = models.ForeignKey(
"wagtailimages.Image",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
hero_source = models.CharField(
max_length=255, help_text="e.g. 'Reuters', 'Wikipedia'", blank=True
)
teaser = models.TextField()
intro = RichTextField(features=DEFAULT_RICH_TEXT_FEATURES)
solutions_enabled_for = models.ManyToManyField(SchoolClass)
content_panels = [
FieldPanel("title", classname="full title"),
FieldPanel("meta_title", classname="full title"),
FieldPanel("level"),
FieldPanel("category"),
FieldPanel("hero_image"),
FieldPanel("hero_source"),
FieldPanel("teaser"),
FieldPanel("intro"),
]
base_form_class = ModulePageForm
edit_handler = TabbedInterface(
[ObjectList(content_panels, heading="Content"), get_default_settings()]
)
template = "generic_page.html"
parent_page_types = ["books.Topic"]
subpage_types = ["books.Chapter"]
def is_translated(self) -> bool:
return self.get_translations().count() > 0
# todo: isn't this a duplicate definition?
def get_child_ids(self):
return self.get_children().values_list("id", flat=True)
def sync_from_school_class(self, school_class_template, school_class_to_sync):
# import here so we don't get a circular import error
from books.models import Chapter, ContentBlock
# get chapters of module
chapters = Chapter.get_by_parent(self)
content_block_query = ContentBlock.objects.none()
# get content blocks of chapters
for chapter in chapters:
content_block_query = content_block_query.union(
ContentBlock.get_by_parent(chapter)
)
# clear all `hidden for` and `visible for` for class `school_class_to_sync`
for content_block in school_class_to_sync.hidden_content_blocks.intersection(
content_block_query
):
content_block.hidden_for.remove(school_class_to_sync)
for content_block in school_class_to_sync.visible_content_blocks.intersection(
content_block_query
):
content_block.visible_for.remove(school_class_to_sync)
# get all content blocks with `hidden for` for class `school_class_pattern`
for content_block in school_class_template.hidden_content_blocks.intersection(
content_block_query
):
# add `school_class_to_sync` to these blocks' `hidden for`
content_block.hidden_for.add(school_class_to_sync)
# get all content blocks with `visible for` for class `school_class_pattern`
for content_block in school_class_template.visible_content_blocks.intersection(
content_block_query
):
# add `school_class_to_sync` to these blocks' `visible for`
content_block.visible_for.add(school_class_to_sync)
for chapter in chapters:
chapter.sync_title_visibility(school_class_template, school_class_to_sync)
chapter.sync_description_visibility(
school_class_template, school_class_to_sync
)
objective_groups = self.objective_groups.all()
for objective_group in objective_groups:
objective_group.sync_visibility(school_class_template, school_class_to_sync)
def get_admin_display_title(self):
return f"{self.meta_title} - {self.title}"
@staticmethod
def _slug_is_available(slug, page):
# modeled after `Page._slug_is_available`
modules = Module.objects.filter(slug=slug).not_page(page)
return not modules.exists()
class RecentModule(models.Model):
module = models.ForeignKey(
Module, on_delete=models.CASCADE, related_name="recent_modules"
)
user = models.ForeignKey("users.User", on_delete=models.CASCADE)
visited = models.DateTimeField(default=timezone.now)
class Meta:
get_latest_by = "visited"