website/hub/services/models/base.py

183 lines
4.8 KiB
Python

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"
# 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))