From e6fc91ee81e73d6bb8841ec8deab326809dd225f Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 08:23:47 +0200 Subject: [PATCH 001/189] add fields (choose email template type/name) into the 'MassCommunication' model --- .../migrations/0048_auto_20191014_0818.py | 38 +++++++++++++++++++ apps/aklub/models.py | 33 ++++++++++++++++ .../0011_auto_20191014_0818.py | 22 +++++++++++ .../0010_auto_20191014_0818.py | 27 +++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 apps/aklub/migrations/0048_auto_20191014_0818.py create mode 100644 local_migrations/migrations_admin/0011_auto_20191014_0818.py create mode 100644 local_migrations/migrations_advanced_filters/0010_auto_20191014_0818.py diff --git a/apps/aklub/migrations/0048_auto_20191014_0818.py b/apps/aklub/migrations/0048_auto_20191014_0818.py new file mode 100644 index 000000000..d6d2b7f00 --- /dev/null +++ b/apps/aklub/migrations/0048_auto_20191014_0818.py @@ -0,0 +1,38 @@ +# Generated by Django 2.2.6 on 2019-10-14 06:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0047_auto_20190919_1514'), + ] + + operations = [ + migrations.AddField( + model_name='masscommunication', + name='template_en_name', + field=models.CharField(blank=True, max_length=350, null=True, verbose_name='Choose template'), + ), + migrations.AddField( + model_name='masscommunication', + name='template_en_type', + field=models.CharField(blank=True, choices=[('new', 'Create new email body'), ('existed', 'Choose email body template')], default='new', max_length=350, verbose_name='Choose how to create an email body'), + ), + migrations.AddField( + model_name='masscommunication', + name='template_name', + field=models.CharField(blank=True, max_length=350, null=True, verbose_name='Choose template'), + ), + migrations.AddField( + model_name='masscommunication', + name='template_type', + field=models.CharField(blank=True, choices=[('new', 'Create new email body'), ('existed', 'Choose email body template')], default='new', max_length=350, verbose_name='Choose how to create an email body'), + ), + migrations.AlterField( + model_name='accountstatements', + name='type', + field=models.CharField(choices=[('account', 'Account statement - Fio Banka'), ('account_cs', 'Account statement - Česká spořitelna'), ('account_kb', 'Account statement - Komerční Banka'), ('account_csob', 'Account statement - ČSOB'), ('account_sberbank', 'Account statement - Sberbank'), ('darujme', 'Darujme.cz')], max_length=20), + ), + ] diff --git a/apps/aklub/models.py b/apps/aklub/models.py index cd7e5122c..d753f7466 100644 --- a/apps/aklub/models.py +++ b/apps/aklub/models.py @@ -2644,6 +2644,11 @@ class Meta: verbose_name = _("Mass Communication") verbose_name_plural = _("Mass Communications") + EMAIL_BODY_CREATION_CHOICES = ( + ("new", _("Create new email body")), + ('existed', _("Choose email body template")), + ) + name = models.CharField( verbose_name=_("Name"), max_length=50, @@ -2680,6 +2685,20 @@ class Meta: null=True, validators=[gender_strings_validator, variable_validator], ) + template_type = models.CharField( + verbose_name=_("Choose how to create an email body"), + max_length=350, + null=False, + blank=True, + default="new", + choices=EMAIL_BODY_CREATION_CHOICES, + ) + template_name = models.CharField( + verbose_name=_("Choose template"), + max_length=350, + null=True, + blank=True, + ) template = models.TextField( verbose_name=_("Template"), help_text=_("Template can contain following variable substitutions:
") + ("{mr|mrs} or {mr/mrs}, $" + ", $" @@ -2689,6 +2708,20 @@ class Meta: null=True, validators=[gender_strings_validator, variable_validator], ) + template_en_type = models.CharField( + verbose_name=_("Choose how to create an email body"), + max_length=350, + null=False, + blank=True, + default="new", + choices=EMAIL_BODY_CREATION_CHOICES, + ) + template_en_name = models.CharField( + verbose_name=_("Choose template"), + max_length=350, + null=True, + blank=True, + ) template_en = models.TextField( verbose_name=_("English template"), max_length=50000, diff --git a/local_migrations/migrations_admin/0011_auto_20191014_0818.py b/local_migrations/migrations_admin/0011_auto_20191014_0818.py new file mode 100644 index 000000000..71a896850 --- /dev/null +++ b/local_migrations/migrations_admin/0011_auto_20191014_0818.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.6 on 2019-10-14 06:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +from aklub.migrations.data_migration.old_user_profile_model_hack import Settings as settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0010_auto_20190912_1254'), + ] + + operations = [ + migrations.AlterField( + model_name='logentry', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user'), + ), + ] diff --git a/local_migrations/migrations_advanced_filters/0010_auto_20191014_0818.py b/local_migrations/migrations_advanced_filters/0010_auto_20191014_0818.py new file mode 100644 index 000000000..97b828b99 --- /dev/null +++ b/local_migrations/migrations_advanced_filters/0010_auto_20191014_0818.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.6 on 2019-10-14 06:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +from aklub.migrations.data_migration.old_user_profile_model_hack import Settings as settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('advanced_filters', '0009_auto_20190912_1254'), + ] + + operations = [ + migrations.AlterField( + model_name='advancedfilter', + name='created_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_advanced_filters', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='advancedfilter', + name='users', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Users'), + ), + ] From de7244c7a20ef0e0094fd2550a200414e5423d72 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 08:52:33 +0200 Subject: [PATCH 002/189] set load html template form field widget for the 'template'/'template_en' form field in the 'MassCommunication' form --- apps/aklub/admin.py | 52 ++++++++++++++++++- apps/aklub/custom_form_widgets.py | 20 +++++++ .../form_field_html_template_widget.html | 3 ++ ...form_field_html_template_widget_attrs.html | 1 + apps/aklub/utils.py | 15 ++++++ 5 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 apps/aklub/custom_form_widgets.py create mode 100644 apps/aklub/templates/form_field_html_template_widget.html create mode 100644 apps/aklub/templates/form_field_html_template_widget_attrs.html diff --git a/apps/aklub/admin.py b/apps/aklub/admin.py index b4c28ce3a..7b8522c04 100644 --- a/apps/aklub/admin.py +++ b/apps/aklub/admin.py @@ -80,6 +80,7 @@ from . import darujme, filters, mailing, tasks +from .custom_form_widgets import HtmlTemplateWidget from .filters import ProfileTypeFilter, unit_admin_mixin_generator from .forms import ( CompanyProfileAddForm, CompanyProfileChangeForm, EventForm, TaxConfirmationForm, UnitUserProfileAddForm, @@ -96,7 +97,10 @@ ProfileModelResource, get_polymorphic_parent_child_fields, ) from .profile_model_resources_mixin import ProfileModelResourceMixin -from .utils import check_annotate_filters, edit_donor_annotate_filter, sweet_text +from .utils import ( + check_annotate_filters, edit_donor_annotate_filter, + get_email_templates_names, sweet_text, +) def admin_links(args_generator): @@ -1536,9 +1540,55 @@ def save_form(self, request, form, change): class MassCommunicationForm(forms.ModelForm): + hidden_template = forms.CharField(widget=forms.HiddenInput(), required=False) + hidden_template_en = forms.CharField(widget=forms.HiddenInput(), required=False) + template_textarea = forms.CharField(widget=forms.Textarea(), required=False) + template_en_textarea = forms.CharField(widget=forms.Textarea(), required=False) + class Meta: model = MassCommunication fields = '__all__' + widgets = { + 'template': HtmlTemplateWidget( + attrs={ + 'class': 'template', + }, + ), + 'template_en': HtmlTemplateWidget( + attrs={ + 'class': 'template', + }, + ), + } + + class Media: + js = ( + 'jquery/dist/jquery.min.js', + 'jquery-ui/jquery-ui.min.js', + 'jquery.inlineStyler/jquery.inlineStyler.min.js', + 'webui-popover/dist/jquery.webui-popover.min.js', + 'aklub/js/csrf_token.js', + 'aklub/js/html_template.js', + 'aklub/js/masscommunication_init.js', + ) + css = { + 'all': ( + 'jquery-ui/themes/base/jquery-ui.min.css', + 'webui-popover/dist/jquery.webui-popover.min.css', + 'aklub/css/template.css', + ) + } + + def __init__(self, *args, **kwargs): + super(MassCommunicationForm, self).__init__(*args, **kwargs) + self.fields['template_name'] = forms.ChoiceField( + choices=get_email_templates_names(), + required=False + ) + self.fields['template_en_name'] = forms.ChoiceField( + choices=get_email_templates_names(), + required=False + ) def clean_send_to_users(self): v = EmailValidator() diff --git a/apps/aklub/custom_form_widgets.py b/apps/aklub/custom_form_widgets.py new file mode 100644 index 000000000..8489f3548 --- /dev/null +++ b/apps/aklub/custom_form_widgets.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from django.forms.widgets import Widget +from django.template import loader +from django.utils.safestring import mark_safe + + +class HtmlTemplateWidget(Widget): + template_name = "form_field_html_template_widget.html" + + def __init__(self, *args, **kwargs): + if kwargs.get('urls'): + self.urls = kwargs.pop('urls') + super().__init__(*args, **kwargs) + + def render(self, name, value, attrs=None, renderer=None, **kwargs): + context = super().get_context(name, value, attrs) + template = loader.get_template(self.template_name).render(context) + + return mark_safe(template) diff --git a/apps/aklub/templates/form_field_html_template_widget.html b/apps/aklub/templates/form_field_html_template_widget.html new file mode 100644 index 000000000..5a47b7529 --- /dev/null +++ b/apps/aklub/templates/form_field_html_template_widget.html @@ -0,0 +1,3 @@ +
+ {% if widget.value %}{{ widget.value|safe }}{% endif %} +
diff --git a/apps/aklub/templates/form_field_html_template_widget_attrs.html b/apps/aklub/templates/form_field_html_template_widget_attrs.html new file mode 100644 index 000000000..c52de9eea --- /dev/null +++ b/apps/aklub/templates/form_field_html_template_widget_attrs.html @@ -0,0 +1 @@ +{% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %} diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index a1c406e48..7cabdf203 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- import datetime +import os +import pathlib from django.contrib import messages from django.db import models @@ -120,3 +122,16 @@ def get_admin_url(self): 'admin:%s_%s_change' % (self._meta.app_label, self._meta.model_name), args=[self.id], ) + + +def get_email_templates_names(): + """ Get email templates names """ + template_type = [ + (template, template) for template in [ + template.split('.')[0] for template in os.listdir( + pathlib.PurePath(__file__).parents[0] / 'templates' / 'email_templates') + if template not in ['base.html'] + ] + ] + template_type.insert(0, ("", "---------")) + return template_type From 3d3e58fc4346ab283513a03047e3fc044d19b415 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 09:03:20 +0200 Subject: [PATCH 003/189] add 'ContentTools', 'inline.Styler' (css inline style), 'webui-popover' js library" --- project/settings/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/project/settings/base.py b/project/settings/base.py index 744b4dad1..dc1a2c5f9 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -289,6 +289,7 @@ def normpath(*args): 'jquery-ui#~1.12.1', 'd3#3.3.13', 'nvd3#1.7.1', + 'ContentTools#1.6.10', ) AUTH_PASSWORD_VALIDATORS = [ From 9e34b9ef0cb23d4dfda689307e73029cff6891db Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 09:11:06 +0200 Subject: [PATCH 004/189] change 'MassCommunication' media js module name --- apps/aklub/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/aklub/admin.py b/apps/aklub/admin.py index 7b8522c04..5921d4241 100644 --- a/apps/aklub/admin.py +++ b/apps/aklub/admin.py @@ -1569,7 +1569,7 @@ class Media: 'webui-popover/dist/jquery.webui-popover.min.js', 'aklub/js/csrf_token.js', 'aklub/js/html_template.js', - 'aklub/js/masscommunication_init.js', + 'aklub/js/form_field_widget_init.js', ) css = { 'all': ( From 5fa0456947bb9108dcf6223400a1bad694a8cbe8 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 09:15:28 +0200 Subject: [PATCH 005/189] add js modules handle 'ContentTools' editor initialization --- .../static/aklub/js/edit_html_template.js | 54 +++++ apps/aklub/static/aklub/js/editor.js | 65 ++++++ apps/aklub/static/aklub/js/image_uploader.js | 199 ++++++++++++++++++ 3 files changed, 318 insertions(+) create mode 100644 apps/aklub/static/aklub/js/edit_html_template.js create mode 100644 apps/aklub/static/aklub/js/editor.js create mode 100644 apps/aklub/static/aklub/js/image_uploader.js diff --git a/apps/aklub/static/aklub/js/edit_html_template.js b/apps/aklub/static/aklub/js/edit_html_template.js new file mode 100644 index 000000000..589c3f4d0 --- /dev/null +++ b/apps/aklub/static/aklub/js/edit_html_template.js @@ -0,0 +1,54 @@ +var csrftoken = getCookie('csrftoken') + +var editHtmlTemplate = {} + +editHtmlTemplate.domain = window.origin + +editHtmlTemplate.call = function (type, url, data, auth, onSuccess, onError) { + xhr = new XMLHttpRequest() + xhr.addEventListener('readystatechange', onSuccess) + + if (type == null) { + type = 'get' + } + if (url == null) { + url = '/' + } + if (data == null) { + data = null + } + if (auth == null) { + auth = true + } + if (onSuccess == null) { + onSuccess = null + } + if (onError == null) { + onError = null + } + url = '' + this.domain + url + switch (type) { + case 'get': + xhr.open('GET', url) + break + case 'post': + xhr.open('POST', url) + break + case 'put': + xhr.open('PUT', url) + break + case 'patch': + xhr.open('PATH', url) + break + case 'delete': + xhr.open('DELETE', url) + break + default: + console.log("Request type " + type + " is not supported"); + } + xhr.setRequestHeader('X-CSRFToken', csrftoken) + + if (data) { + xhr.send(data) + } +} \ No newline at end of file diff --git a/apps/aklub/static/aklub/js/editor.js b/apps/aklub/static/aklub/js/editor.js new file mode 100644 index 000000000..94f31ffb4 --- /dev/null +++ b/apps/aklub/static/aklub/js/editor.js @@ -0,0 +1,65 @@ +(function () { + window.onload = function () { + var editor + ContentTools.StylePalette.add([ + new ContentTools.Style('By-line', 'article__by-line', ['p']), + new ContentTools.Style('Caption', 'article__caption', ['p']), + new ContentTools.Style('Example', 'example', ['pre']), + new ContentTools.Style('Example + Good', 'example--good', ['pre']), + new ContentTools.Style('Example + Bad', 'example--bad', ['pre'])]) + + editor = ContentTools.EditorApp.get() + editor.init('.editable', 'data-name') + + function getImages () { + // Return an object containing image URLs and widths for all regions + var descendants, i, images + + images = {} + for (name in editor.regions) { + // Search each region for images + descendants = editor.regions[name].descendants() + for (i = 0; i < descendants.length; i++) { + // Filter out elements that are not images + if (descendants[i].constructor.name !== 'Image') { + continue + } + images[descendants[i].attr('src')] = descendants[i].size()[0] + } + } + + return images + } + + editor.addEventListener('saved', function (ev) { + var onStateChange, payload, xhr + // Collect the contents of each region into a FormData instance + payload = new FormData() + // Check that something changed + regions = ev.detail().regions + if (Object.keys(regions).length == 0) { + return + } + payload.append('page', window.location.pathname) + payload.append('images', JSON.stringify(getImages())) + payload.append('regions', JSON.stringify(regions)) + // Send the updated content to the server to be saved + onStateChange = function (ev) { + // Check if the request is finished + if (ev.target.readyState == 4) { + editor.busy(false) + if (ev.target.status == '201') { + // Save was successful, notify the user with a flash + new ContentTools.FlashUI('ok') + } else { + // Save failed, notify the user with a flash + new ContentTools.FlashUI('no') + } + } + } + element = document.querySelector('meta[name="page-id"]') + page_id = element.getAttribute('content') + editHtmlTemplate.call('post', window.reverse('api:add'), payload, true, onStateChange) + }) + } +}).call(this, editHtmlTemplate) diff --git a/apps/aklub/static/aklub/js/image_uploader.js b/apps/aklub/static/aklub/js/image_uploader.js new file mode 100644 index 000000000..16c1003c1 --- /dev/null +++ b/apps/aklub/static/aklub/js/image_uploader.js @@ -0,0 +1,199 @@ +var imageUploader = function (dialog) { + var image, xhr, xhrComplete, xhrProgress, evtFuncsNames + evtFuncsNames = { + cancelupload: 'imageuploader.cancelupload', + clear: 'imageuploader.clear', + fileready: 'imageuploader.fileready', + save: 'imageuploader.save', + rotateccw: 'imageuploader.rotateccw', + rotatecw: 'imageuploader.rotatecw' + } + + // Set up the event handlers + dialog.addEventListener(evtFuncsNames.cancelupload, function () { + // Cancel the current upload + + // Stop the upload + if (xhr) { + xhr.upload.removeEventListener('progress', xhrProgress) + xhr.removeEventListener('readystatechange', xhrComplete) + xhr.abort() + } + + // Set the dialog to empty + dialog.state('empty') + }) + + dialog.addEventListener(evtFuncsNames.clear, function () { + // Clear the current image + dialog.clear() + image = null + }) + + dialog.addEventListener(evtFuncsNames.fileready, function (ev) { + // Upload a file to the server + var formData + var file = ev.detail().file + + // Define functions to handle upload progress and completion + xhrProgress = function (ev) { + // Set the progress for the upload + dialog.progress((ev.loaded / ev.total) * 100) + } + + xhrComplete = function (ev) { + // Check the request is complete + if (ev.target.readyState !== 4) { + return + } + + // Clear the request + xhr = null + xhrProgress = null + xhrComplete = null + + // Handle the result of the upload + if (parseInt(ev.target.status) === 201) { + // Unpack the response (from JSON) + var response = JSON.parse(ev.target.responseText) + // Store the image details + image = { + id: response.id, + name: response.name, + size: response.size, + width: response.edited_width, + url: response.image + } + // Populate the dialog + dialog.populate(image.url, image.size) + } else { + // The request failed, notify the user + ContentTools.FlashUI('no') + } + } + + // Set the dialog state to uploading and reset the progress bar to 0 + dialog.state('uploading') + dialog.progress(0) + + // Build the form data to post to the server + formData = new FormData() + formData.append('image', file) + // Set the width of the image when it's inserted, this is a default + // the user will be able to resize the image afterwards. + formData.append('width', 600) + // Make the request + xhr = new XMLHttpRequest() + xhr.upload.addEventListener('progress', xhrProgress) + xhr.upload.addEventListener('readystatechange', xhrComplete) + editHtmlTemplate.call('post', window.reverse('api:images_add'), formData, true, xhrComplete) + }) + + dialog.addEventListener(evtFuncsNames.save, function () { + var crop, cropRegion, formData + + // Define a function to handle the request completion + xhrComplete = function (ev) { + // Check the request is complete + if (ev.target.readyState !== 4) { + return + } + + // Clear the request + xhr = null + xhrComplete = null + + // Free the dialog from its busy state + dialog.busy(false) + + // Handle the result of the rotation + if (parseInt(ev.target.status) === 200) { + // Unpack the response (from JSON) + var response = JSON.parse(ev.target.responseText) + + // Trigger the save event against the dialog with details of the + // image to be inserted. + dialog.save( + response.image, + response.size, + { + alt: response.name, + 'data-ce-max-width': image.size[0] + }) + } else { + // The request failed, notify the user + new ContentTools.FlashUI('no') + } + } + + // Set the dialog to busy while the rotate is performed + dialog.busy(true) + + // Build the form data to post to the server + formData = new FormData() + + // Check if a crop region has been defined by the user + if (dialog.cropRegion()) { + formData.append('crop', dialog.cropRegion()) + } + + // Make the request + xhr = new XMLHttpRequest() + xhr.upload.addEventListener('readystatechange', xhrComplete) + editHtmlTemplate.call('put', window.reverse('api:images_update', { id: image.id }), formData, true, xhrComplete) + }) + + function rotateImage (direction) { + // Request a rotated version of the image from the server + var formData + + // Define a function to handle the request completion + xhrComplete = function (ev) { + // Check the request is complete + if (ev.target.readyState != 4) { + return + } + + // Clear the request + xhr = null + xhrComplete = null + + // Free the dialog from its busy state + dialog.busy(false) + + // Handle the result of the rotation + if (parseInt(ev.target.status) == 200) { + // Unpack the response (from JSON) + var response = JSON.parse(ev.target.responseText) + + // Populate the dialog + dialog.populate(response.image, response.size) + } else { + // The request failed, notify the user + new ContentTools.FlashUI('no') + } + } + + // Set the dialog to busy while the rotate is performed + dialog.busy(true) + + // Build the form data to post to the server + formData = new FormData() + formData.append('direction', direction) + + // Make the request + xhr = new XMLHttpRequest() + xhr.upload.addEventListener('readystatechange', xhrComplete) + editHtmlTemplate.call('put', window.reverse('api:images_update', { id: image.id }), formData, true, xhrComplete) + } + + dialog.addEventListener(evtFuncsNames.rotateccw, function () { + rotateImage('CCW') + }) + + dialog.addEventListener(evtFuncsNames.rotatecw, function () { + rotateImage('CW') + }) +} +ContentTools.IMAGE_UPLOADER = imageUploader + From 51155d2d0725b6873e6aaced58ac0782a3ef5189 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 09:26:48 +0200 Subject: [PATCH 006/189] add js modules handle load html template form field widget initialization --- .../static/aklub/js/form_field_widget_init.js | 38 +++ apps/aklub/static/aklub/js/html_template.js | 263 ++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 apps/aklub/static/aklub/js/form_field_widget_init.js create mode 100644 apps/aklub/static/aklub/js/html_template.js diff --git a/apps/aklub/static/aklub/js/form_field_widget_init.js b/apps/aklub/static/aklub/js/form_field_widget_init.js new file mode 100644 index 000000000..683be848e --- /dev/null +++ b/apps/aklub/static/aklub/js/form_field_widget_init.js @@ -0,0 +1,38 @@ +/* + * masscommunication_init.js + * + * MassCommunication form fields (html template widget) initialization + */ + +(function ($, HtmlTemplateFormFieldWidget) { + $(document).ready(function () { + console.log(window.reverse) + var formOpts = { + editDialogPageContainer: 'template_page', + popoverDelay: 3000 + } + var htmlTemplateFieldOpts = { + templateNameFieldId: '#id_template_name', + templateTypeFieldId: '#id_template_type', + templateDivFieldId: '#id_template', + hiddenTemplateFieldId: '#id_hidden_template', + templateTextareaFieldId: '#id_template_textarea', + templateDivFormFieldContainer: '.field-template', + templateTextAreaFormFieldContainer: '.field-template_textarea', + } + Object.assign(htmlTemplateFieldOpts, formOpts) + var htmlTemplateFieldWidget = new HtmlTemplateFormFieldWidget(htmlTemplateFieldOpts) + + var htmlTemplateEnFieldOpts = { + templateNameFieldId: '#id_template_en_name', + templateTypeFieldId: '#id_template_en_type', + templateDivFieldId: '#id_template_en', + hiddenTemplateFieldId: '#id_hidden_template_en', + templateTextareaFieldId: '#id_template_en_textarea', + templateDivFormFieldContainer: '.field-template_en', + templateTextAreaFormFieldContainer: '.field-template_en_textarea', + } + Object.assign(htmlTemplateEnFieldOpts, formOpts) + var htmlTemplateEnFieldWidget = new HtmlTemplateFormFieldWidget(htmlTemplateEnFieldOpts) + }) +}(jQuery, HtmlTemplateFormFieldWidget) diff --git a/apps/aklub/static/aklub/js/html_template.js b/apps/aklub/static/aklub/js/html_template.js new file mode 100644 index 000000000..4b66ca71f --- /dev/null +++ b/apps/aklub/static/aklub/js/html_template.js @@ -0,0 +1,263 @@ +/* + * html_template_form_field_widget.js + * + * HtmlTemplateFormFieldWidget object class + */ + +var HtmlTemplateFormFieldWidget = (function ($) { + + var privateData = {}; var privateId = 0 + + function formFieldWidget (opts) { + /* + * Form field widget (load html template) + */ + Object.defineProperty(this, '_id', { value: privateId++ }) + privateData[this._id] = { + editDialogPageContainer: opts.editDialogPageContainer, + popoverDelay: opts.popoverDelay, + templateDivFieldId: opts.templateDivFieldId, + hiddenTemplateFieldId: opts.hiddenTemplateFieldId, + templateNameFieldId: opts.templateNameFieldId, + templateTypeFieldId: opts.templateTypeFieldId, + templateTextareaFieldId: opts.templateTextareaFieldId, + templateDivFormFieldContainer: opts.templateDivFormFieldContainer, + templateTextAreaFormFieldContainer: opts.templateTextAreaFormFieldContainer, + templateDivFieldContainer: null, + templateTextareaFieldContainer: null, + templateTextareaFieldValue: '', + get editDialogContainerHtmlString () { + return "
" + }, + init: function () { + this.cacheDom() + this.bindEvents() + this.editTemplatePopOverDialog() + this.templateFormFieldWidgetInit() + }, + bindEvents: function () { + this.$templateNameField.bind('change', this.showEditTemplateDialog.bind(this)) + this.$templateTypeField.bind('change', this.replaceTemplateFieldContent.bind(this)) + }, + cacheDom: function () { + this.$templateDivField = $(this.templateDivFieldId) + this.$hiddenTemplateField = $(this.hiddenTemplateFieldId) + this.$templateNameField = $(this.templateNameFieldId) + this.$templateTypeField = $(this.templateTypeFieldId) + this.$templateDivFormFieldContainer = $(this.templateDivFormFieldContainer) + this.$templateTextAreaFormFieldContainer = $(this.templateTextAreaFormFieldContainer) + this.$templateTextareaField = $(this.templateTextareaFieldId) + }, + openEditTemplateDialog: function (e, ui, $dialog, templateName) { + // Hide close X icon + $('.ui-dialog-titlebar-close').hide() + $dialog.find('#' + this.editDialogPageContainer).attr( + { + data: window.reverse('aklub:get_email_template', { template_name: templateName }), + width: $(window).width() - 40, + height: $(window).height() - 250 + } + ) + }, + inlineStyler: function (element) { + element.inlineStyler() + }, + confirmEditTemplateDialog: function ($dialog) { + var templatePage, htmlDocument, article + templatePage = document.querySelector('#' + this.editDialogPageContainer) + htmlDocument = templatePage.contentDocument + article = $(htmlDocument).find('article') + // Convert email template external css into inline + this.inlineStyler(article) + this.$templateDivField.html(article) + this.$hiddenTemplateField.val(article.html()) + $dialog.dialog('destroy') + }, + editTemplateDialog: function (templateName) { + var that = this; var btnNames = {}; var title + btnNames[gettext('Ok')] = function () {that.confirmEditTemplateDialog($(this))} + title = gettext('Edit') + ' ' + templateName + ' ' + gettext('template') + $(this.editDialogContainerHtmlString).dialog({ + modal: true, + title: title, + width: $(window).width() - 20, + heigth: $(window).height() - 115, + position: { my: 'top', at: 'top+112' }, + open: function (e, ui) { + that.openEditTemplateDialog(e, ui, $(this), templateName) + }, + buttons: btnNames + }) + }, + destroyPopover: function () { + this.$templateDivField.webuiPopover('hide') + this.$templateDivField.webuiPopover('destroy') + }, + showPopover: function (element) { + var that = this + element.click(function (e) { + e.preventDefault() + var templateName = that.$templateNameField.find('option:selected').text() + that.editTemplateDialog(templateName) + that.destroyPopover() + }) + setTimeout(function () { + that.destroyPopover() + }, that.popoverDelay) + }, + getPopoverOpts: function () { + var that = this; var content + content = '' + gettext('Edit template') + '' + return { + placement: 'top-right', + trigger: 'manual', + title: 'Edit', + content: content, + offsetTop: 0, + offsetLeft: 0, + backdrop: true, + onShow: function (element) { + that.showPopover(element) + } + } + }, + editTemplatePopOverDialog: function () { + var opts = this.getPopoverOpts() + var that = this + this.$templateDivField.mousemove(function (e) { + var offset = $(this).offset() + var relX = e.pageX - offset.left + var relY = e.pageY - offset.top + opts.offsetTop = relY + opts.offsetLeft = relX + WebuiPopovers.show('#' + that.$templateDivField.attr('id'), opts) + }) + }, + showEditTemplateDialog: function (e) { + var templateName = $(e.target).val() + if (templateName !== '') { + this.editTemplateDialog(templateName) + } + }, + getTemplateFieldContainer: function () { + this.templateDivFieldContainer = this.$templateDivFormFieldContainer + this.templateTextareaFieldContainer = this.$templateTextAreaFormFieldContainer + }, + exchangeContent: function () { + this.getTemplateFieldContainer() + // Replace form field container content + var templateDivFieldContainerChildren = this.templateDivFieldContainer.children() + var templateTextareaFieldContainerChildren = this.templateTextareaFieldContainer.children() + this.templateDivFieldContainer.html(templateTextareaFieldContainerChildren) + this.templateTextareaFieldContainer.html(templateDivFieldContainerChildren) + }, + exchangeFormFieldContainerContent: function () { + /* Replace 'template' div form field widget container + * childrens with childrens from the 'template_textarea' textarea + * form field widget container + */ + + // Set initial template form field textarea widget value + this.$templateTextareaField.html(this.templateTextareaFieldValue) + + this.$templateNameField.attr('disabled', true) + // Exchange form field container childrens + this.exchangeContent() + + // Replace template div form field widget id + $(this.templateDivFieldId).attr( + { + name: this.templateTextareaFieldId.slice(4), + id: this.templateTextareaFieldId.slice(1) + } + ) + // Replace template div form field widget label + this.templateDivFieldContainer.find('label').attr('for', this.templateDivFieldId.slice(1)) + this.templateDivFieldContainer.find('label').text(this.templateTextareaFieldContainer.find('label').text()) + // Replace template textarea form field widget id + $(this.templateTextareaFieldId).attr( + { + name: this.templateDivFieldId.slice(4), + id: this.templateDivFieldId.slice(1) + } + ) + // Enable tinymce form field widget editor + django_wysiwyg.enable(this.templateDivFieldId.slice(4)) + + this.$templateTextAreaFormFieldContainer.addClass('hidden') + // Erase hidden template form field + this.$hiddenTemplateField.val(null) + }, + exchangeFormFieldContainerContentBack: function () { + /* Replace 'template' textarea form field widget container + * childrens with childrens from the 'template_textarea' div + * form field widget container + */ + + // Disable tinymce form field widget editor + django_wysiwyg.disable(this.templateDivFieldId.slice(4)) + + this.$templateNameField.attr('disabled', false) + + // Exchange form field container childrens + this.exchangeContent() + + // Replace template div form field widget id + $(this.templateDivFieldId).attr( + { + name: this.templateTextareaFieldId.slice(4), + id: this.templateTextareaFieldId.slice(1) + } + ) + // Replace template textarea form field widget id + $(this.templateTextareaFieldId).attr( + { + name: this.templateDivFieldId.slice(4), + id: this.templateDivFieldId.slice(1) + } + ) + + this.editTemplatePopOverDialog() + }, + checkTemplateFieldWidget: function () { + this.getTemplateFieldContainer() + var textareaWidget = this.templateDivFieldContainer.find('textarea') + if (textareaWidget.length) { + return 'textarea' + } else { + return 'div' + } + }, + replaceTemplateFieldContent: function (e) { + var templateType = $(e.target).val() + if (templateType === 'new') { + if (this.checkTemplateFieldWidget() !== 'textarea') { + this.exchangeFormFieldContainerContent() + } + } else if (templateType === 'existed') { + if (this.checkTemplateFieldWidget() !== 'div') { + this.exchangeFormFieldContainerContentBack() + } + } + }, + templateFormFieldWidgetInit: function () { + this.$templateTextAreaFormFieldContainer.addClass('hidden') + if (this.$templateTypeField.val() === 'new') { + this.exchangeFormFieldContainerContent() + // Copy value from template form field div widget to textarea and save as global value + var textareaValue = this.$templateDivField.html() + this.$templateTextareaField.html(textareaValue) + this.templateTextareaFieldValue = textareaValue + this.$templateDivField.html(null) + } else if (this.$templateTypeField.val() === 'existed') { + // Copy value from template form field div widget into hidden field after form page loaded + this.$hiddenTemplateField.val(this.$templateDivField.html()) + } + } + } + // Initialize + privateData[this._id].init() + } + return formFieldWidget +}(jQuery)) + From f0b15691ebd59851aa84c6d7f7aeb3e4f1551646 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 09:28:01 +0200 Subject: [PATCH 007/189] add load html template form field widget css file --- apps/aklub/static/aklub/css/template.css | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 apps/aklub/static/aklub/css/template.css diff --git a/apps/aklub/static/aklub/css/template.css b/apps/aklub/static/aklub/css/template.css new file mode 100644 index 000000000..4f92c89a5 --- /dev/null +++ b/apps/aklub/static/aklub/css/template.css @@ -0,0 +1,5 @@ + +.template { + float: left; +} + From 68c779b8e46a4753f0cc051b538b7c0d77744da8 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 09:33:40 +0200 Subject: [PATCH 008/189] add automat newsletter templates with media files --- .../static/aklub/css/automat_newsletter.css | 31 + .../aklub/images/automat_green_newsletter.svg | 401 +++++++++++ .../images/automat_purple_newsletter.svg | 333 +++++++++ .../aklub/images/automat_red_newsletter.svg | 382 +++++++++++ .../images/newsletter_template_sample_pic.png | Bin 0 -> 66954 bytes .../images/newsletter_template_sample_pic.svg | 641 ++++++++++++++++++ .../automat_green_newsletter.html | 21 + .../automat_purple_newsletter.html | 21 + .../automat_red_newsletter.html | 21 + .../aklub/templates/email_templates/base.html | 23 + 10 files changed, 1874 insertions(+) create mode 100644 apps/aklub/static/aklub/css/automat_newsletter.css create mode 100644 apps/aklub/static/aklub/images/automat_green_newsletter.svg create mode 100644 apps/aklub/static/aklub/images/automat_purple_newsletter.svg create mode 100644 apps/aklub/static/aklub/images/automat_red_newsletter.svg create mode 100644 apps/aklub/static/aklub/images/newsletter_template_sample_pic.png create mode 100644 apps/aklub/static/aklub/images/newsletter_template_sample_pic.svg create mode 100644 apps/aklub/templates/email_templates/automat_green_newsletter.html create mode 100644 apps/aklub/templates/email_templates/automat_purple_newsletter.html create mode 100644 apps/aklub/templates/email_templates/automat_red_newsletter.html create mode 100644 apps/aklub/templates/email_templates/base.html diff --git a/apps/aklub/static/aklub/css/automat_newsletter.css b/apps/aklub/static/aklub/css/automat_newsletter.css new file mode 100644 index 000000000..29c6a49c5 --- /dev/null +++ b/apps/aklub/static/aklub/css/automat_newsletter.css @@ -0,0 +1,31 @@ +.newsletter-section { + width: 318px; + height: 980px; +} + +.green-newsletter-section .top-vertical-line { + height: 80px; +} + +.newsletter-section p, h1 { + margin: 0px; +} + +.newsletter-section h1 { + text-align: center; +} +.newsletter-section .sample-pic { + width: 250px; + height: 280px; +} +.newsletter-section .align-center { + text-align: center; +} + +.purple-newsletter-section .top-vertical-line { + height: 100px; +} + +.red-newsletter-section .top-vertical-line { + height: 100px; +} diff --git a/apps/aklub/static/aklub/images/automat_green_newsletter.svg b/apps/aklub/static/aklub/images/automat_green_newsletter.svg new file mode 100644 index 000000000..2df8ec4e6 --- /dev/null +++ b/apps/aklub/static/aklub/images/automat_green_newsletter.svg @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/aklub/static/aklub/images/automat_purple_newsletter.svg b/apps/aklub/static/aklub/images/automat_purple_newsletter.svg new file mode 100644 index 000000000..d3c92b306 --- /dev/null +++ b/apps/aklub/static/aklub/images/automat_purple_newsletter.svg @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/aklub/static/aklub/images/automat_red_newsletter.svg b/apps/aklub/static/aklub/images/automat_red_newsletter.svg new file mode 100644 index 000000000..73120f986 --- /dev/null +++ b/apps/aklub/static/aklub/images/automat_red_newsletter.svg @@ -0,0 +1,382 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/aklub/static/aklub/images/newsletter_template_sample_pic.png b/apps/aklub/static/aklub/images/newsletter_template_sample_pic.png new file mode 100644 index 0000000000000000000000000000000000000000..c740dd1c1d31ef016c7540a7b0332354d47848d3 GIT binary patch literal 66954 zcmX_nb97{1uyt&7$H~MtC$^J`GqH_{ZQHhO+n(6ACbpft{NA_T*K6I?_nyD*sXn!< z_TJTD^0MNHa5!)vARvg65+aHqAfTe(mm3V!_X^AeN5uCRgoBWz63q9<6UHd?`x(|& zLfrub1ZnWU3p9xa>BsjXwxg(;<8K=iM;Cp2V-Ob?7kYDR3kO4eTVr|~d((_d9vl!5 zLJ&z2K_%DB^DH-4rGwSKKGSKEFcDSZrOKp&f_{J$Arv-?g?2skAR`oJNkGLAHQ}0Q zRg=|u`O+F(9(5Capd|~$AYzJ&t)Zkqq7eurXed$(ZUYUu7+-T}BUC=u)4I4u+>`0BM7zuyMzg#iHQ)zJv>%~?cTDKMJ? zOCx?+>`CwON%~D&+DSXzLCe$&{nUX`E)S`n$}G&xC@7CtJ_vE0sR(2kQJ%)Tw}bnv zV+F(__f|&J$o)@s+MBTMbn57W|LZP=z!P(nnLic$@ z^ee)SgIGOJni)Kd)Z1s;psw&%rjdGkxd`bVsbC-(Lla;e{nZ{+Cqeot_x9ZE# zE3i?X^Ud6IzfkArhZ|E5x%3=qq3>9!#Nb;@iN~rG7AysSMw~l12hu7xAE50;CiT8l zsTMnE)nKYMF1}8T9Grx#H=xDoiZV@Q_v&1mIBf2Wm`Goy(_6D=S9USwes+Kl?%00 zdUlf!naRIg3MgTMq1b6J_UucjW7kW+6vwS}D)+8_vj6KCWUnr?pKqakWP>TJ=*jN@ zZHTaf@Y|4hLu}}6egB)N_ zNF-5(>g#iC(xYw^`*?DiESSrdBC7a$%VY@W$DFIF!l#>HP)Ml+UUNa^$)NZTun(n` ztuA=g!4^HlsOq1MQ#k16+LV5~=FtC`l+_7aLx<=}QC`>Y9LtoePFP0*4iYy!DoY<# zIu(xYY^ex!A9JE6WXM3AFe*tE+3c5r7wz59aC_B0xqJPEVw(K{T3Z;gZxQ3QqVc(M zQXbVKv_fWY(_5vPidw$_YD0EiV+M@{^0n*xWTOISVq|pR@f|Lz^3C?!mvl4|(fSfV zE9EQH0u&c=%MlUW;@4fxiX-C6r{oGlS9W;cuMJjg{f=?GN7#{<-UuL4BTo9?_^i&T z^TZ-7Ev$i$!l$qn(e8&@i(VVQ8z!6pbWdL*Lt9dD=7gJpb1FA*M3!AvwNt0R;ih6aR;0FxtLHrd^yTRs1=fbS%Rt3F)wuJ9{ zc-3={va%8iT#j0(RxT4S=pTwMF1AX2O*dc!=4EtZbSnK{aNp0pJXPWqxn6Re(DIk-^;d9d8ER2zIWItmp;JHwMK4u%~ z*xXZ?ffZhh&rvLV0u@N(>oFtjv4mHb=>2ch972akX@SRoBuXI$)3X9n{PtltTsu3P z541Eeo0ICxz1R$0YKs>bIfsQpEI^KXM@&Ec0V68%$+HJk4;@GSynpP6*BtrP4LoQe z8V}={^8M194FiA{7h4Wr@)DFr+h$&Hr=l_uB`;+L%Kwt(D<9-oc7y;$-Ib5psMdiy_`t-Lh!W4qcf(7niVE=`))SA;E2aq?U zptmQ1f?iqUyRlpwCGJ0TndT5Y>$e)94oAL#Udy?M0(Q^R@>xsatf%374?;XB1!n^D z_US^xT6yLLF+o0w_=p`waqMV_``Ev7R^kM!tB7XEkO6(})$Jpc)i!|!DM({_>X7NtT+ zAp?&+udmx-60OrrhtKI~RlV9C+`krd6RrPRqd{|$sER0!D?7lB80`EY)Df|Peae45 zb;Aco?Dz-ki=4Z=ZWQxrBhs|d%A8V-98hat%r&`zao@7 zOE**+jdpo@DypL}eVI^OW2d0q$!l?nA|c-T1z6lSrO5%51qa}JFU^^PC8oKWsR6fy zex~~!!0KfEPW0;}Q z;zKRan=;2f_o-RlE~`C_G!T(;*tvBvun&Th=6NZ^`cQTU8PqzBQgswY2j*?Kl}8vb zCCrI5u~k}y|IjTyH9|+EN?6rO%Ij5%m<<{nbWxFU%}N#AKZiIxhx^5LyfeU?2%?6B z*an>L!=IHQP*Q-g|M1$&SkSv3-M<+N$VAAGn9z9&^cG^1uh&0a_JtP34 zeg_1KV+37Qz2TM(r2Fa)?4@R>Lj34ux(QgEdQ>v<;*x23*H2qiVo%=R2uLm$lazPJ zPmEl-Kz*p_ICzgr@#XTZ>!{^0>K-M&;vsMPsRn1!O`p|6Qp+vOPy=-wFgnVXcoyc< zBhAz}{q04cnH<3Y=sod}3#hFy2O6msF-6H1Th6gN$LwRz$i6mv48a^fYZH1#?%t}X zpG>k-`nnbbCAqZ2DdtUq-v6KxSLQW!-->)Feoz3BfNdq2vKG zqkX0t;wEI!TG(-JV<n6S&xa0pU4|TPgWV12+3exjR~|61eP$o&2h5t_+@OX!TCxeN1PL`ejv>31rTrZ z*!Yd6PP^O*-P)a9`lx(%cSvB|V!CgJ255*?9w3Me;-Ro5>IiA+mT+!aQ>3Gvy)Sqb zZ9hTNcgJn<(Q1h`5WL44?2pvT_z02{Z>!g}z3n+PBc^m`w`d(D`yj5HTc%3?GOAFr zFgcdaHhV8wE1H*s4dg3mTh(YD!MOa3***`fVT#P17O>!eO(2872x}DS7xs|gj%5(} z4q|K@hZxTp=G1O=7wCb0e76Ye%QZ<3)b~Tp_qkBDN>v6lloh4b7x*lKS_TA|3Ms|e)9pr0Z>B>Ps*($2( z6Z4zrOQ=X!6aX8d%|_>-zt%kC6`w+{9u#Zk2BROHnYI-q)Ot@pP=Lz$tuA2@;9Qrc zOCr*)cuhPJug`;Wd^l*gKP6w|f;dYp{W9YpQ)oD)4Temo+wr(W5?AGcZ#DNQ4nj0G ztfvD=28=gn3N?I0QNjTAcyt^PBiIoUHJRTs@qT9P3+N(mSMmVj0a6yS#C^?Ri<+Tk zXJX=hR=o7ng~BPgqvHbf5dH@gU-199ztIP~t&PGghe79(N=T&a)CTw2c*P)G?-t^_ zk|#n7q2wD73U1%=HP&X*@n9zclGty8J|KQ!)#mY3X1oOaF`d)s{%U~q=1mBITU6C$z_St5io-Wdw4#PEG zY{jVT7Pggm1I1BJK`<>iN(bYX#5EbzHoZM&$fWF?%tkPP^3j3rjYC%~gd9ORknSC$ zl_nnuCpV8-lKB_(!Pfq4=~u0M74x|kh44vXsn_aC-Ef3CeSq)ylio_ysWO9#aurl7 z59@;gLIFzX(C4L2=hiGkwdNB(Zra&vc2LLRufPnXCq}^7P7kJLeEasYs~iFhkPztM zV5%@D&}v{tqCVM{F4en2VWTM7z3C%zh$ehYQ^~>pmoN6?~&j zJV-TPzYwc@k4u!_FOv*o2VW2evP#yU(z{?A2t(*UX6uc#5o*Kv2QtPULmhe_g)B|Z zoEY4dPvS*O!&QOZ8R?IY`NQe-!SjAk#GmH{MFMB6@_i%};@*hG#jl@TTd=($80#=W z8Q)8ui>BemtR?yhi1(H84&s#MJ3F1uDtg70k@YC zQGCj02XG5P00Pg5%XhI}u!z-ry<-+K_TIv4DgB!{oLM)n3i+y4BHHHj{B9#`}~V=QLS&$s>91Ff@e$&~XSVELFrM(vSA*TTb% zzuzuyx`~7)Is@no2(VUFLhMolT3|`V% z1xaJu%;!k zUZ`$bC1HcDzcvm|)+*H_-@!QkxoSXnrT)90&l*Tx`yfE$$U^n=4nat92aL7^v&|2! zdtmqbHyQ{IF*Y(HJ@x}n5_vLw^lK?1GlDx%jX+D_b;-N;T^6VCjFw1*QFHYywRP0piPGKUb==x!pw}4xZSGI#IMt*QkNjUvWIKj zwB^V%ae#?sC0svx7|2Of%oPvzfg;v`jE(dIy#1+3Sg=p-6-5RnrR*w%0S%J$c6zs1 z`sgG$tyavVmz#r9$@^!G{cdpWP_sQ0&UI`7Fu@WVho;8NLSEDn)rF8pN>9iVqIlf) z(2;^jhtFh!u1be7PWU1Ez+$-?R`t9>Fr4g1jBrzGb`Exrh?av!;T8cBbn{Qsxh|BF zbwvkg?E{y2_g)K9HT-H{d^1Y=k<0^U4(kYMMVUgJ<6!mDP87+L47_>rmD(!uPBSqV z1IgL=ApOZcM?AYmc_G4E1#jBXK(a+Op_aLbC)aXTGai8HJA4AXk?VQN-G!Z-`zwS} z71<9*-~L<7igH@KG2D-F(;VIt)O$T=AsP(EorbjJ1+z7Me}@@6CoJ^2KPObgQopk# z8!DvE@S%Et=!SQ_JPB-LMKyrdcF+>Z(VdM!@K+naTSCF?pogeBBs3pYtC z>QAq_J!BM5I!_f)%1AQcy)%G3kFeOFCBF~Rqa9SHbW22bY(Gu~H;ez7cHn0Yk-RUxy{@wP z<}}FA#S2q8vZGfi$cEJ8NUndeTeLb=@TkVS9Dj=&Q#EB(riBc1kNt0Aoq6GjP_1eb z?w3*9BI0YkOY26gd`vZkx$oNGca4kJlhGeSc{F-`nd$_M9TDQdqy_~MA#K|bbHa?% zqt=syjO$vCjp-61gw!W%N)^GgXj*ru{?Q?*?P0BE)?I(gb7GvOA{f5S&au~X)!#qo-q>e=3v4{t3rRoil5A6~`)Ml6wVt3{O-l{0dpqk_OydA3Uq)o(=|E&?}__v?_#04#43t zR`TG6;o`KypO}M)|85whtFJAvgx1=kYOq%Wr&^8BQQmHOzCvV>K5qWi1p^wS+ui7F zWwveGLNR@GR$bCR+gWmadk?M&%PSuG<7Q3qTM$0V#V$W|Xaw%*1XfP>g#HMVSjW$g z_jtFPq!FVV;ZD|htPmWq#t9KXcfLy03xw(Y{2KyWUEs=GTTkfls4RH-Mq>IeXSSOD zqcPdUL4!l6lWd-IVaZH$C{7^sdJfh!2k5L)LsL61jqPV5IJU_v8XAJ6lxPozY5*R~ zFwok-ON2pAF}%&`dYu60L&_{_CA?c5{Q9a2C&x)<`Fbi z{OCC2&uH}ksm*0IMr%=Tk&$3Za_i9#aR?RM2^NJq!u=K@Uu0sc?NJt16+U2A>m?@$UAUj7#peP>*lE}kNN+rR-*P*(05A>q1DM2|Zy) zb-B|~O^m>JOx*}MJ(O&LL(>D|cBr?@bKLg#GOb&0yyNRF9nf)TAx1X9RippZ#`MtyN#qlVYb4>?H_gP7)MoY8Zf%h`qu5<%b zOLWGRT4zUD>59nIXhc1P>rDg&(z_+ zo{+alQ)7$?kV6L}T*7pJEw*p?S5w!SK6|zoN0IT!GTHUYlBJ{ z_4{i-ywf)yn3~12?R^!Em+TFJixhI<^YCydQn*i)9X_)7-3r0dyW$$?ejbwy4P_fi zR}nY=V)mN>;R-lU+>@Y*p8S!zr@YSmJ~=G)IyjCP>?MEN=ZPGtT4}~Ao5RQ3*Kw5Elxr&f>f*q=Kd_$PWeSLd<^--^ynSBx! zxzz18k+~jVO*t~!3DAqq67OgDbACM-4Kd+G=EzUv4aS^y9;f^tGNy3Mp0Nybm13Pk zez+;0Ox2$F{C8OA@8and7fpRZ1$33N`Oxb3NxH(9zViKD~l^N6q8E~NzQ}$dD2|0rKP}ElB zfb}HrqCr(`Qz`<4g&>a4npUfWK!n-_ujkg9kiT*95tbMpSa#B4I?W)YBHslLdn)Ui zctQ2J_9oNr^QalLoc+t$elRfg68Gn@e0D(;;HmV~h$he#U?Fw7vf9IWDC1l+?c-vXndD6`yk z-kFnK4q|_1_d}a9gi^L_5AyIs0Ym-9r_nqZpup~!T8xZRfVvLEP=vCwbr6lOWy>(? zFHroz%9)lJw&zX(t=_@Z7P?d7yc;el{D})jqrF(2VS?p)kj4+Odp2N~yVdBF%q#+# z)mqud&03Bi_Oi9_CEtxSd=mt6uz56Rz{0&XNHnr$D~ZyUW$i=H$^|{b2lHP53k!U8 z>FrJZhHL+t<5kQp+jiw?&FAigD{(SBtG>42}Kfhm_HLT6Z<8KF6XlX#8x|% z3cRT_MZ5URa-L02regERRBG#aHs{`qkh;th=?f)axT)IcDyr9|wOZ*JTWl#8AuFr)HaN2b>Ik%(!;q;Uo) zPFtUuus^gdsEp!&*it31Sw8Cyijv-h4Iy0@&GG7&>qum%SeJ;`e(9DPw zs(2_do=m44k;^~P6t%96pJlchn;4z1H$k5Zx=2?~D9+CB>hZ)oQKZ#!(XUD0qjl^U zm`ZkHY48kpgK2S|et}!u3J@+g*9A z_+59>5vnd}G+w3Lc#M`wm1O`mATXC1U=7VL-a4&-t8Dy`cgIj5P|k8=?H6D!T^!0} zXEhp~QnfId_4A0-Bl8I_A8>}|s;6(Ppe7Y!p4QM)V^*Qng~pEcVH4_D=G9>M+7^6* zm=xflMwI_@hrvJ%t18Q$0G4$-(&tjjq!BD?g+kCdjlRN}2JN>h81Cs8Pe%gtLk=6p z1qr_sC=urF>!;xJjq;Ly){&!fV~jFZ1oNw<<@4p^&KkdCsJA`IgW3Nby?>b&tJR%w zxAL3`uoK?0ctTSN-_Qxi3swuXAq(@g{>jm|lhIU@{B|^a%{NI@ngYGN7@`va)ASr= zHsvg{h~Ck*R~f-f)!Z;lybR`%y?JUJ%3agcL>L$tIFB4x#QDS`FGo|G6cFff9HH-( zX)ctnE=WP%=7Rp08ke>cwVlYUEG&mslzaa-3&6k5*kxlEh(dhLE*1Dp?*)qKX9*cd z;0#aX&XJwq7Pb@EiXLG1oq4{cP1NxiE9yD4n!D|@fv-H-7UOuKzUy_e) z=>49!O`?Ghum&|ULR*7pHsxLVgL#U4kiTW}{=@CJ33B5?8qTDnm*|}{<{5NANmr@01+A=Y?&51OuO5)apz44#17;(c9e{zqm(c+5If-fKkTnpzHg(5L* z9ClJ=0ZNxr4&Obpf%=RH(LJV&% zzA0dd+X1Kl%y^XM>`{l^yPBOErQ%SGtPOsDB%N&%h@Ah{2c7%Jt;Jaid?`v5n|g&j zqq~4UFrDNhAhm23otaX`XuJeE_urq)qspH3RN%s-uja=ZgsRAlXL{+hb{{gRw<;lw zf>>mH;dP+=d(0o(Q=oRIsQb8O3oUbl&(;R8;Rv#`;E~DWzmPvNQ}jO23x3-V@{kbY zAG*CIo^n$Wy>of_Y?v+=Vi-0!YoE6Sr>D)7BD4vZXvA{c9bO(3bs-B+@z&5Mxbr75 z1VvprO4&@jRPr0fKROS2gwE;kR|{J$dOvy#>6?f)qQ>VBP3<0Pc?lB$J-QJWgoY#7 z*gUGtFmv_^_Q20De2ri>YWvN#xpq!#c0swBcK`d%LQX=f_}BiY<;3Mh^|CX^Eyv7K z`h16MD^h^6w{k4!U0#2GzwlM;yxY9p))(F#+=c0Q-wmCpZcZ{QjU~?s&r?4&!&cod z5vd7s!Mj6c`gfAv_o0Z(`D(oO7eP_eY0Y-ZT+w3PHW>uSL#Wn8U%!R|4E>bhrXbE| z3YbQ5)cyzN%pV6r5g}gmDTpo?T`)z2Butab>RS6L4O#PMOZ12b#VzFMA z!y(nBaMf$NL|QPD>Tw#;T(~Qz`|;i(J-#ROLnD>+oBHbLT#AL<&{2woG9-$rs>+H! z5ksenv78=MgA0u(CtETyZXWx4P%9!jr1nnf7nEHqaJ3YuhQs(WV}qDe!myKpB+C}u{iV{KVBx8T@<%gnxO>@7(;jn^=Y zyb^Jn0?7!bMF(OjR-D0qlSLh~Q_qD*t@y)Qd7125^|5Mz&yL)*5PVPlgKsY9*W+m1 zF79)6H!|&@Uw~o;#NShzzjajE+jkn{f|p&hr#zX+*=Up=vDse9>^xnR1(~tV_t{-9kNHGENocY)72&w4uUo;r*AE5xQBnoH8EsIb_LJy@WbJL&f zKPxe2n^Q#9H}q_i`n%)Xx;h3T0*bfesL_cR_gr!su z66qH(MgLB>{t;6nih-P_U(r=k=Yb@OjemeH&f9*ecpHt?m1f0~c1**X%s2V$jE}FC zb#G6TCKbSMtIu++bZb)kHli%0mq52+)dXBYxlxTcoRWVyc}xv63PHMo{}E1$S-%4W z{qeQ&vFn*+gKGUz%RIXRzLAydKJ~%PMIcsuL^dVDHROn)oV4&ZgxI6}xl2K8hb>g{ z`~K75+MeRBC17OgaZjQfc2?SKoS2I_WxDQx&>Xt0RXL4}y6^}oJ(pO@Op};$df0gXs`d)M}`TVS}kGzN4I*~ zi-Xx>Dq5*ETib11(GU@`k%_S^0n}ELNE~tLnAKehT8LBNJRJhmmKgVo13OVo>22}hq3PGV!VbZMlIdKVfAwG8N`rwT-NC5MW}e#J#Zv5oJxdS1w;#mI z&aDTKAKu>#B}nkgUwnDU&awtq#6DVDv9eS>P(o*79o=LP3pnJ zDP?JxCo})|wp)N$=`Yr*cr!S;?={UOB-)WoWiXBo3r}qU_~WXfq`M>W_1?-#FlaMt zAsZ^!+*ViJ()y}yI%T!{Co|+A(<4<3U71wW^Ci8LWg>yKLi>!!-uOwA5k541mKKsm zIe=M=SRyvF{|@O4kVa)?)oCwBzNwvm6IfBZPR?ZPniAE-j0^sH5py-_>zxkxm_d%Zl(ei_AI!`AfF{~bGK#B2tg~lqq_^$!YtClT=j+;# zbz*dpj6h}B(bMlWCp)pWM?oqs04J`Ul`pU5fHB&CLY;+!8P=|)bXTp_=rX+er($F4 z3S#R(5)CcwPv=ETT@kcE7|#XV?OU(PzpT?ySCtAD7_WQ_df1axhnrCJrtiZ2Bw}%M7Nx5 z4LRP20=@N;0J{>KzePGswaPt+srZ(duAe_jNV`{d1KpyUM~<_<=W)QJxG;A!mwebL z#U3?ddQ4BQTG<>-nD0xwDwG!tV%L;~A|X2dw0wc)Z0Qs-GPOAis;9B$SaACcq>N$0 z@SCt0x0B?0>#H5BK+UDF6H*eXkQ9oEgm$+MC<~~<+z^WGu&Tak{VWjH(3xx6A(*E+ zPFOVN7KYLSzYkkypVs|_jD}xIUG{XBh|OxKq(w$#`>XfV5*K3`=Ph9*3Y1C4q|DPf_3Ay zg3v$45e+;K+83ZVrRb+C4=(LoXzEoCD&KWU&=hk+BZ)d?e<^SNkvqn7mdr^`xfd2R zZR{bxj_J->cq=N%*0xpDUEk7{Ti@%d7?zg#1-GyiCmH_7`aC;WlXf+xm@|R?x25tL zhCx2z)A#W|(%`+gpjONZkcBp9*;d3*OqOo|N3%x@!tUnUjM4NWuk zn9Yy*`Gxu6;!zAah9qVHgnK#kU&~*R#9UaYJMfYbvNG*dDE4b+6Ue) zAEVXb;#oL*}$O_#|dHu)OrW3|c+!^|i=%@Opv zsfF-Cv4}Jav@uvWcpU0Yk==!mD|}MQ>d!sn3g>z{lYQeVdcU}S2)OWUD&!tC?YjOc zQwpt6d)c$peI_IR5v0!7cFPT=LaJ2)u!v)6%MZU4Czdv5+#a;w4>K`Olu*^Mb9dss ztjzuXRPTa`-7a7r85kt?eg|B$1taB1=I+)8#r{6-IrXjOjnxf|BHTvOa=%u#E0Zo1 zExlN0&z6!eI2zY*6LOxde!F!Gw*?%VQcf2hBDS}+8y#)$NJRaHpseDA?XZN3ju5o4 z(4TkB5fU?v9OFHJ$QY!9Lh7Ogvstx#q^bySc?vDR7mS>e$~2>#o`1E3@NREz+)|u4 zjKpTyLpqQ40e?^IM*e}e#e}fF2)~JZfuLS%zklLth3u;%{On>6KUOn}t!`qB&`=^h zt~hG|mcarUNcc2ap@pS!?Q{}sXdc}IpNfR@$LCr&k>4X&>*F$%Z=L3P=*%e)(>OX( zkI#3>7RNamv=4$sWwswpvah{yb1%Jt$r#%Yt;Z0l%q@frRi-nbH^rs{E=lC<8 z%+2kEOLirEnp5Jbw5Qfi`{T0G1M)WD{ehd^cA_!(*ha-wkXQG~JH0S&bT(L}z{}BzC&kS33Ar;9TH5tnb3~uZc zDIuJmj{_*@DE>1!CJfJe=iz0r+l}M?%@Z2FW&y+}DKF~wM9`9Sw(s={Yb*=f9>`5> zK8G5|pI{Ov5?gx`mGytAZuJsQn&-CM>>Aw&X35}C7LHCr?~nImx5H#6x?t0QIs7w zH2;8y#u;HhX`Kp8%F!TYChS!Q_KaayRwE>Mw&v0xD z?H1@bVyN}BGXHq0E%k77?vwOF?SwsWe9X4ayCWS_x^F(6%kpNnKA93MQ2N*Q_!jlW zF9GyKZkygZ3EdEz!PU6|tI6kLNM$qldtVs%A=3`>1u| z@-<2nCEPZAYVIN?miN)P?@}Z1^Dh_bmU&4m(3-hw(Y>_Tz6fDKpl2Q&EZ9g{F^JA}nC_&7rdln!P zd1?LqE}hI)uV7=g9E!sBV7u*oi2n+CgdRs4dOf&^UnpJj-!c9Thw21%9iY`GFd&MB z?w4dckA0Hs495T-4vJkRd9?yvvkgZ;3cIcXyR%oco-$&S)^-~|JK&JHiKr{Af;rbv zh`@fX+K`z9!sxvdg(`Vd@W^X*UTupFu=?|CI4l1-7ZX0-v0l_g=@59%WB77eu*Ci= zy+iLm8R@wtK8~MQbKwJ0)O&y%&EqmD}&g zm0z&^_=msXq!ElQn~FQDrETgIGJ)ZH8mq+@r ze@lCk;{wRPet2Crtxsw8TnEgApjM4crza8p@-S_{4-r@eCAoBOd7EDHUsHx`IWh!; zCU(&$bSH{VOo~%V`%n6f+lFaw%pao&@JMRi;zZv&HN;(R!4tog7im0PVI)IAP+ZK}A-Hk`jV~xc zu>Tr(o)mWYVgz!2>lJjU`Q`EozhM-`rc0buG{^>(j&`m}9dDuB>Ee?VD3y{Hldyxe zkUT*x}>6o5&s%#+8S_%G^qd^6ig} z0SA4o5qzG1e{J^n3kf57Fn$Nv_Yyce3Xh42E~cXlMx`(ef)jPhewUmW_=KVGSA5=C zE%on6z!%McdXOZPZ5RUl@d*kRy&V5W1~u>noww(99hRBIlS!AhRxf$yi==zpATW_(2@*8we5t3(dg6xe<8V5nJqd`9&*WYL@qkOlIevL2svO_ndfb~7j2;R&!9Ps)bwcoh~^UV?`P zh9qXA_yHzg*1JCxdd%P?$?K;ZZ#Vs1CyfdHM2Xum>GWjD-YjX-R18qx3pANZ?JfERlEwo35^?ptNxPi0LQ_ce!nIQgjAF=I04VTa`Y*FimirA*#|6=@Xr-(Aw zVKzH~)A&*2`}~w@Q_p5JKw0@q-pS-c*+L45pQ!kde2bRxVW&sgNzCbF3b}G2%X_kF zxZGR?kf!8WS)2O=8-U&bNu{n7E!(tJ) zlKP#2WAd;l8I{U@-C9-nR+rh$ z`-;)aN(TF06Xf{XF&JqjZDz>CJ+M#-9!ULwRS$U)d{Si&&k#S0|1HpA-*BUiBSmle z`UGg14qej?Fy(H04^ZY^yus8$*U(L$j!`a2Ov_9tG3Dep*w4Kc>q~32m<@Y~PfGu9 zdjj?}cL_>)YbL(9X)7@)_0BX#{7q?2PMJ8v1`L_H5r%!e4bl%9gSDT}B`4P_m!_vP zd8HZ?8q{6(9e&^6n!z%0GhMx{hP9=6W3o)Yz>qGn)zZ06#;)RLj9|n54cFjFI%}n_ zN7bl#`B}FO^MR}xtl0vt-F$$Bb#t#zE`l1zQ=dR5gZ4Q5;FcI2hS85a@KWiBN;=7T zjD)V`NGh`X|3q)%oPr$%;qVGf@B6yR>u5-%GG}-$ipexbh9wiS_(SRUgi_sZqj>ZE zs_)G2eT)O@^>n!?aL=xJvDF-^HB&fE`~v=rEi!Ep)T==Jn?jpyL$z4Ti_2lT8a4$j z_=U7LCOOuzSmT?nkQM?N^oOa9N-(AMN^xd+(2?pMqm|q*P<7k*HY|BPC%qo8mQ>yn zyPu~5#q9fk1m9Yj)RALzcF?JfEvcqEfoFRmUZ z0^dB#k2oZAQKZ$|O7df0-MBa#1iX@WAjGl=5I=X z4${xv^)ECaK8}vdVGJBHy|y-Mjjty=l;H1m*i#{+d;gtnHAM2I_EI)eN?h?IS1Rc_ZHhSK$yuY4(LahOqY!VC2#(plWL1H1g<-~$ifnhRqSM(2c zyoXw&$Y14^3H`4CFI$eJg9NrpDbvG?gxaFT6ZbYBtm8nlUQM7DdI8SWG6uWYrZ}Ht z9hKZKcM=|l9^4QQZiR~i-IPriO_ftT)#&D!b>h;6U1oHPI8nb&ZPmt!M^m#CcUY~g zkxtr9mRfbXtZQFg(4^CN@fry&`&M!#W6vUJBCNIA>TAh7?R=zp>ApN!Ei3$ha6b^4 zes4M%*C;Qf&;#Qlu8l3U5&$h%i=(D!nG(*ove(|2#EcrZ;v~e7eYYE+JRHF=v@o-O zZ{WU4iMZDlW1@jQ0&cW(e$B%0#4P>AB;RJuurfFDy#kV4p`Zulh*S3wI{w=aC%&B{ zL{jR8<>d3m>>fPuLhgiO94MP6xihOFG48*|r}gc<>qHT}qvD&lZ6x1bj!)o>&ex@G za9@)DlaJ2)BZ`&1Q8~4Xa?0Y%B-Gqp{NK{kJXcyjpZm{3ZvzkEOg#xFatzoB%Ma56 zeMala{MVMM$n`Y0arqxx4}`nu0UdBj+J8z7XZP}xW|dqJ?56;EFrnaXeDp}Bqs=Hi z*A&CO??l1+-MRhpe#o_M6e^zbH+-0yV$+eHlGk3Wi!{*n3a3cyA`j|biKd1`zq~3 zE(_!+q$xeljMlL3WBhN1IGOiDMM|6`$J$pQYPU(w{X4f8L^aq05Z=PVO88*7zKi9? z2g&JFX9c6gNCe5K9W%~Th`0S$h}$NG*td$Q%8rPB5ORYZ z?#(AW6U47yvBbT{or3#_Nyd&C2AigE(N1I!pG0_kWap;lEb`kS`f9%z365|xSNz-j zggpe?E6W{^jA(Y+Za98?Nl>^#Wx6aAoA&9C&&`x>K6 zPOocsSqe-d!y7KvAD~W6gMhPRr4`V=;3{~$$BR4H z7hR`)tNFSRS^F*u56R{ASxj!lyE9z7>i*LobyhIfum(E6wQu#0T^vR`DCP{-1XJ!1)OyFT{Z!Etf%pZU!*_KkVtOA)?RNGgZVMt6Q0>W-xQ_*cyOx*&OPwdc; zkco`0xKi|F2Z`@+-g6C0D zv}(|-?7`7hXT@vacrZIHA={v=6)St;ApO*)YxN_Rm#jAMR~;kPJ$96ole!Oc++cT9 zhi(E@HGi>cH)BHm_6`s-xF|lcj#;}m66yR+UQ!#pA?D7SS&Wq|6HYYzzT?}Lf(=8i zV{nZBds|te{Q$H>;vc`Nz(1 zD%oW2;ubk5LpMkiy{ln|M{`U-6L`{bn(ys)L27@|32(5HDG>uyiQEI>%2+GW$a+4N zF%fSlMMd|MH)KP*j6)CEohSx6OJpn>*+cI)?(a~)(y+7?Nt5q+|BCcgLZGT9Uo?Dw zO?~y=)oB<^OwIeHFR3$p5LRsMh>=JrKYYL~pb#sTD2AHrLEn|o`g6t-pUL*r-wanY zLzao%{?uK!T4isId?QyE|LW6DvDth(HF%<@*}+}@sZk@TSba-iUpOy*W_32=`Rw~d zvN`$W9t@LyP)J1x+p3qmYHr>m%AZsYDrT}|}o(^Lz7a=TK1L%V_}(vResV$6?Kl`OaU zAwaQw1IMb|RjPDZVCW{WsZ?vXC|51p6$fDc1VU+-wLp-m0aA17zJsgaG}D;JrM4bD z4I`uV(LHUZ!dqnj$r>(pcG!CAqF^=K^xSmp(u>K(G+mn5yfm#6#k!{%vsGfWU}6fH zDowx3+Ne!@_MtZK%hbi{Lc`n5w-)J$*fg7kHz1Qk0S2sPxyqFt{0cNSYs!GuQv55ydy>rnA`L!x{PA=&4X%+lp%f7#jVZ2kR8y?ZXNoFbQMRV*upwDI zcr4*eX)b~-RIO)rLtV&*fXu`CAPHfDPYg;>t^9$u{^oY;G`OXQelPR*U^Hx0Zh&{x zh7`JcrRqdGLK0Re(dc;G(EvS=1?{yUm6YEXlL0jVzzjzCb-vqFGEg%%)l9TZlf z|JBq8FVQnjs^A7B^^)Pa@UB9J9e|PPx1|IqMWROs{_=oK&Exr@1Aepac!R=l`r#^; z*T^T(Mmzt~Q2;>(cIb_;dm&VdKc2nPCM3cV_aS9?zxXYv(=?0uBQnVc%_1=Md>q-! zK-5@C#kB``O-Gis02g1=$(u~HtqPIYy?Qqme@B8;<`DvJ4-vwE7{V8jMd#ULuQ}CF zVL|-SNhuhle}I&@2I2f;DFJ$XmJE*PBk}~_(k-iZ^<$E@oc={3kJ%G0&Zof;%5I5} zh=Un5#%8B6{;?%=+kRzN%K1K7`3ZEhv@!xzI;9A zEc?5TSvD_to=$;ht-a-qU}x!MGjNlC4Rpp-iBOttAk_`K3M;nKlzD1cEKp4++o|#v zll+;RExYSR1Bd$_$#lAkj38ab*uKn}bMC(+*T z-;Ywowdmet-tI13yx=-9@^4yIcKXE3Y1equ;|Gfn5$D~D5np><&ZXL}GzwoIno z9^(SrmRHek?$L%!S~hztP{j-)1kkb1PEEC56cJIJ#a56P~A;Y zc0G9Rtm#G;k$XU0Vbcf5zkb_naJ&JX_24HWEmGpmrM#yRYeUJ>KZ3>sMZJkYN!uiA zm-o$eU0ng9ekzHFmQKbpZ>Y$Vq~vD#@tg_=C>q8lSTRB*hI4i?QIb7iX-Rd^2)=Ey zwT@2@KswkQxl~3GP7okSsjBq3J4Mq*e88zOw#*vMRdg4{x|xZt?nR*)PR&u~>GO2-Ek`5agU0603_tbh`!$iCZ{aWmy1HtQ@Z5c<4)k>ZWtgVcI>|F!;$Tt+T?=;KY zZ;&hzSzfNjGe-e_Z|aqGgWBOwE)2v<^Kat%UlP_kwzOsoA55=suJ`RNdLj8S2vcC4 z9S2ibsRk|rheIVVuTa+IB~)0n3mBEkr_z{1q=Me><(_)fm2KBPWB(X+I#@mLSVa&# zeIga)>ghZC9%%ESMZrWUV^1?G9iaa@DNJ0&ZE2$Xb0aVzLQ{KI%xT)cny2XO3siaf zyhf==o4l|LELuSh1w&^cB|`>B2dwzQ{eeEDSyO-WdP0Ny4Q4}OL5a>}OD4$q={HAS z&ceix8qntM(-~e-pJ<9N&?@m;bj238`e$J?bB5Ag1#8j0z-E4KI1UXjm;w54P44nX zA)+pBcC_pBYmY1c7I8fZg6qD=+uQQCg9A=aoTMK$dpAWZm|!Yl~B}*qoZ8;e(Yn?-xx~QqwYB6KKNt zQVo}p+&Yaj3@}8hPgJ*#-r~faxiTz`F$+%oX8LCex#vipZ365)}#6Lu|^;M;=P&v$j9>5M8)j2DuLh(pxWe?~7cAa^|} zx_@$kjXmh8YmOR@;%4@Em@`a>|IOT@vsqwfPox`4&^4gQgs-TOsHv=FKs`>LwvFwSR%xINk*-2ao^tK2cBN16g(#KGR3DKWbp zX`krtmEzxYjWBA37q96vOkGz1#W5Cz@md+>JH_5~i1{~{@)6YhqHt{%HwQI8GFyO? zt$nr>ACk_?L=tDlWNT+$UbRIUEn4eDFBAChEj7iP6b7ABnp&JZ`2 zG4G=+!?BK=bG)BgQ6C0hZeN{jB}M#?m(GYR>=se6t!2%(QTJ+&%twm6U<+c`dQWIO z%e1BAVEhLn=RLq|ddS9j#;F0`VL6*BKbP>sZv+P;*I(>L$NQK3DxC}G4>+siR;kBs zoFT;Ye{;n;uN0oy>FFU=vi|WE^d~fvd;w}})e)N#ld~zwOn~!Anp%hfN2{*kC*&qE zcyBvlTn;Palv{fs@0387Hk5ShQRm!_Q$q@U_Qd|su7LSuBqLM4fD!fGIPc1C!bR%` za2_hS*px#L0I#`_G9%J`;b>p#dLK()*XjjU%SKi_a!QxEqsVNL(-RU843WW*GFB_+ zrt>x(u|H!zAoI#jH?A^NcsFH%>$-}}I-*&WVZLxVc9L&$%sNp1i_M;>9aKr)>a zzUf8IhkfsOR9mEziXz{6-;qYYRZV#cq7>oVH!NiIcurp=^r7U%B)S@2j-Q{RL3L~l zQ^?tV`$uTzi4BnY#1%vfHzxW85i@!iHyLs})Qxhmzp_S$$bNZ1!>@@x#5GHE@Sdg- zc)Q13#@3k0SL!~E$Er8{}Mo_Ae2?n0W(x2TH{XRu4NRkj)l(e6BD zPH+)w^QZxrY%+WAizuOcbw$L~6pl4gsh_7zfXCVA$AZdeVO1!YW=kLxSKD4d3u36R z?g0e=qJ!ORd>EgYH!G^f2f4x_gRB}kb-ldskGf1S)*}f?n46ltKJa~j>41n6++G@B zcOT8z{YM}X9DOyw@KwS7T|$npcy*Qi;E9G$kMB@wS{b+$hzv8eavQl<;9O^HxhLo= z(9^!kJ+EX*Ls6cR;c9eLOV+^VeHu9S-YcdFLYcu>rr2a1>oT@ZsO99kQ#II*mRl}u zRWak^@}m;VGhz+gGa?zh&TR$r zt-x{ez*T>A)2Y;cc_42|VcYy#V$~63vVrr+Jv0IIN}+jTUbX38MfL}j}r73B9&?@745+M3Xw6gOv2($-h5CNcYJ$)!Oi1=9qz{TY&gQ2 z8_gI~W-?}=!t)1eV7))4h%3OUlq4YgZLtbz)A3H7@t%Q6O@4}9JK=dMUls}N}s!%!F45Cf7U z6DDjI=^2D7yFXMn<4C4m4@%XKEwC7TJXaaowr3XXD6+jDCLyK{FXi&{i;x-a3z8x1 z-fP#9p-psAlIOidA)xp;=Lajf7hFZ(ar44j}d2HS_cYpjj1>CS+JLN1Ny}Arq@b#+fyT1w3kA7li z{LIg^M6KA^>SXQ040nM$|GF( zdTrXEd6~Evi%@qQzlt7eU;(1v8ft){CmrNp`qd>Q0wic)VN z3>5-8JKU06;X=XqWhQV>fh+K*TW?Fe&DsTKagXFi$4Eq58K zgisewk*kd|?}<$B5I0_t_sUuD&Ek@Xy1e*PhScN+ds3CZ#~PF@Pmw2tZ0?!gyxNa% zPc&3W!|Vq=cua|?434~u_-To}bd61L!qd%VQD{`gcH2Oo>h7pK> zaH7t|z3Cdr^&SHIuMk=+tepDQL2#=vB|#@h(2OY$*Ixx}hK{elCjy{8+Zqt_zMj(A z@X{InTkW2E8}9E40et3BK{D6E{!_}V`~pp`VvD`YKWh+(09$yT;+WPewsme&m==5< z$zlr)a`A7?o`=3c*MMaU!)K?F5xR7i9a8+)O?M{rPG4~FxJTI4sxu=rN{{U;=Rct# z&~EBjbedY$L2xH!(1aeI@%4C4Fx)rHd-RVmVC1X?7to+wjH@qVPPmCg zQ2PJU|9OL1<_cKa31}^`J%(q<2m;E zQC&Il`!B5n+(f-|V;Pvx`We3X9;B$83tL{pX*dPzGm>Z(`4+Af?5Ky%R$=TA0o_(+ z0^p}^ld<~$R)Wz8{NL0Xdr8P>)?X(p+61{X)m2T7{GkQa7q;fo zfKCb;&6W$^f5>v^pe(=+MS*q+V;Ynca%$tEbGpwKl85Cmb-P%ar~ZjX!k+@JxSbCd zN&K@%(lt0{9+WrZ6^O$_Khiq=GbpLc-=QM?-N4avVuwl#m;ew|Jjf;H7#a)&N(m1_ z%A=D@!3J`gHxoFZb!6 zs*LH)?oIT1rwvu*u8LWGd;sH6ZU?ywqBYV}sdqu_`z|&HMh1v+KR!~FIg~O^$Pu||54V+%x_uWdr+uY|KX9;8s1NxcU6*ST{o65d$*R${P5Kf z>(yu4cxEq#%lshSe~W%ke~PUlT9UL#!|f5LjxpkK%GV;*x0O^R%f8n**QnXP54B+U zYc6(WA-+?zm$rh@K-$n?Gq6>DtcHCE7UKVTH)6z_y@Ug?+nR9}70B;cBS(OnY{TH* zbGfX#Px>vTupqBwgS%{mn0tcW0UvFr=e0v+M;v^Z!9uehK>3|_xas8%ah`394-XtY zwr+1I%Cq4`8brNtjQvL|bT14IV}<*|joJA9tN#LS^y;RHEmiDY2|Gj8EMO{~O%GEL z&-hTyQhJp~j4nYaJHT;GIyVAz>Z~l7k(50b<1Ce=dm2X?PE(x``LI?Cg}> z30VmUQAhlmudvntFZ>KwU0Q@&<_MvBIob)oQB5hI>%$pD4~l_MEq@+|>jfKmL4~$6 zAm~I(M}`C8(2mq(6h;)x(`$TEGPTUvaiGj`BPF_(-;F4F73>@iis!1NBn5VTvKKz| z8~ZO9ulHhH;4!e_8s4|#49 z5DI*ftHtCEutab`x}Pf5jYr3Mv) zlL(CB7Z8Gl4I11Sii&U_+xqV$h0Z?&!M%eM^V{nI<;T^7SZklW*!MJT+A3MlJONHI zDYxR@)qM$Eh-x6rrmpc=z)OawoJn=;`lYWaKIKi~wOeB!#;G!5I-79R2VdeHtx!NcYFLx}lFPW1s%pE(OP*-^)FZ}@u(qb)yXew9Y1-MN3aHUM0 z6;1o_&1b3ej$=RPnDoGr0%!bI<8aTTE5COG>1LJ}cO8X31P;5-UHk!4q?IWq8hQ{> z{<WJBE+huoW@Q7Kt7UUEF&ibORW%Njjol&#EZ zo;&6Lbk{lV=%OBC(Hgh+s4U!psyojgn_~ZYh?M@_xo>DBgbTv{&Zss|*VTL#Gefff zX8e^{3oH}#hY;v@J(yFeBC>UG<4~5lY*i;zhAy}W4{YMX;1{-S>_pE@5ZUKAY)ZmF!)NHg=6&H=vB*H@je=R}R zY&z-x*Ay68|HBjgn$vZBJp@pFsUJd5Uh)5+BP)4+9x{|D)^B+%v|N6wCJQMmv&zuW zEaZ7O9yV4ich>IVgb$De;?mU~)@N~_JNy%=1eTtvGW|ngE{0t}OQ~Y45r}uN=&vgl za}v<;5Kt`fdg^$4dV`eJB7UM6C4l6EIJ>}z1FniKmeDtH3bt@Ly+L1=niFL%0z)Ky*mu%~*uX(C-3gRqB zWFN8Ov6U3uMLOOEV$RP#^gP;G@gkTWm)1}szAm6F0Tc}ixn_dM3||LavF+%S@|tvn zd$Ro^V5AK18i)~bGAfH&WwbbBh$1pc`l&Fww-(>J_(7?oZ~nJ3?}BUOgav}a7Mw@D z2|u9N-@5>PZ?~?po<=J=-FxUhl2PE}RZM6STJ2Bf&0woE2c1QY;}nPlMP@Qkt z3pnUndJlYNrQ`p{4df=%8%+N*4y)A`f`m)6-v{aS`uJQM`W*>O2$_U(gg#tX#JYRG z+V7=@U*JY{O;b4D1Q;(rEhM4(P`7t`Ykj1&7O?As{<7~`pgy#PUbXgAR-qholvgJ! zFNQ7l@Raoqb}ZXN8Ek=)R&OFF9dK77bIJK~t?=_J>c`Jl{hX?O@%A%q5nWW&tc4zC zm*JLuWlRc`f(hr1ZCErS5E(kh}kJn#(hrh7#{!&d)#4%u9Ix@pv3>&+`)|4x_nXPN9DZRr#ABRmLn~Kv-BrX0^N;2gd-G0vEDX`&5RX$;b z8vYf4^+4;t)pMuF>oB-f6353mk_Xs?!iNcpTy7Nv)917J zXpTDt9K2Jf1Go>NXoQm9iGDVg)zf&GF&Kq!$klj%Q0njnszd%xFjEsP5p4GP`fR!# zEH#vHe-Wc$3E>J+hJ{q6=VM@;(zH4Mk1IEJemlAQ!ke2)$wCkN33i-2!cy-z(v-jX zU*&=vi?OYslI=u@1}=ASva!HKaxc{E*pQ>U6HauvdB_1+tZ6V}kI06&{PvKDOEz+$ zN|#mJY&;BJMnfVTv%>)R9RYZ$Bt`>es`UyoSpgv%xL8Vtqj^}3o zGMbJluh#o!7bS#~jSopi+NFw3VP$#wnRPhd#iIg%e(Lrx*lJ6#g=MCnl+WM0Shyn1;7w_AMOE9Sy%w+UCje$|$5mnrU6 z9{L`(;*Gw`Qlkqc0>S|=c*)sq&0OyEFa5}FIO&b95QSSxK8dp9GbJju#&J0@&2c;lANkprTK`==z|aUa1dhuz8=(nGupobl|2S12{Sx$tj@E32n_xOsfaIOOH5++x8^ z*}nfB{@LDjbG9Rp&1mXje4_O5cv<^kP(gB$#M+UF&oT7KRXA+sa@lP3`5spG`aMZr zVNK+r^tZeFq}*o20-*gA`tKFHPwPicVWVctsY3K=4c-AkhL474?kl5Ixx{dLiM8YC zN1hDCsiL>W##=woJLbKFxA5yv)0u!!NS^h~Pjin6cE>So(aSzfcxldoKhbsDx;J2? zCWKJp2m^Ik8-+~YK+TLbUp!-rBbuZH#MxhEpwaL85ewjYi}xLJ3y7@n-zSQ57nSk3 zNJ~qL?)0?I6q)yh1DFMOygm*^X~8bHo0tRfHx4`SL2vv`ub#M3Y6l^}Wi&^n zl|?GC?Lo_Hn-G@2_6$q3B{<4z4|n{G&s1wTMSI-9@x}5=m?7@g1l5)fTr2lW(RkZr z#1nB$HinljH;oKkmkK6y9Iqc2UUjV;ytF@}?k_y?RL)725d|F*br^%pS!i=I# zbE%`_NOfNZu;V9puVpXMe6trqvkuWRW;T=~gp^-XzSRwSpddF0ED72GU4@;xGQGCN*$}5zWk>{>Mn7DZ0T85I1Ns=g+3kbh$LvW#LU(a3q@5FP`asd zWpPvi;(PWHmfxDyqOI4Tg)&uquyhWub>3NXI+1(2bVnkil9y@<^tBcu&fnUF!)L@W zTqh~;5vL0qLotjnT)*F+-xUABuJce|#*D7P}TUwILSHd(|-Mo7wTs3VVQb!6T0GicT7*$~mt?wjhav)Os5RRScjdIjd4 zFhv5h^O#5)KX?VkeuJe_G0`NiRLT4WE7NddzeYflc4R?5CgkP5M=8$l;x4!m`kPQX zOyzN)DB+}41F}7NsAv*6C2vL^`SE$t@JkDk=Sd-{Nna~O^=gW1bB~G6-R&dh*>O^` zdR37-$)jqJq2i{^dP@j~-XfCfxBe8#jQ+gGfTTq-v<@UEp>Q*uU_r{Ad{j1gzCdS& zu@?={?XPQ11LbJf(E7Uz=-f!lqF=Kxjj|$q^v5EzA)xqHoVpOrZP4c!k?!xUhqz8V zHygaqR20l83o1iPbXUxP)sz@IODg5hTuFPzY)ynrMdQymwGP97cw8?>(PNH~Gibmy z=Uqldo+o3IwRbC;I~qwj#J z_avl%qXXF=+_{-4iY?0?=mgd!ilH04fht1PP6TM2*2}YuLBs+l+cUPpddc6rkX~a* zHm04|Td08znGR8+WaN9$qP}+GLtuu|NrS}UBcSZbT=N)7;r!at;M&lDGq%k(K@-R# zS0oAKK$nH6Re$xw?u_aE=C{iASWr*viP3F6L|kT8V3-D&da~;I!;u)1i_)?+j3?3l z`Ea)6YCcBJqOa{??I@evCPcTk#SG-GnaFDLLZvYT6z%Datlg+?;;%{~A^^cdC zTQ^EPOVD#Bzc5nDUgxppOh- zpuezdXk;cn#M%;%zs8U|%|`7)1&5)ru^|a9<|$XZpkUVa+XY?r5sHx~E3{I=;V6_A zHL;NHL9KTGDqNsXD6SQ^D$H7*NEM zP}bu)ChUn_jo=?p%)R@CbCAZ?0m(!2U{x#Y%)gVKiE_GNban-_PLyfHFqkHkCz`%n z6$*e^aWRpB&YsNlWRd*`!4TX%^DshwCF6nay~T0bKf_b{n9omD1byMtBkjG#GL3bG z!`{V~f(_>bNfdP|4Mk)0I3}L0n{C(GP%_f&Hqd13!!9lE`Ii&Rz5)<0eA-JzFi;qs z;9<#WP5fh9dx_EOk7=m3o#B^mlazl`&o^f~ab&Fd9%pu@N)(2T?L4l}@uMtWn%x_^ zfy}QbT=cmNntY5mnYGZ#3O4jpH+s+v0^7K3f|XR20))(ZKiIpU zj;@aeaDHSqK2otnE&%T&%*0-xp}8NW7$JIPrWXpv+SMQAvG!MKV5*I0{`r3=mq{nP zPa_)QJ2f6Cl1*{uA%S}q9EL8>6&)*i1kk<%R@~u-Mgy2uh8U*%n z_(`xB9Kv&S8lT(m4bjWE9e^UVi_;5(9l7;`ba#ZZcvg95Ah38T*zIuoDV+ zklepJ(T~CB(Q)uciFv&}MwLT#LAQnMOhK4YttW#!dTQt{yyXHKnV_C)dwvF;y#f2u z{@ENH7@5Q@1nH=J&C)Yo4ck|jx?TMgPVI*3jC;;xUIShhGvDQYK&6lSzmy1wvEJ-G z=A>XW@Fp3!u^puOfMW_(a2A#^aX$ZycS=DlfSDueFIZnL4))uQ5z2pJy-F{J_+-WZ z3LS`gF;Pv?uO!{33|m zDzZ|HMkA@ouj_=Dd*~6~X2$%ShTT0XeTpJ9?4jFU*}R?;RkF>VNatK;iVOmB63%tM zSx~X{fkb>wb5xZiAt^E2=doQlCJ4~nZ?^}9s`+SR7G<6`sNs_N{k{mhf;6fSM!-kO zQ&^g6*gg3ixdbI$K!v(O0-+Yyx0**JrX&OT)lVDq4y-R%!{*nE%;q{UW0>eWVI_EN z|E*^3O;sVB%%Ty9N1t^;jX)$W-t-yT45Zcj-oseDTLjqd&t~x06UxW4*qY%sls}g6 z7TVb+njBn){6tycQ;@bMDiXH-3%lb6xx3q@UBqM$0vJ= zy^8os!GLpt@xry6Wv@e@V@;DR{{HBKQgH&AC1_}YP2s_MDZbCZWAd3gbE9+UPdBO# zihR*!cQFolAL^8lr>CdU;cNs~_BDmCLGlB}f3m^(x8$5o#JTz)X`#KB2(@`R7kgqM zY^mRyP`^9PtYN?rWv0e5lZNV|=kgS4>FS=}Ql7;&C+u@LM}svoM7%74Gr z_Ec~RLa_6k6O?-<7=d*+=vIduM-hs~^D8Cy%|}OpUI{x6!5gxY8EK;ALxYC+F3w}9GX#YOGWnM8^ z;ooazGu3M{$XO{&hCLe*yf}ochjq4c; zfr}=2O1ojvacL^MU$n$=c{1!;9D>NBwhNl`Z7~qpzz?TL$d@87yABcf@Q|qkkpaSg zVLMXFe3u9Id~NQi&}V+{4dX_f{_FY8mzDPnezTmM&WtPV3)Nkx}!j=5^!5cgU3|9A8n6Po4)_!ia3GCB)R(68Ae8 zCoj;BSzOMC;Tc(e%8wUVTsF^GpoZHkGA79^Ak7v}I0$T4F}5|#Ua@rNf#$${^rZx` zYT?kXRcfC}#7t)@em2nqYUTo(M`E^Oy^e|=w5ni~ipVbi9t3mpmFRVEj=q1O*g{FL zr*OOs*tIA-`4T2x4uskp-udvG3)vserg1#%aG~)Nj5qLajM0q=NbtT*l%7Y72zVyND3qCqVpLxAm%@2M*6JR%K033;z8p7^(H=}lnxCI{ny+Dw zSgbB>po4tnu15+D>Vt`S;BHXu+VjO6DB=$`N}#uZKREpSAqpzh)#%=gN8S&3lW04j z9mv)CUG50-_x$)2KVxTv&c>2$-xg*IJN_6|W8@OUA^Cl~SlI;oG`cCAyGzK0vnOzHAZR09gw zLj~S%3CcJfi;J21nHm>SRf-ysQNsGTe@?^@V0UDcYetziO{Us-p*X~uLb5^pV|{6c zdhUE6b!i4>`NYu=KduF&LC6}pP7piLPnkJV4~TZ?=6LpJ$nKnb4{CrO3_n==D#?4= zW^iR*4-M4Qtugd$!)T(PmMP~8mN4(^TCftrXkKJD+e{;oJ#%_N`?i&zZ8E`QeclG7?*%)e6(b!nI4JZt&=TM5JzvW18*G#l zpZC&mB`awss?rMN>~jlwy4vDRndfsXf77B*dmHgzg65g;36lJ`G2n3wuq(%Jwrg)8 zKX+z(C%LA;THAVmmVbkrz{Suz4()EFla!WLpwsKxLrB=?`5p*fH0looW?I&8i^hQK z0-!79P*T7R$Mswp{d_}0kEqx2LpaRcHE z9Q?5DiUXViZ&)j;YOnyjqj1F2VF|Z68lQ_zRe98`?$wCiViZo)&!`?KY zff}(^4*1IJ?_#7zO+m~UV))YZ$FIlgtuMACOFRsh+$5Iy)^XI!K7#lael^ z)gUVO9f3vD?FDfvokY=5P2zn-EVsQPoN!9Ru%)H~-jr(Lo7Se^hfk^jTnj{@{-O%h zhVLh^&+`TEbY`D*7f+U!Cd;nD(a#MbTSw9}&HW@!Y)pi%7$?6=VT1j@{}gS`p?lU* z(fj_OXZX;g7RTC&>K`fh9T`Spa96t4`zSN--3@nSfQTXb#jPQsaFgNB8_^}ni;LJ_ zA-Rob5Nv2&R>7I!@yE_C|MuDC<=K((Jj+F=b9yjWDL2ppjJjjBfdcbA=OA& z8j9peh>WTj@x8!mf%K41bmPTJ8XoqvR;?(z7ApYay~iF%r#FYaP%J55mQOwkLi0={ zwb)D!c%5j*Skp?Vx2%}009}w+FX`2Kn=bk6-4B#j4+7{nWDlr+Q39%P=NwIE!I{zc z(lT{)LrKuU=L2rHvpQ;kZn)3s6|kG-K{u9rolVfM3%_`uP7urnQ9@&k^+&@75ktXJ zbL2(r?ejb#x1T=n_9?6pb}vO-$Q(2)eBr1+bNJi;99a}}VWUi66`5zDXha)6O}_@6 zf&U#Qj-pZvs5!EzPzq@p(MBmHHPBi`8rj)(C36c7=t2dUN5K*^|BXl_-G);1kDL_$x~G@eF53Uy~kp_-dOZ+(#iJoOdYN z>d$x{xW&G&gD{y~c2Q&+P0B(dOds*vhK=jKW$8Ux7*~wHDdwRp7=I@;tud)nAMzmo@ zjqtC>9io*MAl+uHmDPqEFu^h)i_*eYB&!Z4Wqhfrv zo$E#m98YtFg|j!5V|Lb)RER`QPED3iUNJpv6c8}0dzDV z@eBA%?VK;%_T38sD*>M0b#VI|#lEs1VmwM{#F8sOfgN+dS`tds*P84QVSI}c8;cOl z8^aac9@kE@9A*H>6hAvQEJO<4QHmUH$U`gMBzPk+IegBqQ2XkNiW1=mOB91(A`tzs zJR3ZX-2*88m>QinHa)1*|26K|vizVdEw48gsBib2LHYu(#iwshG~=WbxUV1FpS?zD>X%JHR1M6y z&g4JbRh-^7q?tV$ zK#K$C%VH`4Lc4Y4%>mn;M;6dHLFj|$AK3ehL&jn@s#l5ocOom-1Sx`vn&IAzRY*5% zt`xr?5ZWlaKUcGQR<*Z5Ex8fm@Ka)}eXLa}CCHj{w311`MoQtra*lr>zDgV}k$RT$ zW&svWZ9-Cj;dT9ArxqF6YO~MOnrJJuopZ?m$+&M9Kf~Epw~s^JuR>K!CKG%MM`RUL zvM)f*;$@gUtM?XkIm*Jn{CD9V@uNBNQb}rPnqmDhLQ!`zY4_}EgS4mg;8+%QRzC^K zjEw_KPxUjniVZQ^2btl0m%oM4;(U!on0VkJ0@EkPxMa%LN6UZN%Sm7kjklF@l1*Ie zj^3Iq2}i7Y;{SehWM(>Y611h5%O{_GIx{5GNjt^j7^#@D&Y}fFd?-X?JxduB=XT`w zoe2nf%2&K09lQI0bOPzJCOD6DHctwuES)(oG1_RuSp-PH&&X_atuUk z%z(Kelrx-W<9^|Y8om_NCbx_7o+qMmcPM{(KRMa)@QiwR5eB0?Mn5#68q(wJTK`<) zMSmGsy}kg@zA}(L2GLHf;=?~CF+XTGg|sRy)oFbpEE~_Rv(K*&?``?VwZ?KkseES7 zV&|UVI*UqB5D=H}>_r2ac&XX8+7&~OAOr-7PTv0sD#|vN$+B?-WJGTr*%j-&D{l+n zKD`m(!3-|kFt(q$!O?<&rvnSl5tBr-^-ig?RkPa zCvmdIb^6uetD!j*YRjWYf87omzR0QX`?m?Fs#=J3>0UeKn`J98y#ve>8GMpq4*o%m zX4dTkk?GRCDFx09b@=@V{ARMZM~C6k%n-T>3jC(I7-$at79kl4tsB&SE{N^&YFd>! zrTNQ$_Xl~*dmkEu3>lMmki+OQYkL>T7Yjd9IiQ{X=SF&u3Yx_1W72##k6^AGY@gsu z-Y4I!oNP`85vW1%EeR??xg*D?0Tb_ywe4Ga1fpby89Q<$wZPB-A6yw7xqL!0Hqik! zb~KO*9%lIrdA-}GIx5uwAN*DAUQubaHvXu>`=gE0jFhL?125~EM&C~9Zdq&}cak*y ziW_1=iSA6dIOY4fZ~L80Xouu|Tw5=iN4aGF49rftZ=jBd#kg!F@uJOJ^1~Jq<_v6) z)b3m8}G%7Xe<)GLmSqOQmteDCcLlIub7Q;f%!JBgevYrfvI~E zPU+ch;>2O94n~yKa|9JolMwc5(JDCauw{}7f(3z$3jOjzE)zL*f~t&QxCO_XggfQh zHLi6!qj5JdJkn3R$J3lH4#)2Kp_e+uNYk2?iwy;d>(E{7vJQ@vwf0vVs{A0G-6wX6 zDO|GI!aW2;d(=hr>w z(#GEfB?nvT%vRQ53DLI>ilYK&%oF1&T+>Bpy{kL=-}zRYN7zQ(XDfs%qtVD*bWT~l+Rl+?L80N|*96hH_-wv*xuI|;>)my7c4gk>3kq+kodvR&Q_!MNg zf3F-%CY3V+8u5RcG!D>CGounzS@BTXg+sWmDy^i| zPzP2nL4O1NN^){r+c+Fy9Egw?W<6AN>qpUAq*90FfKlGYrUxDIhCqjNe*}8rbojG| zk@_5@;KM&+o@xgEPYcjF<`u{;6SJNNxjz+WRs&~%!lQYjS;IbgeFnKOBYu-kCS;szwLGvO$XUj>x}>?}VoF3@z-fwZsmyckVQ~>9X#ZpspCZykb$L1tLB~>&WFUbvzso-(XJKLoKeRF z?bRpakPx(OTK@+!LCwD7Iia{;)8R*3@F1GbCqi!q{)D?e z%5-x*h%=SslFDwNGdU=ET&W~tTt?w3wMk48YWgP)fiaR?KX43G(^kyyPy)XN&#XTj z8fKwflA`rEL*!57`yBd6KhOa_FQxS@JnS$7j=vGEO-_u)@7n-uXoEXN?a&lF{z}^n zh)*=hpWCNc4ZJdo)aSQspo?kDJS)k72$petO)6H@f_gYSxl`L1C?qt~a*2Dfi9oO) znGGx8AO%Et2jy4JhkgD>;5#W_O>%@meh9_aCLIu)(Z+(JTH0o9H)pXPh+zm6hwSQK z<4#{L85H3UROejElq8i|_Tf|{zy|UL^ohIk((4$a(>dAs)U0#8ta#H3D_jjMtGobo zCFwh)NPzqYA_e%JI%5`Jg5}!RmiQdF5N`ilswZw^L6M&T$!f&8m3K-+JungEYe%EX zf+JyHi&!hs0D1-aIk&m>P>F%h<_fb-3IAT~FW%+E)4Ahu|3j#SV2=?a`M0Lz~cBfxB_H zwzsh?Rv*Q^6=p1M0C85mKWpt#WEiVyT`Po-LB`|vt#ms<-qkntz{51p=iBZsD=TYu zI;jQ(SCFz^T&aO?PilHJkBVMPIl5OHvjMnDW>8}H4#5B^^&uB1_4%1(d%)7AUOw&e zSM(t7v_u{z-*>&ULB(Dy)u%Jlup+lS4;?4LZT2im=2ah$sr@kJFpS7Ii030FX0MMo zqzwi|0G$TRz*$oNS)dT)n8LIMdIB$oLWfR#_VIh0?T&+|H$4I-dq`)3i~-;Cu`bYSxd}bM&Ow1YWd_A|ZnAb3 zM;RHcBQ+YE4W_|q11BaH6#$@o-7$nCWx%Ku+&c-h3cem9lS90kf*Fpq&*WOqlFq7u3TPkY}BQ(0~;fI@* zZv?LdeT?MZKfi&T-^Ti(PA3;@=O3$$>`_kOdMPGhy=AbBu_*I2$%8wAYkhv~m81mL z=7MJdX!7Md$*~(#2Qw+xVSe?FQJ^o;HV!)*fa{?6iMUrSc>gf!!*?S50DBuMMVNy( zbVKt-hBh1&0W=p_MD%FChd+SC`8Ly=vDyROu$VVl^7xfT5EcAbj3ptHTz&yT?70L=-}{PCdkjh7xN24Z=f?_-hl*% zE6M_dBAG$LFM_Hvu11-G%lEtpzxVw=duJXVRh9MYcb%#X3U&+5h>`&(DnXPPM43cI zMFkRYYEel7B#8QTJG7r|VYiCfw{3<938OQq04gfB0|Z26Rw1dV&5(dow=&qxAQ|ec z`$q)?lBzoAR3)JIzR%<1L)G5<>?Bp|?6db;zh8#MQ`@5Qfr!5Bz=OFFklV@h%(2(U zdn)i5O{dTkj_tq@@P?NWeC3N)xN6@5Iykt-HcaE{1FObkJSYO_*#v!Gj;taG5ZvdB zaOc)D3I2hd*e%`%Voy!j=Wh5~sBq4IYCorUG{G%W+hLi*CMt+II@uqwK{29s-LM&w%LQXf|f%)(yv$R1V3r4Pee z(z~y89Ej7KaEy^6GCkJ!KY^eTQB0-7cuehp^tXaeHkkz-q%0b?1F z10p~R)6jPYXXoAY^Glx3z|#@rs=6BR2DTXFCtxAyaM*rP#DJK??N|?p0G%;dVI^fJ zJ(ITrZqg+vn;$k!N#G}7jyu44mIC6UjR_Q$Js0#~J{AHZo2ZV@w%<)2%_F$yWtRYOxotPT`RiK~aR0N>-S|Xip^UblfcrB;vvWtPNIp^Qd8){aou{8P zda2ieqgq4bjUbVDR^V(;Xct0PDIjdBMWf*JcL05zrcJvFkH}qQcx-<5pJ)M9pVDa7 zsg~GJP-yl=%RPK$pW^H@6N+uWu%Kuay#!c{voHOlP+xe`5{SarZ)-;qm$kTG@EHaj)SfyFh~k{E802Orr4i>>Kx$sCckz_ zx*86A%a8ao-7M%hO(qT|flT>*9@srGrL}iz)|Da~O)VfC%tVlLtgq zp{VSBROfO`1w_8aZQN!*3t{|S9NmDp65=}93WxyGIHj|{vY@Dm1OuWIZMt2+Ew=4R zk7QWddEPa1!QHC@4f*tm{dD}ab{*M)N;WvHZA^V~r8a7qW zF~`y$H5N<-Iyva4!0mM>-O-cTeg#N(h<1-lKY;4)WO|%w!~sLI#||(9#%iQdMF^4+ z$+Fu?vx!3~#+mNn27cmq68zhRzD5Qv9l?nX#;=d>volF_F9Qb0WV~1+@+XF+g|J)= zg||82guhu>kZa$iGFT1ocPq#%_qI9327YJ;1Htwcy+t z219-fe@k@6ujrsf;|V6_-fXkuTA(LL_|x2Z{)!*xMs?%A4Wu9O);{27Da6_e#piP&?H)sk;o($FF+r~+Gu@_?nYSIVhNeFx z`Un^d-q^^w1zj;Ki19YyuWV0I6vPXRgvdexKu*|zIB&!Gkgy7o+p+$;a-(_|&GxRs z_t}{MWM&2!mX-^8H=*?&{|SK}Q3b?NccmG%vZkp5B2Z*IAbK${c@Ad*5nzC3hZYIZ zkz9bu$J7sq5DWtIG{>q)i04dHqE%YjkG0RDeMBZW2#ATm%QSsJoB{NWSwIBpwyf;s z6GueW{mO#k0c0k@lh$y8u!SI@b@_bjlQE6uQK+1>2lspUKKc2}pP4=pWEk~5KK=pw zyO4R|mkzJR(5$?x3^hN57?>(qLD`fTFZRpGzU5Cur$XKzSgvF0pFCx`AaZ_COH;T%xsP~@eUx;wnW(zUBztW%xAny?T@-!eBBDm(`tY-R9 zE`a!%hrN>NnTP1lDLW#Qp!^y#J6RLWY=9v-dE?aJO{~eR#eynDhrKj;vaK(==5Q(C z@L2gSnmlbO%`PE1v}Uu7y#H!iTV~pdz0Z=PGL>#fH#if+{-)^62&hT|qX4>{48IH_9_Hn}X;NGSU|u_S!tZ2a^lQ z^jKDrK9Z}(j!iheMhj4Gi+YZB5E(mv+TQ7e*sHI6o`fr~i0C|86g37jtewr1TW<>R2~Bt7 z+yt&c@UU~AoO$KvV44d#QbJB-5<}bFeK1Mg_wjM?o41Hc%vaX2zxFbIcj4au2ZADi zwg6rucoKOhNNClQMH>^?Q&mZ9o%cQ>GcBg}2!pflK0~DTXowKJIlo}aAZ$FcCDc<`WCs)h|e(t4y}=h#Rq#Q&-}4UlTHk`lSsmh;zLPYRhltAR?2<@Qkz9sUQx% zTSicAswsFE5{AQryBv>DRCWczTnLe4syxq+`$ryJEx-Q72~^d-fHS50w@Gjh{h3B^0s?xPqYmVt5&-J=Hyly%lK>=9Z?$YOot}#$G7YuAu1y;#a0yM__jkCyW1S zq<{#~2gE91f~9~6;r4|AUWeU%!&a5t+i)A)IkdQ~}ZF z&qjEYPyrD@_#G`0-^RB#xbnU3{M}@xT@R82SXH;$4w6H;=b8fnF*x-9=WvQjA4lZD zn2)R6#IUqiq8(;%&e)-XK1b7^9@eVRd%?6RwyGvHi_Qoi(ljdMcKHstK?+%AyJvA} z4^&>~z#gW^EKdGDhdzD6pEUbQ{{Z47(RvRAI@v#r8-Eq3lYCIS)Pmd%kK{WZ!RL=h zn2PEh);nn#7*`)wJh5nF0=xI$jBqW;r5s){^*yi-rHGn@R~xeb{r^GnNQ(V0pn6Np z$5kEAuyk(}BgoL4-1`)HoTIVAS00>SIK@_tY+dTir!>`xxINYrRM&A{+UfJR16_nL zmyEQ?22Vxh{SotRoa}w7-pa6a>ylak{N+rjb*egVhaDgp@QCyDn3umi8F%eGhuaF@ zV#qZN^)#Fi?(;VXn!`^&Svk=B6_xqL2-QZZr$ofAm&rj!j2Mx)JF)ppRc?yhx-rla zW7qfgY#9KQsmL)I5Q~8kR=i!plIbbO_wfZ}Sa8XkzafQyS%jm*AelvGn$3U+Q2Q4e zKOmNZ>pFPY>6QS@D{YTi^E!~^aD-Nv`T@oMOfo$6E1fbudk7Y4SmOVYJw-c+s9b5k>nEhO}@?rX2TRYn8Z8XN6wU)SATl+0Ta>m{$ zrY<4&C*hV+(Fv~> z1>ReL@zITQkSgFd@Q$vMs+wz>`gU6GR_o%b8SOp-HpO;U^2VWN+mRX%9hK*;b0|BSfo0XnW{uZ&uj83Q zbjiuFW(96KNdaq{{L00p0}#WhlKVU|JrUe0Eu-M|60By4rVjf%FfIe{^NvUGeRd|z z_AUpt))uCrpCHCK|CF;A0(i!3>x^N(20Z4UlJu;l zfr`p*LzokzX(=WbS-Il}J(yiDf;ve+jU~GR;=cH`X@9S@(@Ap}hBCL2=42n`6`FIW zJcA_>(@twTooATF5K&1Zcw!z0{0G>qqD$ke!cY)h9&Nbl-5u--b+%C1OeRt>UU)Aq1{<^vwuHBTb&w%T|>-@BSp8q0D zeT~L9`7%K+w=1qPQt4>GN57(=sNxyB?NwFXSUx4`4~M?fKCd!40E<-h-SShC?!g7F2H!P2ZEQIa6Hj zdkh$+&^ko3O`rvFwbg}h?|l1^d^~R#g0omr4;}g5hABxE#JPHkaFOSStwl*AD#D9}iYRVZ zCPCqdX!r+OUT;1}l94^;5=_?$j($J{(aE+0;#d-JQN*zk5ceS5U?m{-F6r&=vZ~9o zD!LqN6(7xkyQ|}?H*{UuHL_v+P>&y%(GApw|FS#~E+BgG8vRbW{z1d#{!-w7!URNs zwjfW}xD&SYKlO$~EEK$}!PO0jbf~DnlfZDt0Z~;bD)l1LL?&Od3@LxyZm$|Bq)vtW zSFut|ziVqp)6_*S=(b@>QpogxwqrLYBxu|ml(Pf42Yr#>I=snG(B!-r=xKXW!gHu( zNAhPLGHPrVs`(sU1M(m$8&0U+-F42KISrMth3=NcB*dylVWYAIWsppdd^77^(mQeA z@%vsCl`9=yD*y~%)os>xcT9fd?17drF=uJy8}0!7s>@v`k#9Jt2h%pIEbQ;F8==YLzL+! zCBV)(!03({5WcwaH^f!liEssQ3DE42cmD;l0?Z=Vy)aTj z?@gmex2RH=52e4IIX5!5&M%l^t7<|=(i(%$fRh?&PF5)d@}v+;`hs0nb;%Hwus1Cm zeA#km%M0Hg_;!`0)y6^=+RDR&LP@ZB~nym%K2qiHdR&s>s?k~E`&y!m@64Y}-yvw#)jH)S! zeBZ3*IBUY6!NT!p>i!E#KpE1UioVTmRt{j5N%#j(E_9R7R|Ekk}tlKh?@!eWoP`S(BdD#+(w5K`)@{d22E@7wFVK}zn^%ROam(9 zzu2JIE_bU3(Z|J<%T_#dmMz~{PhMqT;6WoG-PaYia<+kdbNgL@EqSD&*<%MNW)a~e zk(!==*3>q$Uq<#Fk0K8CT4TpflwnfH`*u2QJ9}F{Vw^{*qPJr;C;|*idyZoN*C_LW z&%pm!xaKv1>T!rP3)jXb=j}M9W&l4N>4^A4}1BD8J@+MdKm%NO=M<< zuPYacdETlaYS#)lG_QYg0qqshF`R_%4RlB!S;>iu8jO0n4qGFj0t%O@w# zw$rcsJzpL7-J5(`T<~S4B06NEXK6R+*1wi;iyCboX&_Cd`8%{Md|kh#YePQP6qw3#QmABU3L10bT^oXryUHqL4GVCW?Rv zjpR|HqV?PbV}Xfz8jW!vDx_wR5#5cgsQgk4V+BVeAfjc+e}C_w<5LRV!Zr9s1;w3to zptsX88j)@BhUvtf^Z|s(Nz1XP8s$zYBuBhadnh+bKB#G!mi7AVAnIzYgm_mm#m?1& zeEw|2d>_=Qe|I$O#U#^`Y}+B^CYcn%Az?q5t$+@EmDQwlrEr<`reTJ!w7E0R?7c|-K3vS-^i zP#_;9ZEWQo1~OTNEBsTE-zl5YW}*mpiSkanyvj(G2nQLOJ@$_vZ*XjsywyWh9xN`% z4_jATug_>&0CQ>jFF`2=on#tIV>O_Pmc?qUL#R&NpSUt?3oi!V&M9^WuK8g{1;Hfo z*rJ5C_zgwSgZUjqj$ZYWGM9`rTPi(X#c%}!q9qmbUqnz$%iVensy7hl`W-Y}0u>5( zjqw1YUp05TS2x^3#gkY07d5%h1(%jjPCDrGP&PSf0j3NVnB^H;F~ZUX4R%0OBBr&7 ztcjw3I2J7^oNP<{RY3?ai|Cb+?V&6pflFi>m9ZSwG|Zw{j#tMAOZzO1@c7*EJ}^JS zd$Fyu!lf*&1+}I2Vg)hOmQE&fKR*}vm{^FUIA;@yZrj^F-@tgaG4EqOD7GK_b!!!! z4sx^qnX`7=UW0A82Ckg>OVVXe|!A?`066igxce)hD18JIY0O7lU&p;L}X)P^$%I= z3Oj`@DH}3+bbCzKBH&DmliX#fIjFes*@JboLvr%Q0bz{SEn}L(<*!bgVr#{rBWaJp zDw?i3p$a8SrrY?!Ho)S-)Efky222A+iD+)0tcrue4V_na?rfOG3SyI1^zN!|tF8$7 zuD8R9;QExqSK12lDBiVRD;MZZZ@2r@6ITPa_qQ8FnDYL=U$gcI>?<7y#{U65Iw@EO z5OT>#v$asTT{$H#r~}!`PQPP%rRdqd)e9OZKPAa>8CS!ImQ6|af;{CC*U}4eD_e!M zfB*m>07*naR0meYS1%U$;gpk;23k4E{vyBi4%cys15mT&{`gbgJnz0Q!m9etpE`9z zK-`z0MBES-!MH>_FmCXkE`%w>w@Fqh&Q*HH7BpzmAGa$k-yi%+y z#d|?q=EY?bQ~n*Ranr_DUaiVpRVJ0^xA|YYdz2X&>MB2d>tvMfwHQ}_IpyrIGDR78 z-qX@_*MyR10w}Gi;mU=xri2`4aQ0n&M9g<_mTbS&*!l1Nw%z!$c>##M|8>KB3S1wJ z2Xo2nq(rT86$0l-P2h$;xs?Y64c?u3Rx5WP?kz+=*#6s!dowc$0CZc??Qbf2kCW?( z%C>+jFmO$mHKAq7#$TL^N~D9q_rN_A@2ZGf1^rLGeh5&6^A+yD)zH3+|Mhx#*j6up zc{1+m`E^F}N1LxeI*{qHwRM)G+^^tBK;xKPDYhTA=~Uo(Q5g}-fCx}FIq5BNnQhDS zlOD1g5CMvdB_P6!D5-7QW1w_FIToO${e0Szqt+d8irf6K^o~piw21I}XcYgJ))Ew6fIQOI{dmt`T!$-~ax#`6A|EAiNG} z0|?@G8+3~p5a*S)$6d4b7zv2Ahz_?K5JxFpG>RvIV{oOY;>p{(P?ZhAz;$fl8C!89 zDt{N1JInIhylJDW3>}sCh$;uP>|QgDkGrY(nPXgN~2x|e`rav}O;86-0FHZ91>R2|T?L`68d{vb>{c^T=!?i8(_I7Klubu_HuqGha zUNL&}_W(UV?AfZ;U3(9ZL+GDdfsa-6|E%u1`qi+Ldb)ix_QtKno)XeGpab!K5;0Y# zcgw-8-R{5yf?F$*ZkBsR^zoYGTKsKkWd!kHpMNOm91fSoJ9-HBlaUtw5bFp=C@03l zCj>oq9A5Sq!;`o5KG4C5)!TbTI?QNV4j$IrEGC2O3Q-(jNOo>62Ir=qe#$*wZ+MHy z8?tiGH4IsWGZBfu&M%nqhse!-Mn{lHe0_bnMqm%h?_@gRgG&UK6eeeaOh%0CzFC_r zDZHp6OA1pS5%gBv4D5Sn#X(Wss}^0Au;Zj1T}0&)pasE_z<&e(bOl_?KkoUlwc2Ly z#m|E{ajV+}MmxM~ENK~DdHVH9H34oE;WD5-LA!(ml+6g`pbO(_t6w|ZpQQm*1)u*u zgvU8{o*v(!+%qtupVARzE3a_4V6JVBsi0Wry!U3ntzEzPdvfK`p3eb1W4>>W>!%k{ z^LQg*3@e@5##S|m69(OJ-wAPX`<(f*y9Nnz>;MJI?dv`@~K$jaGP8Q<|X)M*Nh#QHcPxr=T9#(nfi&iw-`@u05 z5KYv$#cn_xrKAb`H5L;%W`bf(txQ$bH$I<|Y|BZmG4YJqdWq@M4-uyMrzGbz)#Loo zQF)IjGSpdb8C^VMiYuRr$Xe4&_I-VImnS{g}8 z%`w7b3~OggC-YXNnF&7NxL8c#ma;WBQe zPaj*-a9PT5f#1b)B1ctuyZ!I~)hw`ozfX{RH%v^iWv$s5c<%YC<>xh9i$H1uu z6{h|tT>CZ;^uNfZG90wC3aJDio%R6#0{y_iwP^E%cP$51HEXX2aUz1c*L0bg^CP79 z072=e?I&M?$@z%1K@gDt0OeE%KB)7a0YIj*MRVyv4X+V&Fto#Fgh5h3_`J9<_8ha9Df9ZS{wWckx-b1eX4BmB-ulH!@&Y0$8 zU)5TLh}_PQcD9tu?^N1K(8)2T9M#ijPhQ1ys#05?pLFNZ zoCm-KcWpk$FkK5(^YY{O z_65@>&k5Ve=RX}a|A-jF!w7?#AaA4c6vNtmaB%z)B$B{$D47|1JaR;o!1M;Djsi zE)Bmj@^8L$EdvYs=1Kk77_S<)oi}hUcDy}e0*K$OkEYD9K7bYM|@&lJYieaq8T0jK2NQF0& zALyUocI;INks8l0m@*Nyb6Dy10ZwQ&rWW# zc(Pgu^p7SWE-q-BS{9r zoo)ql5kDW^v;^Z_yq&D);ms}Uj7vV@SPh7<7L^C=cB&3892(_kIx0cYGv=$dh)lvH z)2^*R%;%D~H5+8Msb;2sa`K!oO(p66{xd-z#{LRhfH7vX>Q{Pudq>=|m|r;MHB=)X zbSRq=<0Z7doX_W?s4O3JGIm98Rc|4_dNn@(=>SqpK&EpS%H1@avQvw2mlTp?)s(&= z7EepQRK#2^qR;ltsmSRQDR4xIw%Yb6$es_r+iWkL!+Y_SpiVqO0KUR7!XFfQJ3fHw zbJQ31O#pp~@tv&IY#-p+oQRrA~$TUa$6F4 zonM&03L_BiprcCBVJ}Ud9J*RT+C7Fu^Wo9+fOqk&J-E*))0v2n0UWx2xf7Esq`+y8 zUXUfzQ?tb-MIy#ceX=T|PQs1E`vs|efDuv?7~%EW$jyAL>uDgfalZX@3H$j7Z^yG6 zcu(+ld>VKn%=iYXf5Y2xBEAUbQ`eop8_KUnWSXOy3i*_Ku7N-|AhvtDB4fz(Y_ij3 zjB=L3^q9`$2%3CP(UVu1k8-L%KPjWBuYG{?bH6^xCAITVw!4l?9I@fAr|%8dzRi98 zFE9)?5SxQ3(@?Y5<|mg}dQ`h1H}@o|HC_|+@PiJKvGb=*4xK7gTzU;E3yHiDbm|5f$J{I+UhvZ(kkyvO+7WvrJo3l=##z0 zdS09t11^3Cq<;*i@f~mt-gQCGA+IMM5}pIGQ>OMBa5LU@k+)lXrQ<-J!P;j+QGJwQ z>E56F`&)vBQ+%?HGJ|1hS$4X375BHW3~;h#VVNdfDW;9B91L7Tyeq7uUSpD;_f@(} zYS$pT%703mVUYqNfXgL+B6cVBhAGBlRvW*XpTFa$c>06*4te>_`O_wcUP*b?KxG24 zy6LHAGq?OOpJ186N&@sCfzHtc#DC0a-BMd^dj}-7YQJ$wQv^h>7bD)KFCse2RzL)x z?WaHW_wX46?}xYroU4W;=>wv3Zv#Ra*Q0}Wxe z3ip;zPI}2opCoVZ6J@`9JFzu?SJQ2zZvD%Ip5o)&N<`6#|QBf`wd@tQ32D)ve;5tHRSqCm^bLa<|@Zh)ffVtE~n^0Hey?m)L&^ zL~1p(gnh&4yuvB>8!od;*nk*pdbQ)3g+e3~ug2{2QZg;|N&`N?kjMLGRg7z@fVg-@ zau?uk)6iRO1;i)PTY&KbvHvVU@GvEsfC#V;Olv>Suy*fb8a<(&hUHE~2ZRlX0BNP? zg3-s(EQN7)17fyvhTyoU!7g7mNl^S@d*x#Y|0$o`#+H*@ENDA+V}fVw*5^d@dQ7uR z*_5-_+8aR|MMlfOw4=3mkm6^bwNfbG7y_X!NdNq-SPWEBm@-Zcovwxs>YKIMmR)63 z*PWeoqL?xlF|zxOtgw?PEL6qM1{yjTITm;e+`aIAR%yA%@OEVc+}#PPo&!`upa&W0 z;rF^<-1r+zV*#j>B7tIfAl{Dm0M|9joT5X4Ct=+nD|QC;xbggNsZQvP@+>Z+ z2bmX!-y8O-LHzz#a8_r$nd*d*b~}9w4*{{-!wp9vaLpXy$3`vlOB=W4G!4isRk_t4 zV_Shpc*az80P{6L=LAiw93m=>vh}LY?K>^dILgVUIL<#TDLD6JHEQPz^6x$7Pd%^A z`8KvHJP}x2m^xO`v$vmkoV|k#??s&mrpFv>><7)l`^k$@jdII5{{?a_wQc8r3}m2# ztJGi`x71zBGIgiTLmXVs4%Gr<3WS*lUTD0PqE`(9#*2y+mgl!w+-TQnfOffCGcn{H zRUH>qKm-)Al}BY2G4#8}7`KjZ8f8>hz)@w%`_q%#tJJPhWkv6in};=ZKwMUkk}l}O zCfsC8UB~Nb4#rDBeB;f|5#M4Mmqr~B?ND4g7#ylt3HLF)-AcRNyh3d)^b&Z-(RCxU z1Gw_gfC!B!2;tej^&yaW&%e+9i(^Fq+o*VQx7~+mmT4IMKA&{9BLUJw5#=*&goR>2 zSQ@dBZ~b`@A88syzXXYPdfQcT1)x4^@?Xgwg%7wk#lY{Y|4#lJ99Ake_@4 z_w>)+VpH)au>d%?Q6}b`PjMU9@xN=nYO*WND{YS{&*SKkNf$9RZDPoG&*KDYxdi20 zgtjQhA?!ohgTZH{uq<;6gId5$Vr!;WYQl9hgnAhujS>{wjosD@HOIJWjZWc&6=I2J z%=gVPwmuJ>>Z+~n;h)wvCS^%mo_znKpED^mlfpMs3&;{qWpE!(yE<0*Un($Fk6F$?T-WC50)^BYK@_Xy>C95EoBRxgE6QUj50Q=*Xj; zJ$02I#|jEE9`D*4tp>#Py1v=x`g!^n`o($cb$x5K^Z7Y|&*uVt31{g-Cdh5vbl&zu zU(~jEG?n)e-U2ePqLwXma49nz`H8qz7yVST-%?98sRc)SGPWVS0NfVwx`l$2Z>WY_SIzjCZY<2v)G4 z>*w9ewS0-=JzW-?VK@z*!oW;w95_n3kxf za0kk#KtD%g!kPVNGv?9Dc`xoK+g{yO0=E?ke6M^1XU>{xS`%I5OA&R+I3~e18w}?;%ZUjW|U|ptoC8( z(0~8+-O#0lKb-nF4VN-y&}{Epy!xp{yHn$7&`%uA@^E%Sz>>0Y$@HXd?m*mXV1D1M zEjRbL_v@y6%sX@*vy&1720l^!W8YB~>+SXaZ$3f0&|`tGfy?l&3+r3tSASc-`WKp* zA1j(m!-4E%MF%Uopqz65=lc0&5wD(k@ed@pyJP#HLzW@sk01Jqwq-oQ1C1k9e{6GF z1p}Y%iQipi`9SeI;Iab2Qci5lju6uC+S<`Hb&(6YZJ3hOsP(HE8R{xOz49@X!3GQ} zos#TGz2lNgmba*>iSyBL9@Jh{8MbcEZXAM~8j6h0t>~GGy_parvfIDp7mwH%IoZ#nAEo$;nY0Cxzp@`@T5Ay*J?{#7Ky9o5Mc)d$Oa|Ti32D&jA(!;VMf4 zs*<;Qb=QLj9Vz$sKU+m#0I^dwvQz?+z2)imHim5Ra_MQf&FahR5zqLDtyBeH*&~QN60Q}ZgUCpK^WY2T z&=r$qKsbTr%?R@_Sr7aGBmy3UD}mnOrnS2zfBgMwt^gWZ`4$5s3dNo?WlVx%T5jcw zrpWH{DQz5@_gK8n*lnq*=0ea9PdPa$cd1t#36OT}x^e(e1Xz3&WzwouUGF}mB1cjl zy!pLN3{nvhDEGXh2RHzUik=AA0Tpy$!_2o zj@$nv*LDi~VJgY@G&kdZdclB`TyA5?rFU#{thOBs-YZBs+a;_PF|Y5FRcSphuApNa z-?1F%+IX|GL;&F-9$6Q0E2vaUQw@wYID2Y23_)&aC+)0R3m28%QK!vVKM)H~l96^$ zMO#5gWew1=UfUr5M)*COcu7L+mug!sMwt#=+|c{L?(dW7@bG*5nV+)-RN<-V@E{U? zAGRT)4vL=K%8??BJ0Z^1WyLdRHQF9hyRqAPiJC$j4?nk_g3# zi9h{r>C!%jIS#h4~?k1&N}vhY@UBI|4Nr(u<*< zRhC)@gKs~OzChPt{i^&H_zGbo$b0C2-Xya}#J}URS*d5$)|jsgtoaQs^E&0MYZ6cF zP^>NJ7`NNlxy(d-{j#?F-BxFB$BDpQjWaXL>;}CF?JiZo#N?8)XOljUW~^{{aw~HY9#S!el}|o9^qyR}F+BeG z=FN9c1@4GqDod)W5{7*C*+sGQ3@P^aLMZ`tpuQsSf&VHpGn*tA^Z-j{r1r$bhw^0K zoXwM(^4g(TJU#WVqIyA}tS#4woz=nkj#s0+!O=Bw+k{I8@$e^>zC-0&ezBr!iS3+6 zd&FZsA2yiK{T9#~_z?mZlX>A`X9=3hQW}1&-G$IuHnG|2<>~_CQlNW-;{|9XmFyp- zoH|XSuuho2zI9Zm&!R>Zuu~0ggxJ@8bj)5G*Di1CR77(EZtdYTAa?1p>ZIn)cfUh) z0TG~YLPFJt9Xqa#jate}8(N_HqQih#f!nyTsRLq3LGnmc=V9vaj+ua1n0l3%nknkO z%~nA4c5RLF97i`GzDGiJ7pnmgK+)mEe$KbqYCSZ41Jrf{HXw|y7ZA_m1VRhKdhG;6 z0P#}|WYe%ePz#J;u|iYVMicotT)=(AV45xBbdoX1X zgD;4ns~9_cK3t0nQzod$A276c-%(qdw4OZ_?@v!YO<;x^GNMn;KetEjQHQs;8){zx zPHMc_MM4TWgR6P4bXVlIhjDo$Os4Zj-5Akr@-Q+xe8b*mUCBtRuf!do4-Fpx?V-w< z$BJyG3__I0O2~anYZR9a$IvCf8Bymc`ky¹R5RY+uev$s2DZ+4=yb{Oa9Za$al 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 literal 0 HcmV?d00001 diff --git a/apps/aklub/static/aklub/images/newsletter_template_sample_pic.svg b/apps/aklub/static/aklub/images/newsletter_template_sample_pic.svg new file mode 100644 index 000000000..8591953db --- /dev/null +++ b/apps/aklub/static/aklub/images/newsletter_template_sample_pic.svg @@ -0,0 +1,641 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/apps/aklub/templates/email_templates/automat_green_newsletter.html b/apps/aklub/templates/email_templates/automat_green_newsletter.html new file mode 100644 index 000000000..1e0ae8be1 --- /dev/null +++ b/apps/aklub/templates/email_templates/automat_green_newsletter.html @@ -0,0 +1,21 @@ +{% extends 'email_templates/base.html' %} + +{% load static %} + +{% block content %} +
+ +
+{% endblock %} diff --git a/apps/aklub/templates/email_templates/automat_purple_newsletter.html b/apps/aklub/templates/email_templates/automat_purple_newsletter.html new file mode 100644 index 000000000..ec2295a9c --- /dev/null +++ b/apps/aklub/templates/email_templates/automat_purple_newsletter.html @@ -0,0 +1,21 @@ +{% extends 'email_templates/base.html' %} + +{% load static %} + +{% block content %} +
+ +
+{% endblock %} diff --git a/apps/aklub/templates/email_templates/automat_red_newsletter.html b/apps/aklub/templates/email_templates/automat_red_newsletter.html new file mode 100644 index 000000000..98b2acaac --- /dev/null +++ b/apps/aklub/templates/email_templates/automat_red_newsletter.html @@ -0,0 +1,21 @@ +{% extends 'email_templates/base.html' %} + +{% load static %} + +{% block content %} +
+ +
+{% endblock %} diff --git a/apps/aklub/templates/email_templates/base.html b/apps/aklub/templates/email_templates/base.html new file mode 100644 index 000000000..400acc21b --- /dev/null +++ b/apps/aklub/templates/email_templates/base.html @@ -0,0 +1,23 @@ +{% load static %} + + + + + + + ContentTools sandbox + + + + + + {% block content %} + {% endblock %} + + + + + + + + From 4a07036abd059018606c7d63c71b674fd5b65866 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 09:42:00 +0200 Subject: [PATCH 009/189] add 'get_email_template' view function return rendered email html template --- apps/aklub/urls.py | 1 + apps/aklub/views.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/apps/aklub/urls.py b/apps/aklub/urls.py index 8a429d4dc..63f6ed044 100644 --- a/apps/aklub/urls.py +++ b/apps/aklub/urls.py @@ -36,4 +36,5 @@ auth_views.PasswordResetCompleteView.as_view(template_name='password/password_reset_complete.html'), name='password_reset_complete', ), + path('get_email_template//', views.get_email_template, name='get_email_template'), ] diff --git a/apps/aklub/views.py b/apps/aklub/views.py index b14252c4f..c1a0fbde9 100644 --- a/apps/aklub/views.py +++ b/apps/aklub/views.py @@ -19,6 +19,7 @@ # Create your views here. import json +import pathlib from collections import OrderedDict from betterforms.multiform import MultiModelForm @@ -34,8 +35,9 @@ from django.core.validators import MinLengthValidator, RegexValidator, ValidationError from django.db.models import Case, CharField, Count, IntegerField, Q, Sum, Value, When from django.db.models.functions import TruncMonth -from django.http import JsonResponse +from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render, render_to_response +from django.template import loader from django.template.loader import render_to_string from django.urls import reverse_lazy from django.utils.decorators import method_decorator @@ -54,6 +56,7 @@ from sesame.backends import ModelBackend from . import autocom +from .html_template_editor.models import TemplateContent from .models import ( AdministrativeUnit, DonorPaymentChannel, Event, MoneyAccount, Payment, Profile, ProfileEmail, Source, Telephone, UserInCampaign, @@ -866,3 +869,27 @@ def get(self, request, *args, **kwargs): template_name="password/password_reset.html", context={"password_reset_form": password_reset_form}, ) + + +def get_email_template(request, template_name): + _template_name = template_name + template_name = '.'.join([template_name, 'html']) + template_dir = pathlib.PurePath('email_templates') + template_path = template_dir / template_name + template = loader.get_template(template_path.as_posix()) + template_url = reverse_lazy( + 'aklub:get_email_template', + kwargs={'template_name': _template_name} + ) + template_obj = TemplateContent.objects.filter(page=template_url) + if (template_obj): + content = template_obj.latest('created') + context = { + 'page': content + } + regions = json.loads(content.regions) + context.update(regions) + else: + context = {} + + return HttpResponse(template.render(context)) From cf0b126b62f57278930ed4f1c4887db6c8540807 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 10:13:14 +0200 Subject: [PATCH 010/189] add 'django-rest-swagger', 'django-filter', 'PyYAML', 'mkdocs', 'django-js-urls' app as dependency for 'hmt_template_editor' app --- Pipfile | 6 +++- Pipfile.lock | 59 +++++++++++++++++++++++++++++++++++----- project/settings/base.py | 5 ++++ 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/Pipfile b/Pipfile index 795be1682..6cd32013b 100644 --- a/Pipfile +++ b/Pipfile @@ -92,7 +92,11 @@ faker = "*" # TODO: fix computedfields error in version 0.1.0 (syntax changed in models) django-computedfields = "==0.0.23" django-oauth-toolkit = "*" - +django-rest-swagger = "*" +django-filter = "*" +PyYAML = "*" +mkdocs = "*" +django-js-urls = "*" [dev-packages] coverage = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 13c3ad966..17bc62d1a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -73,7 +73,6 @@ "sha256:4c4532aa683f170f40bd76f928b70bc06ff171a959e06e71bf35f2f9d6031ef9", "sha256:528e56767ae7e43a16cfef24ee1062491f5754368d38fcfffa861cdb9ef219be" ], - "index": "pypi", "version": "==4.3.0" }, "certifi": { @@ -206,7 +205,6 @@ "hashes": [ "sha256:0238b6280f66a8ba6d1c730ab4acc52bc2bf37686940fd4db42e7af458c96635" ], - "index": "pypi", "version": "==1.0.2" }, "django-betterforms": { @@ -227,7 +225,6 @@ "hashes": [ "sha256:eac59448965ebacae426a86bc572ec8f6683094a20fe4574179856ba0005b19f" ], - "index": "pypi", "version": "==4.0.1" }, "django-bower": { @@ -235,7 +232,6 @@ "sha256:3ab5699b782d63d1aa8e5d61f9bdcdb8d29137e7ba6f19d094d566241afee0f2", "sha256:f1cf649842c0cb28f8aa28617e99fc7fb7773bac4e8886ee4e36962a38c99a6e" ], - "index": "pypi", "version": "==5.2.0" }, "django-braces": { @@ -340,6 +336,14 @@ "index": "pypi", "version": "==0.5.0" }, + "django-filter": { + "hashes": [ + "sha256:558c727bce3ffa89c4a7a0b13bc8976745d63e5fd576b3a9a851650ef11c401b", + "sha256:c3deb57f0dd7ff94d7dce52a047516822013e2b441bed472b722a317658cfd14" + ], + "index": "pypi", + "version": "==2.2.0" + }, "django-grapesjs": { "hashes": [ "sha256:a54b1fb95e6516fa33bf94db268360158f8a5edeb2fee202cc24ca71f343b757" @@ -379,6 +383,14 @@ "index": "pypi", "version": "==0.2.0" }, + "django-js-urls": { + "hashes": [ + "sha256:9e578a1dae530968ea1c3d609a9177c7fe7bbc2774e5e61dcb2da4b311527cce", + "sha256:c51660f5f7ab22037e91ce161e4e74405bc2ee714573ce762899ef73c5d818a8" + ], + "index": "pypi", + "version": "==0.1.1" + }, "django-js-asset": { "hashes": [ "sha256:8ec12017f26eec524cab436c64ae73033368a372970af4cf42d9354fcb166bdd", @@ -465,7 +477,6 @@ "hashes": [ "sha256:73268ab20805c9f2e92b8cf54c36adfe03661d90eb5d58da69a5cb4d932c2757" ], - "index": "pypi", "version": "==1.1.0" }, "django-redis": { @@ -497,6 +508,14 @@ ], "version": "==2.0.0" }, + "django-rest-swagger": { + "hashes": [ + "sha256:48f6aded9937e90ae7cbe9e6c932b9744b8af80cc4e010088b3278c700e0685b", + "sha256:b039b0288bab4665cd45dc5d16f94b13911bc4ad0ed55f74ad3b90aa31c87c17" + ], + "index": "pypi", + "version": "==2.2.0" + }, "django-ses": { "hashes": [ "sha256:45f041acb9f2f8df85f3fddd63b04f1d381982bdc7730a3dc1f88e5795758bdd", @@ -771,6 +790,13 @@ ], "version": "==4.6.11" }, + "livereload": { + "hashes": [ + "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", + "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66" + ], + "version": "==2.6.1" + }, "lxml": { "hashes": [ "sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f", @@ -866,6 +892,21 @@ ], "version": "==1.1.1" }, + "mkdocs": { + "hashes": [ + "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", + "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a" + ], + "index": "pypi", + "version": "==1.0.4" + }, + "more-itertools": { + "hashes": [ + "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39", + "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288" + ], + "version": "==8.1.0" + }, "newrelic": { "hashes": [ "sha256:0eaab77f295f6efa16887ef8594ec8601b1f1071fef8fc78818d71334e5545d4", @@ -898,6 +939,12 @@ ], "version": "==1.4.1" }, + "openapi-codec": { + "hashes": [ + "sha256:1bce63289edf53c601ea3683120641407ff6b708803b8954c8a876fe778d2145" + ], + "version": "==1.3.2" + }, "openpyxl": { "hashes": [ "sha256:18e11f9a650128a12580a58e3daba14e00a11d9e907c554a17ea016bf1a2c71b", @@ -1406,7 +1453,6 @@ "sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2", "sha256:e551fb498759fa3a5384a94ccd4c3c02eb7c00ea424426e212ac0c57be9dfbde" ], - "index": "pypi", "version": "==1.2.0" }, "xlwt": { @@ -1458,7 +1504,6 @@ "sha256:4c4532aa683f170f40bd76f928b70bc06ff171a959e06e71bf35f2f9d6031ef9", "sha256:528e56767ae7e43a16cfef24ee1062491f5754368d38fcfffa861cdb9ef219be" ], - "index": "pypi", "version": "==4.3.0" }, "certifi": { diff --git a/project/settings/base.py b/project/settings/base.py index dc1a2c5f9..2a123810a 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -249,6 +249,9 @@ def normpath(*args): "pinax.invitations", "pinax.teams", "reversion", + 'rest_framework_swagger', + 'rest_framework.authtoken', + 'js_urls', ) REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ # noqa @@ -290,6 +293,8 @@ def normpath(*args): 'd3#3.3.13', 'nvd3#1.7.1', 'ContentTools#1.6.10', + 'https://github.com/Karl33to/jquery.inlineStyler.git#1.0.1', + 'webui-popover#2.1.15', ) AUTH_PASSWORD_VALIDATORS = [ From e692b7cdbbe8fab43c2f55cbfcf8c66a4e8df2b7 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 10:20:54 +0200 Subject: [PATCH 011/189] fix 'html_template_editor' app module name/import order --- apps/aklub/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/aklub/views.py b/apps/aklub/views.py index c1a0fbde9..c1d1fbd6f 100644 --- a/apps/aklub/views.py +++ b/apps/aklub/views.py @@ -51,12 +51,13 @@ from extra_views import InlineFormSet, UpdateWithInlinesView +from html_template_editor.models import TemplateContent + from interactions.models import PetitionSignature # TODO: Not sure if it works (model moved from aklub to interaction app) from sesame.backends import ModelBackend from . import autocom -from .html_template_editor.models import TemplateContent from .models import ( AdministrativeUnit, DonorPaymentChannel, Event, MoneyAccount, Payment, Profile, ProfileEmail, Source, Telephone, UserInCampaign, From 5fae3d326c1e09d0a76f589b5b71cca71207abc2 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 10:29:00 +0200 Subject: [PATCH 012/189] fix js syntaxt --- apps/aklub/static/aklub/js/form_field_widget_init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/aklub/static/aklub/js/form_field_widget_init.js b/apps/aklub/static/aklub/js/form_field_widget_init.js index 683be848e..108d83585 100644 --- a/apps/aklub/static/aklub/js/form_field_widget_init.js +++ b/apps/aklub/static/aklub/js/form_field_widget_init.js @@ -35,4 +35,4 @@ Object.assign(htmlTemplateEnFieldOpts, formOpts) var htmlTemplateEnFieldWidget = new HtmlTemplateFormFieldWidget(htmlTemplateEnFieldOpts) }) -}(jQuery, HtmlTemplateFormFieldWidget) +}(jQuery, HtmlTemplateFormFieldWidget)) From 1718ba61e365ada293b5ff1449a877d8cf5fbe3d Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 10:29:47 +0200 Subject: [PATCH 013/189] add 'html_template_editor' rest app settings --- project/settings/base.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/project/settings/base.py b/project/settings/base.py index 2a123810a..570103c85 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -252,6 +252,7 @@ def normpath(*args): 'rest_framework_swagger', 'rest_framework.authtoken', 'js_urls', + 'html_template_editor', ) REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ # noqa @@ -534,3 +535,26 @@ def get_payment_channel_resource(): 'companyprofile': ['is_staff', 'is_superuser', 'user_permissions', 'groups'], }, } + +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAdminUser', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', + ), + 'DEFAULT_PARSER_CLASSES': ( + 'rest_framework.parsers.JSONParser', + 'rest_framework.parsers.FormParser', + 'rest_framework.parsers.MultiPartParser', + ), + 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', + } + +SWAGGER_SETTINGS = { + 'is_authenticated': False, + 'is_superuser': False, +} From cdbd8896c6bf7b9925828aa5d2e8367b271bc6ba Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 10:31:30 +0200 Subject: [PATCH 014/189] set js reverse url path config --- project/settings/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/project/settings/base.py b/project/settings/base.py index 570103c85..393d11887 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -558,3 +558,10 @@ def get_payment_channel_resource(): 'is_authenticated': False, 'is_superuser': False, } + +JS_URLS = ( + 'html_template_editor:add', + 'html_template_editor:images_add', + 'html_template_editor:images_update', + 'aklub:get_email_template', +) From 3380bbf1366dba3142b3c9fd5cbf35bb271e4d94 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 10:39:02 +0200 Subject: [PATCH 015/189] add 'html_template_editor' app urls --- project/urls.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/project/urls.py b/project/urls.py index b0c39314d..6218407e8 100644 --- a/project/urls.py +++ b/project/urls.py @@ -6,11 +6,16 @@ from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth import views as auth_views -from django.urls import path +from django.urls import path, re_path from django.views.i18n import JavaScriptCatalog +from rest_framework.authtoken.views import obtain_auth_token + +from rest_framework_swagger.views import get_swagger_view admin.autodiscover() +schema_view = get_swagger_view(title='API') + urlpatterns = [ url(r'^desk/', include("helpdesk.urls")), @@ -37,6 +42,15 @@ path('notifications/', include('django_nyt.urls')), path('help/', include('wiki.urls')), path('api/', include('api.urls')), + re_path( + r'^html_template_editor/', + include( + ('html_template_editor.urls', 'html_template_editor'), + namespace='html_template_editor') + ), + re_path(r'^docs/', schema_view), + re_path(r'^api-auth/', include('rest_framework.urls')), + re_path(r'^token-auth/', obtain_auth_token), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += i18n_patterns( From 348fdd4bab446fa2dbf3841b143a1bb55165a13e Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 10:40:38 +0200 Subject: [PATCH 016/189] load reverse url js module in the 'MassCommunication' template disable django 'tinymce' editor initialization for the 'template'/'template_en' form field --- .../templates/admin/aklub/masscommunication/change_form.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/aklub/templates/admin/aklub/masscommunication/change_form.html b/apps/aklub/templates/admin/aklub/masscommunication/change_form.html index b20271725..e43da91c1 100644 --- a/apps/aklub/templates/admin/aklub/masscommunication/change_form.html +++ b/apps/aklub/templates/admin/aklub/masscommunication/change_form.html @@ -18,7 +18,7 @@ {{ block.super }} {% wysiwyg_setup %} - + {% endblock %} {% block extrastyle %} @@ -75,8 +75,10 @@ {{ block.super }} {# Replace XXXXXXXX with field name: #} + {% comment %} {% wysiwyg_editor "id_template" %} {% wysiwyg_editor "id_template_en" %} + {% comment %} {% comment %} {# ckeditor custom config example #} From e94e167734ac2d58b88fbad4fe6b429dfcfa3e62 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 10:43:09 +0200 Subject: [PATCH 017/189] add js reverse url path app urls --- project/urls.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/project/urls.py b/project/urls.py index 6218407e8..2c8daf899 100644 --- a/project/urls.py +++ b/project/urls.py @@ -9,6 +9,8 @@ from django.urls import path, re_path from django.views.i18n import JavaScriptCatalog +from js_urls.views import JsUrlsView + from rest_framework.authtoken.views import obtain_auth_token from rest_framework_swagger.views import get_swagger_view @@ -51,6 +53,7 @@ re_path(r'^docs/', schema_view), re_path(r'^api-auth/', include('rest_framework.urls')), re_path(r'^token-auth/', obtain_auth_token), + re_path(r'^js-urls/$', JsUrlsView.as_view(), name='js_urls'), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += i18n_patterns( From fefe318376098ad4b79ad8f3c478625b361fe135 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 10:54:59 +0200 Subject: [PATCH 018/189] set 'aklub' app urls namespace --- project/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/urls.py b/project/urls.py index 2c8daf899..990f47efc 100644 --- a/project/urls.py +++ b/project/urls.py @@ -40,10 +40,10 @@ url(r'^advanced_filters/', include('advanced_filters.urls')), url(r'^nested_admin/', include('nested_admin.urls')), path('admin_tools_stats/', include('admin_tools_stats.urls')), - url(r'', include("aklub.urls")), path('notifications/', include('django_nyt.urls')), path('help/', include('wiki.urls')), path('api/', include('api.urls')), + url(r'', include(('aklub.urls', 'aklub'), namespace='aklub')), re_path( r'^html_template_editor/', include( From 4578695433bbcad447e62b463ca91b2b97eda787 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 11:09:36 +0200 Subject: [PATCH 019/189] fix variable name --- apps/aklub/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index 7cabdf203..12e418948 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -126,12 +126,13 @@ def get_admin_url(self): def get_email_templates_names(): """ Get email templates names """ - template_type = [ + templates_names = [ (template, template) for template in [ template.split('.')[0] for template in os.listdir( pathlib.PurePath(__file__).parents[0] / 'templates' / 'email_templates') if template not in ['base.html'] ] ] - template_type.insert(0, ("", "---------")) - return template_type + templates_names.insert(0, ("", "---------")) + + return templates_names From c149ff463a731aa4a73f7f075a38d4b84d6f3920 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 11:19:11 +0200 Subject: [PATCH 020/189] fix 'html_template_editor' app name in the reverse ulr js function --- apps/aklub/static/aklub/js/editor.js | 2 +- apps/aklub/static/aklub/js/image_uploader.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/aklub/static/aklub/js/editor.js b/apps/aklub/static/aklub/js/editor.js index 94f31ffb4..9310be9f1 100644 --- a/apps/aklub/static/aklub/js/editor.js +++ b/apps/aklub/static/aklub/js/editor.js @@ -59,7 +59,7 @@ } element = document.querySelector('meta[name="page-id"]') page_id = element.getAttribute('content') - editHtmlTemplate.call('post', window.reverse('api:add'), payload, true, onStateChange) + editHtmlTemplate.call('post', window.reverse('html_template_editor:add'), payload, true, onStateChange) }) } }).call(this, editHtmlTemplate) diff --git a/apps/aklub/static/aklub/js/image_uploader.js b/apps/aklub/static/aklub/js/image_uploader.js index 16c1003c1..5ce394f0a 100644 --- a/apps/aklub/static/aklub/js/image_uploader.js +++ b/apps/aklub/static/aklub/js/image_uploader.js @@ -86,7 +86,7 @@ var imageUploader = function (dialog) { xhr = new XMLHttpRequest() xhr.upload.addEventListener('progress', xhrProgress) xhr.upload.addEventListener('readystatechange', xhrComplete) - editHtmlTemplate.call('post', window.reverse('api:images_add'), formData, true, xhrComplete) + editHtmlTemplate.call('post', window.reverse('html_template_editor:images_add'), formData, true, xhrComplete) }) dialog.addEventListener(evtFuncsNames.save, function () { @@ -140,7 +140,7 @@ var imageUploader = function (dialog) { // Make the request xhr = new XMLHttpRequest() xhr.upload.addEventListener('readystatechange', xhrComplete) - editHtmlTemplate.call('put', window.reverse('api:images_update', { id: image.id }), formData, true, xhrComplete) + editHtmlTemplate.call('put', window.reverse('html_template_editor:images_update', { id: image.id }), formData, true, xhrComplete) }) function rotateImage (direction) { @@ -184,7 +184,7 @@ var imageUploader = function (dialog) { // Make the request xhr = new XMLHttpRequest() xhr.upload.addEventListener('readystatechange', xhrComplete) - editHtmlTemplate.call('put', window.reverse('api:images_update', { id: image.id }), formData, true, xhrComplete) + editHtmlTemplate.call('put', window.reverse('html_template_editor:images_update', { id: image.id }), formData, true, xhrComplete) } dialog.addEventListener(evtFuncsNames.rotateccw, function () { From 56930a2deb5908c482188427bc371c3ef6d51df7 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 11:40:07 +0200 Subject: [PATCH 021/189] add 'html_template_editor' app files --- apps/html_template_editor/__init__.py | 0 apps/html_template_editor/admin.py | 3 + apps/html_template_editor/forms.py | 0 .../migrations/0001_initial.py | 40 +++++++ .../migrations/__init__.py | 0 apps/html_template_editor/models.py | 91 ++++++++++++++ apps/html_template_editor/serializers.py | 56 +++++++++ apps/html_template_editor/tests.py | 112 ++++++++++++++++++ apps/html_template_editor/upload_views.py | 94 +++++++++++++++ apps/html_template_editor/urls.py | 47 ++++++++ apps/html_template_editor/views.py | 58 +++++++++ 11 files changed, 501 insertions(+) create mode 100644 apps/html_template_editor/__init__.py create mode 100644 apps/html_template_editor/admin.py create mode 100644 apps/html_template_editor/forms.py create mode 100644 apps/html_template_editor/migrations/0001_initial.py create mode 100644 apps/html_template_editor/migrations/__init__.py create mode 100644 apps/html_template_editor/models.py create mode 100644 apps/html_template_editor/serializers.py create mode 100644 apps/html_template_editor/tests.py create mode 100644 apps/html_template_editor/upload_views.py create mode 100644 apps/html_template_editor/urls.py create mode 100644 apps/html_template_editor/views.py diff --git a/apps/html_template_editor/__init__.py b/apps/html_template_editor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/html_template_editor/admin.py b/apps/html_template_editor/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/html_template_editor/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/html_template_editor/forms.py b/apps/html_template_editor/forms.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/html_template_editor/migrations/0001_initial.py b/apps/html_template_editor/migrations/0001_initial.py new file mode 100644 index 000000000..4a540caa6 --- /dev/null +++ b/apps/html_template_editor/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 2.2.6 on 2019-10-07 04:47 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Images', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('image', models.ImageField(null=True, upload_to='images')), + ('name', models.CharField(blank=True, max_length=100, null=True)), + ('edited_width', models.IntegerField(blank=True, null=True)), + ('edited_crop', models.CharField(blank=True, max_length=15, null=True)), + ('edited_direction', models.CharField(blank=True, max_length=5, null=True)), + ], + ), + migrations.CreateModel( + name='TemplateContent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('images', models.CharField(blank=True, max_length=100, null=True)), + ('page', models.CharField(blank=True, max_length=100, null=True)), + ('regions', models.TextField(blank=True, default='', null=True)), + ], + ), + ] diff --git a/apps/html_template_editor/migrations/__init__.py b/apps/html_template_editor/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py new file mode 100644 index 000000000..5040c0531 --- /dev/null +++ b/apps/html_template_editor/models.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +import uuid + +# from django.contrib.auth import get_user_model +from django.db import models + +# User = get_user_model() + + +class TemplateContent(models.Model): + + created = models.DateTimeField( + auto_now_add=True, + ) + modified = models.DateTimeField( + auto_now=True, + ) + uuid = models.UUIDField( + default=uuid.uuid4, + editable=False, + unique=True, + ) + images = models.CharField( + max_length=100, + blank=True, + null=True, + ) + page = models.CharField( + max_length=100, + blank=True, + null=True, + ) + regions = models.TextField( + blank=True, + default='', + null=True, + ) + + +class Images(models.Model): + + created = models.DateTimeField( + auto_now_add=True, + ) + modified = models.DateTimeField( + auto_now=True, + ) + image = models.ImageField( + upload_to='images', + height_field=None, + width_field=None, + max_length=100, + null=True, + ) + name = models.CharField( + max_length=100, + blank=True, + null=True, + ) + edited_width = models.IntegerField( + blank=True, + null=True, + ) + edited_crop = models.CharField( + max_length=80, + blank=True, + null=True, + ) + edited_direction = models.CharField( + max_length=5, + blank=True, + null=True, + ) + + def size(self): + return [self.image.width, self.image.height] + + +# class FileUpload(models.Model): +# created = models.DateTimeField( +# auto_now_add=True, +# ) +# owner = models.ForeignKey( +# User, +# on_delete=models.SET_NULL, +# null=True, +# ) +# datafile = models.FileField( +# upload_to='files', +# ) diff --git a/apps/html_template_editor/serializers.py b/apps/html_template_editor/serializers.py new file mode 100644 index 000000000..d13bdd7e5 --- /dev/null +++ b/apps/html_template_editor/serializers.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +import json +import time + +from .models import TemplateContent, Images + +from rest_framework import serializers + + +class TemplateContentSerializer(serializers.ModelSerializer): + + class Meta: + model = TemplateContent + fields = ('id', + 'created', + 'uuid', + 'page', + 'regions', + 'images') + + def to_representation(self, instance): + ret = super(TemplateContentSerializer, self).to_representation(instance) + ret['regions'] = json.loads(ret['regions']) + return ret + + def validate(self, data): + return data + + +class ImagesSerializer(serializers.ModelSerializer): + + size = serializers.ListField( + read_only=True, + child=serializers.IntegerField() + ) + + class Meta: + model = Images + fields = ('id', + 'created', + 'image', + 'name', + 'size', + 'edited_width', + 'edited_crop', + 'edited_direction', + ) + + def to_representation(self, instance): + ret = super(ImagesSerializer, self).to_representation(instance) + if ret['image']: + # Modify the image URL by adding an _ignore param + # This will force the browser to reload the image + ret['image'] = "%s?_ignore=%s" % (ret['image'], time.time()) + return ret diff --git a/apps/html_template_editor/tests.py b/apps/html_template_editor/tests.py new file mode 100644 index 000000000..3ccc479d9 --- /dev/null +++ b/apps/html_template_editor/tests.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +import io + +from PIL import Image + +from django.core.files.base import ContentFile +from django.urls import reverse + + +from rest_framework import status +from rest_framework.authtoken.models import Token +from rest_framework.test import APIClient +from rest_framework.test import APITestCase + +from aklub.tests.test_admin import CreateSuperUserMixin + + +# Create your tests here. +class TemplateContentTests(CreateSuperUserMixin, APITestCase): + + def setUp(self): + super().setUp() + self.token = Token.objects.create(user=self.superuser) + self.client = APIClient() + + def test_add(self): + """ Test if we can add, retrieve and list """ + self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(self.token.key)) + + # Create the add URL + url = reverse('api:add') + + data = { + 'images': ['{}'], + 'regions': [ + ''.join([ + """{"article": "

""", + """About the author

""", + """Anthony Blackshaw""", + """

""", + """Anthony Blackshaw is a co-founder of Getme, an employee owned""", + """company with a focus on web tech. He enjoys writing and talking""", + """about tech, especially code and the occasional""", + """Montecristo No.2s./p>"}""", + ]) + ], + 'page': ['/'] + } + response = self.client.post(url, data, format='multipart') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + object_id = response.data['uuid'] + + # Retrieve + url = reverse('api:retrieve', args=[object_id]) + response = self.client.get(url, format='json') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class ImageUploadTests(CreateSuperUserMixin, APITestCase): + + def setUp(self): + super().setUp() + self.token = Token.objects.create(user=self.superuser) + self.client = APIClient() + + def test_upload_image(self): + """ Test if we can add, update image """ + self.client.credentials(HTTP_AUTHORIZATION='Token {}'.format(self.token.key)) + + url = reverse('api:images_add') + + file_obj = io.BytesIO() + image = Image.new("RGBA", size=(50, 50), color=(256, 0, 0)) + image.save(file_obj, 'png') + + file_obj.seek(0) + + django_friendly_file = ContentFile(file_obj.read(), 'test.png') + + data = { + 'width': ['600'], + 'image': django_friendly_file + } + + # Upload a file with an authenticated user + response = self.client.post(url, data, format='multipart') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertIn('created', response.data) + + image_id = response.data['id'] + + # + # Crop Image + # + + url = reverse('api:images_update', args=[image_id]) + + data = { + 'crop': ['0,0,10,10'] + } + + response = self.client.post(url, data, format='multipart') + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # assert unauthenticated user can not upload file + self.client.logout() + response = self.client.post(url, data, format='multipart') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/apps/html_template_editor/upload_views.py b/apps/html_template_editor/upload_views.py new file mode 100644 index 000000000..0a80c11fd --- /dev/null +++ b/apps/html_template_editor/upload_views.py @@ -0,0 +1,94 @@ + +import decimal + +from PIL import Image + +from rest_framework import generics, permissions + +from .models import Images +from .serializers import ImagesSerializer + +# +# Mixin for all company views. +# Defines serializers, queryset and permissions +# + + +class ImagesMixin(object): + + def get_queryset(self): + return Images.objects.filter() + + def get_serializer_class(self): + return ImagesSerializer + + def get_permissions(self): + return [permissions.IsAuthenticated()] + + +class ImagesAdd(ImagesMixin, generics.CreateAPIView): + + def perform_create(self, serializer): + serializer.save( + image=self.request.data['image'], + name=self.request.data['image'].name, + edited_width=self.request.data['width']) + + +class ImagesList(ImagesMixin, generics.ListAPIView): + ordering_fields = ('created') + ordering = ('-created',) + + +class ImagesUpdate(ImagesMixin, generics.UpdateAPIView): + + lookup_field = 'id' + + def post(self, request, *args, **kwargs): + response = self.update(request, *args, **kwargs) + return response + + def perform_update(self, serializer): + # Get image from image model instance + image = self.get_object().image + + if self.request.data.get('crop'): + + crop_values = self.request.data['crop'].split(',') + + # Crop values are percentages, so we need to convert them into + # pixel values + + top = round(decimal.Decimal(crop_values[0]) * image.height) + left = round(decimal.Decimal(crop_values[1]) * image.width) + bottom = round(decimal.Decimal(crop_values[2]) * image.height) + right = round(decimal.Decimal(crop_values[3]) * image.width) + + crop_box = (left, top, right, bottom) + + # Open the image with PIL + im = Image.open(image.path) + + # Action the image rotation + im = im.crop(crop_box) + + # Save the image + im.save(image.path) + + serializer.save( + edited_crop=self.request.data['crop'], + ) + + if self.request.data.get('direction'): + angle = 270 if self.request.data['direction'] == "CW" else 90 + + # Open the image with PIL + im = Image.open(image.path) + + # Action the image rotation + im = im.rotate(angle) + + # Save the image + im.save(image.path) + + serializer.save(edited_direction=self.request.data['direction']) diff --git a/apps/html_template_editor/urls.py b/apps/html_template_editor/urls.py new file mode 100644 index 000000000..7f4892c40 --- /dev/null +++ b/apps/html_template_editor/urls.py @@ -0,0 +1,47 @@ + +from django.urls import path + +from . import upload_views, views + +urlpatterns = [ + path( + 'add/', + views.TemplateContentAdd.as_view(), + name='add', + ), + path( + 'retrieve//', + views.TemplateContentRetrieve.as_view(), + name="retrieve", + ), + path( + 'update//', + views.TemplateContentUpdate.as_view(), + name='update', + ), + path( + 'list/', + views.TemplateContentList.as_view(), + name="list", + ), + path( + 'delete//', + views.TemplateContentDelete.as_view(), + name="delete", + ), + path( + 'images/add/', + upload_views.ImagesAdd.as_view(), + name='images_add', + ), + path( + 'images/update//', + upload_views.ImagesUpdate.as_view(), + name='images_update', + ), + path( + 'images/list/', + upload_views.ImagesList.as_view(), + name='images_list', + ), +] diff --git a/apps/html_template_editor/views.py b/apps/html_template_editor/views.py new file mode 100644 index 000000000..f291c160e --- /dev/null +++ b/apps/html_template_editor/views.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + + +from .models import TemplateContent +from .serializers import TemplateContentSerializer + +from rest_framework import filters, generics +from rest_framework.parsers import FormParser, MultiPartParser + +# +# Mixin for all company views. +# Defines serializers, queryset and permissions +# + + +class TemplateContentMixin(object): + def get_queryset(self): + return TemplateContent.objects.filter() + + def get_serializer_class(self): + return TemplateContentSerializer + + +class TemplateContentAdd(TemplateContentMixin, generics.CreateAPIView): + parser_classes = (MultiPartParser, FormParser,) + + def perform_create(self, serializer): + regions = self.request.data.get('regions') + images = self.request.data.get('images') + page = self.request.data.get('page') + + serializer.save( + regions=regions, + images=images, + page=page, + ) + + +class TemplateContentRetrieve(TemplateContentMixin, generics.RetrieveAPIView): + lookup_field = 'uuid' + + +class TemplateContentUpdate(TemplateContentMixin, generics.UpdateAPIView): + def post(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def perform_update(self, serializer): + serializer.save() + + +class TemplateContentList(TemplateContentMixin, generics.ListAPIView): + filter_backends = (filters.OrderingFilter,) + ordering_fields = ('created') + ordering = ('-created',) + + +class TemplateContentDelete(TemplateContentMixin, generics.DestroyAPIView): + lookup_field = 'uuid' From 0a954f181fb5a3c562f006673f384632907769c2 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 11:43:54 +0200 Subject: [PATCH 022/189] fix 'Images' model 'edited_crop' field 'max_length' parameter --- .../migrations/0002_auto_20191014_1140.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/html_template_editor/migrations/0002_auto_20191014_1140.py diff --git a/apps/html_template_editor/migrations/0002_auto_20191014_1140.py b/apps/html_template_editor/migrations/0002_auto_20191014_1140.py new file mode 100644 index 000000000..495cb4717 --- /dev/null +++ b/apps/html_template_editor/migrations/0002_auto_20191014_1140.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2019-10-14 09:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='images', + name='edited_crop', + field=models.CharField(blank=True, max_length=80, null=True), + ), + ] From 106641c1a4a33ad81bde6be4adad800644e8d944 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 11:45:41 +0200 Subject: [PATCH 023/189] remove debug info --- apps/aklub/static/aklub/js/form_field_widget_init.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/aklub/static/aklub/js/form_field_widget_init.js b/apps/aklub/static/aklub/js/form_field_widget_init.js index 108d83585..245a7a7a6 100644 --- a/apps/aklub/static/aklub/js/form_field_widget_init.js +++ b/apps/aklub/static/aklub/js/form_field_widget_init.js @@ -6,7 +6,6 @@ (function ($, HtmlTemplateFormFieldWidget) { $(document).ready(function () { - console.log(window.reverse) var formOpts = { editDialogPageContainer: 'template_page', popoverDelay: 3000 From 9a9d225882df99c0f217ae8ceda0203eb0be2ffc Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 11:50:23 +0200 Subject: [PATCH 024/189] rename js module file --- .../js/{html_template.js => form_field_html_template_widget.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/aklub/static/aklub/js/{html_template.js => form_field_html_template_widget.js} (100%) diff --git a/apps/aklub/static/aklub/js/html_template.js b/apps/aklub/static/aklub/js/form_field_html_template_widget.js similarity index 100% rename from apps/aklub/static/aklub/js/html_template.js rename to apps/aklub/static/aklub/js/form_field_html_template_widget.js From 501a7a0f194a8ae9a83fe806b95b9e28977f5575 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 11:51:50 +0200 Subject: [PATCH 025/189] rename js module in the 'MassCommunication' media class --- apps/aklub/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/aklub/admin.py b/apps/aklub/admin.py index 5921d4241..72027e8b4 100644 --- a/apps/aklub/admin.py +++ b/apps/aklub/admin.py @@ -1568,7 +1568,7 @@ class Media: 'jquery.inlineStyler/jquery.inlineStyler.min.js', 'webui-popover/dist/jquery.webui-popover.min.js', 'aklub/js/csrf_token.js', - 'aklub/js/html_template.js', + 'aklub/js/form_field_html_template_widget.js', 'aklub/js/form_field_widget_init.js', ) css = { From 08b1046cdaef7804ebb00b54f85943a2ca17cdf5 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 13:03:40 +0200 Subject: [PATCH 026/189] fix endcomment tag name --- .../templates/admin/aklub/masscommunication/change_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/aklub/templates/admin/aklub/masscommunication/change_form.html b/apps/aklub/templates/admin/aklub/masscommunication/change_form.html index e43da91c1..16f891a9a 100644 --- a/apps/aklub/templates/admin/aklub/masscommunication/change_form.html +++ b/apps/aklub/templates/admin/aklub/masscommunication/change_form.html @@ -78,7 +78,7 @@ {% comment %} {% wysiwyg_editor "id_template" %} {% wysiwyg_editor "id_template_en" %} - {% comment %} + {% endcomment %} {% comment %} {# ckeditor custom config example #} From 6306cb945a03e6fd90738bdc2dbd67e6d8fd54f3 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 13:15:40 +0200 Subject: [PATCH 027/189] enable initialize wysiwyg editor for more than one form field via 'js' api (django-wysiwyg app) --- Pipfile | 2 +- Pipfile.lock | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Pipfile b/Pipfile index 6cd32013b..2206477be 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ django-stdimage = "*" reportlab = "*" django-extensions = "*" "psycopg2" = "*" -django-wysiwyg = "*" +django-wysiwyg = {editable = true,git = "https://github.com/tmszi/django-wysiwyg.git",ref = "enable_editor_for_more_fields"} django-tinymce = "*" django-admin-tools = "*" feedparser = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 17bc62d1a..52df60d85 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -585,12 +585,9 @@ "version": "==3.0.2" }, "django-wysiwyg": { - "hashes": [ - "sha256:4de7214c0da50807cf1bef32d1ad16976048006d187a1b8daf0380ed326c424d", - "sha256:7c37c685379fa23402f415ba7f8f6bce3552d3836ed2cf99e37591e1cc0dffc3" - ], - "index": "pypi", - "version": "==0.8.0" + "editable": true, + "git": "https://github.com/tmszi/django-wysiwyg.git", + "ref": "def387340646697e49f17423a7534b4ba964bc24" }, "djangorestframework": { "hashes": [ From d9fed37a3862873adb159d3dfa312edf51690ce4 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 13:31:28 +0200 Subject: [PATCH 028/189] set 'MassCommunication' 'template' model field 'blank' argument as True --- .../migrations/0049_auto_20191014_1320.py | 20 ++++++++++++++ apps/aklub/models.py | 2 +- .../0012_auto_20191014_1320.py | 22 +++++++++++++++ .../0011_auto_20191014_1320.py | 27 +++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 apps/aklub/migrations/0049_auto_20191014_1320.py create mode 100644 local_migrations/migrations_admin/0012_auto_20191014_1320.py create mode 100644 local_migrations/migrations_advanced_filters/0011_auto_20191014_1320.py diff --git a/apps/aklub/migrations/0049_auto_20191014_1320.py b/apps/aklub/migrations/0049_auto_20191014_1320.py new file mode 100644 index 000000000..44c70e325 --- /dev/null +++ b/apps/aklub/migrations/0049_auto_20191014_1320.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.6 on 2019-10-14 11:20 + +import aklub.autocom +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0048_auto_20191014_0818'), + ] + + operations = [ + migrations.AlterField( + model_name='masscommunication', + name='template', + field=models.TextField(blank=True, help_text='Šablona může obsahovat následující proměnné:
{mr|mrs} or {mr/mrs}, $addressment, $name, $firstname, $surname, $street, $city, $zipcode, $email, $telephone, $regular_amount, $regular_frequency, $var_symbol, $last_payment_amount, $auth_token', max_length=50000, null=True, validators=[aklub.autocom.gendrify_text, django.core.validators.RegexValidator('^([^$]*(\\$(addressment|name|firstname|surname|street|city|zipcode|email|telephone|regular_amount|regular_frequency|var_symbol|last_payment_amount|auth_token)\\b)?)*$', 'Unknown variable')], verbose_name='Template'), + ), + ] diff --git a/apps/aklub/models.py b/apps/aklub/models.py index d753f7466..44e494026 100644 --- a/apps/aklub/models.py +++ b/apps/aklub/models.py @@ -2704,7 +2704,7 @@ class Meta: help_text=_("Template can contain following variable substitutions:
") + ("{mr|mrs} or {mr/mrs}, $" + ", $" .join(autocom.KNOWN_VARIABLES)), max_length=50000, - blank=False, + blank=True, null=True, validators=[gender_strings_validator, variable_validator], ) diff --git a/local_migrations/migrations_admin/0012_auto_20191014_1320.py b/local_migrations/migrations_admin/0012_auto_20191014_1320.py new file mode 100644 index 000000000..d62e3abaa --- /dev/null +++ b/local_migrations/migrations_admin/0012_auto_20191014_1320.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.6 on 2019-10-14 11:20 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +from aklub.migrations.data_migration.old_user_profile_model_hack import Settings as settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0011_auto_20191014_0818'), + ] + + operations = [ + migrations.AlterField( + model_name='logentry', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user'), + ), + ] diff --git a/local_migrations/migrations_advanced_filters/0011_auto_20191014_1320.py b/local_migrations/migrations_advanced_filters/0011_auto_20191014_1320.py new file mode 100644 index 000000000..80f590bfb --- /dev/null +++ b/local_migrations/migrations_advanced_filters/0011_auto_20191014_1320.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.6 on 2019-10-14 11:20 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + +from aklub.migrations.data_migration.old_user_profile_model_hack import Settings as settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('advanced_filters', '0010_auto_20191014_0818'), + ] + + operations = [ + migrations.AlterField( + model_name='advancedfilter', + name='created_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_advanced_filters', to=settings.AUTH_USER_MODEL, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='advancedfilter', + name='users', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Users'), + ), + ] From b97f3f5ccd6300818706b7d77bcd84fa3c0badc1 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 13:39:27 +0200 Subject: [PATCH 029/189] add 'clean' method (handle save form field html template value) in the 'MassCommunication' form class --- apps/aklub/admin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/aklub/admin.py b/apps/aklub/admin.py index 72027e8b4..aa006b565 100644 --- a/apps/aklub/admin.py +++ b/apps/aklub/admin.py @@ -1607,6 +1607,14 @@ def clean_send_to_users(self): ) return self.cleaned_data['send_to_users'] + def clean(self): + cleaned_data = self.cleaned_data + if self.cleaned_data.get('hidden_template'): + cleaned_data['template'] = self.cleaned_data.get('hidden_template') + if self.cleaned_data.get('hidden_template_en'): + cleaned_data['template_en'] = self.cleaned_data.get('hidden_template_en') + return cleaned_data + class MassCommunicationAdmin(unit_admin_mixin_generator('administrative_unit'), large_initial.LargeInitialMixin, admin.ModelAdmin): save_as = True From d46f697e1234a431d86bb79a5a55152ebb4956d2 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 14:44:50 +0200 Subject: [PATCH 030/189] fix jquery edit template dialog title name --- apps/aklub/static/aklub/js/form_field_html_template_widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/aklub/static/aklub/js/form_field_html_template_widget.js b/apps/aklub/static/aklub/js/form_field_html_template_widget.js index 4b66ca71f..fee6a0a8d 100644 --- a/apps/aklub/static/aklub/js/form_field_html_template_widget.js +++ b/apps/aklub/static/aklub/js/form_field_html_template_widget.js @@ -76,7 +76,7 @@ var HtmlTemplateFormFieldWidget = (function ($) { editTemplateDialog: function (templateName) { var that = this; var btnNames = {}; var title btnNames[gettext('Ok')] = function () {that.confirmEditTemplateDialog($(this))} - title = gettext('Edit') + ' ' + templateName + ' ' + gettext('template') + title = gettext('Edit') + ' \'' + templateName + '\' ' + gettext('template') $(this.editDialogContainerHtmlString).dialog({ modal: true, title: title, From 790811000d5f2a096d8f86bb0a4db9961ce8cf8e Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 14:56:49 +0200 Subject: [PATCH 031/189] add 'cssutils' python module for the parse css --- Pipfile | 1 + Pipfile.lock | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/Pipfile b/Pipfile index 2206477be..823c7611e 100644 --- a/Pipfile +++ b/Pipfile @@ -97,6 +97,7 @@ django-filter = "*" PyYAML = "*" mkdocs = "*" django-js-urls = "*" +cssutils = "*" [dev-packages] coverage = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 52df60d85..4bf972d2f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -110,6 +110,14 @@ ], "version": "==0.0.4" }, + "cssutils": { + "hashes": [ + "sha256:a2fcf06467553038e98fea9cfe36af2bf14063eb147a70958cfcaa8f5786acaf", + "sha256:c74dbe19c92f5052774eadb15136263548dd013250f1ed1027988e7fef125c8d" + ], + "index": "pypi", + "version": "==1.0.2" + }, "defusedxml": { "hashes": [ "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", From d91ae2013b85fb1b27cfa9da0c53b0031addd9ad Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 16:42:01 +0200 Subject: [PATCH 032/189] rename automat newsletter email template background svg images --- .../aklub/images/automat_green_newsletter.svg | 401 ------------------ .../images/automat_purple_newsletter.svg | 333 --------------- .../aklub/images/automat_red_newsletter.svg | 382 ----------------- 3 files changed, 1116 deletions(-) delete mode 100644 apps/aklub/static/aklub/images/automat_green_newsletter.svg delete mode 100644 apps/aklub/static/aklub/images/automat_purple_newsletter.svg delete mode 100644 apps/aklub/static/aklub/images/automat_red_newsletter.svg diff --git a/apps/aklub/static/aklub/images/automat_green_newsletter.svg b/apps/aklub/static/aklub/images/automat_green_newsletter.svg deleted file mode 100644 index 2df8ec4e6..000000000 --- a/apps/aklub/static/aklub/images/automat_green_newsletter.svg +++ /dev/null @@ -1,401 +0,0 @@ - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/aklub/static/aklub/images/automat_purple_newsletter.svg b/apps/aklub/static/aklub/images/automat_purple_newsletter.svg deleted file mode 100644 index d3c92b306..000000000 --- a/apps/aklub/static/aklub/images/automat_purple_newsletter.svg +++ /dev/null @@ -1,333 +0,0 @@ - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/aklub/static/aklub/images/automat_red_newsletter.svg b/apps/aklub/static/aklub/images/automat_red_newsletter.svg deleted file mode 100644 index 73120f986..000000000 --- a/apps/aklub/static/aklub/images/automat_red_newsletter.svg +++ /dev/null @@ -1,382 +0,0 @@ - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From f60d6d09d5cda76b25eae34811c234f126842155 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 14 Oct 2019 16:42:55 +0200 Subject: [PATCH 033/189] change automat html email newsletter template background image format to the jpeg --- .../templates/email_templates/automat_green_newsletter.html | 2 +- .../templates/email_templates/automat_purple_newsletter.html | 2 +- .../aklub/templates/email_templates/automat_red_newsletter.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/aklub/templates/email_templates/automat_green_newsletter.html b/apps/aklub/templates/email_templates/automat_green_newsletter.html index 1e0ae8be1..f6adcf0ef 100644 --- a/apps/aklub/templates/email_templates/automat_green_newsletter.html +++ b/apps/aklub/templates/email_templates/automat_green_newsletter.html @@ -4,7 +4,7 @@ {% block content %}

{% endblock %} diff --git a/apps/aklub/templates/email_templates/automat_purple_newsletter.html b/apps/aklub/templates/email_templates/automat_purple_newsletter.html index 7e80de5f8..fbe0cb2f7 100644 --- a/apps/aklub/templates/email_templates/automat_purple_newsletter.html +++ b/apps/aklub/templates/email_templates/automat_purple_newsletter.html @@ -119,5 +119,6 @@

Title

+ {% if footer %} {% include "email_templates/footer.html" %} {% endif %} {% endblock %} diff --git a/apps/aklub/templates/email_templates/automat_red_newsletter.html b/apps/aklub/templates/email_templates/automat_red_newsletter.html index 5382bf93e..8161b83ea 100644 --- a/apps/aklub/templates/email_templates/automat_red_newsletter.html +++ b/apps/aklub/templates/email_templates/automat_red_newsletter.html @@ -119,5 +119,6 @@

Title

+ {% if footer %} {% include "email_templates/footer.html" %} {% endif %} {% endblock %} diff --git a/apps/aklub/templates/email_templates/footer.html b/apps/aklub/templates/email_templates/footer.html new file mode 100644 index 000000000..7a6e46611 --- /dev/null +++ b/apps/aklub/templates/email_templates/footer.html @@ -0,0 +1,102 @@ +{% load static %} +{% load filters %} + + \ No newline at end of file diff --git a/apps/aklub/templates/email_templates/new_empty_template.html b/apps/aklub/templates/email_templates/new_empty_template.html index 6ddfd0589..61e076400 100644 --- a/apps/aklub/templates/email_templates/new_empty_template.html +++ b/apps/aklub/templates/email_templates/new_empty_template.html @@ -40,5 +40,6 @@ + {% if footer %} {% include "email_templates/footer.html" %} {% endif %} {% endblock %} diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index 3afbe614d..4adc05261 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -13,7 +13,9 @@ from django.utils.html import format_html_join, mark_safe from django.utils.translation import ugettext_lazy as _ -from html_template_editor.models import Images, TemplateContent +from html_template_editor.models import ( + Images, TemplateContent, TemplateFooter, +) from . import models as aklub_models @@ -133,6 +135,12 @@ def get_admin_url(self): def get_email_templates_names(): """ Get email templates names """ + templates_blacklist = [ + 'base.html', + 'new_empty_template.html', + 'footer', + ] + def get_templates_files(): path = pathlib.Path(__file__).parents[0] / 'templates' / 'email_templates' return os.listdir(path) @@ -141,7 +149,7 @@ def get_templates_files(): file_templates_names = [ (template, template) for template in [ template.split('.')[0] for template in get_templates_files() - if template not in ['base.html', 'new_empty_template.html'] + if template not in templates_blacklist ] ] # Db templates @@ -176,6 +184,10 @@ def get_email_template_context(template_path, template_url): else: context = {} + footer = TemplateFooter.objects.filter(show=True).first() + if footer: + context['footer'] = footer + background_image = Images.objects.filter( template_url=template_url, edited_crop__isnull=False, diff --git a/apps/html_template_editor/admin.py b/apps/html_template_editor/admin.py index 846f6b406..175b31837 100644 --- a/apps/html_template_editor/admin.py +++ b/apps/html_template_editor/admin.py @@ -1 +1,74 @@ + +from django.contrib import admin + +from .models import ( + CompanyEmail, CompanyPhone, CompanySocialMedia, + CompanyUrl, TemplateFooter, +) + # Register your models here. + + +@admin.register(CompanyPhone) +class CompanyPhoneAdmin(admin.ModelAdmin): + pass + + +@admin.register(CompanyUrl) +class CompanyUrlAdmin(admin.ModelAdmin): + pass + + +@admin.register(CompanyEmail) +class CompanyEmailAdmin(admin.ModelAdmin): + pass + + +@admin.register(CompanySocialMedia) +class CompanySocialMediaAdmin(admin.ModelAdmin): + pass + + +class CompanyPhoneAdminInline(admin.StackedInline): + model = TemplateFooter.phone.through + extra = 0 + + +class CompanyUrlAdminInline(admin.StackedInline): + model = TemplateFooter.url.through + extra = 0 + + +class CompanyEmailAdminInline(admin.StackedInline): + model = TemplateFooter.email.through + extra = 0 + + +class CompanySocialMediaAdminInline(admin.StackedInline): + model = TemplateFooter.social_media.through + extra = 0 + + +@admin.register(TemplateFooter) +class TemplateFooterAdminInline(admin.ModelAdmin): + list_display = ( + 'company_name', + 'address', + 'get_phone', + 'get_email', + 'get_url', + 'get_social_media', + 'show', + ) + exclude = ( + 'url', + 'phone', + 'email', + 'social_media', + ) + inlines = [ + CompanyPhoneAdminInline, + CompanyUrlAdminInline, + CompanyEmailAdminInline, + CompanySocialMediaAdminInline, + ] diff --git a/apps/html_template_editor/migrations/0004_companyemail_companyphone_companysocialmedia_companyurl_templatefooter.py b/apps/html_template_editor/migrations/0004_companyemail_companyphone_companysocialmedia_companyurl_templatefooter.py new file mode 100644 index 000000000..521078d9d --- /dev/null +++ b/apps/html_template_editor/migrations/0004_companyemail_companyphone_companysocialmedia_companyurl_templatefooter.py @@ -0,0 +1,76 @@ +# Generated by Django 2.2.10 on 2020-02-17 08:50 + +from django.db import migrations, models +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0003_auto_20200130_0656'), + ] + + operations = [ + migrations.CreateModel( + name='CompanyEmail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Company email')), + ], + options={ + 'verbose_name': 'Company email', + 'verbose_name_plural': 'Company email', + }, + ), + migrations.CreateModel( + name='CompanyPhone', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, verbose_name='Phone number')), + ], + options={ + 'verbose_name': 'Company phone number', + 'verbose_name_plural': 'Company phone numbers', + }, + ), + migrations.CreateModel( + name='CompanySocialMedia', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('icon_name', models.CharField(blank=True, choices=[('facebook', 'facebook'), ('google', 'google'), ('instagram', 'instagram'), ('twitter', 'twitter'), ('vimeo', 'vimeo'), ('youtube', 'youtube')], max_length=20, null=True, verbose_name='Icon name')), + ('url', models.URLField(blank=True, null=True, verbose_name='Url')), + ], + options={ + 'verbose_name': 'Social media', + 'verbose_name_plural': 'Social media', + }, + ), + migrations.CreateModel( + name='CompanyUrl', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.URLField(blank=True, null=True, verbose_name='Url')), + ], + options={ + 'verbose_name': 'Company url', + 'verbose_name_plural': 'Company urls', + }, + ), + migrations.CreateModel( + name='TemplateFooter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('company_name', models.CharField(blank=True, max_length=80, null=True, verbose_name='Company name')), + ('address', models.CharField(blank=True, max_length=80, null=True, verbose_name='Address')), + ('show', models.BooleanField(default=False, help_text='Show footer', verbose_name='Show')), + ('email', models.ManyToManyField(blank=True, to='html_template_editor.CompanyEmail', verbose_name='Email')), + ('phone', models.ManyToManyField(blank=True, to='html_template_editor.CompanyPhone', verbose_name='Phone')), + ('social_media', models.ManyToManyField(blank=True, to='html_template_editor.CompanySocialMedia', verbose_name='Social media')), + ('url', models.ManyToManyField(blank=True, to='html_template_editor.CompanyUrl', verbose_name='Url')), + ], + options={ + 'verbose_name': 'Template footer', + 'verbose_name_plural': 'Template footers', + }, + ), + ] diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index 20c027c30..13a4324a0 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -4,6 +4,12 @@ # from django.contrib.auth import get_user_model from django.db import models +from django.utils.html import mark_safe +from django.utils.translation import ugettext_lazy as _ + +from phonenumber_field.modelfields import PhoneNumberField + +from .utils import get_social_media_icons_names # User = get_user_model() @@ -97,3 +103,135 @@ def size(self): # datafile = models.FileField( # upload_to='files', # ) + + +class CompanyUrl(models.Model): + + class Meta: + verbose_name = _("Company url") + verbose_name_plural = _("Company urls") + + url = models.URLField( + verbose_name=_("Url"), + null=True, + blank=True, + ) + + def __str__(self): + return self.url + + +class CompanyPhone(models.Model): + + class Meta: + verbose_name = _("Company phone number") + verbose_name_plural = _("Company phone numbers") + + phone = PhoneNumberField( + verbose_name=_("Phone number"), + null=True, + blank=True, + ) + + def __str__(self): + return str(self.phone) + + +class CompanyEmail(models.Model): + + class Meta: + verbose_name = _("Company email") + verbose_name_plural = _("Company email") + + email = models.EmailField( + verbose_name=_("Company email"), + null=True, + blank=True, + ) + + def __str__(self): + return self.email + + +class CompanySocialMedia(models.Model): + + class Meta: + verbose_name = _("Social media") + verbose_name_plural = _("Social media") + + icon_name = models.CharField( + verbose_name=_("Icon name"), + choices=get_social_media_icons_names(), + max_length=20, + null=True, + blank=True, + ) + url = models.URLField( + verbose_name=_("Url"), + null=True, + blank=True, + ) + + def __str__(self): + return f'{self.icon_name} : {self.url}' + + +class TemplateFooter(models.Model): + + class Meta: + verbose_name = _("Template footer") + verbose_name_plural = _("Template footers") + + company_name = models.CharField( + verbose_name=_("Company name"), + max_length=80, + null=True, + blank=True, + ) + address = models.CharField( + verbose_name=_("Address"), + max_length=80, + null=True, + blank=True, + ) + phone = models.ManyToManyField( + verbose_name=_("Phone"), + to=CompanyPhone, + blank=True, + ) + email = models.ManyToManyField( + verbose_name=_("Email"), + to=CompanyEmail, + blank=True, + ) + url = models.ManyToManyField( + verbose_name=_("Url"), + to=CompanyUrl, + blank=True, + ) + social_media = models.ManyToManyField( + verbose_name=_("Social media"), + to=CompanySocialMedia, + blank=True, + ) + show = models.BooleanField( + verbose_name=_("Show"), + default=False, + help_text=_("Show footer"), + ) + + def __str__(self): + return self.company_name + + def get_url(self): + return mark_safe("
".join([o.url for o in self.url.all()])) + + def get_email(self): + return mark_safe("
".join([o.email for o in self.email.all()])) + + def get_phone(self): + return mark_safe("
".join([str(o.phone) for o in self.phone.all()])) + + def get_social_media(self): + objs = self.social_media.all() + return mark_safe("
".join([f"{o.icon_name}: {o.url}" for o in objs])) diff --git a/apps/html_template_editor/templatetags/__init__.py b/apps/html_template_editor/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/html_template_editor/templatetags/filters.py b/apps/html_template_editor/templatetags/filters.py new file mode 100644 index 000000000..6ddfa361d --- /dev/null +++ b/apps/html_template_editor/templatetags/filters.py @@ -0,0 +1,16 @@ + +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + + +@register.filter(name='get_link_element') +def get_link_element(value, arg): + if arg == 'list': + links = [] + for i in value: + links.append(mark_safe(f'
{i}')) + return links + elif arg == 'str': + return mark_safe(f'{value}') diff --git a/apps/html_template_editor/utils.py b/apps/html_template_editor/utils.py new file mode 100644 index 000000000..f8bbc59b2 --- /dev/null +++ b/apps/html_template_editor/utils.py @@ -0,0 +1,18 @@ +import pathlib + +from django.contrib.staticfiles import finders + + +def get_social_media_icons_names(): + social_media_choices = [] + _finders = finders.AppDirectoriesFinder() + + img_src_dir = _finders.find_in_app( + app='aklub', + path='aklub/images/social_media', + ) + imgs = pathlib.Path(img_src_dir).glob('**/*') + for img in imgs: + social_media_choices.append((img.stem, (img.stem))) + + return sorted(social_media_choices, key=lambda tup: tup[0]) diff --git a/project/settings/base.py b/project/settings/base.py index 89e53dc84..7d4cede4a 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -567,6 +567,7 @@ def get_payment_channel_resource(): 'html_template_editor:images_update', 'aklub:get_email_template', 'aklub:get_email_template_from_db', + 'aklub:get_email_template_footer', ) STATICFILES_STORAGE = 'project.settings.custom_storage.CustomStaticFilesStorage' From 8dc3abdb5ad8c3305eca763ab2722a379a8502d7 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Tue, 18 Feb 2020 07:45:15 +0100 Subject: [PATCH 124/189] fix: remove table cell border --- .../email_templates/automat_green_newsletter.html | 4 ++-- .../email_templates/automat_purple_newsletter.html | 4 ++-- .../email_templates/automat_red_newsletter.html | 13 +++++++------ .../email_templates/new_empty_template.html | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/apps/aklub/templates/email_templates/automat_green_newsletter.html b/apps/aklub/templates/email_templates/automat_green_newsletter.html index 550eea4e5..40d19f84c 100644 --- a/apps/aklub/templates/email_templates/automat_green_newsletter.html +++ b/apps/aklub/templates/email_templates/automat_green_newsletter.html @@ -11,7 +11,7 @@ > - + - - "),g=$(""),f=$(""),f.css({border:"none",padding:0,margin:0}),f.attr({align:a,width:b,heigth:c}),d.append(e.append(g.append(f)))},b.prototype._replaceImgSrc=function(){var a;return a=this._$editTemplatePageContainer.find("img"),a.each(function(a){return function(b,c){var d,e,f,g,h;return d=$(c).closest(a.getClassFormat(a._videoClass)),e=d.length>0?d:$(c),0===d.length&&(h=$(c).attr("src"),g=new RegExp(window.origin),h.match(g)||(f=""+window.origin+$(c).attr("src"),$(c).attr("src",f))),a.convertImageCssPositionToTable(e)}}(this))},b.prototype.convertImageCssPositionToTable=function(a){var b,c,d,e,f,g,h;a=a.find("img").length>0?a.find("img"):a,f=parseInt(a.css("margin-left").split(".")[0].replace("px","")),g=parseInt(a.css("margin-right").split(".")[0].replace("px","")),e=a.css("float"),a.css("display",""),f===g&&"none"===e?c=this.getElementPosition("center","",""):f>g?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):g>f?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):"left"!==e&&"right"!==e||(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)),h=this.getClassFormat(this._videoClass),d=a.closest(h),c.find("td").append(d.length>0?d.clone():a.clone()),"left"===e?c.find("tr").append(b):c.find("tr").prepend(b),d.length>0?d.after(c):a.after(c),d.length>0?d.remove():a.remove()},b.prototype.convertImgFloatCssPositionToTable=function(a,b,c){var d,e,f,g,h,i,j;return i=15,j="left"===c?"0 0 0 "+i+"px":"0 "+i+"px 0 0",e=$(""),e.css({border:"none",padding:j,margin:"0"}),f=a.closest(this.getClassFormat(this._videoClass)),d=f.length>0?f.next().clone():a.next().clone(),h=parseFloat(b.find("#content_table").width()),g=h-parseFloat(a.attr("width"))-i,d.css("width",g),e.attr("width",g),e.append(d),f.length>0?f.next().remove():a.next().remove(),e},b.prototype._convertTextCssPositionToTable=function(){var a;return a=this._textContainerTags.join(", "),this._$editTemplatePageContainer.find(a).each(function(a){return function(b,c){var d,e;switch(e=$(c).attr("class")?$(c).attr("class").split(" "):[]){case e.indexOf("text-left")>-1:d=a.getElementPosition("left","","");break;case e.indexOf("text-right")>-1:d=a.getElementPosition("right","","");break;case e.indexOf("text-center")>-1:d=a.getElementPosition("center","","");break;default:d=a.getElementPosition("center","","")}return a.adjustListCssProperties($(c)),d.find("td").append($(c).clone()),$(c).after(d),$(c).remove()}}(this))},b.prototype.adjustListCssProperties=function(a){if(a.is("ul")||a.is("ol"))return a.css({margin:"10px",padding:"15px"})},b.prototype._fixMargin=function(){return $(this._htmlDoc).find("article").css("margin","")},b.prototype._getNumberValue=function(a,b){return null==b&&(b=-2),parseInt(a.slice(0,a.length-b))},b.prototype._getPaddingValue=function(a){var b,c;return null==a&&(a=!0),c=!0===a?"padding--left":"padding--top",b=$(this._htmlDoc).find(".article__content p").first().css(c),this._getNumberValue(b)},b.prototype._fixContentWidth=function(){var a,b;return b=this._getPaddingValue(),a=this._textContainerTags.join(", "),$(this._htmlDoc).find("article").find(a).each(function(a){return function(c,d){var e;return e=a._getNumberValue($(d).css("width")),$(d).css({width:e-2*b})}}(this))},b.prototype._eraseVerticalSpaceContent=function(){var a;return a=this.getClassFormat(this._verticalSpaceClassName),this._$editTemplatePageContainer.find(a).each(function(a,b){return $(b).find("center").remove()})},b}(b)}).call(this); \ No newline at end of file diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee index 8c97a373a..4689a5658 100644 --- a/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee +++ b/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee @@ -167,9 +167,6 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin # childrens with childrens from the 'template_textarea' div # form field widget container - # Disable tinymce form field widget editor - @_disableFormFieldWysiwygEditor() - @_$templateNameField.attr('disabled', false) # Exchange form field container childrens @@ -279,20 +276,11 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin @_$templateTextareaField.attr attr - # Enable tinymce form field widget editor - @_enableFormFieldWysiwygEditor() - @_$templateTextAreaFormFieldContainer.addClass('hidden') # Erase hidden template form field @_$hiddenTemplateField.val('') - _enableFormFieldWysiwygEditor: () -> - django_wysiwyg.enable(@_templateDivFieldId.slice(3)) - - _disableFormFieldWysiwygEditor: () -> - django_wysiwyg.disable(@_templateDivFieldId.slice(3)) - _exchangeContent: () -> @_getTemplateFieldContainer() From 6af044e0ea593cb525d4bd3810e936e807addaf1 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Wed, 29 Apr 2020 14:14:10 +0200 Subject: [PATCH 156/189] feat: load ContentTools translation file --- .../static/aklub/js/internationalization.js | 34 +++++++++++++++++++ .../aklub/templates/email_templates/base.html | 4 +++ apps/aklub/urls.py | 5 +++ apps/aklub/views.py | 21 +++++++++++- project/settings/base.py | 1 + 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 apps/aklub/static/aklub/js/internationalization.js diff --git a/apps/aklub/static/aklub/js/internationalization.js b/apps/aklub/static/aklub/js/internationalization.js new file mode 100644 index 000000000..257323932 --- /dev/null +++ b/apps/aklub/static/aklub/js/internationalization.js @@ -0,0 +1,34 @@ +(function() { + var editor, xhr; + + // Get the editor + editor = new ContentTools.EditorApp.get(); + + // Define our request for the French translation file + xhr = new XMLHttpRequest(); + xhr.open( + 'GET', + window.reverse('aklub:get_contenttools_translation', djangoLang), + true); + + function onStateChange (ev) { + var translations; + if (ev.target.readyState == 4) { + // Convert the JSON data to a native Object + translations = JSON.parse(ev.target.responseText); + + // Add the translations for the French language + ContentEdit.addTranslations(djangoLang, translations); + + // Set French as the editors current language + ContentEdit.LANGUAGE = djangoLang; + } + } + + if (djangoLang != "en") { + xhr.addEventListener('readystatechange', onStateChange); + } + + // Load the language + xhr.send(null); +}()); diff --git a/apps/aklub/templates/email_templates/base.html b/apps/aklub/templates/email_templates/base.html index f45951119..6e2e201fc 100644 --- a/apps/aklub/templates/email_templates/base.html +++ b/apps/aklub/templates/email_templates/base.html @@ -1,7 +1,9 @@ {% load static %} +{% load i18n %} {% load automat_fonts %} {% get_automat_fonts %} +{% get_current_language as LANGUAGE_CODE %} @@ -23,10 +25,12 @@ + + diff --git a/apps/aklub/urls.py b/apps/aklub/urls.py index 3e37c7dc9..ecdeb4b36 100644 --- a/apps/aklub/urls.py +++ b/apps/aklub/urls.py @@ -48,4 +48,9 @@ login_required(views.get_email_template_from_db), name='get_email_template_from_db', ), + path( + 'get_contenttools_translation//', + views.get_contenttools_translation, + name='get_contenttools_translation', + ), ] diff --git a/apps/aklub/views.py b/apps/aklub/views.py index f9a145495..97dd82813 100644 --- a/apps/aklub/views.py +++ b/apps/aklub/views.py @@ -1,4 +1,3 @@ - # -*- coding: utf-8 -*- # Author: Petr Dlouhý # @@ -908,3 +907,23 @@ def get_email_template_from_db(request, template_name): template, context = get_email_template_context(template_path, template_url) return HttpResponse(template.render(context)) + + +def get_contenttools_translation(request, language): + """ Get ContentTools translation file """ + + cttools_language_file = ( + pathlib.Path(settings.BASE_DIR) / 'bower_components' / + 'ContentTools' / 'translations' / f"{language}.json" + ) + + if not cttools_language_file.exists(): + message_text = _( + "language '%(lang)s.json' translation file doesn't exist", + ) % {'lang': language} + return JsonResponse({'message': message_text}) + + with open(cttools_language_file) as f: + translation = json.load(f) + + return JsonResponse(translation) diff --git a/project/settings/base.py b/project/settings/base.py index 7d4cede4a..51e6dd7a7 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -568,6 +568,7 @@ def get_payment_channel_resource(): 'aklub:get_email_template', 'aklub:get_email_template_from_db', 'aklub:get_email_template_footer', + 'aklub:get_contenttools_translation', ) STATICFILES_STORAGE = 'project.settings.custom_storage.CustomStaticFilesStorage' From d11850296d4b9fb309af2be7dc7723d8098ba9d2 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Wed, 29 Apr 2020 14:15:57 +0200 Subject: [PATCH 157/189] fix: remove unused import --- apps/aklub/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/aklub/views.py b/apps/aklub/views.py index 97dd82813..a193255a8 100644 --- a/apps/aklub/views.py +++ b/apps/aklub/views.py @@ -51,8 +51,6 @@ from extra_views import InlineFormSet, UpdateWithInlinesView -from html_template_editor.models import TemplateContent - from interactions.models import PetitionSignature # TODO: Not sure if it works (model moved from aklub to interaction app) from sesame.backends import ModelBackend From 3f0c59b73caa74c6198db3d38c1067ff4efd3372 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 4 May 2020 07:30:47 +0200 Subject: [PATCH 158/189] feat: add custom ContentTools tools cs language translation file --- .../aklub/js/contenttools/src/translations/cs.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 apps/aklub/static/aklub/js/contenttools/src/translations/cs.json diff --git a/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json b/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json new file mode 100644 index 000000000..07c98a81f --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json @@ -0,0 +1,9 @@ +{ + "Align justify": "Zarovnat do bloku", + "Background image": "Obrázek na pozadí", + "Button": "Tlačidlo", + "Decrease font size": "Zmenšit velikost písma", + "Font size": "Velikost písma", + "Increase font size": "Zvětšit velikost písma", + "Line spacing": "Řádkování" +} From 1eccad8cc9145f6a16502d83f3e35e3eaf471b95 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 4 May 2020 07:32:21 +0200 Subject: [PATCH 159/189] feat: append custom localization files in the 'build_contenttools' command --- .../management/commands/build_contenttools.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/apps/aklub/management/commands/build_contenttools.py b/apps/aklub/management/commands/build_contenttools.py index 533748e41..6b75f4e41 100644 --- a/apps/aklub/management/commands/build_contenttools.py +++ b/apps/aklub/management/commands/build_contenttools.py @@ -1,3 +1,4 @@ +import json import os import pathlib import re @@ -58,6 +59,10 @@ def handle(self, *args, **options): pathlib.Path(self.styles_src_dir) / 'contenttools' / 'src' / 'sandbox' / 'styles' ) + self.cttools_src_trans_dir = ( + pathlib.Path(self.js_src_dir) / 'contenttools' / 'src' / + 'translations' + ) # Target dirs self.cttools_target_dir = ( @@ -80,11 +85,15 @@ def handle(self, *args, **options): self.cttools_target_sandbox__build_dir = ( self.cttools_target_dir / 'sandbox' ) + self.cttools_target_trans_dir = ( + self.cttools_target_dir / 'translations' + ) self.__copy_coffee_scripts() self.__copy_sass_styles() self.__copy_fonts() self.__copy_gruntfile() + self.__copy_localizations() os.chdir(self.cttools_target_dir) @@ -233,6 +242,37 @@ def __copy_gruntfile(self): ) shutil.copy(grunt_file, self.cttools_target_dir / grunt_file.name) + def __copy_localizations(self): + """Copy localization json files""" + + trans_src_files = self.cttools_src_trans_dir.glob('*.json') + + for trans_src_file in trans_src_files: + + trans_target_file = (self.cttools_target_trans_dir / + trans_src_file.name) + + with open(trans_src_file, 'r') as src_read_trans_f, \ + open(trans_target_file, 'r+', encoding='utf-8') as \ + target_write_trans_f: + + src_trans = json.load(src_read_trans_f) + target_trans = json.load(target_write_trans_f) + target_trans_keys = target_trans.keys() + update_trans = {} + + for trans_string in src_trans.keys(): + if trans_string not in target_trans_keys: + update_trans[trans_string] = src_trans[trans_string] + + if update_trans: + target_trans.update(update_trans) + + target_write_trans_f.seek(0) + target_write_trans_f.truncate(0) + json.dump(target_trans, target_write_trans_f, indent=4, + ensure_ascii=False, sort_keys=True) + def __install(self, command, program, package='install'): """Install/build package""" From d2072174a21c51b058662cc0e4e9a002065002af Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 4 May 2020 08:39:42 +0200 Subject: [PATCH 160/189] fix: ContentTools custom decrease font size tool translation text --- .../js/contenttools/src/scripts/decrease-font-size-tool.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee index 42c20c178..7df69c04c 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee @@ -191,7 +191,7 @@ class GetDecreaseFontSizeValueDialog extends ContentTools.LinkDialog @_domInput.setAttribute 'name', 'size' @_domInput.setAttribute 'value', @size @_domInput.setAttribute 'placeholder', \ - ContentEdit._ 'Enter a font size deacrease factor (px)' + ContentEdit._ 'Enter a font size decrease factor (px)' # Remove the new window target DOM element @_domElement.removeChild @_domTargetButton From d64fa4eddd6269a11af454924af30c58a5d5fa4e Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 4 May 2020 08:40:30 +0200 Subject: [PATCH 161/189] fix: update ContentTools cs language translation file --- .../aklub/js/contenttools/src/translations/cs.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json b/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json index 07c98a81f..f7afc5ea9 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json +++ b/apps/aklub/static/aklub/js/contenttools/src/translations/cs.json @@ -5,5 +5,14 @@ "Decrease font size": "Zmenšit velikost písma", "Font size": "Velikost písma", "Increase font size": "Zvětšit velikost písma", - "Line spacing": "Řádkování" + "Line spacing": "Řádkování", + "Add vertical space": "Přidejte svislú mezeru", + "Enter number to set the line height": "Zadejte číslo pro nastavení výšky řádku", + "Enter a text color": "Zadejte barvu textu", + "Enter a button padding space": "Zadejte hodnotu vzdálenosti textu tlačidla od okraje", + "Enter a background color": "Zadejte barvu pozadí", + "Enter a font size deacrease factor (px)": "Zadejte faktor snížení velikosti písma (px)", + "Enter a font size increase factor (px)": "Zadejte faktor zvětšení velikosti písma (px)", + "Enter a font size (px)": "Zadejte velikost písma (px)", + "Enter a vertical space (px)": "Zadejte svislú mezeru (px)" } From 26e02ddb70451ba8f4e2ce5c028907b34d4a9516 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 11 May 2020 06:14:42 +0200 Subject: [PATCH 162/189] fix: flake8 errors after rebase --- apps/aklub/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index 957b8ef0b..566f3f1b7 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -19,6 +19,10 @@ from . import models as aklub_models +from html_template_editor.models import ( + Images, TemplateContent, TemplateFooter, TemplateHeader, +) + def sweet_text(generator): """ From a39734a21b0dc4bbee9c90b44cc3e0ba2be2cf3a Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 11 May 2020 06:23:28 +0200 Subject: [PATCH 163/189] chore: add merged migration file --- apps/aklub/migrations/0076_merge_20200511_0622.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/aklub/migrations/0076_merge_20200511_0622.py diff --git a/apps/aklub/migrations/0076_merge_20200511_0622.py b/apps/aklub/migrations/0076_merge_20200511_0622.py new file mode 100644 index 000000000..4b2d5c9c3 --- /dev/null +++ b/apps/aklub/migrations/0076_merge_20200511_0622.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-05-11 04:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0075_delete_useryearpayments'), + ('aklub', '0075_merge_20200421_0651'), + ] + + operations = [ + ] From 6d060f19b8f40e7de2753a2a9dbbbbf488593e2e Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 11 May 2020 08:25:04 +0200 Subject: [PATCH 164/189] fix: add missing model field verbose name arg into the all fields/models --- .../migrations/0009_auto_20200511_0823.py | 123 ++++++++++++++++++ apps/html_template_editor/models.py | 40 +++++- 2 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 apps/html_template_editor/migrations/0009_auto_20200511_0823.py diff --git a/apps/html_template_editor/migrations/0009_auto_20200511_0823.py b/apps/html_template_editor/migrations/0009_auto_20200511_0823.py new file mode 100644 index 000000000..a0f9c0ccf --- /dev/null +++ b/apps/html_template_editor/migrations/0009_auto_20200511_0823.py @@ -0,0 +1,123 @@ +# Generated by Django 2.2.12 on 2020-05-11 06:23 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0008_auto_20200425_0532'), + ] + + operations = [ + migrations.AlterModelOptions( + name='companyemail', + options={'verbose_name': 'Company email', 'verbose_name_plural': 'Company emails'}, + ), + migrations.AlterModelOptions( + name='companysocialmedia', + options={'verbose_name': 'Company social media', 'verbose_name_plural': 'Company social media'}, + ), + migrations.AlterModelOptions( + name='images', + options={'verbose_name': 'Images', 'verbose_name_plural': 'Images'}, + ), + migrations.AlterModelOptions( + name='templatecontent', + options={'verbose_name': 'Template content', 'verbose_name_plural': 'Template contents'}, + ), + migrations.AlterField( + model_name='images', + name='background_image', + field=models.BooleanField(default=False, verbose_name='Background image'), + ), + migrations.AlterField( + model_name='images', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='Created'), + ), + migrations.AlterField( + model_name='images', + name='edited_crop', + field=models.CharField(blank=True, max_length=80, null=True, verbose_name='Edited crop'), + ), + migrations.AlterField( + model_name='images', + name='edited_direction', + field=models.CharField(blank=True, max_length=5, null=True, verbose_name='Edited direction'), + ), + migrations.AlterField( + model_name='images', + name='edited_width', + field=models.IntegerField(blank=True, null=True, verbose_name='Edited width'), + ), + migrations.AlterField( + model_name='images', + name='image', + field=models.ImageField(null=True, upload_to='images', verbose_name='Image'), + ), + migrations.AlterField( + model_name='images', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='Modified'), + ), + migrations.AlterField( + model_name='images', + name='name', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='images', + name='template_url', + field=models.CharField(blank=True, max_length=350, null=True, verbose_name='Template url'), + ), + migrations.AlterField( + model_name='templatecontent', + name='created', + field=models.DateTimeField(auto_now_add=True, verbose_name='Created'), + ), + migrations.AlterField( + model_name='templatecontent', + name='images', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Images'), + ), + migrations.AlterField( + model_name='templatecontent', + name='modified', + field=models.DateTimeField(auto_now=True, verbose_name='Modified'), + ), + migrations.AlterField( + model_name='templatecontent', + name='page', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Pages'), + ), + migrations.AlterField( + model_name='templatecontent', + name='regions', + field=models.TextField(blank=True, default='', null=True, verbose_name='Regions'), + ), + migrations.AlterField( + model_name='templatecontent', + name='styles', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='Styles'), + ), + migrations.AlterField( + model_name='templatecontent', + name='uuid', + field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='Universally unique identifier'), + ), + migrations.AlterField( + model_name='templatefooter', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + migrations.AlterField( + model_name='templateheader', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + ] diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index 95586041f..cb9037411 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -17,33 +17,44 @@ class TemplateContent(models.Model): + class Meta: + verbose_name = _("Template content") + verbose_name_plural = _("Template contents") + created = models.DateTimeField( + verbose_name=_("Created"), auto_now_add=True, ) modified = models.DateTimeField( + verbose_name=_("Modified"), auto_now=True, ) uuid = models.UUIDField( + verbose_name=_("Universally unique identifier"), default=uuid.uuid4, editable=False, unique=True, ) images = models.CharField( + verbose_name=_("Images"), max_length=100, blank=True, null=True, ) page = models.CharField( + verbose_name=_("Pages"), max_length=100, blank=True, null=True, ) regions = models.TextField( + verbose_name=_("Regions"), blank=True, default='', null=True, ) styles = JSONField( + verbose_name=_("Styles"), blank=True, null=True, ) @@ -51,13 +62,20 @@ class TemplateContent(models.Model): class Images(models.Model): + class Meta: + verbose_name = _("Images") + verbose_name_plural = _("Images") + created = models.DateTimeField( + verbose_name=_("Created"), auto_now_add=True, ) modified = models.DateTimeField( + verbose_name=_("Modified"), auto_now=True, ) image = models.ImageField( + verbose_name=_("Image"), upload_to='images', height_field=None, width_field=None, @@ -65,28 +83,34 @@ class Images(models.Model): null=True, ) name = models.CharField( + verbose_name=_("Name"), max_length=100, blank=True, null=True, ) edited_width = models.IntegerField( + verbose_name=_("Edited width"), blank=True, null=True, ) edited_crop = models.CharField( + verbose_name=_("Edited crop"), max_length=80, blank=True, null=True, ) edited_direction = models.CharField( + verbose_name=_("Edited direction"), max_length=5, blank=True, null=True, ) background_image = models.BooleanField( + verbose_name=_("Background image"), default=False, ) template_url = models.CharField( + verbose_name=_("Template url"), max_length=350, blank=True, null=True, @@ -146,7 +170,7 @@ class CompanyEmail(models.Model): class Meta: verbose_name = _("Company email") - verbose_name_plural = _("Company email") + verbose_name_plural = _("Company emails") email = models.EmailField( verbose_name=_("Company email"), @@ -161,8 +185,8 @@ def __str__(self): class CompanySocialMedia(models.Model): class Meta: - verbose_name = _("Social media") - verbose_name_plural = _("Social media") + verbose_name = _("Company social media") + verbose_name_plural = _("Company social media") icon_name = models.CharField( verbose_name=_("Icon name"), @@ -232,8 +256,9 @@ class Meta: blank=True, ) user = models.ForeignKey( - settings.AUTH_USER_MODEL, - models.SET_NULL, + verbose_name=_("User"), + to=settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, blank=True, null=True, ) @@ -334,8 +359,9 @@ class Meta: help_text=_("Left or right column logo position"), ) user = models.ForeignKey( - settings.AUTH_USER_MODEL, - models.SET_NULL, + verbose_name=_("User"), + to=settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, blank=True, null=True, ) From 1b0f715aa7936c6be5a64e4e1353ee676b2b2623 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 11 May 2020 09:05:36 +0200 Subject: [PATCH 165/189] fix: logo image file size validator message --- apps/html_template_editor/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/html_template_editor/validators.py b/apps/html_template_editor/validators.py index 220b7ce5c..69d347801 100644 --- a/apps/html_template_editor/validators.py +++ b/apps/html_template_editor/validators.py @@ -28,7 +28,7 @@ def validate_logo_image(image): if image_file_size > megabyte_limit: raise ValidationError( - _("Image size is larger than what is allowed " + _("Image file size is larger than what is allowed " "%(max_file_size)s MB"), params={'max_file_size': megabyte_limit}, ) From 9895486776c7c4cfd33b8181c4538a4e38d8b255 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 11 May 2020 09:41:09 +0200 Subject: [PATCH 166/189] feat: add 'html_template_editor' app cs language localization file --- .../locale/cs_CZ/LC_MESSAGES/django.mo | Bin 0 -> 3948 bytes .../locale/cs_CZ/LC_MESSAGES/django.po | 277 ++++++++++++++++++ project/settings/base.py | 1 + 3 files changed, 278 insertions(+) create mode 100644 apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.mo create mode 100644 apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.po diff --git a/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.mo b/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..9adedc15d38a4111a0f25aea3754b703cf0b82d6 GIT binary patch literal 3948 zcma)-U2I%O700J(NWpxSG(bxDn0_SKajvs=oDVi}nzge@-0s?zckMt_5smkbcW?LJ zndRQQ8}Gs+F9-q>YDFv~5hE($hg4O03u=Y5HWECws>Dk_o|+dxhzNvu2#EigJL_G? zj>729{msXjnKS2{nfY+fZ%F1O5Q~6nF#N2fhk^9=vVqZ-bvi{SHWScR^a`zqWl3CZ~1wg4F*Eh%S}_ zY1|3$Yv8!8FIXz@D`;6hL452D+kP7)`QL%0|Ayr|Ajy9Keh&OINbB4K zY5$L6F>2ojeil3gQu_&8zi8{1L7JxmlI!Xq*`aCcKLcs~74SHC6Z|cF{U?yt`zbbk z6kG2$Uy1_JP!X0Hk>bLE7&NAjQ>-;4@$q#K(SuAL;pXa2ouTt^X4wzuW_dz+G_W zm%w3={F=iLOS3EBB)AAtyu1#=BwN_FgMR_Zp8H@l(*H1c z5*!8LUZc|&KuY8<8bkAR5=Y?>@_|kRNO!>ivb_RK{L>jZjXaD@^B+N`c%oRMM0W?p z#n-8Vgyqftj)4>pyO7C#7Q;dBB zc?6l_f)d%0lCghMG`9aD%BO6dK=DOsG=YhSG|DODXOZDb<12u%jO&Inide??^)=~o zFA!zP7j+njn0paF*l${{CN0rG#M!tgRm-8SD=W>$bsXz}O-L2XkWJ{ICRCHlKzRPv zTC`bJB9NPvT16|#RXtef8Q$C29T(}6Cwv}A*AtuL>tS~tlp!5T5lfe4T@T=;P}e$T z*9&DS_O$9WT=U$x!m>u$3tD3b+$eWR3K!*y4!xFEF&2&GQp{^Q^2~7C-z!T3%(gdV z7<;8{qb5sQCG$^`p_%#fMbDQ!@>-HC?u)RDh2x4){8}aPg0S8>*dK_ct0aDv#Nmzn zcrr)Ca&j>?KdwU_dgV&YOWLmoiu<~(yNc(i*0Zh69hnZ}ZDxv_9eJ@dWCO!kiyup+ zcx~wj7YG3;)amSQ<2dP+_gvSPEU#T}(StXp$mcWIb;M@K#ypXw=<@4Fu!sPT+->j_ZFn01$*>j!odO326I?elLri+d_78vB5 zhgs5mBsG$9QpcU)VV*jb9yvLPdJ2NhtZaBRxt04?HZ$wwr=9Z|erYTwtiv?FFqg~msZ2i0&t&tN$=tcj`SY{cLg6e#Rk;qoJ4GpiG|y$GF3yq4 zXL7l7zBi=9a#seb<|p&n?*4{{QhgJN2c2TG2EFN+9ffN>b()tdB8+4_Hdnmhoa)Mv zT8lDtvZ{pWQ{^;2wcy2lGk!f3zH^n)K@?C+ z4e;l9il;d`&yMktr1Q)eAKflCY@}%5Qy9o;(`Zz zV$ZfUw>CZ>wAYm0+E$dlT+|9v^7ZTOTao{;ezA2t9l`dhPvbE=Zsx7sm*s+Pv{$(> z7U2CjDg!%7tR)i7oh0U)aYJLTO0&6IC>pTf8b?F>R!h}m*rL<=CXa&k6`V9HwaPbE z+qX8BtHRoYO~Kl-!CURsnxK)F>s01N7hpzew<8tCuJ+lC^&1Koa{#MrAoV6+L= zdS?!=s#TK}V5mS=*}U}85Sa)NEtqJn0>kV2f(}hEG}c#QQvL{6a^o!uk6IEu8ga72 zJ7DImHrc!{8!%Gm=xXp1q6t-ofv_smLjaHVt9oRYT zO0z3CQlf=Z(J6GD-P>Z_xh;Cxv|AM|I&jhbfo{8 zNtWLtbbr&1Cueh61gFl;FOjD*!i4=ixzc;)Z5hW{X5}%-GzCI^Kg?V+*P^D6xhuhTVi literal 0 HcmV?d00001 diff --git a/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.po b/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.po new file mode 100644 index 000000000..30c61d6f1 --- /dev/null +++ b/apps/html_template_editor/locale/cs_CZ/LC_MESSAGES/django.po @@ -0,0 +1,277 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-05-11 08:27+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n " +"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" + +#: apps/html_template_editor/models.py:21 +msgid "Template content" +msgstr "Obsah šablony" + +#: apps/html_template_editor/models.py:22 +msgid "Template contents" +msgstr "Obsah šablony" + +#: apps/html_template_editor/models.py:25 +#: apps/html_template_editor/models.py:70 +msgid "Created" +msgstr "Vytvořeno" + +#: apps/html_template_editor/models.py:29 +#: apps/html_template_editor/models.py:74 +msgid "Modified" +msgstr "Upraveno" + +#: apps/html_template_editor/models.py:33 +msgid "Universally unique identifier" +msgstr "Univerzálně jedinečný identifikátor" + +#: apps/html_template_editor/models.py:39 +#: apps/html_template_editor/models.py:66 +#: apps/html_template_editor/models.py:67 +msgid "Images" +msgstr "Snímky" + +#: apps/html_template_editor/models.py:45 +msgid "Pages" +msgstr "Stránky" + +#: apps/html_template_editor/models.py:51 +msgid "Regions" +msgstr "Regiony" + +#: apps/html_template_editor/models.py:57 +msgid "Styles" +msgstr "Styly" + +#: apps/html_template_editor/models.py:78 +msgid "Image" +msgstr "Snímek" + +#: apps/html_template_editor/models.py:86 +msgid "Name" +msgstr "Název" + +#: apps/html_template_editor/models.py:92 +msgid "Edited width" +msgstr "Upravená šířka" + +#: apps/html_template_editor/models.py:97 +msgid "Edited crop" +msgstr "Upravené oříznutí" + +#: apps/html_template_editor/models.py:103 +msgid "Edited direction" +msgstr "Upravený směr" + +#: apps/html_template_editor/models.py:109 +msgid "Background image" +msgstr "Obrázek na pozadí" + +#: apps/html_template_editor/models.py:113 +msgid "Template url" +msgstr "Webová adresa (url) šablony" + +#: apps/html_template_editor/models.py:140 +msgid "Company url" +msgstr "Webová adresa (url) společnosti" + +#: apps/html_template_editor/models.py:141 +msgid "Company urls" +msgstr "Webové adresy (url) společnosti" + +#: apps/html_template_editor/models.py:144 +#: apps/html_template_editor/models.py:199 +#: apps/html_template_editor/models.py:249 +#: apps/html_template_editor/models.py:276 +msgid "Url" +msgstr "Webová adressa (url)" + +#: apps/html_template_editor/models.py:156 +msgid "Company phone number" +msgstr "Telefonní číslo společnosti" + +#: apps/html_template_editor/models.py:157 +msgid "Company phone numbers" +msgstr "Telefonní čísla společnosti" + +#: apps/html_template_editor/models.py:160 +#: apps/html_template_editor/models.py:284 +msgid "Phone number" +msgstr "Telefonní číslo" + +#: apps/html_template_editor/models.py:172 +#: apps/html_template_editor/models.py:176 +msgid "Company email" +msgstr "E-mail společnosti" + +#: apps/html_template_editor/models.py:173 +msgid "Company emails" +msgstr "E-maily společnosti" + +#: apps/html_template_editor/models.py:188 +#: apps/html_template_editor/models.py:189 +msgid "Company social media" +msgstr "Sociální média společnosti" + +#: apps/html_template_editor/models.py:192 +msgid "Icon name" +msgstr "Název ikony" + +#: apps/html_template_editor/models.py:217 +msgid "Template footer" +msgstr "Zápatí šablony" + +#: apps/html_template_editor/models.py:218 +msgid "Template footers" +msgstr "Zápatí šablony" + +#: apps/html_template_editor/models.py:221 +msgid "Footer name" +msgstr "Název zápatí" + +#: apps/html_template_editor/models.py:227 +msgid "Company name" +msgstr "Jméno společnosti" + +#: apps/html_template_editor/models.py:233 +msgid "Address" +msgstr "Adresa" + +#: apps/html_template_editor/models.py:239 +msgid "Phone" +msgstr "Telefon" + +#: apps/html_template_editor/models.py:244 +#: apps/html_template_editor/models.py:280 +msgid "Email" +msgstr "E-mail" + +#: apps/html_template_editor/models.py:254 +msgid "Social media" +msgstr "Sociální média" + +#: apps/html_template_editor/models.py:259 +#: apps/html_template_editor/models.py:362 +msgid "User" +msgstr "Uživatel" + +#: apps/html_template_editor/models.py:266 +#: apps/html_template_editor/models.py:369 +msgid "Show" +msgstr "Zobrazit" + +#: apps/html_template_editor/models.py:268 +msgid "Show footer" +msgstr "Zobrazit zápatí" + +#: apps/html_template_editor/models.py:289 +msgid "Social media link" +msgstr "Odkaz na sociální média" + +#: apps/html_template_editor/models.py:295 +msgid "Template header" +msgstr "Záhlaví šablony" + +#: apps/html_template_editor/models.py:296 +msgid "Template headers" +msgstr "Záhlaví šablony" + +#: apps/html_template_editor/models.py:299 +msgid "Header name" +msgstr "Název záhlaví" + +#: apps/html_template_editor/models.py:305 +msgid "Header text" +msgstr "Text záhlaví" + +#: apps/html_template_editor/models.py:311 +msgid "Header horizontal text position" +msgstr "Horizontální poloha záhlaví" + +#: apps/html_template_editor/models.py:312 +#: apps/html_template_editor/models.py:342 +#: apps/html_template_editor/models.py:353 +msgid "Left" +msgstr "Vlevo" + +#: apps/html_template_editor/models.py:313 +#: apps/html_template_editor/models.py:343 +msgid "Center" +msgstr "Vystředit" + +#: apps/html_template_editor/models.py:314 +#: apps/html_template_editor/models.py:344 +#: apps/html_template_editor/models.py:354 +msgid "Right" +msgstr "Vpravo" + +#: apps/html_template_editor/models.py:321 +msgid "Header vertical text position" +msgstr "Vertikální poloha záhlaví textu" + +#: apps/html_template_editor/models.py:322 +msgid "Top" +msgstr "Horní" + +#: apps/html_template_editor/models.py:323 +msgid "Middle" +msgstr "Střední" + +#: apps/html_template_editor/models.py:324 +msgid "Bottom" +msgstr "Spodní" + +#: apps/html_template_editor/models.py:331 +msgid "Logo image" +msgstr "Obrázek loga" + +#: apps/html_template_editor/models.py:337 +msgid "Max. image width 300 px, and max. image file size 5 MB" +msgstr "Max. šířka obrázku 300 px a max. velikost obrazového souboru je 5 MB" + +#: apps/html_template_editor/models.py:340 +msgid "Logo horizontal position" +msgstr "Horizontální poloha loga" + +#: apps/html_template_editor/models.py:351 +msgid "Logo position" +msgstr "Pozice loga" + +#: apps/html_template_editor/models.py:359 +msgid "Left or right column logo position" +msgstr "Umístění loga v levém nebo pravém sloupci" + +#: apps/html_template_editor/models.py:371 +msgid "Show header" +msgstr "Zobrazit záhlaví" + +#: apps/html_template_editor/validators.py:19 +#, python-format +msgid "Allowed image format is %(image_formats)s " +msgstr "Povolený formát obrázku je %(image_formats)s" + +#: apps/html_template_editor/validators.py:25 +#, python-format +msgid "Width is larger than what is allowed %(max_width)s px" +msgstr "Šířka je větší, než je povoleno %(max_width)s px" + +#: apps/html_template_editor/validators.py:31 +#, python-format +msgid "Image file size is larger than what is allowed %(max_file_size)s MB" +msgstr "Velikost obrazového souboru je větší, než je povoleno %(max_file_size)s MB" diff --git a/project/settings/base.py b/project/settings/base.py index 51e6dd7a7..53f9b7b4f 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -173,6 +173,7 @@ def normpath(*args): LOCALE_PATHS = [ normpath(PROJECT_ROOT, 'apps/aklub/locale'), + normpath(PROJECT_ROOT, 'apps/html_template_editor/locale'), ] USE_L10N = True From 53aaae42a6c505af4a6199233763384c6f2f9ca8 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Wed, 13 May 2020 07:17:29 +0200 Subject: [PATCH 167/189] feat: add masscommunication model html template widget cs language js translation files --- .../locale/cs_CZ/LC_MESSAGES/djangojs.mo | Bin 0 -> 1187 bytes .../locale/cs_CZ/LC_MESSAGES/djangojs.po | 54 ++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.mo create mode 100644 apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.po diff --git a/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.mo b/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000000000000000000000000000000000..c36a1523743cee198b03134ca1a72c370c312137 GIT binary patch literal 1187 zcmc&y%Wl&^6g5yDN`*j(4HAnhw#@{$Jk**})iiF2h?6KyDuLKc;@fl@d&U}1(xgka z2(e;;V3QAsSh1uV6A8Y69Up>JiR-qsw0p!z$4B=u_a58l=Z&d%7M&}E+k}^dX~IWB zcD@m=6224e5PlHO6Mhl0{666(Vfvh9Nn#+5h~E+~6ZQ$$DPFo@S#!iy;%wI|;%mgO ziEj~qAnp@?KH~h8Wzp4xbD5r$KG=Jhu`DWEmk8Oci&@eOgaLz59CCw}&j-N6Q0-s< zBH|r{9TAu<5D7fAZHNa2tJ<|1N9`bagr_6Gfk;f@Ofup_Xe*ge3DhQVwwni=E##pm z1Jp@dX-x7Um4PJYZA^g60HPjsx3D85i69LeLNo?-{-D}I$Ykm^Pw?Na%e0){R?})5 z+lG|_wn6fRk`C0|m1pjn2OC~}y;QB_>KH3+n4fe+z@GFv32P_^aVpv0S zna~eZ+JV((xeOK8_h8ZU-BNkkEf(wE`uY;J%1)2|DQh5)94Nb$wPq%Hv0PpbMM3G# zNe@Z&OTKsVezOHTx1yv$N$Z9&={7hpcgZ$FK7#fZ*9n@(&BiL5Kk3V4ZK7tLY^y-X zjsx>8VRBY8*1S)?n~ZoXRI-=(PDJS_l<62^)mI_PbVTKJJbB;cpjtW|?RI;xjmJK$ zDbv?vUaLYS?EjfHJR2W6vbRkxInxQzHeF866kQbx ph;SG6kc>#yB%!zi~e>0X?UlL#e7$F)}a#X!WvboAfW{s!ZIb$tK; literal 0 HcmV?d00001 diff --git a/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.po b/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..25ea51384 --- /dev/null +++ b/apps/aklub/locale/cs_CZ/LC_MESSAGES/djangojs.po @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-05-11 09:34+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: static/aklub/js/image_uploader.js:80 +msgid "Max allowed image width is 600 px." +msgstr "Maximální povolená šířka obrázku je 600 px." + +#: static/aklub/js/template_widget.min.js:2 static/template_widget.js:16 +#: static/template_widget.js:525 +msgid "Edit template" +msgstr "Upravit šablonu" + +#: static/aklub/js/template_widget.min.js:2 static/template_widget.js:124 +#: static/template_widget.js:166 +msgid "Template name" +msgstr "Název šablony" + +#: static/aklub/js/template_widget.min.js:2 +msgid "" +"Template name may consist of a-z, 0-9, underscores, not spaces and must " +"begin with a letter, and be lowercase." +msgstr "Název šablony může obsahovat a-z, 0-9, podtržítka, nikoli mezery " +"a musí začínat písmenem a musí být malá." + +#: static/aklub/js/template_widget.min.js:2 static/template_widget.js:228 +msgid "Template name exist." +msgstr "Název šablony existuje." + +#: static/aklub/js/template_widget.min.js:2 static/template_widget.js:262 +msgid "Ok" +msgstr "Potvrdit" + +#: static/template_widget.js:226 +msgid "" +"Template name may consist of a-z, 0-9, underscores, not spaces and must " +"begin with a letter,and be lowercase." +msgstr "Název šablony může obsahovat a-z, 0-9, podtržítka, nikoli mezery " +"a musí začínat písmenem a musí být malá." From b6988c22e2a3770e405fc8b040e275803ae57036 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Thu, 14 May 2020 06:06:00 +0200 Subject: [PATCH 168/189] chore: add merged migration file --- apps/aklub/migrations/0079_merge_20200514_0604.py | 14 ++++++++++++++ apps/aklub/migrations/0080_merge_20200701_1411.py | 14 ++++++++++++++ apps/aklub/migrations/0081_merge_20200706_0437.py | 14 ++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 apps/aklub/migrations/0079_merge_20200514_0604.py create mode 100644 apps/aklub/migrations/0080_merge_20200701_1411.py create mode 100644 apps/aklub/migrations/0081_merge_20200706_0437.py diff --git a/apps/aklub/migrations/0079_merge_20200514_0604.py b/apps/aklub/migrations/0079_merge_20200514_0604.py new file mode 100644 index 000000000..5d528aa40 --- /dev/null +++ b/apps/aklub/migrations/0079_merge_20200514_0604.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-05-14 04:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0078_remove_taxconfirmation_administrative_unit'), + ('aklub', '0076_merge_20200511_0622'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0080_merge_20200701_1411.py b/apps/aklub/migrations/0080_merge_20200701_1411.py new file mode 100644 index 000000000..960f5fe30 --- /dev/null +++ b/apps/aklub/migrations/0080_merge_20200701_1411.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-07-01 12:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0079_merge_20200514_0604'), + ('aklub', '0079_accountstatements_pair_log'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0081_merge_20200706_0437.py b/apps/aklub/migrations/0081_merge_20200706_0437.py new file mode 100644 index 000000000..98c47ae98 --- /dev/null +++ b/apps/aklub/migrations/0081_merge_20200706_0437.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-07-06 02:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0080_merge_20200701_1411'), + ('aklub', '0080_auto_20200625_1216'), + ] + + operations = [ + ] From b369911e2547cac6ecc762a05a4bd6af975d1fe8 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 13 Jul 2020 08:21:10 +0200 Subject: [PATCH 169/189] fix: manual setting of 'tablib' py module hashes (wrong hashes setting during rebase) --- Pipfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 49ff453ac..3e0f85adf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1432,8 +1432,8 @@ "yaml" ], "hashes": [ - "sha256:4d1909aa3ff1c85ba97ad16176c0aeec33c8e894dc7ea6f10f2dd44701e99ba7", - "sha256:80f6c3453431cedf1125f23d16b3d96b92b426495714ebf0b4dede1fa75b447d" + "sha256:00654241e5beee437ba544e4fa4abef70ccec3668503aa95406c1250bb660770", + "sha256:6336e7aa3f0e5894b47270a3dc639cc2e78eb823c7ccb8c4512fb408cdc18d08" ], "index": "pypi", "version": "==0.14.0" From bb67de4e892486e9a6933cc3bfa3f06549e5d181 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 17 Jul 2020 06:07:08 +0200 Subject: [PATCH 170/189] style: fix flake8 error --- apps/aklub/admin.py | 1 + apps/aklub/management/commands/build_contenttools.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/aklub/admin.py b/apps/aklub/admin.py index 9418a279c..d68ea179c 100644 --- a/apps/aklub/admin.py +++ b/apps/aklub/admin.py @@ -103,6 +103,7 @@ ) + def admin_links(args_generator): return format_html_join( mark_safe('
'), diff --git a/apps/aklub/management/commands/build_contenttools.py b/apps/aklub/management/commands/build_contenttools.py index 6b75f4e41..ebdd589b3 100644 --- a/apps/aklub/management/commands/build_contenttools.py +++ b/apps/aklub/management/commands/build_contenttools.py @@ -270,8 +270,10 @@ def __copy_localizations(self): target_write_trans_f.seek(0) target_write_trans_f.truncate(0) - json.dump(target_trans, target_write_trans_f, indent=4, - ensure_ascii=False, sort_keys=True) + json.dump( + target_trans, target_write_trans_f, + indent=4, ensure_ascii=False, sort_keys=True, + ) def __install(self, command, program, package='install'): """Install/build package""" From 70aee815302d9f4bcb0602bc99017fb811156cec Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 17 Jul 2020 06:18:47 +0200 Subject: [PATCH 171/189] fix: travis config file --- .travis.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index b99444eb8..85379c4c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,17 +21,10 @@ python: before_install: - sudo apt-get install -y python-gdal install: -<<<<<<< HEAD - pip install pipenv - pipenv install --dev --ignore-pipfile --python=python$TRAVIS_PYTHON_VERSION - pipenv run python --version - - pipenv run pip install flake8-strict # not installable with pipenv, move ther when repaired -======= - - - - pip install pipenv - - pipenv install --dev --ignore-pipfile - pipenv run pip install flake8-strict # not installable with pipenv, move ther when repaired ->>>>>>> set official Django 2.2.x release into the Travis config file - pipenv run pip install -q $DJANGO_VERSION before_script: - npm install -g bower sass @@ -50,6 +43,6 @@ after_script: addons: postgresql: "9.6" matrix: - allow_failures: - - env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' - - env: DJANGO_VERSION="Django>=3.0,<3.1" + allow_failures: + - env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' + - env: DJANGO_VERSION="Django>=3.0,<3.1" From 865f16b443ba59a54b270d7be66d844de34fee78 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 17 Jul 2020 08:46:46 +0200 Subject: [PATCH 172/189] style: fix quoutes --- apps/aklub/management/commands/build_contenttools.py | 4 ++-- apps/html_template_editor/models.py | 2 +- apps/html_template_editor/templatetags/filters.py | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/aklub/management/commands/build_contenttools.py b/apps/aklub/management/commands/build_contenttools.py index ebdd589b3..2b30f8f42 100644 --- a/apps/aklub/management/commands/build_contenttools.py +++ b/apps/aklub/management/commands/build_contenttools.py @@ -280,8 +280,8 @@ def __install(self, command, program, package='install'): def raise_command_error(stream): raise CommandError( - f'Error executing command {program} ' - f'{package}\n{stream}' + f"Error executing command {program} " + f"{package}\n{stream}" ) # Install package diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index cb9037411..107aaf0de 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -208,7 +208,7 @@ def __init__(self, *args, **kwargs): )() def __str__(self): - return f'{self.icon_name} : {self.url}' + return f"{self.icon_name} : {self.url}" class TemplateFooter(models.Model): diff --git a/apps/html_template_editor/templatetags/filters.py b/apps/html_template_editor/templatetags/filters.py index 6ddfa361d..511b867e4 100644 --- a/apps/html_template_editor/templatetags/filters.py +++ b/apps/html_template_editor/templatetags/filters.py @@ -1,4 +1,3 @@ - from django import template from django.utils.safestring import mark_safe @@ -10,7 +9,10 @@ def get_link_element(value, arg): if arg == 'list': links = [] for i in value: - links.append(mark_safe(f'{i}')) + links.append( + mark_safe(f"{i}"), + ) return links elif arg == 'str': - return mark_safe(f'{value}') + return mark_safe( + f"{value}") From a688751e98b4f1c19503b018e53ad1e6862da671 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 17 Jul 2020 08:55:46 +0200 Subject: [PATCH 173/189] fix: replace social media choices model field lazy loading with normal loading --- .../migrations/0010_auto_20200717_0854.py | 18 +++++++++++++++++ apps/html_template_editor/models.py | 20 ++++++++++--------- 2 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 apps/html_template_editor/migrations/0010_auto_20200717_0854.py diff --git a/apps/html_template_editor/migrations/0010_auto_20200717_0854.py b/apps/html_template_editor/migrations/0010_auto_20200717_0854.py new file mode 100644 index 000000000..5b0442c98 --- /dev/null +++ b/apps/html_template_editor/migrations/0010_auto_20200717_0854.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.14 on 2020-07-17 06:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0009_auto_20200511_0823'), + ] + + operations = [ + migrations.AlterField( + model_name='companysocialmedia', + name='icon_name', + field=models.CharField(blank=True, choices=[('facebook', 'facebook'), ('google', 'google'), ('instagram', 'instagram'), ('twitter', 'twitter'), ('vimeo', 'vimeo'), ('youtube', 'youtube')], max_length=20, null=True, verbose_name='Icon name'), + ), + ] diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index 107aaf0de..6e2d66bde 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -1,20 +1,28 @@ # -*- coding: utf-8 -*- import uuid +from enum import Enum from django.conf import settings from django.contrib.postgres.fields import JSONField from django.db import models -from django.utils.functional import lazy from django.utils.html import mark_safe from django.utils.translation import ugettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField -from .utils import get_social_media_icons_names from .validators import validate_logo_image +class SocialMedia(Enum): + facebook = 'facebook' + google = 'google' + instagram = 'instagram' + twitter = 'twitter' + vimeo = 'vimeo' + youtube = 'youtube' + + class TemplateContent(models.Model): class Meta: @@ -190,7 +198,7 @@ class Meta: icon_name = models.CharField( verbose_name=_("Icon name"), - choices=(), + choices=((tag.name, tag.value) for tag in SocialMedia), max_length=20, null=True, blank=True, @@ -201,12 +209,6 @@ class Meta: blank=True, ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._meta.get_field('icon_name').choices = lazy( - get_social_media_icons_names, - )() - def __str__(self): return f"{self.icon_name} : {self.url}" From 8528f62cb1ac24d64a351a60b78ed742471338d0 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 17 Jul 2020 09:44:21 +0200 Subject: [PATCH 174/189] fix: return model str value --- apps/html_template_editor/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index 6e2d66bde..99acf9638 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -155,7 +155,7 @@ class Meta: ) def __str__(self): - return self.url + return self.url or '' class CompanyPhone(models.Model): @@ -187,7 +187,7 @@ class Meta: ) def __str__(self): - return self.email + return self.email or '' class CompanySocialMedia(models.Model): @@ -374,4 +374,4 @@ class Meta: ) def __str__(self): - return self.name + return self.name or '' From 637fc5c0932d9539cf0a2ddde8611d23ab4da9a8 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 17 Jul 2020 10:02:25 +0200 Subject: [PATCH 175/189] fix: deployment error (compile messages) --- bin/post_compile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/post_compile b/bin/post_compile index 7c55378c1..b6f680e3f 100755 --- a/bin/post_compile +++ b/bin/post_compile @@ -5,5 +5,4 @@ pip3 install jsonfield2 npm install bower && python manage.py bower install python manage.py migrate python manage.py collectstatic --noinput -python3 manage.py compilemessages - +cd apps/aklub/ && pipenv run env DJANGO_SETTINGS_MODULE="" django-admin compilemessages From 4e9eabbefdbbd5d871060cb9699edd3e145bb792 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sat, 18 Jul 2020 08:36:55 +0200 Subject: [PATCH 176/189] fix: remove obsolete init wysiwyg setup --- .../aklub/masscommunication/change_form.html | 157 +++++++++--------- 1 file changed, 78 insertions(+), 79 deletions(-) diff --git a/apps/aklub/templates/admin/aklub/masscommunication/change_form.html b/apps/aklub/templates/admin/aklub/masscommunication/change_form.html index 386f11a46..bcdcd7a0c 100644 --- a/apps/aklub/templates/admin/aklub/masscommunication/change_form.html +++ b/apps/aklub/templates/admin/aklub/masscommunication/change_form.html @@ -14,10 +14,9 @@ {% endblock %} {% block extrahead %} - - {{ block.super }} - - {% wysiwyg_setup %} + + {{ block.super }} + {% endblock %} @@ -26,83 +25,83 @@ {% load automat_fonts %} {% get_automat_fonts %} - {{ block.super }} - - - - {% include "email_templates/automat_fonts.html" %} - + {{ block.super }} + + + + {% include "email_templates/automat_fonts.html" %} + {% endblock %} {% block content %} - - {{ block.super }} - - {# Replace XXXXXXXX with field name: #} - {% comment %} - {% wysiwyg_editor "id_template" %} - {% wysiwyg_editor "id_template_en" %} - {% endcomment %} - - {% comment %} - {# ckeditor custom config example #} - {% wysiwyg_editor "id_XXXXXXXX" "id_XXXXXXXX_editor" "{ toolbar: 'compact_toolbar' }" %} - {% endcomment %} - - {# ... add more here... #} - - {# Edit template field modal dialog #} -
-
- -
-
- {# Edit template_en field modal dialog #} -
-
- -
-
+ + {{ block.super }} + + {# Replace XXXXXXXX with field name: #} + {% comment %} + {% wysiwyg_editor "id_template" %} + {% wysiwyg_editor "id_template_en" %} + {% endcomment %} + + {% comment %} + {# ckeditor custom config example #} + {% wysiwyg_editor "id_XXXXXXXX" "id_XXXXXXXX_editor" "{ toolbar: 'compact_toolbar' }" %} + {% endcomment %} + + {# ... add more here... #} + + {# Edit template field modal dialog #} +
+
+ +
+
+ {# Edit template_en field modal dialog #} +
+
+ +
+
{% endblock %} From 14e9cf190f4d6e4d514f52d4056a7b89ddb9fbca Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sat, 18 Jul 2020 08:45:32 +0200 Subject: [PATCH 177/189] fix: set 'X_FRAME_OPTIONS' to make email templates work on Heroku --- project/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/settings/base.py b/project/settings/base.py index 53f9b7b4f..8ca301d70 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -450,7 +450,7 @@ def get_helpdesk_query_class(): SECURE_HSTS_SECONDS = 60 SECURE_HSTS_PRELOAD = True SESSION_COOKIE_SECURE = True -X_FRAME_OPTIONS = 'DENY' +X_FRAME_OPTIONS = 'SAMEORIGIN' BROKER_URL = os.environ.get('REDIS_URL', 'redis://redis') SMMAPDFS_CELERY = True From ab317d9b4a2f821e4adf35efe02b711202e11200 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sat, 18 Jul 2020 11:08:45 +0200 Subject: [PATCH 178/189] fix: update 'Pipfile.lock' --- Pipfile.lock | 145 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 49 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 3e0f85adf..cc2bae8ab 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -91,10 +91,10 @@ }, "celery": { "hashes": [ - "sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f", - "sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2" + "sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916", + "sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da" ], - "version": "==4.4.0" + "version": "==4.4.6" }, "certifi": { "hashes": [ @@ -166,10 +166,10 @@ }, "cssselect2": { "hashes": [ - "sha256:07e9c3b1b52d81dd08b177532bbd6b9ced650d87abfd641f4e4ec7de34b98807", - "sha256:70485a680cd72b023f0ce5ae4dcd392e2b10f7280e20afdb1735334bd6af7e6a" + "sha256:5c2716f06b5de93f701d5755a9666f2ee22cbcd8b4da8adddfc30095ffea3abc", + "sha256:97d7d4234f846f9996d838964d38e13b45541c18143bc55cf00e4bc1281ace76" ], - "version": "==0.2.2" + "version": "==0.3.0" }, "defusedxml": { "hashes": [ @@ -317,7 +317,6 @@ "sha256:fdf1255eecfbeb770c6521fe3e69989dfc6373cd5a7f0fe62038d37f80f47e48", "sha256:fe0b2a1b31d4a6234fea4b31986ddfd4644a48fab216ce1843f3ed0ddd2e9097" ], - "index": "pypi", "version": "==2.0.0" }, "django-celery-email": { @@ -392,20 +391,20 @@ "index": "pypi", "version": "==0.13.0" }, - "django-flexible-filter-conditions": { + "django-filter": { "hashes": [ - "sha256:662a0705cd6ea47778cab702de33bf40d6b1932f1c645a62aacf8a7c067d9f3d" + "sha256:11e63dd759835d9ba7a763926ffb2662cf8a6dcb4c7971a95064de34dbc7e5af", + "sha256:616848eab6fc50193a1b3730140c49b60c57a3eda1f7fc57fa8505ac156c6c75" ], "index": "pypi", - "version": "==0.5.0" + "version": "==2.3.0" }, - "django-filter": { + "django-flexible-filter-conditions": { "hashes": [ - "sha256:558c727bce3ffa89c4a7a0b13bc8976745d63e5fd576b3a9a851650ef11c401b", - "sha256:c3deb57f0dd7ff94d7dce52a047516822013e2b441bed472b722a317658cfd14" + "sha256:662a0705cd6ea47778cab702de33bf40d6b1932f1c645a62aacf8a7c067d9f3d" ], "index": "pypi", - "version": "==2.2.0" + "version": "==0.5.0" }, "django-grapesjs": { "hashes": [ @@ -845,6 +844,13 @@ ], "version": "==0.10.0" }, + "joblib": { + "hashes": [ + "sha256:8f52bf24c64b608bf0b2563e0e47d6fcf516abc8cfafe10cfd98ad66d94f92d6", + "sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49" + ], + "version": "==0.16.0" + }, "jsonfield2": { "hashes": [ "sha256:39610a8345b6fc71b45825d177b678b3d7fa9140c60cc488cf82bd1a35367a2b" @@ -860,10 +866,19 @@ }, "livereload": { "hashes": [ - "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", - "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66" + "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be" ], - "version": "==2.6.1" + "version": "==2.6.2" + }, + "lunr": { + "extras": [ + "languages" + ], + "hashes": [ + "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca", + "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e" + ], + "version": "==0.5.8" }, "lxml": { "hashes": [ @@ -899,7 +914,6 @@ "sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd", "sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee" ], - "index": "pypi", "version": "==4.5.2" }, "markdown": { @@ -962,11 +976,11 @@ }, "mkdocs": { "hashes": [ - "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", - "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a" + "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9", + "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39" ], "index": "pypi", - "version": "==1.0.4" + "version": "==1.1.2" }, "newrelic": { "hashes": [ @@ -987,6 +1001,12 @@ "index": "pypi", "version": "==5.20.1.150" }, + "nltk": { + "hashes": [ + "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35" + ], + "version": "==3.5" + }, "oauthlib": { "hashes": [ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", @@ -1022,11 +1042,11 @@ }, "phonenumbers": { "hashes": [ - "sha256:685e81ac131fea8dbd71b7769d2ac480c9fe9d5a45838e8546f5de9910670e8e", - "sha256:9ca4332425ae4f4d1e7925bbf98ef9446755b59a903ae6a63cf4fb19adcd168b" + "sha256:d332078fe71c6153b5a263ac87283618b2afe514a248a14f06a0d39ce1f5ce0b", + "sha256:e49b8e21c557f0dafee966ddd55fb2bd3d6db155451999b75fb1b012e8d2016c" ], "index": "pypi", - "version": "==8.11.4" + "version": "==8.12.6" }, "pillow": { "hashes": [ @@ -1102,6 +1122,13 @@ "index": "pypi", "version": "==2.8.6" }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "version": "==2.20" + }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", @@ -1109,12 +1136,6 @@ ], "version": "==2.4.7" }, - "pycparser": { - "hashes": [ - "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" - ], - "version": "==2.19" - }, "pypdf2": { "hashes": [ "sha256:e28f902f2f0a1603ea95ebe21dff311ef09be3d0f0ef29a3e44a932729564385" @@ -1189,7 +1210,6 @@ "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" ], - "index": "pypi", "version": "==2020.1" }, "pyyaml": { @@ -1224,6 +1244,32 @@ "index": "pypi", "version": "==3.5.3" }, + "regex": { + "hashes": [ + "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204", + "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162", + "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f", + "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb", + "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6", + "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7", + "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88", + "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99", + "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644", + "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a", + "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840", + "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067", + "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd", + "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4", + "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e", + "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89", + "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e", + "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc", + "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf", + "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341", + "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7" + ], + "version": "==2020.7.14" + }, "reportlab": { "hashes": [ "sha256:0145233d3596fa5828972eb474b5a9f3fd5dea45d6f196fe006a7a7a461fcd03", @@ -1382,7 +1428,6 @@ "sha256:fed0f22bf1313ff79c7fc318f7199d6c2f96d4de3234b2f12a1eab350e597c06", "sha256:ffd4e4877a78c84d693e491b223385e0271278f5f4e1476a4962dca6824ecfeb" ], - "index": "pypi", "version": "==3.17.2" }, "single-beat": { @@ -1398,7 +1443,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "index": "pypi", "version": "==1.15.0" }, "sorl-thumbnail": { @@ -1432,11 +1476,10 @@ "yaml" ], "hashes": [ - "sha256:00654241e5beee437ba544e4fa4abef70ccec3668503aa95406c1250bb660770", - "sha256:6336e7aa3f0e5894b47270a3dc639cc2e78eb823c7ccb8c4512fb408cdc18d08" + "sha256:8cc2fa10bc37219ac5e76850eb7fbd50de313c7a1a7895c44af2a8dd206b7be7", + "sha256:c5a77c96ad306d5b380def1309d521ad5e98b4f83726f84857ad87e142d13279" ], - "index": "pypi", - "version": "==0.14.0" + "version": "==2.0.0" }, "text-unidecode": { "hashes": [ @@ -1445,12 +1488,6 @@ ], "version": "==1.3" }, - "tornadis": { - "hashes": [ - "sha256:690a38728ae1867253c69c8c824beda84d6b8752536d8c5619c8ea5f86ab3239" - ], - "version": "==0.8.1" - }, "tinycss2": { "hashes": [ "sha256:6427d0e3faa0a5e0e8c9f6437e2de26148a7a197a8b0992789f23d9a802788cf", @@ -1458,6 +1495,12 @@ ], "version": "==1.0.2" }, + "tornadis": { + "hashes": [ + "sha256:690a38728ae1867253c69c8c824beda84d6b8752536d8c5619c8ea5f86ab3239" + ], + "version": "==0.8.1" + }, "tornado": { "hashes": [ "sha256:0662d28b1ca9f67108c7e3b77afabfb9c7e87bde174fbda78186ecedc2499a9d", @@ -1470,6 +1513,13 @@ ], "version": "==5.1.1" }, + "tqdm": { + "hashes": [ + "sha256:6baa75a88582b1db6d34ce4690da5501d2a1cb65c34664840a456b2c9f794d29", + "sha256:fcb7cb5b729b60a27f300b15c1ffd4744f080fb483b88f31dc8654b082cc8ea5" + ], + "version": "==4.48.0" + }, "unicodecsv": { "hashes": [ "sha256:018c08037d48649a0412063ff4eda26eaa81eff1546dbffa51fa5293276ff7fc" @@ -1534,7 +1584,6 @@ "sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2", "sha256:e551fb498759fa3a5384a94ccd4c3c02eb7c00ea424426e212ac0c57be9dfbde" ], - "index": "pypi", "version": "==1.2.0" }, "xlwt": { @@ -1583,10 +1632,10 @@ }, "celery": { "hashes": [ - "sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f", - "sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2" + "sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916", + "sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da" ], - "version": "==4.4.0" + "version": "==4.4.6" }, "certifi": { "hashes": [ @@ -1930,7 +1979,6 @@ "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" ], - "index": "pypi", "version": "==2020.1" }, "requests": { @@ -1945,7 +1993,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "index": "pypi", "version": "==1.15.0" }, "sqlparse": { From da6f1c2ac057f0b97966765b0afa233209d0d9a6 Mon Sep 17 00:00:00 2001 From: Timothy Hobbs Date: Thu, 16 Jul 2020 20:45:14 +0200 Subject: [PATCH 179/189] fix: Heroku collectstatic phase --- Procfile | 2 +- bin/post_compile | 1 + project/settings/heroku.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Procfile b/Procfile index 7893c0384..7c473a9c1 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ -release: python manage.py migrate +release: ./release.sh web: ./launch_on_heroku celery: ./launch_celery diff --git a/bin/post_compile b/bin/post_compile index b6f680e3f..c10e5228c 100755 --- a/bin/post_compile +++ b/bin/post_compile @@ -4,5 +4,6 @@ pip3 uninstall jsonfield2 -y pip3 install jsonfield2 npm install bower && python manage.py bower install python manage.py migrate +python manage.py build_contenttools python manage.py collectstatic --noinput cd apps/aklub/ && pipenv run env DJANGO_SETTINGS_MODULE="" django-admin compilemessages diff --git a/project/settings/heroku.py b/project/settings/heroku.py index 7420a2240..bc540bace 100644 --- a/project/settings/heroku.py +++ b/project/settings/heroku.py @@ -48,3 +48,5 @@ ) django_heroku.settings(locals()) + +STATICFILES_STORAGE = 'project.settings.custom_storage.CustomStaticFilesStorage' From c9af59c13c241f3647e434a5de5826dadd38e373 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sat, 18 Jul 2020 15:36:28 +0200 Subject: [PATCH 180/189] fix: add install 'sass' css preprocessor (build_contenttools cmd dependency), if deploy on Heroku --- bin/post_compile | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/post_compile b/bin/post_compile index c10e5228c..fa30ca1d2 100755 --- a/bin/post_compile +++ b/bin/post_compile @@ -3,6 +3,7 @@ pip3 uninstall django-jsonfield -y pip3 uninstall jsonfield2 -y pip3 install jsonfield2 npm install bower && python manage.py bower install +npm install -g bower sass && python manage.py bower install python manage.py migrate python manage.py build_contenttools python manage.py collectstatic --noinput From c7164e97a64fbc111e1f6d6086f6ffb909df8b36 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sun, 11 Oct 2020 17:25:47 +0200 Subject: [PATCH 181/189] fix: better way how to set template (loaded from file or db) background image table container width, height --- .../automat_green_newsletter.html | 22 +++++++------ .../automat_purple_newsletter.html | 22 +++++++------ .../automat_red_newsletter.html | 22 +++++++------ .../email_templates/new_empty_template.html | 16 +++++---- apps/aklub/utils.py | 33 ++++++++++++++++--- 5 files changed, 74 insertions(+), 41 deletions(-) diff --git a/apps/aklub/templates/email_templates/automat_green_newsletter.html b/apps/aklub/templates/email_templates/automat_green_newsletter.html index bc482e5ed..a0c856c68 100644 --- a/apps/aklub/templates/email_templates/automat_green_newsletter.html +++ b/apps/aklub/templates/email_templates/automat_green_newsletter.html @@ -7,12 +7,13 @@ cellpadding="0" border="0" bgcolor="ffffff" - style="padding:0" + style="padding: 0" align="center" >
- - +
+
+ - - "),g=$(""),f=$(""),f.css({border:"none",padding:0,margin:0}),f.attr({align:a,width:b,heigth:c}),d.append(e.append(g.append(f)))},b.prototype._replaceImgSrc=function(){var a;return a=this._$editTemplatePageContainer.find("img"),a.each(function(a){return function(b,c){var d,e,f,g,h;return d=$(c).closest(a.getClassFormat(a._videoClass)),e=d.length>0?d:$(c),0===d.length&&(h=$(c).attr("src"),g=new RegExp(window.origin),h.match(g)||(f=""+window.origin+$(c).attr("src"),$(c).attr("src",f))),a.convertImageCssPositionToTable(e)}}(this))},b.prototype.convertImageCssPositionToTable=function(a){var b,c,d,e,f,g,h;a=a.find("img").length>0?a.find("img"):a,f=parseInt(a.css("margin-left").split(".")[0].replace("px","")),g=parseInt(a.css("margin-right").split(".")[0].replace("px","")),e=a.css("float"),a.css("display",""),f===g&&"none"===e?c=this.getElementPosition("center","",""):f>g?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):g>f?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):"left"!==e&&"right"!==e||(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)),h=this.getClassFormat(this._videoClass),d=a.closest(h),c.find("td").append(d.length>0?d.clone():a.clone()),"left"===e?c.find("tr").append(b):c.find("tr").prepend(b),d.length>0?d.after(c):a.after(c),d.length>0?d.remove():a.remove()},b.prototype.convertImgFloatCssPositionToTable=function(a,b,c){var d,e,f,g,h,i,j;return i=15,j="left"===c?"0 0 0 "+i+"px":"0 "+i+"px 0 0",e=$(""),e.css({border:"none",padding:j,margin:"0"}),f=a.closest(this.getClassFormat(this._videoClass)),d=f.length>0?f.next().clone():a.next().clone(),h=parseFloat(b.find("#content_table").width()),g=h-parseFloat(a.attr("width"))-i,d.css("width",g),e.attr("width",g),e.append(d),f.length>0?f.next().remove():a.next().remove(),e},b.prototype._convertTextCssPositionToTable=function(){var a;return a=this._textContainerTags.join(", "),this._$editTemplatePageContainer.find(a).each(function(a){return function(b,c){var d,e;switch(e=$(c).attr("class")?$(c).attr("class").split(" "):[]){case e.indexOf("text-left")>-1:d=a.getElementPosition("left","","");break;case e.indexOf("text-right")>-1:d=a.getElementPosition("right","","");break;case e.indexOf("text-center")>-1:d=a.getElementPosition("center","","");break;default:d=a.getElementPosition("center","","")}return a.adjustListCssProperties($(c)),d.find("td").append($(c).clone()),$(c).after(d),$(c).remove()}}(this))},b.prototype.adjustListCssProperties=function(a){if(a.is("ul")||a.is("ol"))return a.css({margin:"10px",padding:"15px"})},b.prototype._fixMargin=function(){return $(this._htmlDoc).find("article").css("margin","")},b.prototype._getNumberValue=function(a,b){return null==b&&(b=-2),parseInt(a.slice(0,a.length-b))},b.prototype._getPaddingValue=function(a){var b,c;return null==a&&(a=!0),c=!0===a?"padding--left":"padding--top",b=$(this._htmlDoc).find(".article__content p").first().css(c),this._getNumberValue(b)},b.prototype._fixContentWidth=function(){var a,b;return b=this._getPaddingValue(),a=this._textContainerTags.join(", "),$(this._htmlDoc).find("article").find(a).each(function(a){return function(c,d){var e;return e=a._getNumberValue($(d).css("width")),$(d).css({width:e-2*b})}}(this))},b.prototype._eraseVerticalSpaceContent=function(){var a;return a=this.getClassFormat(this._verticalSpaceClassName),this._$editTemplatePageContainer.find(a).each(function(a,b){return $(b).find("center").remove()})},b}(b)}).call(this); \ No newline at end of file +(function(){var a,b,c,d,e,f,g={}.hasOwnProperty,h=function(a,b){function c(){this.constructor=a}for(var d in b)g.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},i=function(a,b){return function(){return a.apply(b,arguments)}};b=function(){function a(){}return a.prototype.getIdFormat=function(a){return"#"+a},a.prototype.getClassFormat=function(a){return"."+a},a.prototype.getNameFormat=function(a,b){return a+"[name='"+b+"']"},a}(),$(document).ready(function(){var a,b,c,e;return a=500,c=750,e=3e3,b=!0,new d("edit_template_dialog","edit_template",a,"id_template_name","id_template_type","id_hidden_template","field-template","field-template_textarea","id_template_textarea","template_page","id_template",c,b,e,"edit_template_btn"),new d("edit_template_en_dialog","edit_template_en",a,"id_template_en_name","id_template_en_type","id_hidden_template_en","field-template_en","field-template_en_textarea","id_template_en_textarea","template_en_page","id_template_en",c,b,e,"edit_template_en_btn")}),e=function(b){function c(a,b,c,d,e,f,g,h,i){this._title=a,this._editTemplateModalDialogId=c,this._backdrop=b,this._$templateNameField=d,this._destroyDelay=e,this._$mountElement=f,this._popoverModalDialogEditTemplateLinkElementId=g,this._$editTemplateModalDialogPageContainer=h,this._$hiddenTemplateField=i,this._$templateDivField=f}return h(c,b),c.editTemplateLinkText=gettext("Edit template"),c.prototype.show=function(b){return b.click(function(b){return function(c){var d,e,f,g;return c.preventDefault(),f=b._$templateNameField.find("option:selected"),e=f.text(),g=f.val().split(":"),d=new a(b._editTemplateModalDialogId,b._popoverModalDialogEditTemplateLinkElementId,b._$editTemplateModalDialogPageContainer,b._$hiddenTemplateField,b._$templateDivField),d.mount(e,"openViaPopoverDialog",g),b.unmount()}}(this)),this.delay(this._destroyDelay,function(a){return function(){return a.unmount()}}(this))},c.prototype.mount=function(a,b){var c;return c=this._getOpt(),c.offsetTop=a,c.offsetLeft=b,WebuiPopovers.show(this._$mountElement,c)},c.prototype.unmount=function(){return this._$mountElement.webuiPopover("hide"),this._$mountElement.webuiPopover("destroy")},c.prototype.delay=function(a,b){return setTimeout(b,a)},c.getContent=function(a,b){var c,d;return c=$(""),c.attr({id:this.dialogId,href:""}),d={id:a,"data-type":"modal","data-target":b,"data-fullscreen":"true","data-overlayClick":"true"},c.attr(d),c.html(""+this.editTemplateLinkText+""),c},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b){return a.show(b)}}(this),a=c.getContent(this._popoverModalDialogEditTemplateLinkElementId,this.getIdFormat(this._editTemplateModalDialogId)),{placement:"top-right",trigger:"manual,",title:this._title,content:a,offsetTop:0,offsetLeft:0,animation:"fade",backdrop:this._backdrop,onShow:b}},c}(b),c=function(b){function c(a,b,c,d,e,f,g,h){this._getBtns=i(this._getBtns,this),this._confirmTemplateName=i(this._confirmTemplateName,this),this._dialogId="get_template_name",this._title=gettext("Template name"),this._showDelay=a,this._confirmBtnId="confirm_btn",this._$templateDivField=b,this._templateNameInputId="template_name",this._errorListCSSClass="errorlist",this._templateNameFieldId=c,this._newEmptyTemplateName=null,this._editTemplateModalDialogId=d,this._popoverModalDialogEditTemplateLinkElementId=e,this._templateName=f,this._$editTemplateModalDialogPageContainer=g,this._$hiddenTemplateField=h}return h(c,b),c.prototype.unmount=function(){return $(this.getIdFormat(this._dialogId)).dialog("destroy")},c.prototype.mount=function(){return this._getContainer().dialog(this._getOpt())},c.prototype._getContainer=function(){var a,b,c,d,e,f;return e=$("
"),e.attr({id:this._dialogId}),f=$(""),f.attr({id:"template_name_form"}),a=$("
"),d=$("
    "),d.attr({class:this._errorListCSSClass}),c=$(""),c.attr({for:this._templateNameInputId}),c.text(gettext("Template name")),b=$(""),b.attr({id:this._templateNameInputId,name:this._templateNameInputId}),a.append(c),a.append(b),a.append(d),e.append(f.append(a)),e},c.prototype.getNeEmptyTemplateName=function(){return this._newEmptyTemplateName},c.prototype._triggerConfirmBtn=function(){var a;return a=function(a){return function(b){if(b.keyCode===$.ui.keyCode.ENTER)return $(a.getIdFormat(a._confirmBtnId)).click()}}(this),$(this.getIdFormat(this._dialogId)).keypress(a)},c.prototype._checkRegexp=function(a,b,c){var d;return!!b.test(a.val())||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
  • "+c+"
  • "),!1)},c.prototype._checkTemplateName=function(a,b,c){var d,e,f;return e=[],f=this.getIdFormat(this._templateNameFieldId)+" > option",$(f).each(function(){return e.push($(this).text())}),!(e.indexOf(b)>-1)||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
  • "+c+"
  • "),!1)},c.prototype._validateTemplateName=function(){var a,b,c,d;return d=!0,a=$(this.getIdFormat(this._templateNameInputId)),$(this.getClassFormat(this._errorListCSSClass)).html(""),c=gettext("Template name may consist of a-z, 0-9, underscores, not spaces and must begin with a letter, and be lowercase."),d=d&&this._checkRegexp(a,/^[a-z]([0-9a-z_])+$/,c),c=gettext("Template name exist."),b=$(this.getIdFormat(this._templateNameInputId)),d=d&&this._checkTemplateName(b,a.val(),c)},c.prototype._confirmTemplateName=function(){var b,c;if(this._validateTemplateName())return this._newEmptyTemplateName=$(this.getIdFormat(this._templateNameInputId)).val(),this.unmount(),this._setNewEmptyTemplateName(),c=[this._newEmptyTemplateName],b=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),b.mount(this._templateName,"openViaSelectBox",c)},c.prototype._setNewEmptyTemplateName=function(){if(null!=this._newEmptyTemplateName)return sessionStorage.setItem("newEmptyTemplateName",this._newEmptyTemplateName)},c.prototype._getBtns=function(){var a,b;return a={},b=function(a){return function(){return a._confirmTemplateName()}}(this),a.ok={text:gettext("Ok"),id:this._confirmBtnId,click:b},a},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b,c){return a._triggerConfirmBtn()}}(this),a=function(a){return function(b,c){return a.unmount(),a._$templateDivField.html("")}}(this),{modal:!0,title:this._title,show:{effect:"fade",delay:this._showDelay},resizable:!1,open:b,beforeClose:a,buttons:this._getBtns()}},c}(b),a=function(a){function b(a,b,c,d,e){this._closeEditTemplateModalDialog=i(this._closeEditTemplateModalDialog,this),this._dialogId=a,this._sessionStorageKey="dbTemplateName",this._popoverEditTemplateLinkElementId=b,this._closeBtnClass="mdl-close",this._$editTemplateModalDialogPageContainer=c,this._$hiddenTemplateField=d,this._$templateDivField=e,this._popoverDialog=null}return h(b,a),b.prototype.mount=function(a,b,c){var d,f;if(sessionStorage.removeItem(this._sessionStorageKey),this._popoverDialog||(this._popoverDialog=e.getContent(this._popoverEditTemplateLinkElementId,this.getIdFormat(this._dialogId)),$("body").append(this._popoverDialog)),this.init(this.getIdFormat(this._popoverEditTemplateLinkElementId)),c.length>1?(f=this._setPageContainerArgs(a,"aklub:get_email_template_from_db"),sessionStorage.setItem(this.sessionStorageKey,f)):f=this._setPageContainerArgs(a,"aklub:get_email_template"),this.show(this.getIdFormat(this._dialogId)),d=$(this.getIdFormat(this._dialogId)+" "+this.getClassFormat(this._closeBtnClass)),d.off("click",this._closeEditTemplateModalDialog),d.on("click",this._closeEditTemplateModalDialog),"openViaSelectBox"===b)return $(this.getIdFormat(this._popoverEditTemplateLinkElementId)).remove()},b.prototype._closeEditTemplateModalDialog=function(a){var b;return b=this._getIframeTemplateContent(),new f(b,$(b).find("article"),this._$templateDivField,this._$hiddenTemplateField)},b.prototype._getIframeTemplateContent=function(){var a;return a=document.querySelector(this.getIdFormat(this._$editTemplateModalDialogPageContainer.attr("id"))),null!=a?a.contentDocument:void 0},b.prototype._setPageContainerArgs=function(a,b){var c,d;return d=window.reverse(b,{template_name:a}),c={data:d,width:$(window).width(),height:$(window).height()},this._$editTemplateModalDialogPageContainer.attr(c),d},b.prototype.init=function(a){return $(a).mdl()},b.prototype.show=function(a){return mdl_open(a)},b}(b),d=function(b){function d(a,b,c,d,e,f,g,h,j,k,l,m,n,o,p){this._confirmTemplateEdit=i(this._confirmTemplateEdit,this),this._loadTemplateContent=i(this._loadTemplateContent,this),this._showEditTemplateModalDialog=i(this._showEditTemplateModalDialog,this),this._editTemplateBtnClickEvt=i(this._editTemplateBtnClickEvt,this),this._mouseMoveOverTemplateEvt=i(this._mouseMoveOverTemplateEvt,this),this._replaceTemplateFieldContent=i(this._replaceTemplateFieldContent,this),this._templateDivFieldId=l,this._hiddenTemplateFieldId=f,this._templateNameFieldId=d,this._getTemplateNameDialogShowDelay=c,this._editTemplateModalDialogId=a,this._popoverModalDialogEditTemplateLinkElementId=b,this._templateDivFormFieldContainerCSSClass=g,this._templateTextareaFieldId=j,this._editTemplateModalDialogPageContainerId=k,this._$templateDivFieldContainer=null,this._$templateTextareaFieldContainer=null,this._templateTextareaFieldValue="",this._showPopoverModalDialogDelay=m,this._popoverModalDialogBackDrop=n,this._popoverModalDialogDestroyDelay=o,this._templateTextAreaFormFieldContainerCSSClass=h,this._templateTypeFieldId=e,this._newEmptyTemplateName=null,this._getTemplateNameDialog=null,this._popoverDialogOpts=null,this._editTemplateBtnName=p,this._editTemplateModalDialog=null,this._cacheDom(),this._bindEvents(),this._templateFormFieldWidgetInit()}return h(d,b),d.prototype._cacheDom=function(){return this._$templateDivField=$(this.getIdFormat(this._templateDivFieldId)),this._$hiddenTemplateField=$(this.getIdFormat(this._hiddenTemplateFieldId)),this._$templateNameField=$(this.getIdFormat(this._templateNameFieldId)),this._$templateTypeField=$(this.getIdFormat(this._templateTypeFieldId)),this._$templateDivFormFieldContainer=$(this.getClassFormat(this._templateDivFormFieldContainerCSSClass)),this._$templateTextAreaFormFieldContainer=$(this.getClassFormat(this._templateTextAreaFormFieldContainerCSSClass)),this._$templateTextareaField=$(this.getIdFormat(this._templateTextareaFieldId)),this._$editTemplateModalDialogPageContainer=$(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),this._$editTemplateBtn=$(this.getNameFormat("input",this._editTemplateBtnName))},d.prototype._bindEvents=function(){return this._$templateNameField.bind("change",this._showEditTemplateModalDialog),this._$templateTypeField.bind("change",this._replaceTemplateFieldContent),this._$editTemplateModalDialogPageContainer.bind("load",this._loadTemplateContent),this._$editTemplateBtn.bind("click",this._editTemplateBtnClickEvt)},d.prototype._templateFormFieldWidgetInit=function(){var a;return this._$templateTextAreaFormFieldContainer.addClass("hidden"),"new"===this._$templateTypeField.val()?(this._exchangeFormFieldContainerContent(),a=this._$templateDivField.html(),this._$templateTextareaField.html(a),this._templateTextareaFieldValue=a,this._$templateDivField.html("")):"existed"===this._$templateTypeField.val()&&this._$hiddenTemplateField.val(this._$templateDivField.html()),this._initEditTemplateModalDialog(),this._enableDisableEditBtn(!0)},d.prototype._enableDisableEditBtn=function(a,b){var c;return null==b&&(b=!0),b?(c=this._$templateNameField.find("option:selected").val(),0===c.length?this._$editTemplateBtn.prop("disabled",a):void 0):this._$editTemplateBtn.prop("disabled",a)},d.prototype._getTemplateFieldContainer=function(){this._$templateDivFieldContainer=this._$templateDivFormFieldContainer,this._$templateTextareaFieldContainer=this._$templateTextAreaFormFieldContainer},d.prototype._checkTemplateFieldWidget=function(){var a;return this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.find("textarea"),a.length?"textarea":"div"},d.prototype._replaceTemplateFieldContent=function(a){var b;b=$(a.target).val(),"new"===b?"textarea"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContent():"existed"===b&&"div"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContentBack()},d.prototype._exchangeFormFieldContainerContentBack=function(){var a;return this._$templateNameField.attr("disabled",!1),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._$editTemplateBtn.off("click",this._editTemplateBtnClickEvt),this._$editTemplateBtn.on("click",this._editTemplateBtnClickEvt)},d.prototype.delay=function(a,b){return setTimeout(b,a)},d.prototype._mouseMoveOverTemplateEvt=function(a){var b,c,d,e,f;return b=$(a.currentTarget).offset(),e=a.pageX-b.left,f=a.pageY-b.top,d=f,c=e,this.delay(this._showPopoverModalDialogDelay,function(a){return function(){return a._initPopoverModalDialog(d,c)}}(this))},d.prototype._initPopoverModalDialog=function(a,b){return this._popoverDialogOpts.mount(a,b)},d.prototype._bindPopoverDialogMouseMoveEvt=function(){return this._popoverDialogOpts=new e(gettext("Edit template"),this._popoverModalDialogBackDrop,this._editTemplateModalDialogId,this._$templateNameField,this._popoverModalDialogDestroyDelay,this._$templateDivField,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._$templateDivField.mousemove(this._mouseMoveOverTemplateEvt)},d.prototype._initEditTemplateModalDialog=function(){return this._editTemplateModalDialog=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField)},d.prototype._editTemplateBtnClickEvt=function(a){var b,c,d;return a.preventDefault(),c=this._$templateNameField.find("option:selected"),b=c.text(),d=c.val().split(":"),this._editTemplateModalDialog.mount(b,"openViaBtn",d)},d.prototype._exchangeFormFieldContainerContent=function(){var a;return this._$templateTextareaField.html(this._templateTextareaFieldValue),this._$templateNameField.attr("disabled",!0),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),this._$templateDivFieldContainer.find("label").attr("for",this._templateDivFieldId),this._$templateDivFieldContainer.find("label").text(this._$templateTextareaFieldContainer.find("label").text()),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._$templateTextAreaFormFieldContainer.addClass("hidden"),this._$hiddenTemplateField.val("")},d.prototype._exchangeContent=function(){var a,b;this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.children(),b=this._$templateTextareaFieldContainer.children(),this._$templateDivFieldContainer.html(b),this._$templateTextareaFieldContainer.html(a)},d.prototype._showEditTemplateModalDialog=function(b){var d,e,f,g;if(sessionStorage.removeItem("newEmptyTemplateName"),e=""+this.getIdFormat(b.target.id),f=$(e+" option:selected").text(),$(""+e).val().length>0)return"new_empty_template"===f?(this._getTemplateNameDialog=new c(this._getTemplateNameDialogShowDelay,this._$templateDivField,this._templateNameFieldId,this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,f,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._getTemplateNameDialog.mount()):(this._enableDisableEditBtn(!1,!1),g=this._$templateNameField.find("option:selected").val().split(":"),d=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),d.mount(f,"openViaSelectBox",g))},d.prototype._loadTemplateContent=function(a){var b;if(this._newEmptyTemplateName=null!=(b=this._getTemplateNameDialog)?b.getNeEmptyTemplateName():void 0,null!=this._newEmptyTemplateName)return this._addEditTemplateConfirmEvent()},d.prototype._addEditTemplateConfirmEvent=function(){var a,b,c,d;return b="ct-ignition__button--confirm",d=document.querySelector(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),c=d.contentDocument,a=$(c).find(this.getClassFormat(b)),a.unbind("click"),a.bind("click",this._confirmTemplateEdit)},d.prototype._confirmTemplateEdit=function(){if(null!=this._newEmptyTemplateName)return this._addNewSelectOpt(this._newEmptyTemplateName,this._$templateNameField),this._eraseNewTemplateName()},d.prototype._eraseNewTemplateName=function(){return this._newEmptyTemplateName=null},d.prototype._addNewSelectOpt=function(a,b){var c,d,e,f;if(d=$(this.getIdFormat(this._templateNameFieldId)+" option"),f=$.map(d,function(a){return a.value}),e="new_empty_template:"+a,-1===f.indexOf(e))return c=new Option(a,e),b.append(c),b.val(e)},d}(b),f=function(a){function b(a,b,c,d){this._htmlDoc=a,this._$editTemplatePageContainer=b,this._$templateDivField=c,this._$hiddenTemplateField=d,this._videoClass="video",this._verticalSpaceClassName="vertical-space",this._textContainerTags=["p","ul","ol","h1","h2","h3","h4","h5","h6"],this.convertVideoIframeToImgThumbnail()}return h(b,a),b.prototype.convertVideoIframeToImgThumbnail=function(){var a,b,c,d,e,f;return c=[],b=!1,f=new RegExp("youtube.com.*(v=|/embed/)(.{11})"),e=new RegExp("vimeo.com.*./(.*)"),a=function(a,b,c,d,e){return a.css("float",e),b.append(a),b.append(c),$(d).after(b),$(d).remove()},d=function(b){var c;return"vimeo"===this.videoType&&(c={src:b.thumbnail_url,width:b.thumbnail_width,height:b.thumbnail_height},this.$videoThumbnail.attr(c)),this.$videoCaption.text(b.title),a(this.$videoThumbnail,this.$clickableVideoThumbnail,this.$videoCaption,this.element,this.iframeCssFloatProperty)},$(this._htmlDoc).find("iframe").each(function(a){return function(g,h){var i,j,k,l,m,n,o,p,q,r,s,t,u;return p=$(h).attr("src"),n=$(h).css("float"),q=$(h).attr("width"),o=$(h).attr("height"),i=$(''),i.attr("href",p),k=$(''),k.attr({width:q,height:o}),j=$("
    "),m={"text-align":"center","font-size":"12px"},j.css(m),p.match(f)?(b=!0,t=p.match(f).pop(),i.attr("id",t),i.addClass("youtube"),u="//img.youtube.com/vi/"+t+"/0.jpg",k.attr("src",u),r="http://www.youtube.com/watch?v="+t+"&",s="https://noembed.com/embed?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"youtube"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):p.match(e)?(b=!0,t=p.match(e).pop(),i.attr("id",t),i.addClass("vimeo"),r="https://vimeo.com/"+t+"& width="+q+"&height="+o,s="https://vimeo.com/api/oembed.json?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"vimeo"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):void 0}}(this)),$.when.apply(null,c).done(function(a){return function(){return a.convertCssToInlineStyle()}}(this))},b.prototype.getAjaxData=function(a,b,c,d,e){return null==d&&(d="GET"),null==e&&(e="json"),{type:d,url:b,jsonp:"callback",dataType:e,context:a,success:c}},b.prototype.convertCssToInlineStyle=function(){return this.inlineStyler($(this._htmlDoc)),this._fixContentWidth(),this._fixMargin(),this._replaceImgSrc(),this._convertTextCssPositionToTable(),this._eraseVerticalSpaceContent(),this._$templateDivField.html(this._$editTemplatePageContainer),this._$hiddenTemplateField.val(this._$editTemplatePageContainer.html())},b.prototype.inlineStyler=function(a){return a.inlineStyler()},b.prototype.getElementPosition=function(a,b,c){var d,e,f,g;return null==b&&(b=""),null==c&&(c=""),d=$("
    +
    + - - +
    +
    @@ -60,9 +60,10 @@

    Title

    Sample image

    - + - "),g=$(""),f=$(""),f.css({border:"none",padding:0,margin:0}),f.attr({align:a,width:b,heigth:c}),d.append(e.append(g.append(f)))},b.prototype._replaceImgSrc=function(){var a;return a=this._$editTemplatePageContainer.find("img"),a.each(function(a){return function(b,c){var d,e,f,g,h;return d=$(c).closest(a.getClassFormat(a._videoClass)),e=d.length>0?d:$(c),0===d.length&&(h=$(c).attr("src"),g=new RegExp(window.origin),h.match(g)||(f=""+window.origin+$(c).attr("src"),$(c).attr("src",f))),a.convertImageCssPositionToTable(e)}}(this))},b.prototype.convertImageCssPositionToTable=function(a){var b,c,d,e,f,g,h;a=a.find("img").length>0?a.find("img"):a,f=parseInt(a.css("margin-left").split(".")[0].replace("px","")),g=parseInt(a.css("margin-right").split(".")[0].replace("px","")),e=a.css("float"),a.css("display",""),f===g&&"none"===e?c=this.getElementPosition("center","",""):f>g?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):g>f?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):"left"!==e&&"right"!==e||(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)),h=this.getClassFormat(this._videoClass),d=a.closest(h),c.find("td").append(d.length>0?d.clone():a.clone()),"left"===e?c.find("tr").append(b):c.find("tr").prepend(b),d.length>0?d.after(c):a.after(c),d.length>0?d.remove():a.remove()},b.prototype.convertImgFloatCssPositionToTable=function(a,b,c){var d,e,f,g,h,i,j;return i=15,j="left"===c?"0 0 0 "+i+"px":"0 "+i+"px 0 0",e=$(""),e.css({border:"none",padding:j,margin:"0"}),f=a.closest(this.getClassFormat(this._videoClass)),d=f.length>0?f.next().clone():a.next().clone(),h=parseFloat(b.find("#content_table").width()),g=h-parseFloat(a.attr("width"))-i,d.css("width",g),e.attr("width",g),e.append(d),f.length>0?f.next().remove():a.next().remove(),e},b.prototype._convertTextCssPositionToTable=function(){var a;return a=this._textContainerTags.join(", "),this._$editTemplatePageContainer.find(a).each(function(a){return function(b,c){var d,e;switch(e=$(c).attr("class")?$(c).attr("class").split(" "):[]){case e.indexOf("text-left")>-1:d=a.getElementPosition("left","","");break;case e.indexOf("text-right")>-1:d=a.getElementPosition("right","","");break;case e.indexOf("text-center")>-1:d=a.getElementPosition("center","","");break;default:d=a.getElementPosition("center","","")}return a.adjustListCssProperties($(c)),d.find("td").append($(c).clone()),$(c).after(d),$(c).remove()}}(this))},b.prototype.adjustListCssProperties=function(a){if(a.is("ul")||a.is("ol"))return a.css({margin:"10px",padding:"15px"})},b.prototype._fixMargin=function(){return $(this._htmlDoc).find("article").css("margin","")},b.prototype._getNumberValue=function(a,b){return null==b&&(b=-2),parseInt(a.slice(0,a.length-b))},b.prototype._getPaddingValue=function(a){var b,c;return null==a&&(a=!0),c=!0===a?"padding--left":"padding--top",b=$(this._htmlDoc).find(".article__content p").first().css(c),this._getNumberValue(b)},b.prototype._fixContentWidth=function(){var a,b;return b=this._getPaddingValue(),a=this._textContainerTags.join(", "),$(this._htmlDoc).find("article").find(a).each(function(a){return function(c,d){var e;return e=a._getNumberValue($(d).css("width")),$(d).css({width:e-2*b})}}(this))},b.prototype._eraseVerticalSpaceContent=function(){var a;return a=this.getClassFormat(this._verticalSpaceClassName),this._$editTemplatePageContainer.find(a).each(function(a,b){return $(b).find("center").remove()})},b}(b)}).call(this); \ No newline at end of file diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/edit_template_dialog.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/edit_template_dialog.coffee index 98a169842..6847a94fd 100644 --- a/apps/aklub/static/aklub/js/template_widget/src/script/edit_template_dialog.coffee +++ b/apps/aklub/static/aklub/js/template_widget/src/script/edit_template_dialog.coffee @@ -20,7 +20,8 @@ class EditTemplateModalDialog extends FormatSelectorMixin @_closeBtnClass = 'mdl-close' - @_$editTemplateModalDialogPageContainer = $editTemplateModalDialogPageContainer + @_$editTemplateModalDialogPageContainer = \ + $editTemplateModalDialogPageContainer @_$hiddenTemplateField = $hiddenTemplateField @@ -42,14 +43,15 @@ class EditTemplateModalDialog extends FormatSelectorMixin @getIdFormat @_dialogId ) - $('body').append @_popoverDialog + $('body').append @_popoverDialog # Initialize dialog @init @getIdFormat @_popoverEditTemplateLinkElementId # Set html template page container attr if templateType.length > 1 - url = @_setPageContainerArgs templateName, 'aklub:get_email_template_from_db' + url = @_setPageContainerArgs templateName, \ + 'aklub:get_email_template_from_db' sessionStorage.setItem @sessionStorageKey, url else url = @_setPageContainerArgs templateName, 'aklub:get_email_template' @@ -58,7 +60,8 @@ class EditTemplateModalDialog extends FormatSelectorMixin @show @getIdFormat @_dialogId # Bind close modal template dialog event - $closeBtn = $ "#{ @getIdFormat @_dialogId } #{ @getClassFormat @_closeBtnClass }" + $closeBtn = $ "#{ @getIdFormat @_dialogId } + #{ @getClassFormat @_closeBtnClass }" $closeBtn.off 'click', @_closeEditTemplateModalDialog $closeBtn.on 'click', @_closeEditTemplateModalDialog @@ -78,14 +81,15 @@ class EditTemplateModalDialog extends FormatSelectorMixin ) _getIframeTemplateContent: () -> - templatePage = document.querySelector @getIdFormat @_$editTemplateModalDialogPageContainer.attr 'id' + templatePage = document.querySelector @getIdFormat \ + @_$editTemplateModalDialogPageContainer.attr 'id' templatePage?.contentDocument _setPageContainerArgs: (templateName, urlName) -> # Set html template page container attr url = window.reverse urlName, {template_name: templateName} - attr = + attr = data: url, width: $(window).width(), height: $(window).height() @@ -99,4 +103,3 @@ class EditTemplateModalDialog extends FormatSelectorMixin show: (elementId) -> mdl_open(elementId) - diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/get_template_name_dialog.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/get_template_name_dialog.coffee index 6b5981e3a..3f8b59f75 100644 --- a/apps/aklub/static/aklub/js/template_widget/src/script/get_template_name_dialog.coffee +++ b/apps/aklub/static/aklub/js/template_widget/src/script/get_template_name_dialog.coffee @@ -12,7 +12,7 @@ class GetTemplateNameDialog extends FormatSelectorMixin popoverModalDialogEditTemplateLinkElementId, templateName, $editTemplateModalDialogPageContainer, - $hiddenTemplateField, + $hiddenTemplateField ) -> @_dialogId = 'get_template_name' @@ -35,11 +35,13 @@ class GetTemplateNameDialog extends FormatSelectorMixin @_editTemplateModalDialogId = editTemplateModalDialogId - @_popoverModalDialogEditTemplateLinkElementId = popoverModalDialogEditTemplateLinkElementId + @_popoverModalDialogEditTemplateLinkElementId = \ + popoverModalDialogEditTemplateLinkElementId @_templateName = templateName - @_$editTemplateModalDialogPageContainer = $editTemplateModalDialogPageContainer + @_$editTemplateModalDialogPageContainer = \ + $editTemplateModalDialogPageContainer @_$hiddenTemplateField = $hiddenTemplateField @@ -66,7 +68,9 @@ class GetTemplateNameDialog extends FormatSelectorMixin $formContentLabel.text gettext 'Template name' $formContentInput = $ '' - $formContentInput.attr {id: @_templateNameInputId, name: @_templateNameInputId} + $formContentInput.attr + id: @_templateNameInputId, + name: @_templateNameInputId $formContentContainer.append($formContentLabel) $formContentContainer.append($formContentInput) @@ -97,7 +101,8 @@ class GetTemplateNameDialog extends FormatSelectorMixin _checkTemplateName: (o, name, m) -> opts = [] - $("#{ @getIdFormat @_templateNameFieldId } > option").each () -> opts.push($(@).text()) + selector = "#{ @getIdFormat @_templateNameFieldId } > option" + $(selector).each () -> opts.push($(@).text()) if opts.indexOf(name) > -1 errorList = o.closest('form').find @getClassFormat @_errorListCSSClass @@ -112,15 +117,17 @@ class GetTemplateNameDialog extends FormatSelectorMixin $templateName = $ @getIdFormat @_templateNameInputId $(@getClassFormat @_errorListCSSClass).html '' - message = gettext('Template name may consist of a-z, 0-9, ' + - 'underscores, not spaces and must begin with a letter,' + - 'and be lowercase.') - valid = valid and @_checkRegexp $templateName, /^[a-z]([0-9a-z_])+$/, message + message = gettext('Template name may consist of a-z, 0-9, + underscores, not spaces and must begin with a letter, + and be lowercase.') + valid = valid and @_checkRegexp $templateName, \ + /^[a-z]([0-9a-z_])+$/, message message = gettext 'Template name exist.' $templateNameInput = $ @getIdFormat @_templateNameInputId - valid = valid and @_checkTemplateName $templateNameInput, $templateName.val(), message + valid = valid and @_checkTemplateName $templateNameInput, \ + $templateName.val(), message return valid @@ -159,7 +166,7 @@ class GetTemplateNameDialog extends FormatSelectorMixin okBtnClick = () => @_confirmTemplateName() - btnNames.ok = + btnNames.ok = text: gettext('Ok'), id: @_confirmBtnId, click: okBtnClick diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee index 83e658fde..8c97a373a 100644 --- a/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee +++ b/apps/aklub/static/aklub/js/template_widget/src/script/html_template_widget.coffee @@ -32,13 +32,16 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin @_editTemplateModalDialogId = editTemplateModalDialogId - @_popoverModalDialogEditTemplateLinkElementId = popoverModalDialogEditTemplateLinkElementId + @_popoverModalDialogEditTemplateLinkElementId = \ + popoverModalDialogEditTemplateLinkElementId - @_templateDivFormFieldContainerCSSClass = templateDivFormFieldContainerCSSClass + @_templateDivFormFieldContainerCSSClass = \ + templateDivFormFieldContainerCSSClass - @_templateTextareaFieldId = templateTextareaFieldId + @_templateTextareaFieldId = templateTextareaFieldId - @_editTemplateModalDialogPageContainerId = editTemplateModalDialogPageContainerId + @_editTemplateModalDialogPageContainerId = \ + editTemplateModalDialogPageContainerId @_$templateDivFieldContainer = null @@ -52,14 +55,15 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin @_popoverModalDialogDestroyDelay = popoverModalDialogDestroyDelay - @_templateTextAreaFormFieldContainerCSSClass = templateTextAreaFormFieldContainerCSSClass + @_templateTextAreaFormFieldContainerCSSClass = \ + templateTextAreaFormFieldContainerCSSClass @_templateTypeFieldId = templateTypeFieldId @_newEmptyTemplateName = null @_getTemplateNameDialog = null - + @_popoverDialogOpts = null @_editTemplateBtnName = editTemplateBtnName @@ -80,10 +84,13 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin @_$hiddenTemplateField = $ @getIdFormat @_hiddenTemplateFieldId @_$templateNameField = $ @getIdFormat @_templateNameFieldId @_$templateTypeField = $ @getIdFormat @_templateTypeFieldId - @_$templateDivFormFieldContainer = $ @getClassFormat @_templateDivFormFieldContainerCSSClass - @_$templateTextAreaFormFieldContainer = $ @getClassFormat @_templateTextAreaFormFieldContainerCSSClass + @_$templateDivFormFieldContainer = $ @getClassFormat \ + @_templateDivFormFieldContainerCSSClass + @_$templateTextAreaFormFieldContainer = $ @getClassFormat \ + @_templateTextAreaFormFieldContainerCSSClass @_$templateTextareaField = $ @getIdFormat @_templateTextareaFieldId - @_$editTemplateModalDialogPageContainer = $ @getIdFormat @_editTemplateModalDialogPageContainerId + @_$editTemplateModalDialogPageContainer = $ @getIdFormat \ + @_editTemplateModalDialogPageContainerId @_$editTemplateBtn = $ @getNameFormat 'input', @_editTemplateBtnName _bindEvents: () -> @@ -100,15 +107,17 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin @_exchangeFormFieldContainerContent() - # Copy value from template form field div widget to textarea and save as global value + # Copy value from template form field div widget to textarea and + # save as global value textareaValue = @_$templateDivField.html() - @_$templateTextareaField.html textareaValue + @_$templateTextareaField.html textareaValue @_templateTextareaFieldValue = textareaValue @_$templateDivField.html '' else if @_$templateTypeField.val() is 'existed' - # Copy value from template form field div widget into hidden field after form page loaded + # Copy value from template form field div widget into hidden field + # after form page loaded @_$hiddenTemplateField.val @_$templateDivField.html() # Init edit template modal dialog @@ -126,12 +135,12 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin @_$editTemplateBtn.prop('disabled', state) else - @_$editTemplateBtn.prop('disabled', state) + @_$editTemplateBtn.prop('disabled', state) _getTemplateFieldContainer: () -> @_$templateDivFieldContainer = @_$templateDivFormFieldContainer @_$templateTextareaFieldContainer = @_$templateTextAreaFormFieldContainer - return + return _checkTemplateFieldWidget: () -> @_getTemplateFieldContainer() @@ -149,8 +158,8 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin if @_checkTemplateFieldWidget() isnt 'textarea' @_exchangeFormFieldContainerContent() else if templateType is 'existed' - if @_checkTemplateFieldWidget() isnt 'div' - @_exchangeFormFieldContainerContentBack() + if @_checkTemplateFieldWidget() isnt 'div' + @_exchangeFormFieldContainerContentBack() return _exchangeFormFieldContainerContentBack: () -> @@ -188,7 +197,7 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin delay: (ms, func) -> setTimeout func, ms _mouseMoveOverTemplateEvt: (evt) => - # Mouse move event over template content + # Mouse move event over template content # Initialize popover dialog offset = $(evt.currentTarget).offset() relX = evt.pageX - offset.left @@ -196,7 +205,8 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin offsetTop = relY offsetLeft = relX - @delay @_showPopoverModalDialogDelay, => @_initPopoverModalDialog offsetTop, offsetLeft + @delay @_showPopoverModalDialogDelay, => \ + @_initPopoverModalDialog offsetTop, offsetLeft _initPopoverModalDialog: (offsetTop, offsetLeft) -> @_popoverDialogOpts.mount(offsetTop, offsetLeft) @@ -204,7 +214,7 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin _bindPopoverDialogMouseMoveEvt: () -> @_popoverDialogOpts = new PopoverModalDialog( - gettext('Edit template'), + gettext('Edit template'), @_popoverModalDialogBackDrop, @_editTemplateModalDialogId, @_$templateNameField, @@ -254,12 +264,13 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin attr = id: @_templateTextareaFieldId name: @_templateTextareaFieldId.slice(3) - + @_$templateDivField.attr attr # Replace template div form field widget label - @_$templateDivFieldContainer.find('label').attr('for', @_templateDivFieldId) - @_$templateDivFieldContainer.find('label').text(@_$templateTextareaFieldContainer.find('label').text()) + @_$templateDivFieldContainer.find('label').attr 'for', @_templateDivFieldId + @_$templateDivFieldContainer.find('label').text \ + @_$templateTextareaFieldContainer.find('label').text() # Replace template textarea form field widget id attr = @@ -287,7 +298,8 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin # Replace form field container content $templateDivFieldContainerChildren = @_$templateDivFieldContainer.children() - $templateTextareaFieldContainerChildren = @_$templateTextareaFieldContainer.children() + $templateTextareaFieldContainerChildren = \ + @_$templateTextareaFieldContainer.children() @_$templateDivFieldContainer .html($templateTextareaFieldContainerChildren) @_$templateTextareaFieldContainer.html($templateDivFieldContainerChildren) @@ -323,7 +335,8 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin # Enable edit btn if empty template name @_enableDisableEditBtn(false, false) - templateType = @_$templateNameField.find('option:selected').val().split ':' + templateType = @_$templateNameField.find( + 'option:selected').val().split ':' dialog = new EditTemplateModalDialog( @_editTemplateModalDialogId, @@ -344,7 +357,8 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin _addEditTemplateConfirmEvent: () -> confirmEditBtnClass = 'ct-ignition__button--confirm' - templatePage = document.querySelector @getIdFormat @_editTemplateModalDialogPageContainerId + templatePage = document.querySelector \ + @getIdFormat @_editTemplateModalDialogPageContainerId htmlDocument = templatePage.contentDocument confirmEditBtn = $(htmlDocument).find @getClassFormat confirmEditBtnClass @@ -372,4 +386,3 @@ class HtmlTemplateFormFieldWidget extends FormatSelectorMixin opt = new Option(text, value) element.append(opt) element.val(value) - diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/popover_dialog.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/popover_dialog.coffee index a91f852e2..669d583ab 100644 --- a/apps/aklub/static/aklub/js/template_widget/src/script/popover_dialog.coffee +++ b/apps/aklub/static/aklub/js/template_widget/src/script/popover_dialog.coffee @@ -5,7 +5,7 @@ class PopoverModalDialog extends FormatSelectorMixin @editTemplateLinkText = gettext('Edit template') - + constructor: ( title, backdrop, @@ -30,9 +30,11 @@ class PopoverModalDialog extends FormatSelectorMixin @_$mountElement = $mountElement - @_popoverModalDialogEditTemplateLinkElementId = popoverModalDialogEditTemplateLinkElementId + @_popoverModalDialogEditTemplateLinkElementId = \ + popoverModalDialogEditTemplateLinkElementId - @_$editTemplateModalDialogPageContainer = $editTemplateModalDialogPageContainer + @_$editTemplateModalDialogPageContainer = \ + $editTemplateModalDialogPageContainer @_$hiddenTemplateField = $hiddenTemplateField @@ -44,20 +46,20 @@ class PopoverModalDialog extends FormatSelectorMixin element.click (evt) => evt.preventDefault() - + templateNameSelectedOpt = @_$templateNameField.find('option:selected') templateName = templateNameSelectedOpt.text() templateType = templateNameSelectedOpt.val().split(':') # Show edit template modal dialog dialog = new EditTemplateModalDialog( - @_editTemplateModalDialogId, + @_editTemplateModalDialogId, @_popoverModalDialogEditTemplateLinkElementId, @_$editTemplateModalDialogPageContainer, @_$hiddenTemplateField, @_$templateDivField ) - + dialog.mount templateName, 'openViaPopoverDialog', templateType # Destroy popover dialog @@ -81,23 +83,26 @@ class PopoverModalDialog extends FormatSelectorMixin delay: (ms, func) -> setTimeout func, ms - @getContent: (popoverModalDialogEditTemplateLinkElementId, editTemplateModalDialogId) -> + @getContent: ( + popoverModalDialogEditTemplateLinkElementId, + editTemplateModalDialogId, + ) -> - $element = $ '' - $element.attr - id: @dialogId, - href: '' + $element = $ '' + $element.attr + id: @dialogId, + href: '' - editTemplateModalDialogAttr = - id: popoverModalDialogEditTemplateLinkElementId, - 'data-type': 'modal', - 'data-target': editTemplateModalDialogId, - 'data-fullscreen': 'true', - 'data-overlayClick': 'true' + editTemplateModalDialogAttr = + id: popoverModalDialogEditTemplateLinkElementId, + 'data-type': 'modal', + 'data-target': editTemplateModalDialogId, + 'data-fullscreen': 'true', + 'data-overlayClick': 'true' - $element.attr editTemplateModalDialogAttr - $element.html "#{ @editTemplateLinkText }" - return $element + $element.attr editTemplateModalDialogAttr + $element.html "#{ @editTemplateLinkText }" + return $element _getOpt: () -> @@ -105,7 +110,7 @@ class PopoverModalDialog extends FormatSelectorMixin @show element content = PopoverModalDialog.getContent( - @_popoverModalDialogEditTemplateLinkElementId, + @_popoverModalDialogEditTemplateLinkElementId, @getIdFormat @_editTemplateModalDialogId ) @@ -119,4 +124,3 @@ class PopoverModalDialog extends FormatSelectorMixin animation: 'fade' backdrop: @_backdrop, onShow: show - diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/process_template.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/process_template.coffee index 0d9ada475..f5030588b 100644 --- a/apps/aklub/static/aklub/js/template_widget/src/script/process_template.coffee +++ b/apps/aklub/static/aklub/js/template_widget/src/script/process_template.coffee @@ -4,24 +4,39 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin - constructor: (htmlDoc, $editTemplatePageContainer, $templateDivField, $hiddenTemplateField) -> + constructor: ( + htmlDoc, + $editTemplatePageContainer, + $templateDivField, + $hiddenTemplateField + ) -> - @_htmlDoc = htmlDoc + @_htmlDoc = htmlDoc - @_$editTemplatePageContainer = $editTemplatePageContainer + @_$editTemplatePageContainer = $editTemplatePageContainer - @_$templateDivField = $templateDivField + @_$templateDivField = $templateDivField - @_$hiddenTemplateField = $hiddenTemplateField - - @_videoClass = 'video' + @_$hiddenTemplateField = $hiddenTemplateField - @_verticalSpaceClassName = 'vertical-space' + @_videoClass = 'video' - @_textContainerTags = ['p', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] + @_verticalSpaceClassName = 'vertical-space' - # Methods - @convertVideoIframeToImgThumbnail() + @_textContainerTags = [ + 'p', + 'ul', + 'ol', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6' + ] + + # Methods + @convertVideoIframeToImgThumbnail() convertVideoIframeToImgThumbnail: () -> @@ -32,24 +47,30 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin vimeoRegex = new RegExp 'vimeo.com.*./(.*)' - addThumbnail = ($videoThumbnail, $clickableVideoThumbnail, $videoCaption, element, iframeCssFloatProperty) -> + addThumbnail = ( + $videoThumbnail, + $clickableVideoThumbnail, + $videoCaption, + element, + iframeCssFloatProperty + ) -> - $videoThumbnail.css 'float', iframeCssFloatProperty + $videoThumbnail.css 'float', iframeCssFloatProperty - # Add video thumbnail - $clickableVideoThumbnail.append $videoThumbnail + # Add video thumbnail + $clickableVideoThumbnail.append $videoThumbnail - # Add video thumbnail figcaption - $clickableVideoThumbnail.append $videoCaption + # Add video thumbnail figcaption + $clickableVideoThumbnail.append $videoCaption - $(element).after $clickableVideoThumbnail + $(element).after $clickableVideoThumbnail - # Remove video iframe - $(element).remove() + # Remove video iframe + $(element).remove() success = (data) -> # videoThumbnailSrc = data[0].thumbnail_large - # attr = + # attr = # 'src': videoThumbnailSrc if @videoType is 'vimeo' @@ -77,7 +98,7 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin iframeCssFloatProperty = $(element).css 'float' iframeWidth = $(element).attr 'width' iframeHeight = $(element).attr 'height' - + $clickableVideoThumbnail = $ '' $clickableVideoThumbnail.attr 'href', iframeSrc @@ -107,7 +128,7 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin subUrl = "http://www.youtube.com/watch?v=#{ videoId }&" url = "https://noembed.com/embed?url=#{ subUrl }" - context = + context = $videoThumbnail: $videoThumbnail, $clickableVideoThumbnail: $clickableVideoThumbnail, $videoCaption: $videoCaption, @@ -115,7 +136,8 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin iframeCssFloatProperty: iframeCssFloatProperty, videoType: 'youtube' - promises.push $.ajax @getAjaxData context, url, success, type='GET', dataType='jsonp' + promises.push $.ajax @getAjaxData context, url, success, \ + type='GET', dataType='jsonp' else if iframeSrc.match(vimeoRegex) @@ -133,10 +155,11 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin https://developer.vimeo.com/api/oembed/videos ### - subUrl = "https://vimeo.com/#{ videoId }&width=#{ iframeWidth }&height=#{ iframeHeight }" + subUrl = "https://vimeo.com/#{ videoId }& + width=#{ iframeWidth }&height=#{ iframeHeight }" url = "https://vimeo.com/api/oembed.json?url=#{ subUrl }" - context = + context = $videoThumbnail: $videoThumbnail, $clickableVideoThumbnail: $clickableVideoThumbnail, $videoCaption: $videoCaption, @@ -144,7 +167,8 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin iframeCssFloatProperty: iframeCssFloatProperty, videoType: 'vimeo' - promises.push $.ajax @getAjaxData context, url, success, type='GET', dataType='jsonp' + promises.push $.ajax @getAjaxData context, url, success, \ + type='GET', dataType='jsonp' # Wait for an all ajax request done $.when.apply(null, promises).done () => @@ -209,7 +233,8 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin $imgs.each (i, element) => $clickableVideoThumbnail = $(element).closest @getClassFormat @_videoClass - $img = if $clickableVideoThumbnail.length > 0 then $clickableVideoThumbnail else $(element) + $img = if $clickableVideoThumbnail.length > 0 \ + then $clickableVideoThumbnail else $(element) if $clickableVideoThumbnail.length == 0 src = $(element).attr('src') @@ -226,8 +251,12 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin $img = if $img.find('img').length > 0 then $img.find 'img' else $img - marginLeft = parseInt($img.css('margin-left').split('.')[0].replace('px', '')) - marginRight = parseInt($img.css('margin-right').split('.')[0].replace('px', '')) + marginLeft = parseInt( + $img.css('margin-left').split('.')[0].replace('px', '') + ) + marginRight = parseInt( + $img.css('margin-right').split('.')[0].replace('px', '') + ) floatPosition = $img.css 'float' $img.css 'display', '' @@ -236,64 +265,64 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin else if marginLeft > marginRight - $tableWrapper = @getElementPosition( - floatPosition, - $img.attr('width'), - $img.attr('height') - ) - $tableCell = @convertImgFloatCssPositionToTable( - $img, - @_$editTemplatePageContainer, - floatPosition - ) + $tableWrapper = @getElementPosition( + floatPosition, + $img.attr('width'), + $img.attr('height') + ) + $tableCell = @convertImgFloatCssPositionToTable( + $img, + @_$editTemplatePageContainer, + floatPosition + ) else if marginRight > marginLeft - $tableWrapper = @getElementPosition( - floatPosition, - $img.attr('width'), - $img.attr('height') - ) - $tableCell = @convertImgFloatCssPositionToTable( - $img, - @_$editTemplatePageContainer, - floatPosition - ) + $tableWrapper = @getElementPosition( + floatPosition, + $img.attr('width'), + $img.attr('height') + ) + $tableCell = @convertImgFloatCssPositionToTable( + $img, + @_$editTemplatePageContainer, + floatPosition + ) else if (floatPosition is'left') or (floatPosition is'right') - $tableWrapper = @getElementPosition( - floatPosition, - $img.attr('width'), - $img.attr('height') - ) - $tableCell = @convertImgFloatCssPositionToTable( - $img, - @_$editTemplatePageContainer, - floatPosition - ) + $tableWrapper = @getElementPosition( + floatPosition, + $img.attr('width'), + $img.attr('height') + ) + $tableCell = @convertImgFloatCssPositionToTable( + $img, + @_$editTemplatePageContainer, + floatPosition + ) videoClassSelector = @getClassFormat @_videoClass $videoIframe = $img.closest(videoClassSelector) $tableWrapper.find('td').append( if $videoIframe.length > 0 - $videoIframe.clone() + $videoIframe.clone() else $img.clone() ) if floatPosition is 'left' - $tableWrapper.find('tr').append $tableCell - else + $tableWrapper.find('tr').append $tableCell + else $tableWrapper.find('tr').prepend $tableCell if $videoIframe.length > 0 - $videoIframe.after $tableWrapper - else + $videoIframe.after $tableWrapper + else $img.after $tableWrapper if $videoIframe.length > 0 - $videoIframe.remove() + $videoIframe.remove() else $img.remove() @@ -301,7 +330,8 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin convertImgFloatCssPositionToTable: ($img, $editContainer, position) -> padding = 15 - paddingValues = if position is 'left' then "0 0 0 #{ padding }px" else "0 #{ padding }px 0 0" + paddingValues = if position is 'left' then "0 0 0 #{ padding }px" \ + else "0 #{ padding }px 0 0" $tableCell = $('') $tableCell.css({ @@ -312,31 +342,37 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin $videoIframe = $img.closest(@getClassFormat @_videoClass) if $videoIframe.length > 0 - $contextElement = $videoIframe.next().clone() + $contextElement = $videoIframe.next().clone() else $contextElement = $img.next().clone() - editContainerWidth = parseFloat($editContainer.find('#content_table').width()) + editContainerWidth = parseFloat( + $editContainer.find('#content_table').width() + ) contextWidth = editContainerWidth - parseFloat($img.attr('width')) - padding - $contextElement.css 'width', contextWidth + $contextElement.css 'width', contextWidth $tableCell.attr 'width', contextWidth $tableCell.append $contextElement - if $videoIframe.length > 0 - $videoIframe.next().remove() - else + if $videoIframe.length > 0 + $videoIframe.next().remove() + else $img.next().remove() return $tableCell _convertTextCssPositionToTable: () -> - @_$editTemplatePageContainer.find(@_textContainerTags.join(', ')).each (i, element) => - elementCssClass = if $(element).attr('class') then $(element).attr('class').split(' ') else [] + containers = @_textContainerTags.join(', ') + + @_$editTemplatePageContainer.find(containers).each (i, element) => + + elementCssClass = if $(element).attr('class') \ + then $(element).attr('class').split(' ') else [] switch elementCssClass - when elementCssClass.indexOf('text-left') > -1 + when elementCssClass.indexOf('text-left') > -1 $tableWrapper = @getElementPosition('left', '', '') when elementCssClass.indexOf('text-right') > -1 $tableWrapper = @getElementPosition('right', '', '') @@ -372,23 +408,25 @@ class PostProcessHtmlTemplate extends FormatSelectorMixin else position = 'padding--top' - paddingValue = $(@_htmlDoc).find('.article__content p').first().css(position) + paddingValue = $(@_htmlDoc).find( + '.article__content p').first().css(position) @_getNumberValue(paddingValue) _fixContentWidth: () -> paddingValue = @_getPaddingValue() + containers = @_textContainerTags.join(', ') - $(@_htmlDoc).find('article').find(@_textContainerTags.join(', ')).each (index, value) => + $(@_htmlDoc).find('article').find(containers).each (index, value) => origWidth = @_getNumberValue($(value).css('width')) - + # Set correct width (orig width - 2 * padding value) $(value).css width: origWidth - paddingValue * 2 _eraseVerticalSpaceContent: () -> # Erase vertical space content - @_$editTemplatePageContainer.find(@getClassFormat @_verticalSpaceClassName).each (i, element) => + selector = @getClassFormat @_verticalSpaceClassName + @_$editTemplatePageContainer.find(selector).each (i, element) -> $(element).find('center').remove() - \ No newline at end of file From acbb5f55114a94a8c845bc6157edb67b3d91c6db Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 6 Apr 2020 06:22:12 +0200 Subject: [PATCH 143/189] chore: add merged migration files --- apps/aklub/migrations/0069_merge_20200406_0617.py | 14 ++++++++++++++ apps/aklub/migrations/0074_merge_20200414_0547.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 apps/aklub/migrations/0069_merge_20200406_0617.py create mode 100644 apps/aklub/migrations/0074_merge_20200414_0547.py diff --git a/apps/aklub/migrations/0069_merge_20200406_0617.py b/apps/aklub/migrations/0069_merge_20200406_0617.py new file mode 100644 index 000000000..f4880f232 --- /dev/null +++ b/apps/aklub/migrations/0069_merge_20200406_0617.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.10 on 2020-04-06 04:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0067_merge_20200203_0833'), + ('aklub', '0068_auto_20200319_1414'), + ] + + operations = [ + ] diff --git a/apps/aklub/migrations/0074_merge_20200414_0547.py b/apps/aklub/migrations/0074_merge_20200414_0547.py new file mode 100644 index 000000000..3f867e2a9 --- /dev/null +++ b/apps/aklub/migrations/0074_merge_20200414_0547.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.10 on 2020-04-14 03:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0073_auto_20200206_1440'), + ('aklub', '0069_merge_20200406_0617'), + ] + + operations = [ + ] From e2204b8f828c8e3e0f433d268d83b03639e96d83 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Tue, 14 Apr 2020 07:45:04 +0200 Subject: [PATCH 144/189] fix: lazy loading icon_name model field choices --- .../migrations/0006_auto_20200414_0736.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/html_template_editor/migrations/0006_auto_20200414_0736.py diff --git a/apps/html_template_editor/migrations/0006_auto_20200414_0736.py b/apps/html_template_editor/migrations/0006_auto_20200414_0736.py new file mode 100644 index 000000000..032efdc9f --- /dev/null +++ b/apps/html_template_editor/migrations/0006_auto_20200414_0736.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-04-14 05:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0005_templatecontent_styles'), + ] + + operations = [ + migrations.AlterField( + model_name='companysocialmedia', + name='icon_name', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Icon name'), + ), + ] From a2b087652ab6e944303ea9ad8e0edda3029426b0 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Tue, 14 Apr 2020 17:33:16 +0200 Subject: [PATCH 145/189] feat: add TemplateHeader model --- .../migrations/0007_templateheader.py | 36 ++++++++ apps/html_template_editor/models.py | 90 ++++++++++++++++++- apps/html_template_editor/validators.py | 26 ++++++ 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 apps/html_template_editor/migrations/0007_templateheader.py create mode 100644 apps/html_template_editor/validators.py diff --git a/apps/html_template_editor/migrations/0007_templateheader.py b/apps/html_template_editor/migrations/0007_templateheader.py new file mode 100644 index 000000000..5ef99577a --- /dev/null +++ b/apps/html_template_editor/migrations/0007_templateheader.py @@ -0,0 +1,36 @@ +# Generated by Django 2.2.10 on 2020-04-14 15:32 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import html_template_editor.validators + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('html_template_editor', '0006_auto_20200414_0736'), + ] + + operations = [ + migrations.CreateModel( + name='TemplateHeader', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Header name')), + ('text', models.CharField(blank=True, max_length=255, null=True, verbose_name='Header text')), + ('text_horizontal_position', models.CharField(blank=True, choices=[('left', 'Left'), ('center', 'Center'), ('right', 'Right')], default='left', max_length=20, null=True, verbose_name='Header horizontal text position')), + ('text_vertical_position', models.CharField(blank=True, choices=[('top', 'Top'), ('middle', 'Middle'), ('bottom', 'Bottom')], default='middle', max_length=20, null=True, verbose_name='Header vertical text position')), + ('logo', models.ImageField(help_text='Max. image width 300 px, and max. image file size 5 MB', upload_to='images', validators=[html_template_editor.validators.validate_logo_image], verbose_name='Logo image')), + ('logo_horizontal_position', models.CharField(blank=True, choices=[('left', 'Left'), ('center', 'Center'), ('right', 'Right')], default='left', max_length=20, null=True, verbose_name='Logo horizontal position')), + ('logo_position', models.CharField(blank=True, choices=[('left', 'Left'), ('right', 'Right')], default='left', help_text='Left or right column logo position', max_length=20, null=True, verbose_name='Logo position')), + ('show', models.BooleanField(default=False, help_text='Show header', verbose_name='Show')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Template header', + 'verbose_name_plural': 'Template headers', + }, + ), + ] diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index 650bd962f..1fd8f3b85 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -2,7 +2,7 @@ import uuid -# from django.contrib.auth import get_user_model +from django.conf import settings from django.contrib.postgres.fields import JSONField from django.db import models from django.utils.functional import lazy @@ -12,8 +12,7 @@ from phonenumber_field.modelfields import PhoneNumberField from .utils import get_social_media_icons_names - -# User = get_user_model() +from .validators import validate_logo_image class TemplateContent(models.Model): @@ -247,3 +246,88 @@ def get_phone(self): def get_social_media(self): objs = self.social_media.all() return mark_safe("
    ".join([f"{o.icon_name}: {o.url}" for o in objs])) + + +class TemplateHeader(models.Model): + + class Meta: + verbose_name = _("Template header") + verbose_name_plural = _("Template headers") + + name = models.CharField( + verbose_name=_("Header name"), + max_length=255, + null=True, + blank=True, + ) + text = models.CharField( + verbose_name=_("Header text"), + max_length=255, + null=True, + blank=True, + ) + text_horizontal_position = models.CharField( + verbose_name=_("Header horizontal text position"), + choices=(("left", _("Left")), + ("center", _("Center")), + ("right", _("Right"))), + default="left", + max_length=20, + null=True, + blank=True, + ) + text_vertical_position = models.CharField( + verbose_name=_("Header vertical text position"), + choices=(("top", _("Top")), + ("middle", _("Middle")), + ("bottom", _("Bottom"))), + default="middle", + max_length=20, + null=True, + blank=True, + ) + logo = models.ImageField( + verbose_name=_("Logo image"), + upload_to='images', + height_field=None, + width_field=None, + max_length=100, + validators=[validate_logo_image], + help_text=_("Max. image width 300 px, and max. image file size 5 MB"), + ) + logo_horizontal_position = models.CharField( + verbose_name=_("Logo horizontal position"), + choices=( + ("left", _("Left")), + ("center", _("Center")), + ("right", _("Right"))), + default="left", + max_length=20, + null=True, + blank=True, + ) + logo_position = models.CharField( + verbose_name=_("Logo position"), + choices=( + ("left", _("Left")), + ("right", _("Right"))), + default="left", + max_length=20, + null=True, + blank=True, + help_text=_("Left or right column logo position"), + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + models.SET_NULL, + blank=True, + null=True, + ) + show = models.BooleanField( + verbose_name=_("Show"), + default=False, + help_text=_("Show header"), + ) + + def __str__(self): + return self.name diff --git a/apps/html_template_editor/validators.py b/apps/html_template_editor/validators.py new file mode 100644 index 000000000..fd0676d2c --- /dev/null +++ b/apps/html_template_editor/validators.py @@ -0,0 +1,26 @@ +from PIL import Image + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + + +def validate_logo_image(image): + max_width = 300 + megabyte_limit = 5.0 * 1024 * 1024 + + width, height = Image.open(image).size + image_file_size = image.tell() + + print(width, image_file_size) + if width > max_width: + raise ValidationError( + _("Width is larger than what is allowed %(max_width)s px"), + params={'max_width': max_width}, + ) + + if image_file_size > megabyte_limit: + raise ValidationError( + _("Image size is larger than what is allowed " + "%(max_file_size)s MB"), + params={'max_file_size': megabyte_limit}, + ) From c44d7490c1bd09f5dec7cfab1cbc5ecc6ca0f0cd Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Tue, 14 Apr 2020 18:01:54 +0200 Subject: [PATCH 146/189] feat: add TemplateHeaderAdminInline admin model class --- apps/html_template_editor/admin.py | 41 ++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/html_template_editor/admin.py b/apps/html_template_editor/admin.py index 175b31837..0745723e6 100644 --- a/apps/html_template_editor/admin.py +++ b/apps/html_template_editor/admin.py @@ -1,9 +1,10 @@ - +from django import forms from django.contrib import admin + from .models import ( CompanyEmail, CompanyPhone, CompanySocialMedia, - CompanyUrl, TemplateFooter, + CompanyUrl, TemplateFooter, TemplateHeader, ) # Register your models here. @@ -72,3 +73,39 @@ class TemplateFooterAdminInline(admin.ModelAdmin): CompanyEmailAdminInline, CompanySocialMediaAdminInline, ] + + +class TemplateHeaderAdminForm(forms.ModelForm): + class Meta: + model = TemplateHeader + exclude = ('user',) + + def save(self, commit=True): + instance = super(TemplateHeaderAdminForm, self).save(commit=False) + instance.user = self.user + if commit: + instance.save() + return instance + + +@admin.register(TemplateHeader) +class TemplateHeaderAdminInline(admin.ModelAdmin): + form = TemplateHeaderAdminForm + list_display = ( + 'name', + 'text', + 'logo', + 'user', + 'show', + ) + list_filter = ( + 'name', + 'text', + 'user', + 'show', + ) + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + form.user = request.user + return form From 46cac0fa7a909b94d830a0feff646c2bc465e42f Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Tue, 14 Apr 2020 18:30:41 +0200 Subject: [PATCH 147/189] feat: add TemplateHeader object into the template context --- apps/aklub/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index d5669145c..957b8ef0b 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -14,7 +14,7 @@ from django.utils.translation import ugettext_lazy as _ from html_template_editor.models import ( - Images, TemplateContent, TemplateFooter, + Images, TemplateContent, TemplateFooter, TemplateHeader, ) from . import models as aklub_models @@ -140,6 +140,7 @@ def get_email_templates_names(): 'new_empty_template.html', 'footer.html', 'automat_fonts.html', + 'header.html', ] def get_templates_files(): @@ -191,6 +192,10 @@ def get_email_template_context(template_path, template_url): if footer: context['footer'] = footer + header = TemplateHeader.objects.filter(show=True).first() + if header: + context['header'] = header + background_image = Images.objects.filter( template_url=template_url, edited_crop__isnull=False, From 9447d3d94db5628b8bbf5faf0bd18b7e60f38dfc Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Tue, 14 Apr 2020 18:37:56 +0200 Subject: [PATCH 148/189] feat: add email template header.html file --- .../templates/email_templates/header.html | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 apps/aklub/templates/email_templates/header.html diff --git a/apps/aklub/templates/email_templates/header.html b/apps/aklub/templates/email_templates/header.html new file mode 100644 index 000000000..8f34cf7cb --- /dev/null +++ b/apps/aklub/templates/email_templates/header.html @@ -0,0 +1,25 @@ +{% load static %} + +
    +
    + - "),g=$(""),f=$(""),f.css({border:"none",padding:0,margin:0}),f.attr({align:a,width:b,heigth:c}),d.append(e.append(g.append(f)))},b.prototype._replaceImgSrc=function(){var a;return a=this._$editTemplatePageContainer.find("img"),a.each(function(a){return function(b,c){var d,e,f,g,h;return d=$(c).closest(a.getClassFormat(a._videoClass)),e=d.length>0?d:$(c),0===d.length&&(h=$(c).attr("src"),g=new RegExp(window.origin),h.match(g)||(f=""+window.origin+$(c).attr("src"),$(c).attr("src",f))),a.convertImageCssPositionToTable(e)}}(this))},b.prototype.convertImageCssPositionToTable=function(a){var b,c,d,e,f,g,h;a=a.find("img").length>0?a.find("img"):a,f=parseInt(a.css("margin-left").split(".")[0].replace("px","")),g=parseInt(a.css("margin-right").split(".")[0].replace("px","")),e=a.css("float"),a.css("display",""),f===g&&"none"===e?c=this.getElementPosition("center","",""):f>g?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):g>f?(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)):"left"!==e&&"right"!==e||(c=this.getElementPosition(e,a.attr("width"),a.attr("height")),b=this.convertImgFloatCssPositionToTable(a,this._$editTemplatePageContainer,e)),h=this.getClassFormat(this._videoClass),d=a.closest(h),c.find("td").append(d.length>0?d.clone():a.clone()),"left"===e?c.find("tr").append(b):c.find("tr").prepend(b),d.length>0?d.after(c):a.after(c),d.length>0?d.remove():a.remove()},b.prototype.convertImgFloatCssPositionToTable=function(a,b,c){var d,e,f,g,h,i,j;return i=15,j="left"===c?"0 0 0 "+i+"px":"0 "+i+"px 0 0",e=$(""),e.css({border:"none",padding:j,margin:"0"}),f=a.closest(this.getClassFormat(this._videoClass)),d=f.length>0?f.next().clone():a.next().clone(),h=parseFloat(b.find("#content_table").width()),g=h-parseFloat(a.attr("width"))-i,d.css("width",g),e.attr("width",g),e.append(d),f.length>0?f.next().remove():a.next().remove(),e},b.prototype._convertTextCssPositionToTable=function(){return this._$editTemplatePageContainer.find(this._textContainerTags.join(", ")).each(function(a){return function(b,c){var d,e;switch(e=$(c).attr("class")?$(c).attr("class").split(" "):[]){case e.indexOf("text-left")>-1:d=a.getElementPosition("left","","");break;case e.indexOf("text-right")>-1:d=a.getElementPosition("right","","");break;case e.indexOf("text-center")>-1:d=a.getElementPosition("center","","");break;default:d=a.getElementPosition("center","","")}return a.adjustListCssProperties($(c)),d.find("td").append($(c).clone()),$(c).after(d),$(c).remove()}}(this))},b.prototype.adjustListCssProperties=function(a){if(a.is("ul")||a.is("ol"))return a.css({margin:"10px",padding:"15px"})},b.prototype._fixMargin=function(){return $(this._htmlDoc).find("article").css("margin","")},b.prototype._getNumberValue=function(a,b){return null==b&&(b=-2),parseInt(a.slice(0,a.length-b))},b.prototype._getPaddingValue=function(a){var b,c;return null==a&&(a=!0),c=!0===a?"padding--left":"padding--top",b=$(this._htmlDoc).find(".article__content p").first().css(c),this._getNumberValue(b)},b.prototype._fixContentWidth=function(){var a;return a=this._getPaddingValue(),$(this._htmlDoc).find("article").find(this._textContainerTags.join(", ")).each(function(b){return function(c,d){var e;return e=b._getNumberValue($(d).css("width")),$(d).css({width:e-2*a})}}(this))},b.prototype._eraseVerticalSpaceContent=function(){return this._$editTemplatePageContainer.find(this.getClassFormat(this._verticalSpaceClassName)).each(function(a){return function(a,b){return $(b).find("center").remove()}}())},b}(b)}).call(this); \ No newline at end of file +(function(){var a,b,c,d,e,f,g={}.hasOwnProperty,h=function(a,b){function c(){this.constructor=a}for(var d in b)g.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},i=function(a,b){return function(){return a.apply(b,arguments)}};b=function(){function a(){}return a.prototype.getIdFormat=function(a){return"#"+a},a.prototype.getClassFormat=function(a){return"."+a},a.prototype.getNameFormat=function(a,b){return a+"[name='"+b+"']"},a}(),$(document).ready(function(){var a,b,c,e;return a=500,c=750,e=3e3,b=!0,new d("edit_template_dialog","edit_template",a,"id_template_name","id_template_type","id_hidden_template","field-template","field-template_textarea","id_template_textarea","template_page","id_template",c,b,e,"edit_template_btn"),new d("edit_template_en_dialog","edit_template_en",a,"id_template_en_name","id_template_en_type","id_hidden_template_en","field-template_en","field-template_en_textarea","id_template_en_textarea","template_en_page","id_template_en",c,b,e,"edit_template_en_btn")}),e=function(b){function c(a,b,c,d,e,f,g,h,i){this._title=a,this._editTemplateModalDialogId=c,this._backdrop=b,this._$templateNameField=d,this._destroyDelay=e,this._$mountElement=f,this._popoverModalDialogEditTemplateLinkElementId=g,this._$editTemplateModalDialogPageContainer=h,this._$hiddenTemplateField=i,this._$templateDivField=f}return h(c,b),c.editTemplateLinkText=gettext("Edit template"),c.prototype.show=function(b){return b.click(function(b){return function(c){var d,e,f,g;return c.preventDefault(),f=b._$templateNameField.find("option:selected"),e=f.text(),g=f.val().split(":"),d=new a(b._editTemplateModalDialogId,b._popoverModalDialogEditTemplateLinkElementId,b._$editTemplateModalDialogPageContainer,b._$hiddenTemplateField,b._$templateDivField),d.mount(e,"openViaPopoverDialog",g),b.unmount()}}(this)),this.delay(this._destroyDelay,function(a){return function(){return a.unmount()}}(this))},c.prototype.mount=function(a,b){var c;return c=this._getOpt(),c.offsetTop=a,c.offsetLeft=b,WebuiPopovers.show(this._$mountElement,c)},c.prototype.unmount=function(){return this._$mountElement.webuiPopover("hide"),this._$mountElement.webuiPopover("destroy")},c.prototype.delay=function(a,b){return setTimeout(b,a)},c.getContent=function(a,b){var c,d;return c=$(""),c.attr({id:this.dialogId,href:""}),d={id:a,"data-type":"modal","data-target":b,"data-fullscreen":"true","data-overlayClick":"true"},c.attr(d),c.html(""+this.editTemplateLinkText+""),c},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b){return a.show(b)}}(this),a=c.getContent(this._popoverModalDialogEditTemplateLinkElementId,this.getIdFormat(this._editTemplateModalDialogId)),{placement:"top-right",trigger:"manual,",title:this._title,content:a,offsetTop:0,offsetLeft:0,animation:"fade",backdrop:this._backdrop,onShow:b}},c}(b),c=function(b){function c(a,b,c,d,e,f,g,h){this._getBtns=i(this._getBtns,this),this._confirmTemplateName=i(this._confirmTemplateName,this),this._dialogId="get_template_name",this._title=gettext("Template name"),this._showDelay=a,this._confirmBtnId="confirm_btn",this._$templateDivField=b,this._templateNameInputId="template_name",this._errorListCSSClass="errorlist",this._templateNameFieldId=c,this._newEmptyTemplateName=null,this._editTemplateModalDialogId=d,this._popoverModalDialogEditTemplateLinkElementId=e,this._templateName=f,this._$editTemplateModalDialogPageContainer=g,this._$hiddenTemplateField=h}return h(c,b),c.prototype.unmount=function(){return $(this.getIdFormat(this._dialogId)).dialog("destroy")},c.prototype.mount=function(){return this._getContainer().dialog(this._getOpt())},c.prototype._getContainer=function(){var a,b,c,d,e,f;return e=$("
    "),e.attr({id:this._dialogId}),f=$(""),f.attr({id:"template_name_form"}),a=$("
    "),d=$("
      "),d.attr({class:this._errorListCSSClass}),c=$(""),c.attr({for:this._templateNameInputId}),c.text(gettext("Template name")),b=$(""),b.attr({id:this._templateNameInputId,name:this._templateNameInputId}),a.append(c),a.append(b),a.append(d),e.append(f.append(a)),e},c.prototype.getNeEmptyTemplateName=function(){return this._newEmptyTemplateName},c.prototype._triggerConfirmBtn=function(){var a;return a=function(a){return function(b){if(b.keyCode===$.ui.keyCode.ENTER)return $(a.getIdFormat(a._confirmBtnId)).click()}}(this),$(this.getIdFormat(this._dialogId)).keypress(a)},c.prototype._checkRegexp=function(a,b,c){var d;return!!b.test(a.val())||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
    • "+c+"
    • "),!1)},c.prototype._checkTemplateName=function(a,b,c){var d,e,f;return e=[],f=this.getIdFormat(this._templateNameFieldId)+" > option",$(f).each(function(){return e.push($(this).text())}),!(e.indexOf(b)>-1)||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
    • "+c+"
    • "),!1)},c.prototype._validateTemplateName=function(){var a,b,c,d;return d=!0,a=$(this.getIdFormat(this._templateNameInputId)),$(this.getClassFormat(this._errorListCSSClass)).html(""),c=gettext("Template name may consist of a-z, 0-9, underscores, not spaces and must begin with a letter, and be lowercase."),d=d&&this._checkRegexp(a,/^[a-z]([0-9a-z_])+$/,c),c=gettext("Template name exist."),b=$(this.getIdFormat(this._templateNameInputId)),d=d&&this._checkTemplateName(b,a.val(),c)},c.prototype._confirmTemplateName=function(){var b,c;if(this._validateTemplateName())return this._newEmptyTemplateName=$(this.getIdFormat(this._templateNameInputId)).val(),this.unmount(),this._setNewEmptyTemplateName(),c=[this._newEmptyTemplateName],b=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),b.mount(this._templateName,"openViaSelectBox",c)},c.prototype._setNewEmptyTemplateName=function(){if(null!=this._newEmptyTemplateName)return sessionStorage.setItem("newEmptyTemplateName",this._newEmptyTemplateName)},c.prototype._getBtns=function(){var a,b;return a={},b=function(a){return function(){return a._confirmTemplateName()}}(this),a.ok={text:gettext("Ok"),id:this._confirmBtnId,click:b},a},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b,c){return a._triggerConfirmBtn()}}(this),a=function(a){return function(b,c){return a.unmount(),a._$templateDivField.html("")}}(this),{modal:!0,title:this._title,show:{effect:"fade",delay:this._showDelay},resizable:!1,open:b,beforeClose:a,buttons:this._getBtns()}},c}(b),a=function(a){function b(a,b,c,d,e){this._closeEditTemplateModalDialog=i(this._closeEditTemplateModalDialog,this),this._dialogId=a,this._sessionStorageKey="dbTemplateName",this._popoverEditTemplateLinkElementId=b,this._closeBtnClass="mdl-close",this._$editTemplateModalDialogPageContainer=c,this._$hiddenTemplateField=d,this._$templateDivField=e,this._popoverDialog=null}return h(b,a),b.prototype.mount=function(a,b,c){var d,f;if(sessionStorage.removeItem(this._sessionStorageKey),this._popoverDialog||(this._popoverDialog=e.getContent(this._popoverEditTemplateLinkElementId,this.getIdFormat(this._dialogId)),$("body").append(this._popoverDialog)),this.init(this.getIdFormat(this._popoverEditTemplateLinkElementId)),c.length>1?(f=this._setPageContainerArgs(a,"aklub:get_email_template_from_db"),sessionStorage.setItem(this.sessionStorageKey,f)):f=this._setPageContainerArgs(a,"aklub:get_email_template"),this.show(this.getIdFormat(this._dialogId)),d=$(this.getIdFormat(this._dialogId)+" "+this.getClassFormat(this._closeBtnClass)),d.off("click",this._closeEditTemplateModalDialog),d.on("click",this._closeEditTemplateModalDialog),"openViaSelectBox"===b)return $(this.getIdFormat(this._popoverEditTemplateLinkElementId)).remove()},b.prototype._closeEditTemplateModalDialog=function(a){var b;return b=this._getIframeTemplateContent(),new f(b,$(b).find("article"),this._$templateDivField,this._$hiddenTemplateField)},b.prototype._getIframeTemplateContent=function(){var a;return a=document.querySelector(this.getIdFormat(this._$editTemplateModalDialogPageContainer.attr("id"))),null!=a?a.contentDocument:void 0},b.prototype._setPageContainerArgs=function(a,b){var c,d;return d=window.reverse(b,{template_name:a}),c={data:d,width:$(window).width(),height:$(window).height()},this._$editTemplateModalDialogPageContainer.attr(c),d},b.prototype.init=function(a){return $(a).mdl()},b.prototype.show=function(a){return mdl_open(a)},b}(b),d=function(b){function d(a,b,c,d,e,f,g,h,j,k,l,m,n,o,p){this._confirmTemplateEdit=i(this._confirmTemplateEdit,this),this._loadTemplateContent=i(this._loadTemplateContent,this),this._showEditTemplateModalDialog=i(this._showEditTemplateModalDialog,this),this._editTemplateBtnClickEvt=i(this._editTemplateBtnClickEvt,this),this._mouseMoveOverTemplateEvt=i(this._mouseMoveOverTemplateEvt,this),this._replaceTemplateFieldContent=i(this._replaceTemplateFieldContent,this),this._templateDivFieldId=l,this._hiddenTemplateFieldId=f,this._templateNameFieldId=d,this._getTemplateNameDialogShowDelay=c,this._editTemplateModalDialogId=a,this._popoverModalDialogEditTemplateLinkElementId=b,this._templateDivFormFieldContainerCSSClass=g,this._templateTextareaFieldId=j,this._editTemplateModalDialogPageContainerId=k,this._$templateDivFieldContainer=null,this._$templateTextareaFieldContainer=null,this._templateTextareaFieldValue="",this._showPopoverModalDialogDelay=m,this._popoverModalDialogBackDrop=n,this._popoverModalDialogDestroyDelay=o,this._templateTextAreaFormFieldContainerCSSClass=h,this._templateTypeFieldId=e,this._newEmptyTemplateName=null,this._getTemplateNameDialog=null,this._popoverDialogOpts=null,this._editTemplateBtnName=p,this._editTemplateModalDialog=null,this._cacheDom(),this._bindEvents(),this._templateFormFieldWidgetInit()}return h(d,b),d.prototype._cacheDom=function(){return this._$templateDivField=$(this.getIdFormat(this._templateDivFieldId)),this._$hiddenTemplateField=$(this.getIdFormat(this._hiddenTemplateFieldId)),this._$templateNameField=$(this.getIdFormat(this._templateNameFieldId)),this._$templateTypeField=$(this.getIdFormat(this._templateTypeFieldId)),this._$templateDivFormFieldContainer=$(this.getClassFormat(this._templateDivFormFieldContainerCSSClass)),this._$templateTextAreaFormFieldContainer=$(this.getClassFormat(this._templateTextAreaFormFieldContainerCSSClass)),this._$templateTextareaField=$(this.getIdFormat(this._templateTextareaFieldId)),this._$editTemplateModalDialogPageContainer=$(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),this._$editTemplateBtn=$(this.getNameFormat("input",this._editTemplateBtnName))},d.prototype._bindEvents=function(){return this._$templateNameField.bind("change",this._showEditTemplateModalDialog),this._$templateTypeField.bind("change",this._replaceTemplateFieldContent),this._$editTemplateModalDialogPageContainer.bind("load",this._loadTemplateContent),this._$editTemplateBtn.bind("click",this._editTemplateBtnClickEvt)},d.prototype._templateFormFieldWidgetInit=function(){var a;return this._$templateTextAreaFormFieldContainer.addClass("hidden"),"new"===this._$templateTypeField.val()?(this._exchangeFormFieldContainerContent(),a=this._$templateDivField.html(),this._$templateTextareaField.html(a),this._templateTextareaFieldValue=a,this._$templateDivField.html("")):"existed"===this._$templateTypeField.val()&&this._$hiddenTemplateField.val(this._$templateDivField.html()),this._initEditTemplateModalDialog(),this._enableDisableEditBtn(!0)},d.prototype._enableDisableEditBtn=function(a,b){var c;return null==b&&(b=!0),b?(c=this._$templateNameField.find("option:selected").val(),0===c.length?this._$editTemplateBtn.prop("disabled",a):void 0):this._$editTemplateBtn.prop("disabled",a)},d.prototype._getTemplateFieldContainer=function(){this._$templateDivFieldContainer=this._$templateDivFormFieldContainer,this._$templateTextareaFieldContainer=this._$templateTextAreaFormFieldContainer},d.prototype._checkTemplateFieldWidget=function(){var a;return this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.find("textarea"),a.length?"textarea":"div"},d.prototype._replaceTemplateFieldContent=function(a){var b;b=$(a.target).val(),"new"===b?"textarea"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContent():"existed"===b&&"div"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContentBack()},d.prototype._exchangeFormFieldContainerContentBack=function(){var a;return this._disableFormFieldWysiwygEditor(),this._$templateNameField.attr("disabled",!1),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._$editTemplateBtn.off("click",this._editTemplateBtnClickEvt),this._$editTemplateBtn.on("click",this._editTemplateBtnClickEvt)},d.prototype.delay=function(a,b){return setTimeout(b,a)},d.prototype._mouseMoveOverTemplateEvt=function(a){var b,c,d,e,f;return b=$(a.currentTarget).offset(),e=a.pageX-b.left,f=a.pageY-b.top,d=f,c=e,this.delay(this._showPopoverModalDialogDelay,function(a){return function(){return a._initPopoverModalDialog(d,c)}}(this))},d.prototype._initPopoverModalDialog=function(a,b){return this._popoverDialogOpts.mount(a,b)},d.prototype._bindPopoverDialogMouseMoveEvt=function(){return this._popoverDialogOpts=new e(gettext("Edit template"),this._popoverModalDialogBackDrop,this._editTemplateModalDialogId,this._$templateNameField,this._popoverModalDialogDestroyDelay,this._$templateDivField,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._$templateDivField.mousemove(this._mouseMoveOverTemplateEvt)},d.prototype._initEditTemplateModalDialog=function(){return this._editTemplateModalDialog=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField)},d.prototype._editTemplateBtnClickEvt=function(a){var b,c,d;return a.preventDefault(),c=this._$templateNameField.find("option:selected"),b=c.text(),d=c.val().split(":"),this._editTemplateModalDialog.mount(b,"openViaBtn",d)},d.prototype._exchangeFormFieldContainerContent=function(){var a;return this._$templateTextareaField.html(this._templateTextareaFieldValue),this._$templateNameField.attr("disabled",!0),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),this._$templateDivFieldContainer.find("label").attr("for",this._templateDivFieldId),this._$templateDivFieldContainer.find("label").text(this._$templateTextareaFieldContainer.find("label").text()),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._enableFormFieldWysiwygEditor(),this._$templateTextAreaFormFieldContainer.addClass("hidden"),this._$hiddenTemplateField.val("")},d.prototype._enableFormFieldWysiwygEditor=function(){return django_wysiwyg.enable(this._templateDivFieldId.slice(3))},d.prototype._disableFormFieldWysiwygEditor=function(){return django_wysiwyg.disable(this._templateDivFieldId.slice(3))},d.prototype._exchangeContent=function(){var a,b;this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.children(),b=this._$templateTextareaFieldContainer.children(),this._$templateDivFieldContainer.html(b),this._$templateTextareaFieldContainer.html(a)},d.prototype._showEditTemplateModalDialog=function(b){var d,e,f,g;if(sessionStorage.removeItem("newEmptyTemplateName"),e=""+this.getIdFormat(b.target.id),f=$(e+" option:selected").text(),$(""+e).val().length>0)return"new_empty_template"===f?(this._getTemplateNameDialog=new c(this._getTemplateNameDialogShowDelay,this._$templateDivField,this._templateNameFieldId,this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,f,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._getTemplateNameDialog.mount()):(this._enableDisableEditBtn(!1,!1),g=this._$templateNameField.find("option:selected").val().split(":"),d=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),d.mount(f,"openViaSelectBox",g))},d.prototype._loadTemplateContent=function(a){var b;if(this._newEmptyTemplateName=null!=(b=this._getTemplateNameDialog)?b.getNeEmptyTemplateName():void 0,null!=this._newEmptyTemplateName)return this._addEditTemplateConfirmEvent()},d.prototype._addEditTemplateConfirmEvent=function(){var a,b,c,d;return b="ct-ignition__button--confirm",d=document.querySelector(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),c=d.contentDocument,a=$(c).find(this.getClassFormat(b)),a.unbind("click"),a.bind("click",this._confirmTemplateEdit)},d.prototype._confirmTemplateEdit=function(){if(null!=this._newEmptyTemplateName)return this._addNewSelectOpt(this._newEmptyTemplateName,this._$templateNameField),this._eraseNewTemplateName()},d.prototype._eraseNewTemplateName=function(){return this._newEmptyTemplateName=null},d.prototype._addNewSelectOpt=function(a,b){var c,d,e,f;if(d=$(this.getIdFormat(this._templateNameFieldId)+" option"),f=$.map(d,function(a){return a.value}),e="new_empty_template:"+a,-1===f.indexOf(e))return c=new Option(a,e),b.append(c),b.val(e)},d}(b),f=function(a){function b(a,b,c,d){this._htmlDoc=a,this._$editTemplatePageContainer=b,this._$templateDivField=c,this._$hiddenTemplateField=d,this._videoClass="video",this._verticalSpaceClassName="vertical-space",this._textContainerTags=["p","ul","ol","h1","h2","h3","h4","h5","h6"],this.convertVideoIframeToImgThumbnail()}return h(b,a),b.prototype.convertVideoIframeToImgThumbnail=function(){var a,b,c,d,e,f;return c=[],b=!1,f=new RegExp("youtube.com.*(v=|/embed/)(.{11})"),e=new RegExp("vimeo.com.*./(.*)"),a=function(a,b,c,d,e){return a.css("float",e),b.append(a),b.append(c),$(d).after(b),$(d).remove()},d=function(b){var c;return"vimeo"===this.videoType&&(c={src:b.thumbnail_url,width:b.thumbnail_width,height:b.thumbnail_height},this.$videoThumbnail.attr(c)),this.$videoCaption.text(b.title),a(this.$videoThumbnail,this.$clickableVideoThumbnail,this.$videoCaption,this.element,this.iframeCssFloatProperty)},$(this._htmlDoc).find("iframe").each(function(a){return function(g,h){var i,j,k,l,m,n,o,p,q,r,s,t,u;return p=$(h).attr("src"),n=$(h).css("float"),q=$(h).attr("width"),o=$(h).attr("height"),i=$(''),i.attr("href",p),k=$(''),k.attr({width:q,height:o}),j=$("
      "),m={"text-align":"center","font-size":"12px"},j.css(m),p.match(f)?(b=!0,t=p.match(f).pop(),i.attr("id",t),i.addClass("youtube"),u="//img.youtube.com/vi/"+t+"/0.jpg",k.attr("src",u),r="http://www.youtube.com/watch?v="+t+"&",s="https://noembed.com/embed?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"youtube"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):p.match(e)?(b=!0,t=p.match(e).pop(),i.attr("id",t),i.addClass("vimeo"),r="https://vimeo.com/"+t+"& width="+q+"&height="+o,s="https://vimeo.com/api/oembed.json?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"vimeo"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):void 0}}(this)),$.when.apply(null,c).done(function(a){return function(){return a.convertCssToInlineStyle()}}(this))},b.prototype.getAjaxData=function(a,b,c,d,e){return null==d&&(d="GET"),null==e&&(e="json"),{type:d,url:b,jsonp:"callback",dataType:e,context:a,success:c}},b.prototype.convertCssToInlineStyle=function(){return this.inlineStyler($(this._htmlDoc)),this._fixContentWidth(),this._fixMargin(),this._replaceImgSrc(),this._convertTextCssPositionToTable(),this._eraseVerticalSpaceContent(),this._$templateDivField.html(this._$editTemplatePageContainer),this._$hiddenTemplateField.val(this._$editTemplatePageContainer.html())},b.prototype.inlineStyler=function(a){return a.inlineStyler()},b.prototype.getElementPosition=function(a,b,c){var d,e,f,g;return null==b&&(b=""),null==c&&(c=""),d=$("
      +
      Date: Tue, 18 Feb 2020 07:48:48 +0100 Subject: [PATCH 125/189] fix: update load html email template blacklist filter --- apps/aklub/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index 4adc05261..c6b27ea81 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -138,7 +138,8 @@ def get_email_templates_names(): templates_blacklist = [ 'base.html', 'new_empty_template.html', - 'footer', + 'footer.html', + 'automat_fonts.html', ] def get_templates_files(): From 79a63d509b1925ba48ece973588b8b393bd215a8 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Tue, 18 Feb 2020 14:03:27 +0100 Subject: [PATCH 126/189] fix: set correct background image container height according the uploaded image height --- .../src/scripts/background-image-tool.coffee | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee index cc2937687..249b43647 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee @@ -117,6 +117,20 @@ class BackgroundImageDialog extends SuperClass @dispatchEvent(@createEvent('imageuploader.mount')) + save: (imageURL, imageSize, imageAttrs) -> + # Save and insert the current image + # Set correct container height according uploaded image + document.getElementsByClassName('content-table')[0].setAttribute('height', "#{ imageSize[1] }") + @dispatchEvent( + @createEvent( + 'save', + { + 'imageURL': imageURL, + 'imageSize': imageSize, + 'imageAttrs': imageAttrs + }) + ) + _addDOMEventListeners: () -> # Add event listeners for the widget From 34e4a6af8bfe777cb4d780fbfd76e77c5c1570f9 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Thu, 27 Feb 2020 16:00:31 +0100 Subject: [PATCH 127/189] feat: add increase/decrease font size ContentTools tool --- .../src/styles/ui/_custom-toolbox.scss | 6 +- .../aklub/fonts/contenttools/icons.woff | Bin 5584 -> 5768 bytes .../aklub/js/contenttools/Gruntfile.coffee | 4 +- .../scripts/decrease-font-size-tool.coffee | 201 ++++++++++++++++++ .../scripts/increase-font-size-tool.coffee | 200 +++++++++++++++++ 5 files changed, 408 insertions(+), 3 deletions(-) create mode 100644 apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee create mode 100644 apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee diff --git a/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss index 6e76787b5..9ec968da3 100644 --- a/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss +++ b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss @@ -9,9 +9,11 @@ &--align-justify:before { content: "\ea7a"; } &--background-image:before { content: "\e90e"; } &--vertical-space:before { content: "\ea72"; } + &--increase-font-size:before { content: "\ea0a"; } + &--decrease-font-size:before { content: "\ea0b"; } } } // Color picker dialog .cp-app { - z-index: 10010; -} \ No newline at end of file + z-index: 10010; +} diff --git a/apps/aklub/static/aklub/fonts/contenttools/icons.woff b/apps/aklub/static/aklub/fonts/contenttools/icons.woff index 2711f6f190a7731d2fb0eb0bbcf3f79d7a4ce74b..328320d8774c053f767514e107da46aaf12e07da 100644 GIT binary patch delta 565 zcmcbh-JvT|?(gQtz{mguVjT?JVA^J~B9p{K9r^k>;VG< zV;cz1^y2(okY5bcD3&qtPq*>~ph5$naux;wpvM$mvcBYg$@kLlW!lRrFW0=>{qpoH z?#U|{4H>y6-(b|2WEW!*{Vuv+bgSqV(bb|WMVE>$7VTkRW?%#cg3e?ECf3cqOnjX6 z4F3_Ir+&Tbq9ca4-obUGxD*@O{@S~ECRAr0gR0p((@8?Q-NX{3=E8I zAUxBH^LIghaS6}|8z%ngRy+Yzs0UQe!oUagn8Hiem)tM;Ui!UEdpYIhnwPs@o}Rpl z(QxuzMm=FB(XXO=MK_CX5?v{}Ty(MMLeZ|t3QWeELz(zEH?#A!vdb{X&wm}yZ}XLb zn+0ep1H-MjC+}hO~0h join: true build: - # options: + # options: # sourceMap: true files: 'src/tmp/content-tools.js': [ @@ -51,6 +51,8 @@ module.exports = (grunt) -> 'src/scripts/button-tool.coffee' 'src/scripts/background-image-tool.coffee' 'src/scripts/vertical-space-tool.coffee' + 'src/scripts/increase-font-size-tool.coffee' + 'src/scripts/decrease-font-size-tool.coffee' ] sandbox: diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee new file mode 100644 index 000000000..50ee8e146 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee @@ -0,0 +1,201 @@ +class DecreaseFontSizeTool extends ContentTools.Tools.Bold + + # Apply a class to justify align the contents of the current text block. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'decrease-font-size') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Decrease font size' + @icon = 'decrease-font-size' + @tagName = 'span' + @showOnce = false + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + element.storeState() + + IncreaseFontSizeTool.showOnce = false + + if not @showOnce + # Add a fake selection wrapper to the selected text so that it + # appears to be + # selected when the focus is lost by the element. + selectTag = new HTMLString.Tag 'span', {'class': 'ct--pseudo-select'} + [from, to] = selection.get() + + element.content = element.content.format from, to, selectTag + element.updateInnerHTML() + + # Measure a rectangle of the content selected so we can position the + # dialog centrally to it. + domElement = element.domElement() + measureSpan = domElement.getElementsByClassName 'ct--pseudo-select' + rect = measureSpan[0].getBoundingClientRect() + + element.content = element.content.unformat from, to, selectTag + element.updateInnerHTML() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Modal + modal = new ContentTools.ModalUI() + + # Dialog + size = if IncreaseFontSizeTool._fontSizeFactor? then \ + IncreaseFontSizeTool._fontSizeFactor else null + dialog = new GetDecreaseFontSizeValueDialog(size) + dialog.position @getDialogPosition rect + + # Listen for save events against the dialog + dialog.addEventListener 'save', (ev) => + + # Get color value + @_fontSizeFactor = ev.detail().size + + modal.hide() + dialog.hide() + + @_apply element, selection + + # Trigger the callback + callback(true) + + app.attach modal + app.attach dialog + + modal.show() + dialog.show() + else + @_apply element, selection + + callback(true) + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + @getNewFontSize: (styleClass) => + defaultFontSize = styleClass.getPropertyValue 'font-size' + defaultFontSize = parseInt \ + defaultFontSize.slice 0, defaultFontSize.length - 2 + newFontSize = defaultFontSize - parseInt(@_fontSizeFactor) + + @setNewCSS: (fontSizeId, fontSizeCSSClass) -> + style = document.createElement('style') + style.type = 'text/css' + style.id = fontSizeId + style.innerHTML = fontSizeCSSClass + document.getElementsByTagName('head')[0].appendChild(style) + + @index: (elementRef) -> + nodes = Array.prototype.slice.call \ + document.getElementsByClassName('[ article__content ]')[0].children + return nodes.indexOf elementRef + + @_apply: (element, selection) => + + elementIndex = @index element.domElement() + [from, to] = selection.get() + fontSizeCSSClassName = "font-size-#{ elementIndex }-#{ from }-#{ to }" + fontSizeId = "font_size_#{ elementIndex }_#{ from }_#{ to }" + + styleClass = window.getComputedStyle element.domElement() + @_newFontSize = @getNewFontSize(styleClass) + + if @isApplied element, selection + style = document.getElementById(fontSizeId) + + styleClass = window.getComputedStyle \ + document.getElementsByClassName(fontSizeCSSClassName)[0] + @_newFontSize = @getNewFontSize(styleClass) + + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_newFontSize }px; + } + """ + style.innerHTML = fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + @_newFontSize -= parseInt @_fontSizeFactor + + @showOnce = true + + else + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_newFontSize }px; + } + """ + + @setNewCSS fontSizeId, fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + + @showOnce = true + + element.content.optimize() + element.updateInnerHTML() + element.taint() + + element.restoreState() + + +class GetDecreaseFontSizeValueDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a padding css prop value + + constructor: (@size) -> + super() + + @_defDecreaseFontSizeValue = '' + + mount: () -> + super() + + # Update the name and placeholder for the input field provided by the + # dialog. + + @size = if @size? then @size else @_defDecreaseFontSizeValue + @_domInput.setAttribute 'name', 'size' + @_domInput.setAttribute 'value', @size + @_domInput.setAttribute 'placeholder', ContentEdit._ \ + 'Enter a font size deacrease factor (px)' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the padding. + detail = { + size: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +ContentTools.DEFAULT_TOOLS[0].push('decrease-font-size') diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee new file mode 100644 index 000000000..6e7f507a9 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee @@ -0,0 +1,200 @@ +class IncreaseFontSizeTool extends ContentTools.Tools.Bold + + # Apply a class to justify align the contents of the current text block. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'increase-font-size') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Increase font size' + @icon = 'increase-font-size' + @tagName = 'span' + @showOnce = false + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + element.storeState() + + DecreaseFontSizeTool.showOnce = false + + if not @showOnce + # Add a fake selection wrapper to the selected text so that it + # appears to be + # selected when the focus is lost by the element. + selectTag = new HTMLString.Tag 'span', {'class': 'ct--pseudo-select'} + [from, to] = selection.get() + + element.content = element.content.format from, to, selectTag + element.updateInnerHTML() + + # Measure a rectangle of the content selected so we can position the + # dialog centrally to it. + domElement = element.domElement() + measureSpan = domElement.getElementsByClassName 'ct--pseudo-select' + rect = measureSpan[0].getBoundingClientRect() + + element.content = element.content.unformat from, to, selectTag + element.updateInnerHTML() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Modal + modal = new ContentTools.ModalUI() + + # Dialog + size = if DecreaseFontSizeTool._fontSizeFactor? \ + then DecreaseFontSizeTool._fontSizeFactor else null + dialog = new GetIncreaseFontSizeValueDialog(size) + dialog.position @getDialogPosition rect + + # Listen for save events against the dialog + dialog.addEventListener 'save', (ev) => + + # Get color value + @_fontSizeFactor = ev.detail().size + + modal.hide() + dialog.hide() + + @_apply element, selection + + # Trigger the callback + callback(true) + + app.attach modal + app.attach dialog + + modal.show() + dialog.show() + else + @_apply element, selection + + callback(true) + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + @getNewFontSize: (styleClass) => + defaultFontSize = styleClass.getPropertyValue 'font-size' + defaultFontSize = parseInt \ + defaultFontSize.slice 0, defaultFontSize.length - 2 + newFontSize = defaultFontSize + parseInt @_fontSizeFactor + + @setNewCSS: (fontSizeId, fontSizeCSSClass) -> + style = document.createElement('style') + style.type = 'text/css' + style.id = fontSizeId + style.innerHTML = fontSizeCSSClass + document.getElementsByTagName('head')[0].appendChild(style) + + @index: (elementRef) -> + nodes = Array.prototype.slice.call \ + document.getElementsByClassName('[ article__content ]')[0].children + return nodes.indexOf elementRef + + @_apply: (element, selection) => + + elementIndex = @index element.domElement() + [from, to] = selection.get() + fontSizeCSSClassName = "font-size-#{ elementIndex }-#{ from }-#{ to }" + fontSizeId = "font_size_#{ elementIndex }_#{ from }_#{ to }" + + styleClass = window.getComputedStyle element.domElement() + @_newFontSize = @getNewFontSize styleClass + + if @isApplied element, selection + + style = document.getElementById fontSizeId + + styleClass = window.getComputedStyle \ + document.getElementsByClassName(fontSizeCSSClassName)[0] + @_newFontSize = @getNewFontSize styleClass + + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_newFontSize }px; + } + """ + style.innerHTML = fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + @_newFontSize += parseInt @_fontSizeFactor + + @showOnce = true + + else + + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_newFontSize }px; + } + """ + + @setNewCSS fontSizeId, fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + + @showOnce = true + + element.content.optimize() + element.updateInnerHTML() + element.taint() + + element.restoreState() + + +class GetIncreaseFontSizeValueDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a padding css prop value + + constructor: (@size) -> + super() + + @_defIncreaseFontSizeValue = '1' + + mount: () -> + super() + + @size = if @size? then @size else @_defIncreaseFontSizeValue + @_domInput.setAttribute 'name', 'size' + @_domInput.setAttribute 'value', @size + @_domInput.setAttribute 'placeholder', ContentEdit._ \ + 'Enter a font size increase factor (px)' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the padding. + detail = { + size: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +ContentTools.DEFAULT_TOOLS[0].push('increase-font-size') From 623eeb70891e851f2a30d6f707821771004c4033 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sun, 1 Mar 2020 20:05:15 +0100 Subject: [PATCH 128/189] feat: add font size ContentTools tool --- .../src/styles/ui/_custom-toolbox.scss | 1 + .../aklub/fonts/contenttools/icons.woff | Bin 5768 -> 5864 bytes .../aklub/js/contenttools/Gruntfile.coffee | 1 + .../src/scripts/font-size-tool.coffee | 170 ++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee diff --git a/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss index 9ec968da3..99063b3c4 100644 --- a/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss +++ b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss @@ -9,6 +9,7 @@ &--align-justify:before { content: "\ea7a"; } &--background-image:before { content: "\e90e"; } &--vertical-space:before { content: "\ea72"; } + &--font-size:before { content: "\ea5f"; } &--increase-font-size:before { content: "\ea0a"; } &--decrease-font-size:before { content: "\ea0b"; } } diff --git a/apps/aklub/static/aklub/fonts/contenttools/icons.woff b/apps/aklub/static/aklub/fonts/contenttools/icons.woff index 328320d8774c053f767514e107da46aaf12e07da..c13a52631d9262865da65d6dd70b60e528849c31 100644 GIT binary patch delta 477 zcmeCseW5E-?(gQtz{mguVlNoD!StNThD;I@b>!>60J&eR^?v+FPb@BAU|@;?@;RVb zAU&rt4Jekvz#v!w!Ug^=A2L!CfqdaRKs9C{EXmk#GXp5dz#t+5Gv}2<&>9eUhaN*`W5#p zfmd>`!e7Nt-oR+g7(e+Dqapte(QTqzMc0U~5?v;`M6`FZ0+Z?HU?w|mKPI4YK(m+| z7&sZk85mTJ+0~5&mCePKRRz_R1&u`&O@Ul?LFR^r1q;CVKZC=8hJOtW2OJI@IKadJ zB>por{B>|}U^>vixVfMAFRM&*rperRew(ih+$$IQTd`I0`uS0QE2eu^K}v10P5~0~0Xngf~AH HS;`0iD=mN5 delta 395 zcmaE%+o3B`?(gQtz{mguVjT?JVA^J~A(O;J9r^k>;VG< zV;cz1^y2(okY5bcD3&qtPq*d;ph5$naux;wpvM$mvcBYg$@kLlW!lRrFW0=>{qpoH z?pFe@zvTVR zDsz3Z=l^(qo39MqEI{)a7;e>yNy6w!BIb;$lPyHzC(jm1)4s#{h0TbqitQD<5W5w7 k3VRp(4)$jp3LG9BQ@{bO%8<&y2R4HVXpit_XVImM030M}AOHXW diff --git a/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee index e812f4723..d3ce3c019 100644 --- a/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee +++ b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee @@ -51,6 +51,7 @@ module.exports = (grunt) -> 'src/scripts/button-tool.coffee' 'src/scripts/background-image-tool.coffee' 'src/scripts/vertical-space-tool.coffee' + 'src/scripts/font-size-tool.coffee' 'src/scripts/increase-font-size-tool.coffee' 'src/scripts/decrease-font-size-tool.coffee' ] diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee new file mode 100644 index 000000000..d45a02ac6 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee @@ -0,0 +1,170 @@ +class FontSizeTool extends ContentTools.Tools.Bold + + # Apply a class to justify align the contents of the current text block. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'font-size') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Font size' + @icon = 'font-size' + @tagName = 'span' + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + element.storeState() + + # Add a fake selection wrapper to the selected text so that it + # appears to be + # selected when the focus is lost by the element. + selectTag = new HTMLString.Tag 'span', {'class': 'ct--pseudo-select'} + [from, to] = selection.get() + + element.content = element.content.format from, to, selectTag + element.updateInnerHTML() + + # Measure a rectangle of the content selected so we can position the + # dialog centrally to it. + domElement = element.domElement() + measureSpan = domElement.getElementsByClassName 'ct--pseudo-select' + rect = measureSpan[0].getBoundingClientRect() + + element.content = element.content.unformat from, to, selectTag + element.updateInnerHTML() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Modal + modal = new ContentTools.ModalUI() + + # Dialog + if not @isApplied element, selection + styleClass = window.getComputedStyle element.domElement() + fontSize = @getFontSize styleClass + else + fontSize = @_fontSize + + dialog = new GetIncreaseFontSizeValueDialog(fontSize) + dialog.position @getDialogPosition rect + + # Listen for save events against the dialog + dialog.addEventListener 'save', (ev) => + + # Get color value + @_fontSize = ev.detail().size + + modal.hide() + dialog.hide() + + @_apply element, selection + + # Trigger the callback + callback(true) + + app.attach modal + app.attach dialog + + modal.show() + dialog.show() + + callback(true) + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + @getFontSize: (styleClass) -> + defaultFontSize = styleClass.getPropertyValue 'font-size' + defaultFontSize = parseInt \ + defaultFontSize.slice 0, defaultFontSize.length - 2 + + @setNewCSS: (fontSizeId, fontSizeCSSClass) -> + style = document.createElement('style') + style.type = 'text/css' + style.id = fontSizeId + style.innerHTML = fontSizeCSSClass + document.getElementsByTagName('head')[0].appendChild(style) + + @index: (elementRef) -> + nodes = Array.prototype.slice.call \ + document.getElementsByClassName('[ article__content ]')[0].children + return nodes.indexOf elementRef + + @_apply: (element, selection) => + + elementIndex = @index element.domElement() + [from, to] = selection.get() + fontSizeCSSClassName = "font-size-#{ elementIndex }-#{ from }-#{ to }" + fontSizeId = "font_size_#{ elementIndex }_#{ from }_#{ to }" + + fontSizeCSSClass = """ + .#{ fontSizeCSSClassName } { + font-size: #{ @_fontSize }px; + } + """ + + if @isApplied element, selection + style = document.getElementById fontSizeId + style.innerHTML = fontSizeCSSClass + else + @setNewCSS fontSizeId, fontSizeCSSClass + + element.content = element.content.format( + from, + to, + new HTMLString.Tag @tagName, {'class': fontSizeCSSClassName} + ) + + element.content.optimize() + element.updateInnerHTML() + element.taint() + + element.restoreState() + + +class FontSizeValueDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a padding css prop value + + constructor: (@size) -> + super() + + @_defFontSizeValue = '14' + + mount: () -> + super() + + @size = if @size? then @size else @_defFontSizeValue + @_domInput.setAttribute 'name', 'size' + @_domInput.setAttribute 'value', @size + @_domInput.setAttribute 'placeholder', ContentEdit._ \ + 'Enter a font size (px)' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the padding. + detail = { + size: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +ContentTools.DEFAULT_TOOLS[0].push('font-size') From 484b2e8141555a93ca1b2a14fddb54f376ad97ff Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sun, 1 Mar 2020 20:29:18 +0100 Subject: [PATCH 129/189] fix: font size/increare/decrease font size ContentTools tools * add same css class only once --- .../src/scripts/decrease-font-size-tool.coffee | 7 ++++++- .../js/contenttools/src/scripts/font-size-tool.coffee | 4 ++-- .../src/scripts/increase-font-size-tool.coffee | 6 +++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee index 50ee8e146..fa5484cdd 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee @@ -143,13 +143,18 @@ class DecreaseFontSizeTool extends ContentTools.Tools.Bold @showOnce = true else + fontSizeCSSClass = """ .#{ fontSizeCSSClassName } { font-size: #{ @_newFontSize }px; } """ - @setNewCSS fontSizeId, fontSizeCSSClass + style = document.getElementById fontSizeId + if style? + style.innerHTML = fontSizeCSSClass + else + @setNewCSS fontSizeId, fontSizeCSSClass element.content = element.content.format( from, diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee index d45a02ac6..111b82e0e 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee @@ -119,8 +119,8 @@ class FontSizeTool extends ContentTools.Tools.Bold } """ - if @isApplied element, selection - style = document.getElementById fontSizeId + style = document.getElementById fontSizeId + if style? style.innerHTML = fontSizeCSSClass else @setNewCSS fontSizeId, fontSizeCSSClass diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee index 6e7f507a9..a07c20069 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee @@ -151,7 +151,11 @@ class IncreaseFontSizeTool extends ContentTools.Tools.Bold } """ - @setNewCSS fontSizeId, fontSizeCSSClass + style = document.getElementById fontSizeId + if style? + style.innerHTML = fontSizeCSSClass + else + @setNewCSS fontSizeId, fontSizeCSSClass element.content = element.content.format( from, From 6f4576c11d91d87c67dd5643fddad43a200a4998 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sun, 1 Mar 2020 21:52:42 +0100 Subject: [PATCH 130/189] feat: add line spacing ContentTools tool --- .../src/styles/ui/_custom-toolbox.scss | 1 + .../aklub/fonts/contenttools/icons.woff | Bin 5864 -> 6008 bytes .../aklub/js/contenttools/Gruntfile.coffee | 1 + .../src/scripts/line-spacing-tool.coffee | 143 ++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee diff --git a/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss index 99063b3c4..18d9292e3 100644 --- a/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss +++ b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss @@ -12,6 +12,7 @@ &--font-size:before { content: "\ea5f"; } &--increase-font-size:before { content: "\ea0a"; } &--decrease-font-size:before { content: "\ea0b"; } + &--line-spacing:before { content: "\ea72"; } } } // Color picker dialog diff --git a/apps/aklub/static/aklub/fonts/contenttools/icons.woff b/apps/aklub/static/aklub/fonts/contenttools/icons.woff index c13a52631d9262865da65d6dd70b60e528849c31..762aeb1ea73e4e2ca54d0496dc406dfdd3ffa669 100644 GIT binary patch delta 501 zcmYk3PbdUY9LIleX6%esW;FBiw^+N9T3amEL76Cry>O5m{G06BQM+tWRw<+8KnZhF z4%%#zly=K`l7o|+_vEk#xhYDSU4F9>uRimA-*4aE>-Xll^CO_FjgE`}0bI2yaF^9@ zb}@B_{^m^^tu0Q7k(joU1YjcQg&(9?e0>44I6w{dYV2Fz_KqIau%qbM>2s(jg?Al8 z%udn!9E3FYtS44enBAd2aWD|} z0J7t79IpIKF0ZWO1e1sMp8E-lJy_>JF^@yLeg9u?gtQP^puq=!Gi6j+4YLOU2DykCe zY>S3OEcJIWYDpzl$smRe#6VR`7T~?`W#!V~!JqLD{F~4!j0w9!Rw$K!X*)!rx_kUydG delta 360 zcmeyN_d-{s+~3WOfsp|S#9lCPgXuYw4Vfe+>d4oB0dl`s>;3qVo>*MKz`zs( 'src/scripts/font-size-tool.coffee' 'src/scripts/increase-font-size-tool.coffee' 'src/scripts/decrease-font-size-tool.coffee' + 'src/scripts/line-spacing-tool.coffee' ] sandbox: diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee new file mode 100644 index 000000000..500e309c3 --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee @@ -0,0 +1,143 @@ +class LineSpacingTool extends CustomAlignLeftTool + + # Apply a class to justify align the contents of the current text block. + + # Register the tool with the toolshelf + ContentTools.ToolShelf.stow(@, 'line-spacing') + + # The tooltip and icon modifier CSS class for the tool + @label = 'Line spacing' + @icon = 'line-spacing' + @className = 'line-spacing' + + @apply: (element, selection, callback) -> + # Apply the tool to the current element + + # Dispatch `apply` event + toolDetail = { + 'tool': this, + 'element': element, + 'selection': selection + } + if not @dispatchEditorEvent('tool-apply', toolDetail) + return + + # # Measure a rectangle of the content selected so we can position the + # # dialog centrally to it. + domElement = element.domElement() + rect = domElement.getBoundingClientRect() + + # Set-up the dialog + app = ContentTools.EditorApp.get() + + # Modal + modal = new ContentTools.ModalUI() + + # List items and table cells use child nodes to manage their content + # which don't support classes, so we need to use the parent. + if element.type() in ['ListItemText', 'TableCellText'] + element = element.parent() + + dialog = new LineSpacingDialog() + dialog.position @getDialogPosition rect + + # Listen for save events against the dialog + dialog.addEventListener 'save', (ev) => + + # Get line spacing number value + @_lineSpacing = ev.detail().lineSpacing + + modal.hide() + dialog.hide() + + @_apply element, selection + + # Trigger the callback + callback(true) + + app.attach modal + app.attach dialog + + modal.show() + dialog.show() + + callback(true) + + callback(true) + + # Dispatch `applied` event + @dispatchEditorEvent('tool-applied', toolDetail) + + @getDialogPosition: (rect) -> + [ + rect.left + (rect.width / 2) + window.scrollX, + rect.top + (rect.height / 2) + window.scrollY + ] + + @setNewCSS: (fontSizeId, fontSizeCSSClass) -> + style = document.createElement('style') + style.type = 'text/css' + style.id = fontSizeId + style.innerHTML = fontSizeCSSClass + document.getElementsByTagName('head')[0].appendChild(style) + + @index: (elementRef) -> + nodes = Array.prototype.slice.call \ + document.getElementsByClassName('[ article__content ]')[0].children + return nodes.indexOf elementRef + + @_apply: (element, selection) => + + elementIndex = @index element.domElement() + [from, to] = selection.get() + lineSpacingCSSClassName = "line-spacing-#{ elementIndex }" + lineSpacingId = "line_spacing_#{ elementIndex }" + + lineSpacingCSSClass = """ + .article__content .#{ lineSpacingCSSClassName } { + line-height: #{ @_lineSpacing }; + } + """ + + style = document.getElementById lineSpacingId + if style? + style.innerHTML = lineSpacingCSSClass + else + @setNewCSS lineSpacingId, lineSpacingCSSClass + + # Add the alignment class to the element + element.addCSSClass(lineSpacingCSSClassName) + + +class LineSpacingDialog extends ContentTools.LinkDialog + + # An anchored dialog to support inserting/modifying a line spacing + # css prop value + + constructor: (@lineSpacing) -> + super() + + @_defLineSpacingValue = 1.0 + + mount: () -> + super() + + @lineSpacing = if @lineSpacing? then @lineSpacing else \ + @_defLineSpacingValue + @_domInput.setAttribute 'name', 'lineSpacing' + @_domInput.setAttribute 'value', @lineSpacing + @_domInput.setAttribute 'placeholder', ContentEdit._ \ + 'Enter number to set the line height' + + # Remove the new window target DOM element + @_domElement.removeChild @_domTargetButton + + save: () -> + # Save the padding. + detail = { + lineSpacing: @_domInput.value.trim() + } + @dispatchEvent(@createEvent('save', detail)) + + +ContentTools.DEFAULT_TOOLS[0].push('line-spacing') From 6aba4bb84d539cb77926ab17235ab65ff348e18c Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sun, 1 Mar 2020 21:55:29 +0100 Subject: [PATCH 131/189] fix: change add vertical space ContentTools tool icon to the 'page-break' icon --- .../aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss index 18d9292e3..d84ec67b7 100644 --- a/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss +++ b/apps/aklub/static/aklub/css/contenttools/src/styles/ui/_custom-toolbox.scss @@ -8,7 +8,7 @@ &--button:before { content: "\ea1e"; } &--align-justify:before { content: "\ea7a"; } &--background-image:before { content: "\e90e"; } - &--vertical-space:before { content: "\ea72"; } + &--vertical-space:before { content: "\ea68"; } &--font-size:before { content: "\ea5f"; } &--increase-font-size:before { content: "\ea0a"; } &--decrease-font-size:before { content: "\ea0b"; } From e78453370c3c08e745da51b1dfe39a1499cff342 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 2 Mar 2020 10:02:43 +0100 Subject: [PATCH 132/189] style: all custom ContentTools tools * remove trailing space * line width 80 characters * comment code --- .../src/scripts/background-image-tool.coffee | 21 +-- .../src/scripts/button-tool.coffee | 124 +++++++++++------- .../scripts/decrease-font-size-tool.coffee | 21 +-- .../src/scripts/font-size-tool.coffee | 12 +- .../scripts/increase-font-size-tool.coffee | 19 +-- .../src/scripts/line-spacing-tool.coffee | 10 +- .../src/scripts/superclass.coffee | 14 +- .../src/scripts/vertical-space-tool.coffee | 26 ++-- 8 files changed, 142 insertions(+), 105 deletions(-) diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee index 249b43647..20dab3971 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/background-image-tool.coffee @@ -1,11 +1,11 @@ class BackgroundImageDialog extends SuperClass @include ContentTools.DialogUI - @include ContentTools.ImageDialog + @include ContentTools.ImageDialog constructor: () -> ContentTools.ImageDialog.call @ - + mount: () -> ContentTools.DialogUI::mount.call @ @@ -120,7 +120,9 @@ class BackgroundImageDialog extends SuperClass save: (imageURL, imageSize, imageAttrs) -> # Save and insert the current image # Set correct container height according uploaded image - document.getElementsByClassName('content-table')[0].setAttribute('height', "#{ imageSize[1] }") + document.getElementsByClassName('content-table')[0].setAttribute( + 'height', "#{ imageSize[1] }" + ) @dispatchEvent( @createEvent( 'save', @@ -162,7 +164,10 @@ class BackgroundImageDialog extends SuperClass # Set backgroundImage property ### @dispatchEvent( - @createEvent('imageuploader.fileready', {file: file, backgroundImage: true}) + @createEvent( + 'imageuploader.fileready', + {file: file, backgroundImage: true} + ) ) # Cancel upload @@ -196,8 +201,8 @@ class BackgroundImageDialog extends SuperClass _setEmailTemplateBgImage: (containerClassName) -> ### - # Set html email template container background image - ### + # Set html email template container background image + ### container = document.getElementsByClassName(containerClassName)[0] container.style.backgroundImage = "url(#{ @_imageURL })" @@ -245,7 +250,7 @@ class BackgroundImage extends ContentTools.Tools.Image dialog = new BackgroundImageDialog() # Support cancelling the dialog - dialog.addEventListener 'cancel', () => + dialog.addEventListener 'cancel', () -> modal.hide() dialog.hide() @@ -280,4 +285,4 @@ class BackgroundImage extends ContentTools.Tools.Image index = ContentTools.DEFAULT_TOOLS[2].indexOf('image') -ContentTools.DEFAULT_TOOLS[2].splice(index + 1, 0, 'background-image') \ No newline at end of file +ContentTools.DEFAULT_TOOLS[2].splice(index + 1, 0, 'background-image') diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee index f3c4d1f97..dc1ea320b 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee @@ -14,16 +14,17 @@ class ButtonTool extends ContentTools.Tools.Bold @wrapperParentTagName = 'a' @_btnCSSProp = {} - + @_btnHrefParentAttr = {target: '_blank'} @_btnHrefParentAttrDef = @_btnHrefParentAttr - + @_btnAttr = {onclick: 'btnClickListener(this)'} @_btnAttrDef = @_btnAttr @_openDialogDelay = 1000 - @apply: (element, selection, callback, btnHrefParentAttr, btnCSSStyle, btnCreate = true) -> + @apply: (element, selection, callback, btnHrefParentAttr, btnCSSStyle, + btnCreate = true) -> # Apply the tool to the specified element and selection # Store the selection state of the element so we can restore it once done @@ -33,13 +34,13 @@ class ButtonTool extends ContentTools.Tools.Bold # selected when the focus is lost by the element. selectTag = new HTMLString.Tag 'span', {'class': 'ct--pseudo-select'} [from, to] = selection.get() - + # Add btn position @_btnAttr.position = "#{ from },#{ to }" element.content = element.content.format from, to, selectTag element.updateInnerHTML() - + # Set-up the dialog app = ContentTools.EditorApp.get() @@ -69,11 +70,13 @@ class ButtonTool extends ContentTools.Tools.Bold rect = measureSpan[0].getBoundingClientRect() # Create background color dialog - bgColorDialog = new GetBgColorDialog @getBgColor btnCSSStyle, element, selection + bgColorDialog = new GetBgColorDialog @getBgColor btnCSSStyle, \ + element, selection bgColorDialog.position @getDialogPosition rect # Create text color dialog - textColorDialog = new GetTextColorDialog @getTextColor btnCSSStyle, element, selection + textColorDialog = new GetTextColorDialog @getTextColor btnCSSStyle, \ + element, selection textColorDialog.position @getDialogPosition rect # Create btn url link dialog @@ -83,7 +86,8 @@ class ButtonTool extends ContentTools.Tools.Bold btnLinkDialog.position @getDialogPosition rect # Create padding dialog - paddingDialog = new GetPaddindDialog @parsePaddingToInput @getPadding btnCSSStyle, element, selection + paddingDialog = new GetPaddindDialog @parsePaddingToInput \ + @getPadding btnCSSStyle, element, selection paddingDialog.position @getDialogPosition rect # Listen for save events against the dialog @@ -112,7 +116,8 @@ class ButtonTool extends ContentTools.Tools.Bold link = ev.detail().href if link - @_btnHrefParentAttr = @extendObj false, {'href': link}, @_btnHrefParentAttr + @_btnHrefParentAttr = @extendObj false, {'href': link}, \ + @_btnHrefParentAttr # Insert btn element applyBtn() @@ -137,7 +142,7 @@ class ButtonTool extends ContentTools.Tools.Bold # Trigger the callback callback(true) - + # Listen for save events against the dialog bgColorDialog.addEventListener 'save', (ev) => @@ -145,7 +150,8 @@ class ButtonTool extends ContentTools.Tools.Bold color = ev.detail().color if color - @_btnCSSProp = @extendObj false, {'background-color': color}, @_btnCSSProp + @_btnCSSProp = @extendObj false, {'background-color': color}, \ + @_btnCSSProp # Hide dialog bgColorDialog.hide() @@ -162,13 +168,15 @@ class ButtonTool extends ContentTools.Tools.Bold element.content = element.content.unformat from, to, @wrapperParentTagName if Object.keys(@_btnCSSProp).length > 0 - - # Btn href parent elelement - btnHrefParent = new HTMLString.Tag @wrapperParentTagName, @_btnHrefParentAttr + + # Btn href parent elelement + btnHrefParent = new HTMLString.Tag @wrapperParentTagName, \ + @_btnHrefParentAttr element.content = element.content.format from, to, btnHrefParent # Btn element - btn = new HTMLString.Tag(@tagName, @extendObj(false, @_btnAttr, @getCCCStr(@_btnCSSProp))) + btn = new HTMLString.Tag @tagName, @extendObj(false, \ + @_btnAttr, @getCCCStr(@_btnCSSProp)) element.content = element.content.format from, to, btn element.updateInnerHTML() @@ -214,14 +222,14 @@ class ButtonTool extends ContentTools.Tools.Bold editor = ContentTools.EditorApp.get() - # Prevent deafult if editor is not in editting mode + # Prevent deafult if editor is not in editing mode if editor.isEditing() is true # Get evt target btn = evt btnText = btn.innerText # Create btn text node - btnToText = document.createTextNode btnText + btnToText = document.createTextNode btnText # Replace btn node with text btnParent = btn.parentElement @@ -231,19 +239,20 @@ class ButtonTool extends ContentTools.Tools.Bold [from, to] = btn.getAttribute('position').split ',' # Get btn css prop - btnCSSStyle = @getBtnCSSProp btn + btnCSSStyle = @getBtnCSSProp btn # Get btn attr link = btnParent.getAttribute 'href' - btnHrefParentAttr = @extendObj false, {'href': link}, @_btnHrefParentAttrDef - + btnHrefParentAttr = @extendObj false, {'href': link}, \ + @_btnHrefParentAttrDef + # Replace btn with btn text node btnHrefParent.replaceChild btnToText, btnParent # Create selection range range = new ContentSelect.Range parseInt(from), parseInt(to) - range.select btnHrefParent - + range.select btnHrefParent + # Get selected text element element = ContentEdit.Root.get().focused() @@ -255,15 +264,17 @@ class ButtonTool extends ContentTools.Tools.Bold callback = () -> # Apply tool - @apply element, selection, callback, btnHrefParentAttr, btnCSSStyle, false - + @apply element, selection, callback, btnHrefParentAttr, \ + btnCSSStyle, false + # @addBtnListener: (element) -> # btns = element.domElement().getElementsByTagName('button') # for btn in btns # btn.addEventListener 'click', @btnClickListener - @getBgColor: (btnCSSStyle, element, selection, cssPropName = 'background-color') -> + @getBgColor: (btnCSSStyle, element, selection, + cssPropName = 'background-color') -> # Get CSS bg color prop value if btnCSSStyle? btnCSSStyle[cssPropName] @@ -271,7 +282,7 @@ class ButtonTool extends ContentTools.Tools.Bold @getTextColor: (btnCSSStyle, element, selection, cssPropName = 'color') -> # Get CSS color prop value - if btnCSSStyle? + if btnCSSStyle? btnCSSStyle[cssPropName] else @getCSSStyle(cssPropName, element, selection, @tagName) @@ -281,7 +292,8 @@ class ButtonTool extends ContentTools.Tools.Bold btnHrefParentAttr[attrName] else @getAttr(attrName, element, selection, 'a') - @getTargetAttr: (btnHrefParentAttr, element, selection, attrName = 'target') -> + @getTargetAttr: (btnHrefParentAttr, element, selection, + attrName = 'target') -> # Get target attr value if btnHrefParentAttr? btnHrefParentAttr[attrName] @@ -297,7 +309,7 @@ class ButtonTool extends ContentTools.Tools.Bold prop = [] for k, v of cssProp - prop.push "#{ k }:#{ v }" + prop.push "#{ k }:#{ v }" {'style': prop.join ';'} @@ -307,25 +319,25 @@ class ButtonTool extends ContentTools.Tools.Bold 'color': @getColorCSSProp 'color', element 'background-color': @getColorCSSProp 'background-color', element 'padding': @getPaddingCSSProp @getCSSProp 'padding', element - + cssStyle @getColorCSSProp: (propName, element) -> # Convert rgb(255, 255, 255) to the #ffffff format regExp = /\(([^)]+)\)/ color = element.style[propName] - + if color.length > 0 [r, g, b] = regExp.exec(color)[1].split ',' return @rgbToHex parseInt(r), parseInt(g), parseInt(b) - + return '' @getCSSProp: (propName, element) -> element.style[propName] @getPaddingCSSProp: (prop) -> - # If padding is same in every direction + # If padding is same in every direction # set two same padding value in return string propSplit = prop.split ' ' return if propSplit.length > 1 then prop else [prop, prop].join ' ' @@ -356,16 +368,17 @@ class ButtonTool extends ContentTools.Tools.Bold @showDialog: (dialog, colorPicker = true) -> # Show dialog dialog.show() - + # Initialize color picker if colorPicker @initColorPicker 'input.color' @initColorPicker: (selector) -> # Initialize color picker - initial = (elm, colors) -> + initial = (elm, colors) -> elm.style.backgroundColor = elm.value - elm.style.color = if colors.rgbaMixBG.luminance > 0.22 then '#222' else '#ddd' + elm.style.color = if colors.rgbaMixBG.luminance > 0.22 \ + then '#222' else '#ddd' jsColorPicker(selector, { memoryColors: @getAutomatColors(), @@ -387,18 +400,19 @@ class ButtonTool extends ContentTools.Tools.Bold @getCSSStyle: (cssPropName, element, selection, childElement) -> # Return any existing style value for the element and selection - # Find the first character in the selected text that has a `childElement` tag and + # Find the first character in the selected text that has a + # `childElement` tag and # return its `cssPropName` value. [from, to] = selection.get() selectedContent = element.content.slice from, to for c in selectedContent.characters - # Does this character have a time tag applied? + # Does this character have a button tag applied? if not c.hasTags childElement continue - # Find the time tag and return the datetime attribute value + # Find the button tag and return the attribute value for tag in c.tags() if tag.name() == childElement return tag.style cssPropName @@ -407,7 +421,8 @@ class ButtonTool extends ContentTools.Tools.Bold @getAttr: (attrName, element, selection, childElement) -> # Return any existing attribute value for the element and selection - # Find the first character in the selected text that has a `childElement` tag and + # Find the first character in the selected text that has a + # `childElement` tag and # return its `attrName` value. [from, to] = selection.get() @@ -430,13 +445,16 @@ class ButtonTool extends ContentTools.Tools.Bold # Merge the object into the extended object merge = (obj) -> - + for prop in Object.keys obj - + if Object.prototype.hasOwnProperty.call obj, prop - + # If deep merge and property is an object, merge properties - if deep and Object.prototype.toString.call obj[prop] is '[object Object]' + if ( + deep and + Object.prototype.toString.call obj[prop] is '[object Object]' + ) extended[prop] = @extendObj true, extended[prop], obj[prop] else extended[prop] = obj[prop] @@ -450,11 +468,12 @@ class ButtonTool extends ContentTools.Tools.Bold class GetBgColorDialog extends ContentTools.LinkDialog - # An anchored dialog to support inserting/modifying a background css color prop value + # An anchored dialog to support inserting/modifying a + # background css color prop value constructor: (color) -> @_color = color - + super() mount: () -> @@ -464,13 +483,15 @@ class GetBgColorDialog extends ContentTools.LinkDialog # link dialog. cssClass = @_domInput.getAttribute 'class' colorPickerCssClass = 'color' - color = if @_color then @_color else ButtonTool.getHexFormat ButtonTool.getAutomatColors()[1] + color = if @_color then @_color else \ + ButtonTool.getHexFormat ButtonTool.getAutomatColors()[1] # purpleColor = ButtonTool.getHexFormat ButtonTool.getAutomatColors()[1] @_domInput.setAttribute 'name', 'color' @_domInput.setAttribute 'class', "#{ cssClass } #{ colorPickerCssClass }" @_domInput.setAttribute 'value', color - @_domInput.setAttribute 'placeholder', ContentEdit._ 'Enter a background color' + @_domInput.setAttribute 'placeholder', ContentEdit._ \ + 'Enter a background color' # Remove the new window target DOM element @_domElement.removeChild @_domTargetButton @@ -490,7 +511,8 @@ class GetBgColorDialog extends ContentTools.LinkDialog class GetTextColorDialog extends GetBgColorDialog - # An anchored dialog to support inserting/modifying a background css color prop value + # An anchored dialog to support inserting/modifying a + # text-color css color prop value mount: () -> super() @@ -509,7 +531,7 @@ class GetPaddindDialog extends ContentTools.LinkDialog constructor: (@padding) -> super() - + @_defPadding = '20, 20' mount: () -> @@ -521,7 +543,8 @@ class GetPaddindDialog extends ContentTools.LinkDialog @padding = if @padding then @padding else @_defPadding @_domInput.setAttribute 'name', 'padding' @_domInput.setAttribute 'value', @padding - @_domInput.setAttribute 'placeholder', ContentEdit._ 'Enter a button padding space' + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a button padding space' # Remove the new window target DOM element @_domElement.removeChild @_domTargetButton @@ -535,4 +558,3 @@ class GetPaddindDialog extends ContentTools.LinkDialog ContentTools.DEFAULT_TOOLS[0].push('button') - diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee index fa5484cdd..42c20c178 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/decrease-font-size-tool.coffee @@ -1,6 +1,6 @@ class DecreaseFontSizeTool extends ContentTools.Tools.Bold - # Apply a class to justify align the contents of the current text block. + # Decrease font size of the current selection of text. # Register the tool with the toolshelf ContentTools.ToolShelf.stow(@, 'decrease-font-size') @@ -54,7 +54,7 @@ class DecreaseFontSizeTool extends ContentTools.Tools.Bold # Dialog size = if IncreaseFontSizeTool._fontSizeFactor? then \ - IncreaseFontSizeTool._fontSizeFactor else null + IncreaseFontSizeTool._fontSizeFactor else null dialog = new GetDecreaseFontSizeValueDialog(size) dialog.position @getDialogPosition rect @@ -94,7 +94,7 @@ class DecreaseFontSizeTool extends ContentTools.Tools.Bold @getNewFontSize: (styleClass) => defaultFontSize = styleClass.getPropertyValue 'font-size' defaultFontSize = parseInt \ - defaultFontSize.slice 0, defaultFontSize.length - 2 + defaultFontSize.slice 0, defaultFontSize.length - 2 newFontSize = defaultFontSize - parseInt(@_fontSizeFactor) @setNewCSS: (fontSizeId, fontSizeCSSClass) -> @@ -106,7 +106,7 @@ class DecreaseFontSizeTool extends ContentTools.Tools.Bold @index: (elementRef) -> nodes = Array.prototype.slice.call \ - document.getElementsByClassName('[ article__content ]')[0].children + document.getElementsByClassName('[ article__content ]')[0].children return nodes.indexOf elementRef @_apply: (element, selection) => @@ -123,7 +123,7 @@ class DecreaseFontSizeTool extends ContentTools.Tools.Bold style = document.getElementById(fontSizeId) styleClass = window.getComputedStyle \ - document.getElementsByClassName(fontSizeCSSClassName)[0] + document.getElementsByClassName(fontSizeCSSClassName)[0] @_newFontSize = @getNewFontSize(styleClass) fontSizeCSSClass = """ @@ -173,12 +173,13 @@ class DecreaseFontSizeTool extends ContentTools.Tools.Bold class GetDecreaseFontSizeValueDialog extends ContentTools.LinkDialog - # An anchored dialog to support inserting/modifying a padding css prop value + # An anchored dialog to support inserting/modifying a decrease font + # size value constructor: (@size) -> super() - @_defDecreaseFontSizeValue = '' + @_defDecreaseFontSizeValue = '1' mount: () -> super() @@ -189,14 +190,14 @@ class GetDecreaseFontSizeValueDialog extends ContentTools.LinkDialog @size = if @size? then @size else @_defDecreaseFontSizeValue @_domInput.setAttribute 'name', 'size' @_domInput.setAttribute 'value', @size - @_domInput.setAttribute 'placeholder', ContentEdit._ \ - 'Enter a font size deacrease factor (px)' + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a font size deacrease factor (px)' # Remove the new window target DOM element @_domElement.removeChild @_domTargetButton save: () -> - # Save the padding. + # Save the decrease font size value. detail = { size: @_domInput.value.trim() } diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee index 111b82e0e..d9bafbc56 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/font-size-tool.coffee @@ -1,6 +1,6 @@ class FontSizeTool extends ContentTools.Tools.Bold - # Apply a class to justify align the contents of the current text block. + # Set font size of the current selection of text. # Register the tool with the toolshelf ContentTools.ToolShelf.stow(@, 'font-size') @@ -92,7 +92,7 @@ class FontSizeTool extends ContentTools.Tools.Bold @getFontSize: (styleClass) -> defaultFontSize = styleClass.getPropertyValue 'font-size' defaultFontSize = parseInt \ - defaultFontSize.slice 0, defaultFontSize.length - 2 + defaultFontSize.slice 0, defaultFontSize.length - 2 @setNewCSS: (fontSizeId, fontSizeCSSClass) -> style = document.createElement('style') @@ -103,7 +103,7 @@ class FontSizeTool extends ContentTools.Tools.Bold @index: (elementRef) -> nodes = Array.prototype.slice.call \ - document.getElementsByClassName('[ article__content ]')[0].children + document.getElementsByClassName('[ article__content ]')[0].children return nodes.indexOf elementRef @_apply: (element, selection) => @@ -140,7 +140,7 @@ class FontSizeTool extends ContentTools.Tools.Bold class FontSizeValueDialog extends ContentTools.LinkDialog - # An anchored dialog to support inserting/modifying a padding css prop value + # An anchored dialog to support inserting/modifying a font size value constructor: (@size) -> super() @@ -153,8 +153,8 @@ class FontSizeValueDialog extends ContentTools.LinkDialog @size = if @size? then @size else @_defFontSizeValue @_domInput.setAttribute 'name', 'size' @_domInput.setAttribute 'value', @size - @_domInput.setAttribute 'placeholder', ContentEdit._ \ - 'Enter a font size (px)' + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a font size (px)' # Remove the new window target DOM element @_domElement.removeChild @_domTargetButton diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee index a07c20069..1c88beecf 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/increase-font-size-tool.coffee @@ -1,6 +1,6 @@ class IncreaseFontSizeTool extends ContentTools.Tools.Bold - # Apply a class to justify align the contents of the current text block. + # Increase font size of the current selection of text. # Register the tool with the toolshelf ContentTools.ToolShelf.stow(@, 'increase-font-size') @@ -54,7 +54,7 @@ class IncreaseFontSizeTool extends ContentTools.Tools.Bold # Dialog size = if DecreaseFontSizeTool._fontSizeFactor? \ - then DecreaseFontSizeTool._fontSizeFactor else null + then DecreaseFontSizeTool._fontSizeFactor else null dialog = new GetIncreaseFontSizeValueDialog(size) dialog.position @getDialogPosition rect @@ -94,7 +94,7 @@ class IncreaseFontSizeTool extends ContentTools.Tools.Bold @getNewFontSize: (styleClass) => defaultFontSize = styleClass.getPropertyValue 'font-size' defaultFontSize = parseInt \ - defaultFontSize.slice 0, defaultFontSize.length - 2 + defaultFontSize.slice 0, defaultFontSize.length - 2 newFontSize = defaultFontSize + parseInt @_fontSizeFactor @setNewCSS: (fontSizeId, fontSizeCSSClass) -> @@ -106,7 +106,7 @@ class IncreaseFontSizeTool extends ContentTools.Tools.Bold @index: (elementRef) -> nodes = Array.prototype.slice.call \ - document.getElementsByClassName('[ article__content ]')[0].children + document.getElementsByClassName('[ article__content ]')[0].children return nodes.indexOf elementRef @_apply: (element, selection) => @@ -124,7 +124,7 @@ class IncreaseFontSizeTool extends ContentTools.Tools.Bold style = document.getElementById fontSizeId styleClass = window.getComputedStyle \ - document.getElementsByClassName(fontSizeCSSClassName)[0] + document.getElementsByClassName(fontSizeCSSClassName)[0] @_newFontSize = @getNewFontSize styleClass fontSizeCSSClass = """ @@ -174,7 +174,8 @@ class IncreaseFontSizeTool extends ContentTools.Tools.Bold class GetIncreaseFontSizeValueDialog extends ContentTools.LinkDialog - # An anchored dialog to support inserting/modifying a padding css prop value + # An anchored dialog to support inserting/modifying a increase font + # size value constructor: (@size) -> super() @@ -187,14 +188,14 @@ class GetIncreaseFontSizeValueDialog extends ContentTools.LinkDialog @size = if @size? then @size else @_defIncreaseFontSizeValue @_domInput.setAttribute 'name', 'size' @_domInput.setAttribute 'value', @size - @_domInput.setAttribute 'placeholder', ContentEdit._ \ - 'Enter a font size increase factor (px)' + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a font size increase factor (px)' # Remove the new window target DOM element @_domElement.removeChild @_domTargetButton save: () -> - # Save the padding. + # Save the increase font size value. detail = { size: @_domInput.value.trim() } diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee index 500e309c3..a8aeebb80 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/line-spacing-tool.coffee @@ -83,7 +83,7 @@ class LineSpacingTool extends CustomAlignLeftTool @index: (elementRef) -> nodes = Array.prototype.slice.call \ - document.getElementsByClassName('[ article__content ]')[0].children + document.getElementsByClassName('[ article__content ]')[0].children return nodes.indexOf elementRef @_apply: (element, selection) => @@ -123,17 +123,17 @@ class LineSpacingDialog extends ContentTools.LinkDialog super() @lineSpacing = if @lineSpacing? then @lineSpacing else \ - @_defLineSpacingValue + @_defLineSpacingValue @_domInput.setAttribute 'name', 'lineSpacing' @_domInput.setAttribute 'value', @lineSpacing - @_domInput.setAttribute 'placeholder', ContentEdit._ \ - 'Enter number to set the line height' + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter number to set the line height' # Remove the new window target DOM element @_domElement.removeChild @_domTargetButton save: () -> - # Save the padding. + # Save the line spacing value. detail = { lineSpacing: @_domInput.value.trim() } diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/superclass.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/superclass.coffee index e55b6a0f6..8a69a3745 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/superclass.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/superclass.coffee @@ -1,11 +1,11 @@ moduleKeywords = ['included', 'extended'] class SuperClass - @include: (obj) -> - throw('include(obj) requires obj') unless obj - for key, value of obj.prototype when key not in moduleKeywords - @::[key] = value + @include: (obj) -> + throw('include(obj) requires obj') unless obj + for key, value of obj.prototype when key not in moduleKeywords + @::[key] = value - included = obj.included - included.apply(this) if included - @ \ No newline at end of file + included = obj.included + included.apply(this) if included + @ diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/vertical-space-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/vertical-space-tool.coffee index 1c25bddfa..c094e1a4c 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/vertical-space-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/vertical-space-tool.coffee @@ -1,6 +1,7 @@ class VerticalSpace extends ContentTools.Tool - # Insert a vertical space in to the current element at the specified selection. + # Insert a vertical space in to the current element at the specified + # selection. ContentTools.ToolShelf.stow(@, 'vertical-space') @@ -73,11 +74,15 @@ class VerticalSpace extends ContentTools.Tool tdContent="vertical space: #{ verticalSpace } px" - tableHtmlString = "" + - "
      " + - "
      vertical space
      " + tableHtmlString = "" + + "
      " + + "
      vertical space
      " - table = new HTMLString.String(tableHtmlString, element.content.preserveWhitespace()) + table = new HTMLString.String( + tableHtmlString, + element.content.preserveWhitespace() + ) element.content = tip.concat(table, tail) element.updateInnerHTML() element.taint() @@ -114,7 +119,8 @@ class VerticalSpace extends ContentTools.Tool class GetVerticalSpaceDialog extends ContentTools.LinkDialog - # An anchored dialog to support inserting/modifying a vertical space px unit + # An anchored dialog to support inserting/modifying a vertical space + # px unit constructor: (@verticalSpace) -> super() @@ -127,10 +133,12 @@ class GetVerticalSpaceDialog extends ContentTools.LinkDialog # Update the name and placeholder for the input field provided by the # link dialog. - @verticalSpace = if @verticalSpace then @verticalSpace else @_defVerticalSpace + @verticalSpace = if @verticalSpace then @verticalSpace else \ + @_defVerticalSpace @_domInput.setAttribute 'name', 'verticalSpace' @_domInput.setAttribute 'value', @verticalSpace - @_domInput.setAttribute 'placeholder', ContentEdit._ 'Enter a vertical space (px)' + @_domInput.setAttribute 'placeholder', \ + ContentEdit._ 'Enter a vertical space (px)' # Remove the new window target DOM element @_domElement.removeChild @_domTargetButton @@ -145,4 +153,4 @@ class GetVerticalSpaceDialog extends ContentTools.LinkDialog index = ContentTools.DEFAULT_TOOLS[2].indexOf('line-break') -ContentTools.DEFAULT_TOOLS[2].splice(index + 1, 0, 'vertical-space') \ No newline at end of file +ContentTools.DEFAULT_TOOLS[2].splice(index + 1, 0, 'vertical-space') From 2af312021d73986f79d49fe4960eda503357c7f6 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 2 Mar 2020 10:12:07 +0100 Subject: [PATCH 133/189] fix: add button ContentTools tool * get correct btn parent container element if is applied another tool to the text selection (font-size/increase/decrease font-size tool) --- .../aklub/js/contenttools/src/scripts/button-tool.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee index dc1ea320b..9d14dfb41 100644 --- a/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/button-tool.coffee @@ -234,6 +234,9 @@ class ButtonTool extends ContentTools.Tools.Bold # Replace btn node with text btnParent = btn.parentElement btnHrefParent = btnParent.parentElement + btnParentEditableContainer = document.getElementsByClassName( + 'ce-element--focused' + )[0] # Get position [from, to] = btn.getAttribute('position').split ',' @@ -251,7 +254,7 @@ class ButtonTool extends ContentTools.Tools.Bold # Create selection range range = new ContentSelect.Range parseInt(from), parseInt(to) - range.select btnHrefParent + range.select btnParentEditableContainer # Get selected text element element = ContentEdit.Root.get().focused() From 7e73ec0ca4c99457972fb5472600ed18b95687c3 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 6 Mar 2020 06:34:28 +0100 Subject: [PATCH 134/189] fix: save/load dynamic created css class definition into the db * font-size/increase/decrease font size ContentTools tools --- .../aklub/js/contenttools/Gruntfile.coffee | 1 + .../src/scripts/custom-editor.coffee | 103 ++++++++++++++++++ apps/aklub/static/aklub/js/editor.js | 5 +- .../aklub/templates/email_templates/base.html | 3 + apps/aklub/utils.py | 2 + .../migrations/0005_templatecontent_styles.py | 19 ++++ apps/html_template_editor/models.py | 5 + apps/html_template_editor/serializers.py | 3 +- apps/html_template_editor/views.py | 2 + 9 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 apps/aklub/static/aklub/js/contenttools/src/scripts/custom-editor.coffee create mode 100644 apps/html_template_editor/migrations/0005_templatecontent_styles.py diff --git a/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee index 0ef704847..0237416b8 100644 --- a/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee +++ b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee @@ -55,6 +55,7 @@ module.exports = (grunt) -> 'src/scripts/increase-font-size-tool.coffee' 'src/scripts/decrease-font-size-tool.coffee' 'src/scripts/line-spacing-tool.coffee' + 'src/scripts/custom-editor.coffee' ] sandbox: diff --git a/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-editor.coffee b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-editor.coffee new file mode 100644 index 000000000..de79cdfab --- /dev/null +++ b/apps/aklub/static/aklub/js/contenttools/src/scripts/custom-editor.coffee @@ -0,0 +1,103 @@ +class CustomEditorApp extends _EditorApp + + # The custom editor application + + getStyles: () -> + _styles = [] + styles = document.querySelectorAll('[id^="font_size_"]') + for item, index in styles + _styles.push({id: item.id, style: item.innerHTML}) + return _styles + + save: (passive) -> + # Save changes to the current page + if not @dispatchEvent(@createEvent('save', {passive: passive})) + return + + # Blur any active element to ensure empty elements are not retained + root = ContentEdit.Root.get() + if root.focused() and not passive + root.focused().blur() + + # Check the document has changed, if not we don't need do anything + if root.lastModified() == @_rootLastModified and passive + # Trigger the saved event early with no modified regions, + @dispatchEvent( + @createEvent('saved', {regions: {}, passive: passive}) + ) + return + + # Build a map of the modified regions + domRegions = [] + modifiedRegions = {} + for name, region of @_regions + # Check for regions that contain only a place holder + html = region.html() + if region.children.length == 1 and not region.type() is 'Fixture' + child = region.children[0] + if child.content and not child.content.html() + html = '' + + # Apply the changes made to the DOM (affectively resetting the DOM + # to a non-editable state). + unless passive + # Unmount all children + for child in region.children + child.unmount() + + # Handle fixtures vs. standard regions + if region.children.length is 1 and region.children[0].isFixed() + wrapper = @constructor.createDiv() + wrapper.innerHTML = html + domRegions.push(wrapper.firstElementChild) + region.domElement().parentNode.replaceChild( + wrapper.firstElementChild, + region.domElement() + ) + else + domRegions.push(region.domElement()) + region.domElement().innerHTML = html + + # Check the region has been modified, if not we don't include it in + # the output. + if region.lastModified() == @_regionsLastModified[name] + continue + + modifiedRegions[name] = html + + # Set the region back to not modified + @_regionsLastModified[name] = region.lastModified() + + # Resync the DOM regions, this is required as fixture will replace the + # existing DOM region element (regions wont). + @_domRegions = domRegions + + # Trigger the saved event with a region HTML map for the changed + # content. + @dispatchEvent( + @createEvent( + 'saved', + { + regions: modifiedRegions, + passive: passive, + styles: @getStyles() + } + ) + ) + + +class ContentTools.EditorApp + + # The `ContentTools.EditorApp` class is a singleton, this code provides + # access to the singleton instance of the protected `_EditorApp` class which + # is initialized the first time the class method `get` is called. + + # Storage for the singleton instance that will be created for the editor app + instance = null + + @get: () -> + cls = ContentTools.EditorApp.getCls() + instance ?= new cls() + + @getCls: () -> + return CustomEditorApp diff --git a/apps/aklub/static/aklub/js/editor.js b/apps/aklub/static/aklub/js/editor.js index 93e598586..0902688fb 100644 --- a/apps/aklub/static/aklub/js/editor.js +++ b/apps/aklub/static/aklub/js/editor.js @@ -78,7 +78,8 @@ element, pageId, successSaveFlash, - failSaveFlash; + failSaveFlash, + styles; // Collect the contents of each region into a FormData instance payload = new FormData(); // Check that something changed @@ -86,9 +87,11 @@ if (Object.keys(regions).length == 0) { return; } + styles = ev.detail().styles; payload.append("page", getTemplatePageUrl()); payload.append("images", JSON.stringify(getImages())); payload.append("regions", JSON.stringify(regions)); + payload.append("styles", JSON.stringify(styles)); // Send the updated content to the server to be saved onStateChange = function(ev) { // Check if the request is finished diff --git a/apps/aklub/templates/email_templates/base.html b/apps/aklub/templates/email_templates/base.html index 9ebc2751a..1852007b6 100644 --- a/apps/aklub/templates/email_templates/base.html +++ b/apps/aklub/templates/email_templates/base.html @@ -13,6 +13,9 @@ {% include "email_templates/automat_fonts.html" %} + {% for style in styles %} + + {% endfor %} {% block content %} diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index c6b27ea81..d5669145c 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -182,6 +182,8 @@ def get_email_template_context(template_path, template_url): } regions = json.loads(content.regions) context.update(regions) + styles = json.loads(content.styles) + context.update({'styles': styles}) else: context = {} diff --git a/apps/html_template_editor/migrations/0005_templatecontent_styles.py b/apps/html_template_editor/migrations/0005_templatecontent_styles.py new file mode 100644 index 000000000..bc591d964 --- /dev/null +++ b/apps/html_template_editor/migrations/0005_templatecontent_styles.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.10 on 2020-03-02 12:24 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('html_template_editor', '0004_companyemail_companyphone_companysocialmedia_companyurl_templatefooter'), + ] + + operations = [ + migrations.AddField( + model_name='templatecontent', + name='styles', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + ] diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index 13a4324a0..eaa373613 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -3,6 +3,7 @@ import uuid # from django.contrib.auth import get_user_model +from django.contrib.postgres.fields import JSONField from django.db import models from django.utils.html import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -42,6 +43,10 @@ class TemplateContent(models.Model): default='', null=True, ) + styles = JSONField( + blank=True, + null=True, + ) class Images(models.Model): diff --git a/apps/html_template_editor/serializers.py b/apps/html_template_editor/serializers.py index eb7ec7714..b673a939d 100644 --- a/apps/html_template_editor/serializers.py +++ b/apps/html_template_editor/serializers.py @@ -17,7 +17,8 @@ class Meta: 'uuid', 'page', 'regions', - 'images') + 'images', + 'styles') def to_representation(self, instance): ret = super(TemplateContentSerializer, self).to_representation(instance) diff --git a/apps/html_template_editor/views.py b/apps/html_template_editor/views.py index 6ea3a0574..2c4a317a9 100644 --- a/apps/html_template_editor/views.py +++ b/apps/html_template_editor/views.py @@ -27,11 +27,13 @@ def perform_create(self, serializer): regions = self.request.data.get('regions') images = self.request.data.get('images') page = self.request.data.get('page') + styles = self.request.data.get('styles') serializer.save( regions=regions, images=images, page=page, + styles=styles, ) From 15bf35377efa0ab600b37198037187e1a9f66b69 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 6 Mar 2020 15:12:43 +0100 Subject: [PATCH 135/189] fix: refactor build ContentTools sandbox css file * add custom sandbox-append.scss (append custom styles only) * rename custom sandbox.scss file -> sandbox-replace.scss (replace styles var with custom var only) * concat sandbox-append.scss, sandbox-replace.scss into one file sandbox-concat.scss --- .../management/commands/build_contenttools.py | 87 +++++++++++++++---- .../{sandbox.scss => sandbox-append.scss} | 2 - .../src/sandbox/styles/sandbox-replace.scss | 1 + .../aklub/js/contenttools/Gruntfile.coffee | 11 ++- 4 files changed, 80 insertions(+), 21 deletions(-) rename apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/{sandbox.scss => sandbox-append.scss} (98%) create mode 100644 apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss diff --git a/apps/aklub/management/commands/build_contenttools.py b/apps/aklub/management/commands/build_contenttools.py index b83b6355f..6b5e8a35f 100644 --- a/apps/aklub/management/commands/build_contenttools.py +++ b/apps/aklub/management/commands/build_contenttools.py @@ -1,5 +1,6 @@ import os import pathlib +import re import shutil import subprocess @@ -64,6 +65,7 @@ def handle(self, *args, **options): self.cttools_target_styles_dir = self.cttools_target_dir / 'src' / 'styles' / 'ui' self.cttools_target_fonts_dir = self.cttools_target_dir / 'build' / 'images' self.cttools_target_sandbox_dir = self.cttools_target_dir / 'src' / 'sandbox' + self.cttools_target_sandbox__build_dir = self.cttools_target_dir / 'sandbox' self.__copy_coffee_scripts() self.__copy_sass_styles() @@ -84,16 +86,16 @@ def handle(self, *args, **options): 'install grunt-cli', ) # Build ContentTools - program = self.cttools_target_dir / 'node_modules' / 'grunt-cli' / 'bin' / 'grunt' + grunt = self.cttools_target_dir / 'node_modules' / 'grunt-cli' / 'bin' / 'grunt' self.__install( - [program, 'build'], - program, + [grunt, 'build'], + grunt, 'build', ) # Build ContentTools sandbox self.__install( - [program, 'sandbox'], - program, + [grunt, 'sandbox'], + grunt, 'sandbox', ) @@ -118,23 +120,72 @@ def __copy_sass_styles(self): if '@import "custom-toolbox";' not in content: f.write('\n@import "custom-toolbox";') - _src_sandbox_scss_file = self.cttools_src_sandbox_styles_dir / 'sandbox.scss' + # Copy sandbox append file + sandbox_append_scss_file = self.cttools_src_sandbox_styles_dir / 'sandbox-append.scss' + shutil.copy(sandbox_append_scss_file, self.cttools_target_sandbox_dir) + + # Apply sandbox replace file + _src_sandbox_scss_file = self.cttools_src_sandbox_styles_dir / 'sandbox-replace.scss' _target_sandbox_scss_file = self.cttools_target_sandbox_dir / 'sandbox.scss' - with open(_src_sandbox_scss_file, 'r') as f_read, open(_target_sandbox_scss_file, 'r+') as f_write: - text = f_read.readlines() - append_text = [] - base_text = '$max-width: 920px;' - for line in text: + + with open(_src_sandbox_scss_file, 'r') as f_read, \ + open(_target_sandbox_scss_file, 'r+') as f_write: + + new_content = None + source_content = f_read.readlines() + + max_width_base_text = '$max-width: 600px;' + + for line in source_content: if '$max-width:' in line: - replace_text = line - else: - append_text.append(line) + max_width_replace_text = line + target_content = f_write.read() - if base_text in target_content: - content = target_content.replace(base_text, replace_text) - content += ''.join(append_text) + + if max_width_base_text not in target_content: + new_content = target_content.replace( + max_width_base_text, + max_width_replace_text, + ) + + if not new_content: + new_content = target_content + + new_content = self.__replace_css_import(new_content) + + if new_content: f_write.seek(0) - f_write.write(content) + f_write.truncate(0) + f_write.write(new_content) + + def __replace_css_import(self, content): + """Replace default css import path with correct path""" + + css_imports = re.findall('@import.*.;', content) + + for css_import in css_imports: + css_import_split = css_import.split(' ') + + try: + css_import_path = pathlib.PurePath( + css_import_split[1][:-1].replace('"', ''), + ) + except IndexError: + continue + + css_import_path_parts = list(css_import_path.parts) + if css_import_path_parts.count('..') < 2: + continue + + css_import_path_parts.pop(0) + + css_import_new_path = pathlib.PurePath(*css_import_path_parts) + new_css_import_text = ( + f"{css_import_split[0]} \"{css_import_new_path.as_posix()}\";" + ) + content = content.replace(css_import, new_css_import_text) + + return content def __copy_fonts(self): # Copy font icons diff --git a/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox.scss b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-append.scss similarity index 98% rename from apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox.scss rename to apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-append.scss index 31d4fe51a..a980e46a3 100644 --- a/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox.scss +++ b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-append.scss @@ -1,5 +1,3 @@ -$max-width: 600px; - .text-justify { text-align: justify; } diff --git a/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss new file mode 100644 index 000000000..103e52aec --- /dev/null +++ b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss @@ -0,0 +1 @@ +$max-width: 920px; diff --git a/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee index 0237416b8..6a1feb79d 100644 --- a/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee +++ b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee @@ -107,7 +107,7 @@ module.exports = (grunt) -> sandbox: files: - 'sandbox/sandbox.css': 'src/sandbox/sandbox.scss' + 'sandbox/sandbox.css': 'sandbox/sandbox-concat.scss' cssnano: options: @@ -137,6 +137,14 @@ module.exports = (grunt) -> ] dest: 'build/content-tools.js' + sandbox: + src: [ + 'src/sandbox/sandbox.scss' + 'src/sandbox/sandbox-append.scss' + ] + dest: 'sandbox/sandbox-concat.scss' + + clean: build: ['src/tmp'] @@ -189,6 +197,7 @@ module.exports = (grunt) -> grunt.registerTask 'sandbox', [ 'coffee:sandbox' + 'concat:sandbox' 'sass:sandbox' ] From 6ddd29218c7d6a6a6e04f0757c63ac14e6151a32 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 6 Mar 2020 15:55:25 +0100 Subject: [PATCH 136/189] style: build_contenttools command * line width 80 characters * comment methods * replace format string function with the f-string --- .../management/commands/build_contenttools.py | 78 ++++++++++++++----- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/apps/aklub/management/commands/build_contenttools.py b/apps/aklub/management/commands/build_contenttools.py index 6b5e8a35f..85d7bbd9d 100644 --- a/apps/aklub/management/commands/build_contenttools.py +++ b/apps/aklub/management/commands/build_contenttools.py @@ -27,7 +27,7 @@ def handle(self, *args, **options): self.npm_path = pathlib.Path(self.npm_path) if not self.npm_path.exists(): - raise CommandError("Npm path '{}' doesn't exist.".format(self.npm_path)) + raise CommandError(f"Npm path '{self.npm_path}' doesn't exist.") # Aklub app assets dirs self.js_src_dir = _finders.find_in_app( @@ -44,28 +44,42 @@ def handle(self, *args, **options): ) self.cttools_coffee_src_scripts_dir = ( - pathlib.Path(self.js_src_dir) / 'contenttools' / 'src' / 'scripts' + pathlib.Path(self.js_src_dir) / 'contenttools' / 'src' / + 'scripts' ) self.cttools_src_styles_dir = ( - pathlib.Path(self.styles_src_dir) / 'contenttools' / 'src' / 'styles' / 'ui' + pathlib.Path(self.styles_src_dir) / 'contenttools' / 'src' / + 'styles' / 'ui' ) self.cttools_src_fonts_dir = ( pathlib.Path(self.fonts_src_dir) / 'contenttools' ) self.cttools_src_sandbox_styles_dir = ( - pathlib.Path(self.styles_src_dir) / 'contenttools' / 'src' / 'sandbox' / 'styles' + pathlib.Path(self.styles_src_dir) / 'contenttools' / 'src' / + 'sandbox' / 'styles' ) # Target dirs self.cttools_target_dir = ( - pathlib.Path(settings.BASE_DIR) / 'bower_components' / 'ContentTools' + pathlib.Path(settings.BASE_DIR) / 'bower_components' / + 'ContentTools' ) - self.cttools_target_scripts_dir = self.cttools_target_dir / 'src' / 'scripts' - self.cttools_target_styles_dir = self.cttools_target_dir / 'src' / 'styles' / 'ui' - self.cttools_target_fonts_dir = self.cttools_target_dir / 'build' / 'images' - self.cttools_target_sandbox_dir = self.cttools_target_dir / 'src' / 'sandbox' - self.cttools_target_sandbox__build_dir = self.cttools_target_dir / 'sandbox' + self.cttools_target_scripts_dir = ( + self.cttools_target_dir / 'src' / 'scripts' + ) + self.cttools_target_styles_dir = ( + self.cttools_target_dir / 'src' / 'styles' / 'ui' + ) + self.cttools_target_fonts_dir = ( + self.cttools_target_dir / 'build' / 'images' + ) + self.cttools_target_sandbox_dir = ( + self.cttools_target_dir / 'src' / 'sandbox' + ) + self.cttools_target_sandbox__build_dir = ( + self.cttools_target_dir / 'sandbox' + ) self.__copy_coffee_scripts() self.__copy_sass_styles() @@ -86,7 +100,10 @@ def handle(self, *args, **options): 'install grunt-cli', ) # Build ContentTools - grunt = self.cttools_target_dir / 'node_modules' / 'grunt-cli' / 'bin' / 'grunt' + grunt = ( + self.cttools_target_dir / 'node_modules' / 'grunt-cli' / + 'bin' / 'grunt' + ) self.__install( [grunt, 'build'], grunt, @@ -105,13 +122,19 @@ def __copy_coffee_scripts(self): # Copy coffee scripts scripts = self.cttools_coffee_src_scripts_dir.glob('*.coffee') for script_file in scripts: - shutil.copy(script_file, self.cttools_target_scripts_dir / script_file.name) + shutil.copy( + script_file, + self.cttools_target_scripts_dir / script_file.name + ) def __copy_sass_styles(self): # Copy sass styles styles = self.cttools_src_styles_dir.glob('*.scss') for style_file in styles: - shutil.copy(style_file, self.cttools_target_styles_dir / style_file.name) + shutil.copy( + style_file, + self.cttools_target_styles_dir / style_file.name + ) _ui_scss_file = self.cttools_target_styles_dir / '_ui.scss' with open(_ui_scss_file, 'a+') as f: @@ -121,12 +144,18 @@ def __copy_sass_styles(self): f.write('\n@import "custom-toolbox";') # Copy sandbox append file - sandbox_append_scss_file = self.cttools_src_sandbox_styles_dir / 'sandbox-append.scss' + sandbox_append_scss_file = ( + self.cttools_src_sandbox_styles_dir / 'sandbox-append.scss' + ) shutil.copy(sandbox_append_scss_file, self.cttools_target_sandbox_dir) # Apply sandbox replace file - _src_sandbox_scss_file = self.cttools_src_sandbox_styles_dir / 'sandbox-replace.scss' - _target_sandbox_scss_file = self.cttools_target_sandbox_dir / 'sandbox.scss' + _src_sandbox_scss_file = ( + self.cttools_src_sandbox_styles_dir / 'sandbox-replace.scss' + ) + _target_sandbox_scss_file = ( + self.cttools_target_sandbox_dir / 'sandbox.scss' + ) with open(_src_sandbox_scss_file, 'r') as f_read, \ open(_target_sandbox_scss_file, 'r+') as f_write: @@ -188,16 +217,25 @@ def __replace_css_import(self, content): return content def __copy_fonts(self): - # Copy font icons + """Copy font icons""" + font_icon_file = self.cttools_src_fonts_dir / 'icons.woff' - shutil.copy(font_icon_file, self.cttools_target_fonts_dir / font_icon_file.name) + shutil.copy( + font_icon_file, + self.cttools_target_fonts_dir / font_icon_file.name + ) def __copy_gruntfile(self): - # Copy Gruntfile - grunt_file = pathlib.Path(self.js_src_dir) / 'contenttools' / 'Gruntfile.coffee' + """Copy Gruntfile""" + + grunt_file = ( + pathlib.Path(self.js_src_dir) / 'contenttools' / 'Gruntfile.coffee' + ) shutil.copy(grunt_file, self.cttools_target_dir / grunt_file.name) def __install(self, command, program, package='install'): + """Install/build package""" + def raise_command_error(stream): raise CommandError( f'Error executing command {program} ' From ba68cf10afd6be13a09f0941196d2c2eebcc2bf2 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 6 Mar 2020 19:38:55 +0100 Subject: [PATCH 137/189] fix: set ContentTools template width to the 600px --- apps/aklub/management/commands/build_contenttools.py | 4 ++-- .../css/contenttools/src/sandbox/styles/sandbox-replace.scss | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/aklub/management/commands/build_contenttools.py b/apps/aklub/management/commands/build_contenttools.py index 85d7bbd9d..3a417cb75 100644 --- a/apps/aklub/management/commands/build_contenttools.py +++ b/apps/aklub/management/commands/build_contenttools.py @@ -163,7 +163,7 @@ def __copy_sass_styles(self): new_content = None source_content = f_read.readlines() - max_width_base_text = '$max-width: 600px;' + max_width_base_text = '$max-width: 920px;' for line in source_content: if '$max-width:' in line: @@ -171,7 +171,7 @@ def __copy_sass_styles(self): target_content = f_write.read() - if max_width_base_text not in target_content: + if max_width_base_text in target_content: new_content = target_content.replace( max_width_base_text, max_width_replace_text, diff --git a/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss index 103e52aec..eaa86ce13 100644 --- a/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss +++ b/apps/aklub/static/aklub/css/contenttools/src/sandbox/styles/sandbox-replace.scss @@ -1 +1 @@ -$max-width: 920px; +$max-width: 600px; From 911ec9540658267959e5e5f1444e02db55c15187 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Fri, 6 Mar 2020 19:58:56 +0100 Subject: [PATCH 138/189] feat: minify ContentTools sandbox css file * add minify sandbox css file grunt task --- apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee | 6 ++++++ apps/aklub/templates/email_templates/base.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee index 6a1feb79d..ff2677a91 100644 --- a/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee +++ b/apps/aklub/static/aklub/js/contenttools/Gruntfile.coffee @@ -118,6 +118,11 @@ module.exports = (grunt) -> 'build/content-tools.min.css': 'build/content-tools.min.css' + sandbox: + files: + 'sandbox/sandbox.min.css': + 'sandbox/sandbox.css' + uglify: options: banner: '/*! <%= pkg.name %> v<%= pkg.version %> by <%= pkg.author.name %> <<%= pkg.author.email %>> (<%= pkg.author.url %>) */\n' @@ -199,6 +204,7 @@ module.exports = (grunt) -> 'coffee:sandbox' 'concat:sandbox' 'sass:sandbox' + 'cssnano:sandbox' ] grunt.registerTask 'spec', [ diff --git a/apps/aklub/templates/email_templates/base.html b/apps/aklub/templates/email_templates/base.html index 1852007b6..f45951119 100644 --- a/apps/aklub/templates/email_templates/base.html +++ b/apps/aklub/templates/email_templates/base.html @@ -9,7 +9,7 @@ ContentTools sandbox - + {% include "email_templates/automat_fonts.html" %} From 685e60e8b4994675c78ffe21739cc90db48eed35 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 9 Mar 2020 07:00:04 +0100 Subject: [PATCH 139/189] style: fix flake8 error --- apps/aklub/management/commands/build_contenttools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/aklub/management/commands/build_contenttools.py b/apps/aklub/management/commands/build_contenttools.py index 3a417cb75..533748e41 100644 --- a/apps/aklub/management/commands/build_contenttools.py +++ b/apps/aklub/management/commands/build_contenttools.py @@ -124,7 +124,7 @@ def __copy_coffee_scripts(self): for script_file in scripts: shutil.copy( script_file, - self.cttools_target_scripts_dir / script_file.name + self.cttools_target_scripts_dir / script_file.name, ) def __copy_sass_styles(self): @@ -133,7 +133,7 @@ def __copy_sass_styles(self): for style_file in styles: shutil.copy( style_file, - self.cttools_target_styles_dir / style_file.name + self.cttools_target_styles_dir / style_file.name, ) _ui_scss_file = self.cttools_target_styles_dir / '_ui.scss' @@ -222,7 +222,7 @@ def __copy_fonts(self): font_icon_file = self.cttools_src_fonts_dir / 'icons.woff' shutil.copy( font_icon_file, - self.cttools_target_fonts_dir / font_icon_file.name + self.cttools_target_fonts_dir / font_icon_file.name, ) def __copy_gruntfile(self): From 52fdffa6ae04a0c2c4102ff5e7e082b311f1d1ba Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Mon, 9 Mar 2020 08:30:26 +0100 Subject: [PATCH 140/189] fix: icon_name model field choices lazy loading --- apps/html_template_editor/models.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index eaa373613..650bd962f 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -5,6 +5,7 @@ # from django.contrib.auth import get_user_model from django.contrib.postgres.fields import JSONField from django.db import models +from django.utils.functional import lazy from django.utils.html import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -166,7 +167,7 @@ class Meta: icon_name = models.CharField( verbose_name=_("Icon name"), - choices=get_social_media_icons_names(), + choices=(), max_length=20, null=True, blank=True, @@ -177,6 +178,12 @@ class Meta: blank=True, ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._meta.get_field('icon_name').choices = lazy( + get_social_media_icons_names, + )() + def __str__(self): return f'{self.icon_name} : {self.url}' From 14a69cd0e67c001fb0997b51777ddfcd133ba47b Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Wed, 11 Mar 2020 05:20:23 +0100 Subject: [PATCH 141/189] style: fix indent --- .../aklub/js/template_widget/src/script/app_init.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/aklub/static/aklub/js/template_widget/src/script/app_init.coffee b/apps/aklub/static/aklub/js/template_widget/src/script/app_init.coffee index 650b12256..bdcf708fc 100644 --- a/apps/aklub/static/aklub/js/template_widget/src/script/app_init.coffee +++ b/apps/aklub/static/aklub/js/template_widget/src/script/app_init.coffee @@ -4,7 +4,7 @@ $(document).ready -> showPopoverDialog = 750 showPopoverDialogDelay = 3000 popoverDialogBackdrop = true - + htmlTemplateFieldWidget = new HtmlTemplateFormFieldWidget( 'edit_template_dialog', 'edit_template', @@ -26,7 +26,7 @@ $(document).ready -> htmlTemplateEnFieldWidget = new HtmlTemplateFormFieldWidget( 'edit_template_en_dialog', 'edit_template_en', - getTemplateNameDialogShowDelay, + getTemplateNameDialogShowDelay, 'id_template_en_name', 'id_template_en_type', 'id_hidden_template_en', @@ -39,4 +39,4 @@ $(document).ready -> popoverDialogBackdrop, showPopoverDialogDelay, 'edit_template_en_btn' - ) \ No newline at end of file + ) From c05ccd8fb96643950309f1b96847d51406deefa1 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Wed, 11 Mar 2020 07:34:24 +0100 Subject: [PATCH 142/189] style: template_widget * line width 80 characters * indent code --- .../static/aklub/js/template_widget.min.js | 2 +- .../src/script/edit_template_dialog.coffee | 17 +- .../script/get_template_name_dialog.coffee | 29 ++- .../src/script/html_template_widget.coffee | 65 +++--- .../src/script/popover_dialog.coffee | 48 +++-- .../src/script/process_template.coffee | 200 +++++++++++------- 6 files changed, 213 insertions(+), 148 deletions(-) diff --git a/apps/aklub/static/aklub/js/template_widget.min.js b/apps/aklub/static/aklub/js/template_widget.min.js index 538c2dd41..b988daad2 100644 --- a/apps/aklub/static/aklub/js/template_widget.min.js +++ b/apps/aklub/static/aklub/js/template_widget.min.js @@ -1,2 +1,2 @@ /*! HtmlTemplateEditorDialog v0.0.1 by Tomas Zigo */ -(function(){var a,b,c,d,e,f,g={}.hasOwnProperty,h=function(a,b){function c(){this.constructor=a}for(var d in b)g.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},i=function(a,b){return function(){return a.apply(b,arguments)}};b=function(){function a(){}return a.prototype.getIdFormat=function(a){return"#"+a},a.prototype.getClassFormat=function(a){return"."+a},a.prototype.getNameFormat=function(a,b){return a+"[name='"+b+"']"},a}(),$(document).ready(function(){var a,b,c,e;return a=500,c=750,e=3e3,b=!0,new d("edit_template_dialog","edit_template",a,"id_template_name","id_template_type","id_hidden_template","field-template","field-template_textarea","id_template_textarea","template_page","id_template",c,b,e,"edit_template_btn"),new d("edit_template_en_dialog","edit_template_en",a,"id_template_en_name","id_template_en_type","id_hidden_template_en","field-template_en","field-template_en_textarea","id_template_en_textarea","template_en_page","id_template_en",c,b,e,"edit_template_en_btn")}),e=function(b){function c(a,b,c,d,e,f,g,h,i){this._title=a,this._editTemplateModalDialogId=c,this._backdrop=b,this._$templateNameField=d,this._destroyDelay=e,this._$mountElement=f,this._popoverModalDialogEditTemplateLinkElementId=g,this._$editTemplateModalDialogPageContainer=h,this._$hiddenTemplateField=i,this._$templateDivField=f}return h(c,b),c.editTemplateLinkText=gettext("Edit template"),c.prototype.show=function(b){return b.click(function(b){return function(c){var d,e,f,g;return c.preventDefault(),f=b._$templateNameField.find("option:selected"),e=f.text(),g=f.val().split(":"),d=new a(b._editTemplateModalDialogId,b._popoverModalDialogEditTemplateLinkElementId,b._$editTemplateModalDialogPageContainer,b._$hiddenTemplateField,b._$templateDivField),d.mount(e,"openViaPopoverDialog",g),b.unmount()}}(this)),this.delay(this._destroyDelay,function(a){return function(){return a.unmount()}}(this))},c.prototype.mount=function(a,b){var c;return c=this._getOpt(),c.offsetTop=a,c.offsetLeft=b,WebuiPopovers.show(this._$mountElement,c)},c.prototype.unmount=function(){return this._$mountElement.webuiPopover("hide"),this._$mountElement.webuiPopover("destroy")},c.prototype.delay=function(a,b){return setTimeout(b,a)},c.getContent=function(a,b){var c,d;return c=$(""),c.attr({id:this.dialogId,href:""}),d={id:a,"data-type":"modal","data-target":b,"data-fullscreen":"true","data-overlayClick":"true"},c.attr(d),c.html(""+this.editTemplateLinkText+""),c},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b){return a.show(b)}}(this),a=c.getContent(this._popoverModalDialogEditTemplateLinkElementId,this.getIdFormat(this._editTemplateModalDialogId)),{placement:"top-right",trigger:"manual,",title:this._title,content:a,offsetTop:0,offsetLeft:0,animation:"fade",backdrop:this._backdrop,onShow:b}},c}(b),c=function(b){function c(a,b,c,d,e,f,g,h){this._getBtns=i(this._getBtns,this),this._confirmTemplateName=i(this._confirmTemplateName,this),this._dialogId="get_template_name",this._title=gettext("Template name"),this._showDelay=a,this._confirmBtnId="confirm_btn",this._$templateDivField=b,this._templateNameInputId="template_name",this._errorListCSSClass="errorlist",this._templateNameFieldId=c,this._newEmptyTemplateName=null,this._editTemplateModalDialogId=d,this._popoverModalDialogEditTemplateLinkElementId=e,this._templateName=f,this._$editTemplateModalDialogPageContainer=g,this._$hiddenTemplateField=h}return h(c,b),c.prototype.unmount=function(){return $(this.getIdFormat(this._dialogId)).dialog("destroy")},c.prototype.mount=function(){return this._getContainer().dialog(this._getOpt())},c.prototype._getContainer=function(){var a,b,c,d,e,f;return e=$("
      "),e.attr({id:this._dialogId}),f=$("
      "),f.attr({id:"template_name_form"}),a=$("
      "),d=$("
        "),d.attr({class:this._errorListCSSClass}),c=$(""),c.attr({for:this._templateNameInputId}),c.text(gettext("Template name")),b=$(""),b.attr({id:this._templateNameInputId,name:this._templateNameInputId}),a.append(c),a.append(b),a.append(d),e.append(f.append(a)),e},c.prototype.getNeEmptyTemplateName=function(){return this._newEmptyTemplateName},c.prototype._triggerConfirmBtn=function(){var a;return a=function(a){return function(b){if(b.keyCode===$.ui.keyCode.ENTER)return $(a.getIdFormat(a._confirmBtnId)).click()}}(this),$(this.getIdFormat(this._dialogId)).keypress(a)},c.prototype._checkRegexp=function(a,b,c){var d;return!!b.test(a.val())||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
      • "+c+"
      • "),!1)},c.prototype._checkTemplateName=function(a,b,c){var d,e;return e=[],$(this.getIdFormat(this._templateNameFieldId)+" > option").each(function(){return e.push($(this).text())}),!(e.indexOf(b)>-1)||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
      • "+c+"
      • "),!1)},c.prototype._validateTemplateName=function(){var a,b,c,d;return d=!0,a=$(this.getIdFormat(this._templateNameInputId)),$(this.getClassFormat(this._errorListCSSClass)).html(""),c=gettext("Template name may consist of a-z, 0-9, underscores, not spaces and must begin with a letter,and be lowercase."),d=d&&this._checkRegexp(a,/^[a-z]([0-9a-z_])+$/,c),c=gettext("Template name exist."),b=$(this.getIdFormat(this._templateNameInputId)),d=d&&this._checkTemplateName(b,a.val(),c)},c.prototype._confirmTemplateName=function(){var b,c;if(this._validateTemplateName())return this._newEmptyTemplateName=$(this.getIdFormat(this._templateNameInputId)).val(),this.unmount(),this._setNewEmptyTemplateName(),c=[this._newEmptyTemplateName],b=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),b.mount(this._templateName,"openViaSelectBox",c)},c.prototype._setNewEmptyTemplateName=function(){if(null!=this._newEmptyTemplateName)return sessionStorage.setItem("newEmptyTemplateName",this._newEmptyTemplateName)},c.prototype._getBtns=function(){var a,b;return a={},b=function(a){return function(){return a._confirmTemplateName()}}(this),a.ok={text:gettext("Ok"),id:this._confirmBtnId,click:b},a},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b,c){return a._triggerConfirmBtn()}}(this),a=function(a){return function(b,c){return a.unmount(),a._$templateDivField.html("")}}(this),{modal:!0,title:this._title,show:{effect:"fade",delay:this._showDelay},resizable:!1,open:b,beforeClose:a,buttons:this._getBtns()}},c}(b),a=function(a){function b(a,b,c,d,e){this._closeEditTemplateModalDialog=i(this._closeEditTemplateModalDialog,this),this._dialogId=a,this._sessionStorageKey="dbTemplateName",this._popoverEditTemplateLinkElementId=b,this._closeBtnClass="mdl-close",this._$editTemplateModalDialogPageContainer=c,this._$hiddenTemplateField=d,this._$templateDivField=e,this._popoverDialog=null}return h(b,a),b.prototype.mount=function(a,b,c){var d,f;if(sessionStorage.removeItem(this._sessionStorageKey),this._popoverDialog||(this._popoverDialog=e.getContent(this._popoverEditTemplateLinkElementId,this.getIdFormat(this._dialogId)),$("body").append(this._popoverDialog)),this.init(this.getIdFormat(this._popoverEditTemplateLinkElementId)),c.length>1?(f=this._setPageContainerArgs(a,"aklub:get_email_template_from_db"),sessionStorage.setItem(this.sessionStorageKey,f)):f=this._setPageContainerArgs(a,"aklub:get_email_template"),this.show(this.getIdFormat(this._dialogId)),d=$(this.getIdFormat(this._dialogId)+" "+this.getClassFormat(this._closeBtnClass)),d.off("click",this._closeEditTemplateModalDialog),d.on("click",this._closeEditTemplateModalDialog),"openViaSelectBox"===b)return $(this.getIdFormat(this._popoverEditTemplateLinkElementId)).remove()},b.prototype._closeEditTemplateModalDialog=function(a){var b;return b=this._getIframeTemplateContent(),new f(b,$(b).find("article"),this._$templateDivField,this._$hiddenTemplateField)},b.prototype._getIframeTemplateContent=function(){var a;return a=document.querySelector(this.getIdFormat(this._$editTemplateModalDialogPageContainer.attr("id"))),null!=a?a.contentDocument:void 0},b.prototype._setPageContainerArgs=function(a,b){var c,d;return d=window.reverse(b,{template_name:a}),c={data:d,width:$(window).width(),height:$(window).height()},this._$editTemplateModalDialogPageContainer.attr(c),d},b.prototype.init=function(a){return $(a).mdl()},b.prototype.show=function(a){return mdl_open(a)},b}(b),d=function(b){function d(a,b,c,d,e,f,g,h,j,k,l,m,n,o,p){this._confirmTemplateEdit=i(this._confirmTemplateEdit,this),this._loadTemplateContent=i(this._loadTemplateContent,this),this._showEditTemplateModalDialog=i(this._showEditTemplateModalDialog,this),this._editTemplateBtnClickEvt=i(this._editTemplateBtnClickEvt,this),this._mouseMoveOverTemplateEvt=i(this._mouseMoveOverTemplateEvt,this),this._replaceTemplateFieldContent=i(this._replaceTemplateFieldContent,this),this._templateDivFieldId=l,this._hiddenTemplateFieldId=f,this._templateNameFieldId=d,this._getTemplateNameDialogShowDelay=c,this._editTemplateModalDialogId=a,this._popoverModalDialogEditTemplateLinkElementId=b,this._templateDivFormFieldContainerCSSClass=g,this._templateTextareaFieldId=j,this._editTemplateModalDialogPageContainerId=k,this._$templateDivFieldContainer=null,this._$templateTextareaFieldContainer=null,this._templateTextareaFieldValue="",this._showPopoverModalDialogDelay=m,this._popoverModalDialogBackDrop=n,this._popoverModalDialogDestroyDelay=o,this._templateTextAreaFormFieldContainerCSSClass=h,this._templateTypeFieldId=e,this._newEmptyTemplateName=null,this._getTemplateNameDialog=null,this._popoverDialogOpts=null,this._editTemplateBtnName=p,this._editTemplateModalDialog=null,this._cacheDom(),this._bindEvents(),this._templateFormFieldWidgetInit()}return h(d,b),d.prototype._cacheDom=function(){return this._$templateDivField=$(this.getIdFormat(this._templateDivFieldId)),this._$hiddenTemplateField=$(this.getIdFormat(this._hiddenTemplateFieldId)),this._$templateNameField=$(this.getIdFormat(this._templateNameFieldId)),this._$templateTypeField=$(this.getIdFormat(this._templateTypeFieldId)),this._$templateDivFormFieldContainer=$(this.getClassFormat(this._templateDivFormFieldContainerCSSClass)),this._$templateTextAreaFormFieldContainer=$(this.getClassFormat(this._templateTextAreaFormFieldContainerCSSClass)),this._$templateTextareaField=$(this.getIdFormat(this._templateTextareaFieldId)),this._$editTemplateModalDialogPageContainer=$(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),this._$editTemplateBtn=$(this.getNameFormat("input",this._editTemplateBtnName))},d.prototype._bindEvents=function(){return this._$templateNameField.bind("change",this._showEditTemplateModalDialog),this._$templateTypeField.bind("change",this._replaceTemplateFieldContent),this._$editTemplateModalDialogPageContainer.bind("load",this._loadTemplateContent),this._$editTemplateBtn.bind("click",this._editTemplateBtnClickEvt)},d.prototype._templateFormFieldWidgetInit=function(){var a;return this._$templateTextAreaFormFieldContainer.addClass("hidden"),"new"===this._$templateTypeField.val()?(this._exchangeFormFieldContainerContent(),a=this._$templateDivField.html(),this._$templateTextareaField.html(a),this._templateTextareaFieldValue=a,this._$templateDivField.html("")):"existed"===this._$templateTypeField.val()&&this._$hiddenTemplateField.val(this._$templateDivField.html()),this._initEditTemplateModalDialog(),this._enableDisableEditBtn(!0)},d.prototype._enableDisableEditBtn=function(a,b){var c;return null==b&&(b=!0),b?(c=this._$templateNameField.find("option:selected").val(),0===c.length?this._$editTemplateBtn.prop("disabled",a):void 0):this._$editTemplateBtn.prop("disabled",a)},d.prototype._getTemplateFieldContainer=function(){this._$templateDivFieldContainer=this._$templateDivFormFieldContainer,this._$templateTextareaFieldContainer=this._$templateTextAreaFormFieldContainer},d.prototype._checkTemplateFieldWidget=function(){var a;return this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.find("textarea"),a.length?"textarea":"div"},d.prototype._replaceTemplateFieldContent=function(a){var b;b=$(a.target).val(),"new"===b?"textarea"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContent():"existed"===b&&"div"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContentBack()},d.prototype._exchangeFormFieldContainerContentBack=function(){var a;return this._disableFormFieldWysiwygEditor(),this._$templateNameField.attr("disabled",!1),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._$editTemplateBtn.off("click",this._editTemplateBtnClickEvt),this._$editTemplateBtn.on("click",this._editTemplateBtnClickEvt)},d.prototype.delay=function(a,b){return setTimeout(b,a)},d.prototype._mouseMoveOverTemplateEvt=function(a){var b,c,d,e,f;return b=$(a.currentTarget).offset(),e=a.pageX-b.left,f=a.pageY-b.top,d=f,c=e,this.delay(this._showPopoverModalDialogDelay,function(a){return function(){return a._initPopoverModalDialog(d,c)}}(this))},d.prototype._initPopoverModalDialog=function(a,b){return this._popoverDialogOpts.mount(a,b)},d.prototype._bindPopoverDialogMouseMoveEvt=function(){return this._popoverDialogOpts=new e(gettext("Edit template"),this._popoverModalDialogBackDrop,this._editTemplateModalDialogId,this._$templateNameField,this._popoverModalDialogDestroyDelay,this._$templateDivField,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._$templateDivField.mousemove(this._mouseMoveOverTemplateEvt)},d.prototype._initEditTemplateModalDialog=function(){return this._editTemplateModalDialog=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField)},d.prototype._editTemplateBtnClickEvt=function(a){var b,c,d;return a.preventDefault(),c=this._$templateNameField.find("option:selected"),b=c.text(),d=c.val().split(":"),this._editTemplateModalDialog.mount(b,"openViaBtn",d)},d.prototype._exchangeFormFieldContainerContent=function(){var a;return this._$templateTextareaField.html(this._templateTextareaFieldValue),this._$templateNameField.attr("disabled",!0),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),this._$templateDivFieldContainer.find("label").attr("for",this._templateDivFieldId),this._$templateDivFieldContainer.find("label").text(this._$templateTextareaFieldContainer.find("label").text()),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._enableFormFieldWysiwygEditor(),this._$templateTextAreaFormFieldContainer.addClass("hidden"),this._$hiddenTemplateField.val("")},d.prototype._enableFormFieldWysiwygEditor=function(){return django_wysiwyg.enable(this._templateDivFieldId.slice(3))},d.prototype._disableFormFieldWysiwygEditor=function(){return django_wysiwyg.disable(this._templateDivFieldId.slice(3))},d.prototype._exchangeContent=function(){var a,b;this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.children(),b=this._$templateTextareaFieldContainer.children(),this._$templateDivFieldContainer.html(b),this._$templateTextareaFieldContainer.html(a)},d.prototype._showEditTemplateModalDialog=function(b){var d,e,f,g;if(sessionStorage.removeItem("newEmptyTemplateName"),e=""+this.getIdFormat(b.target.id),f=$(e+" option:selected").text(),$(""+e).val().length>0)return"new_empty_template"===f?(this._getTemplateNameDialog=new c(this._getTemplateNameDialogShowDelay,this._$templateDivField,this._templateNameFieldId,this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,f,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._getTemplateNameDialog.mount()):(this._enableDisableEditBtn(!1,!1),g=this._$templateNameField.find("option:selected").val().split(":"),d=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),d.mount(f,"openViaSelectBox",g))},d.prototype._loadTemplateContent=function(a){var b;if(this._newEmptyTemplateName=null!=(b=this._getTemplateNameDialog)?b.getNeEmptyTemplateName():void 0,null!=this._newEmptyTemplateName)return this._addEditTemplateConfirmEvent()},d.prototype._addEditTemplateConfirmEvent=function(){var a,b,c,d;return b="ct-ignition__button--confirm",d=document.querySelector(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),c=d.contentDocument,a=$(c).find(this.getClassFormat(b)),a.unbind("click"),a.bind("click",this._confirmTemplateEdit)},d.prototype._confirmTemplateEdit=function(){if(null!=this._newEmptyTemplateName)return this._addNewSelectOpt(this._newEmptyTemplateName,this._$templateNameField),this._eraseNewTemplateName()},d.prototype._eraseNewTemplateName=function(){return this._newEmptyTemplateName=null},d.prototype._addNewSelectOpt=function(a,b){var c,d,e,f;if(d=$(this.getIdFormat(this._templateNameFieldId)+" option"),f=$.map(d,function(a){return a.value}),e="new_empty_template:"+a,-1===f.indexOf(e))return c=new Option(a,e),b.append(c),b.val(e)},d}(b),f=function(a){function b(a,b,c,d){this._htmlDoc=a,this._$editTemplatePageContainer=b,this._$templateDivField=c,this._$hiddenTemplateField=d,this._videoClass="video",this._verticalSpaceClassName="vertical-space",this._textContainerTags=["p","ul","ol","h1","h2","h3","h4","h5","h6"],this.convertVideoIframeToImgThumbnail()}return h(b,a),b.prototype.convertVideoIframeToImgThumbnail=function(){var a,b,c,d,e,f;return c=[],b=!1,f=new RegExp("youtube.com.*(v=|/embed/)(.{11})"),e=new RegExp("vimeo.com.*./(.*)"),a=function(a,b,c,d,e){return a.css("float",e),b.append(a),b.append(c),$(d).after(b),$(d).remove()},d=function(b){var c;return"vimeo"===this.videoType&&(c={src:b.thumbnail_url,width:b.thumbnail_width,height:b.thumbnail_height},this.$videoThumbnail.attr(c)),this.$videoCaption.text(b.title),a(this.$videoThumbnail,this.$clickableVideoThumbnail,this.$videoCaption,this.element,this.iframeCssFloatProperty)},$(this._htmlDoc).find("iframe").each(function(a){return function(g,h){var i,j,k,l,m,n,o,p,q,r,s,t,u;return p=$(h).attr("src"),n=$(h).css("float"),q=$(h).attr("width"),o=$(h).attr("height"),i=$(''),i.attr("href",p),k=$(''),k.attr({width:q,height:o}),j=$("
        "),m={"text-align":"center","font-size":"12px"},j.css(m),p.match(f)?(b=!0,t=p.match(f).pop(),i.attr("id",t),i.addClass("youtube"),u="//img.youtube.com/vi/"+t+"/0.jpg",k.attr("src",u),r="http://www.youtube.com/watch?v="+t+"&",s="https://noembed.com/embed?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"youtube"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):p.match(e)?(b=!0,t=p.match(e).pop(),i.attr("id",t),i.addClass("vimeo"),r="https://vimeo.com/"+t+"&width="+q+"&height="+o,s="https://vimeo.com/api/oembed.json?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"vimeo"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):void 0}}(this)),$.when.apply(null,c).done(function(a){return function(){return a.convertCssToInlineStyle()}}(this))},b.prototype.getAjaxData=function(a,b,c,d,e){return null==d&&(d="GET"),null==e&&(e="json"),{type:d,url:b,jsonp:"callback",dataType:e,context:a,success:c}},b.prototype.convertCssToInlineStyle=function(){return this.inlineStyler($(this._htmlDoc)),this._fixContentWidth(),this._fixMargin(),this._replaceImgSrc(),this._convertTextCssPositionToTable(),this._eraseVerticalSpaceContent(),this._$templateDivField.html(this._$editTemplatePageContainer),this._$hiddenTemplateField.val(this._$editTemplatePageContainer.html())},b.prototype.inlineStyler=function(a){return a.inlineStyler()},b.prototype.getElementPosition=function(a,b,c){var d,e,f,g;return null==b&&(b=""),null==c&&(c=""),d=$("
        "),d.attr({cellspacing:0,cellpadding:0,border:0,width:"100%"}),e=$("
        "),d.attr({cellspacing:0,cellpadding:0,border:0,width:"100%"}),e=$("
        + + + {% if header.logo_position == "left" %} + + + {% else %} + + + {% endif %} + + +
        + + + {{ header.text }}{{ header.logo_position }} + + {{ header.text }}{{ header.logo_position }} + + +
        + From 1fb1bd4b6a938c094ab64ea471dc9d1adf300dfe Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Tue, 14 Apr 2020 18:46:02 +0200 Subject: [PATCH 149/189] feat: include header.html file in the all email templates --- .../templates/email_templates/automat_green_newsletter.html | 1 + .../templates/email_templates/automat_purple_newsletter.html | 1 + apps/aklub/templates/email_templates/automat_red_newsletter.html | 1 + apps/aklub/templates/email_templates/new_empty_template.html | 1 + 4 files changed, 4 insertions(+) diff --git a/apps/aklub/templates/email_templates/automat_green_newsletter.html b/apps/aklub/templates/email_templates/automat_green_newsletter.html index 40d19f84c..bc482e5ed 100644 --- a/apps/aklub/templates/email_templates/automat_green_newsletter.html +++ b/apps/aklub/templates/email_templates/automat_green_newsletter.html @@ -1,5 +1,6 @@ {% extends 'email_templates/base.html' %} {% load static %} {% block content %}
        + {% if header %} {% include "email_templates/header.html" %} {% endif %} + {% if header %} {% include "email_templates/header.html" %} {% endif %}
        + {% if header %} {% include "email_templates/header.html" %} {% endif %}
        + {% if header %} {% include "email_templates/header.html" %} {% endif %}
        Date: Tue, 21 Apr 2020 06:52:43 +0200 Subject: [PATCH 150/189] chore: add merged migration files --- apps/aklub/migrations/0075_merge_20200421_0651.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/aklub/migrations/0075_merge_20200421_0651.py diff --git a/apps/aklub/migrations/0075_merge_20200421_0651.py b/apps/aklub/migrations/0075_merge_20200421_0651.py new file mode 100644 index 000000000..30bf1d892 --- /dev/null +++ b/apps/aklub/migrations/0075_merge_20200421_0651.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.12 on 2020-04-21 04:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aklub', '0074_auto_20200417_1112'), + ('aklub', '0074_merge_20200414_0547'), + ] + + operations = [ + ] From 8eb94e02d1af1077c0941fc9ff07a7cfb40bd247 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Tue, 21 Apr 2020 08:06:22 +0200 Subject: [PATCH 151/189] feat: validate template header logo image format --- apps/html_template_editor/validators.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/html_template_editor/validators.py b/apps/html_template_editor/validators.py index fd0676d2c..220b7ce5c 100644 --- a/apps/html_template_editor/validators.py +++ b/apps/html_template_editor/validators.py @@ -7,11 +7,19 @@ def validate_logo_image(image): max_width = 300 megabyte_limit = 5.0 * 1024 * 1024 + allowed_image_formats = ['JPEG', 'PNG'] - width, height = Image.open(image).size + img = Image.open(image) + width, height = img.size image_file_size = image.tell() + image_file_format = img.format + + if image_file_format not in allowed_image_formats: + raise ValidationError( + _("Allowed image format is %(image_formats)s "), + params={'image_formats': ', '.join(allowed_image_formats)}, + ) - print(width, image_file_size) if width > max_width: raise ValidationError( _("Width is larger than what is allowed %(max_width)s px"), From ee3f28796980b441dc736091013f52668ff1b377 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sat, 25 Apr 2020 05:32:40 +0200 Subject: [PATCH 152/189] feat: add TemplateFooter model name, user field --- .../migrations/0008_auto_20200425_0532.py | 26 +++++++++++++++++++ apps/html_template_editor/models.py | 12 +++++++++ 2 files changed, 38 insertions(+) create mode 100644 apps/html_template_editor/migrations/0008_auto_20200425_0532.py diff --git a/apps/html_template_editor/migrations/0008_auto_20200425_0532.py b/apps/html_template_editor/migrations/0008_auto_20200425_0532.py new file mode 100644 index 000000000..0d3100bfd --- /dev/null +++ b/apps/html_template_editor/migrations/0008_auto_20200425_0532.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.12 on 2020-04-25 03:32 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('html_template_editor', '0007_templateheader'), + ] + + operations = [ + migrations.AddField( + model_name='templatefooter', + name='name', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Footer name'), + ), + migrations.AddField( + model_name='templatefooter', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index 1fd8f3b85..ecbf3534b 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -193,6 +193,12 @@ class Meta: verbose_name = _("Template footer") verbose_name_plural = _("Template footers") + name = models.CharField( + verbose_name=_("Footer name"), + max_length=255, + null=True, + blank=True, + ) company_name = models.CharField( verbose_name=_("Company name"), max_length=80, @@ -225,6 +231,12 @@ class Meta: to=CompanySocialMedia, blank=True, ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + models.SET_NULL, + blank=True, + null=True, + ) show = models.BooleanField( verbose_name=_("Show"), default=False, From 4fb0f2ae74f9c5c68cf70c54730d199a89fdaabe Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sat, 25 Apr 2020 05:51:13 +0200 Subject: [PATCH 153/189] feat: add TemplateFooterAdminForm admin model form class --- apps/html_template_editor/admin.py | 36 +++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/html_template_editor/admin.py b/apps/html_template_editor/admin.py index 0745723e6..73a03db3a 100644 --- a/apps/html_template_editor/admin.py +++ b/apps/html_template_editor/admin.py @@ -50,8 +50,28 @@ class CompanySocialMediaAdminInline(admin.StackedInline): extra = 0 +class TemplateFooterAdminForm(forms.ModelForm): + class Meta: + model = TemplateFooter + exclude = ( + 'url', + 'phone', + 'email', + 'social_media', + 'user', + ) + + def save(self, commit=True): + instance = super(TemplateFooterAdminForm, self).save(commit=False) + instance.user = self.user + if commit: + instance.save() + return instance + + @admin.register(TemplateFooter) class TemplateFooterAdminInline(admin.ModelAdmin): + form = TemplateFooterAdminForm list_display = ( 'company_name', 'address', @@ -59,13 +79,14 @@ class TemplateFooterAdminInline(admin.ModelAdmin): 'get_email', 'get_url', 'get_social_media', + 'user', 'show', ) - exclude = ( - 'url', - 'phone', - 'email', - 'social_media', + list_filter = ( + 'name', + 'company_name', + 'user', + 'show', ) inlines = [ CompanyPhoneAdminInline, @@ -74,6 +95,11 @@ class TemplateFooterAdminInline(admin.ModelAdmin): CompanySocialMediaAdminInline, ] + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + form.user = request.user + return form + class TemplateHeaderAdminForm(forms.ModelForm): class Meta: From a974170770b5e62115993b5d4bce85d14d9f89a1 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sat, 25 Apr 2020 06:18:44 +0200 Subject: [PATCH 154/189] feat: set TemplateFooter model fields short description --- apps/html_template_editor/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/html_template_editor/models.py b/apps/html_template_editor/models.py index ecbf3534b..95586041f 100644 --- a/apps/html_template_editor/models.py +++ b/apps/html_template_editor/models.py @@ -248,16 +248,20 @@ def __str__(self): def get_url(self): return mark_safe("
        ".join([o.url for o in self.url.all()])) + get_url.short_description = _('Url') def get_email(self): return mark_safe("
        ".join([o.email for o in self.email.all()])) + get_email.short_description = _('Email') def get_phone(self): return mark_safe("
        ".join([str(o.phone) for o in self.phone.all()])) + get_phone.short_description = _('Phone number') def get_social_media(self): objs = self.social_media.all() return mark_safe("
        ".join([f"{o.icon_name}: {o.url}" for o in objs])) + get_social_media.short_description = _('Social media link') class TemplateHeader(models.Model): From 424125726cb37f8257d5a66e36c85ff10905dbb9 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Wed, 29 Apr 2020 08:15:51 +0200 Subject: [PATCH 155/189] fix: remove use django wysiwyg TinyMCE editor from template form field widget --- apps/aklub/static/aklub/js/template_widget.min.js | 2 +- .../src/script/html_template_widget.coffee | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/apps/aklub/static/aklub/js/template_widget.min.js b/apps/aklub/static/aklub/js/template_widget.min.js index b988daad2..534455731 100644 --- a/apps/aklub/static/aklub/js/template_widget.min.js +++ b/apps/aklub/static/aklub/js/template_widget.min.js @@ -1,2 +1,2 @@ /*! HtmlTemplateEditorDialog v0.0.1 by Tomas Zigo */ -(function(){var a,b,c,d,e,f,g={}.hasOwnProperty,h=function(a,b){function c(){this.constructor=a}for(var d in b)g.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},i=function(a,b){return function(){return a.apply(b,arguments)}};b=function(){function a(){}return a.prototype.getIdFormat=function(a){return"#"+a},a.prototype.getClassFormat=function(a){return"."+a},a.prototype.getNameFormat=function(a,b){return a+"[name='"+b+"']"},a}(),$(document).ready(function(){var a,b,c,e;return a=500,c=750,e=3e3,b=!0,new d("edit_template_dialog","edit_template",a,"id_template_name","id_template_type","id_hidden_template","field-template","field-template_textarea","id_template_textarea","template_page","id_template",c,b,e,"edit_template_btn"),new d("edit_template_en_dialog","edit_template_en",a,"id_template_en_name","id_template_en_type","id_hidden_template_en","field-template_en","field-template_en_textarea","id_template_en_textarea","template_en_page","id_template_en",c,b,e,"edit_template_en_btn")}),e=function(b){function c(a,b,c,d,e,f,g,h,i){this._title=a,this._editTemplateModalDialogId=c,this._backdrop=b,this._$templateNameField=d,this._destroyDelay=e,this._$mountElement=f,this._popoverModalDialogEditTemplateLinkElementId=g,this._$editTemplateModalDialogPageContainer=h,this._$hiddenTemplateField=i,this._$templateDivField=f}return h(c,b),c.editTemplateLinkText=gettext("Edit template"),c.prototype.show=function(b){return b.click(function(b){return function(c){var d,e,f,g;return c.preventDefault(),f=b._$templateNameField.find("option:selected"),e=f.text(),g=f.val().split(":"),d=new a(b._editTemplateModalDialogId,b._popoverModalDialogEditTemplateLinkElementId,b._$editTemplateModalDialogPageContainer,b._$hiddenTemplateField,b._$templateDivField),d.mount(e,"openViaPopoverDialog",g),b.unmount()}}(this)),this.delay(this._destroyDelay,function(a){return function(){return a.unmount()}}(this))},c.prototype.mount=function(a,b){var c;return c=this._getOpt(),c.offsetTop=a,c.offsetLeft=b,WebuiPopovers.show(this._$mountElement,c)},c.prototype.unmount=function(){return this._$mountElement.webuiPopover("hide"),this._$mountElement.webuiPopover("destroy")},c.prototype.delay=function(a,b){return setTimeout(b,a)},c.getContent=function(a,b){var c,d;return c=$(""),c.attr({id:this.dialogId,href:""}),d={id:a,"data-type":"modal","data-target":b,"data-fullscreen":"true","data-overlayClick":"true"},c.attr(d),c.html(""+this.editTemplateLinkText+""),c},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b){return a.show(b)}}(this),a=c.getContent(this._popoverModalDialogEditTemplateLinkElementId,this.getIdFormat(this._editTemplateModalDialogId)),{placement:"top-right",trigger:"manual,",title:this._title,content:a,offsetTop:0,offsetLeft:0,animation:"fade",backdrop:this._backdrop,onShow:b}},c}(b),c=function(b){function c(a,b,c,d,e,f,g,h){this._getBtns=i(this._getBtns,this),this._confirmTemplateName=i(this._confirmTemplateName,this),this._dialogId="get_template_name",this._title=gettext("Template name"),this._showDelay=a,this._confirmBtnId="confirm_btn",this._$templateDivField=b,this._templateNameInputId="template_name",this._errorListCSSClass="errorlist",this._templateNameFieldId=c,this._newEmptyTemplateName=null,this._editTemplateModalDialogId=d,this._popoverModalDialogEditTemplateLinkElementId=e,this._templateName=f,this._$editTemplateModalDialogPageContainer=g,this._$hiddenTemplateField=h}return h(c,b),c.prototype.unmount=function(){return $(this.getIdFormat(this._dialogId)).dialog("destroy")},c.prototype.mount=function(){return this._getContainer().dialog(this._getOpt())},c.prototype._getContainer=function(){var a,b,c,d,e,f;return e=$("
        "),e.attr({id:this._dialogId}),f=$("
        "),f.attr({id:"template_name_form"}),a=$("
        "),d=$("
          "),d.attr({class:this._errorListCSSClass}),c=$(""),c.attr({for:this._templateNameInputId}),c.text(gettext("Template name")),b=$(""),b.attr({id:this._templateNameInputId,name:this._templateNameInputId}),a.append(c),a.append(b),a.append(d),e.append(f.append(a)),e},c.prototype.getNeEmptyTemplateName=function(){return this._newEmptyTemplateName},c.prototype._triggerConfirmBtn=function(){var a;return a=function(a){return function(b){if(b.keyCode===$.ui.keyCode.ENTER)return $(a.getIdFormat(a._confirmBtnId)).click()}}(this),$(this.getIdFormat(this._dialogId)).keypress(a)},c.prototype._checkRegexp=function(a,b,c){var d;return!!b.test(a.val())||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
        • "+c+"
        • "),!1)},c.prototype._checkTemplateName=function(a,b,c){var d,e,f;return e=[],f=this.getIdFormat(this._templateNameFieldId)+" > option",$(f).each(function(){return e.push($(this).text())}),!(e.indexOf(b)>-1)||(d=a.closest("form").find(this.getClassFormat(this._errorListCSSClass)),d.append("
        • "+c+"
        • "),!1)},c.prototype._validateTemplateName=function(){var a,b,c,d;return d=!0,a=$(this.getIdFormat(this._templateNameInputId)),$(this.getClassFormat(this._errorListCSSClass)).html(""),c=gettext("Template name may consist of a-z, 0-9, underscores, not spaces and must begin with a letter, and be lowercase."),d=d&&this._checkRegexp(a,/^[a-z]([0-9a-z_])+$/,c),c=gettext("Template name exist."),b=$(this.getIdFormat(this._templateNameInputId)),d=d&&this._checkTemplateName(b,a.val(),c)},c.prototype._confirmTemplateName=function(){var b,c;if(this._validateTemplateName())return this._newEmptyTemplateName=$(this.getIdFormat(this._templateNameInputId)).val(),this.unmount(),this._setNewEmptyTemplateName(),c=[this._newEmptyTemplateName],b=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),b.mount(this._templateName,"openViaSelectBox",c)},c.prototype._setNewEmptyTemplateName=function(){if(null!=this._newEmptyTemplateName)return sessionStorage.setItem("newEmptyTemplateName",this._newEmptyTemplateName)},c.prototype._getBtns=function(){var a,b;return a={},b=function(a){return function(){return a._confirmTemplateName()}}(this),a.ok={text:gettext("Ok"),id:this._confirmBtnId,click:b},a},c.prototype._getOpt=function(){var a,b;return b=function(a){return function(b,c){return a._triggerConfirmBtn()}}(this),a=function(a){return function(b,c){return a.unmount(),a._$templateDivField.html("")}}(this),{modal:!0,title:this._title,show:{effect:"fade",delay:this._showDelay},resizable:!1,open:b,beforeClose:a,buttons:this._getBtns()}},c}(b),a=function(a){function b(a,b,c,d,e){this._closeEditTemplateModalDialog=i(this._closeEditTemplateModalDialog,this),this._dialogId=a,this._sessionStorageKey="dbTemplateName",this._popoverEditTemplateLinkElementId=b,this._closeBtnClass="mdl-close",this._$editTemplateModalDialogPageContainer=c,this._$hiddenTemplateField=d,this._$templateDivField=e,this._popoverDialog=null}return h(b,a),b.prototype.mount=function(a,b,c){var d,f;if(sessionStorage.removeItem(this._sessionStorageKey),this._popoverDialog||(this._popoverDialog=e.getContent(this._popoverEditTemplateLinkElementId,this.getIdFormat(this._dialogId)),$("body").append(this._popoverDialog)),this.init(this.getIdFormat(this._popoverEditTemplateLinkElementId)),c.length>1?(f=this._setPageContainerArgs(a,"aklub:get_email_template_from_db"),sessionStorage.setItem(this.sessionStorageKey,f)):f=this._setPageContainerArgs(a,"aklub:get_email_template"),this.show(this.getIdFormat(this._dialogId)),d=$(this.getIdFormat(this._dialogId)+" "+this.getClassFormat(this._closeBtnClass)),d.off("click",this._closeEditTemplateModalDialog),d.on("click",this._closeEditTemplateModalDialog),"openViaSelectBox"===b)return $(this.getIdFormat(this._popoverEditTemplateLinkElementId)).remove()},b.prototype._closeEditTemplateModalDialog=function(a){var b;return b=this._getIframeTemplateContent(),new f(b,$(b).find("article"),this._$templateDivField,this._$hiddenTemplateField)},b.prototype._getIframeTemplateContent=function(){var a;return a=document.querySelector(this.getIdFormat(this._$editTemplateModalDialogPageContainer.attr("id"))),null!=a?a.contentDocument:void 0},b.prototype._setPageContainerArgs=function(a,b){var c,d;return d=window.reverse(b,{template_name:a}),c={data:d,width:$(window).width(),height:$(window).height()},this._$editTemplateModalDialogPageContainer.attr(c),d},b.prototype.init=function(a){return $(a).mdl()},b.prototype.show=function(a){return mdl_open(a)},b}(b),d=function(b){function d(a,b,c,d,e,f,g,h,j,k,l,m,n,o,p){this._confirmTemplateEdit=i(this._confirmTemplateEdit,this),this._loadTemplateContent=i(this._loadTemplateContent,this),this._showEditTemplateModalDialog=i(this._showEditTemplateModalDialog,this),this._editTemplateBtnClickEvt=i(this._editTemplateBtnClickEvt,this),this._mouseMoveOverTemplateEvt=i(this._mouseMoveOverTemplateEvt,this),this._replaceTemplateFieldContent=i(this._replaceTemplateFieldContent,this),this._templateDivFieldId=l,this._hiddenTemplateFieldId=f,this._templateNameFieldId=d,this._getTemplateNameDialogShowDelay=c,this._editTemplateModalDialogId=a,this._popoverModalDialogEditTemplateLinkElementId=b,this._templateDivFormFieldContainerCSSClass=g,this._templateTextareaFieldId=j,this._editTemplateModalDialogPageContainerId=k,this._$templateDivFieldContainer=null,this._$templateTextareaFieldContainer=null,this._templateTextareaFieldValue="",this._showPopoverModalDialogDelay=m,this._popoverModalDialogBackDrop=n,this._popoverModalDialogDestroyDelay=o,this._templateTextAreaFormFieldContainerCSSClass=h,this._templateTypeFieldId=e,this._newEmptyTemplateName=null,this._getTemplateNameDialog=null,this._popoverDialogOpts=null,this._editTemplateBtnName=p,this._editTemplateModalDialog=null,this._cacheDom(),this._bindEvents(),this._templateFormFieldWidgetInit()}return h(d,b),d.prototype._cacheDom=function(){return this._$templateDivField=$(this.getIdFormat(this._templateDivFieldId)),this._$hiddenTemplateField=$(this.getIdFormat(this._hiddenTemplateFieldId)),this._$templateNameField=$(this.getIdFormat(this._templateNameFieldId)),this._$templateTypeField=$(this.getIdFormat(this._templateTypeFieldId)),this._$templateDivFormFieldContainer=$(this.getClassFormat(this._templateDivFormFieldContainerCSSClass)),this._$templateTextAreaFormFieldContainer=$(this.getClassFormat(this._templateTextAreaFormFieldContainerCSSClass)),this._$templateTextareaField=$(this.getIdFormat(this._templateTextareaFieldId)),this._$editTemplateModalDialogPageContainer=$(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),this._$editTemplateBtn=$(this.getNameFormat("input",this._editTemplateBtnName))},d.prototype._bindEvents=function(){return this._$templateNameField.bind("change",this._showEditTemplateModalDialog),this._$templateTypeField.bind("change",this._replaceTemplateFieldContent),this._$editTemplateModalDialogPageContainer.bind("load",this._loadTemplateContent),this._$editTemplateBtn.bind("click",this._editTemplateBtnClickEvt)},d.prototype._templateFormFieldWidgetInit=function(){var a;return this._$templateTextAreaFormFieldContainer.addClass("hidden"),"new"===this._$templateTypeField.val()?(this._exchangeFormFieldContainerContent(),a=this._$templateDivField.html(),this._$templateTextareaField.html(a),this._templateTextareaFieldValue=a,this._$templateDivField.html("")):"existed"===this._$templateTypeField.val()&&this._$hiddenTemplateField.val(this._$templateDivField.html()),this._initEditTemplateModalDialog(),this._enableDisableEditBtn(!0)},d.prototype._enableDisableEditBtn=function(a,b){var c;return null==b&&(b=!0),b?(c=this._$templateNameField.find("option:selected").val(),0===c.length?this._$editTemplateBtn.prop("disabled",a):void 0):this._$editTemplateBtn.prop("disabled",a)},d.prototype._getTemplateFieldContainer=function(){this._$templateDivFieldContainer=this._$templateDivFormFieldContainer,this._$templateTextareaFieldContainer=this._$templateTextAreaFormFieldContainer},d.prototype._checkTemplateFieldWidget=function(){var a;return this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.find("textarea"),a.length?"textarea":"div"},d.prototype._replaceTemplateFieldContent=function(a){var b;b=$(a.target).val(),"new"===b?"textarea"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContent():"existed"===b&&"div"!==this._checkTemplateFieldWidget()&&this._exchangeFormFieldContainerContentBack()},d.prototype._exchangeFormFieldContainerContentBack=function(){var a;return this._disableFormFieldWysiwygEditor(),this._$templateNameField.attr("disabled",!1),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._$editTemplateBtn.off("click",this._editTemplateBtnClickEvt),this._$editTemplateBtn.on("click",this._editTemplateBtnClickEvt)},d.prototype.delay=function(a,b){return setTimeout(b,a)},d.prototype._mouseMoveOverTemplateEvt=function(a){var b,c,d,e,f;return b=$(a.currentTarget).offset(),e=a.pageX-b.left,f=a.pageY-b.top,d=f,c=e,this.delay(this._showPopoverModalDialogDelay,function(a){return function(){return a._initPopoverModalDialog(d,c)}}(this))},d.prototype._initPopoverModalDialog=function(a,b){return this._popoverDialogOpts.mount(a,b)},d.prototype._bindPopoverDialogMouseMoveEvt=function(){return this._popoverDialogOpts=new e(gettext("Edit template"),this._popoverModalDialogBackDrop,this._editTemplateModalDialogId,this._$templateNameField,this._popoverModalDialogDestroyDelay,this._$templateDivField,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._$templateDivField.mousemove(this._mouseMoveOverTemplateEvt)},d.prototype._initEditTemplateModalDialog=function(){return this._editTemplateModalDialog=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField)},d.prototype._editTemplateBtnClickEvt=function(a){var b,c,d;return a.preventDefault(),c=this._$templateNameField.find("option:selected"),b=c.text(),d=c.val().split(":"),this._editTemplateModalDialog.mount(b,"openViaBtn",d)},d.prototype._exchangeFormFieldContainerContent=function(){var a;return this._$templateTextareaField.html(this._templateTextareaFieldValue),this._$templateNameField.attr("disabled",!0),this._exchangeContent(),a={id:this._templateTextareaFieldId,name:this._templateTextareaFieldId.slice(3)},this._$templateDivField.attr(a),this._$templateDivFieldContainer.find("label").attr("for",this._templateDivFieldId),this._$templateDivFieldContainer.find("label").text(this._$templateTextareaFieldContainer.find("label").text()),a={id:this._templateDivFieldId,name:this._templateDivFieldId.slice(3)},this._$templateTextareaField.attr(a),this._enableFormFieldWysiwygEditor(),this._$templateTextAreaFormFieldContainer.addClass("hidden"),this._$hiddenTemplateField.val("")},d.prototype._enableFormFieldWysiwygEditor=function(){return django_wysiwyg.enable(this._templateDivFieldId.slice(3))},d.prototype._disableFormFieldWysiwygEditor=function(){return django_wysiwyg.disable(this._templateDivFieldId.slice(3))},d.prototype._exchangeContent=function(){var a,b;this._getTemplateFieldContainer(),a=this._$templateDivFieldContainer.children(),b=this._$templateTextareaFieldContainer.children(),this._$templateDivFieldContainer.html(b),this._$templateTextareaFieldContainer.html(a)},d.prototype._showEditTemplateModalDialog=function(b){var d,e,f,g;if(sessionStorage.removeItem("newEmptyTemplateName"),e=""+this.getIdFormat(b.target.id),f=$(e+" option:selected").text(),$(""+e).val().length>0)return"new_empty_template"===f?(this._getTemplateNameDialog=new c(this._getTemplateNameDialogShowDelay,this._$templateDivField,this._templateNameFieldId,this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,f,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField),this._getTemplateNameDialog.mount()):(this._enableDisableEditBtn(!1,!1),g=this._$templateNameField.find("option:selected").val().split(":"),d=new a(this._editTemplateModalDialogId,this._popoverModalDialogEditTemplateLinkElementId,this._$editTemplateModalDialogPageContainer,this._$hiddenTemplateField,this._$templateDivField),d.mount(f,"openViaSelectBox",g))},d.prototype._loadTemplateContent=function(a){var b;if(this._newEmptyTemplateName=null!=(b=this._getTemplateNameDialog)?b.getNeEmptyTemplateName():void 0,null!=this._newEmptyTemplateName)return this._addEditTemplateConfirmEvent()},d.prototype._addEditTemplateConfirmEvent=function(){var a,b,c,d;return b="ct-ignition__button--confirm",d=document.querySelector(this.getIdFormat(this._editTemplateModalDialogPageContainerId)),c=d.contentDocument,a=$(c).find(this.getClassFormat(b)),a.unbind("click"),a.bind("click",this._confirmTemplateEdit)},d.prototype._confirmTemplateEdit=function(){if(null!=this._newEmptyTemplateName)return this._addNewSelectOpt(this._newEmptyTemplateName,this._$templateNameField),this._eraseNewTemplateName()},d.prototype._eraseNewTemplateName=function(){return this._newEmptyTemplateName=null},d.prototype._addNewSelectOpt=function(a,b){var c,d,e,f;if(d=$(this.getIdFormat(this._templateNameFieldId)+" option"),f=$.map(d,function(a){return a.value}),e="new_empty_template:"+a,-1===f.indexOf(e))return c=new Option(a,e),b.append(c),b.val(e)},d}(b),f=function(a){function b(a,b,c,d){this._htmlDoc=a,this._$editTemplatePageContainer=b,this._$templateDivField=c,this._$hiddenTemplateField=d,this._videoClass="video",this._verticalSpaceClassName="vertical-space",this._textContainerTags=["p","ul","ol","h1","h2","h3","h4","h5","h6"],this.convertVideoIframeToImgThumbnail()}return h(b,a),b.prototype.convertVideoIframeToImgThumbnail=function(){var a,b,c,d,e,f;return c=[],b=!1,f=new RegExp("youtube.com.*(v=|/embed/)(.{11})"),e=new RegExp("vimeo.com.*./(.*)"),a=function(a,b,c,d,e){return a.css("float",e),b.append(a),b.append(c),$(d).after(b),$(d).remove()},d=function(b){var c;return"vimeo"===this.videoType&&(c={src:b.thumbnail_url,width:b.thumbnail_width,height:b.thumbnail_height},this.$videoThumbnail.attr(c)),this.$videoCaption.text(b.title),a(this.$videoThumbnail,this.$clickableVideoThumbnail,this.$videoCaption,this.element,this.iframeCssFloatProperty)},$(this._htmlDoc).find("iframe").each(function(a){return function(g,h){var i,j,k,l,m,n,o,p,q,r,s,t,u;return p=$(h).attr("src"),n=$(h).css("float"),q=$(h).attr("width"),o=$(h).attr("height"),i=$(''),i.attr("href",p),k=$(''),k.attr({width:q,height:o}),j=$("
          "),m={"text-align":"center","font-size":"12px"},j.css(m),p.match(f)?(b=!0,t=p.match(f).pop(),i.attr("id",t),i.addClass("youtube"),u="//img.youtube.com/vi/"+t+"/0.jpg",k.attr("src",u),r="http://www.youtube.com/watch?v="+t+"&",s="https://noembed.com/embed?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"youtube"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):p.match(e)?(b=!0,t=p.match(e).pop(),i.attr("id",t),i.addClass("vimeo"),r="https://vimeo.com/"+t+"& width="+q+"&height="+o,s="https://vimeo.com/api/oembed.json?url="+r,l={$videoThumbnail:k,$clickableVideoThumbnail:i,$videoCaption:j,element:h,iframeCssFloatProperty:n,videoType:"vimeo"},c.push($.ajax(a.getAjaxData(l,s,d,"GET","jsonp")))):void 0}}(this)),$.when.apply(null,c).done(function(a){return function(){return a.convertCssToInlineStyle()}}(this))},b.prototype.getAjaxData=function(a,b,c,d,e){return null==d&&(d="GET"),null==e&&(e="json"),{type:d,url:b,jsonp:"callback",dataType:e,context:a,success:c}},b.prototype.convertCssToInlineStyle=function(){return this.inlineStyler($(this._htmlDoc)),this._fixContentWidth(),this._fixMargin(),this._replaceImgSrc(),this._convertTextCssPositionToTable(),this._eraseVerticalSpaceContent(),this._$templateDivField.html(this._$editTemplatePageContainer),this._$hiddenTemplateField.val(this._$editTemplatePageContainer.html())},b.prototype.inlineStyler=function(a){return a.inlineStyler()},b.prototype.getElementPosition=function(a,b,c){var d,e,f,g;return null==b&&(b=""),null==c&&(c=""),d=$("
          "),d.attr({cellspacing:0,cellpadding:0,border:0,width:"100%"}),e=$("
          "),d.attr({cellspacing:0,cellpadding:0,border:0,width:"100%"}),e=$("
          + + {% with default_width="600" default_height="auto" %} + {% endwith %} - - +
          +
          {% if article %} {{ article | safe }} {% else %} @@ -40,7 +42,7 @@ border="0" >
          @@ -73,7 +75,7 @@

          Title

          border="0" >
          @@ -98,7 +100,7 @@

          Title

          border="0" > - + diff --git a/apps/aklub/templates/email_templates/automat_purple_newsletter.html b/apps/aklub/templates/email_templates/automat_purple_newsletter.html index 988777638..d7e8bbe79 100644 --- a/apps/aklub/templates/email_templates/automat_purple_newsletter.html +++ b/apps/aklub/templates/email_templates/automat_purple_newsletter.html @@ -7,12 +7,13 @@ cellpadding="0" border="0" bgcolor="ffffff" - style="padding:0" + style="padding: 0" align="center" > - + + {% with default_width="600" default_height="auto" %} + {% endwith %} - - +
          +
          {% if article %} {{ article | safe }} {% else %} @@ -40,7 +42,7 @@ border="0" >
          @@ -72,7 +74,7 @@

          Title

          border="0" > - + @@ -97,7 +99,7 @@

          Title

          border="0" > - + diff --git a/apps/aklub/templates/email_templates/automat_red_newsletter.html b/apps/aklub/templates/email_templates/automat_red_newsletter.html index 4ee0b7655..a0c856c68 100644 --- a/apps/aklub/templates/email_templates/automat_red_newsletter.html +++ b/apps/aklub/templates/email_templates/automat_red_newsletter.html @@ -7,12 +7,13 @@ cellpadding="0" border="0" bgcolor="ffffff" - style="padding:0" + style="padding: 0" align="center" > - + + {% with default_width="600" default_height="auto" %} + {% endwith %} - - +
          +
          {% if article %} {{ article | safe }} {% else %} @@ -40,7 +42,7 @@ border="0" >
          @@ -73,7 +75,7 @@

          Title

          border="0" > - + @@ -98,7 +100,7 @@

          Title

          border="0" > - + diff --git a/apps/aklub/templates/email_templates/new_empty_template.html b/apps/aklub/templates/email_templates/new_empty_template.html index 32653193e..95e16db28 100644 --- a/apps/aklub/templates/email_templates/new_empty_template.html +++ b/apps/aklub/templates/email_templates/new_empty_template.html @@ -7,12 +7,13 @@ cellpadding="0" border="0" bgcolor="ffffff" - style="padding:0" + style="padding: 0" align="center" > - + + {% with default_width="600" default_height="auto" %} + {% endwith %} -
          +
          {% if article %} {{ article | safe }} {% else %} {% endif %} diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index 566f3f1b7..da75782b9 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -6,7 +6,11 @@ import pathlib from operator import itemgetter +from PIL import Image + from django.contrib import messages +from django.contrib.staticfiles import finders +from django.contrib.staticfiles.templatetags.staticfiles import static from django.db import models from django.template import loader from django.urls import reverse @@ -19,10 +23,6 @@ from . import models as aklub_models -from html_template_editor.models import ( - Images, TemplateContent, TemplateFooter, TemplateHeader, -) - def sweet_text(generator): """ @@ -178,6 +178,14 @@ def get_templates_files(): def get_email_template_context(template_path, template_url): """ Get email template context """ + templates_in_dir = { + 'automat_green_newsletter': 'automat_green_newsletter.jpeg', + 'automat_red_newsletter': 'automat_red_newsletter.jpeg', + 'automat_purple_newsletter': 'automat_purple_newsletter.jpeg', + } + + _finders = finders.AppDirectoriesFinder() + template = loader.get_template(str(template_path)) template_obj = TemplateContent.objects.filter(page=template_url) if (template_obj): @@ -200,6 +208,7 @@ def get_email_template_context(template_path, template_url): if header: context['header'] = header + # Templates in db background_image = Images.objects.filter( template_url=template_url, edited_crop__isnull=False, @@ -208,5 +217,21 @@ def get_email_template_context(template_path, template_url): context['bg_img'] = background_image.image.url context['bg_img_width'] = background_image.image.width context['bg_img_height'] = background_image.image.height + else: + # Templates in dir + for template_name in templates_in_dir.keys(): + if template_name in template_url: + img_name = templates_in_dir.get(template_name) + static_imgs_dir = _finders.find_in_app( + app='aklub', + path='aklub/images', + ) + img = Image.open( + pathlib.Path(static_imgs_dir) / img_name, + ) + + context['bg_img'] = static(f"aklub/images/{img_name}") + context['bg_img_width'] = img.width + context['bg_img_height'] = img.height return template, context From 2dc42db5200a627eae968a98ece118c4233ca11d Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sun, 11 Oct 2020 19:16:06 +0200 Subject: [PATCH 182/189] fix: load email templates files from directory --- apps/aklub/utils.py | 56 ++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/apps/aklub/utils.py b/apps/aklub/utils.py index da75782b9..73e967500 100644 --- a/apps/aklub/utils.py +++ b/apps/aklub/utils.py @@ -2,7 +2,6 @@ import datetime import json -import os import pathlib from operator import itemgetter @@ -23,6 +22,14 @@ from . import models as aklub_models +EMAIL_TEMPLATES_DENYLIST = [ + 'base.html', + 'new_empty_template.html', + 'footer.html', + 'automat_fonts.html', + 'header.html', +] + def sweet_text(generator): """ @@ -136,26 +143,31 @@ def get_admin_url(self): ) -def get_email_templates_names(): - """ Get email templates names """ - - templates_blacklist = [ - 'base.html', - 'new_empty_template.html', - 'footer.html', - 'automat_fonts.html', - 'header.html', +def get_templates_files(): + """ Get email templates files + + :return list: email templates files list + """ + path = pathlib.Path(__file__).parents[0] / 'templates' \ + / 'email_templates' + + return [ + f.name for f in path.glob('*.html') if f.is_file() ] - def get_templates_files(): - path = pathlib.Path(__file__).parents[0] / 'templates' / 'email_templates' - return os.listdir(path) - # File template +def get_email_templates_names(): + """ Get email templates names + + :return list: sorted list of email templates files tuples + (code, value) + """ + + # File templates file_templates_names = [ (template, template) for template in [ template.split('.')[0] for template in get_templates_files() - if template not in templates_blacklist + if template not in EMAIL_TEMPLATES_DENYLIST ] ] # Db templates @@ -178,10 +190,12 @@ def get_templates_files(): def get_email_template_context(template_path, template_url): """ Get email template context """ - templates_in_dir = { - 'automat_green_newsletter': 'automat_green_newsletter.jpeg', - 'automat_red_newsletter': 'automat_red_newsletter.jpeg', - 'automat_purple_newsletter': 'automat_purple_newsletter.jpeg', + # File templates + file_templates = { + template: f"{template}.jpeg" for template in [ + template.split('.')[0] for template in get_templates_files() + if template not in EMAIL_TEMPLATES_DENYLIST + ] } _finders = finders.AppDirectoriesFinder() @@ -219,9 +233,9 @@ def get_email_template_context(template_path, template_url): context['bg_img_height'] = background_image.image.height else: # Templates in dir - for template_name in templates_in_dir.keys(): + for template_name in file_templates.keys(): if template_name in template_url: - img_name = templates_in_dir.get(template_name) + img_name = file_templates.get(template_name) static_imgs_dir = _finders.find_in_app( app='aklub', path='aklub/images', From 6945e31d4cd62cdc215651c8b338108c36e8eee9 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sun, 11 Oct 2020 19:19:19 +0200 Subject: [PATCH 183/189] fix: remove duplicated URL path --- apps/aklub/urls.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/aklub/urls.py b/apps/aklub/urls.py index ecdeb4b36..a209d257e 100644 --- a/apps/aklub/urls.py +++ b/apps/aklub/urls.py @@ -37,7 +37,6 @@ auth_views.PasswordResetCompleteView.as_view(template_name='password/password_reset_complete.html'), name='password_reset_complete', ), - path('get_email_template//', views.get_email_template, name='get_email_template'), path( 'get_email_template//', login_required(views.get_email_template), From aaf463fee5fb4e15c4819118018b78151b141f4d Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sun, 11 Oct 2020 19:21:20 +0200 Subject: [PATCH 184/189] fix: add condition to set/read page id value --- apps/aklub/static/aklub/js/editor.js | 4 +++- apps/aklub/templates/email_templates/base.html | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/aklub/static/aklub/js/editor.js b/apps/aklub/static/aklub/js/editor.js index 0902688fb..d9cd58ef1 100644 --- a/apps/aklub/static/aklub/js/editor.js +++ b/apps/aklub/static/aklub/js/editor.js @@ -107,7 +107,9 @@ } }; element = document.querySelector('meta[name="page-id"]'); - pageId = element.getAttribute("content"); + if (element != null) { + pageId = element.getAttribute("content"); + } editHtmlTemplate.call( "post", window.reverse("html_template_editor:add"), diff --git a/apps/aklub/templates/email_templates/base.html b/apps/aklub/templates/email_templates/base.html index 6e2e201fc..28e91a31e 100644 --- a/apps/aklub/templates/email_templates/base.html +++ b/apps/aklub/templates/email_templates/base.html @@ -8,8 +8,10 @@ - - + + {% if page %} + + {% endif %} ContentTools sandbox From 09cb37bd6b6c444f1f7bc65a89d0625231ca3b61 Mon Sep 17 00:00:00 2001 From: Tomas Zigo Date: Sun, 11 Oct 2020 20:11:59 +0200 Subject: [PATCH 185/189] fix: add condition in the footer template for checking footer model m2m fields value existence --- .../templates/email_templates/footer.html | 88 ++++++++++--------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/apps/aklub/templates/email_templates/footer.html b/apps/aklub/templates/email_templates/footer.html index 7a6e46611..77af90874 100644 --- a/apps/aklub/templates/email_templates/footer.html +++ b/apps/aklub/templates/email_templates/footer.html @@ -2,8 +2,9 @@ {% load filters %}
          -