diff --git a/app/deploy/tasks.py b/app/deploy/tasks.py index 9fe1794..7ff2fb5 100644 --- a/app/deploy/tasks.py +++ b/app/deploy/tasks.py @@ -42,7 +42,7 @@ common_configure = [ {"action": "apt", "params": ["git", "python-setuptools", "python-dev", "build-essential", "python-pip", "libmysqlclient-dev", - "mysql-server", "mysql-client"], + "mysql-server", "mysql-client", "libjpeg-dev"], "message":"Installing apt-get packages" }, # {"action": "sudo", "params": "mysql_secure_installation", diff --git a/app/settings.py b/app/settings.py index 2541bf2..2b1ff46 100644 --- a/app/settings.py +++ b/app/settings.py @@ -218,7 +218,7 @@ INSTALLED_APPS = ( 'aldryn_bootstrap3', - 'djangocms_forms', + 'djangocms_forms' # 'debug_toolbar', ) diff --git a/djangocms_forms/__init__.py b/djangocms_forms/__init__.py new file mode 100755 index 0000000..5e208af --- /dev/null +++ b/djangocms_forms/__init__.py @@ -0,0 +1,3 @@ +__version__ = '0.1.6' + +default_app_config = 'djangocms_forms.apps.DjangoCMSFormsConfig' diff --git a/djangocms_forms/admin.py b/djangocms_forms/admin.py new file mode 100644 index 0000000..8aefb5a --- /dev/null +++ b/djangocms_forms/admin.py @@ -0,0 +1,263 @@ +import datetime +from functools import update_wrapper + +from django.contrib import admin, messages +from django.contrib.admin.helpers import AdminErrorList, AdminForm +from django.contrib.auth import get_permission_codename +from django.contrib.auth.admin import csrf_protect_m +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse +from django.shortcuts import redirect +from django.template.defaultfilters import slugify, yesno +from django.template.response import TemplateResponse +from django.utils import timezone +from django.utils.encoding import force_text +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ +from tablib import Dataset + +from .conf import settings +from .forms import SubmissionExportForm +from .models import Form, FormSubmission + +try: + from django.contrib.admin.utils import unquote +except ImportError: + from django.contrib.admin.util import unquote + +try: + from django.http import JsonResponse +except ImportError: + from .compat import JsonResponse + + +class FormFilter(admin.SimpleListFilter): + title = _('Forms') + parameter_name = 'form' + + def lookups(self, request, model_admin): + forms = Form.active_objects.all() + for obj in forms: + yield ( + str(obj.id), u'%s (%s)' % (obj.name, obj.submission_count) + ) + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter(plugin_id=self.value()) + + +class FormSubmissionAdmin(admin.ModelAdmin): + change_form_template = 'admin/djangocms_forms/formsubmission/change_form.html' + change_list_template = 'admin/djangocms_forms/formsubmission/change_list.html' + export_form_template = 'admin/djangocms_forms/formsubmission/export_form.html' + list_display = ('plugin', 'creation_date_display', 'created_by', 'ip', 'referrer', ) + list_filter = (FormFilter, ) + readonly_fields = ('creation_date_display', 'created_by', 'plugin', 'ip', 'referrer', ) + date_hierarchy = 'creation_date' + fieldsets = ( + (None, { + 'fields': ('creation_date_display', 'created_by', 'ip', 'referrer', ) + }), + ) + + class Media: + js = ( + 'js/djangocms_forms/admin/jquery-form-export.js', + ) + + def has_add_permission(self, request): + return False + + def has_export_permission(self, request): + opts = self.opts + codename = get_permission_codename('export', opts) + return request.user.has_perm('%s.%s' % (opts.app_label, codename)) + + def creation_date_display(self, obj): + return obj.creation_date.strftime(settings.DJANGOCMS_FORMS_DATETIME_FORMAT) + creation_date_display.short_description = _('Sent On') + + def get_queryset(self, request): + qs = super(FormSubmissionAdmin, self).get_queryset(request) + return qs.select_related('created_by', 'plugin', ) + + def get_urls(self): + """ + Add the export view to urls. + """ + urls = super(FormSubmissionAdmin, self).get_urls() + from django.conf.urls import patterns, url + + def wrap(view): + def wrapper(*args, **kwargs): + return self.admin_site.admin_view(view)(*args, **kwargs) + return update_wrapper(wrapper, view) + + info = self.model._meta.app_label, self.model._meta.model_name + + extra_urls = patterns( + '', + url(r'^export/$', wrap(self.export_view), name='%s_%s_export' % info), + ) + return extra_urls + urls + + @csrf_protect_m + def changelist_view(self, request, extra_context=None): + context = extra_context or {} + context.update({ + 'title': self.opts.verbose_name_plural.title(), + 'has_export_permission': self.has_export_permission(request), + }) + return super(FormSubmissionAdmin, self).changelist_view( + request, extra_context=context) + + def change_view(self, request, object_id, form_url='', extra_context=None): + context = extra_context or {} + obj = self.get_object(request, unquote(object_id)) + + if obj: + context.update({ + 'title': force_text(obj.plugin), + }) + + return super(FormSubmissionAdmin, self).change_view( + request, object_id, form_url=form_url, + extra_context=context) + + def export_view(self, request, form_url=''): + """The 'export' admin view for this model.""" + + info = self.opts.app_label, self.opts.model_name + + if not self.has_export_permission(request): + raise PermissionDenied + + form = SubmissionExportForm(data=request.POST if request.method == 'POST' else None) + + if form.is_valid(): + data = form.cleaned_data + queryset = self.get_queryset(request) \ + .filter(plugin_id=data.get('form')) \ + .select_related('created_by', 'plugin', ) + + from_date, to_date = data.get('from_date'), data.get('to_date') + headers = data.get('headers', []) + + if from_date: + queryset = queryset.filter(creation_date__gte=from_date) + if to_date: + queryset = queryset.filter(creation_date__lt=to_date + datetime.timedelta(days=1)) + + if not queryset.exists(): + message = _('No matching %s found for the given criteria. ' + 'Please try again.') % self.opts.verbose_name_plural + self.message_user(request, message, level=messages.WARNING) + if request.is_ajax(): + data = { + 'reloadBrowser': True, + 'submissionCount': 0, + } + return JsonResponse(data) + return redirect('admin:%s_%s_export' % info) + + latest_submission = queryset[:1].get() + dataset = Dataset(title=latest_submission.plugin.name) + + if not headers: + headers = [field['label'].strip() for field in latest_submission.form_data] + for submission in queryset: + for field in submission.form_data: + label = field['label'].strip() + if label not in headers: + headers.append(label) + + if request.is_ajax(): + data = { + 'reloadBrowser': False, + 'submissionCount': queryset.count(), + 'availableHeaders': headers, + } + return JsonResponse(data) + + headers.extend(['Submitted on', 'Sender IP', 'Referrer URL']) + dataset.headers = headers + + def humanize(field): + value = field['value'] + field_type = field['type'] + + if value in (None, '', [], (), {}): + return None + + if field_type == 'checkbox': + value = yesno(bool(value), u'{0},{1}'.format(_('Yes'), _('No'))) + if field_type == 'checkbox_multiple': + value = ', '.join(list(value)) + return value + + for submission in queryset: + row = [None] * len(headers) + for field in submission.form_data: + label = field['label'].strip() + if label in headers: + row[headers.index(label)] = humanize(field) + row[-3] = submission.creation_date.strftime( + settings.DJANGOCMS_FORMS_DATETIME_FORMAT) + row[-2] = submission.ip + row[-1] = submission.referrer + dataset.append(row) + + mimetype = { + 'xls': 'application/vnd.ms-excel', + 'csv': 'text/csv', + 'html': 'text/html', + 'yaml': 'text/yaml', + 'json': 'application/json', + } + + file_type = data.get('file_type', 'xls') + filename = settings.DJANGOCMS_FORMS_EXPORT_FILENAME.format( + form_name=slugify(latest_submission.plugin.name)) + filename = timezone.now().strftime(filename) + filename = '%s.%s' % (filename, file_type) + + response = HttpResponse( + getattr(dataset, file_type), { + 'content_type': mimetype.get(file_type, 'application/octet-stream') + }) + + response['Content-Disposition'] = 'attachment; filename=%s' % filename + return response + + # Wrap in all admin layout + fieldsets = ((None, {'fields': form.fields.keys()}),) + adminform = AdminForm(form, fieldsets, {}, model_admin=self) + media = self.media + adminform.media + + context = { + 'title': _('Export %s') % force_text(self.opts.verbose_name_plural), + 'adminform': adminform, + 'is_popup': '_popup' in request.REQUEST, + 'media': mark_safe(media), + 'errors': AdminErrorList(form, ()), + 'app_label': self.opts.app_label, + } + return self.render_export_form(request, context, form_url) + + def render_export_form(self, request, context, form_url=''): + """ + Render the from submission export form. + """ + context.update({ + 'has_change_permission': self.has_change_permission(request), + 'form_url': mark_safe(form_url), + 'opts': self.opts, + 'add': True, + 'save_on_top': self.save_on_top, + }) + + return TemplateResponse(request, self.export_form_template, context) + + +admin.site.register(FormSubmission, FormSubmissionAdmin) diff --git a/djangocms_forms/apps.py b/djangocms_forms/apps.py new file mode 100644 index 0000000..57b720b --- /dev/null +++ b/djangocms_forms/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class DjangoCMSFormsConfig(AppConfig): + name = 'djangocms_forms' + verbose_name = _('Forms') diff --git a/djangocms_forms/cms_plugins.py b/djangocms_forms/cms_plugins.py new file mode 100644 index 0000000..77608f5 --- /dev/null +++ b/djangocms_forms/cms_plugins.py @@ -0,0 +1,117 @@ +from cms.plugin_base import CMSPluginBase +from cms.plugin_pool import plugin_pool +from django import forms +from django.contrib import admin +from django.db import models +from django.template.loader import select_template +from django.utils.translation import ugettext_lazy as _ + +from .conf import settings +from .forms import FormBuilder, FormDefinitionAdminForm, FormFieldInlineForm +from .models import FormDefinition, FormField + + +class FormFieldInline(admin.StackedInline): + model = FormField + form = FormFieldInlineForm + extra = 0 + fieldsets = ( + (None, { + 'fields': (('label', 'field_type', 'required'), + 'initial', 'placeholder_text', 'help_text', + 'choice_values', 'position', ) + }), + ) + + formfield_overrides = { + models.TextField: { + 'widget': forms.Textarea( + attrs={'rows': 4, 'cols': 50}) + }, + } + + class Media: + css = { + 'all': ('css/djangocms_forms/admin/djangocms_forms.css',) + } + js = ( + 'js/djangocms_forms/libs/jquery.min.js', + 'js/djangocms_forms/libs/jquery-ui.min.js', + + 'js/djangocms_forms/admin/jquery-inline-positioning.js', + 'js/djangocms_forms/admin/jquery-inline-rename.js', + 'js/djangocms_forms/admin/jquery-inline-collapsible.js', + 'js/djangocms_forms/admin/jquery-inline-toggle-fields.js', + ) + + +class FormPlugin(CMSPluginBase): + name = settings.DJANGOCMS_FORMS_PLUGIN_NAME + module = settings.DJANGOCMS_FORMS_PLUGIN_MODULE + model = FormDefinition + cache = False + form = FormDefinitionAdminForm + inlines = (FormFieldInline, ) + render_template = settings.DJANGOCMS_FORMS_DEFAULT_TEMPLATE + + def get_fieldsets(self, request, obj=None): + if settings.DJANGOCMS_FORMS_FIELDSETS: + return settings.DJANGOCMS_FORMS_FIELDSETS + + fieldsets = ( + (None, {'fields': ('name', )}), + + (None, { + 'description': _('The Title and Description ' + 'will display above the input fields and Submit button.'), + 'fields': ('title', 'description', ) + }), + (None, { + 'description': _('By default, the Submit Button will say Submit. ' + 'You can change this to say whatever you want'), + 'fields': ('submit_btn_txt', 'form_template', ) + }), + (None, { + 'description': _('You can also change the message that appears after someone ' + 'submits your form. ' + 'By default, this says Thank you!, ' + 'but you are welcome to change this text as well.'), + 'fields': ('post_submit_msg', ) + }), + (None, { + 'fields': ('success_redirect', ('page_redirect', 'external_redirect'), ), + }), + (None, { + 'description': 'Submission Settings — ' + 'Choose storage options to capture form data. You can enter ' + 'an email address to have the form submissions emailed to you or ' + 'log all the form submissions to the database.', + 'fields': ('email_to', 'email_from', 'email_subject', + 'email_uploaded_files', 'save_data', ), + }), + ) + return fieldsets + + def get_render_template(self, context, instance, placeholder): + # returns the first template that exists, falling back to bundled template + return select_template([ + instance.form_template, + settings.DJANGOCMS_FORMS_DEFAULT_TEMPLATE, + 'djangocms_forms/form_template/default.html' + ]) + + def render(self, context, instance, placeholder): + context = super(FormPlugin, self).render(context, instance, placeholder) + request = context['request'] + + form = FormBuilder( + initial={'referrer': request.path_info}, form_definition=instance, + label_suffix='', auto_id='%s') + + context.update({ + 'form': form + }) + return context + + +plugin_pool.register_plugin(FormPlugin) diff --git a/djangocms_forms/compat.py b/djangocms_forms/compat.py new file mode 100644 index 0000000..70dedb5 --- /dev/null +++ b/djangocms_forms/compat.py @@ -0,0 +1,15 @@ +import json + +from django.core.serializers.json import DjangoJSONEncoder +from django.http import HttpResponse + + +class JsonResponse(HttpResponse): + + def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, **kwargs): + if safe and not isinstance(data, dict): + raise TypeError('In order to allow non-dict objects to be ' + 'serialized set the safe parameter to False') + kwargs.setdefault('content_type', 'application/json') + data = json.dumps(data, cls=encoder) + super(JsonResponse, self).__init__(content=data, **kwargs) diff --git a/djangocms_forms/conf.py b/djangocms_forms/conf.py new file mode 100644 index 0000000..8882a7e --- /dev/null +++ b/djangocms_forms/conf.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +from appconf import AppConf +from django.conf import settings # noqa +from django.utils.translation import ugettext_lazy as _ + + +class DjangoCMSFormsConf(AppConf): + PLUGIN_MODULE = _('Generic') + PLUGIN_NAME = _('Form') + FIELDSETS = None + FILE_STORAGE_DIR = 'djangocms_forms' + FILE_STORAGE = settings.DEFAULT_FILE_STORAGE + + ALLOWED_FILE_TYPES = ( + 'aac', 'ace', 'ai', 'aiff', 'avi', 'bmp', 'dir', 'doc', 'docx', 'dmg', + 'eps', 'fla', 'flv', 'gif', 'gz', 'hqx', 'ico', 'indd', 'inx', 'jpg', + 'jar', 'jpeg', 'md', 'mov', 'mp3', 'mp4', 'mpc', 'mkv', 'mpg', 'mpeg', + 'ogg', 'odg', 'odf', 'odp', 'ods', 'odt', 'otf', 'pdf', 'png', 'pps', + 'ppsx', 'ps', 'psd', 'rar', 'rm', 'rtf', 'sit', 'swf', 'tar', 'tga', + 'tif', 'tiff', 'ttf', 'txt', 'wav', 'wma', 'wmv', 'xls', 'xlsx', 'xml', + 'zip' + ) + MAX_UPLOAD_SIZE = 5242880 # 5MB + + FIELD_TYPES = ( + ('text', _('Text')), + ('textarea', _('Text Area')), + ('email', _('Email')), + ('number', _('Number')), + ('phone', _('Phone')), + ('url', _('URL')), + ('checkbox', _('Checkbox')), + ('checkbox_multiple', _('Multi Checkbox')), + ('select', _('Drop down')), + ('radio', _('Radio')), + ('file', _('File Upload')), + ('date', _('Date')), + ('time', _('Time')), + ('password', _('Password')), + ('hidden', _('Hidden')), + ) + + DEFAULT_FIELD_TYPE = 'text' + + SPAM_PROTECTIONS = ( + (0, _('None')), + (1, _('Honeypot')), + (2, _('ReCAPTCHA')), + ) + + DEFAULT_SPAM_PROTECTION = 0 + + TEMPLATES = ( + ('djangocms_forms/form_template/default.html', _('Default')), + ) + + DEFAULT_TEMPLATE = 'djangocms_forms/form_template/default.html' + + DATETIME_FORMAT = '%d/%m/%Y %H:%M' + EXPORT_FILENAME = 'export-{form_name}-%Y-%m-%d' + + HASHIDS_SALT = settings.SECRET_KEY + + class Meta: + prefix = 'djangocms_forms' diff --git a/djangocms_forms/fields.py b/djangocms_forms/fields.py new file mode 100644 index 0000000..68ebd8f --- /dev/null +++ b/djangocms_forms/fields.py @@ -0,0 +1,86 @@ +import os + +from django import forms +from django.core.exceptions import ValidationError +from django.db import models +from django.db.models import SET_NULL +from django.template.defaultfilters import filesizeformat +from django.utils.translation import ugettext_lazy as _ + +from .conf import settings + + +class FormBuilderFileField(forms.FileField): + def __init__(self, *args, **kwargs): + self.max_upload_size = kwargs.pop( + 'max_upload_size', settings.DJANGOCMS_FORMS_MAX_UPLOAD_SIZE) + allowed_file_types = kwargs.pop( + 'allowed_file_types', settings.DJANGOCMS_FORMS_ALLOWED_FILE_TYPES) + self.allowed_file_types = [i.lstrip('.').lower() + for i in allowed_file_types] + + super(FormBuilderFileField, self).__init__(*args, **kwargs) + + def clean(self, *args, **kwargs): + uploaded_file = super(FormBuilderFileField, self).clean(*args, **kwargs) + if not uploaded_file: + if self.required: + raise forms.ValidationError(_('This field is required.')) + return uploaded_file + + if not os.path.splitext(uploaded_file.name)[1].lstrip('.').lower() in \ + self.allowed_file_types: + raise forms.ValidationError( + _('Sorry, this filetype is not allowed. ' + 'Allowed filetype: %s') % ', '.join(self.allowed_file_types)) + + if uploaded_file._size > self.max_upload_size: + params = { + 'max_size': filesizeformat(self.max_upload_size), + 'size': filesizeformat(uploaded_file._size) + } + msg = _( + 'Please keep file size under %(max_size)s. Current size is %(size)s.') % params + raise forms.ValidationError(msg) + + return uploaded_file + + +class PluginReferenceField(models.ForeignKey): + def __init__(self, *args, **kwargs): + kwargs.update({'null': True}) # always allow Null + kwargs.update({'editable': False}) # never allow edits in admin + kwargs.update({'on_delete': SET_NULL}) # never delete plugin + super(PluginReferenceField, self).__init__(*args, **kwargs) + + def _create(self, model_instance): + return self.rel.to._default_manager.create(name=model_instance.name) + + def pre_save(self, model_instance, add): + if not model_instance.pk and add: + setattr(model_instance, self.name, self._create(model_instance)) + else: + reference = getattr(model_instance, self.name) + if not reference: + setattr(model_instance, self.name, self._create(model_instance)) + reference = getattr(model_instance, self.name) + if reference.name != model_instance.name: + reference.name = model_instance.name + reference.save() + return super(PluginReferenceField, self).pre_save(model_instance, add) + + def south_field_triple(self): + """Returns a suitable description of this field for South.""" + # We'll just introspect ourselves, since we inherit. + from south.modelsinspector import introspector + field_class = 'django.db.models.fields.related.ForeignKey' + args, kwargs = introspector(self) + return (field_class, args, kwargs) + + +class MultipleChoiceAutoCompleteField(forms.MultipleChoiceField): + + def validate(self, value): + if self.required and not value: + raise ValidationError(self.error_messages['required'], code='required') + return value diff --git a/djangocms_forms/forms.py b/djangocms_forms/forms.py new file mode 100644 index 0000000..d9a2583 --- /dev/null +++ b/djangocms_forms/forms.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import re + +from django import forms +from django.contrib.admin.widgets import (AdminDateWidget, + FilteredSelectMultiple) +from django.core.mail import EmailMultiAlternatives +from django.core.urlresolvers import reverse +from django.template.defaultfilters import slugify +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ +from ipware.ip import get_ip +from unidecode import unidecode + +from .fields import FormBuilderFileField, MultipleChoiceAutoCompleteField +from .models import Form, FormDefinition, FormField, FormSubmission +from .utils import int_to_hashid +from .widgets import DateInput, TelephoneInput, TimeInput + + +class FormFieldInlineForm(forms.ModelForm): + def clean(self): + cleaned_data = super(FormFieldInlineForm, self).clean() + + requires_choice_values = ['checkbox_multiple', 'select', 'radio'] + if (cleaned_data.get('field_type') in requires_choice_values and + not cleaned_data.get('choice_values')): + + error_msg = _('This field is required.') + self._errors['choice_values'] = self.error_class([error_msg]) + + return cleaned_data + + class Meta: + model = FormField + fields = '__all__' + + +class FormDefinitionAdminForm(forms.ModelForm): + def clean(self): + cleaned_data = super(FormDefinitionAdminForm, self).clean() + + populated_count = 0 + storage_fields = ('email_to', 'save_data', ) + + for field in storage_fields: + if cleaned_data.get(field, None): + populated_count += 1 + + if not populated_count: + error_msg = _( + 'You must choose a storage option for this Form. ' + 'You can choose to use multiple storage options if you prefer. ') + for field in storage_fields: + self._errors[field] = self.error_class([error_msg]) + + return cleaned_data + + class Meta: + model = FormDefinition + fields = '__all__' + + +class FormBuilder(forms.Form): + error_css_class = 'error' + required_css_class = 'required' + + form_id = forms.CharField(widget=forms.HiddenInput) + referrer = forms.CharField(widget=forms.HiddenInput) + + def __init__(self, form_definition, *args, **kwargs): + super(FormBuilder, self).__init__(*args, **kwargs) + self.form_definition = form_definition + self.field_names = [] + self.file_fields = [] + self.field_types = {} + + self.submission_url = reverse('djangocms_forms_submissions') + self.fields['form_id'].initial = int_to_hashid(form_definition.pk) + self.redirect_url = form_definition.redirect_url + + for field in form_definition.fields.all(): + if hasattr(self, 'prepare_%s' % field.field_type): + field_name = self.get_unique_field_name(field) + form_field = getattr(self, 'prepare_%s' % field.field_type)(field) + + self.fields[field_name] = form_field + + if isinstance(form_field, FormBuilderFileField): + self.file_fields.append(field_name) + + def get_unique_field_name(self, field): + field_name = '%s' % (slugify(unidecode(field.label)).replace('-', '_')) + if field_name in self.field_names: + i = 1 + while True: + if i > 1: + if i > 2: + field_name = field_name.rsplit('_', 1)[0] + field_name = '%s_%s' % (field_name, i) + if field_name not in self.field_names: + break + i += 1 + + self.field_names.append(field_name) + self.field_types[field_name] = field.field_type + return field_name + + def split_choices(self, choices): + return [x.strip() for x in choices.split(',') if x.strip()] + + def to_bool(self, value): + return value.lower() in ('yes', 'y', 'true', 't', '1') + + def prepare_text(self, field): + kwargs = field.field_attrs() + if field.placeholder_text and not field.initial: + kwargs.update({ + 'widget': forms.TextInput({ + 'placeholder': field.placeholder_text + }) + }) + return forms.CharField(**kwargs) + + def prepare_textarea(self, field): + kwargs = field.field_attrs() + kwargs.update({ + 'widget': forms.Textarea({ + 'placeholder': field.placeholder_text or '', + }) + }) + return forms.CharField(**kwargs) + + def prepare_email(self, field): + kwargs = field.field_attrs() + kwargs.update({ + 'widget': forms.EmailInput({ + 'placeholder': field.placeholder_text or '', + 'autocomplete': 'email', + }), + }) + return forms.EmailField(**kwargs) + + def prepare_checkbox(self, field): + kwargs = field.field_attrs() + if field.initial: + kwargs.update({ + 'initial': self.to_bool(field.initial) + }) + return forms.BooleanField(**kwargs) + + def prepare_checkbox_multiple(self, field): + kwargs = field.field_attrs() + kwargs.update({ + 'widget': forms.CheckboxSelectMultiple(), + 'choices': field.get_choices(), + }) + + if field.initial: + kwargs.update({ + 'initial': self.split_choices(field.initial) + }) + return forms.MultipleChoiceField(**kwargs) + + def prepare_select(self, field): + kwargs = field.field_attrs() + if field.choice_values: + choice_list = field.get_choices() + if not field.required: + choice_list.insert(0, ('', field.placeholder_text or _('Please select an option'))) + kwargs.update({ + 'choices': choice_list + }) + return forms.ChoiceField(**kwargs) + + def prepare_radio(self, field): + kwargs = field.field_attrs() + kwargs.update({ + 'widget': forms.RadioSelect(), + 'choices': field.get_choices(), + }) + return forms.ChoiceField(**kwargs) + + def prepare_file(self, field): + kwargs = field.field_attrs() + if field.choice_values: + regex = re.compile('[\s]*\n[\s]*') + choices = regex.split(field.choice_values) + kwargs.update({ + 'allowed_file_types': [i.lstrip('.').lower() for i in choices] + }) + return FormBuilderFileField(**kwargs) + + def prepare_date(self, field): + kwargs = field.field_attrs() + kwargs.update({ + 'widget': DateInput(), + }) + return forms.DateField(**kwargs) + + def prepare_time(self, field): + # @todo: needs proper widget + kwargs = field.field_attrs() + kwargs.update({ + 'widget': TimeInput(), + }) + return forms.TimeField(**kwargs) + + def prepare_hidden(self, field): + kwargs = field.field_attrs() + kwargs.update({ + 'widget': forms.HiddenInput(), + }) + return forms.CharField(**kwargs) + + def prepare_number(self, field): + kwargs = field.field_attrs() + return forms.IntegerField(**kwargs) + + def prepare_url(self, field): + kwargs = field.field_attrs() + return forms.URLField(**kwargs) + + def prepare_password(self, field): + kwargs = field.field_attrs() + kwargs.update({ + 'widget': forms.PasswordInput(), + }) + return forms.CharField(**kwargs) + + def prepare_phone(self, field): + kwargs = field.field_attrs() + kwargs.update({ + 'widget': TelephoneInput(), + }) + return forms.CharField(**kwargs) + + def save(self, request): + form_data = [] + for field in self.field_names: + value = self.cleaned_data[field] + if hasattr(value, 'url'): + value = value.url + form_data.append({ + 'name': field, + 'label': self.fields[field].label, + 'value': value, + 'type': self.field_types[field], + }) + + referrer = self.cleaned_data.get('referrer', '') + + if self.form_definition.save_data: + self.save_to_db(form_data, request=request, referrer=referrer) + if self.form_definition.email_to: + self.email_submission(form_data, request=request, referrer=referrer) + + def save_to_db(self, form_data, request, referrer): + user = request.user if request.user.is_authenticated() else None + # Если есть сессия для этой формы, то сохраняем в неё. + form_slug = "form-%d" % self.form_definition.plugin_reference.id + if request.session.get(form_slug, False): + try: + form_instance = FormSubmission.objects.get(id=request.session.get(form_slug, False)) + form_instance.form_data += form_data + form_instance.save() + # Удаляем сессию, если это последний шаг. Признак последнего шага + for data in form_data: + if data['name'] == 'end_session': + request.session[form_slug] = None + return + except (FormSubmission.DoesNotExist): + pass + form_instance = FormSubmission.objects.create( + plugin=self.form_definition.plugin_reference, + ip=get_ip(request), + referrer=referrer, + form_data=form_data, + created_by=user) + + request.session[form_slug] = form_instance.id + + def email_submission(self, form_data, request, referrer): + mail_to = re.compile('\s*[,;]+\s*').split(self.form_definition.email_to) + mail_from = self.form_definition.email_from or None + mail_subject = self.form_definition.email_subject or \ + 'Form Submission - %s' % self.form_definition.name + context = { + 'form': self.form_definition, + 'referrer': referrer, + 'title': mail_subject, + 'form_data': form_data, + 'request': request, + 'recipients': mail_to, + } + + message = render_to_string('djangocms_forms/email_template/email.txt', context) + message_html = render_to_string('djangocms_forms/email_template/email.html', context) + + email = EmailMultiAlternatives(mail_subject, message, mail_from, mail_to) + email.attach_alternative(message_html, 'text/html') + + if self.files and self.form_definition.email_uploaded_files: + for file_path in self.files: + email.attach_file(file_path) + + email.send(fail_silently=False) + + +class SubmissionExportForm(forms.Form): + FORMAT_CHOICES = ( + ('csv', _('CSV')), + ('json', _('JSON')), + ('yaml', _('YAML')), + ('xls', _('Microsoft Excel')), + ) + + form = forms.ModelChoiceField( + queryset=Form.active_objects.all(), label=_('Select a Form'), + error_messages={'required': _('Please select a form.')}, + help_text=_('Select the form you would like to export entry data from. ' + 'You may only export data from one form at a time.')) + file_type = forms.ChoiceField(choices=FORMAT_CHOICES, initial='xls', required=False) + headers = MultipleChoiceAutoCompleteField( + label=_('Fields'), required=False, + widget=FilteredSelectMultiple(verbose_name=_('Fields'), is_stacked=False),) + from_date = forms.DateField( + label=_('From date'), required=False, widget=AdminDateWidget) + to_date = forms.DateField( + label=_('To date'), required=False, widget=AdminDateWidget) diff --git a/djangocms_forms/managers.py b/djangocms_forms/managers.py new file mode 100644 index 0000000..a3ecccf --- /dev/null +++ b/djangocms_forms/managers.py @@ -0,0 +1,9 @@ +from django.db import models +from django.db.models import Count + + +class ActiveFormManager(models.Manager): + def get_queryset(self): + qs = super(ActiveFormManager, self).get_queryset() + return qs.annotate(submission_count=Count('submissions')) \ + .filter(submission_count__gt=0) diff --git a/djangocms_forms/migrations/0001_initial.py b/djangocms_forms/migrations/0001_initial.py new file mode 100644 index 0000000..e605d1b --- /dev/null +++ b/djangocms_forms/migrations/0001_initial.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import jsonfield.fields +import djangocms_forms.fields +import django.db.models.deletion +import cms.models.fields +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('cms', '__first__'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Form', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(verbose_name='Name', max_length=255, editable=False, db_index=True)), + ], + options={ + 'verbose_name': 'Form', + 'verbose_name_plural': 'Forms', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='FormDefinition', + fields=[ + ('cmsplugin_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='cms.CMSPlugin')), + ('name', models.CharField(max_length=255, verbose_name='Form Name')), + ('title', models.CharField(max_length=150, verbose_name='Title', blank=True)), + ('description', models.TextField(verbose_name='Description', blank=True)), + ('submit_btn_txt', models.CharField(default='Submit', help_text="Text for the Submit Button. The default is 'Submit'", max_length=100, verbose_name='Submit Button Text')), + ('post_submit_msg', models.TextField(default='Thank You', help_text='Display this message to users after they submit your form.', verbose_name='Post Submit Message', blank=True)), + ('success_redirect', models.BooleanField(default=False, help_text='HTTP redirect after successful submission', verbose_name='Redirect?')), + ('external_redirect', models.URLField(help_text='e.g. http://example.com/thank-you', verbose_name='External URL', blank=True)), + ('email_to', models.CharField(help_text='Separate several addresses with a comma.', max_length=255, verbose_name='Send form data to e-mail address', blank=True)), + ('email_from', models.EmailField(max_length=255, verbose_name='Sender Email Address', blank=True)), + ('email_subject', models.CharField(max_length=255, verbose_name='Email Subject', blank=True)), + ('email_uploaded_files', models.BooleanField(default=True, verbose_name='Send uploaded files as email attachments')), + ('save_data', models.BooleanField(default=True, help_text='Logs all form submissions to the database.', verbose_name='Save to database')), + ('spam_protection', models.SmallIntegerField(default=0, verbose_name='Spam Protection', choices=[(0, 'None'), (1, 'Honeypot'), (2, 'ReCAPTCHA')])), + ('form_template', models.CharField(default=b'djangocms_forms/form_template/default.html', max_length=150, verbose_name='Form Template', blank=True, choices=[(b'djangocms_forms/form_template/default.html', 'Default')])), + ('page_redirect', cms.models.fields.PageField(on_delete=django.db.models.deletion.SET_NULL, blank=True, to='cms.Page', help_text='A page has priority over an external URL', null=True, verbose_name='Page URL')), + ('plugin_reference', djangocms_forms.fields.PluginReferenceField(related_name='plugin', on_delete=django.db.models.deletion.SET_NULL, editable=False, to='djangocms_forms.Form', null=True)), + ], + options={ + 'verbose_name': 'Form', + 'verbose_name_plural': 'Forms', + }, + bases=('cms.cmsplugin',), + ), + migrations.CreateModel( + name='FormField', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('field_type', models.CharField(default=b'text', max_length=100, verbose_name='Field Type', choices=[(b'text', 'Text'), (b'textarea', 'Text Area'), (b'email', 'Email'), (b'number', 'Number'), (b'phone', 'Phone'), (b'url', 'URL'), (b'checkbox', 'Checkbox'), (b'checkbox_multiple', 'Multi Checkbox'), (b'select', 'Drop down'), (b'radio', 'Radio'), (b'file', 'File Upload'), (b'date', 'Date'), (b'time', 'Time'), (b'password', 'Password'), (b'hidden', 'Hidden')])), + ('label', models.CharField(max_length=255, verbose_name='name')), + ('placeholder_text', models.CharField(max_length=100, verbose_name='Placeholder Text', blank=True)), + ('required', models.BooleanField(default=True, verbose_name='Required')), + ('help_text', models.TextField(help_text='A description / instructions for this field.', verbose_name='Description', blank=True)), + ('initial', models.CharField(max_length=255, verbose_name='Default Value', blank=True)), + ('choice_values', models.TextField(help_text='Enter options one per line. For "File Upload" field type, enter allowed filetype (e.g .pdf) one per line.', verbose_name='Choices', blank=True)), + ('position', models.PositiveIntegerField(null=True, verbose_name='Position', blank=True)), + ('form', models.ForeignKey(related_name='fields', to='djangocms_forms.FormDefinition')), + ], + options={ + 'ordering': ('position',), + 'verbose_name': 'field', + 'verbose_name_plural': 'fields', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='FormSubmission', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('creation_date', models.DateTimeField(auto_now=True, verbose_name='Date')), + ('ip', models.GenericIPAddressField(null=True, verbose_name=b'IP', blank=True)), + ('form_data', jsonfield.fields.JSONField(verbose_name='Form Data')), + ('created_by', models.ForeignKey(editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='User')), + ('plugin', models.ForeignKey(related_name='submissions', editable=False, to='djangocms_forms.Form', verbose_name='Form')), + ], + options={ + 'ordering': ('-creation_date',), + 'verbose_name': 'Form Submission', + 'verbose_name_plural': 'Form Submissions', + }, + bases=(models.Model,), + ), + ] diff --git a/djangocms_forms/migrations/0002_alter_model_options.py b/djangocms_forms/migrations/0002_alter_model_options.py new file mode 100644 index 0000000..87be7d0 --- /dev/null +++ b/djangocms_forms/migrations/0002_alter_model_options.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_forms', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='form', + options={'verbose_name': 'form', 'verbose_name_plural': 'forms'}, + ), + migrations.AlterModelOptions( + name='formdefinition', + options={'verbose_name': 'form', 'verbose_name_plural': 'forms'}, + ), + migrations.AlterModelOptions( + name='formsubmission', + options={'ordering': ('-creation_date',), 'verbose_name': 'form submission', 'verbose_name_plural': 'form submissions', 'permissions': (('export_formsubmission', 'Can export Form Submission'),)}, + ), + ] diff --git a/djangocms_forms/migrations/0003_add_referrer_field.py b/djangocms_forms/migrations/0003_add_referrer_field.py new file mode 100644 index 0000000..70aae12 --- /dev/null +++ b/djangocms_forms/migrations/0003_add_referrer_field.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_forms', '0002_alter_model_options'), + ] + + operations = [ + migrations.AddField( + model_name='formsubmission', + name='referrer', + field=models.CharField(max_length=150, verbose_name='Referrer URL', blank=True), + ), + ] diff --git a/djangocms_forms/migrations/__init__.py b/djangocms_forms/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djangocms_forms/models.py b/djangocms_forms/models.py new file mode 100644 index 0000000..d722c52 --- /dev/null +++ b/djangocms_forms/models.py @@ -0,0 +1,176 @@ +import re + +from cms.models import CMSPlugin +from cms.models.fields import PageField +from django.db import models +from django.template.defaultfilters import slugify +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ +from jsonfield import JSONField +from unidecode import unidecode + +from .conf import settings +from .fields import PluginReferenceField +from .managers import ActiveFormManager + + +@python_2_unicode_compatible +class Form(models.Model): + name = models.CharField(_('Name'), max_length=255, db_index=True, editable=False) + + objects = models.Manager() + active_objects = ActiveFormManager() + + class Meta: + verbose_name = _('form') + verbose_name_plural = _('forms') + + def __str__(self): + return self.name + + +@python_2_unicode_compatible +class FormDefinition(CMSPlugin): + name = models.CharField(_('Form Name'), max_length=255) + + title = models.CharField(_('Title'), max_length=150, blank=True) + description = models.TextField(_('Description'), blank=True) + submit_btn_txt = models.CharField( + _('Submit Button Text'), max_length=100, default=_('Submit'), + help_text=_('Text for the Submit Button. The default is \'Submit\'')) + + post_submit_msg = models.TextField( + _('Post Submit Message'), blank=True, default=_('Thank You'), + help_text=_('Display this message to users after they submit your form.')) + + # 'HTTP redirect after successful submission' + success_redirect = models.BooleanField( + _('Redirect?'), default=False, + help_text=_('HTTP redirect after successful submission')) + page_redirect = PageField( + verbose_name=_('Page URL'), blank=True, null=True, + on_delete=models.SET_NULL, + help_text=_('A page has priority over an external URL')) + external_redirect = models.URLField( + _('External URL'), blank=True, + help_text=_('e.g. http://example.com/thank-you')) + + # Email + email_to = models.CharField( + _('Send form data to e-mail address'), max_length=255, blank=True, + help_text=_('Separate several addresses with a comma.')) + email_from = models.EmailField(_('Sender Email Address'), max_length=255, blank=True) + email_subject = models.CharField(_('Email Subject'), max_length=255, blank=True) + email_uploaded_files = models.BooleanField( + _('Send uploaded files as email attachments'), default=True) + + # Save to database + save_data = models.BooleanField( + _('Save to database'), default=True, + help_text=_('Logs all form submissions to the database.')) + spam_protection = models.SmallIntegerField( + _('Spam Protection'), + choices=settings.DJANGOCMS_FORMS_SPAM_PROTECTIONS, + default=settings.DJANGOCMS_FORMS_DEFAULT_SPAM_PROTECTION) + + form_template = models.CharField( + _('Form Template'), max_length=150, blank=True, + choices=settings.DJANGOCMS_FORMS_TEMPLATES, + default=settings.DJANGOCMS_FORMS_DEFAULT_TEMPLATE, + ) + + plugin_reference = PluginReferenceField(Form, related_name='plugin') + + class Meta: + verbose_name_plural = _('forms') + verbose_name = _('form') + + def __str__(self): + return self.name + + @property + def redirect_url(self): + if self.page_redirect: + return self.page_redirect.get_absolute_url() + elif self.external_redirect: + return self.external_redirect + + @property + def upload_to(self): + return '%s-%s' % ( + slugify(unidecode(self.name)).replace('_', '-'), + self.plugin_reference_id) + + def copy_relations(self, oldinstance): + for field in oldinstance.fields.all(): + field.pk = None + field.form = self + field.save() + + +@python_2_unicode_compatible +class FormField(models.Model): + form = models.ForeignKey(FormDefinition, related_name='fields') + field_type = models.CharField( + _('Field Type'), max_length=100, + choices=settings.DJANGOCMS_FORMS_FIELD_TYPES, + default=settings.DJANGOCMS_FORMS_DEFAULT_FIELD_TYPE) + label = models.CharField(_('name'), max_length=255) + placeholder_text = models.CharField(_('Placeholder Text'), blank=True, max_length=100) + required = models.BooleanField(_('Required'), default=True) + help_text = models.TextField( + _('Description'), blank=True, + help_text=_('A description / instructions for this field.')) + initial = models.CharField(_('Default Value'), max_length=255, blank=True) + choice_values = models.TextField( + _('Choices'), blank=True, + help_text=_('Enter options one per line. For "File Upload" ' + 'field type, enter allowed filetype (e.g .pdf) one per line.')) + position = models.PositiveIntegerField(_('Position'), blank=True, null=True) + + class Meta: + verbose_name_plural = _('fields') + verbose_name = _('field') + ordering = ('position', ) + + def __str__(self): + return self.label + + def field_attrs(self): + args = { + 'required': self.required, + 'label': self.label if self.label else '', + 'initial': self.initial if self.initial else None, + 'help_text': self.help_text, + } + return args + + def get_choices(self): + if self.choice_values: + regex = re.compile('[\s]*\n[\s]*') + choices = regex.split(self.choice_values) + return [(str(choice.encode('utf-8')), str(choice.encode('utf-8'))) for choice in choices] + + +@python_2_unicode_compatible +class FormSubmission(models.Model): + plugin = models.ForeignKey( + Form, verbose_name=_('Form'), editable=False, related_name='submissions') + creation_date = models.DateTimeField(_('Date'), auto_now=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, verbose_name=_('User'), editable=False, null=True) + ip = models.GenericIPAddressField(verbose_name='IP', blank=True, null=True) + referrer = models.CharField(_('Referrer URL'), max_length=150, blank=True) + + form_data = JSONField(_('Form Data')) + + class Meta: + verbose_name_plural = _('form submissions') + verbose_name = _('form submission') + ordering = ('-creation_date', ) + permissions = ( + ('export_formsubmission', 'Can export Form Submission'), + ) + + def __str__(self): + return u'%s' % self.plugin diff --git a/djangocms_forms/settings.py b/djangocms_forms/settings.py new file mode 100644 index 0000000..0652cb7 --- /dev/null +++ b/djangocms_forms/settings.py @@ -0,0 +1,35 @@ +from django.utils.translation import ugettext_lazy as _ + +TEXT = 'text' +TEXTAREA = 'textarea' +EMAIL = 'email' +CHECKBOX = 'checkbox' +CHECKBOX_MULTIPLE = 'checkbox_multiple' +SELECT = 'select' +RADIO = 'radio' +FILE = 'file' +DATE = 'date' +TIME = 'time' +HIDDEN = 'hidden' +NUMBER = 'number' +URL = 'url' +PASSWORD = 'password' +PHONE = 'phone' + +FIELD_TYPES = ( + (TEXT, _('Text')), + (TEXTAREA, _('Text Area')), + (EMAIL, _('Email')), + (NUMBER, _('Number')), + (PHONE, _('Phone')), + (URL, _('URL')), + (CHECKBOX, _('Checkbox')), + (CHECKBOX_MULTIPLE, _('Multi Checkbox')), + (SELECT, _('Drop down')), + (RADIO, _('Radio')), + (FILE, _('File Upload')), + (DATE, _('Date')), + (TIME, _('Time')), + (PASSWORD, _('Password')), + (HIDDEN, _('Hidden')), +) diff --git a/djangocms_forms/signals.py b/djangocms_forms/signals.py new file mode 100644 index 0000000..f6b4113 --- /dev/null +++ b/djangocms_forms/signals.py @@ -0,0 +1,5 @@ +from __future__ import unicode_literals + +from django.dispatch import Signal + +form_submission = Signal(providing_args=['form', 'cleaned_data']) diff --git a/djangocms_forms/south_migrations/0001_initial.py b/djangocms_forms/south_migrations/0001_initial.py new file mode 100644 index 0000000..a40a568 --- /dev/null +++ b/djangocms_forms/south_migrations/0001_initial.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Form' + db.create_table(u'djangocms_forms_form', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + )) + db.send_create_signal(u'djangocms_forms', ['Form']) + + # Adding model 'FormDefinition' + db.create_table(u'djangocms_forms_formdefinition', ( + (u'cmsplugin_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['cms.CMSPlugin'], unique=True, primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=150, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(blank=True)), + ('submit_btn_txt', self.gf('django.db.models.fields.CharField')(default=u'Submit', max_length=100)), + ('post_submit_msg', self.gf('django.db.models.fields.TextField')(default=u'Thank You', blank=True)), + ('success_redirect', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('page_redirect', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['cms.Page'], null=True, on_delete=models.SET_NULL, blank=True)), + ('external_redirect', self.gf('django.db.models.fields.URLField')(max_length=200, blank=True)), + ('email_to', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('email_from', self.gf('django.db.models.fields.EmailField')(max_length=255, blank=True)), + ('email_subject', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('email_uploaded_files', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('save_data', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('spam_protection', self.gf('django.db.models.fields.SmallIntegerField')(default=0)), + ('form_template', self.gf('django.db.models.fields.CharField')(default='djangocms_forms/form_template/default.html', max_length=150, blank=True)), + ('plugin_reference', self.gf('django.db.models.fields.related.ForeignKey')(related_name='plugin', null=True, on_delete=models.SET_NULL, to=orm['djangocms_forms.Form'])), + )) + db.send_create_signal(u'djangocms_forms', ['FormDefinition']) + + # Adding model 'FormField' + db.create_table(u'djangocms_forms_formfield', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('form', self.gf('django.db.models.fields.related.ForeignKey')(related_name='fields', to=orm['djangocms_forms.FormDefinition'])), + ('field_type', self.gf('django.db.models.fields.CharField')(default='text', max_length=100)), + ('label', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('placeholder_text', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)), + ('required', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('help_text', self.gf('django.db.models.fields.TextField')(blank=True)), + ('initial', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('choice_values', self.gf('django.db.models.fields.TextField')(blank=True)), + ('position', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)), + )) + db.send_create_signal(u'djangocms_forms', ['FormField']) + + # Adding model 'FormSubmission' + db.create_table(u'djangocms_forms_formsubmission', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('plugin', self.gf('django.db.models.fields.related.ForeignKey')(related_name='submissions', to=orm['djangocms_forms.Form'])), + ('creation_date', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('created_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)), + ('ip', self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39, null=True, blank=True)), + ('form_data', self.gf('jsonfield.fields.JSONField')()), + )) + db.send_create_signal(u'djangocms_forms', ['FormSubmission']) + + + def backwards(self, orm): + # Deleting model 'Form' + db.delete_table(u'djangocms_forms_form') + + # Deleting model 'FormDefinition' + db.delete_table(u'djangocms_forms_formdefinition') + + # Deleting model 'FormField' + db.delete_table(u'djangocms_forms_formfield') + + # Deleting model 'FormSubmission' + db.delete_table(u'djangocms_forms_formsubmission') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'cms.cmsplugin': { + 'Meta': {'object_name': 'CMSPlugin'}, + 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}), + 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}), + 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + }, + 'cms.page': { + 'Meta': {'ordering': "('tree_id', 'lft')", 'unique_together': "(('publisher_is_draft', 'application_namespace'), ('reverse_id', 'site', 'publisher_is_draft'))", 'object_name': 'Page'}, + 'application_namespace': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'application_urls': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}), + 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'in_navigation': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'is_home': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'languages': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'limit_visibility_in_menu': ('django.db.models.fields.SmallIntegerField', [], {'default': 'None', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'login_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'navigation_extenders': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['cms.Page']"}), + 'placeholders': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['cms.Placeholder']", 'symmetrical': 'False'}), + 'publication_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'publication_end_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'publisher_is_draft': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'publisher_public': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'publisher_draft'", 'unique': 'True', 'null': 'True', 'to': "orm['cms.Page']"}), + 'reverse_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'revision_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'djangocms_pages'", 'to': u"orm['sites.Site']"}), + 'soft_root': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'template': ('django.db.models.fields.CharField', [], {'default': "'INHERIT'", 'max_length': '100'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'xframe_options': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'cms.placeholder': { + 'Meta': {'object_name': 'Placeholder'}, + 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'djangocms_forms.form': { + 'Meta': {'object_name': 'Form'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + u'djangocms_forms.formdefinition': { + 'Meta': {'object_name': 'FormDefinition', '_ormbases': ['cms.CMSPlugin']}, + u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email_from': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}), + 'email_subject': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'email_to': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'email_uploaded_files': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'external_redirect': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'form_template': ('django.db.models.fields.CharField', [], {'default': "'djangocms_forms/form_template/default.html'", 'max_length': '150', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'page_redirect': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Page']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'plugin_reference': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'plugin'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['djangocms_forms.Form']"}), + 'post_submit_msg': ('django.db.models.fields.TextField', [], {'default': "u'Thank You'", 'blank': 'True'}), + 'save_data': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'spam_protection': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'submit_btn_txt': ('django.db.models.fields.CharField', [], {'default': "u'Submit'", 'max_length': '100'}), + 'success_redirect': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '150', 'blank': 'True'}) + }, + u'djangocms_forms.formfield': { + 'Meta': {'ordering': "('position',)", 'object_name': 'FormField'}, + 'choice_values': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'field_type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '100'}), + 'form': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fields'", 'to': u"orm['djangocms_forms.FormDefinition']"}), + 'help_text': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'initial': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'placeholder_text': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'position': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'required': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'djangocms_forms.formsubmission': { + 'Meta': {'ordering': "('-creation_date',)", 'object_name': 'FormSubmission'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'form_data': ('jsonfield.fields.JSONField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}), + 'plugin': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'submissions'", 'to': u"orm['djangocms_forms.Form']"}) + }, + u'sites.site': { + 'Meta': {'ordering': "(u'domain',)", 'object_name': 'Site', 'db_table': "u'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['djangocms_forms'] \ No newline at end of file diff --git a/djangocms_forms/south_migrations/0002_auto__add_field_formsubmission_referrer.py b/djangocms_forms/south_migrations/0002_auto__add_field_formsubmission_referrer.py new file mode 100644 index 0000000..e719b82 --- /dev/null +++ b/djangocms_forms/south_migrations/0002_auto__add_field_formsubmission_referrer.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'FormSubmission.referrer' + db.add_column(u'djangocms_forms_formsubmission', 'referrer', + self.gf('django.db.models.fields.CharField')(default='', max_length=150, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'FormSubmission.referrer' + db.delete_column(u'djangocms_forms_formsubmission', 'referrer') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'cms.cmsplugin': { + 'Meta': {'object_name': 'CMSPlugin'}, + 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.CMSPlugin']", 'null': 'True', 'blank': 'True'}), + 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}), + 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + }, + 'cms.page': { + 'Meta': {'ordering': "('tree_id', 'lft')", 'unique_together': "(('publisher_is_draft', 'application_namespace'), ('reverse_id', 'site', 'publisher_is_draft'))", 'object_name': 'Page'}, + 'application_namespace': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'application_urls': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}), + 'changed_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.CharField', [], {'max_length': '70'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'in_navigation': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'is_home': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'languages': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'limit_visibility_in_menu': ('django.db.models.fields.SmallIntegerField', [], {'default': 'None', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'login_required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'navigation_extenders': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '80', 'null': 'True', 'blank': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['cms.Page']"}), + 'placeholders': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['cms.Placeholder']", 'symmetrical': 'False'}), + 'publication_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'publication_end_date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'publisher_is_draft': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'publisher_public': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'publisher_draft'", 'unique': 'True', 'null': 'True', 'to': "orm['cms.Page']"}), + 'reverse_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'revision_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'site': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'djangocms_pages'", 'to': u"orm['sites.Site']"}), + 'soft_root': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'template': ('django.db.models.fields.CharField', [], {'default': "'INHERIT'", 'max_length': '100'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'xframe_options': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'cms.placeholder': { + 'Meta': {'object_name': 'Placeholder'}, + 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'djangocms_forms.form': { + 'Meta': {'object_name': 'Form'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + u'djangocms_forms.formdefinition': { + 'Meta': {'object_name': 'FormDefinition', '_ormbases': ['cms.CMSPlugin']}, + u'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'unique': 'True', 'primary_key': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'email_from': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}), + 'email_subject': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'email_to': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'email_uploaded_files': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'external_redirect': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'form_template': ('django.db.models.fields.CharField', [], {'default': "'djangocms_forms/form_template/default.html'", 'max_length': '150', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'page_redirect': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Page']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'plugin_reference': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'plugin'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['djangocms_forms.Form']"}), + 'post_submit_msg': ('django.db.models.fields.TextField', [], {'default': "u'Thank You'", 'blank': 'True'}), + 'save_data': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'spam_protection': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'submit_btn_txt': ('django.db.models.fields.CharField', [], {'default': "u'Submit'", 'max_length': '100'}), + 'success_redirect': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '150', 'blank': 'True'}) + }, + u'djangocms_forms.formfield': { + 'Meta': {'ordering': "('position',)", 'object_name': 'FormField'}, + 'choice_values': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'field_type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '100'}), + 'form': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'fields'", 'to': u"orm['djangocms_forms.FormDefinition']"}), + 'help_text': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'initial': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'placeholder_text': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'position': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'required': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'djangocms_forms.formsubmission': { + 'Meta': {'ordering': "('-creation_date',)", 'object_name': 'FormSubmission'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'form_data': ('jsonfield.fields.JSONField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}), + 'plugin': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'submissions'", 'to': u"orm['djangocms_forms.Form']"}), + 'referrer': ('django.db.models.fields.CharField', [], {'max_length': '150', 'blank': 'True'}) + }, + u'sites.site': { + 'Meta': {'ordering': "(u'domain',)", 'object_name': 'Site', 'db_table': "u'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['djangocms_forms'] \ No newline at end of file diff --git a/djangocms_forms/south_migrations/__init__.py b/djangocms_forms/south_migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djangocms_forms/static/css/djangocms_forms/admin/djangocms_forms.css b/djangocms_forms/static/css/djangocms_forms/admin/djangocms_forms.css new file mode 100644 index 0000000..1079552 --- /dev/null +++ b/djangocms_forms/static/css/djangocms_forms/admin/djangocms_forms.css @@ -0,0 +1,43 @@ +.inline-related { + border: 1px solid #CCC; + border-radius: 2px; + box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.08); +} + +.inline-related h3 { + background-color: #FCFCFC; + padding: 10px 20px 10px 3px; + margin: 0; + text-align: center; +} + +.inline-related .inline_label { + font-weight: 300; +} + + +.inline-related .collapse-expand { + display: inline-block; + text-transform: uppercase; + padding: 0px 10px 0px 10px; + width: 44px; + float: left; + text-decoration:none!important; +} + +.add-row { + height: 26px; + line-height: 26px; + text-align: center; + border: 1px dashed #CCC!important; + border-radius: 2px; + margin: 0; +} + +.add-row a { + display: block; + padding-left: 14px; + text-indent: -9999px; + font-size: 11px; + background-position: 50% 50%!important; +} diff --git a/djangocms_forms/static/js/djangocms_forms/admin/jquery-form-export.js b/djangocms_forms/static/js/djangocms_forms/admin/jquery-form-export.js new file mode 100644 index 0000000..70fadc7 --- /dev/null +++ b/djangocms_forms/static/js/djangocms_forms/admin/jquery-form-export.js @@ -0,0 +1,53 @@ +(function($) { + $(function() { + var formsubmission_form = $('#formsubmission_form'); + var form_field = $('select[id="id_form"]', formsubmission_form); + var headers_field = $('.field-headers', formsubmission_form); + + var loadHeaders = function() { + if (!form_field.val()) { + headers_field.slideUp('fast'); + return; + } + + var data = { + 'csrfmiddlewaretoken': $('input[name="csrfmiddlewaretoken"]').val(), + 'form': $('select[name="form"]').val(), + 'file_type': $('select[name="file_type"]').val() + }; + + $.ajax({ + type: 'POST', + url: '', + data: data, + success: function(response) { + if (response.reloadBrowser) { + location.reload(); + } + $('select[id="id_headers_to"]').find('option').remove(); + + var headers_input_from = $('select[id="id_headers_from"]'); + headers_input_from.find('option').remove(); + + $.each(response.availableHeaders, function(index, value) { + headers_input_from.append(''); + }); + + SelectBox.init('id_headers_from'); + SelectBox.init('id_headers_to'); + headers_field.slideDown(); + }, + error: function() { + alert('We\'re sorry. Something unexpected happened. Please reload the page and try again.'); + } + }); + + return false; + }; + + if ($('body').hasClass('export-form')) { + loadHeaders(); + form_field.change(loadHeaders); + } + }); +})(django.jQuery); diff --git a/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-collapsible.js b/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-collapsible.js new file mode 100644 index 0000000..46ad126 --- /dev/null +++ b/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-collapsible.js @@ -0,0 +1,66 @@ +/* +Makes all inline forms collapsible. +*/ + +jQuery(function($) { + $.makeCollapsible = function(target, item, collapsible, triggerTarget, setInitStatus, setFirstStatus) + { + var triggerExpand = 'Edit'; + var triggerCollapse = 'Done'; + var triggerClass = 'collapse-expand'; + var triggerLink = ''; + + $(target).find(item).each(function(i) { + if ($(this).data('isCollapsible')) return; + $(this).data('isCollapsible', true); + + $(this).find(collapsible).hide(); + + // trigger already exists if created with "Add another" link + var trigger = $(this).find(triggerTarget).find('.'+triggerClass); + if (!trigger.length) { + trigger = $(triggerLink); + $(this).find(triggerTarget).html(trigger); + } + + var item = this; + var toggleCollapse = function(status, speed) + { + if (status == null) { + status = !item.collapseStatus; + } + if (speed == null) { + speed = 1; + } + item.collapseStatus = status; + if (status) { + trigger.html(triggerCollapse); + $(item).find(collapsible).slideDown('slow'); + } else { + trigger.html(triggerExpand); + $(item).find(collapsible).slideUp('slow'); + } + } + + trigger.click(function(event) { + event.preventDefault(); + toggleCollapse(null, 'normal') + }) + + // Collapse by default unless there are errors + initStatus = setInitStatus != null ? setInitStatus : $(this).find('.errors').length != 0; + firstStatus = setFirstStatus != null ? setFirstStatus : initStatus; + + toggleCollapse(i == 0 ? firstStatus : initStatus) + }); + }; + + var init = function() { + $.makeCollapsible('div.inline-group', 'div.inline-related', 'fieldset', 'h3 b'); + }; + init(); + // init again when "Add another" link is clicked + $('.add-row a').click(function() { + init(); + }) +}); diff --git a/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-positioning.js b/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-positioning.js new file mode 100644 index 0000000..ce72862 --- /dev/null +++ b/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-positioning.js @@ -0,0 +1,95 @@ +/* +Enables repositioning of all inline elements by drag & drop. + +The inline model requires is a "position" field that is blank by default. +This value will be set automatically by this code snippet when dragging elements. +The model instances can then be ordered by that "position" field. +*/ + +jQuery(function($) { + + var positionField = 'position'; + var target = $('div.inline-group#fields-group'); + var handle = 'h3'; + var item = 'div.inline-related'; + var positionInput = 'input[id$=-'+positionField+']'; + var hidePositionFieldClosest = '.form-row'; + + var renumberAll = function() { + var pos = 1; + target.find(item).each(function(i) { + $(this).removeClass('odd even').addClass(i % 2 ? 'even' : 'odd'); + if ($(this).find(positionInput).val() != '') { + $(this).find(positionInput).val(pos); + pos++; + } + }); + }; + + var init = function() { + target.find(item).each(function(i) { + if ($(this).data('isSortable')) return; + $(this).data('isSortable', true); + $(this).find(handle).css('cursor', 'ns-resize'); + $(this).find(handle).addClass('draggable'); + $(this).find(positionInput).each(function() { + if (hidePositionFieldClosest) { + var hidden =$(''); + hidden.val($(this).val()); + $(this).closest(hidePositionFieldClosest).replaceWith(hidden); + } + }); + $(this).find('input, select, textarea').change(function() { + $(this).closest(item).find('input[id$='+positionField+']').val('X'); // mark for renumberAll() to fill in + renumberAll($('div.inline-group')); + }); + }); + }; + + var addRow = target.find('.add-row'); + addRow.remove(); + var ordered = []; + var unordered = []; + // Initially, remove and re-append all inlines ordered by their "position" value + target.find(item).each(function(i) { + var initialPos = $(this).find(positionInput).val(); + if (initialPos) { + while (initialPos < ordered.length && ordered[initialPos]) { + initialPos++; + } + ordered[initialPos] = this; + } else { + unordered[unordered.length] = this; + } + $(this).removeClass('odd even').addClass(i % 2 ? 'even' : 'odd'); + this.parentElement.removeChild(this); + }); + for (var i = 0; i < ordered.length; i++) { + var el = ordered[i]; + if (el) { + target.append(el); + } + } + // Add "position"-less elements in the end + for (var i = 0; i < unordered.length; i++) { + var el = unordered[i]; + target.append(el); + } + target.append(addRow); + + target.sortable({ + containment: 'parent', + items: item, + handle: handle, + update: renumberAll, + revert: true, + opacity: 0.9 + }); + + init(); + // init again when "Add another" link is clicked + $('.add-row a').click(function() { + init(); + }); + +}); diff --git a/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-rename.js b/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-rename.js new file mode 100644 index 0000000..d1236c4 --- /dev/null +++ b/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-rename.js @@ -0,0 +1,45 @@ +/* +Replaces the name in an inline element's header while typing it in the input of the "name" field. +This way, the extra inline element's header will be named instead of numbered #4, #5 etc. +*/ + +jQuery(function($) { + + var target = $('div.inline-group'); + var item = 'div.inline-related'; + var nameInput = 'input[id*=-label]'; + var typeInput = 'select[id*=-field_type]'; + + + var init = function() { + target.find(item).each(function() { + var nameField = $(this).find(nameInput); + var typeField = $(this).find(typeInput); + var label = $('.inline_label', this); + var rename = function() { + if (nameField.val()) { + label.text(nameField.val()); + } + else { + label.text(typeField.find('option:selected').text() + ' Field'); + } + }; + nameField.keyup(function(event) { + // Update name while typing + rename(); + }); + + typeField.change(function(){ + rename(); + }); + rename(); + }) + } + + init(); + + // init again when "Add another" link is clicked + $('.add-row a').click(function() { + init(); + }) +}); diff --git a/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-toggle-fields.js b/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-toggle-fields.js new file mode 100644 index 0000000..50a63b2 --- /dev/null +++ b/djangocms_forms/static/js/djangocms_forms/admin/jquery-inline-toggle-fields.js @@ -0,0 +1,35 @@ +jQuery(function($) { + var target = $('div.inline-group'); + var item = 'div.inline-related'; + var typeInput = 'select[id*=-field_type]'; + + var init = function() { + target.find(item).each(function() { + var that = $(this); + var inputField = that.find(typeInput); + var toggleField = function() { + var selectedFieldType = inputField.val(); + var choiceFields = ['checkbox_multiple', 'select', 'radio', 'file']; + var showChoiceField = $.inArray(selectedFieldType, choiceFields) >= 0; + that.find('.field-choice_values') + .toggle(showChoiceField) + .toggleClass('required', showChoiceField); + + that.find('.field-placeholder_text, .field-help_text, .field-choice_values:not(:hidden)') + .toggle(selectedFieldType != 'hidden') + + }; + inputField.change(function() { + toggleField(); + }); + toggleField(); + }) + }; + + init(); + + // init again when "Add another" link is clicked + $('.add-row a').click(function() { + init(); + }) +}); diff --git a/djangocms_forms/static/js/djangocms_forms/djangocms_forms.js b/djangocms_forms/static/js/djangocms_forms/djangocms_forms.js new file mode 100644 index 0000000..134158b --- /dev/null +++ b/djangocms_forms/static/js/djangocms_forms/djangocms_forms.js @@ -0,0 +1,100 @@ +(function ($) { + $.fn.djangocms_forms = function (options) { + if (options === undefined) { options = {}; } + + var defaults = { + form_wrapper: '.form-wrapper', + field_wrapper: '.field-wrapper', + form_errors: '.form-errors', + field_errors: '.field-errors', + form_success: '.form-success', + errorlist_css_class: 'errorlist', + error_css_class: 'error', + server_error: 'We\'re sorry. Something Unexpected Happened. Please Try Again Later.' + }; + + this.each(function(options) { + var options = $.extend( {}, defaults, options) ; + + var el = $(this), + form_wrapper = $(options.form_wrapper, el), + form_success = $(options.form_success, el), + form = $('form', form_wrapper); + + + function clearErrors() { + form.find(options.form_errors).fadeOut().empty(); //clear form errors + form.find(options.field_errors).fadeOut().empty(); //clear field errors + form.find(options.field_wrapper).removeClass(options.error_css_class); //remove error classes + } + + // post-submit callback + function ajaxSuccess(response) { + if (response.status == 'success'){ + formValid(response.redirect_url); + } + else if (response.status == 'error'){ + formInvalid(response.form_errors); + } + } + + function formValid(success_url) { + form_success.fadeIn('slow'); + form_wrapper.slideUp('slow').remove(); + + if (success_url){ + setTimeout(function(){ + window.location = success_url; + }, 1000); + } + + } + + function formInvalid(form_errors) { + clearErrors() + $.each(form_errors, function(key, value) { + var field = form.find(':input[name=' + key + ']').first(); + var field_wrapper = field.parents(options.field_wrapper).addClass(options.error_css_class); + var field_error = $('