From 98b2c09ef1f1c8288a31517f61423264c6b3291c Mon Sep 17 00:00:00 2001 From: Jan Szejko Date: Wed, 20 Sep 2017 15:00:40 +0200 Subject: [PATCH] add contact app + konkurs contact form --- requirements/requirements.txt | 3 + src/chunks/templatetags/__init__.py | 0 src/chunks/templatetags/chunks.py | 36 ++++ src/contact/__init__.py | 53 ++++++ src/contact/admin.py | 177 ++++++++++++++++++ src/contact/fields.py | 13 ++ src/contact/forms.py | 111 +++++++++++ src/contact/locale/pl/LC_MESSAGES/django.po | 92 +++++++++ src/contact/migrations/0001_initial.py | 43 +++++ src/contact/migrations/__init__.py | 0 src/contact/models.py | 50 +++++ .../admin/contact/contact/change_list.html | 18 ++ .../contact/disabled_contact_form.html | 13 ++ src/contact/templates/contact/form.html | 28 +++ src/contact/templates/contact/mail_body.txt | 6 + .../templates/contact/mail_managers_body.txt | 12 ++ .../contact/mail_managers_subject.txt | 1 + .../templates/contact/mail_subject.txt | 1 + src/contact/templates/contact/thanks.html | 14 ++ src/contact/templatetags/__init__.py | 0 src/contact/templatetags/contact_tags.py | 10 + src/contact/urls.py | 11 ++ src/contact/views.py | 83 ++++++++ src/contact/widgets.py | 13 ++ src/wolnelektury/contact_forms.py | 68 +++++++ src/wolnelektury/settings/__init__.py | 1 + src/wolnelektury/settings/custom.py | 2 + src/wolnelektury/static/scss/main/form.scss | 4 +- src/wolnelektury/templates/main_page.html | 1 + src/wolnelektury/urls.py | 1 + src/wolnelektury/utils.py | 34 ++++ 31 files changed, 897 insertions(+), 2 deletions(-) create mode 100644 src/chunks/templatetags/__init__.py create mode 100644 src/chunks/templatetags/chunks.py create mode 100644 src/contact/__init__.py create mode 100644 src/contact/admin.py create mode 100644 src/contact/fields.py create mode 100644 src/contact/forms.py create mode 100644 src/contact/locale/pl/LC_MESSAGES/django.po create mode 100644 src/contact/migrations/0001_initial.py create mode 100644 src/contact/migrations/__init__.py create mode 100644 src/contact/models.py create mode 100644 src/contact/templates/admin/contact/contact/change_list.html create mode 100644 src/contact/templates/contact/disabled_contact_form.html create mode 100644 src/contact/templates/contact/form.html create mode 100644 src/contact/templates/contact/mail_body.txt create mode 100644 src/contact/templates/contact/mail_managers_body.txt create mode 100644 src/contact/templates/contact/mail_managers_subject.txt create mode 100644 src/contact/templates/contact/mail_subject.txt create mode 100644 src/contact/templates/contact/thanks.html create mode 100755 src/contact/templatetags/__init__.py create mode 100755 src/contact/templatetags/contact_tags.py create mode 100644 src/contact/urls.py create mode 100644 src/contact/views.py create mode 100644 src/contact/widgets.py create mode 100644 src/wolnelektury/contact_forms.py diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 447c3757c..72169806a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -14,6 +14,9 @@ django-modeltranslation>=0.10,<0.11 django-allauth>=0.24,<0.25 django-extensions +# contact +pyyaml + polib django-babel diff --git a/src/chunks/templatetags/__init__.py b/src/chunks/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/chunks/templatetags/chunks.py b/src/chunks/templatetags/chunks.py new file mode 100644 index 000000000..968d284fd --- /dev/null +++ b/src/chunks/templatetags/chunks.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from django import template +from django.core.cache import cache +from ..models import Chunk, Attachment + + +register = template.Library() + + +@register.simple_tag +def chunk(key, cache_time=0): + try: + cache_key = 'chunk_' + key + c = cache.get(cache_key) + if c is None: + c = Chunk.objects.get(key=key) + cache.set(cache_key, c, int(cache_time)) + content = c.content + except Chunk.DoesNotExist: + n = Chunk(key=key) + n.save() + return '' + return content + + +@register.simple_tag +def attachment(key, cache_time=0): + try: + cache_key = 'attachment_' + key + c = cache.get(cache_key) + if c is None: + c = Attachment.objects.get(key=key) + cache.set(cache_key, c, int(cache_time)) + return c.attachment.url + except Attachment.DoesNotExist: + return '' diff --git a/src/contact/__init__.py b/src/contact/__init__.py new file mode 100644 index 000000000..3bce2e392 --- /dev/null +++ b/src/contact/__init__.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Generic app for creating contact forms. + +0. Add 'contact' to your INSTALLED_APPS and include 'contact.urls' somewhere +in your urls.py, like: + url(r'^contact/', + include('contact.urls')) + +1. Migrate. + +2. Create somewhere in your project a module with some subclasses of +contact.forms.ContactForm, specyfing form_tag and some fields in each. + +3. Set CONTACT_FORMS_MODULE in your settings to point to the module. + +4. Link to the form with {% url 'contact_form' form_tag %}. + +5. Optionally override some templates in form-specific template directories +(/contact//...). + +6. Receive submitted forms by email and read them in admin. + + +Example: +======== + +settings.py: + CONTACT_FORMS_MODULE = 'myproject.contact_forms' + +myproject/contact_forms.py: + from django import forms + from contact.forms import ContactForm + from django.utils.translation import ugettext_lazy as _ + + class RegistrationForm(ContactForm): + form_tag = 'register' + name = forms.CharField(label=_('Name'), max_length=128) + presentation = forms.FileField(label=_('Presentation')) + +some_template.html: + {% url 'contact:form' 'register' %} + +""" + +from fnpdjango.utils.app import AppSettings + + +class Settings(AppSettings): + FORMS_MODULE = "contact_forms" + + +app_settings = Settings('CONTACT') diff --git a/src/contact/admin.py b/src/contact/admin.py new file mode 100644 index 000000000..af14c6b02 --- /dev/null +++ b/src/contact/admin.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +import csv +import json + +from django.contrib import admin +from django.utils.translation import ugettext as _ +from django.utils.safestring import mark_safe +from django.conf.urls import patterns, url +from django.http import HttpResponse, Http404 + +from wolnelektury.utils import UnicodeCSVWriter +from .forms import contact_forms, admin_list_width +from .models import Contact + + +class ContactAdminMeta(admin.ModelAdmin.__class__): + def __getattr__(cls, name): + if name.startswith('admin_list_'): + return lambda self: "" + raise AttributeError(name) + + +class ContactAdmin(admin.ModelAdmin): + __metaclass__ = ContactAdminMeta + date_hierarchy = 'created_at' + list_display = ['created_at', 'contact', 'form_tag'] + \ + ["admin_list_%d" % i for i in range(admin_list_width)] + fields = ['form_tag', 'created_at', 'contact', 'ip'] + readonly_fields = ['form_tag', 'created_at', 'contact', 'ip'] + list_filter = ['form_tag'] + + @staticmethod + def admin_list(obj, nr): + try: + field_name = contact_forms[obj.form_tag].admin_list[nr] + except BaseException: + return '' + else: + return Contact.pretty_print(obj.body.get(field_name, ''), for_html=True) + + def __getattr__(self, name): + if name.startswith('admin_list_'): + nr = int(name[len('admin_list_'):]) + return lambda obj: self.admin_list(obj, nr) + raise AttributeError(name) + + def change_view(self, request, object_id, form_url='', extra_context=None): + if object_id: + try: + instance = Contact.objects.get(pk=object_id) + assert isinstance(instance.body, dict) + except (Contact.DoesNotExist, AssertionError): + pass + else: + # Create readonly fields from the body JSON. + attachments = list(instance.attachment_set.all()) + body_keys = instance.body.keys() + [a.tag for a in attachments] + + # Find the original form. + try: + orig_fields = contact_forms[instance.form_tag]().fields + except KeyError: + orig_fields = {} + + # Try to preserve the original order. + orig_keys = list(orig_fields.keys()) + admin_keys = [key for key in orig_keys if key in body_keys] + \ + [key for key in body_keys if key not in orig_keys] + admin_fields = ['body__%s' % key for key in admin_keys] + + self.readonly_fields.extend(admin_fields) + + self.fieldsets = [ + (None, {'fields': self.fields}), + (_('Body'), {'fields': admin_fields}), + ] + + # Create field getters for fields and attachments. + def attach_getter(key, value): + def f(self): + return value + f.short_description = orig_fields[key].label if key in orig_fields else _(key) + setattr(self, "body__%s" % key, f) + + for k, v in instance.body.items(): + attach_getter(k, Contact.pretty_print(v, for_html=True)) + + download_link = "%(url)s" + for attachment in attachments: + link = mark_safe(download_link % { + 'url': attachment.get_absolute_url()}) + attach_getter(attachment.tag, link) + return super(ContactAdmin, self).change_view( + request, object_id, form_url=form_url, extra_context=extra_context) + + def changelist_view(self, request, extra_context=None): + context = dict() + if 'form_tag' in request.GET: + form = contact_forms.get(request.GET['form_tag']) + context['extract_types'] = [ + {'slug': 'all', 'label': _('all')}, + {'slug': 'contacts', 'label': _('contacts')}] + context['extract_types'] += [type for type in getattr(form, 'extract_types', [])] + return super(ContactAdmin, self).changelist_view(request, extra_context=context) + + def get_urls(self): + # urls = super(ContactAdmin, self).get_urls() + return patterns( + '', + url(r'^extract/(?P[\w-]+)/(?P[\w-]+)/$', + self.admin_site.admin_view(extract_view), name='contact_extract') + ) + super(ContactAdmin, self).get_urls() + + +def extract_view(request, form_tag, extract_type_slug): + contacts_by_spec = dict() + form = contact_forms.get(form_tag) + if form is None and extract_type_slug not in ('contacts', 'all'): + raise Http404 + + q = Contact.objects.filter(form_tag=form_tag) + at_year = request.GET.get('created_at__year') + at_month = request.GET.get('created_at__month') + if at_year: + q = q.filter(created_at__year=at_year) + if at_month: + q = q.filter(created_at__month=at_month) + + # Segregate contacts by body key sets + if form: + orig_keys = list(form().fields.keys()) + else: + orig_keys = [] + for contact in q.all(): + if extract_type_slug == 'contacts': + keys = ['contact'] + elif extract_type_slug == 'all': + keys = contact.body.keys() + ['contact'] + keys = [key for key in orig_keys if key in keys] + [key for key in keys if key not in orig_keys] + else: + keys = form.get_extract_fields(contact, extract_type_slug) + contacts_by_spec.setdefault(tuple(keys), []).append(contact) + + response = HttpResponse(content_type='text/csv') + csv_writer = UnicodeCSVWriter(response) + + # Generate list for each body key set + for keys, contacts in contacts_by_spec.items(): + csv_writer.writerow(keys) + for contact in contacts: + if extract_type_slug == 'contacts': + records = [dict(contact=contact.contact)] + elif extract_type_slug == 'all': + records = [dict(contact=contact.contact, **contact.body)] + else: + records = form.get_extract_records(keys, contact, extract_type_slug) + + for record in records: + for key in keys: + if key not in record: + record[key] = '' + if isinstance(record[key], basestring): + pass + elif isinstance(record[key], bool): + record[key] = 'tak' if record[key] else 'nie' + elif isinstance(record[key], (list, tuple)) and all(isinstance(v, basestring) for v in record[key]): + record[key] = ', '.join(record[key]) + else: + record[key] = json.dumps(record[key]) + + csv_writer.writerow([record[key] for key in keys]) + csv_writer.writerow([]) + + response['Content-Disposition'] = 'attachment; filename="kontakt.csv"' + return response + +admin.site.register(Contact, ContactAdmin) diff --git a/src/contact/fields.py b/src/contact/fields.py new file mode 100644 index 000000000..c2c97b307 --- /dev/null +++ b/src/contact/fields.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django import forms +from .widgets import HeaderWidget + + +class HeaderField(forms.CharField): + def __init__(self, required=False, widget=None, *args, **kwargs): + if widget is None: + widget = HeaderWidget + super(HeaderField, self).__init__(required=required, widget=widget, *args, **kwargs) diff --git a/src/contact/forms.py b/src/contact/forms.py new file mode 100644 index 000000000..3cdf59dee --- /dev/null +++ b/src/contact/forms.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +from django.contrib.sites.models import Site +from django.core.exceptions import ValidationError +from django.core.files.uploadedfile import UploadedFile +from django.core.mail import send_mail, mail_managers +from django.core.urlresolvers import reverse +from django.core.validators import validate_email +from django import forms +from django.template.loader import render_to_string +from django.template import RequestContext +from django.utils.translation import ugettext_lazy as _ + + +contact_forms = {} +admin_list_width = 0 + + +class ContactFormMeta(forms.Form.__class__): + def __new__(cls, name, bases, attrs): + global admin_list_width + model = super(ContactFormMeta, cls).__new__(cls, name, bases, attrs) + assert model.form_tag not in contact_forms, 'Duplicate form_tag.' + if model.admin_list: + admin_list_width = max(admin_list_width, len(model.admin_list)) + contact_forms[model.form_tag] = model + return model + + +class ContactForm(forms.Form): + """Subclass and define some fields.""" + __metaclass__ = ContactFormMeta + + form_tag = None + form_title = _('Contact form') + submit_label = _('Submit') + admin_list = None + result_page = False + + required_css_class = 'required' + # a subclass has to implement this field, but doing it here breaks the order + contact = NotImplemented + + def save(self, request, formsets=None): + from .models import Attachment, Contact + body = {} + for name, value in self.cleaned_data.items(): + if not isinstance(value, UploadedFile) and name != 'contact': + body[name] = value + + for formset in formsets or []: + for f in formset.forms: + sub_body = {} + for name, value in f.cleaned_data.items(): + if not isinstance(value, UploadedFile): + sub_body[name] = value + if sub_body: + body.setdefault(f.form_tag, []).append(sub_body) + + contact = Contact.objects.create( + body=body, + ip=request.META['REMOTE_ADDR'], + contact=self.cleaned_data['contact'], + form_tag=self.form_tag) + for name, value in self.cleaned_data.items(): + if isinstance(value, UploadedFile): + attachment = Attachment(contact=contact, tag=name) + attachment.file.save(value.name, value) + attachment.save() + + site = Site.objects.get_current() + dictionary = { + 'form_tag': self.form_tag, + 'site_name': getattr(self, 'site_name', site.name), + 'site_domain': getattr(self, 'site_domain', site.domain), + 'contact': contact, + } + context = RequestContext(request) + mail_managers_subject = render_to_string([ + 'contact/%s/mail_managers_subject.txt' % self.form_tag, + 'contact/mail_managers_subject.txt', + ], dictionary, context).strip() + mail_managers_body = render_to_string([ + 'contact/%s/mail_managers_body.txt' % self.form_tag, + 'contact/mail_managers_body.txt', + ], dictionary, context) + mail_managers(mail_managers_subject, mail_managers_body, fail_silently=True) + + try: + validate_email(contact.contact) + except ValidationError: + pass + else: + mail_subject = render_to_string([ + 'contact/%s/mail_subject.txt' % self.form_tag, + 'contact/mail_subject.txt', + ], dictionary, context).strip() + if self.result_page: + mail_body = render_to_string( + 'contact/%s/results_email.txt' % contact.form_tag, + { + 'contact': contact, + 'results': self.results(contact), + }, context) + else: + mail_body = render_to_string([ + 'contact/%s/mail_body.txt' % self.form_tag, + 'contact/mail_body.txt', + ], dictionary, context) + send_mail(mail_subject, mail_body, 'no-reply@%s' % site.domain, [contact.contact], fail_silently=True) + + return contact diff --git a/src/contact/locale/pl/LC_MESSAGES/django.po b/src/contact/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 000000000..cabbe00d9 --- /dev/null +++ b/src/contact/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,92 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-11-05 10:16+0100\n" +"PO-Revision-Date: 2012-10-10 13:12+0100\n" +"Last-Translator: Radek Czajka \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2)\n" + +#: admin.py:77 +msgid "Body" +msgstr "Treść" + +#: admin.py:101 +msgid "all" +msgstr "wszystko" + +#: admin.py:101 +msgid "contacts" +msgstr "kontakty" + +#: forms.py:29 +msgid "Contact form" +msgstr "Formularz kontaktowy" + +#: forms.py:30 +msgid "Submit" +msgstr "Wyślij" + +#: models.py:11 +msgid "submission date" +msgstr "data wysłania" + +#: models.py:12 +msgid "IP address" +msgstr "adres IP" + +#: models.py:13 +msgid "contact" +msgstr "kontakt" + +#: models.py:14 +msgid "form" +msgstr "formularz" + +#: models.py:15 +msgid "body" +msgstr "treść" + +#: models.py:29 +msgid "submitted form" +msgstr "przysłany formularz" + +#: models.py:30 +msgid "submitted forms" +msgstr "przysłane formularze" + +#: templates/admin/contact/contact/change_list.html:12 +msgid "Extract" +msgstr "Ekstrakt" + +#: templates/contact/mail_body.txt:2 templates/contact/mail_subject.txt:1 +#, python-format +msgid "Thank you for contacting us at %(site_name)s." +msgstr "Dziękujemy za skontaktowanie się z nami na stronie %(site_name)s." + +#: templates/contact/mail_body.txt:3 +msgid "Your submission has been referred to the project coordinator." +msgstr "Twoje zgłoszenie zostało przekazane osobie koordynującej projekt." + +#: templates/contact/mail_body.txt:6 +msgid "Message sent automatically. Please do not reply to it." +msgstr "Wiadomość wysłana automatycznie, prosimy nie odpowiadać." + +#: templates/contact/thanks.html:4 templates/contact/thanks.html.py:8 +msgid "Thank you" +msgstr "Dziękujemy" + +#: templates/contact/thanks.html:11 +msgid "Thank you for submitting the form." +msgstr "Dziękujemy za wypełnienie formularza." diff --git a/src/contact/migrations/0001_initial.py b/src/contact/migrations/0001_initial.py new file mode 100644 index 000000000..e824a3c7a --- /dev/null +++ b/src/contact/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Attachment', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('tag', models.CharField(max_length=64)), + ('file', models.FileField(upload_to=b'contact/attachment')), + ], + ), + migrations.CreateModel( + name='Contact', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='submission date')), + ('ip', models.GenericIPAddressField(verbose_name='IP address')), + ('contact', models.EmailField(max_length=128, verbose_name='contact')), + ('form_tag', models.CharField(max_length=32, verbose_name='form', db_index=True)), + ('body', jsonfield.fields.JSONField(verbose_name='body')), + ], + options={ + 'ordering': ('-created_at',), + 'verbose_name': 'submitted form', + 'verbose_name_plural': 'submitted forms', + }, + ), + migrations.AddField( + model_name='attachment', + name='contact', + field=models.ForeignKey(to='contact.Contact'), + ), + ] diff --git a/src/contact/migrations/__init__.py b/src/contact/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/contact/models.py b/src/contact/models.py new file mode 100644 index 000000000..adffafa38 --- /dev/null +++ b/src/contact/models.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +import yaml +from hashlib import sha1 +from django.db import models +from django.utils.encoding import smart_unicode +from django.utils.translation import ugettext_lazy as _ +from jsonfield import JSONField +from . import app_settings + + +class Contact(models.Model): + created_at = models.DateTimeField(_('submission date'), auto_now_add=True) + ip = models.GenericIPAddressField(_('IP address')) + contact = models.EmailField(_('contact'), max_length=128) + form_tag = models.CharField(_('form'), max_length=32, db_index=True) + body = JSONField(_('body')) + + @staticmethod + def pretty_print(value, for_html=False): + if type(value) in (tuple, list, dict): + value = yaml.safe_dump(value, allow_unicode=True, default_flow_style=False) + if for_html: + value = smart_unicode(value).replace(u" ", unichr(160)) + return value + + class Meta: + ordering = ('-created_at',) + verbose_name = _('submitted form') + verbose_name_plural = _('submitted forms') + + def __unicode__(self): + return unicode(self.created_at) + + def digest(self): + serialized_body = ';'.join(sorted('%s:%s' % item for item in self.body.iteritems())) + data = '%s%s%s%s%s' % (self.id, self.contact, serialized_body, self.ip, self.form_tag) + return sha1(data).hexdigest() + + +class Attachment(models.Model): + contact = models.ForeignKey(Contact) + tag = models.CharField(max_length=64) + file = models.FileField(upload_to='contact/attachment') + + @models.permalink + def get_absolute_url(self): + return 'contact_attachment', [self.contact_id, self.tag] + + +__import__(app_settings.FORMS_MODULE) diff --git a/src/contact/templates/admin/contact/contact/change_list.html b/src/contact/templates/admin/contact/contact/change_list.html new file mode 100644 index 000000000..3b47c97db --- /dev/null +++ b/src/contact/templates/admin/contact/contact/change_list.html @@ -0,0 +1,18 @@ +{% extends "admin/change_list.html" %} +{% load i18n %} +{% load admin_urls %} + + +{% block object-tools-items %} + {{block.super}} + {% if request.GET.form_tag %} + {% for extract_type in extract_types %} +
  • + + {% trans 'Extract' %}: {{extract_type.label}} + +
  • + {% endfor %} + {% endif %} +{% endblock %} + diff --git a/src/contact/templates/contact/disabled_contact_form.html b/src/contact/templates/contact/disabled_contact_form.html new file mode 100644 index 000000000..5fbf85e37 --- /dev/null +++ b/src/contact/templates/contact/disabled_contact_form.html @@ -0,0 +1,13 @@ +{% extends "base/base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block body %} + +

    {{ title }}

    + + {% block contact_form_description %} +

    Rejestracja została zamknięta.

    + {% endblock %} + +{% endblock %} diff --git a/src/contact/templates/contact/form.html b/src/contact/templates/contact/form.html new file mode 100644 index 000000000..346de58ff --- /dev/null +++ b/src/contact/templates/contact/form.html @@ -0,0 +1,28 @@ +{% extends form.base_template|default:"base/base.html" %} +{% load chunks %} +{% load honeypot %} + +{% block title %}{{ form.form_title }}{% endblock %} + +{% block body %} + +

    {% block contact_form_title %}{{ form.form_title }}{% endblock %}

    + +
    + {% block contact_form_description %} + {% chunk "contact_form__"|add:form.form_tag %} + {% endblock %} +
    + +
    + {% csrf_token %} + {% render_honeypot_field %} + {% block form %} + + {{ form.as_table }} + +
    + {% endblock %} +
    + +{% endblock %} diff --git a/src/contact/templates/contact/mail_body.txt b/src/contact/templates/contact/mail_body.txt new file mode 100644 index 000000000..50157572f --- /dev/null +++ b/src/contact/templates/contact/mail_body.txt @@ -0,0 +1,6 @@ +{% load i18n %} +{% blocktrans %}Thank you for contacting us at {{ site_name }}.{% endblocktrans %} +{% trans "Your submission has been referred to the project coordinator." %} + +-- +{% trans "Message sent automatically. Please do not reply to it." %} diff --git a/src/contact/templates/contact/mail_managers_body.txt b/src/contact/templates/contact/mail_managers_body.txt new file mode 100644 index 000000000..b7f97cf25 --- /dev/null +++ b/src/contact/templates/contact/mail_managers_body.txt @@ -0,0 +1,12 @@ +{% load pretty_print from contact_tags %}{% load subdomainurls %}Wypełniono formularz {{ form_tag }} na stronie {{ site_name }}. + +{% url 'admin:contact_contact_change' None contact.pk %} + +{% for k, v in contact.body.items %} +{{ k }}: +{{ v|pretty_print }} +{% endfor %} +{% for attachment in contact.attachment_set.all %} +{{ attachment.tag }}: +http://{{ site_domain }}{{ attachment.get_absolute_url }} +{% endfor %} diff --git a/src/contact/templates/contact/mail_managers_subject.txt b/src/contact/templates/contact/mail_managers_subject.txt new file mode 100644 index 000000000..12d2c8ec9 --- /dev/null +++ b/src/contact/templates/contact/mail_managers_subject.txt @@ -0,0 +1 @@ +Wypełniono formularz {{ form_tag }} na stronie {{ site_name }}. \ No newline at end of file diff --git a/src/contact/templates/contact/mail_subject.txt b/src/contact/templates/contact/mail_subject.txt new file mode 100644 index 000000000..b8f586e3b --- /dev/null +++ b/src/contact/templates/contact/mail_subject.txt @@ -0,0 +1 @@ +{% load i18n %}{% blocktrans %}Thank you for contacting us at {{ site_name }}.{% endblocktrans %} \ No newline at end of file diff --git a/src/contact/templates/contact/thanks.html b/src/contact/templates/contact/thanks.html new file mode 100644 index 000000000..00dc0fe3f --- /dev/null +++ b/src/contact/templates/contact/thanks.html @@ -0,0 +1,14 @@ +{% extends base_template|default:"base.html" %} +{% load i18n %} + +{% block title %}{% trans "Thank you" %}{% endblock %} + +{% block body %} + +

    {% block contact_form_title %}{% trans "Thank you" %}{% endblock %}

    + + {% block contact_form_description %} +

    {% trans "Thank you for submitting the form." %}

    + {% endblock %} + +{% endblock %} diff --git a/src/contact/templatetags/__init__.py b/src/contact/templatetags/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/src/contact/templatetags/contact_tags.py b/src/contact/templatetags/contact_tags.py new file mode 100755 index 000000000..aadba16e1 --- /dev/null +++ b/src/contact/templatetags/contact_tags.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from django.template import Library +from contact.models import Contact + +register = Library() + + +@register.filter +def pretty_print(value): + return Contact.pretty_print(value) diff --git a/src/contact/urls.py b/src/contact/urls.py new file mode 100644 index 000000000..f2ef9444e --- /dev/null +++ b/src/contact/urls.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from django.conf.urls import patterns, url +from . import views + +urlpatterns = patterns( + 'contact.views', + url(r'^(?P[^/]+)/$', views.form, name='contact_form'), + url(r'^(?P[^/]+)/thanks/$', views.thanks, name='contact_thanks'), + url(r'^attachment/(?P\d+)/(?P[^/]+)/$', views.attachment, name='contact_attachment'), + url(r'^results/(?P\d+)/(?P[0-9a-f]+)/', views.results, name='contact_results'), +) diff --git a/src/contact/views.py b/src/contact/views.py new file mode 100644 index 000000000..82e0347ca --- /dev/null +++ b/src/contact/views.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +from urllib import unquote + +from django.contrib.auth.decorators import permission_required +from django.http import Http404 +from django.shortcuts import get_object_or_404, redirect, render +from fnpdjango.utils.views import serve_file +from honeypot.decorators import check_honeypot + +from .forms import contact_forms +from .models import Attachment, Contact + + +@check_honeypot +def form(request, form_tag, force_enabled=False): + try: + form_class = contact_forms[form_tag] + except KeyError: + raise Http404 + if (getattr(form_class, 'disabled', False) and + not (force_enabled and request.user.is_superuser)): + template = getattr(form_class, 'disabled_template', None) + if template: + return render(request, template, {'title': form_class.form_title}) + raise Http404 + if request.method == 'POST': + form = form_class(request.POST, request.FILES) + else: + form = form_class(initial=request.GET) + formset_classes = getattr(form, 'form_formsets', {}) + if request.method == 'POST': + formsets = { + prefix: formset_class(request.POST, request.FILES, prefix=prefix) + for prefix, formset_class in formset_classes.iteritems()} + if form.is_valid() and all(formset.is_valid() for formset in formsets.itervalues()): + contact = form.save(request, formsets.values()) + if form.result_page: + return redirect('contact_results', contact.id, contact.digest()) + else: + return redirect('contact_thanks', form_tag) + else: + formsets = {prefix: formset_class(prefix=prefix) for prefix, formset_class in formset_classes.iteritems()} + + return render( + request, ['contact/%s/form.html' % form_tag, 'contact/form.html'], + {'form': form, 'formsets': formsets} + ) + + +def thanks(request, form_tag): + try: + form_class = contact_forms[form_tag] + except KeyError: + raise Http404 + + return render( + request, ['contact/%s/thanks.html' % form_tag, 'contact/thanks.html'], + {'base_template': getattr(form_class, 'base_template', None)}) + + +def results(request, contact_id, digest): + contact = get_object_or_404(Contact, id=contact_id) + if digest != contact.digest(): + raise Http404 + try: + form_class = contact_forms[contact.form_tag] + except KeyError: + raise Http404 + + return render( + request, 'contact/%s/results.html' % contact.form_tag, + { + 'results': form_class.results(contact), + 'base_template': getattr(form_class, 'base_template', None), + } + ) + + +@permission_required('contact.change_attachment') +def attachment(request, contact_id, tag): + attachment = get_object_or_404(Attachment, contact_id=contact_id, tag=tag) + attachment_url = unquote(attachment.file.url) + return serve_file(attachment_url) diff --git a/src/contact/widgets.py b/src/contact/widgets.py new file mode 100644 index 000000000..785e019fd --- /dev/null +++ b/src/contact/widgets.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django import forms +from django.forms.util import flatatt +from django.utils.html import format_html + + +class HeaderWidget(forms.widgets.Widget): + def render(self, name, value, attrs=None): + attrs.update(self.attrs) + return format_html('', flatatt(attrs)) diff --git a/src/wolnelektury/contact_forms.py b/src/wolnelektury/contact_forms.py new file mode 100644 index 000000000..6ca78d619 --- /dev/null +++ b/src/wolnelektury/contact_forms.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.utils.functional import lazy +from django.utils.safestring import mark_safe + +from contact.forms import ContactForm +from contact.fields import HeaderField +from django import forms + +mark_safe_lazy = lazy(mark_safe, unicode) + + +class KonkursForm(ContactForm): + form_tag = 'konkurs' + form_title = u"Konkurs Trzy strony" + admin_list = ['podpis', 'contact', 'temat'] + + opiekun_header = HeaderField(label=u'Dane\xa0Opiekuna/Opiekunki') + opiekun_nazwisko = forms.CharField(label=u'Imię i nazwisko', max_length=128) + contact = forms.EmailField(label=u'Adres e-mail', max_length=128) + opiekun_tel = forms.CharField(label=u'Numer telefonu', max_length=32) + nazwa_dkk = forms.CharField(label=u'Nazwa DKK', max_length=128) + adres_dkk = forms.CharField(label=u'Adres DKK', max_length=128) + + uczestnik_header = HeaderField(label=u'Dane\xa0Uczestnika/Uczestniczki') + uczestnik_imie = forms.CharField(label=u'Imię', max_length=128) + uczestnik_nazwisko = forms.CharField(label=u'Nazwisko', max_length=128) + uczestnik_email = forms.EmailField(label=u'Adres e-mail', max_length=128) + wiek = forms.ChoiceField(label=u'Kategoria wiekowa', choices=( + ('0-11', 'do 11 lat'), + ('12-15', '12–15 lat'), + ('16-19', '16–19 lat'), + )) + tytul = forms.CharField(label=u'Tytuł opowiadania', max_length=255) + plik = forms.FileField( + label=u'Plik z opowiadaniem', + help_text=u'Prosimy o nazwanie pliku imieniem i nazwiskiem autora.') + + agree_header = HeaderField(label=u'Oświadczenia') + agree_terms = forms.BooleanField( + label='Regulamin', + help_text=mark_safe_lazy( + u'Znam i akceptuję ' + u'Regulamin Konkursu.'), + ) + agree_data = forms.BooleanField( + label='Przetwarzanie danych osobowych', + help_text=u'Oświadczam, że wyrażam zgodę na przetwarzanie danych osobowych zawartych w niniejszym formularzu ' + u'zgłoszeniowym przez Fundację Nowoczesna Polska (administratora danych) z siedzibą w Warszawie (00-514) ' + u'przy ul. Marszałkowskiej 84/92 lok. 125 na potrzeby organizacji Konkursu. Jednocześnie oświadczam, ' + u'że zostałam/em poinformowana/y o tym, że mam prawo wglądu w treść swoich danych i możliwość ich ' + u'poprawiania oraz że ich podanie jest dobrowolne, ale niezbędne do dokonania zgłoszenia.') + agree_license = forms.BooleanField( + label='Licencja', + help_text=mark_safe_lazy( + u'Wyrażam zgodę oraz potwierdzam, że autor/ka (lub ich przedstawiciele ustawowi – gdy dotyczy) ' + u'wyrazili zgodę na korzystanie z opowiadania zgodnie z postanowieniami wolnej licencji ' + u'Creative Commons Uznanie autorstwa – ' + u'Na tych samych warunkach 3.0. Licencja pozwala każdemu na swobodne, nieodpłatne korzystanie z utworu ' + u'w oryginale oraz w postaci opracowań do wszelkich celów wymagając poszanowania autorstwa i innych praw ' + u'osobistych oraz tego, aby ewentualne opracowania utworu były także udostępniane na tej samej licencji.')) + agree_wizerunek = forms.BooleanField( + label='Rozpowszechnianie wizerunku', + help_text=u'Wyrażam zgodę oraz potwierdzam, że autor/ka opowiadania (lub ich przedstawiciele ustawowi – ' + u'gdy dotyczy) wyrazili zgodę na fotografowanie i nagrywanie podczas gali wręczenia nagród i następnie ' + u'rozpowszechnianie ich wizerunków.') diff --git a/src/wolnelektury/settings/__init__.py b/src/wolnelektury/settings/__init__.py index a9079bf0e..ac366513f 100644 --- a/src/wolnelektury/settings/__init__.py +++ b/src/wolnelektury/settings/__init__.py @@ -58,6 +58,7 @@ INSTALLED_APPS_OUR = [ 'polls', 'libraries', 'newsletter', + 'contact', ] GETPAID_BACKENDS = ( diff --git a/src/wolnelektury/settings/custom.py b/src/wolnelektury/settings/custom.py index acea28b9f..08ad482b3 100644 --- a/src/wolnelektury/settings/custom.py +++ b/src/wolnelektury/settings/custom.py @@ -28,3 +28,5 @@ CATALOGUE_COUNTERS_FILE = os.path.join(VAR_DIR, 'catalogue_counters.p') CATALOGUE_MIN_INITIALS = 60 PICTURE_PAGE_SIZE = 20 + +CONTACT_FORMS_MODULE = 'wolnelektury.contact_forms' diff --git a/src/wolnelektury/static/scss/main/form.scss b/src/wolnelektury/static/scss/main/form.scss index 42e867595..3695d2881 100755 --- a/src/wolnelektury/static/scss/main/form.scss +++ b/src/wolnelektury/static/scss/main/form.scss @@ -8,8 +8,8 @@ form table { padding-bottom: 1em; } - .required th:after { - content: " *"; + .required th:before { + content: "* "; } .errorlist { diff --git a/src/wolnelektury/templates/main_page.html b/src/wolnelektury/templates/main_page.html index 7912c2421..1d59c1379 100755 --- a/src/wolnelektury/templates/main_page.html +++ b/src/wolnelektury/templates/main_page.html @@ -139,6 +139,7 @@