from django.db import models from django.core.exceptions import ValidationError from django.utils.text import slugify from django_prose_editor.fields import ProseEditorField import mimetypes import xml.etree.ElementTree as ET # Centralized ProseEditorField configuration PROSE_EDITOR_CONFIG = { "extensions": { "Bold": True, "Italic": True, "Strike": True, "Underline": True, "HardBreak": True, "Heading": {"levels": [1, 2, 3, 4, 5, 6]}, "BulletList": True, "OrderedList": True, "Blockquote": True, "Link": True, "Table": True, "History": True, "HTML": True, "Typographic": True, }, "sanitize": True, } def get_prose_editor_field(**kwargs): """ Returns a ProseEditorField with the standard configuration. Additional kwargs can be passed to override or add field options. """ config = PROSE_EDITOR_CONFIG.copy() config.update(kwargs) return ProseEditorField(**config) def validate_image_size(value, mb=1): filesize = value.size if filesize > mb * 1024 * 1024: raise ValidationError(f"Maximum file size is {mb} MB") def validate_image_or_svg(value): """ Validate that the uploaded file is either a valid image or SVG file. """ # Check file size first validate_image_size(value) # Get the file extension and MIME type filename = value.name.lower() mime_type, _ = mimetypes.guess_type(filename) # List of allowed image formats allowed_image_types = [ "image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp", "image/tiff", "image/svg+xml", ] # Check if it's an SVG file if filename.endswith(".svg") or mime_type == "image/svg+xml": try: # Reset file pointer and read content value.seek(0) content = value.read() value.seek(0) # Reset for later use # Try to parse as XML to ensure it's valid SVG ET.fromstring(content) return # Valid SVG except ET.ParseError: raise ValidationError("Invalid SVG file format") # For non-SVG files, check MIME type if mime_type not in allowed_image_types: raise ValidationError( f"Unsupported file type. Allowed types: JPEG, PNG, GIF, WebP, BMP, TIFF, SVG" ) class Currency(models.TextChoices): CHF = "CHF", "Swiss Franc" EUR = "EUR", "Euro" USD = "USD", "US Dollar" class Term(models.TextChoices): MTH = "MTH", "per Month (30d)" DAY = "DAY", "per Day" HR = "HR", "per Hour" MIN = "MIN", "per Minute" class Unit(models.TextChoices): GIB = "GIB", "GiB" MIB = "MIB", "MiB" CPU = "CPU", "vCPU" class PartnerCategory(models.TextChoices): CONSULTING = "CONSULTING", "Consulting" TRAINING = "TRAINING", "Training" # This should be a relation, but for now this is good enough :TM: class ManagedServiceProvider(models.TextChoices): VS = "VS", "VSHN" class ReusableText(models.Model): name = models.CharField(max_length=100) textsnippet = models.ForeignKey( "self", on_delete=models.SET_NULL, null=True, blank=True, related_name="children", ) text = get_prose_editor_field() class Meta: ordering = ["name"] def __str__(self): return self.name def get_full_text(self): """Returns the text with all nested textsnippet content recursively included in reverse order""" text_parts = [] # Recursively collect all text parts def collect_text(snippet): if snippet is None: return collect_text(snippet.textsnippet) # Collect deepest snippets first text_parts.append(snippet.text) # Start collection with the deepest snippets collect_text(self.textsnippet) # Add the main text at the end text_parts.append(self.text) # Join all text parts return "".join(text_parts) class Category(models.Model): name = models.CharField(max_length=100) slug = models.SlugField(unique=True) parent = models.ForeignKey( "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children" ) description = models.TextField(blank=True) order = models.IntegerField(default=0) class Meta: verbose_name = "Service Category" verbose_name_plural = "Service Categories" ordering = ["order", "name"] def __str__(self): if self.parent: return f"{self.parent} > {self.name}" return self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super().save(*args, **kwargs) @property def full_path(self): path = [self.name] parent = self.parent while parent: path.append(parent.name) parent = parent.parent return " > ".join(reversed(path))