rework prose editor configuration

This commit is contained in:
Tobias Brunner 2025-07-08 16:24:28 +02:00
parent 6b689704b0
commit 166f518db0
No known key found for this signature in database
5 changed files with 46 additions and 15 deletions

View file

@ -2,9 +2,8 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django_prose_editor.fields import ProseEditorField
from django.utils import timezone 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 .services import Service
from .providers import CloudProvider, ConsultingPartner from .providers import CloudProvider, ConsultingPartner
from .images import ImageReference from .images import ImageReference
@ -16,7 +15,7 @@ class Article(ImageReference):
excerpt = models.TextField( excerpt = models.TextField(
max_length=500, help_text="Brief description of the article" max_length=500, help_text="Brief description of the article"
) )
content = ProseEditorField() content = get_prose_editor_field()
meta_keywords = models.CharField( meta_keywords = models.CharField(
max_length=255, blank=True, help_text="SEO keywords separated by commas" max_length=255, blank=True, help_text="SEO keywords separated by commas"
) )
@ -57,7 +56,7 @@ class Article(ImageReference):
blank=True, blank=True,
null=True, null=True,
validators=[validate_image_size], 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 # Publishing controls

View file

@ -6,6 +6,38 @@ import mimetypes
import xml.etree.ElementTree as ET 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): def validate_image_size(value, mb=1):
filesize = value.size filesize = value.size
if filesize > mb * 1024 * 1024: if filesize > mb * 1024 * 1024:
@ -88,7 +120,7 @@ class ReusableText(models.Model):
blank=True, blank=True,
related_name="children", related_name="children",
) )
text = ProseEditorField() text = get_prose_editor_field()
class Meta: class Meta:
ordering = ["name"] ordering = ["name"]

View file

@ -1,10 +1,10 @@
from django.db import models from django.db import models
from django_prose_editor.fields import ProseEditorField from .base import get_prose_editor_field
class WebsiteFaq(models.Model): class WebsiteFaq(models.Model):
question = models.CharField(max_length=200) question = models.CharField(max_length=200)
answer = ProseEditorField() answer = get_prose_editor_field()
order = models.IntegerField(default=0) order = models.IntegerField(default=0)
class Meta: class Meta:

View file

@ -1,16 +1,15 @@
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify 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 from .images import ImageReference
class CloudProvider(ImageReference): class CloudProvider(ImageReference):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
description = ProseEditorField() description = get_prose_editor_field()
website = models.URLField() website = models.URLField()
linkedin = models.URLField(blank=True) linkedin = models.URLField(blank=True)
phone = models.CharField(max_length=25, blank=True, null=True) phone = models.CharField(max_length=25, blank=True, null=True)
@ -45,7 +44,7 @@ class CloudProvider(ImageReference):
class ConsultingPartner(ImageReference): class ConsultingPartner(ImageReference):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
description = ProseEditorField() description = get_prose_editor_field()
website = models.URLField(blank=True) website = models.URLField(blank=True)
linkedin = models.URLField(blank=True) linkedin = models.URLField(blank=True)
phone = models.CharField(max_length=25, blank=True, null=True) phone = models.CharField(max_length=25, blank=True, null=True)

View file

@ -11,6 +11,7 @@ from .base import (
ManagedServiceProvider, ManagedServiceProvider,
validate_image_size, validate_image_size,
Currency, Currency,
get_prose_editor_field,
) )
from .providers import CloudProvider from .providers import CloudProvider
from .images import ImageReference from .images import ImageReference
@ -19,10 +20,10 @@ from .images import ImageReference
class Service(ImageReference): class Service(ImageReference):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
slug = models.SlugField(max_length=250, unique=True) 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) tagline = models.TextField(max_length=500, blank=True, null=True)
categories = models.ManyToManyField(Category, related_name="services") categories = models.ManyToManyField(Category, related_name="services")
features = ProseEditorField() features = get_prose_editor_field()
is_featured = models.BooleanField(default=False) is_featured = models.BooleanField(default=False)
is_coming_soon = models.BooleanField(default=False) is_coming_soon = models.BooleanField(default=False)
disable_listing = models.BooleanField(default=False) disable_listing = models.BooleanField(default=False)
@ -74,7 +75,7 @@ class ServiceOffering(models.Model):
cloud_provider = models.ForeignKey( cloud_provider = models.ForeignKey(
CloudProvider, on_delete=models.CASCADE, related_name="offerings" 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( offer_description = models.ForeignKey(
ReusableText, ReusableText,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -129,7 +130,7 @@ class PlanPrice(models.Model):
class Plan(models.Model): class Plan(models.Model):
name = models.CharField(max_length=100) 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( plan_description = models.ForeignKey(
ReusableText, ReusableText,
on_delete=models.PROTECT, on_delete=models.PROTECT,