Source code for coalition.content.models.content_block

"""ContentBlock model for flexible homepage content sections."""

from typing import TYPE_CHECKING

from django.core.validators import MaxValueValidator
from django.db import models
from tinymce.models import HTMLField

from coalition.content.html_sanitizer import HTMLSanitizer

if TYPE_CHECKING:
    from typing import Any


[docs] class ContentBlock(models.Model): """ Flexible content blocks that can be added to any page. Allows for dynamic content sections beyond the fixed structure. """
[docs] BLOCK_TYPES = [ ("text", "Text Block"), ("image", "Image Block"), ("text_image", "Text + Image Block"), ("quote", "Quote Block"), ("stats", "Statistics Block"), ("custom_html", "Custom HTML Block"), ]
[docs] PAGE_TYPES = [ ("homepage", "Homepage"), ("about", "About Page"), ("campaigns", "Campaigns Page"), ("contact", "Contact Page"), ]
[docs] LAYOUT_OPTIONS = [ ("default", "Text Left, Image Right"), ("reversed", "Image Left, Text Right"), ("stacked", "Text Above Image"), ("stacked_reversed", "Image Above Text"), ]
[docs] VERTICAL_ALIGNMENT_OPTIONS = [ ("top", "Top"), ("middle", "Center"), ("bottom", "Bottom"), ]
[docs] ANIMATION_OPTIONS = [ ("fade-in", "Fade In"), ("slide-up", "Slide Up"), ("slide-left", "Slide from Left"), ("slide-right", "Slide from Right"), ("scale", "Scale In"), ("none", "No Animation"), ]
[docs] page_type = models.CharField( max_length=20, choices=PAGE_TYPES, default="homepage", help_text="Which page this content block appears on", )
[docs] title = models.CharField( max_length=200, blank=True, help_text="Optional title for this content block", )
[docs] block_type = models.CharField( max_length=20, choices=BLOCK_TYPES, default="text", help_text="Type of content block", )
[docs] content = HTMLField( help_text="Main content for this block (text, HTML, etc.)", )
[docs] image = models.ForeignKey( "content.Image", on_delete=models.SET_NULL, null=True, blank=True, related_name="content_block_images", help_text="Image for image or text+image blocks", )
# Layout options
[docs] layout_option = models.CharField( max_length=20, choices=LAYOUT_OPTIONS, default="default", blank=True, help_text="Layout arrangement for Text + Image blocks", )
[docs] vertical_alignment = models.CharField( max_length=10, choices=VERTICAL_ALIGNMENT_OPTIONS, default="middle", blank=True, help_text=( "Vertical alignment of text relative to image for Text + Image blocks" ), )
[docs] css_classes = models.CharField( max_length=200, blank=True, help_text="Additional CSS classes for styling (optional)", )
[docs] background_color = models.CharField( max_length=7, blank=True, help_text="Background color in hex format (e.g., #ffffff)", )
# Animation
[docs] animation_type = models.CharField( max_length=20, choices=ANIMATION_OPTIONS, default="none", help_text="Animation effect when block enters viewport", )
[docs] animation_delay = models.PositiveIntegerField( default=0, validators=[MaxValueValidator(2000)], help_text="Animation delay in milliseconds (0-2000)", )
# Ordering and visibility
[docs] order = models.PositiveIntegerField( default=0, help_text="Order in which this block appears (lower numbers first)", )
[docs] is_visible = models.BooleanField( default=True, help_text="Whether this block is visible on the homepage", )
[docs] created_at = models.DateTimeField( auto_now_add=True, help_text="When this content block was created", )
[docs] updated_at = models.DateTimeField( auto_now=True, help_text="When this content block was last updated", )
[docs] class Meta:
[docs] db_table = "content_block"
[docs] ordering = ["order", "created_at"]
[docs] verbose_name = "Content Block"
[docs] verbose_name_plural = "Content Blocks"
[docs] def __str__(self) -> str: page_type = self.get_page_type_display() return ( f"Block: {self.title or self.block_type} ({page_type}, Order: {self.order})" )
@property
[docs] def image_url(self) -> str: """Return the URL of the uploaded image, or empty string if no image.""" if self.image and self.image.image and hasattr(self.image.image, "url"): return self.image.image.url return ""
@property
[docs] def image_alt_text(self) -> str: """Return the alt text of the image, or empty string if no image.""" return self.image.alt_text if self.image else ""
@property
[docs] def image_title(self) -> str: """Return the title of the image, or empty string if no image.""" return self.image.title if self.image else ""
@property
[docs] def image_author(self) -> str: """Return the author of the image, or empty string if no image.""" return self.image.author if self.image else ""
@property
[docs] def image_license(self) -> str: """Return the license of the image, or empty string if no image.""" return self.image.license if self.image else ""
@property
[docs] def image_source_url(self) -> str: """Return the source URL of the image, or empty string if no image.""" return self.image.source_url if self.image else ""
@property
[docs] def image_caption(self) -> str: """Return the caption of the image, or empty string if no image.""" return self.image.caption if self.image else ""
@property
[docs] def image_caption_display(self) -> str: """Return the caption display setting of the image, or 'below' if no image.""" return self.image.caption_display if self.image else "below"
[docs] def save(self, *args: "Any", **kwargs: "Any") -> None: """Sanitize content based on block type before saving.""" if self.content: if self.block_type == "quote": # Quotes should be plain text only self.content = HTMLSanitizer.sanitize_plain_text(self.content) else: # All other block types get HTML sanitization self.content = HTMLSanitizer.sanitize(self.content) # Sanitize title (should be plain text) if self.title: self.title = HTMLSanitizer.sanitize_plain_text(self.title) super().save(*args, **kwargs)