Example #1
0
def search(request):
    search_query = request.GET.get('query', None)
    page = request.GET.get('page', 1)

    # Search
    if search_query:
        index.AutocompleteField('title')
        search_results = ArticlePage.objects.search(search_query,
                                                    fields=['title'])
        query = Query.get(search_query)

        # Record hit
        query.add_hit()
    else:
        search_results = Page.objects.none()

    # Pagination
    paginator = Paginator(search_results, 10)
    try:
        search_results = paginator.page(page)
    except PageNotAnInteger:
        search_results = paginator.page(1)
    except EmptyPage:
        search_results = paginator.page(paginator.num_pages)

    return render(request, 'search/search.html', {
        'search_query': search_query,
        'search_results': search_results,
    })
Example #2
0
class Book(index.Indexed, models.Model):
    title = models.CharField(max_length=255)
    authors = models.ManyToManyField(Author, related_name="books")
    publication_date = models.DateField()
    number_of_pages = models.IntegerField()
    tags = TaggableManager()

    search_fields = [
        index.SearchField("title", partial_match=True, boost=2.0),
        index.AutocompleteField("title"),
        index.FilterField("title"),
        index.FilterField("authors"),
        index.RelatedFields("authors", Author.search_fields),
        index.FilterField("publication_date"),
        index.FilterField("number_of_pages"),
        index.RelatedFields(
            "tags",
            [
                index.SearchField("name"),
                index.FilterField("slug"),
            ],
        ),
        index.FilterField("tags"),
    ]

    @classmethod
    def get_indexed_objects(cls):
        indexed_objects = super(Book, cls).get_indexed_objects()

        # Don't index books using Book class that they have a more specific type
        if cls is Book:
            indexed_objects = indexed_objects.exclude(
                id__in=Novel.objects.values_list("book_ptr_id", flat=True))

            indexed_objects = indexed_objects.exclude(
                id__in=ProgrammingGuide.objects.values_list("book_ptr_id",
                                                            flat=True))

        # Exclude Books that have the title "Don't index me!"
        indexed_objects = indexed_objects.exclude(title="Don't index me!")

        return indexed_objects

    def get_indexed_instance(self):
        # Check if this object is a Novel or ProgrammingGuide and return the specific object
        novel = Novel.objects.filter(book_ptr_id=self.id).first()
        programming_guide = ProgrammingGuide.objects.filter(
            book_ptr_id=self.id).first()

        # Return the novel/programming guide object if there is one, otherwise return self
        return novel or programming_guide or self

    def __str__(self):
        return self.title
class Author(index.Indexed, models.Model):
    name = models.CharField(max_length=255)
    date_of_birth = models.DateField(null=True)

    search_fields = [
        index.SearchField('name'),
        index.AutocompleteField('name'),
        index.FilterField('date_of_birth'),
    ]

    def __str__(self):
        return self.name
Example #4
0
class ExternalAttachment(DisplayUrlMixin, CollectionMember, index.Indexed,
                         models.Model):
    """An externally hosted link or file that can be associated with a Page."""

    # replicate the same fields as Document but with URL instead of file; see:
    # https://github.com/wagtail/wagtail/blob/master/wagtail/documents/models.py#L27-L37
    title = models.CharField(max_length=255)
    author = models.CharField(max_length=255,
                              blank=True,
                              help_text="Citation or list of authors")
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    tags = TaggableManager(blank=True)

    # adapted from AbstractDocument but with URL instead; see:
    # https://github.com/wagtail/wagtail/blob/master/wagtail/documents/models.py#L47-L56
    search_fields = CollectionMember.search_fields + [
        index.SearchField("title", partial_match=True, boost=10),
        index.AutocompleteField("title"),
        index.FilterField("title"),
        index.SearchField("url", partial_match=True),
        index.RelatedFields(
            "tags",
            [
                index.SearchField("name", partial_match=True, boost=10),
                index.AutocompleteField("name"),
            ],
        ),
    ]

    # same QS/manager and form fields as Attachment
    objects = DocumentQuerySet.as_manager()
    admin_form_fields = ("title", "author", "url", "collection", "tags")

    def __str__(self):
        """Attachment title, author(s) if present, and URL."""
        parts = [self.title]
        if self.author:
            parts.append(", %s" % self.author)
        parts.append(" (%s)" % self.display_url)
        return "".join(parts)
Example #5
0
    class CategoryProxy(index.Indexed, get_model("catalogue", "Category")):
        search_fields = [
            index.SearchField("name", partial_match=True, boost=2),
            index.AutocompleteField("name"),
            index.SearchField("description"),
            index.SearchField("full_name"),
            index.FilterField("full_slug"),
            index.FilterField("slug"),
            index.FilterField("get_absolute_url"),
        ]

        class Meta:
            proxy = True
Example #6
0
class Definition(index.Indexed, ClusterableModel):
    glossary = models.ForeignKey(Glossary, on_delete=models.CASCADE, related_name="definitions")
    definition = models.TextField(blank=True)

    search_fields = [
        index.FilterField("glossary"),
        index.RelatedFields("glossary", [
            index.FilterField("locale"),
        ]),
        index.RelatedFields("terms", [
            index.SearchField("term"),
            index.AutocompleteField("term"),
        ]),
    ]

    panels = [
        FieldPanel("glossary"),
        InlinePanel("terms", label="Terms"),
        FieldPanel("definition"),
    ]

    def __str__(self):
        terms = self.terms.all().values_list("term", flat=True)[:5]
        return f"{', '.join(terms)}"
Example #7
0
class AbstractDocument(CollectionMember, index.Indexed, models.Model):
    title = models.CharField(max_length=255, verbose_name=_('title'))
    file = EncryptedFileField(upload_to='documents', verbose_name=_('file'))
    created_at = models.DateTimeField(verbose_name=_('created at'),
                                      auto_now_add=True)
    uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL,
                                         verbose_name=_('uploaded by user'),
                                         null=True,
                                         blank=True,
                                         editable=False,
                                         on_delete=models.SET_NULL)

    tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags'))

    file_size = models.PositiveIntegerField(null=True, editable=False)

    objects = DocumentQuerySet.as_manager()

    search_fields = CollectionMember.search_fields + [
        index.SearchField('title', partial_match=True, boost=10),
        index.AutocompleteField('title'),
        index.FilterField('title'),
        index.RelatedFields('tags', [
            index.SearchField('name', partial_match=True, boost=10),
            index.AutocompleteField('name'),
        ]),
        index.FilterField('uploaded_by_user'),
    ]

    def get_file_size(self):
        if self.file_size is None:
            try:
                self.file_size = self.file.size
            except (SystemExit, KeyboardInterrupt):
                raise
            except Exception:
                # File doesn't exist
                return

            self.save(update_fields=['file_size'])

        return self.file_size

    def __str__(self):
        return self.title

    @property
    def filename(self):
        return os.path.basename(self.file.name)

    @property
    def file_extension(self):
        return os.path.splitext(self.filename)[1][1:]

    @property
    def url(self):
        return reverse('wagtaildocs_serve', args=[self.id, self.filename])

    def get_usage(self):
        return get_object_usage(self)

    @property
    def usage_url(self):
        return reverse('wagtaildocs:document_usage', args=(self.id, ))

    def is_editable_by_user(self, user):
        from wagtail.documents.permissions import permission_policy
        return permission_policy.user_has_permission_for_instance(
            user, 'change', self)

    class Meta:
        abstract = True
        verbose_name = _('document')
Example #8
0
class Author(I18nPage):
    """A page that describes an author."""

    template = "cms/preview/author.html"

    GENDER_UNKNOWN = "U"
    GENDER_MALE = "M"
    GENDER_FEMALE = "F"
    GENDER_CHOICES = (
        (GENDER_UNKNOWN, _(TXT["gender.unknown"])),
        (GENDER_MALE, _(TXT["gender.male"])),
        (GENDER_FEMALE, _(TXT["gender.female"])),
    )

    title_image = models.ForeignKey(
        "ImageMedia",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        verbose_name=_(TXT["author.title_image"]),
        help_text=_(TXT["author.title_image.help"]),
    )
    sex = models.CharField(
        max_length=1,
        choices=GENDER_CHOICES,
        default=GENDER_UNKNOWN,
        verbose_name=_(TXT["author.gender"]),
    )  # TODO: rename to gender
    date_of_birth_year = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        validators=[MinValueValidator(0),
                    MaxValueValidator(9999)],
        verbose_name=_(TXT["author.date_of_birth_year"]),
        help_text=_(TXT["author.date_of_birth_year.help"]),
    )
    date_of_birth_month = models.PositiveSmallIntegerField(
        choices=list(dates.MONTHS.items()),
        null=True,
        blank=True,
        validators=[MinValueValidator(1),
                    MaxValueValidator(12)],
        verbose_name=_(TXT["author.date_of_birth_month"]),
        help_text=_(TXT["author.date_of_birth_month.help"]),
    )
    date_of_birth_day = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        validators=[MinValueValidator(1),
                    MaxValueValidator(31)],
        verbose_name=_(TXT["author.date_of_birth_day"]),
        help_text=_(TXT["author.date_of_birth_day.help"]),
    )
    place_of_birth = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_(TXT["author.place_of_birth"]),
        help_text=_(TXT["author.place_of_birth.help"]),
    )
    place_of_birth_de = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_(TXT["author.place_of_birth"]),
        help_text=_(TXT["author.place_of_birth_de.help"]),
    )
    place_of_birth_cs = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_(TXT["author.place_of_birth"]),
        help_text=_(TXT["author.place_of_birth_cs.help"]),
    )
    i18n_place_of_birth = TranslatedField.named("place_of_birth", True)

    date_of_death_year = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        validators=[MinValueValidator(0),
                    MaxValueValidator(9999)],
        verbose_name=_(TXT["author.date_of_death_year"]),
        help_text=_(TXT["author.date_of_death_year.help"]),
    )
    date_of_death_month = models.PositiveSmallIntegerField(
        choices=list(dates.MONTHS.items()),
        null=True,
        blank=True,
        validators=[MinValueValidator(1),
                    MaxValueValidator(12)],
        verbose_name=_(TXT["author.date_of_death_month"]),
        help_text=_(TXT["author.date_of_death_month.help"]),
    )
    date_of_death_day = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        validators=[MinValueValidator(1),
                    MaxValueValidator(31)],
        verbose_name=_(TXT["author.date_of_death_day"]),
        help_text=_(TXT["author.date_of_death_day.help"]),
    )
    place_of_death = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_(TXT["author.place_of_death"]),
        help_text=_(TXT["author.place_of_death.help"]),
    )
    place_of_death_de = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_(TXT["author.place_of_death"]),
        help_text=_(TXT["author.place_of_death_de.help"]),
    )
    place_of_death_cs = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_(TXT["author.place_of_death"]),
        help_text=_(TXT["author.place_of_death_cs.help"]),
    )
    i18n_place_of_death = TranslatedField.named("place_of_death", True)

    language_tags = ParentalManyToManyField(
        "LanguageTag",
        db_table=DB_TABLE_PREFIX + "author_tag_language",
        related_name="authors",
        blank=True,
        verbose_name=_(TXT["author.language.plural"]),
        help_text=_(TXT["author.language.help"]),
    )
    genre_tags = ParentalManyToManyField(
        "GenreTag",
        db_table=DB_TABLE_PREFIX + "author_tag_genre",
        related_name="authors",
        blank=True,
        verbose_name=_(TXT["author.genre.plural"]),
        help_text=_(TXT["author.genre.help"]),
    )
    literary_period_tags = ParentalManyToManyField(
        "PeriodTag",
        db_table=DB_TABLE_PREFIX + "author_tag_literary_period",
        related_name="authors",
        blank=True,
        verbose_name=_(TXT["author.literary_period.plural"]),
        help_text=_(TXT["author.literary_period.help"]),
    )

    @property
    def born(self):
        """Return the year of birth as a datetime object."""
        if (self.date_of_birth_year and self.date_of_birth_month
                and self.date_of_birth_day):
            return datetime.date(
                self.date_of_birth_year,
                self.date_of_birth_month,
                self.date_of_birth_day,
            )

    @property
    def died(self):
        """Return the year of death as a datetime object."""
        if (self.date_of_death_year and self.date_of_death_month
                and self.date_of_death_day):
            return datetime.date(
                self.date_of_death_year,
                self.date_of_death_month,
                self.date_of_death_day,
            )

    @property
    def age(self):
        """Return the age of the author in years."""
        if self.born and self.died:
            diff_year = self.died.year - self.born.year
            diff_remainder = (self.died.month, self.died.day) < (
                self.born.month,
                self.born.day,
            )
            return diff_year - diff_remainder

    parent_page_types = ["AuthorIndex"]
    search_fields = I18nPage.search_fields + [
        index.RelatedFields(
            "names",
            [
                index.SearchField("title"),
                index.AutocompleteField("title"),
                index.SearchField("first_name"),
                index.AutocompleteField("first_name"),
                index.SearchField("last_name"),
                index.AutocompleteField("last_name"),
                index.SearchField("birth_name"),
                index.AutocompleteField("birth_name"),
                index.FilterField("is_pseudonym"),
            ],
        ),
        index.FilterField("sex"),
        index.FilterField("born"),
        index.FilterField("died"),
        index.FilterField("age"),
    ]
    api_fields = I18nPage.api_fields + [
        "sex",
        "genre_tags",
        "genretag_id",
        "language_tags",
        "literary_period_tags",
    ]

    general_panels = [
        ImageChooserPanel("title_image"),
        InlinePanel(
            "names",
            panels=[
                FieldPanel("is_pseudonym"),
                FieldPanelTabs(
                    children=[
                        MultiFieldPanel(
                            children=[
                                FieldPanel("title"),
                                FieldPanel("first_name"),
                                FieldPanel("last_name"),
                                FieldPanel("birth_name"),
                            ],
                            heading=_(TXT["heading.en"]),
                            classname="collapsible",
                        ),
                        MultiFieldPanel(
                            children=[
                                FieldPanel("title_de"),
                                FieldPanel("first_name_de"),
                                FieldPanel("last_name_de"),
                                FieldPanel("birth_name_de"),
                            ],
                            heading=_(TXT["heading.de"]),
                        ),
                        MultiFieldPanel(
                            children=[
                                FieldPanel("title_cs"),
                                FieldPanel("first_name_cs"),
                                FieldPanel("last_name_cs"),
                                FieldPanel("birth_name_cs"),
                            ],
                            heading=_(TXT["heading.cs"]),
                        ),
                    ],
                    heading=_(TXT["author.name"]),
                ),
            ],
            label=_(TXT["author.name.plural"]),
            min_num=1,
            help_text=_(TXT["author.name.help"]),
        ),
        FieldPanel("sex"),
        MultiFieldPanel(
            children=[
                FieldPanel("date_of_birth_day"),
                FieldPanel("date_of_birth_month"),
                FieldPanel("date_of_birth_year"),
                FieldPanelTabs(
                    children=[
                        FieldPanelTab("place_of_birth",
                                      heading=_(TXT["language.en"])),
                        FieldPanelTab("place_of_birth_de",
                                      heading=_(TXT["language.de"])),
                        FieldPanelTab("place_of_birth_cs",
                                      heading=_(TXT["language.cs"])),
                    ],
                    heading=_(TXT["author.place_of_birth"]),
                ),
            ],
            heading=_(TXT["author.date_of_birth"]),
        ),
        MultiFieldPanel(
            children=[
                FieldPanel("date_of_death_day"),
                FieldPanel("date_of_death_month"),
                FieldPanel("date_of_death_year"),
                FieldPanelTabs(
                    children=[
                        FieldPanelTab("place_of_death",
                                      heading=_(TXT["language.en"])),
                        FieldPanelTab("place_of_death_de",
                                      heading=_(TXT["language.de"])),
                        FieldPanelTab("place_of_death_cs",
                                      heading=_(TXT["language.cs"])),
                    ],
                    heading=_(TXT["author.place_of_death"]),
                ),
            ],
            heading=_(TXT["author.date_of_death"]),
        ),
        MultiFieldPanel(
            heading=_(TXT["tag.plural"]),
            children=[
                FieldPanel(
                    field_name="language_tags",
                    widget=autocomplete.ModelSelect2Multiple(
                        url="autocomplete-language"),
                ),
                FieldPanel(
                    field_name="genre_tags",
                    widget=autocomplete.ModelSelect2Multiple(
                        url="autocomplete-genre"),
                ),
                FieldPanel(
                    field_name="literary_period_tags",
                    widget=autocomplete.ModelSelect2Multiple(
                        url="autocomplete-literary-period"),
                ),
            ],
        ),
    ]

    edit_handler = TabbedInterface([
        ObjectList(general_panels, heading=_(TXT["heading.general"])),
        ObjectList(I18nPage.meta_panels, heading=_(TXT["heading.meta"])),
    ])

    @property
    def full_name_title(self):
        """Return the full name of the author including her birth name to be used in titles."""
        return self.names.first().full_name_title(self.sex)

    @property
    def formatted_date_of_birth(self):
        """Format date of birth in human readable string."""
        return format_date(self.date_of_birth_year, self.date_of_birth_month,
                           self.date_of_birth_day)

    @property
    def formatted_date_of_death(self):
        """Format date of death in human readable string."""
        return format_date(self.date_of_death_year, self.date_of_death_month,
                           self.date_of_death_day)

    def full_clean(self, *args, **kwargs):
        """Add autogenerated values for non-editable required fields."""
        name = self.names.first()
        if name:
            self.title = name.full_name()
            self.title_de = name.full_name_de()
            self.title_cs = name.full_name_cs()
            base_slug = text.slugify(name.last_name, allow_unicode=True)
            self.slug = self._get_autogenerated_slug(base_slug)

        super(Author, self).full_clean(*args, **kwargs)

    def clean(self):
        """Validate date components of input."""
        super(Author, self).clean()
        validate_date(self.date_of_birth_year, self.date_of_birth_month,
                      self.date_of_birth_day)
        validate_date(self.date_of_death_year, self.date_of_death_month,
                      self.date_of_death_day)

    def get_context(self, request, *args, **kwargs):
        """Add furthor context information to preview requests."""
        context = super(Author, self).get_context(request, *args, **kwargs)

        # add linked memorials
        if request.is_preview:
            context["memorials"] = Memorial.objects.filter(
                remembered_authors=self)

        return context

    class Meta:
        db_table = DB_TABLE_PREFIX + "author"
        verbose_name = _(TXT["author"])
        verbose_name_plural = _(TXT["author.plural"])

    class JSONAPIMeta:
        resource_name = "authors"
Example #9
0
File: base.py Project: bentrm/lis
class I18nPage(Page):
    """
    An abstract base page class that supports translated content.

    The class should be used for all page types of the CMS.

    Overrides Page.save and Page.save_revision methods to make sure
    multilingual content is handled the same as the default fields.

    """

    objects = TranslatedTitlePageManager()

    ORIGINAL_LANGUAGE_ENGLISH = "en"
    ORIGINAL_LANGUAGE_GERMAN = "de"
    ORIGINAL_LANGUAGE_CZECH = "cs"
    ORIGINAL_LANGUAGE = (
        (ORIGINAL_LANGUAGE_ENGLISH, _(TXT["language.en"])),
        (ORIGINAL_LANGUAGE_GERMAN, _(TXT["language.de"])),
        (ORIGINAL_LANGUAGE_CZECH, _(TXT["language.cs"])),
    )
    RICH_TEXT_FEATURES = ["bold", "italic", "strikethrough", "link"]

    template = "cms/preview/blog.html"

    title_de = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_(TXT["page.title_de"]),
        help_text=_(TXT["page.title_de.help"]),
    )
    title_cs = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_(TXT["page.title_cs"]),
        help_text=_(TXT["page.title_cs.help"]),
    )
    i18n_title = TranslatedField.named("title", True)

    draft_title_de = models.CharField(
        max_length=255,
        blank=True,
        editable=False,
        verbose_name=_(TXT["page.draft_title_de"]),
        help_text=_(TXT["page.draft_title_de.help"]),
    )
    draft_title_cs = models.CharField(
        max_length=255,
        blank=True,
        editable=False,
        verbose_name=_(TXT["page.draft_title_cs"]),
        help_text=_(TXT["page.draft_title_cs.help"]),
    )
    i18n_draft_title = TranslatedField.named("draft_title", True)

    editor = models.CharField(
        max_length=2048,
        verbose_name=_(TXT["page.editor"]),
        help_text=_(TXT["page.editor.help"]),
    )
    original_language = models.CharField(
        max_length=3,
        choices=ORIGINAL_LANGUAGE,
        default=ORIGINAL_LANGUAGE_GERMAN,
        verbose_name=_(TXT["page.original_language"]),
        help_text=_(TXT["page.original_language.help"]),
    )

    temporary_redirect = models.CharField(
        max_length=250,
        blank=True,
        default="",
        verbose_name=_(TXT["page.temporary_redirect"]),
        help_text=_(TXT["page.temporary_redirect.help"]),
    )

    is_creatable = False

    search_fields = Page.search_fields + [
        index.SearchField("title_de", partial_match=True, boost=2),
        index.AutocompleteField("title_de"),
        index.SearchField("title_cs", partial_match=True, boost=2),
        index.AutocompleteField("title_cs"),
        index.FilterField("title_de"),
        index.FilterField("title_cs"),
    ]

    api_fields = [
        APIField("title_de"),
        APIField("title_cs"),
        APIField("editor"),
    ]

    english_panels = [FieldPanel("title", classname="full title")]
    german_panels = [FieldPanel("title_de", classname="full title")]
    czech_panels = [FieldPanel("title_cs", classname="full title")]
    promote_panels = Page.promote_panels + [FieldPanel("temporary_redirect")]
    meta_panels = [
        FieldPanel("owner"),
        FieldPanel("editor"),
        FieldPanel("original_language"),
    ]

    edit_handler = TabbedInterface([
        ObjectList(english_panels, heading=_(TXT["heading.en"])),
        ObjectList(german_panels, heading=_(TXT["heading.de"])),
        ObjectList(czech_panels, heading=_(TXT["heading.cs"])),
        ObjectList(promote_panels, heading=_(TXT["heading.promote"])),
        ObjectList(meta_panels, heading=_(TXT["heading.meta"])),
    ])

    @cached_property
    def is_restricted(self):
        """Return True if this page is restricted to the public in any way."""
        return (self.get_view_restrictions().exclude(
            restriction_type=BaseViewRestriction.NONE).exists())

    def serve(self, request, *args, **kwargs):
        """Return a redirect of the temporary_redirect property is set."""
        # if self.temporary_redirect:
        #     return redirect(self.temporary_redirect, permanent=False)
        return super(I18nPage, self).serve(request, *args, **kwargs)

    def get_admin_display_title(self):
        """Return title to be displayed in the admins UI."""
        return self.i18n_draft_title or self.i18n_title

    def full_clean(self, *args, **kwargs):
        """Set the translated draft titles according the translated title fields."""
        if not self.draft_title_de:
            self.draft_title_de = self.title_de
        if not self.draft_title_cs:
            self.draft_title_cs = self.title_cs
        super(I18nPage, self).full_clean(*args, **kwargs)

    def save_revision(
        self,
        user=None,
        submitted_for_moderation=False,
        approved_go_live_at=None,
        changed=True,
    ):
        """Add applications and translation specific fields to the revision of the page."""

        # TODO: Add explicit read-only permission to support access to admin backend
        if user.groups.filter(name='READONLY').exists():
            raise PermissionDenied

        self.full_clean()

        # Create revision
        revision = self.revisions.create(
            content_json=self.to_json(),
            user=user,
            submitted_for_moderation=submitted_for_moderation,
            approved_go_live_at=approved_go_live_at,
        )

        update_fields = []

        self.latest_revision_created_at = revision.created_at
        update_fields.append("latest_revision_created_at")

        self.draft_title = self.title
        self.draft_title_de = self.title_de
        self.draft_title_cs = self.title_cs
        update_fields.append("draft_title")
        update_fields.append("draft_title_de")
        update_fields.append("draft_title_cs")

        if changed:
            self.has_unpublished_changes = True
            update_fields.append("has_unpublished_changes")

        if update_fields:
            self.save(update_fields=update_fields)

        # Log
        LOGGER.info(
            f'Page edited: "{self.title}" id={self.pk} revision_id={revision.id}'
        )

        if submitted_for_moderation:
            LOGGER.info(f""""
            Page submitted for moderation: \"{self.title}\" id={self.pk} revision_id={revision.id}
            """)

        return revision

    def __str__(self):
        return str(self.i18n_title)
Example #10
0
class AbstractImage(CollectionMember, index.Indexed, models.Model):
    title = models.CharField(max_length=255, verbose_name=_('title'))
    file = models.ImageField(verbose_name=_('file'),
                             upload_to=get_upload_to,
                             width_field='width',
                             height_field='height')
    width = models.IntegerField(verbose_name=_('width'), editable=False)
    height = models.IntegerField(verbose_name=_('height'), editable=False)
    created_at = models.DateTimeField(verbose_name=_('created at'),
                                      auto_now_add=True,
                                      db_index=True)
    uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL,
                                         verbose_name=_('uploaded by user'),
                                         null=True,
                                         blank=True,
                                         editable=False,
                                         on_delete=models.SET_NULL)

    tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags'))

    focal_point_x = models.PositiveIntegerField(null=True, blank=True)
    focal_point_y = models.PositiveIntegerField(null=True, blank=True)
    focal_point_width = models.PositiveIntegerField(null=True, blank=True)
    focal_point_height = models.PositiveIntegerField(null=True, blank=True)

    file_size = models.PositiveIntegerField(null=True, editable=False)
    # A SHA-1 hash of the file contents
    file_hash = models.CharField(max_length=40, blank=True, editable=False)

    objects = ImageQuerySet.as_manager()

    def is_stored_locally(self):
        """
        Returns True if the image is hosted on the local filesystem
        """
        try:
            self.file.path

            return True
        except NotImplementedError:
            return False

    def get_file_size(self):
        if self.file_size is None:
            try:
                self.file_size = self.file.size
            except Exception as e:
                # File not found
                #
                # Have to catch everything, because the exception
                # depends on the file subclass, and therefore the
                # storage being used.
                raise SourceImageIOError(str(e))

            self.save(update_fields=['file_size'])

        return self.file_size

    def _set_file_hash(self, file_contents):
        self.file_hash = hashlib.sha1(file_contents).hexdigest()

    def get_file_hash(self):
        if self.file_hash == '':
            with self.open_file() as f:
                self._set_file_hash(f.read())

            self.save(update_fields=['file_hash'])

        return self.file_hash

    def get_upload_to(self, filename):
        folder_name = 'original_images'
        filename = self.file.field.storage.get_valid_name(filename)

        # do a unidecode in the filename and then
        # replace non-ascii characters in filename with _ , to sidestep issues with filesystem encoding
        filename = "".join(
            (i if ord(i) < 128 else '_') for i in unidecode(filename))

        # Truncate filename so it fits in the 100 character limit
        # https://code.djangoproject.com/ticket/9893
        full_path = os.path.join(folder_name, filename)
        if len(full_path) >= 95:
            chars_to_trim = len(full_path) - 94
            prefix, extension = os.path.splitext(filename)
            filename = prefix[:-chars_to_trim] + extension
            full_path = os.path.join(folder_name, filename)

        return full_path

    def get_usage(self):
        return get_object_usage(self)

    @property
    def usage_url(self):
        return reverse('wagtailimages:image_usage', args=(self.id, ))

    search_fields = CollectionMember.search_fields + [
        index.SearchField('title', partial_match=True, boost=10),
        index.AutocompleteField('title'),
        index.FilterField('title'),
        index.RelatedFields('tags', [
            index.SearchField('name', partial_match=True, boost=10),
            index.AutocompleteField('name'),
        ]),
        index.FilterField('uploaded_by_user'),
    ]

    def __str__(self):
        return self.title

    @contextmanager
    def open_file(self):
        # Open file if it is closed
        close_file = False
        try:
            image_file = self.file

            if self.file.closed:
                # Reopen the file
                if self.is_stored_locally():
                    self.file.open('rb')
                else:
                    # Some external storage backends don't allow reopening
                    # the file. Get a fresh file instance. #1397
                    storage = self._meta.get_field('file').storage
                    image_file = storage.open(self.file.name, 'rb')

                close_file = True
        except IOError as e:
            # re-throw this as a SourceImageIOError so that calling code can distinguish
            # these from IOErrors elsewhere in the process
            raise SourceImageIOError(str(e))

        # Seek to beginning
        image_file.seek(0)

        try:
            yield image_file
        finally:
            if close_file:
                image_file.close()

    @contextmanager
    def get_willow_image(self):
        with self.open_file() as image_file:
            yield WillowImage.open(image_file)

    def get_rect(self):
        return Rect(0, 0, self.width, self.height)

    def get_focal_point(self):
        if self.focal_point_x is not None and \
           self.focal_point_y is not None and \
           self.focal_point_width is not None and \
           self.focal_point_height is not None:
            return Rect.from_point(
                self.focal_point_x,
                self.focal_point_y,
                self.focal_point_width,
                self.focal_point_height,
            )

    def has_focal_point(self):
        return self.get_focal_point() is not None

    def set_focal_point(self, rect):
        if rect is not None:
            self.focal_point_x = rect.centroid_x
            self.focal_point_y = rect.centroid_y
            self.focal_point_width = rect.width
            self.focal_point_height = rect.height
        else:
            self.focal_point_x = None
            self.focal_point_y = None
            self.focal_point_width = None
            self.focal_point_height = None

    def get_suggested_focal_point(self):
        with self.get_willow_image() as willow:
            faces = willow.detect_faces()

            if faces:
                # Create a bounding box around all faces
                left = min(face[0] for face in faces)
                top = min(face[1] for face in faces)
                right = max(face[2] for face in faces)
                bottom = max(face[3] for face in faces)
                focal_point = Rect(left, top, right, bottom)
            else:
                features = willow.detect_features()
                if features:
                    # Create a bounding box around all features
                    left = min(feature[0] for feature in features)
                    top = min(feature[1] for feature in features)
                    right = max(feature[0] for feature in features)
                    bottom = max(feature[1] for feature in features)
                    focal_point = Rect(left, top, right, bottom)
                else:
                    return None

        # Add 20% to width and height and give it a minimum size
        x, y = focal_point.centroid
        width, height = focal_point.size

        width *= 1.20
        height *= 1.20

        width = max(width, 100)
        height = max(height, 100)

        return Rect.from_point(x, y, width, height)

    @classmethod
    def get_rendition_model(cls):
        """ Get the Rendition model for this Image model """
        return cls.renditions.rel.related_model

    def get_rendition(self, filter):
        if isinstance(filter, str):
            filter = Filter(spec=filter)

        cache_key = filter.get_cache_key(self)
        Rendition = self.get_rendition_model()

        try:
            rendition = self.renditions.get(
                filter_spec=filter.spec,
                focal_point_key=cache_key,
            )
        except Rendition.DoesNotExist:
            # Generate the rendition image
            generated_image = filter.run(self, BytesIO())

            # Generate filename
            input_filename = os.path.basename(self.file.name)
            input_filename_without_extension, input_extension = os.path.splitext(
                input_filename)

            # A mapping of image formats to extensions
            FORMAT_EXTENSIONS = {
                'jpeg': '.jpg',
                'png': '.png',
                'gif': '.gif',
            }

            output_extension = filter.spec.replace(
                '|', '.') + FORMAT_EXTENSIONS[generated_image.format_name]
            if cache_key:
                output_extension = cache_key + '.' + output_extension

            # Truncate filename to prevent it going over 60 chars
            output_filename_without_extension = input_filename_without_extension[:(
                59 - len(output_extension))]
            output_filename = output_filename_without_extension + '.' + output_extension

            rendition, created = self.renditions.get_or_create(
                filter_spec=filter.spec,
                focal_point_key=cache_key,
                defaults={
                    'file': File(generated_image.f, name=output_filename)
                })

        return rendition

    def is_portrait(self):
        return (self.width < self.height)

    def is_landscape(self):
        return (self.height < self.width)

    @property
    def filename(self):
        return os.path.basename(self.file.name)

    @property
    def default_alt_text(self):
        # by default the alt text field (used in rich text insertion) is populated
        # from the title. Subclasses might provide a separate alt field, and
        # override this
        return self.title

    def is_editable_by_user(self, user):
        from wagtail.images.permissions import permission_policy
        return permission_policy.user_has_permission_for_instance(
            user, 'change', self)

    class Meta:
        abstract = True
Example #11
0
class AbstractDocument(CollectionMember, index.Indexed, models.Model):
    title = models.CharField(max_length=255, verbose_name=_('title'))
    file = models.FileField(upload_to='documents', verbose_name=_('file'))
    created_at = models.DateTimeField(verbose_name=_('created at'),
                                      auto_now_add=True)
    uploaded_by_user = models.ForeignKey(settings.AUTH_USER_MODEL,
                                         verbose_name=_('uploaded by user'),
                                         null=True,
                                         blank=True,
                                         editable=False,
                                         on_delete=models.SET_NULL)

    tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags'))

    file_size = models.PositiveIntegerField(null=True, editable=False)
    # A SHA-1 hash of the file contents
    file_hash = models.CharField(max_length=40, blank=True, editable=False)

    objects = DocumentQuerySet.as_manager()

    search_fields = CollectionMember.search_fields + [
        index.SearchField('title', partial_match=True, boost=10),
        index.AutocompleteField('title'),
        index.FilterField('title'),
        index.RelatedFields('tags', [
            index.SearchField('name', partial_match=True, boost=10),
            index.AutocompleteField('name'),
        ]),
        index.FilterField('uploaded_by_user'),
    ]

    def is_stored_locally(self):
        """
        Returns True if the image is hosted on the local filesystem
        """
        try:
            self.file.path

            return True
        except NotImplementedError:
            return False

    @contextmanager
    def open_file(self):
        # Open file if it is closed
        close_file = False
        f = self.file

        if f.closed:
            # Reopen the file
            if self.is_stored_locally():
                f.open('rb')
            else:
                # Some external storage backends don't allow reopening
                # the file. Get a fresh file instance. #1397
                storage = self._meta.get_field('file').storage
                f = storage.open(f.name, 'rb')

            close_file = True

        # Seek to beginning
        f.seek(0)

        try:
            yield f
        finally:
            if close_file:
                f.close()

    def get_file_size(self):
        if self.file_size is None:
            try:
                self.file_size = self.file.size
            except Exception:
                # File doesn't exist
                return

            self.save(update_fields=['file_size'])

        return self.file_size

    def _set_file_hash(self, file_contents):
        self.file_hash = hashlib.sha1(file_contents).hexdigest()

    def get_file_hash(self):
        if self.file_hash == '':
            with self.open_file() as f:
                self._set_file_hash(f.read())

            self.save(update_fields=['file_hash'])

        return self.file_hash

    def __str__(self):
        return self.title

    @property
    def filename(self):
        return os.path.basename(self.file.name)

    @property
    def file_extension(self):
        return os.path.splitext(self.filename)[1][1:]

    @property
    def url(self):
        if getattr(settings, 'WAGTAILDOCS_SERVE_METHOD', None) == 'direct':
            try:
                return self.file.url
            except NotImplementedError:
                # backend does not provide a url, so fall back on the serve view
                pass

        return reverse('wagtaildocs_serve', args=[self.id, self.filename])

    def get_usage(self):
        return get_object_usage(self)

    @property
    def usage_url(self):
        return reverse('wagtaildocs:document_usage', args=(self.id, ))

    def is_editable_by_user(self, user):
        from wagtail.documents.permissions import permission_policy
        return permission_policy.user_has_permission_for_instance(
            user, 'change', self)

    class Meta:
        abstract = True
        verbose_name = _('document')
        verbose_name_plural = _('documents')
Example #12
0
class AbstractDocument(CollectionMember, index.Indexed, models.Model):
    title = models.CharField(max_length=255, verbose_name=_('title'))
    file = models.FileField(upload_to='documents', verbose_name=_('file'))
    created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True)
    uploaded_by_user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_('uploaded by user'),
        null=True,
        blank=True,
        editable=False,
        on_delete=models.SET_NULL
    )

    tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags'))

    file_size = models.PositiveIntegerField(null=True, editable=False)
    # A SHA-1 hash of the file contents
    file_hash = models.CharField(max_length=40, blank=True, editable=False)

    objects = DocumentQuerySet.as_manager()

    search_fields = CollectionMember.search_fields + [
        index.SearchField('title', partial_match=True, boost=10),
        index.AutocompleteField('title'),
        index.FilterField('title'),
        index.RelatedFields('tags', [
            index.SearchField('name', partial_match=True, boost=10),
            index.AutocompleteField('name'),
        ]),
        index.FilterField('uploaded_by_user'),
    ]

    def clean(self):
        """
        Checks for WAGTAILDOCS_EXTENSIONS and validates the uploaded file
        based on allowed extensions that were specified.
        Warning : This doesn't always ensure that the uploaded file is valid
        as files can be renamed to have an extension no matter what
        data they contain.

        More info : https://docs.djangoproject.com/en/3.1/ref/validators/#fileextensionvalidator
        """
        allowed_extensions = getattr(settings, "WAGTAILDOCS_EXTENSIONS", None)
        if allowed_extensions:
            validate = FileExtensionValidator(allowed_extensions)
            validate(self.file)

    def is_stored_locally(self):
        """
        Returns True if the image is hosted on the local filesystem
        """
        try:
            self.file.path

            return True
        except NotImplementedError:
            return False

    @contextmanager
    def open_file(self):
        # Open file if it is closed
        close_file = False
        f = self.file

        if f.closed:
            # Reopen the file
            if self.is_stored_locally():
                f.open('rb')
            else:
                # Some external storage backends don't allow reopening
                # the file. Get a fresh file instance. #1397
                storage = self._meta.get_field('file').storage
                f = storage.open(f.name, 'rb')

            close_file = True

        # Seek to beginning
        f.seek(0)

        try:
            yield f
        finally:
            if close_file:
                f.close()

    def get_file_size(self):
        if self.file_size is None:
            try:
                self.file_size = self.file.size
            except Exception:
                # File doesn't exist
                return

            self.save(update_fields=['file_size'])

        return self.file_size

    def _set_file_hash(self, file_contents):
        self.file_hash = hashlib.sha1(file_contents).hexdigest()

    def get_file_hash(self):
        if self.file_hash == '':
            with self.open_file() as f:
                self._set_file_hash(f.read())

            self.save(update_fields=['file_hash'])

        return self.file_hash

    def __str__(self):
        return self.title

    @property
    def filename(self):
        return os.path.basename(self.file.name)

    @property
    def file_extension(self):
        return os.path.splitext(self.filename)[1][1:]

    @property
    def url(self):
        if getattr(settings, 'WAGTAILDOCS_SERVE_METHOD', None) == 'direct':
            try:
                return self.file.url
            except NotImplementedError:
                # backend does not provide a url, so fall back on the serve view
                pass

        return reverse('wagtaildocs_serve', args=[self.id, self.filename])

    def get_usage(self):
        return get_object_usage(self)

    @property
    def usage_url(self):
        return reverse('wagtaildocs:document_usage',
                       args=(self.id,))

    def is_editable_by_user(self, user):
        from wagtail.documents.permissions import permission_policy
        return permission_policy.user_has_permission_for_instance(user, 'change', self)

    @property
    def content_type(self):
        content_types_lookup = getattr(settings, 'WAGTAILDOCS_CONTENT_TYPES', {})
        return (
            content_types_lookup.get(self.file_extension.lower())
            or guess_type(self.filename)[0]
            or 'application/octet-stream'
        )

    @property
    def content_disposition(self):
        inline_content_types = getattr(
            settings, 'WAGTAILDOCS_INLINE_CONTENT_TYPES', ['application/pdf']
        )
        if self.content_type in inline_content_types:
            return 'inline'
        else:
            return "attachment; filename={0}; filename*=UTF-8''{0}".format(
                urllib.parse.quote(self.filename)
            )

    class Meta:
        abstract = True
        verbose_name = _('document')
        verbose_name_plural = _('documents')
Example #13
0
class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed,
                    models.Model):
    title = models.CharField(max_length=255, verbose_name=_("title"))
    file = models.ImageField(
        verbose_name=_("file"),
        upload_to=get_upload_to,
        width_field="width",
        height_field="height",
    )
    width = models.IntegerField(verbose_name=_("width"), editable=False)
    height = models.IntegerField(verbose_name=_("height"), editable=False)
    created_at = models.DateTimeField(verbose_name=_("created at"),
                                      auto_now_add=True,
                                      db_index=True)
    uploaded_by_user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_("uploaded by user"),
        null=True,
        blank=True,
        editable=False,
        on_delete=models.SET_NULL,
    )

    tags = TaggableManager(help_text=None, blank=True, verbose_name=_("tags"))

    focal_point_x = models.PositiveIntegerField(null=True, blank=True)
    focal_point_y = models.PositiveIntegerField(null=True, blank=True)
    focal_point_width = models.PositiveIntegerField(null=True, blank=True)
    focal_point_height = models.PositiveIntegerField(null=True, blank=True)

    file_size = models.PositiveIntegerField(null=True, editable=False)
    # A SHA-1 hash of the file contents
    file_hash = models.CharField(max_length=40,
                                 blank=True,
                                 editable=False,
                                 db_index=True)

    objects = ImageQuerySet.as_manager()

    def _set_file_hash(self, file_contents):
        self.file_hash = hashlib.sha1(file_contents).hexdigest()

    def get_file_hash(self):
        if self.file_hash == "":
            with self.open_file() as f:
                self._set_file_hash(f.read())

            self.save(update_fields=["file_hash"])

        return self.file_hash

    def get_upload_to(self, filename):
        folder_name = "original_images"
        filename = self.file.field.storage.get_valid_name(filename)

        # convert the filename to simple ascii characters and then
        # replace non-ascii characters in filename with _ , to sidestep issues with filesystem encoding
        filename = "".join(
            (i if ord(i) < 128 else "_") for i in string_to_ascii(filename))

        # Truncate filename so it fits in the 100 character limit
        # https://code.djangoproject.com/ticket/9893
        full_path = os.path.join(folder_name, filename)
        if len(full_path) >= 95:
            chars_to_trim = len(full_path) - 94
            prefix, extension = os.path.splitext(filename)
            filename = prefix[:-chars_to_trim] + extension
            full_path = os.path.join(folder_name, filename)

        return full_path

    def get_usage(self):
        return get_object_usage(self)

    @property
    def usage_url(self):
        return reverse("wagtailimages:image_usage", args=(self.id, ))

    search_fields = CollectionMember.search_fields + [
        index.SearchField("title", partial_match=True, boost=10),
        index.AutocompleteField("title"),
        index.FilterField("title"),
        index.RelatedFields(
            "tags",
            [
                index.SearchField("name", partial_match=True, boost=10),
                index.AutocompleteField("name"),
            ],
        ),
        index.FilterField("uploaded_by_user"),
    ]

    def __str__(self):
        return self.title

    def get_rect(self):
        return Rect(0, 0, self.width, self.height)

    def get_focal_point(self):
        if (self.focal_point_x is not None and self.focal_point_y is not None
                and self.focal_point_width is not None
                and self.focal_point_height is not None):
            return Rect.from_point(
                self.focal_point_x,
                self.focal_point_y,
                self.focal_point_width,
                self.focal_point_height,
            )

    def has_focal_point(self):
        return self.get_focal_point() is not None

    def set_focal_point(self, rect):
        if rect is not None:
            self.focal_point_x = rect.centroid_x
            self.focal_point_y = rect.centroid_y
            self.focal_point_width = rect.width
            self.focal_point_height = rect.height
        else:
            self.focal_point_x = None
            self.focal_point_y = None
            self.focal_point_width = None
            self.focal_point_height = None

    def get_suggested_focal_point(self):
        with self.get_willow_image() as willow:
            faces = willow.detect_faces()

            if faces:
                # Create a bounding box around all faces
                left = min(face[0] for face in faces)
                top = min(face[1] for face in faces)
                right = max(face[2] for face in faces)
                bottom = max(face[3] for face in faces)
                focal_point = Rect(left, top, right, bottom)
            else:
                features = willow.detect_features()
                if features:
                    # Create a bounding box around all features
                    left = min(feature[0] for feature in features)
                    top = min(feature[1] for feature in features)
                    right = max(feature[0] for feature in features)
                    bottom = max(feature[1] for feature in features)
                    focal_point = Rect(left, top, right, bottom)
                else:
                    return None

        # Add 20% to width and height and give it a minimum size
        x, y = focal_point.centroid
        width, height = focal_point.size

        width *= 1.20
        height *= 1.20

        width = max(width, 100)
        height = max(height, 100)

        return Rect.from_point(x, y, width, height)

    @classmethod
    def get_rendition_model(cls):
        """Get the Rendition model for this Image model"""
        return cls.renditions.rel.related_model

    def get_rendition(self, filter: Union["Filter",
                                          str]) -> "AbstractRendition":
        """
        Returns a ``Rendition*`` instance with a ``file`` field value (an
        image) reflecting the supplied ``filter`` value and focal point values
        from this object.

        *If using custom image models, an instance of the custom rendition
        model will be returned.
        """
        if isinstance(filter, str):
            filter = Filter(spec=filter)

        Rendition = self.get_rendition_model()

        try:
            rendition = self.find_existing_rendition(filter)
        except Rendition.DoesNotExist:
            rendition = self.create_rendition(filter)
            # Reuse this rendition if requested again from this object
            if "renditions" in getattr(self, "_prefetched_objects_cache", {}):
                self._prefetched_objects_cache[
                    "renditions"]._result_cache.append(rendition)

        try:
            cache = caches["renditions"]
            key = Rendition.construct_cache_key(self.id,
                                                filter.get_cache_key(self),
                                                filter.spec)
            cache.set(key, rendition)
        except InvalidCacheBackendError:
            pass

        return rendition

    def find_existing_rendition(self, filter: "Filter") -> "AbstractRendition":
        """
        Returns an existing ``Rendition*`` instance with a ``file`` field value
        (an image) reflecting the supplied ``filter`` value and focal point
        values from this object.

        If no such rendition exists, a ``DoesNotExist`` error is raised for the
        relevant model.

        *If using custom image models, an instance of the custom rendition
        model will be returned.
        """

        Rendition = self.get_rendition_model()
        cache_key = filter.get_cache_key(self)

        # Interrogate prefetched values first (if available)
        if "renditions" in getattr(self, "_prefetched_objects_cache", {}):
            for rendition in self.renditions.all():
                if (rendition.filter_spec == filter.spec
                        and rendition.focal_point_key == cache_key):
                    return rendition

            # If renditions were prefetched, assume that if a suitable match
            # existed, it would have been present and already returned above
            # (avoiding further cache/db lookups)
            raise Rendition.DoesNotExist

        # Next, query the cache (if configured)
        try:
            cache = caches["renditions"]
            key = Rendition.construct_cache_key(self.id, cache_key,
                                                filter.spec)
            cached_rendition = cache.get(key)
            if cached_rendition:
                return cached_rendition
        except InvalidCacheBackendError:
            pass

        # Resort to a get() lookup
        return self.renditions.get(filter_spec=filter.spec,
                                   focal_point_key=cache_key)

    def create_rendition(self, filter: "Filter") -> "AbstractRendition":
        """
        Creates and returns a ``Rendition*`` instance with a ``file`` field
        value (an image) reflecting the supplied ``filter`` value and focal
        point values from this object.

        This method is usually called by ``Image.get_rendition()``, after first
        checking that a suitable rendition does not already exist.

        *If using custom image models, an instance of the custom rendition
        model will be returned.
        """
        # Because of unique constraints applied to the model, we use
        # get_or_create() to guard against race conditions
        rendition, created = self.renditions.get_or_create(
            filter_spec=filter.spec,
            focal_point_key=filter.get_cache_key(self),
            defaults={"file": self.generate_rendition_file(filter)},
        )
        return rendition

    def generate_rendition_file(self, filter: "Filter") -> File:
        """
        Generates an in-memory image matching the supplied ``filter`` value
        and focal point value from this object, wraps it in a ``File`` object
        with a suitable filename, and returns it. The return value is used
        as the ``file`` field value for rendition objects saved by
        ``AbstractImage.create_rendition()``.

        NOTE: The responsibility of generating the new image from the original
        falls to the supplied ``filter`` object. If you want to do anything
        custom with rendition images (for example, to preserve metadata from
        the original image), you might want to consider swapping out ``filter``
        for an instance of a custom ``Filter`` subclass of your design.
        """

        cache_key = filter.get_cache_key(self)

        logger.debug(
            "Generating '%s' rendition for image %d",
            filter.spec,
            self.pk,
        )

        start_time = time.time()

        try:
            generated_image = filter.run(self, BytesIO())

            logger.debug(
                "Generated '%s' rendition for image %d in %.1fms",
                filter.spec,
                self.pk,
                (time.time() - start_time) * 1000,
            )
        except:  # noqa:B901,E722
            logger.debug(
                "Failed to generate '%s' rendition for image %d",
                filter.spec,
                self.pk,
            )
            raise

        # Generate filename
        input_filename = os.path.basename(self.file.name)
        input_filename_without_extension, input_extension = os.path.splitext(
            input_filename)
        output_extension = (
            filter.spec.replace("|", ".") +
            IMAGE_FORMAT_EXTENSIONS[generated_image.format_name])
        if cache_key:
            output_extension = cache_key + "." + output_extension

        # Truncate filename to prevent it going over 60 chars
        output_filename_without_extension = input_filename_without_extension[:(
            59 - len(output_extension))]
        output_filename = output_filename_without_extension + "." + output_extension

        return File(generated_image.f, name=output_filename)

    def is_portrait(self):
        return self.width < self.height

    def is_landscape(self):
        return self.height < self.width

    @property
    def filename(self):
        return os.path.basename(self.file.name)

    @property
    def default_alt_text(self):
        # by default the alt text field (used in rich text insertion) is populated
        # from the title. Subclasses might provide a separate alt field, and
        # override this
        return self.title

    def is_editable_by_user(self, user):
        from wagtail.images.permissions import permission_policy

        return permission_policy.user_has_permission_for_instance(
            user, "change", self)

    class Meta:
        abstract = True
Example #14
0
class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed, models.Model):
    title = models.CharField(max_length=255, verbose_name=_('title'))
    file = models.ImageField(
        verbose_name=_('file'), upload_to=get_upload_to, width_field='width', height_field='height'
    )
    width = models.IntegerField(verbose_name=_('width'), editable=False)
    height = models.IntegerField(verbose_name=_('height'), editable=False)
    created_at = models.DateTimeField(verbose_name=_('created at'), auto_now_add=True, db_index=True)
    uploaded_by_user = models.ForeignKey(
        settings.AUTH_USER_MODEL, verbose_name=_('uploaded by user'),
        null=True, blank=True, editable=False, on_delete=models.SET_NULL
    )

    tags = TaggableManager(help_text=None, blank=True, verbose_name=_('tags'))

    focal_point_x = models.PositiveIntegerField(null=True, blank=True)
    focal_point_y = models.PositiveIntegerField(null=True, blank=True)
    focal_point_width = models.PositiveIntegerField(null=True, blank=True)
    focal_point_height = models.PositiveIntegerField(null=True, blank=True)

    file_size = models.PositiveIntegerField(null=True, editable=False)
    # A SHA-1 hash of the file contents
    file_hash = models.CharField(max_length=40, blank=True, editable=False)

    objects = ImageQuerySet.as_manager()

    def _set_file_hash(self, file_contents):
        self.file_hash = hashlib.sha1(file_contents).hexdigest()

    def get_file_hash(self):
        if self.file_hash == '':
            with self.open_file() as f:
                self._set_file_hash(f.read())

            self.save(update_fields=['file_hash'])

        return self.file_hash

    def get_upload_to(self, filename):
        folder_name = 'original_images'
        filename = self.file.field.storage.get_valid_name(filename)

        # do a unidecode in the filename and then
        # replace non-ascii characters in filename with _ , to sidestep issues with filesystem encoding
        filename = "".join((i if ord(i) < 128 else '_') for i in string_to_ascii(filename))

        # Truncate filename so it fits in the 100 character limit
        # https://code.djangoproject.com/ticket/9893
        full_path = os.path.join(folder_name, filename)
        if len(full_path) >= 95:
            chars_to_trim = len(full_path) - 94
            prefix, extension = os.path.splitext(filename)
            filename = prefix[:-chars_to_trim] + extension
            full_path = os.path.join(folder_name, filename)

        return full_path

    def get_usage(self):
        return get_object_usage(self)

    @property
    def usage_url(self):
        return reverse('wagtailimages:image_usage',
                       args=(self.id,))

    search_fields = CollectionMember.search_fields + [
        index.SearchField('title', partial_match=True, boost=10),
        index.AutocompleteField('title'),
        index.FilterField('title'),
        index.RelatedFields('tags', [
            index.SearchField('name', partial_match=True, boost=10),
            index.AutocompleteField('name'),
        ]),
        index.FilterField('uploaded_by_user'),
    ]

    def __str__(self):
        return self.title

    def get_rect(self):
        return Rect(0, 0, self.width, self.height)

    def get_focal_point(self):
        if self.focal_point_x is not None and \
           self.focal_point_y is not None and \
           self.focal_point_width is not None and \
           self.focal_point_height is not None:
            return Rect.from_point(
                self.focal_point_x,
                self.focal_point_y,
                self.focal_point_width,
                self.focal_point_height,
            )

    def has_focal_point(self):
        return self.get_focal_point() is not None

    def set_focal_point(self, rect):
        if rect is not None:
            self.focal_point_x = rect.centroid_x
            self.focal_point_y = rect.centroid_y
            self.focal_point_width = rect.width
            self.focal_point_height = rect.height
        else:
            self.focal_point_x = None
            self.focal_point_y = None
            self.focal_point_width = None
            self.focal_point_height = None

    def get_suggested_focal_point(self):
        with self.get_willow_image() as willow:
            faces = willow.detect_faces()

            if faces:
                # Create a bounding box around all faces
                left = min(face[0] for face in faces)
                top = min(face[1] for face in faces)
                right = max(face[2] for face in faces)
                bottom = max(face[3] for face in faces)
                focal_point = Rect(left, top, right, bottom)
            else:
                features = willow.detect_features()
                if features:
                    # Create a bounding box around all features
                    left = min(feature[0] for feature in features)
                    top = min(feature[1] for feature in features)
                    right = max(feature[0] for feature in features)
                    bottom = max(feature[1] for feature in features)
                    focal_point = Rect(left, top, right, bottom)
                else:
                    return None

        # Add 20% to width and height and give it a minimum size
        x, y = focal_point.centroid
        width, height = focal_point.size

        width *= 1.20
        height *= 1.20

        width = max(width, 100)
        height = max(height, 100)

        return Rect.from_point(x, y, width, height)

    @classmethod
    def get_rendition_model(cls):
        """ Get the Rendition model for this Image model """
        return cls.renditions.rel.related_model

    def get_rendition(self, filter):
        if isinstance(filter, str):
            filter = Filter(spec=filter)

        cache_key = filter.get_cache_key(self)
        Rendition = self.get_rendition_model()

        try:
            rendition_caching = True
            cache = caches['renditions']
            rendition_cache_key = Rendition.construct_cache_key(
                self.id,
                cache_key,
                filter.spec
            )
            cached_rendition = cache.get(rendition_cache_key)
            if cached_rendition:
                return cached_rendition
        except InvalidCacheBackendError:
            rendition_caching = False

        try:
            rendition = self.renditions.get(
                filter_spec=filter.spec,
                focal_point_key=cache_key,
            )
        except Rendition.DoesNotExist:
            # Generate the rendition image
            try:
                logger.debug(
                    "Generating '%s' rendition for image %d",
                    filter.spec,
                    self.pk,
                )

                start_time = time.time()
                generated_image = filter.run(self, BytesIO())

                logger.debug(
                    "Generated '%s' rendition for image %d in %.1fms",
                    filter.spec,
                    self.pk,
                    (time.time() - start_time) * 1000
                )
            except:  # noqa:B901,E722
                logger.debug("Failed to generate '%s' rendition for image %d", filter.spec, self.pk)
                raise

            # Generate filename
            input_filename = os.path.basename(self.file.name)
            input_filename_without_extension, input_extension = os.path.splitext(input_filename)

            # A mapping of image formats to extensions
            FORMAT_EXTENSIONS = {
                'jpeg': '.jpg',
                'png': '.png',
                'gif': '.gif',
                'webp': '.webp',
            }

            output_extension = filter.spec.replace('|', '.') + FORMAT_EXTENSIONS[generated_image.format_name]
            if cache_key:
                output_extension = cache_key + '.' + output_extension

            # Truncate filename to prevent it going over 60 chars
            output_filename_without_extension = input_filename_without_extension[:(59 - len(output_extension))]
            output_filename = output_filename_without_extension + '.' + output_extension

            rendition, created = self.renditions.get_or_create(
                filter_spec=filter.spec,
                focal_point_key=cache_key,
                defaults={'file': File(generated_image.f, name=output_filename)}
            )

        if rendition_caching:
            cache.set(rendition_cache_key, rendition)

        return rendition

    def is_portrait(self):
        return (self.width < self.height)

    def is_landscape(self):
        return (self.height < self.width)

    @property
    def filename(self):
        return os.path.basename(self.file.name)

    @property
    def default_alt_text(self):
        # by default the alt text field (used in rich text insertion) is populated
        # from the title. Subclasses might provide a separate alt field, and
        # override this
        return self.title

    def is_editable_by_user(self, user):
        from wagtail.images.permissions import permission_policy
        return permission_policy.user_has_permission_for_instance(user, 'change', self)

    class Meta:
        abstract = True
Example #15
0
    class ProductProxy(index.Indexed, get_model("catalogue", "Product")):
        def popularity(self):
            months_to_run = settings.OSCAR_SEARCH.get(
                "MONTHS_TO_RUN_ANALYTICS", 3)
            orders_above_date = timezone.now() - relativedelta(
                months=months_to_run)

            Line = get_model("order", "Line")

            return Line.objects.filter(
                product=self,
                order__date_placed__gte=orders_above_date).count()

        def price(self):
            selector = get_class("partner.strategy", "Selector")
            strategy = selector().strategy()
            if self.is_parent:
                return strategy.fetch_for_parent(self).price.incl_tax

            return strategy.fetch_for_product(self).price.incl_tax

        def string_attrs(self):
            return [str(a.value_as_text) for a in self.attribute_values.all()]

        def attrs(self):
            values = self.attribute_values.all().select_related("attribute")
            result = {}
            for value in values:
                at = value.attribute
                if at.type == at.OPTION:
                    result[value.attribute.code] = value.value.option
                elif at.type == at.MULTI_OPTION:
                    result[value.attribute.code] = [
                        a.option for a in value.value
                    ]
                elif es_type_for_product_attribute(at) != "text":
                    result[value.attribute.code] = value.value

            if self.is_parent:
                for child in ProductProxy.objects.filter(parent=self):
                    result = merge_dicts(result, child.attrs())

            return result

        def object(self):
            "Mimic a haystack search result"
            return self

        def category_id(self):
            return self.categories.values_list("id", flat=True)

        def category_name(self):
            return list(self.categories.values_list("name", flat=True))

        @classmethod
        def get_search_fields(cls):  # hook extra_product_fields for overriding
            return process_product_fields(super().get_search_fields())

        search_fields = [
            index.FilterField("id"),
            index.SearchField("title", partial_match=True, boost=2),
            index.AutocompleteField("title"),
            index.AutocompleteField("upc", es_extra={"analyzer": "keyword"}),
            index.FilterField("upc"),
            index.SearchField("upc", boost=3, es_extra={"analyzer":
                                                        "keyword"}),
            index.SearchField("description", partial_match=True),
            index.FilterField("popularity"),
            index.FilterField("price", es_extra={"type": "double"}),
            index.FilterField("category_id"),
            index.SearchField("category_name", partial_match=True),
            index.AutocompleteField("category_name"),
            index.RelatedFields(
                "categories",
                [
                    index.SearchField("description", partial_match=True),
                    index.SearchField("slug"),
                    index.SearchField("full_name"),
                    index.SearchField("get_absolute_url"),
                ],
            ),
            index.RelatedFields(
                "stockrecords",
                [
                    index.FilterField("price_currency"),
                    index.SearchField("partner_sku"),
                    index.SearchField("price_excl_tax"),
                    index.FilterField("partner"),
                    index.FilterField("num_in_stock"),
                ],
            ),
            index.FilterField("parent_id"),
            index.FilterField("structure"),
            index.FilterField("is_standalone"),
            index.FilterField("slug"),
            index.FilterField("rating"),
            index.FilterField("date_created"),
            index.FilterField("date_updated"),
            index.SearchField("string_attrs"),
            index.FilterField("attrs",
                              es_extra=product_attributes_es_config()),
        ]

        class Meta:
            proxy = True
Example #16
0
class ProgrammePage(BasePage):
    parent_page_types = ["ProgrammeIndexPage"]
    subpage_types = []
    template = "patterns/pages/programmes/programme_detail.html"

    # Comments resemble tabbed panels in the editor
    # Content
    degree_level = models.ForeignKey(DegreeLevel,
                                     on_delete=models.SET_NULL,
                                     blank=False,
                                     null=True,
                                     related_name="+")
    programme_type = models.ForeignKey(
        ProgrammeType,
        on_delete=models.SET_NULL,
        blank=False,
        null=True,
        related_name="+",
    )
    hero_image = models.ForeignKey(
        "images.CustomImage",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    hero_video = models.URLField(blank=True)
    hero_video_preview_image = models.ForeignKey(
        "images.CustomImage",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    hero_colour_option = models.PositiveSmallIntegerField(
        choices=(HERO_COLOUR_CHOICES))

    # Key Details
    programme_details_credits = models.CharField(max_length=25, blank=True)
    programme_details_credits_suffix = models.CharField(
        max_length=1,
        choices=(("1", "credits"), ("2", "credits at FHEQ Level 6")),
        blank=True,
    )
    programme_details_time = models.CharField(max_length=25, blank=True)
    programme_details_time_suffix = models.CharField(
        max_length=1,
        choices=(
            ("1", "year programme"),
            ("2", "month programme"),
            ("3", "week programme"),
        ),
        blank=True,
    )
    programme_details_duration = models.CharField(
        max_length=1,
        choices=(
            ("1", "Full-time study"),
            ("2", "Full-time study with part-time option"),
            ("3", "Part-time study"),
        ),
        blank=True,
    )

    next_open_day_date = models.DateField(blank=True, null=True)
    link_to_open_days = models.URLField(blank=True)
    application_deadline = models.DateField(blank=True, null=True)
    application_deadline_options = models.CharField(
        max_length=1,
        choices=(
            ("1", "Applications closed. Please check back soon."),
            ("2", "Still accepting applications"),
        ),
        blank=True,
    )

    programme_specification = models.ForeignKey(
        "documents.CustomDocument",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

    # Programme Overview
    programme_description_title = models.CharField(max_length=125, blank=True)
    programme_description_subtitle = models.CharField(max_length=500,
                                                      blank=True)
    programme_image = models.ForeignKey(
        get_image_model_string(),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    programme_video_caption = models.CharField(
        blank=True,
        max_length=80,
        help_text="The text dipsplayed next to the video play button",
    )
    programme_video = models.URLField(blank=True)
    programme_description_copy = RichTextField(blank=True)

    programme_gallery = StreamField([("slide", GalleryBlock())],
                                    blank=True,
                                    verbose_name="Programme gallery")

    # Staff
    staff_link = models.URLField(blank=True)
    staff_link_text = models.CharField(
        max_length=125, blank=True, help_text="E.g. 'See all programme staff'")

    facilities_snippet = models.ForeignKey(
        "utils.FacilitiesSnippet",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    facilities_gallery = StreamField(
        [(
            "slide",
            StructBlock([("title", CharBlock()),
                         ("image", ImageChooserBlock())]),
        )],
        blank=True,
    )

    notable_alumni_links = StreamField(
        [(
            "Link_to_person",
            StructBlock(
                [("name", CharBlock()), ("link", URLBlock(required=False))],
                icon="link",
            ),
        )],
        blank=True,
    )
    contact_email = models.EmailField(blank=True)
    contact_url = models.URLField(blank=True)
    contact_image = models.ForeignKey(
        "images.CustomImage",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

    # TODO
    # Alumni Stories Carousel (api fetch)
    # Related Content (news and events api fetch)

    # Programme Curriculumm
    curriculum_image = models.ForeignKey(
        get_image_model_string(),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    curriculum_subtitle = models.CharField(blank=True, max_length=100)
    curriculum_video_caption = models.CharField(
        blank=True,
        max_length=80,
        help_text="The text dipsplayed next to the video play button",
    )
    curriculum_video = models.URLField(blank=True)
    curriculum_text = models.TextField(blank=True, max_length=250)

    # Pathways
    pathway_blocks = StreamField(
        [("accordion_block", AccordionBlockWithTitle())],
        blank=True,
        verbose_name="Accordion blocks",
    )
    what_you_will_cover_blocks = StreamField(
        [
            ("accordion_block", AccordionBlockWithTitle()),
            ("accordion_snippet",
             SnippetChooserBlock("utils.AccordionSnippet")),
        ],
        blank=True,
        verbose_name="Accordion blocks",
    )

    # Requirements
    requirements_text = RichTextField(blank=True)
    requirements_blocks = StreamField(
        [
            ("accordion_block", AccordionBlockWithTitle()),
            ("accordion_snippet",
             SnippetChooserBlock("utils.AccordionSnippet")),
        ],
        blank=True,
        verbose_name="Accordion blocks",
    )

    # fees
    fees_disclaimer = models.ForeignKey(
        "utils.FeeDisclaimerSnippet",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    # Scholarships
    scholarships_title = models.CharField(max_length=120)
    scholarships_information = models.CharField(max_length=250)
    scholarship_accordion_items = StreamField(
        [("accordion", AccordionBlockWithTitle())], blank=True)
    scholarship_information_blocks = StreamField(
        [("information_block", InfoBlock())], blank=True)
    # More information
    more_information_blocks = StreamField([("information_block", InfoBlock())],
                                          blank=True)

    # Apply
    disable_apply_tab = models.BooleanField(
        default=0,
        help_text=(
            "This setting will remove the apply tab from this programme. "
            "This setting is ignored if the feature has already been disabled"
            " at the global level in Settings > Programme settings."),
    )
    apply_image = models.ForeignKey(
        get_image_model_string(),
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    steps = StreamField(
        [
            ("step", StepBlock()),
            ("step_snippet", SnippetChooserBlock("utils.StepSnippet")),
        ],
        blank=True,
    )

    content_panels = BasePage.content_panels + [
        # Taxonomy, relationships etc
        FieldPanel("degree_level"),
        InlinePanel("subjects", label="Subjects"),
        FieldPanel(
            "programme_type",
            help_text="Used to show content related to this programme page",
        ),
        MultiFieldPanel(
            [
                ImageChooserPanel("hero_image"),
                FieldPanel("hero_video"),
                ImageChooserPanel("hero_video_preview_image"),
                FieldPanel("hero_colour_option"),
            ],
            heading="Hero",
        ),
        MultiFieldPanel(
            [InlinePanel("related_programmes", label="Related programmes")],
            heading="Related Programmes",
        ),
        MultiFieldPanel(
            [
                InlinePanel(
                    "related_schools_and_research_pages",
                    label="Related Schools and Research Pages",
                    max_num=1,
                )
            ],
            heading="Related Schools and Research pages",
        ),
    ]
    key_details_panels = [
        MultiFieldPanel(
            [
                FieldPanel("programme_details_credits"),
                FieldPanel("programme_details_credits_suffix"),
                FieldPanel("programme_details_time"),
                FieldPanel("programme_details_time_suffix"),
                FieldPanel("programme_details_duration"),
            ],
            heading="Details",
        ),
        FieldPanel("next_open_day_date"),
        FieldPanel("link_to_open_days"),
        FieldPanel("application_deadline"),
        FieldPanel(
            "application_deadline_options",
            help_text="Optionally display information about the deadline",
        ),
        InlinePanel("career_opportunities", label="Career Opportunities"),
        DocumentChooserPanel("programme_specification"),
    ]
    programme_overview_pannels = [
        MultiFieldPanel(
            [
                FieldPanel("programme_description_title"),
                FieldPanel("programme_description_subtitle"),
                ImageChooserPanel("programme_image"),
                FieldPanel("programme_video_caption"),
                FieldPanel("programme_video"),
                FieldPanel("programme_description_copy"),
            ],
            heading="Programme Description",
        ),
        MultiFieldPanel([StreamFieldPanel("programme_gallery")],
                        heading="Programme gallery"),
        MultiFieldPanel(
            [
                InlinePanel("related_staff", max_num=2),
                FieldPanel("staff_link"),
                FieldPanel("staff_link_text"),
            ],
            heading="Staff",
        ),
        MultiFieldPanel(
            [
                SnippetChooserPanel("facilities_snippet"),
                StreamFieldPanel("facilities_gallery"),
            ],
            heading="Facilities",
        ),
        MultiFieldPanel([StreamFieldPanel("notable_alumni_links")],
                        heading="Alumni"),
        MultiFieldPanel(
            [
                ImageChooserPanel("contact_image"),
                FieldPanel("contact_email"),
                FieldPanel("contact_url"),
            ],
            heading="Contact information",
        ),
    ]
    programme_curriculum_pannels = [
        MultiFieldPanel(
            [
                ImageChooserPanel("curriculum_image"),
                FieldPanel("curriculum_subtitle"),
                FieldPanel("curriculum_video"),
                FieldPanel("curriculum_video_caption"),
                FieldPanel("curriculum_text"),
            ],
            heading="Curriculum introduction",
        ),
        MultiFieldPanel([StreamFieldPanel("pathway_blocks")],
                        heading="Pathways"),
        MultiFieldPanel(
            [StreamFieldPanel("what_you_will_cover_blocks")],
            heading="What you'll cover",
        ),
    ]

    programme_requirements_pannels = [
        FieldPanel("requirements_text"),
        StreamFieldPanel("requirements_blocks"),
    ]
    programme_fees_and_funding_panels = [
        SnippetChooserPanel("fees_disclaimer"),
        MultiFieldPanel([InlinePanel("fee_items", label="Fee items")],
                        heading="For this program"),
        MultiFieldPanel(
            [
                FieldPanel("scholarships_title"),
                FieldPanel("scholarships_information"),
                StreamFieldPanel("scholarship_accordion_items"),
                StreamFieldPanel("scholarship_information_blocks"),
            ],
            heading="Scholarships",
        ),
        MultiFieldPanel([StreamFieldPanel("more_information_blocks")],
                        heading="More information"),
    ]
    programme_apply_pannels = [
        MultiFieldPanel([FieldPanel("disable_apply_tab")],
                        heading="Apply tab settings"),
        MultiFieldPanel([ImageChooserPanel("apply_image")],
                        heading="Introduction image"),
        MultiFieldPanel([StreamFieldPanel("steps")],
                        heading="Before you begin"),
    ]

    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading="Content"),
        ObjectList(key_details_panels, heading="Key details"),
        ObjectList(programme_overview_pannels, heading="Overview"),
        ObjectList(programme_curriculum_pannels, heading="Curriculum"),
        ObjectList(programme_requirements_pannels, heading="Requirements"),
        ObjectList(programme_fees_and_funding_panels, heading="Fees"),
        ObjectList(programme_apply_pannels, heading="Apply"),
        ObjectList(BasePage.promote_panels, heading="Promote"),
        ObjectList(BasePage.settings_panels, heading="Settings"),
    ])

    search_fields = BasePage.search_fields + [
        index.SearchField("programme_description_subtitle",
                          partial_match=True),
        index.AutocompleteField("programme_description_subtitle",
                                partial_match=True),
        index.SearchField("pathway_blocks", partial_match=True),
        index.AutocompleteField("pathway_blocks", partial_match=True),
        index.RelatedFields(
            "programme_type",
            [
                index.SearchField("display_name", partial_match=True),
                index.AutocompleteField("display_name", partial_match=True),
            ],
        ),
        index.RelatedFields(
            "degree_level",
            [
                index.SearchField("title", partial_match=True),
                index.AutocompleteField("title", partial_match=True),
            ],
        ),
        index.RelatedFields(
            "subjects",
            [
                index.RelatedFields(
                    "subject",
                    [
                        index.SearchField("title", partial_match=True),
                        index.AutocompleteField("title", partial_match=True),
                    ],
                )
            ],
        ),
    ]

    api_fields = [
        APIField("degree_level", serializer=degree_level_serializer()),
        APIField("subjects"),
        APIField("programme_type"),
        APIField("programme_description_subtitle"),
        APIField("pathway_blocks"),
        APIField(
            name="hero_image_square",
            serializer=ImageRenditionField("fill-580x580",
                                           source="hero_image"),
        ),
        APIField("related_schools_and_research_pages"),
    ]

    def get_alumni_stories(self, programme_type_legacy_slug):
        # Use the slug as prefix to the cache key
        cache_key = f"{programme_type_legacy_slug}_programme_latest_alumni_stories"
        stories_data = cache.get(cache_key)
        if stories_data is None:
            try:
                stories_data = content.pull_alumni_stories(
                    programme_type_legacy_slug)
            except content.CantPullFromRcaApi:
                return []
            else:
                cache.set(cache_key, stories_data,
                          settings.API_CONTENT_CACHE_TIMEOUT)
        return stories_data

    def get_news_and_events(self, programme_type_legacy_slug):
        # Use the slug as prefix to the cache key
        cache_key = f"{programme_type_legacy_slug}_programme_latest_news_and_events"
        news_and_events_data = cache.get(cache_key)
        if news_and_events_data is None:
            try:
                news_and_events_data = content.pull_news_and_events(
                    programme_type_legacy_slug)
            except content.CantPullFromRcaApi:
                return []
            else:
                cache.set(cache_key, news_and_events_data,
                          settings.API_CONTENT_CACHE_TIMEOUT)
        return news_and_events_data

    def clean(self):
        errors = defaultdict(list)
        if self.hero_video and not self.hero_video_preview_image:
            errors["hero_video_preview_image"].append(
                "Please add a preview image for the video.")
        if self.programme_details_credits and not self.programme_details_credits_suffix:
            errors["programme_details_credits_suffix"].append(
                "Please add a suffix")
        if self.programme_details_credits_suffix and not self.programme_details_credits:
            errors["programme_details_credits"].append(
                "Please add a credit value")
        if self.programme_details_time and not self.programme_details_time_suffix:
            errors["programme_details_time_suffix"].append(
                "Please add a suffix")
        if self.programme_details_time_suffix and not self.programme_details_time:
            errors["programme_details_time"].append("Please add a time value")
        if self.curriculum_video:
            try:
                embed = embeds.get_embed(self.curriculum_video)
            except EmbedException:
                errors["curriculum_video"].append("invalid embed URL")
            else:
                if embed.provider_name.lower() != "youtube":
                    errors["curriculum_video"].append(
                        "Only YouTube videos are supported for this field ")
        if self.staff_link and not self.staff_link_text:
            errors["staff_link_text"].append(
                "Please the text to be used for the link")
        if self.staff_link_text and not self.staff_link:
            errors["staff_link_text"].append(
                "Please add a URL value for the link")
        if not self.contact_email and not self.contact_url:
            errors["contact_url"].append(
                "Please add a target value for the contact us link")
        if self.contact_email and self.contact_url:
            errors["contact_url"].append(
                "Only one of URL or an Email value is supported here")
        if not self.search_description:
            errors["search_description"].append(
                "Please add a search description for the page.")
        if errors:
            raise ValidationError(errors)

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context["hero_colour"] = "dark"

        if int(self.hero_colour_option) == LIGHT_TEXT_ON_DARK_IMAGE:
            context["hero_colour"] = "light"

        context["related_sections"] = [{
            "title":
            "Related programmes",
            "related_items": [
                rel.page.specific
                for rel in self.related_programmes.select_related("page")
            ],
        }]
        context["related_staff"] = self.related_staff.select_related("image")

        # If one of the slides in the the programme_gallery contains author information
        # we need to set a modifier
        for block in self.programme_gallery:
            if block.value["author"]:
                context[
                    "programme_slideshow_modifier"] = "slideshow--author-info"

        # Set the page tab titles
        context["tabs"] = [
            {
                "title": "Overview"
            },
            {
                "title": "Curriculum"
            },
            {
                "title": "Requirements"
            },
            {
                "title": "Fees & funding"
            },
        ]
        # Only add the 'apply tab' depending global settings or specific programme page settings
        programme_settings = ProgrammeSettings.for_site(request.site)
        if not programme_settings.disable_apply_tab and not self.disable_apply_tab:
            context["tabs"].append({"title": "Apply"})

        # Global fields from ProgrammePageGlobalFieldsSettings
        programme_page_global_fields = ProgrammePageGlobalFieldsSettings.for_site(
            request.site)
        context["programme_page_global_fields"] = programme_page_global_fields

        return context
Example #17
0
class ShortCoursePage(ContactFieldsMixin, BasePage):
    template = "patterns/pages/shortcourses/short_course.html"
    parent_page_types = ["programmes.ProgrammeIndexPage"]
    hero_image = models.ForeignKey(
        "images.CustomImage",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    introduction = models.CharField(max_length=500, blank=True)
    introduction_image = models.ForeignKey(
        get_image_model_string(),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    video_caption = models.CharField(
        blank=True,
        max_length=80,
        help_text="The text dipsplayed next to the video play button",
    )
    video = models.URLField(blank=True)
    body = RichTextField(blank=True)
    about = StreamField(
        [("accordion_block", AccordionBlockWithTitle())],
        blank=True,
        verbose_name=_("About the course"),
    )

    access_planit_course_id = models.IntegerField(blank=True, null=True)
    frequently_asked_questions = models.ForeignKey(
        "utils.ShortCourseDetailSnippet",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    terms_and_conditions = models.ForeignKey(
        "utils.ShortCourseDetailSnippet",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )
    course_details_text = RichTextField(blank=True)
    show_register_link = models.BooleanField(
        default=1,
        help_text="If selected, an automatic 'Register your interest' link will be \
                                                                   visible in the key details section",
    )
    course_details_text = RichTextField(blank=True)
    programme_type = models.ForeignKey(
        ProgrammeType,
        on_delete=models.SET_NULL,
        blank=False,
        null=True,
        related_name="+",
    )
    location = RichTextField(blank=True, features=["link"])
    introduction = models.CharField(max_length=500, blank=True)

    quote_carousel = StreamField(
        [("quote", QuoteBlock())], blank=True, verbose_name=_("Quote carousel")
    )
    staff_title = models.CharField(
        max_length=50,
        blank=True,
        help_text="Heading to display above the short course team members, E.G Programme Team",
    )
    gallery = StreamField(
        [("slide", GalleryBlock())], blank=True, verbose_name="Gallery"
    )
    external_links = StreamField(
        [("link", LinkBlock())], blank=True, verbose_name="External Links"
    )
    application_form_url = models.URLField(
        blank=True,
        help_text="Adding an application form URL will override the Access Planit booking modal",
    )
    manual_registration_url = models.URLField(
        blank=True, help_text="Override the register interest link show in the modal",
    )

    access_planit_and_course_data_panels = [
        MultiFieldPanel(
            [
                FieldPanel("manual_registration_url"),
                HelpPanel(
                    "Defining course details manually will override any Access Planit data configured for this page"
                ),
                InlinePanel("manual_bookings", label="Booking"),
            ],
            heading="Manual course configuration",
        ),
        MultiFieldPanel(
            [FieldPanel("application_form_url")], heading="Application URL"
        ),
        FieldPanel("access_planit_course_id"),
        MultiFieldPanel(
            [
                FieldPanel("course_details_text"),
                SnippetChooserPanel("frequently_asked_questions"),
                SnippetChooserPanel("terms_and_conditions"),
            ],
            heading="course details",
        ),
    ]
    content_panels = BasePage.content_panels + [
        MultiFieldPanel([ImageChooserPanel("hero_image")], heading=_("Hero"),),
        MultiFieldPanel(
            [
                FieldPanel("introduction"),
                ImageChooserPanel("introduction_image"),
                FieldPanel("video"),
                FieldPanel("video_caption"),
                FieldPanel("body"),
            ],
            heading=_("Course Introduction"),
        ),
        StreamFieldPanel("about"),
        FieldPanel("programme_type"),
        StreamFieldPanel("quote_carousel"),
        MultiFieldPanel(
            [FieldPanel("staff_title"), InlinePanel("related_staff", label="Staff")],
            heading="Short course team",
        ),
        StreamFieldPanel("gallery"),
        MultiFieldPanel([*ContactFieldsMixin.panels], heading="Contact information"),
        MultiFieldPanel(
            [InlinePanel("related_programmes", label="Related programmes")],
            heading="Related Programmes",
        ),
        MultiFieldPanel(
            [
                InlinePanel(
                    "related_schools_and_research_pages",
                    label=_("Related Schools and Research centre pages"),
                )
            ],
            heading=_("Related Schools and Research Centre pages"),
        ),
        StreamFieldPanel("external_links"),
    ]
    key_details_panels = [
        InlinePanel("fee_items", label="Fees"),
        FieldPanel("location"),
        FieldPanel("show_register_link"),
        InlinePanel("subjects", label=_("Subjects")),
    ]

    edit_handler = TabbedInterface(
        [
            ObjectList(content_panels, heading="Content"),
            ObjectList(key_details_panels, heading="Key details"),
            ObjectList(
                access_planit_and_course_data_panels, heading="Course configuration"
            ),
            ObjectList(BasePage.promote_panels, heading="Promote"),
            ObjectList(BasePage.settings_panels, heading="Settings"),
        ]
    )

    search_fields = BasePage.search_fields + [
        index.SearchField("introduction", partial_match=True),
        index.AutocompleteField("introduction", partial_match=True),
        index.RelatedFields(
            "programme_type",
            [
                index.SearchField("display_name", partial_match=True),
                index.AutocompleteField("display_name", partial_match=True),
            ],
        ),
        index.RelatedFields(
            "subjects",
            [
                index.RelatedFields(
                    "subject",
                    [
                        index.SearchField("title", partial_match=True),
                        index.AutocompleteField("title", partial_match=True),
                    ],
                )
            ],
        ),
    ]

    api_fields = [
        # Fields for filtering and display, shared with programmes.ProgrammePage.
        APIField("subjects"),
        APIField("programme_type"),
        APIField("related_schools_and_research_pages"),
        APIField("summary", serializer=CharFieldSerializer(source="introduction")),
        APIField(
            name="hero_image_square",
            serializer=ImageRenditionField("fill-580x580", source="hero_image"),
        ),
    ]

    @property
    def get_manual_bookings(self):
        return self.manual_bookings.all()

    def get_access_planit_data(self):
        access_planit_course_data = AccessPlanitXML(
            course_id=self.access_planit_course_id
        )
        return access_planit_course_data.get_data()

    def _format_booking_bar(self, register_interest_link=None, access_planit_data=None):
        """ Booking bar messaging with the next course data available
        Find the next course date marked as status='available' and advertise
        this date in the booking bar. If there are no courses available, add
        a default message."""

        booking_bar = {
            "message": "Bookings not yet open",
            "action": "Register your interest for upcoming dates",
        }
        # If there are no dates the booking link should go to a form, not open
        # a modal, this link is also used as a generic interest link too though.
        booking_bar["link"] = register_interest_link

        # If manual_booking links are defined, format the booking bar and return
        # it before checking access planit
        if self.manual_bookings.first():
            date = self.manual_bookings.first()
            booking_bar["message"] = "Next course starts"
            booking_bar["date"] = date.start_date
            if date.booking_link:
                booking_bar["action"] = (
                    f"Book from \xA3{date.cost}" if date.cost else "Book now"
                )
            booking_bar["modal"] = "booking-details"
            booking_bar["cost"] = date.cost
            return booking_bar

        # If there is access planit data, format the booking bar
        if access_planit_data:
            for date in access_planit_data:
                if date["status"] == "Available":
                    booking_bar["message"] = "Next course starts"
                    booking_bar["date"] = date["start_date"]
                    booking_bar["cost"] = date["cost"]
                    if self.application_form_url:
                        # URL has been provided to override AccessPlanit booking
                        booking_bar["action"] = "Complete form to apply"
                        booking_bar["link"] = self.application_form_url
                        booking_bar["modal"] = None
                    else:
                        # Open AccessPlanit booking modal
                        booking_bar["action"] = f"Book now from \xA3{date['cost']}"
                        booking_bar["link"] = None
                        booking_bar["modal"] = "booking-details"
                    break
            return booking_bar

        # Check for a manual_registration_url if there is no data
        if self.manual_registration_url:
            booking_bar["link"] = self.manual_registration_url
        return booking_bar

    def clean(self):
        super().clean()
        errors = defaultdict(list)
        if (
            self.show_register_link
            and not self.manual_registration_url
            and not self.access_planit_course_id
        ):
            errors["show_register_link"].append(
                "An access planit course ID or manual registration link is needed to show the register links"
            )
        if self.access_planit_course_id:
            try:
                checker = AccessPlanitCourseChecker(
                    course_id=self.access_planit_course_id
                )
                if not checker.course_exists():
                    errors["access_planit_course_id"].append(
                        "Could not find a course with this ID"
                    )
            except AccessPlanitException:
                errors["access_planit_course_id"].append(
                    "Error checking this course ID in Access Planit. Please try again shortly."
                )

        if errors:
            raise ValidationError(errors)

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        access_planit_data = self.get_access_planit_data()
        context[
            "register_interest_link"
        ] = (
            register_interest_link
        ) = f"{settings.ACCESS_PLANIT_REGISTER_INTEREST_BASE}?course_id={self.access_planit_course_id}"
        context["booking_bar"] = self._format_booking_bar(
            register_interest_link, access_planit_data
        )
        context["booking_bar"] = self._format_booking_bar(
            register_interest_link, access_planit_data
        )
        context["access_planit_data"] = access_planit_data
        context["shortcourse_details_fees"] = self.fee_items.values_list(
            "text", flat=True
        )
        context["related_sections"] = [
            {
                "title": "More opportunities to study at the RCA",
                "related_items": [
                    rel.page.specific
                    for rel in self.related_programmes.select_related("page")
                ],
            }
        ]
        context["related_staff"] = self.related_staff.select_related(
            "image"
        ).prefetch_related("page")
        return context
Example #18
0
class Memorial(I18nPage):
    """A geographic place on earth."""

    template = "cms/preview/memorial.html"
    parent_page_types = ["LocationIndex"]

    title_image = models.ForeignKey(
        "ImageMedia",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        verbose_name=_(TXT["memorial_site.title_image"]),
        help_text=_(TXT["memorial_site.title_image.help"]),
    )

    remembered_authors = ParentalManyToManyField(
        "Author",
        # db_table=DB_TABLE_PREFIX + "memorial_author",
        related_name="memorials",
        blank=False,
        verbose_name=_(TXT["memorial_site.authors"]),
        help_text=_(TXT["memorial_site.authors.help"]),
    )

    memorial_type_tags = ParentalManyToManyField(
        "MemorialTag",
        db_table=DB_TABLE_PREFIX + "memorial_site_tag_memorial_type",
        related_name="memorial_site",
        blank=False,
        verbose_name=_(TXT["memorial_site.memorial_type_tags.plural"]),
        help_text=_(TXT["memorial_site.memorial_type_tags.help"]),
    )

    address = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.address"]),
        help_text=_(TXT["memorial_site.address.help"]),
    )
    address_de = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.address"]),
        help_text=_(TXT["memorial_site.address.help"]),
    )
    address_cs = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.address"]),
        help_text=_(TXT["memorial_site.address.help"]),
    )
    i18n_address = TranslatedField.named("address", True)

    contact_info = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.contact_info"]),
        help_text=_(TXT["memorial_site.contact_info.help"]),
    )
    contact_info_de = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.contact_info"]),
        help_text=_(TXT["memorial_site.contact_info.help"]),
    )
    contact_info_cs = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.contact_info"]),
        help_text=_(TXT["memorial_site.contact_info.help"]),
    )
    i18n_contact_info = TranslatedField.named("contact_info", True)

    directions = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.directions"]),
        help_text=_(TXT["memorial_site.directions.help"]),
    )
    directions_de = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.directions"]),
        help_text=_(TXT["memorial_site.directions.help"]),
    )
    directions_cs = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.directions"]),
        help_text=_(TXT["memorial_site.directions.help"]),
    )
    i18n_directions = TranslatedField.named("directions")

    coordinates = PointField(
        verbose_name=_(TXT["memorial_site.coordinates"]),
        help_text=_(TXT["memorial_site.coordinates.help"]),
    )

    introduction = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.introduction"]),
        help_text=_(TXT["memorial_site.introduction.help"]),
    )
    introduction_de = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.introduction"]),
        help_text=_(TXT["memorial_site.introduction.help"]),
    )
    introduction_cs = RichTextField(
        blank=True,
        features=I18nPage.RICH_TEXT_FEATURES,
        verbose_name=_(TXT["memorial_site.introduction"]),
        help_text=_(TXT["memorial_site.introduction.help"]),
    )
    i18n_introduction = TranslatedField.named("introduction")

    description = StreamField(
        [("paragraph", ParagraphStructBlock())],
        blank=True,
        verbose_name=_(TXT["memorial_site.description"]),
        help_text=_(TXT["memorial_site.description.help"]),
    )
    description_de = StreamField(
        [("paragraph", ParagraphStructBlock())],
        blank=True,
        verbose_name=_(TXT["memorial_site.description"]),
        help_text=_(TXT["memorial_site.description.help"]),
    )
    description_cs = StreamField(
        [("paragraph", ParagraphStructBlock())],
        blank=True,
        verbose_name=_(TXT["memorial_site.description"]),
        help_text=_(TXT["memorial_site.description.help"]),
    )
    i18n_description = TranslatedField.named("description")

    detailed_description = StreamField(
        [("paragraph", ParagraphStructBlock())],
        blank=True,
        verbose_name=_(TXT["memorial_site.detailed_description"]),
        help_text=_(TXT["memorial_site.detailed_description.help"]),
    )
    detailed_description_de = StreamField(
        [("paragraph", ParagraphStructBlock())],
        blank=True,
        verbose_name=_(TXT["memorial_site.detailed_description"]),
        help_text=_(TXT["memorial_site.detailed_description.help"]),
    )
    detailed_description_cs = StreamField(
        [("paragraph", ParagraphStructBlock())],
        blank=True,
        verbose_name=_(TXT["memorial_site.detailed_description"]),
        help_text=_(TXT["memorial_site.detailed_description.help"]),
    )
    i18n_detailed_description = TranslatedField.named("detailed_description")

    search_fields = I18nPage.search_fields + [
        index.SearchField("address"),
        index.SearchField("address_de"),
        index.SearchField("address_cs"),
        index.SearchField("contact_info"),
        index.SearchField("contact_info_de"),
        index.SearchField("contact_info_cs"),
        index.SearchField("directions"),
        index.SearchField("directions_de"),
        index.SearchField("directions_cs"),
        index.SearchField("introduction"),
        index.SearchField("introduction_de"),
        index.SearchField("introduction_cs"),
        index.SearchField("description"),
        index.AutocompleteField("description_de"),
        index.AutocompleteField("description_cs"),
        index.AutocompleteField("detailed_description"),
        index.AutocompleteField("detailed_description_de"),
        index.AutocompleteField("detailed_description_cs"),
        index.AutocompleteField("address"),
        index.AutocompleteField("address_de"),
        index.AutocompleteField("address_cs"),
        index.AutocompleteField("contact_info"),
        index.AutocompleteField("contact_info_de"),
        index.AutocompleteField("contact_info_cs"),
        index.AutocompleteField("directions"),
        index.AutocompleteField("directions_de"),
        index.AutocompleteField("directions_cs"),
        index.AutocompleteField("introduction"),
        index.AutocompleteField("introduction_de"),
        index.AutocompleteField("introduction_cs"),
        index.AutocompleteField("description"),
        index.AutocompleteField("description_de"),
        index.AutocompleteField("description_cs"),
        index.AutocompleteField("detailed_description"),
        index.AutocompleteField("detailed_description_de"),
        index.AutocompleteField("detailed_description_cs"),
        index.FilterField("remembered_authors"),
        index.FilterField("memorial_type_tags"),
        index.FilterField("coordinates"),
    ]

    api_fields = I18nPage.api_fields + [
        APIField("title_image"),
        APIField("remembered_authors"),
        APIField("memorial_type_tags"),
        APIField("address"),
        APIField("address_de"),
        APIField("address_cs"),
        APIField("contact_info"),
        APIField("contact_info_de"),
        APIField("contact_info_cs"),
        APIField("directions"),
        APIField("directions_de"),
        APIField("directions_cs"),
        APIField("introduction"),
        APIField("introduction_de"),
        APIField("introduction_cs"),
        APIField("description"),
        APIField("description_de"),
        APIField("description_cs"),
        APIField("detailed_description"),
        APIField("detailed_description_de"),
        APIField("detailed_description_cs"),
        APIField("coordinates"),
    ]

    general_panels = [
        ImageChooserPanel("title_image"),
        FieldPanel(
            "memorial_type_tags",
            widget=autocomplete.ModelSelect2Multiple(
                url="autocomplete-location-type", ),
        ),
        FieldPanel(
            "remembered_authors",
            widget=autocomplete.ModelSelect2Multiple(
                url="autocomplete-author", ),
        ),
        FieldPanelTabs(
            children=[
                FieldPanelTab("address", heading=_(TXT["language.en"])),
                FieldPanelTab("address_de", heading=_(TXT["language.de"])),
                FieldPanelTab("address_cs", heading=_(TXT["language.cs"])),
            ],
            heading=_(TXT["memorial_site.address"]),
            show_label=False,
        ),
        FieldPanelTabs(
            children=[
                FieldPanelTab("contact_info", heading=_(TXT["language.en"])),
                FieldPanelTab("contact_info_de",
                              heading=_(TXT["language.de"])),
                FieldPanelTab("contact_info_cs",
                              heading=_(TXT["language.cs"])),
            ],
            heading=_(TXT["memorial_site.contact_info"]),
            show_label=False,
        ),
        FieldPanelTabs(
            children=[
                FieldPanelTab("directions", heading=_(TXT["language.en"])),
                FieldPanelTab("directions_de", heading=_(TXT["language.de"])),
                FieldPanelTab("directions_cs", heading=_(TXT["language.cs"])),
            ],
            heading=_(TXT["memorial_site.directions"]),
            show_label=False,
        ),
        FieldPanel("coordinates", widget=CustomMapWidget()),
    ]
    english_panels = I18nPage.english_panels + [
        FieldPanel("introduction"),
        StreamFieldPanel("description"),
        StreamFieldPanel("detailed_description"),
    ]
    german_panels = I18nPage.german_panels + [
        FieldPanel("introduction_de"),
        StreamFieldPanel("description_de"),
        StreamFieldPanel("detailed_description_de"),
    ]
    czech_panels = I18nPage.czech_panels + [
        FieldPanel("introduction_cs"),
        StreamFieldPanel("description_cs"),
        StreamFieldPanel("detailed_description_cs"),
    ]
    meta_panels = I18nPage.meta_panels + [FieldPanel("slug")]
    edit_handler = TabbedInterface([
        ObjectList(general_panels, heading=_(TXT["heading.general"])),
        ObjectList(english_panels, heading=_(TXT["heading.en"])),
        ObjectList(german_panels, heading=_(TXT["heading.de"])),
        ObjectList(czech_panels, heading=_(TXT["heading.cs"])),
        ObjectList(meta_panels, heading=_(TXT["heading.meta"])),
    ])

    class Meta:
        db_table = DB_TABLE_PREFIX + "memorial"
        verbose_name = _(TXT["memorial"])
        verbose_name_plural = _(TXT["memorial.plural"])

    class JSONAPIMeta:
        resource_name = 'memorials'
Example #19
0
class ContentPage(BasePage):
    is_creatable = False
    show_in_menus = True

    # This field is used in search indexing as
    #  we can't change the search_analyzer property
    # of the default title field
    search_title = models.CharField(max_length=255, )

    legacy_guid = models.CharField(blank=True,
                                   null=True,
                                   max_length=255,
                                   help_text="""Wordpress GUID""")

    legacy_content = models.TextField(
        blank=True, null=True, help_text="""Legacy content, pre-conversion""")

    body = StreamField([
        ("heading2", blocks.Heading2Block()),
        ("heading3", blocks.Heading3Block()),
        ("heading4", blocks.Heading4Block()),
        ("heading5", blocks.Heading5Block()),
        (
            "text_section",
            blocks.TextBlock(
                blank=True,
                features=RICH_TEXT_FEATURES,
                help_text=
                """Some text to describe what this section is about (will be
            displayed above the list of child pages)""",
            ),
        ),
        ("image", blocks.ImageBlock()),
        ("embed_video", blocks.EmbedVideoBlock(help_text="""Embed a video""")),
        ("media",
         blocks.InternalMediaBlock(help_text="""Link to a media block""")),
        (
            "data_table",
            blocks.DataTableBlock(
                help_text=
                """ONLY USE THIS FOR TABLULAR DATA, NOT FOR FORMATTING"""),
        ),
    ])

    pinned_phrases = models.CharField(
        blank=True,
        null=True,
        max_length=1000,
        help_text="A comma separated list of pinned keywords and phrases. "
        "Do not use quotes for phrases. The page will be pinned "
        "to the first page of search results for these terms.",
    )

    excluded_phrases = models.CharField(
        blank=True,
        null=True,
        max_length=1000,
        help_text="A comma separated list of excluded keywords and phrases. "
        "Do not use quotes for phrases. The page will be removed "
        "from search results for these terms",
    )

    body_no_html = models.TextField(
        blank=True,
        null=True,
    )

    @property
    def preview_text(self):
        if self.body_no_html:
            parts = self.body_no_html.split(" ")
            return " ".join(parts[0:40])

        return None

    subpage_types = []

    search_fields = Page.search_fields + [
        index.SearchField(
            "search_title",
            partial_match=True,
            boost=2,
            es_extra={
                "search_analyzer": "stop_and_synonyms",
            },
        ),
        index.SearchField(
            "body_no_html",
            partial_match=True,
            es_extra={
                "search_analyzer": "stop_and_synonyms",
            },
        ),
        index.AutocompleteField("body_no_html"),
        index.AutocompleteField("search_title"),
        index.FilterField("slug"),
    ]

    content_panels = Page.content_panels + [
        StreamFieldPanel("body"),
    ]

    promote_panels = [
        FieldPanel("slug"),
        FieldPanel("show_in_menus"),
        FieldPanel("pinned_phrases"),
        FieldPanel("excluded_phrases"),
    ]

    def full_clean(self, *args, **kwargs):
        # Required so we can override
        # search analyzer (see above)
        self.search_title = self.title

        super().full_clean(*args, **kwargs)

    def save(self, *args, **kwargs):
        body_string = str(self.body)

        self.body_no_html = BeautifulSoup(body_string, "html.parser").text

        # Required so we can override
        # search analyzer (see above)
        # self.search_title = self.title #

        if self.id:
            manage_excluded(self, self.excluded_phrases)
            manage_pinned(self, self.pinned_phrases)

        return super().save(*args, **kwargs)