Add newsletter template editor
This commit is contained in:
parent
4303c1f5db
commit
95601170f6
@ -1,5 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import ContactFormSubmission
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from .models import ContactFormSubmission, NewsletterTemplate
|
||||||
|
|
||||||
|
|
||||||
class NewsletterSubscribeForm(forms.Form):
|
class NewsletterSubscribeForm(forms.Form):
|
||||||
@ -27,3 +29,52 @@ class ContactForm(forms.ModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
"source_page": forms.HiddenInput(),
|
"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)
|
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):
|
class NewsletterDispatchRecord(models.Model):
|
||||||
campaign = models.ForeignKey(
|
campaign = models.ForeignKey(
|
||||||
NewsletterCampaign,
|
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.models import register_snippet
|
||||||
from wagtail.snippets.views.snippets import CreateView, SnippetViewSet
|
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"
|
SEND_NEWSLETTER_PERMISSION = "base.send_newslettercampaign"
|
||||||
|
|
||||||
@ -83,8 +84,33 @@ class NewsletterDispatchRecordViewSet(SnippetViewSet):
|
|||||||
search_fields = ["email", "subscriber_id", "campaign__title"]
|
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(NewsletterCampaignViewSet)
|
||||||
register_snippet(NewsletterDispatchRecordViewSet)
|
register_snippet(NewsletterDispatchRecordViewSet)
|
||||||
|
register_snippet(NewsletterTemplateViewSet)
|
||||||
|
|
||||||
|
|
||||||
@hooks.register("register_snippet_listing_buttons")
|
@hooks.register("register_snippet_listing_buttons")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user