From e8ac852e62ebcf0bb342cdb984c16c5f88803f2d Mon Sep 17 00:00:00 2001 From: Vladislav Yena Date: Thu, 20 Feb 2020 19:57:48 +0200 Subject: [PATCH 01/11] Fix the HTML rendering function in the Form popup --- aldryn_forms/contrib/email_notifications/cms_plugins.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aldryn_forms/contrib/email_notifications/cms_plugins.py b/aldryn_forms/contrib/email_notifications/cms_plugins.py index 519308ca..045b6997 100644 --- a/aldryn_forms/contrib/email_notifications/cms_plugins.py +++ b/aldryn_forms/contrib/email_notifications/cms_plugins.py @@ -4,6 +4,8 @@ from django.contrib import admin from django.core.mail import get_connection +from django.utils.html import mark_safe + from django.utils.translation import ugettext_lazy as _ from cms.plugin_pool import plugin_pool @@ -109,9 +111,9 @@ def text_variables(self, obj): if fields_li: li_item = u'
  • {0}
  • '.format(category, fields_li) li_items.append(li_item) - unordered_list = u''.format(u''.join(li_items)) - help_text = u'

    {0}

    '.format(self.text_variables_help_text) - return unordered_list + u'\n' + help_text + unordered_list = ''.format(''.join(li_items)) + help_text = '

    {0}

    '.format(self.text_variables_help_text) + return mark_safe(unordered_list + '\n' + help_text) text_variables.allow_tags = True text_variables.short_description = _('available text variables') From 20fd9e6faee7461f17b216bff87d70e145b31eb2 Mon Sep 17 00:00:00 2001 From: Vladislav Yena Date: Thu, 20 Feb 2020 17:30:51 +0200 Subject: [PATCH 02/11] Remove unicode characters, add the correct HTML rendering for the Form Advanced plugin --- aldryn_forms/cms_plugins.py | 2 +- .../contrib/email_notifications/cms_plugins.py | 8 ++++---- .../contrib/email_notifications/models.py | 2 +- aldryn_forms/forms.py | 18 +++++++++--------- aldryn_forms/models.py | 10 +++++----- aldryn_forms/sizefield/models.py | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/aldryn_forms/cms_plugins.py b/aldryn_forms/cms_plugins.py index 01813da6..0552a8fa 100644 --- a/aldryn_forms/cms_plugins.py +++ b/aldryn_forms/cms_plugins.py @@ -285,7 +285,7 @@ class Field(FormElement): def serialize_value(self, instance, value, is_confirmation=False): if isinstance(value, query.QuerySet): - value = u', '.join(map(text_type, value)) + value = ', '.join(map(text_type, value)) elif value is None: value = '-' return text_type(value) diff --git a/aldryn_forms/contrib/email_notifications/cms_plugins.py b/aldryn_forms/contrib/email_notifications/cms_plugins.py index 045b6997..799a1e56 100644 --- a/aldryn_forms/contrib/email_notifications/cms_plugins.py +++ b/aldryn_forms/contrib/email_notifications/cms_plugins.py @@ -4,8 +4,8 @@ from django.contrib import admin from django.core.mail import get_connection -from django.utils.html import mark_safe +from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ from cms.plugin_pool import plugin_pool @@ -106,14 +106,14 @@ def text_variables(self, obj): for category, choices in choices_by_category: #
  • field_1
  • field_2
  • - fields_li = u''.join((u'
  • {0} | {1}
  • '.format(*var) for var in choices)) + fields_li = ''.join(('
  • {0} | {1}
  • '.format(*var) for var in choices)) if fields_li: - li_item = u'
  • {0}
  • '.format(category, fields_li) + li_item = '
  • {0}
  • '.format(category, fields_li) li_items.append(li_item) unordered_list = ''.format(''.join(li_items)) help_text = '

    {0}

    '.format(self.text_variables_help_text) - return mark_safe(unordered_list + '\n' + help_text) + return format_html(unordered_list + '\n' + help_text) text_variables.allow_tags = True text_variables.short_description = _('available text variables') diff --git a/aldryn_forms/contrib/email_notifications/models.py b/aldryn_forms/contrib/email_notifications/models.py index 39718f36..2fa4d1ca 100644 --- a/aldryn_forms/contrib/email_notifications/models.py +++ b/aldryn_forms/contrib/email_notifications/models.py @@ -118,7 +118,7 @@ class Meta: def __str__(self): to_name = self.get_recipient_name() to_email = self.get_recipient_email() - return u'{0} ({1})'.format(to_name, to_email) + return '{0} ({1})'.format(to_name, to_email) def clean(self): recipient_email = self.get_recipient_email() diff --git a/aldryn_forms/forms.py b/aldryn_forms/forms.py index 08e0413b..b2da6451 100644 --- a/aldryn_forms/forms.py +++ b/aldryn_forms/forms.py @@ -219,7 +219,7 @@ def clean(self): min_value = self.cleaned_data.get('min_value') max_value = self.cleaned_data.get('max_value') if min_value and max_value and min_value > max_value: - self.append_to_errors('min_value', _(u'Min value can not be greater than max value.')) + self.append_to_errors('min_value', _('Min value can not be greater than max value.')) return self.cleaned_data @@ -228,11 +228,11 @@ class TextFieldForm(MinMaxValueForm): def __init__(self, *args, **kwargs): super(TextFieldForm, self).__init__(*args, **kwargs) - self.fields['min_value'].label = _(u'Min length') - self.fields['min_value'].help_text = _(u'Required number of characters to type.') + self.fields['min_value'].label = _('Min length') + self.fields['min_value'].help_text = _('Required number of characters to type.') - self.fields['max_value'].label = _(u'Max length') - self.fields['max_value'].help_text = _(u'Maximum number of characters to type.') + self.fields['max_value'].label = _('Max length') + self.fields['max_value'].help_text = _('Maximum number of characters to type.') self.fields['max_value'].required = False class Meta: @@ -309,11 +309,11 @@ class MultipleSelectFieldForm(MinMaxValueForm): def __init__(self, *args, **kwargs): super(MultipleSelectFieldForm, self).__init__(*args, **kwargs) - self.fields['min_value'].label = _(u'Min choices') - self.fields['min_value'].help_text = _(u'Required amount of elements to chose.') + self.fields['min_value'].label = _('Min choices') + self.fields['min_value'].help_text = _('Required amount of elements to chose.') - self.fields['max_value'].label = _(u'Max choices') - self.fields['max_value'].help_text = _(u'Maximum amount of elements to chose.') + self.fields['max_value'].label = _('Max choices') + self.fields['max_value'].help_text = _('Maximum amount of elements to chose.') class Meta: # 'required' and 'required_message' depend on min_value field validator diff --git a/aldryn_forms/models.py b/aldryn_forms/models.py index 96dd1faf..15d1ab3d 100644 --- a/aldryn_forms/models.py +++ b/aldryn_forms/models.py @@ -78,10 +78,10 @@ def field_id(self): field_label = self.label.strip() if field_label: - field_as_string = u'{}-{}'.format(field_label, self.field_type) + field_as_string = '{}-{}'.format(field_label, self.field_type) else: field_as_string = self.name - field_id = u'{}:{}'.format(field_as_string, self.field_occurrence) + field_id = '{}:{}'.format(field_as_string, self.field_occurrence) return field_id @property @@ -261,10 +261,10 @@ def get_form_fields(self): if field_plugin.name: field_name = field_plugin.name else: - field_name = u'{0}_{1}'.format(field_type, field_type_occurrence) + field_name = '{0}_{1}'.format(field_type, field_type_occurrence) if field_label: - field_id = u'{0}_{1}'.format(field_type, field_label) + field_id = '{0}_{1}'.format(field_type, field_label) else: field_id = field_name @@ -591,7 +591,7 @@ def _form_data_hook(self, data, occurrences): if field_label: field_type = data['name'].rpartition('_')[0] - field_id = u'{}_{}'.format(field_type, field_label) + field_id = '{}_{}'.format(field_type, field_label) else: field_id = data['name'] diff --git a/aldryn_forms/sizefield/models.py b/aldryn_forms/sizefield/models.py index d30d6cb1..c33496d4 100644 --- a/aldryn_forms/sizefield/models.py +++ b/aldryn_forms/sizefield/models.py @@ -8,7 +8,7 @@ class FileSizeField(models.BigIntegerField): default_error_messages = { - 'invalid': _(u'Incorrect file size format.'), + 'invalid': _('Incorrect file size format.'), } def formfield(self, **kwargs): From 7153688307b2deeb4efd4d97dad88ed2381e3977 Mon Sep 17 00:00:00 2001 From: Vladimir Nosov Date: Wed, 27 May 2020 20:31:25 +0700 Subject: [PATCH 03/11] fixed flake8 error --- aldryn_forms/admin/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aldryn_forms/admin/views.py b/aldryn_forms/admin/views.py index f8cad4c1..0600fd81 100644 --- a/aldryn_forms/admin/views.py +++ b/aldryn_forms/admin/views.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from django import get_version from django.contrib import messages from django.http import HttpResponse from django.shortcuts import redirect From 6258618c8a21871c0efe6057d5d371b7be10ab49 Mon Sep 17 00:00:00 2001 From: Vladislav Yena Date: Thu, 20 Feb 2020 17:30:51 +0200 Subject: [PATCH 04/11] Remove unicode characters, add the correct HTML rendering for the Form Advanced plugin --- aldryn_forms/cms_plugins.py | 2 +- .../contrib/email_notifications/models.py | 2 +- aldryn_forms/forms.py | 18 +++++++++--------- aldryn_forms/models.py | 10 +++++----- aldryn_forms/sizefield/models.py | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/aldryn_forms/cms_plugins.py b/aldryn_forms/cms_plugins.py index 60a3bfb4..ab87832e 100644 --- a/aldryn_forms/cms_plugins.py +++ b/aldryn_forms/cms_plugins.py @@ -286,7 +286,7 @@ class Field(FormElement): def serialize_value(self, instance, value, is_confirmation=False): if isinstance(value, query.QuerySet): - value = u', '.join(map(str, value)) + value = ', '.join(map(str, value)) elif value is None: value = '-' return str(value) diff --git a/aldryn_forms/contrib/email_notifications/models.py b/aldryn_forms/contrib/email_notifications/models.py index a88b55ec..9d134f24 100644 --- a/aldryn_forms/contrib/email_notifications/models.py +++ b/aldryn_forms/contrib/email_notifications/models.py @@ -119,7 +119,7 @@ class Meta: def __str__(self): to_name = self.get_recipient_name() to_email = self.get_recipient_email() - return u'{0} ({1})'.format(to_name, to_email) + return '{0} ({1})'.format(to_name, to_email) def clean(self): recipient_email = self.get_recipient_email() diff --git a/aldryn_forms/forms.py b/aldryn_forms/forms.py index c0141b88..ce867107 100644 --- a/aldryn_forms/forms.py +++ b/aldryn_forms/forms.py @@ -218,7 +218,7 @@ def clean(self): min_value = self.cleaned_data.get('min_value') max_value = self.cleaned_data.get('max_value') if min_value and max_value and min_value > max_value: - self.append_to_errors('min_value', _(u'Min value can not be greater than max value.')) + self.append_to_errors('min_value', _('Min value can not be greater than max value.')) return self.cleaned_data @@ -227,11 +227,11 @@ class TextFieldForm(MinMaxValueForm): def __init__(self, *args, **kwargs): super(TextFieldForm, self).__init__(*args, **kwargs) - self.fields['min_value'].label = _(u'Min length') - self.fields['min_value'].help_text = _(u'Required number of characters to type.') + self.fields['min_value'].label = _('Min length') + self.fields['min_value'].help_text = _('Required number of characters to type.') - self.fields['max_value'].label = _(u'Max length') - self.fields['max_value'].help_text = _(u'Maximum number of characters to type.') + self.fields['max_value'].label = _('Max length') + self.fields['max_value'].help_text = _('Maximum number of characters to type.') self.fields['max_value'].required = False class Meta: @@ -308,11 +308,11 @@ class MultipleSelectFieldForm(MinMaxValueForm): def __init__(self, *args, **kwargs): super(MultipleSelectFieldForm, self).__init__(*args, **kwargs) - self.fields['min_value'].label = _(u'Min choices') - self.fields['min_value'].help_text = _(u'Required amount of elements to chose.') + self.fields['min_value'].label = _('Min choices') + self.fields['min_value'].help_text = _('Required amount of elements to chose.') - self.fields['max_value'].label = _(u'Max choices') - self.fields['max_value'].help_text = _(u'Maximum amount of elements to chose.') + self.fields['max_value'].label = _('Max choices') + self.fields['max_value'].help_text = _('Maximum amount of elements to chose.') class Meta: # 'required' and 'required_message' depend on min_value field validator diff --git a/aldryn_forms/models.py b/aldryn_forms/models.py index 0bbefd66..eceea320 100644 --- a/aldryn_forms/models.py +++ b/aldryn_forms/models.py @@ -75,10 +75,10 @@ def field_id(self): field_label = self.label.strip() if field_label: - field_as_string = u'{}-{}'.format(field_label, self.field_type) + field_as_string = '{}-{}'.format(field_label, self.field_type) else: field_as_string = self.name - field_id = u'{}:{}'.format(field_as_string, self.field_occurrence) + field_id = '{}:{}'.format(field_as_string, self.field_occurrence) return field_id @property @@ -267,10 +267,10 @@ def get_form_fields(self) -> List[FormField]: if field_plugin.name: field_name = field_plugin.name else: - field_name = u'{0}_{1}'.format(field_type, field_type_occurrence) + field_name = '{0}_{1}'.format(field_type, field_type_occurrence) if field_label: - field_id = u'{0}_{1}'.format(field_type, field_label) + field_id = '{0}_{1}'.format(field_type, field_label) else: field_id = field_name @@ -591,7 +591,7 @@ def _form_data_hook(self, data, occurrences): if field_label: field_type = data['name'].rpartition('_')[0] - field_id = u'{}_{}'.format(field_type, field_label) + field_id = '{}_{}'.format(field_type, field_label) else: field_id = data['name'] diff --git a/aldryn_forms/sizefield/models.py b/aldryn_forms/sizefield/models.py index d30d6cb1..c33496d4 100644 --- a/aldryn_forms/sizefield/models.py +++ b/aldryn_forms/sizefield/models.py @@ -8,7 +8,7 @@ class FileSizeField(models.BigIntegerField): default_error_messages = { - 'invalid': _(u'Incorrect file size format.'), + 'invalid': _('Incorrect file size format.'), } def formfield(self, **kwargs): From 02c04c92c4d9c0aa7de5cdd0998c0c5d1f2da4c7 Mon Sep 17 00:00:00 2001 From: Sergey Gordeychuk Date: Sun, 20 Aug 2023 19:04:47 +0300 Subject: [PATCH 05/11] Django 4 support --- aldryn_forms/action_backends.py | 2 +- aldryn_forms/admin/base.py | 6 +- aldryn_forms/admin/forms.py | 6 +- aldryn_forms/admin/views.py | 4 +- aldryn_forms/cms_apps.py | 2 +- aldryn_forms/cms_plugins.py | 6 +- .../email_notifications/cms_plugins.py | 2 +- .../0006_alter_emailnotification_id.py | 20 +++ .../contrib/email_notifications/models.py | 6 +- .../email_notifications/notification.py | 4 +- aldryn_forms/forms.py | 10 +- ...emailfieldplugin_cmsplugin_ptr_and_more.py | 125 ++++++++++++++++++ aldryn_forms/models.py | 2 +- aldryn_forms/signals.py | 5 +- aldryn_forms/sizefield/models.py | 2 +- aldryn_forms/sizefield/utils.py | 2 +- aldryn_forms/urls.py | 4 +- aldryn_forms/validators.py | 2 +- tests/test_utils.py | 2 +- 19 files changed, 179 insertions(+), 33 deletions(-) create mode 100644 aldryn_forms/contrib/email_notifications/migrations/0006_alter_emailnotification_id.py create mode 100644 aldryn_forms/migrations/0014_alter_emailfieldplugin_cmsplugin_ptr_and_more.py diff --git a/aldryn_forms/action_backends.py b/aldryn_forms/action_backends.py index f368e978..fa8ab30a 100644 --- a/aldryn_forms/action_backends.py +++ b/aldryn_forms/action_backends.py @@ -1,6 +1,6 @@ import logging -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .action_backends_base import BaseAction diff --git a/aldryn_forms/admin/base.py b/aldryn_forms/admin/base.py index 3d856f1b..bed0507b 100644 --- a/aldryn_forms/admin/base.py +++ b/aldryn_forms/admin/base.py @@ -3,7 +3,7 @@ from django.contrib import admin from django.template.loader import render_to_string from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ str_dunder_method = '__str__' @@ -51,11 +51,11 @@ def get_recipients_for_display(self, obj): get_recipients_for_display.short_description = _('people notified') def get_urls(self): - from django.conf.urls import url + from django.urls import re_path def pattern(regex, fn, name): args = [regex, self.admin_site.admin_view(fn)] - return url(*args, name=self.get_admin_url(name)) + return re_path(*args, name=self.get_admin_url(name)) url_patterns = [ pattern(r'export/$', self.get_form_export_view(), 'export'), diff --git a/aldryn_forms/admin/forms.py b/aldryn_forms/admin/forms.py index 00791517..493576c6 100644 --- a/aldryn_forms/admin/forms.py +++ b/aldryn_forms/admin/forms.py @@ -5,8 +5,8 @@ from django.contrib.admin.widgets import AdminDateWidget from django.utils import timezone from django.utils.text import slugify -from django.utils.translation import ugettext -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ from ..models import FormSubmission from .exporter import Exporter @@ -129,6 +129,6 @@ def clean(self): fields = self.get_fields() if not fields: - message = ugettext('Please select at least one field to export.') + message = gettext('Please select at least one field to export.') raise forms.ValidationError(message) return self.cleaned_data diff --git a/aldryn_forms/admin/views.py b/aldryn_forms/admin/views.py index 16884c63..4bfa5af0 100644 --- a/aldryn_forms/admin/views.py +++ b/aldryn_forms/admin/views.py @@ -2,7 +2,7 @@ from django.contrib import messages from django.http import HttpResponse from django.shortcuts import redirect -from django.utils.translation import get_language_from_request, ugettext +from django.utils.translation import get_language_from_request, gettext from ..compat import SessionWizardView from .exporter import Exporter @@ -65,7 +65,7 @@ def render_next_step(self, form, **kwargs): if next_step == self.steps.last and not form.get_queryset().exists(): self.storage.reset() - self.admin.message_user(self.request, ugettext("No records found"), level=messages.WARNING) + self.admin.message_user(self.request, gettext("No records found"), level=messages.WARNING) export_url = 'admin:{}'.format(self.admin.get_admin_url('export')) return redirect(export_url) return super(FormExportWizardView, self).render_next_step(form, **kwargs) diff --git a/aldryn_forms/cms_apps.py b/aldryn_forms/cms_apps.py index 57d61441..ebcd4dec 100644 --- a/aldryn_forms/cms_apps.py +++ b/aldryn_forms/cms_apps.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from cms.app_base import CMSApp from cms.apphook_pool import apphook_pool diff --git a/aldryn_forms/cms_plugins.py b/aldryn_forms/cms_plugins.py index ab87832e..4d71ce41 100644 --- a/aldryn_forms/cms_plugins.py +++ b/aldryn_forms/cms_plugins.py @@ -9,8 +9,8 @@ from django.core.validators import MinLengthValidator from django.db.models import query from django.template.loader import select_template -from django.utils.translation import ugettext -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ from emailit.api import send_mail from filer.models import filemodels from filer.models import imagemodels @@ -713,7 +713,7 @@ class BooleanField(Field): ] def serialize_value(self, instance, value, is_confirmation=False): - return ugettext('Yes') if value else ugettext('No') + return gettext('Yes') if value else gettext('No') class SelectOptionInline(TabularInline): diff --git a/aldryn_forms/contrib/email_notifications/cms_plugins.py b/aldryn_forms/contrib/email_notifications/cms_plugins.py index d7b5c476..31e05667 100644 --- a/aldryn_forms/contrib/email_notifications/cms_plugins.py +++ b/aldryn_forms/contrib/email_notifications/cms_plugins.py @@ -7,7 +7,7 @@ from django.template.defaultfilters import safe from django.utils.html import format_html -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from cms.plugin_pool import plugin_pool diff --git a/aldryn_forms/contrib/email_notifications/migrations/0006_alter_emailnotification_id.py b/aldryn_forms/contrib/email_notifications/migrations/0006_alter_emailnotification_id.py new file mode 100644 index 00000000..16ffafd4 --- /dev/null +++ b/aldryn_forms/contrib/email_notifications/migrations/0006_alter_emailnotification_id.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.4 on 2023-08-20 15:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("email_notifications", "0005_add_field_reply_to_email"), + ] + + operations = [ + migrations.AlterField( + model_name="emailnotification", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ] diff --git a/aldryn_forms/contrib/email_notifications/models.py b/aldryn_forms/contrib/email_notifications/models.py index 9d134f24..a65af07b 100644 --- a/aldryn_forms/contrib/email_notifications/models.py +++ b/aldryn_forms/contrib/email_notifications/models.py @@ -4,8 +4,8 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.db import models -from django.utils.translation import ugettext -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ from djangocms_text_ckeditor.fields import HTMLField from emailit.api import construct_mail @@ -125,7 +125,7 @@ def clean(self): recipient_email = self.get_recipient_email() if self.pk and not recipient_email: - message = ugettext('Please provide a recipient.') + message = gettext('Please provide a recipient.') raise ValidationError(message) def get_recipient_name(self): diff --git a/aldryn_forms/contrib/email_notifications/notification.py b/aldryn_forms/contrib/email_notifications/notification.py index 178a6423..f892d9a7 100644 --- a/aldryn_forms/contrib/email_notifications/notification.py +++ b/aldryn_forms/contrib/email_notifications/notification.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext +from django.utils.translation import gettext from .helpers import get_email_template_name @@ -50,7 +50,7 @@ def get_custom_context(self, form): def get_context_keys_as_choices(self): choices = [ ( - ugettext('Fields'), + gettext('Fields'), list(self.form_plugin.get_form_fields_as_choices()) ), ] diff --git a/aldryn_forms/forms.py b/aldryn_forms/forms.py index ce867107..e74b0f97 100644 --- a/aldryn_forms/forms.py +++ b/aldryn_forms/forms.py @@ -2,8 +2,8 @@ from django.conf import settings from django.forms.forms import NON_FIELD_ERRORS from django.forms.utils import ErrorDict -from django.utils.translation import ugettext -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ from PIL import Image @@ -25,7 +25,7 @@ def clean(self, *args, **kwargs): if self.max_size is not None and data.size > self.max_size: raise forms.ValidationError( - ugettext('File size must be under %(max_size)s. Current file size is %(actual_size)s.') % { + gettext('File size must be under %(max_size)s. Current file size is %(actual_size)s.') % { 'max_size': filesizeformat(self.max_size), 'actual_size': filesizeformat(data.size), }) @@ -59,7 +59,7 @@ def clean(self, *args, **kwargs): if self.max_width and width > self.max_width: raise forms.ValidationError( - ugettext( + gettext( 'Image width must be under %(max_size)s pixels. ' 'Current width is %(actual_size)s pixels.' ) % { @@ -69,7 +69,7 @@ def clean(self, *args, **kwargs): if self.max_height and height > self.max_height: raise forms.ValidationError( - ugettext( + gettext( 'Image height must be under %(max_size)s pixels. ' 'Current height is %(actual_size)s pixels.' ) % { diff --git a/aldryn_forms/migrations/0014_alter_emailfieldplugin_cmsplugin_ptr_and_more.py b/aldryn_forms/migrations/0014_alter_emailfieldplugin_cmsplugin_ptr_and_more.py new file mode 100644 index 00000000..8d5bad6a --- /dev/null +++ b/aldryn_forms/migrations/0014_alter_emailfieldplugin_cmsplugin_ptr_and_more.py @@ -0,0 +1,125 @@ +# Generated by Django 4.2.4 on 2023-08-20 15:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("cms", "0022_auto_20180620_1551"), + ("aldryn_forms", "0013_add_field_is_enable_autofill_from_url_params"), + ] + + operations = [ + migrations.AlterField( + model_name="emailfieldplugin", + name="cmsplugin_ptr", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="%(app_label)s_%(class)s", + serialize=False, + to="cms.cmsplugin", + ), + ), + migrations.AlterField( + model_name="fieldplugin", + name="cmsplugin_ptr", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="%(app_label)s_%(class)s", + serialize=False, + to="cms.cmsplugin", + ), + ), + migrations.AlterField( + model_name="fieldsetplugin", + name="cmsplugin_ptr", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="%(app_label)s_%(class)s", + serialize=False, + to="cms.cmsplugin", + ), + ), + migrations.AlterField( + model_name="fileuploadfieldplugin", + name="cmsplugin_ptr", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="%(app_label)s_%(class)s", + serialize=False, + to="cms.cmsplugin", + ), + ), + migrations.AlterField( + model_name="formbuttonplugin", + name="cmsplugin_ptr", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="%(app_label)s_%(class)s", + serialize=False, + to="cms.cmsplugin", + ), + ), + migrations.AlterField( + model_name="formplugin", + name="cmsplugin_ptr", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="%(app_label)s_%(class)s", + serialize=False, + to="cms.cmsplugin", + ), + ), + migrations.AlterField( + model_name="formsubmission", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="imageuploadfieldplugin", + name="cmsplugin_ptr", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="%(app_label)s_%(class)s", + serialize=False, + to="cms.cmsplugin", + ), + ), + migrations.AlterField( + model_name="option", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="textareafieldplugin", + name="cmsplugin_ptr", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="%(app_label)s_%(class)s", + serialize=False, + to="cms.cmsplugin", + ), + ), + ] diff --git a/aldryn_forms/models.py b/aldryn_forms/models.py index eceea320..c04247e5 100644 --- a/aldryn_forms/models.py +++ b/aldryn_forms/models.py @@ -13,7 +13,7 @@ from django.db import models from django.db.models.functions import Coalesce from django.utils.functional import cached_property -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from djangocms_attributes_field.fields import AttributesField from filer.fields.folder import FilerFolderField diff --git a/aldryn_forms/signals.py b/aldryn_forms/signals.py index 653c9bfa..20059010 100644 --- a/aldryn_forms/signals.py +++ b/aldryn_forms/signals.py @@ -1,5 +1,6 @@ from django.dispatch import Signal -form_pre_save = Signal(providing_args=['instance', 'form', 'request']) -form_post_save = Signal(providing_args=['instance', 'form', 'request']) +# Provides arguments: instance, form, request +form_pre_save = Signal() +form_post_save = Signal() diff --git a/aldryn_forms/sizefield/models.py b/aldryn_forms/sizefield/models.py index c33496d4..a261e879 100644 --- a/aldryn_forms/sizefield/models.py +++ b/aldryn_forms/sizefield/models.py @@ -1,6 +1,6 @@ from django.core import exceptions from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .utils import parse_size from .widgets import FileSizeWidget diff --git a/aldryn_forms/sizefield/utils.py b/aldryn_forms/sizefield/utils.py index 3aed242b..a3ec3ca5 100644 --- a/aldryn_forms/sizefield/utils.py +++ b/aldryn_forms/sizefield/utils.py @@ -4,7 +4,7 @@ from django.conf import settings from django.utils import formats -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ if sys.version_info >= (3, 0): diff --git a/aldryn_forms/urls.py b/aldryn_forms/urls.py index b0b9d4dd..cdeea558 100644 --- a/aldryn_forms/urls.py +++ b/aldryn_forms/urls.py @@ -1,8 +1,8 @@ -from django.conf.urls import url +from django.urls import re_path from .views import submit_form_view urlpatterns = [ - url(r'^$', submit_form_view, name='aldryn_forms_submit_form'), + re_path(r'^$', submit_form_view, name='aldryn_forms_submit_form'), ] diff --git a/aldryn_forms/validators.py b/aldryn_forms/validators.py index a7f974bb..1eb3971d 100644 --- a/aldryn_forms/validators.py +++ b/aldryn_forms/validators.py @@ -4,7 +4,7 @@ from django.core.validators import ( MaxLengthValidator, MinLengthValidator, validate_email, ) -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ def is_valid_recipient(recipient): diff --git a/tests/test_utils.py b/tests/test_utils.py index 54036972..b37a7f5d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ from django.core.exceptions import ImproperlyConfigured from django.test import override_settings -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from cms.test_utils.testcases import CMSTestCase From 0a4fd3305c68a2fb328ccd7e65596a6ddac3769d Mon Sep 17 00:00:00 2001 From: Sergey Gordeychuk Date: Sun, 20 Aug 2023 21:03:43 +0300 Subject: [PATCH 06/11] Bump version 6.2.2 --- aldryn_forms/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aldryn_forms/__init__.py b/aldryn_forms/__init__.py index b133e0c3..ac1df1f7 100644 --- a/aldryn_forms/__init__.py +++ b/aldryn_forms/__init__.py @@ -1 +1 @@ -__version__ = '6.2.1' +__version__ = '6.2.2' From a1e35f7b622c0a1fbfd54c393326f6bb3324c684 Mon Sep 17 00:00:00 2001 From: Hassan Ahmed Date: Tue, 7 Nov 2023 17:00:53 +0500 Subject: [PATCH 07/11] Feature/filefield allowed extensions and attach files to email (#1) * add option to limit allowed extensions of file fields * convert serialization to utility function * add option to define files that should be attached to email * recognize extensions without the starting period * update help text --- aldryn_forms/cms_plugins.py | 15 ++++- .../email_notifications/cms_plugins.py | 61 ++++++++++++++----- .../migrations/0007_auto_20231105_0323.py | 23 +++++++ .../contrib/email_notifications/models.py | 41 ++++++++++++- .../email_notifications/notification.py | 9 +++ .../migrations/0015_auto_20231105_0313.py | 33 ++++++++++ aldryn_forms/models.py | 22 +++++++ aldryn_forms/utils.py | 22 +++++++ aldryn_forms/validators.py | 25 ++++++++ 9 files changed, 231 insertions(+), 20 deletions(-) create mode 100644 aldryn_forms/contrib/email_notifications/migrations/0007_auto_20231105_0323.py create mode 100644 aldryn_forms/migrations/0015_auto_20231105_0313.py diff --git a/aldryn_forms/cms_plugins.py b/aldryn_forms/cms_plugins.py index 4d71ce41..208540c4 100644 --- a/aldryn_forms/cms_plugins.py +++ b/aldryn_forms/cms_plugins.py @@ -38,9 +38,12 @@ from .signals import form_pre_save from .sizefield.utils import filesizeformat from .utils import get_action_backends -from .validators import MaxChoicesValidator -from .validators import MinChoicesValidator -from .validators import is_valid_recipient +from .validators import ( + MaxChoicesValidator, + MinChoicesValidator, + is_valid_recipient, + generate_file_extension_validator, +) class FormElement(CMSPluginBase): @@ -591,6 +594,7 @@ class FileField(Field): fieldset_advanced_fields = [ 'help_text', 'max_size', + 'allowed_extensions', 'required_message', 'custom_classes', ] @@ -604,6 +608,11 @@ def get_form_field_kwargs(self, instance): kwargs['max_size'] = instance.max_size return kwargs + def get_form_field_validators(self, instance: models.FileFieldPluginBase): + validators = super().get_form_field_validators(instance) + validators.append(generate_file_extension_validator(instance.allowed_extensions)) + return validators + def serialize_value(self, instance, value, is_confirmation=False): if value: return value.absolute_uri diff --git a/aldryn_forms/contrib/email_notifications/cms_plugins.py b/aldryn_forms/contrib/email_notifications/cms_plugins.py index 31e05667..a2173f47 100644 --- a/aldryn_forms/contrib/email_notifications/cms_plugins.py +++ b/aldryn_forms/contrib/email_notifications/cms_plugins.py @@ -45,23 +45,31 @@ class ExistingEmailNotificationInline(admin.StackedInline): model = EmailNotification fieldsets = ( - (None, { - 'fields': ( - 'theme', - ) - }), - (_('Recipients'), { - 'fields': ( - 'text_variables', - 'to_user', - ('to_name', 'to_email'), - ('from_name', 'from_email'), - 'reply_to_email', - ) - }), + (None, {"fields": ("theme",)}), + ( + _("Recipients"), + { + "fields": ( + "text_variables", + "to_user", + ("to_name", "to_email"), + ("from_name", "from_email"), + "reply_to_email", + ) + }, + ), + ( + _("Attaching files to email"), + { + "fields": ( + "file_variables", + "files_to_attach_to_email", + ) + }, + ), ) - readonly_fields = ['text_variables'] + readonly_fields = ['text_variables', 'file_variables'] text_variables_help_text = _( 'the variables can be used within the email body, email sender,' @@ -122,6 +130,29 @@ def text_variables(self, obj: EmailNotification) -> str: text_variables.allow_tags = True text_variables.short_description = _('available text variables') + def file_variables(self, obj: EmailNotification) -> str: + if obj.pk is None: + return '' + + # list of tuples - [('category', [('value', 'label')])] + choices_by_category = obj.form.get_notification_text_context_file_keys_as_choices() + + var_items: List[str] = [] + for category, choices in choices_by_category: + for choice_tuple in choices: + field_value = choice_tuple[0] + var_items += '
  • ' + field_value + '
  • ' + + vars_html_list = f'

    {"".join(var_items)}

    ' + help_text = ( + f'

    ' + f'{_("these are the valid file fields that can be attached to the email")}' + f'

    ' + ) + return safe(vars_html_list + u'\n' + help_text) + file_variables.allow_tags = True + file_variables.short_description = _('available file variables') + class Media: css = { 'all': ['email_notifications/admin/email-notifications.css'] diff --git a/aldryn_forms/contrib/email_notifications/migrations/0007_auto_20231105_0323.py b/aldryn_forms/contrib/email_notifications/migrations/0007_auto_20231105_0323.py new file mode 100644 index 00000000..13511188 --- /dev/null +++ b/aldryn_forms/contrib/email_notifications/migrations/0007_auto_20231105_0323.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2 on 2023-11-04 22:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('email_notifications', '0006_alter_emailnotification_id'), + ] + + operations = [ + migrations.AddField( + model_name='emailnotification', + name='files_to_attach_to_email', + field=models.CharField(blank=True, default='', help_text='Comma-separated list of file fields that should be attached directly to the email.', max_length=255, verbose_name='Files to attach to the email'), + ), + migrations.AlterField( + model_name='emailnotification', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/aldryn_forms/contrib/email_notifications/models.py b/aldryn_forms/contrib/email_notifications/models.py index a65af07b..58da3053 100644 --- a/aldryn_forms/contrib/email_notifications/models.py +++ b/aldryn_forms/contrib/email_notifications/models.py @@ -1,14 +1,18 @@ +import mimetypes from email.utils import formataddr from functools import partial from django.conf import settings from django.core.exceptions import ValidationError +from django.core.files.storage import default_storage +from django.core.mail import EmailMultiAlternatives from django.db import models from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ from djangocms_text_ckeditor.fields import HTMLField from emailit.api import construct_mail +from filer.models import File from aldryn_forms.helpers import get_user_name from aldryn_forms.models import FormPlugin @@ -16,7 +20,7 @@ from .helpers import ( get_email_template_name, get_theme_template_name, render_text, ) - +from aldryn_forms.utils import serialize_delimiter_separated_values_string EMAIL_THEMES = getattr( settings, @@ -49,6 +53,11 @@ def get_notification_text_context_keys_as_choices(self): choices = notification_conf.get_context_keys_as_choices() return choices + def get_notification_text_context_file_keys_as_choices(self): + notification_conf = self.get_notification_conf() + choices = notification_conf.get_context_file_keys_as_choices() + return choices + class EmailNotification(models.Model): @@ -110,6 +119,13 @@ class Meta: blank=True, help_text=_('used when rendering the email in html.') ) + files_to_attach_to_email = models.CharField( + max_length=255, + verbose_name=_('Files to attach to the email'), + blank=True, + default="", + help_text=_('Comma-separated list of file fields that should be attached directly to the email.') + ) form = models.ForeignKey( to=EmailNotificationFormPlugin, related_name='email_notifications', @@ -216,9 +232,30 @@ def get_email_kwargs(self, form): return kwargs + def attach_files(self, email: EmailMultiAlternatives, form): + """Attach files if any are needed""" + files_to_attach = serialize_delimiter_separated_values_string( + self.files_to_attach_to_email, delimiter=",", strip=True, lower=True + ) + if not files_to_attach: + return + for field_name in form.fields: + if field_name in files_to_attach: + file_field: File = form.cleaned_data.get(field_name) + if not file_field: + continue + with default_storage.open(file_field.path, "rb") as file: + email.attach( + filename=file_field.original_filename, + content=file.read(), + mimetype=mimetypes.guess_type(file_field.original_filename)[0], + ) + def prepare_email(self, form): email_kwargs = self.get_email_kwargs(form) - return construct_mail(**email_kwargs) + email: EmailMultiAlternatives = construct_mail(**email_kwargs) + self.attach_files(email, form) + return email def render_body_text(self, context): return render_text(self.body_text, context) diff --git a/aldryn_forms/contrib/email_notifications/notification.py b/aldryn_forms/contrib/email_notifications/notification.py index f892d9a7..dbb5424f 100644 --- a/aldryn_forms/contrib/email_notifications/notification.py +++ b/aldryn_forms/contrib/email_notifications/notification.py @@ -59,6 +59,15 @@ def get_context_keys_as_choices(self): choices += list(self.custom_context_choices) return choices + def get_context_file_keys_as_choices(self): + choices = [ + ( + gettext('Fields'), + list(self.form_plugin.get_form_file_fields_as_choices()) + ), + ] + return choices + class DefaultNotificationConf(BaseNotificationConf): html_email_format_enabled = True diff --git a/aldryn_forms/migrations/0015_auto_20231105_0313.py b/aldryn_forms/migrations/0015_auto_20231105_0313.py new file mode 100644 index 00000000..217c8734 --- /dev/null +++ b/aldryn_forms/migrations/0015_auto_20231105_0313.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2 on 2023-11-04 22:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('aldryn_forms', '0014_alter_emailfieldplugin_cmsplugin_ptr_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='fileuploadfieldplugin', + name='allowed_extensions', + field=models.CharField(blank=True, default='', help_text='Comma-separated list of file extensions allowed for this file field.', max_length=255, verbose_name='Allowed extensions'), + ), + migrations.AddField( + model_name='imageuploadfieldplugin', + name='allowed_extensions', + field=models.CharField(blank=True, default='', help_text='Comma-separated list of file extensions allowed for this file field.', max_length=255, verbose_name='Allowed extensions'), + ), + migrations.AlterField( + model_name='formsubmission', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='option', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/aldryn_forms/models.py b/aldryn_forms/models.py index c04247e5..3ae2f3d1 100644 --- a/aldryn_forms/models.py +++ b/aldryn_forms/models.py @@ -305,6 +305,12 @@ def get_form_fields_as_choices(self): for field in fields: yield (field.name, field.label) + def get_form_file_fields_as_choices(self): + fields = self.get_form_fields() + for field in fields: + if field.plugin_instance.IS_FILE_FIELD: + yield field.name, field.label + def get_form_elements(self): from .utils import get_nested_plugins @@ -416,6 +422,8 @@ class FieldPluginBase(CMSPlugin): on_delete=models.CASCADE, ) + IS_FILE_FIELD = False + class Meta: abstract = True @@ -492,6 +500,20 @@ class FileFieldPluginBase(FieldPluginBase): help_text=_('The maximum file size of the upload, in bytes. You can ' 'use common size suffixes (kB, MB, GB, ...).') ) + allowed_extensions = models.CharField( + max_length=255, + verbose_name=_("Allowed extensions"), + blank=True, + default="", + help_text=( + _( + "Comma-separated list of file extensions allowed for this file field. " + "Leave it empty to allow any extension." + ), + ), + ) + + IS_FILE_FIELD = True class Meta: abstract = True diff --git a/aldryn_forms/utils.py b/aldryn_forms/utils.py index b28bd8f5..21d9ed8e 100644 --- a/aldryn_forms/utils.py +++ b/aldryn_forms/utils.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import typing + from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.forms.forms import NON_FIELD_ERRORS @@ -123,3 +125,23 @@ def add_form_error(form, message, field=NON_FIELD_ERRORS): form._errors[field].append(message) except KeyError: form._errors[field] = form.error_class([message]) + + +def serialize_delimiter_separated_values_string( + csl: str = "", delimiter=",", strip=True, lower=True +) -> typing.List[str]: + """Convert a comma-separated string of values to list of strings""" + if not csl: + return [] + + values_list = [item for item in csl.split(delimiter)] + + if strip: + values_list = [item.strip() for item in values_list] + if lower: + values_list = [item.lower() for item in values_list] + + values_list = [item for item in values_list if item] + return values_list + + diff --git a/aldryn_forms/validators.py b/aldryn_forms/validators.py index 1eb3971d..03217c0e 100644 --- a/aldryn_forms/validators.py +++ b/aldryn_forms/validators.py @@ -1,3 +1,4 @@ +import os from email.utils import parseaddr from django.core.exceptions import ValidationError @@ -6,6 +7,30 @@ ) from django.utils.translation import gettext_lazy as _ +from aldryn_forms.utils import serialize_delimiter_separated_values_string + + +def generate_file_extension_validator(allowed_extensions_str: str = ""): + allowed_extensions = serialize_delimiter_separated_values_string( + allowed_extensions_str, delimiter=",", strip=True, lower=True + ) + allowed_extensions = [ + extension if extension.startswith(".") else f".{extension}" + for extension in allowed_extensions + ] + + if not allowed_extensions: + return lambda value: None + + def validator(value): + extension = os.path.splitext(value.name)[1] # [0] returns path+filename + if not extension.lower() in allowed_extensions: + raise ValidationError( + _(f"File extension '{extension}' is not allowed for this field."), + code="invalid_extension", + ) + + return validator def is_valid_recipient(recipient): """ From 2be38fc9bf9594cc34ced1a452d34753449fb748 Mon Sep 17 00:00:00 2001 From: Hassan Ahmed Date: Wed, 8 Nov 2023 20:44:45 +0500 Subject: [PATCH 08/11] avoid lower-casing files_to_attach field since uppercase names are allowed (#3) --- aldryn_forms/contrib/email_notifications/models.py | 2 +- aldryn_forms/validators.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/aldryn_forms/contrib/email_notifications/models.py b/aldryn_forms/contrib/email_notifications/models.py index 58da3053..6188cc26 100644 --- a/aldryn_forms/contrib/email_notifications/models.py +++ b/aldryn_forms/contrib/email_notifications/models.py @@ -235,7 +235,7 @@ def get_email_kwargs(self, form): def attach_files(self, email: EmailMultiAlternatives, form): """Attach files if any are needed""" files_to_attach = serialize_delimiter_separated_values_string( - self.files_to_attach_to_email, delimiter=",", strip=True, lower=True + self.files_to_attach_to_email, delimiter=",", strip=True, lower=False ) if not files_to_attach: return diff --git a/aldryn_forms/validators.py b/aldryn_forms/validators.py index 03217c0e..6534c427 100644 --- a/aldryn_forms/validators.py +++ b/aldryn_forms/validators.py @@ -14,14 +14,15 @@ def generate_file_extension_validator(allowed_extensions_str: str = ""): allowed_extensions = serialize_delimiter_separated_values_string( allowed_extensions_str, delimiter=",", strip=True, lower=True ) + + if not allowed_extensions: + return lambda value: None + allowed_extensions = [ extension if extension.startswith(".") else f".{extension}" for extension in allowed_extensions ] - if not allowed_extensions: - return lambda value: None - def validator(value): extension = os.path.splitext(value.name)[1] # [0] returns path+filename if not extension.lower() in allowed_extensions: From dca077f64d90ef914e1c463548097c21fa8157b0 Mon Sep 17 00:00:00 2001 From: Hassan Ahmed Date: Wed, 8 Nov 2023 22:06:13 +0500 Subject: [PATCH 09/11] add option to store file to filer (default=True) (#4) * add option to store file to filer (default=True) * remove misleading typehint --- aldryn_forms/cms_plugins.py | 57 +++++++++++-------- .../contrib/email_notifications/models.py | 36 ++++++++---- .../migrations/0016_auto_20231108_2151.py | 33 +++++++++++ aldryn_forms/models.py | 13 ++++- 4 files changed, 101 insertions(+), 38 deletions(-) create mode 100644 aldryn_forms/migrations/0016_auto_20231108_2151.py diff --git a/aldryn_forms/cms_plugins.py b/aldryn_forms/cms_plugins.py index 208540c4..f01cca23 100644 --- a/aldryn_forms/cms_plugins.py +++ b/aldryn_forms/cms_plugins.py @@ -1,6 +1,8 @@ from typing import Dict from PIL import Image +from django.core.files.uploadedfile import InMemoryUploadedFile + from aldryn_forms.models import FormPlugin from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool @@ -32,7 +34,6 @@ from .forms import TextAreaFieldForm from .forms import TextFieldForm from .helpers import get_user_name -from .models import FileUploadFieldPlugin from .models import SerializedFormField from .signals import form_post_save from .signals import form_pre_save @@ -592,6 +593,7 @@ class FileField(Field): 'upload_to', ] + Field.fieldset_general_fields fieldset_advanced_fields = [ + 'store_to_filer', 'help_text', 'max_size', 'allowed_extensions', @@ -614,49 +616,53 @@ def get_form_field_validators(self, instance: models.FileFieldPluginBase): return validators def serialize_value(self, instance, value, is_confirmation=False): - if value: + if value and hasattr(value, "absolute_uri"): return value.absolute_uri else: return '-' - def form_pre_save(self, instance, form, **kwargs): + def form_pre_save(self, instance: models.FileUploadFieldPlugin, form, **kwargs): """Save the uploaded file to django-filer The type of model (file or image) is automatically chosen by trying to open the uploaded file. """ + request = kwargs['request'] field_name = form.form_plugin.get_form_field_name(field=instance) - uploaded_file = form.cleaned_data[field_name] + uploaded_file: InMemoryUploadedFile = form.cleaned_data[field_name] if uploaded_file is None: return - try: - with Image.open(uploaded_file) as img: - img.verify() - except: # noqa - model = filemodels.File - else: - model = imagemodels.Image - - filer_file = model( - folder=instance.upload_to, - file=uploaded_file, - name=uploaded_file.name, - original_filename=uploaded_file.name, - is_public=True, - ) - filer_file.save() + if instance.store_to_filer: + try: + with Image.open(uploaded_file) as img: + img.verify() + except: # noqa + model = filemodels.File + else: + model = imagemodels.Image + + filer_file = model( + folder=instance.upload_to, + file=uploaded_file, + name=uploaded_file.name, + original_filename=uploaded_file.name, + is_public=True, + ) + filer_file.save() + + # NOTE: This is a hack to make the full URL available later when we + # need to serialize this field. We avoid to serialize it here directly + # as we could still need access to the original filer File instance. + filer_file.absolute_uri = request.build_absolute_uri(filer_file.url) - # NOTE: This is a hack to make the full URL available later when we - # need to serialize this field. We avoid to serialize it here directly - # as we could still need access to the original filer File instance. - filer_file.absolute_uri = request.build_absolute_uri(filer_file.url) + form.cleaned_data[field_name] = filer_file - form.cleaned_data[field_name] = filer_file + form.cleaned_data[f"{field_name}__in_memory"] = uploaded_file class ImageField(FileField): @@ -670,6 +676,7 @@ class ImageField(FileField): 'upload_to', ] + Field.fieldset_general_fields fieldset_advanced_fields = [ + 'store_to_filer', 'help_text', 'max_size', ('max_width', 'max_height',), diff --git a/aldryn_forms/contrib/email_notifications/models.py b/aldryn_forms/contrib/email_notifications/models.py index 6188cc26..8af4321a 100644 --- a/aldryn_forms/contrib/email_notifications/models.py +++ b/aldryn_forms/contrib/email_notifications/models.py @@ -1,10 +1,11 @@ import mimetypes +import typing from email.utils import formataddr from functools import partial from django.conf import settings from django.core.exceptions import ValidationError -from django.core.files.storage import default_storage +from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.mail import EmailMultiAlternatives from django.db import models from django.utils.translation import gettext @@ -12,10 +13,9 @@ from djangocms_text_ckeditor.fields import HTMLField from emailit.api import construct_mail -from filer.models import File from aldryn_forms.helpers import get_user_name -from aldryn_forms.models import FormPlugin +from aldryn_forms.models import FormPlugin, FileFieldPluginBase from .helpers import ( get_email_template_name, get_theme_template_name, render_text, @@ -237,19 +237,31 @@ def attach_files(self, email: EmailMultiAlternatives, form): files_to_attach = serialize_delimiter_separated_values_string( self.files_to_attach_to_email, delimiter=",", strip=True, lower=False ) + if not files_to_attach: return - for field_name in form.fields: + + # noinspection PyProtectedMember + fields = [ + field + for field in form.base_fields.values() + if hasattr(field, "_model_instance") and field._model_instance.IS_FILE_FIELD + ] + + for field in fields: + # noinspection PyProtectedMember + field_name = field._model_instance.name + if field_name in files_to_attach: - file_field: File = form.cleaned_data.get(field_name) - if not file_field: + field_file_handler_name = f"{field_name}__in_memory" + file: InMemoryUploadedFile = form.cleaned_data.get(field_file_handler_name) + if not file: continue - with default_storage.open(file_field.path, "rb") as file: - email.attach( - filename=file_field.original_filename, - content=file.read(), - mimetype=mimetypes.guess_type(file_field.original_filename)[0], - ) + email.attach( + filename=file.name, + content=file.read(), + mimetype=mimetypes.guess_type(file.name)[0], + ) def prepare_email(self, form): email_kwargs = self.get_email_kwargs(form) diff --git a/aldryn_forms/migrations/0016_auto_20231108_2151.py b/aldryn_forms/migrations/0016_auto_20231108_2151.py new file mode 100644 index 00000000..fd8abab3 --- /dev/null +++ b/aldryn_forms/migrations/0016_auto_20231108_2151.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2 on 2023-11-08 16:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('aldryn_forms', '0015_auto_20231105_0313'), + ] + + operations = [ + migrations.AddField( + model_name='fileuploadfieldplugin', + name='store_to_filer', + field=models.BooleanField(default=True, help_text='Whether to store this file to filer. If this is unchecked and this file is not attached to any email notification, the file will be lost forever and using it as a template variable in any email template will return empty result.', verbose_name='Store this file to filer'), + ), + migrations.AddField( + model_name='imageuploadfieldplugin', + name='store_to_filer', + field=models.BooleanField(default=True, help_text='Whether to store this file to filer. If this is unchecked and this file is not attached to any email notification, the file will be lost forever and using it as a template variable in any email template will return empty result.', verbose_name='Store this file to filer'), + ), + migrations.AlterField( + model_name='fileuploadfieldplugin', + name='allowed_extensions', + field=models.CharField(blank=True, default='', help_text='Comma-separated list of file extensions allowed for this file field. Leave it empty to allow any extension.', max_length=255, verbose_name='Allowed extensions'), + ), + migrations.AlterField( + model_name='imageuploadfieldplugin', + name='allowed_extensions', + field=models.CharField(blank=True, default='', help_text='Comma-separated list of file extensions allowed for this file field. Leave it empty to allow any extension.', max_length=255, verbose_name='Allowed extensions'), + ), + ] diff --git a/aldryn_forms/models.py b/aldryn_forms/models.py index 3ae2f3d1..e031c2d3 100644 --- a/aldryn_forms/models.py +++ b/aldryn_forms/models.py @@ -509,7 +509,18 @@ class FileFieldPluginBase(FieldPluginBase): _( "Comma-separated list of file extensions allowed for this file field. " "Leave it empty to allow any extension." - ), + ) + ), + ) + store_to_filer = models.BooleanField( + verbose_name=_("Store this file to filer"), + default=True, + help_text=( + _( + "Whether to store this file to filer. If this is unchecked and this file is not attached to any email " + "notification, the file will be lost forever and using it as a template variable in any email template " + "will return empty result." + ) ), ) From 2bc6494fbae8532ec348c96d951e760d2365c316 Mon Sep 17 00:00:00 2001 From: Hassan Ahmed Date: Thu, 9 Nov 2023 18:27:17 +0500 Subject: [PATCH 10/11] create a copy of in-memory file to avoid unpredictability (#5) * create a copy of in-memory file to avoid unpredictability * format fix --- aldryn_forms/cms_plugins.py | 17 +++++++++++------ .../contrib/email_notifications/models.py | 15 +++++++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/aldryn_forms/cms_plugins.py b/aldryn_forms/cms_plugins.py index f01cca23..dafc6d08 100644 --- a/aldryn_forms/cms_plugins.py +++ b/aldryn_forms/cms_plugins.py @@ -1,3 +1,4 @@ +import io from typing import Dict from PIL import Image @@ -547,9 +548,9 @@ class EmailField(BaseTextField): form_field_widget = forms.EmailInput form_field_widget_input_type = 'email' fieldset_advanced_fields = [ - 'email_send_notification', - 'email_subject', - 'email_body', + "email_send_notification", + "email_subject", + "email_body", ] + Field.fieldset_advanced_fields email_template_base = 'aldryn_forms/emails/user/notification' @@ -590,7 +591,7 @@ class FileField(Field): 'validators', ] fieldset_general_fields = [ - 'upload_to', + "upload_to", ] + Field.fieldset_general_fields fieldset_advanced_fields = [ 'store_to_filer', @@ -633,6 +634,8 @@ def form_pre_save(self, instance: models.FileUploadFieldPlugin, form, **kwargs): field_name = form.form_plugin.get_form_field_name(field=instance) uploaded_file: InMemoryUploadedFile = form.cleaned_data[field_name] + copy = io.BytesIO(uploaded_file.read()) + uploaded_file.seek(0) if uploaded_file is None: return @@ -662,7 +665,8 @@ def form_pre_save(self, instance: models.FileUploadFieldPlugin, form, **kwargs): form.cleaned_data[field_name] = filer_file - form.cleaned_data[f"{field_name}__in_memory"] = uploaded_file + uploaded_file.close() + form.cleaned_data[f"{field_name}__in_memory"] = {"name": uploaded_file.name, "file": copy} class ImageField(FileField): @@ -673,7 +677,7 @@ class ImageField(FileField): form_field = RestrictedImageField form_field_widget = RestrictedImageField.widget fieldset_general_fields = [ - 'upload_to', + "upload_to", ] + Field.fieldset_general_fields fieldset_advanced_fields = [ 'store_to_filer', @@ -875,6 +879,7 @@ def serialize_field(self, *args, **kwargs): # None means don't serialize me return None + plugin_pool.register_plugin(CaptchaField) diff --git a/aldryn_forms/contrib/email_notifications/models.py b/aldryn_forms/contrib/email_notifications/models.py index 8af4321a..07e27e25 100644 --- a/aldryn_forms/contrib/email_notifications/models.py +++ b/aldryn_forms/contrib/email_notifications/models.py @@ -1,3 +1,4 @@ +import io import mimetypes import typing from email.utils import formataddr @@ -253,14 +254,20 @@ def attach_files(self, email: EmailMultiAlternatives, form): field_name = field._model_instance.name if field_name in files_to_attach: - field_file_handler_name = f"{field_name}__in_memory" - file: InMemoryUploadedFile = form.cleaned_data.get(field_file_handler_name) + field_file_key = f"{field_name}__in_memory" + filed_file_data: typing.Dict[str, typing.Union[str, io.BytesIO]] = form.cleaned_data.get(field_file_key) + file = filed_file_data["file"] + file_name = filed_file_data["name"] if not file: continue + + # Redundant precaution + file.seek(0) + email.attach( - filename=file.name, + filename=file_name, content=file.read(), - mimetype=mimetypes.guess_type(file.name)[0], + mimetype=mimetypes.guess_type(file_name)[0], ) def prepare_email(self, form): From d42a1d06ec1d350f9dbdacf45fde0b83f6eb486d Mon Sep 17 00:00:00 2001 From: Hassan Ahmed Date: Tue, 14 Nov 2023 14:33:50 +0500 Subject: [PATCH 11/11] Feature/filefield custom error on allowed extensions validation (#6) * Added a custom error message field on extension validation * cleanup * only make the static string gettext lazy * Added accept attribute in frontend for file upload plugin * minor optimization --- aldryn_forms/cms_plugins.py | 9 +++- aldryn_forms/forms.py | 23 ++++++++- .../migrations/0017_auto_20231113_1824.py | 48 +++++++++++++++++++ aldryn_forms/models.py | 9 +++- aldryn_forms/validators.py | 6 ++- 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 aldryn_forms/migrations/0017_auto_20231113_1824.py diff --git a/aldryn_forms/cms_plugins.py b/aldryn_forms/cms_plugins.py index dafc6d08..fb31a45d 100644 --- a/aldryn_forms/cms_plugins.py +++ b/aldryn_forms/cms_plugins.py @@ -598,6 +598,7 @@ class FileField(Field): 'help_text', 'max_size', 'allowed_extensions', + 'invalid_extension_message', 'required_message', 'custom_classes', ] @@ -609,11 +610,17 @@ def get_form_field_kwargs(self, instance): kwargs['help_text'] = kwargs['help_text'].replace( 'MAXSIZE', filesizeformat(instance.max_size)) kwargs['max_size'] = instance.max_size + if instance.allowed_extensions: + kwargs['allowed_extensions'] = instance.allowed_extensions return kwargs def get_form_field_validators(self, instance: models.FileFieldPluginBase): validators = super().get_form_field_validators(instance) - validators.append(generate_file_extension_validator(instance.allowed_extensions)) + validators.append( + generate_file_extension_validator( + instance.allowed_extensions, instance.invalid_extension_message + ) + ) return validators def serialize_value(self, instance, value, is_confirmation=False): diff --git a/aldryn_forms/forms.py b/aldryn_forms/forms.py index e74b0f97..0a30230c 100644 --- a/aldryn_forms/forms.py +++ b/aldryn_forms/forms.py @@ -9,7 +9,8 @@ from .models import FormPlugin, FormSubmission from .sizefield.utils import filesizeformat -from .utils import add_form_error, get_user_model +from .utils import add_form_error, get_user_model, serialize_delimiter_separated_values_string +import mimetypes class FileSizeCheckMixin(object): @@ -33,7 +34,25 @@ def clean(self, *args, **kwargs): class RestrictedFileField(FileSizeCheckMixin, forms.FileField): - pass + def __init__(self, *args, **kwargs): + self.allowed_extensions = kwargs.pop('allowed_extensions', None) + super(RestrictedFileField, self).__init__(*args, **kwargs) + + def widget_attrs(self, widget): + attrs = super().widget_attrs(widget) + if self.allowed_extensions: + allowed_extensions_list = [ + extension if extension.startswith(".") else f".{extension}" + for extension in serialize_delimiter_separated_values_string( + self.allowed_extensions, delimiter=",", strip=True, lower=True + ) + ] + accepted_types = [ + mimetypes.guess_type(f"dummy{extension}")[0] or extension + for extension in allowed_extensions_list + ] + attrs.setdefault('accept', ",".join(accepted_types)) + return attrs class RestrictedImageField(FileSizeCheckMixin, forms.ImageField): diff --git a/aldryn_forms/migrations/0017_auto_20231113_1824.py b/aldryn_forms/migrations/0017_auto_20231113_1824.py new file mode 100644 index 00000000..4c042c50 --- /dev/null +++ b/aldryn_forms/migrations/0017_auto_20231113_1824.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2 on 2023-11-13 13:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('aldryn_forms', '0016_auto_20231108_2151'), + ] + + operations = [ + migrations.AddField( + model_name='fileuploadfieldplugin', + name='invalid_extension_message', + field=models.TextField(blank=True, help_text='Error message displayed if extensions are constrained and the uploaded file fails that validation.Default: "File extension [extension] is not allowed for this field."', null=True, verbose_name='Invalid extension error message'), + ), + migrations.AddField( + model_name='imageuploadfieldplugin', + name='invalid_extension_message', + field=models.TextField(blank=True, help_text='Error message displayed if extensions are constrained and the uploaded file fails that validation.Default: "File extension [extension] is not allowed for this field."', null=True, verbose_name='Invalid extension error message'), + ), + migrations.AlterField( + model_name='emailfieldplugin', + name='required_message', + field=models.TextField(blank=True, help_text='Error message displayed if the required field is left empty. Default: "This field is required".', null=True, verbose_name='Field required error message'), + ), + migrations.AlterField( + model_name='fieldplugin', + name='required_message', + field=models.TextField(blank=True, help_text='Error message displayed if the required field is left empty. Default: "This field is required".', null=True, verbose_name='Field required error message'), + ), + migrations.AlterField( + model_name='fileuploadfieldplugin', + name='required_message', + field=models.TextField(blank=True, help_text='Error message displayed if the required field is left empty. Default: "This field is required".', null=True, verbose_name='Field required error message'), + ), + migrations.AlterField( + model_name='imageuploadfieldplugin', + name='required_message', + field=models.TextField(blank=True, help_text='Error message displayed if the required field is left empty. Default: "This field is required".', null=True, verbose_name='Field required error message'), + ), + migrations.AlterField( + model_name='textareafieldplugin', + name='required_message', + field=models.TextField(blank=True, help_text='Error message displayed if the required field is left empty. Default: "This field is required".', null=True, verbose_name='Field required error message'), + ), + ] diff --git a/aldryn_forms/models.py b/aldryn_forms/models.py index e031c2d3..784bcd46 100644 --- a/aldryn_forms/models.py +++ b/aldryn_forms/models.py @@ -370,7 +370,7 @@ class FieldPluginBase(CMSPlugin): label = models.CharField(_('Label'), max_length=255, blank=True) required = models.BooleanField(_('Field is required'), default=False) required_message = models.TextField( - verbose_name=_('Error message'), + verbose_name=_('Field required error message'), blank=True, null=True, help_text=_('Error message displayed if the required field is left ' @@ -512,6 +512,13 @@ class FileFieldPluginBase(FieldPluginBase): ) ), ) + invalid_extension_message = models.TextField( + verbose_name=_('Invalid extension error message'), + blank=True, + null=True, + help_text=_('Error message displayed if extensions are constrained and the uploaded file fails that validation.' + 'Default: "File extension [extension] is not allowed for this field."') + ) store_to_filer = models.BooleanField( verbose_name=_("Store this file to filer"), default=True, diff --git a/aldryn_forms/validators.py b/aldryn_forms/validators.py index 6534c427..17dfcdaa 100644 --- a/aldryn_forms/validators.py +++ b/aldryn_forms/validators.py @@ -10,7 +10,7 @@ from aldryn_forms.utils import serialize_delimiter_separated_values_string -def generate_file_extension_validator(allowed_extensions_str: str = ""): +def generate_file_extension_validator(allowed_extensions_str: str = "", error_message: str = ""): allowed_extensions = serialize_delimiter_separated_values_string( allowed_extensions_str, delimiter=",", strip=True, lower=True ) @@ -27,7 +27,9 @@ def validator(value): extension = os.path.splitext(value.name)[1] # [0] returns path+filename if not extension.lower() in allowed_extensions: raise ValidationError( - _(f"File extension '{extension}' is not allowed for this field."), + error_message + if error_message + else _(f"File extension '{extension}' is not allowed for this field."), code="invalid_extension", )