from django import forms from django.db import models from django.utils import timezone from django.utils.translation import gettext as _ from wagtail.admin.forms import WagtailAdminPageForm from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList from wagtail.fields import RichTextField 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 ) order = models.PositiveIntegerField( null=False, blank=False, default=99, help_text="Order in the Dropdown List" ) class Meta: verbose_name_plural = _("module Levels") verbose_name = _("module level") ordering = ("order", "name") def __str__(self): return self.name class ModuleCategory(models.Model): name = models.CharField(max_length=255) filter_attribute_type = models.CharField( max_length=16, choices=FILTER_ATTRIBUTE_TYPE, default=EXACT ) order = models.PositiveIntegerField( null=False, blank=False, default=99, help_text="Order in the Dropdown List" ) class Meta: verbose_name = _("module type") verbose_name_plural = _("module types") ordering = ("order", "name") 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) @property def route(self): return f"module/{self.slug}" 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"