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"