From 166f518db009a0cfb26678f72fa6a3c5d428e8c2 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 8 Jul 2025 16:24:28 +0200 Subject: [PATCH] rework prose editor configuration --- hub/services/models/articles.py | 7 +++---- hub/services/models/base.py | 34 +++++++++++++++++++++++++++++++- hub/services/models/content.py | 4 ++-- hub/services/models/providers.py | 7 +++---- hub/services/models/services.py | 9 +++++---- 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/hub/services/models/articles.py b/hub/services/models/articles.py index aafba9b..627bea0 100644 --- a/hub/services/models/articles.py +++ b/hub/services/models/articles.py @@ -2,9 +2,8 @@ from django.db import models from django.urls import reverse from django.utils.text import slugify from django.contrib.auth.models import User -from django_prose_editor.fields import ProseEditorField from django.utils import timezone -from .base import validate_image_size +from .base import validate_image_size, get_prose_editor_field from .services import Service from .providers import CloudProvider, ConsultingPartner from .images import ImageReference @@ -16,7 +15,7 @@ class Article(ImageReference): excerpt = models.TextField( max_length=500, help_text="Brief description of the article" ) - content = ProseEditorField() + content = get_prose_editor_field() meta_keywords = models.CharField( max_length=255, blank=True, help_text="SEO keywords separated by commas" ) @@ -57,7 +56,7 @@ class Article(ImageReference): blank=True, null=True, validators=[validate_image_size], - help_text="Optional Open Graph image for social sharing (max 1MB). If not provided, the article's main image will be used." + help_text="Optional Open Graph image for social sharing (max 1MB). If not provided, the article's main image will be used.", ) # Publishing controls diff --git a/hub/services/models/base.py b/hub/services/models/base.py index db365e7..6a4ac7d 100644 --- a/hub/services/models/base.py +++ b/hub/services/models/base.py @@ -6,6 +6,38 @@ 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: @@ -88,7 +120,7 @@ class ReusableText(models.Model): blank=True, related_name="children", ) - text = ProseEditorField() + text = get_prose_editor_field() class Meta: ordering = ["name"] diff --git a/hub/services/models/content.py b/hub/services/models/content.py index 6f4eba1..a2f1d7b 100644 --- a/hub/services/models/content.py +++ b/hub/services/models/content.py @@ -1,10 +1,10 @@ from django.db import models -from django_prose_editor.fields import ProseEditorField +from .base import get_prose_editor_field class WebsiteFaq(models.Model): question = models.CharField(max_length=200) - answer = ProseEditorField() + answer = get_prose_editor_field() order = models.IntegerField(default=0) class Meta: diff --git a/hub/services/models/providers.py b/hub/services/models/providers.py index 1a2c518..e7a33b9 100644 --- a/hub/services/models/providers.py +++ b/hub/services/models/providers.py @@ -1,16 +1,15 @@ from django.db import models from django.urls import reverse from django.utils.text import slugify -from django_prose_editor.fields import ProseEditorField -from .base import validate_image_size +from .base import validate_image_size, get_prose_editor_field from .images import ImageReference class CloudProvider(ImageReference): name = models.CharField(max_length=100) slug = models.SlugField(unique=True) - description = ProseEditorField() + description = get_prose_editor_field() website = models.URLField() linkedin = models.URLField(blank=True) phone = models.CharField(max_length=25, blank=True, null=True) @@ -45,7 +44,7 @@ class CloudProvider(ImageReference): class ConsultingPartner(ImageReference): name = models.CharField(max_length=200) slug = models.SlugField(unique=True) - description = ProseEditorField() + description = get_prose_editor_field() website = models.URLField(blank=True) linkedin = models.URLField(blank=True) phone = models.CharField(max_length=25, blank=True, null=True) diff --git a/hub/services/models/services.py b/hub/services/models/services.py index f7c98af..a884949 100644 --- a/hub/services/models/services.py +++ b/hub/services/models/services.py @@ -11,6 +11,7 @@ from .base import ( ManagedServiceProvider, validate_image_size, Currency, + get_prose_editor_field, ) from .providers import CloudProvider from .images import ImageReference @@ -19,10 +20,10 @@ from .images import ImageReference class Service(ImageReference): name = models.CharField(max_length=200) slug = models.SlugField(max_length=250, unique=True) - description = ProseEditorField() + description = get_prose_editor_field() tagline = models.TextField(max_length=500, blank=True, null=True) categories = models.ManyToManyField(Category, related_name="services") - features = ProseEditorField() + features = get_prose_editor_field() is_featured = models.BooleanField(default=False) is_coming_soon = models.BooleanField(default=False) disable_listing = models.BooleanField(default=False) @@ -74,7 +75,7 @@ class ServiceOffering(models.Model): cloud_provider = models.ForeignKey( CloudProvider, on_delete=models.CASCADE, related_name="offerings" ) - description = ProseEditorField(blank=True, null=True) + description = get_prose_editor_field(blank=True, null=True) offer_description = models.ForeignKey( ReusableText, on_delete=models.PROTECT, @@ -129,7 +130,7 @@ class PlanPrice(models.Model): class Plan(models.Model): name = models.CharField(max_length=100) - description = ProseEditorField(blank=True, null=True) + description = get_prose_editor_field(blank=True, null=True) plan_description = models.ForeignKey( ReusableText, on_delete=models.PROTECT,