Add newsletter template editor
This commit is contained in:
parent
4303c1f5db
commit
95601170f6
@ -1,5 +1,7 @@
|
||||
from django import forms
|
||||
from .models import ContactFormSubmission
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from .models import ContactFormSubmission, NewsletterTemplate
|
||||
|
||||
|
||||
class NewsletterSubscribeForm(forms.Form):
|
||||
@ -27,3 +29,52 @@ class ContactForm(forms.ModelForm):
|
||||
widgets = {
|
||||
"source_page": forms.HiddenInput(),
|
||||
}
|
||||
|
||||
|
||||
class GrapesJSEditorWidget(forms.Textarea):
|
||||
class Media:
|
||||
js = (
|
||||
"js/newsletter_template_editor.js",
|
||||
)
|
||||
css = {
|
||||
"all": (
|
||||
"css/newsletter_template_editor.css",
|
||||
)
|
||||
}
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
attrs = attrs or {}
|
||||
attrs["data-newsletter-html-input"] = "1"
|
||||
textarea = super().render(name, value, attrs, renderer)
|
||||
editor_shell = """
|
||||
<div class="newsletter-editor-shell">
|
||||
<div class="help warning">
|
||||
GrapesJS editor will load from local static assets:
|
||||
<code>/static/vendor/grapesjs/grapes.min.js</code> and
|
||||
<code>/static/vendor/grapesjs-preset-newsletter/grapesjs-preset-newsletter.min.js</code>.
|
||||
If missing, fallback textarea remains available.
|
||||
</div>
|
||||
<div class="newsletter-editor-toolbar">
|
||||
<button type="button" class="button button-small button-secondary" data-newsletter-load-editor>
|
||||
Load visual editor
|
||||
</button>
|
||||
</div>
|
||||
<div id="newsletter-grapesjs-editor" class="newsletter-grapesjs-editor" hidden></div>
|
||||
</div>
|
||||
"""
|
||||
return mark_safe(f"{editor_shell}{textarea}")
|
||||
|
||||
|
||||
class NewsletterTemplateAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = NewsletterTemplate
|
||||
fields = ["name", "subject", "template_json", "template_html"]
|
||||
widgets = {
|
||||
"template_json": forms.HiddenInput(attrs={"data-newsletter-json-input": "1"}),
|
||||
"template_html": GrapesJSEditorWidget(
|
||||
attrs={
|
||||
"rows": 18,
|
||||
"placeholder": "Use {{email_body}} as the content placeholder.",
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
28
innovedus_cms/base/migrations/0010_newslettertemplate.py
Normal file
28
innovedus_cms/base/migrations/0010_newslettertemplate.py
Normal file
@ -0,0 +1,28 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("base", "0009_alter_newslettercampaign_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="NewsletterTemplate",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("name", models.CharField(max_length=255, verbose_name="Name")),
|
||||
("subject", models.CharField(blank=True, default="", max_length=255, verbose_name="Subject")),
|
||||
("template_json", models.JSONField(blank=True, default=dict, verbose_name="Template JSON")),
|
||||
("template_html", models.TextField(blank=True, default="", verbose_name="Template HTML")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
|
||||
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Newsletter Template",
|
||||
"verbose_name_plural": "Newsletter Templates",
|
||||
"ordering": ["-updated_at", "-created_at"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -474,6 +474,30 @@ class NewsletterCampaign(models.Model):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class NewsletterTemplate(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
||||
subject = models.CharField(max_length=255, blank=True, default="", verbose_name=_("Subject"))
|
||||
template_json = models.JSONField(default=dict, blank=True, verbose_name=_("Template JSON"))
|
||||
template_html = models.TextField(blank=True, default="", verbose_name=_("Template HTML"))
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
||||
|
||||
panels = [
|
||||
FieldPanel("name"),
|
||||
FieldPanel("subject"),
|
||||
FieldPanel("template_json"),
|
||||
FieldPanel("template_html"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
ordering = ["-updated_at", "-created_at"]
|
||||
verbose_name = _("Newsletter Template")
|
||||
verbose_name_plural = _("Newsletter Templates")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class NewsletterDispatchRecord(models.Model):
|
||||
campaign = models.ForeignKey(
|
||||
NewsletterCampaign,
|
||||
|
||||
18
innovedus_cms/base/static/css/newsletter_template_editor.css
Normal file
18
innovedus_cms/base/static/css/newsletter_template_editor.css
Normal file
@ -0,0 +1,18 @@
|
||||
.newsletter-editor-shell {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.newsletter-editor-toolbar {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.newsletter-grapesjs-editor {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
textarea[data-newsletter-html-input="1"] {
|
||||
margin-top: 10px;
|
||||
}
|
||||
130
innovedus_cms/base/static/js/newsletter_template_editor.js
Normal file
130
innovedus_cms/base/static/js/newsletter_template_editor.js
Normal file
@ -0,0 +1,130 @@
|
||||
(function () {
|
||||
function loadScript(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureGrapesJS() {
|
||||
if (window.grapesjs) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
await loadScript('/static/vendor/grapesjs/grapes.min.js');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await loadScript('/static/vendor/grapesjs-preset-newsletter/grapesjs-preset-newsletter.min.js');
|
||||
} catch (e) {
|
||||
// Plugin is optional; core editor still works.
|
||||
}
|
||||
|
||||
return !!window.grapesjs;
|
||||
}
|
||||
|
||||
function parseJSON(value, fallback) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function getHtmlWithCss(editor) {
|
||||
var html = editor.getHtml() || '';
|
||||
var css = editor.getCss() || '';
|
||||
if (!css.trim()) {
|
||||
return html;
|
||||
}
|
||||
return '<style>' + css + '</style>\n' + html;
|
||||
}
|
||||
|
||||
function bootEditor() {
|
||||
var htmlInput = document.querySelector('[data-newsletter-html-input="1"]');
|
||||
var jsonInput = document.querySelector('[data-newsletter-json-input="1"]');
|
||||
var container = document.getElementById('newsletter-grapesjs-editor');
|
||||
var loadButton = document.querySelector('[data-newsletter-load-editor]');
|
||||
|
||||
if (!htmlInput || !jsonInput || !container || !loadButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
var initialized = false;
|
||||
|
||||
loadButton.addEventListener('click', async function () {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadButton.disabled = true;
|
||||
loadButton.textContent = 'Loading...';
|
||||
|
||||
var ok = await ensureGrapesJS();
|
||||
if (!ok) {
|
||||
loadButton.textContent = 'GrapesJS static files not found';
|
||||
return;
|
||||
}
|
||||
|
||||
container.hidden = false;
|
||||
container.innerHTML = '';
|
||||
|
||||
var plugins = [];
|
||||
if (window['grapesjs-preset-newsletter']) {
|
||||
plugins.push('grapesjs-preset-newsletter');
|
||||
}
|
||||
|
||||
var editor = window.grapesjs.init({
|
||||
container: '#newsletter-grapesjs-editor',
|
||||
height: '620px',
|
||||
storageManager: false,
|
||||
fromElement: false,
|
||||
plugins: plugins,
|
||||
});
|
||||
|
||||
var savedProject = parseJSON(jsonInput.value || '{}', {});
|
||||
if (savedProject && Object.keys(savedProject).length > 0) {
|
||||
try {
|
||||
editor.loadProjectData(savedProject);
|
||||
} catch (e) {
|
||||
if ((htmlInput.value || '').trim()) {
|
||||
editor.setComponents(htmlInput.value);
|
||||
}
|
||||
}
|
||||
} else if ((htmlInput.value || '').trim()) {
|
||||
editor.setComponents(htmlInput.value);
|
||||
} else {
|
||||
editor.setComponents('<table width="100%" cellpadding="0" cellspacing="0" role="presentation"><tr><td>{{email_body}}</td></tr></table>');
|
||||
}
|
||||
|
||||
var form = htmlInput.closest('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function () {
|
||||
try {
|
||||
var projectData = editor.getProjectData();
|
||||
jsonInput.value = JSON.stringify(projectData);
|
||||
} catch (e) {
|
||||
jsonInput.value = '{}';
|
||||
}
|
||||
htmlInput.value = getHtmlWithCss(editor);
|
||||
});
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
loadButton.textContent = 'Visual editor loaded';
|
||||
loadButton.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bootEditor);
|
||||
} else {
|
||||
bootEditor();
|
||||
}
|
||||
})();
|
||||
@ -8,7 +8,8 @@ from wagtail.permission_policies import ModelPermissionPolicy
|
||||
from wagtail.snippets.models import register_snippet
|
||||
from wagtail.snippets.views.snippets import CreateView, SnippetViewSet
|
||||
|
||||
from .models import NewsletterCampaign, NewsletterDispatchRecord, NewsletterSystemSettings
|
||||
from .forms import NewsletterTemplateAdminForm
|
||||
from .models import NewsletterCampaign, NewsletterDispatchRecord, NewsletterSystemSettings, NewsletterTemplate
|
||||
|
||||
SEND_NEWSLETTER_PERMISSION = "base.send_newslettercampaign"
|
||||
|
||||
@ -83,8 +84,33 @@ class NewsletterDispatchRecordViewSet(SnippetViewSet):
|
||||
search_fields = ["email", "subscriber_id", "campaign__title"]
|
||||
|
||||
|
||||
class NewsletterTemplateViewSet(SnippetViewSet):
|
||||
model = NewsletterTemplate
|
||||
icon = "doc-full-inverse"
|
||||
menu_label = _("Newsletter templates")
|
||||
menu_order = 252
|
||||
add_to_admin_menu = True
|
||||
form_class = NewsletterTemplateAdminForm
|
||||
base_form_class = NewsletterTemplateAdminForm
|
||||
list_display = ["name", "subject", "updated_at", "created_at"]
|
||||
search_fields = ["name", "subject"]
|
||||
panels = [
|
||||
FieldPanel("name"),
|
||||
FieldPanel("subject"),
|
||||
FieldPanel(
|
||||
"template_json",
|
||||
help_text=_("Stored as editor state (hidden in form)."),
|
||||
),
|
||||
FieldPanel(
|
||||
"template_html",
|
||||
help_text=_("Use {{email_body}} as content placeholder."),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
register_snippet(NewsletterCampaignViewSet)
|
||||
register_snippet(NewsletterDispatchRecordViewSet)
|
||||
register_snippet(NewsletterTemplateViewSet)
|
||||
|
||||
|
||||
@hooks.register("register_snippet_listing_buttons")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user