add contact app + konkurs contact form
authorJan Szejko <janek37@gmail.com>
Wed, 20 Sep 2017 13:00:40 +0000 (15:00 +0200)
committerJan Szejko <janek37@gmail.com>
Wed, 20 Sep 2017 14:01:31 +0000 (16:01 +0200)
31 files changed:
requirements/requirements.txt
src/chunks/templatetags/__init__.py [new file with mode: 0644]
src/chunks/templatetags/chunks.py [new file with mode: 0644]
src/contact/__init__.py [new file with mode: 0644]
src/contact/admin.py [new file with mode: 0644]
src/contact/fields.py [new file with mode: 0644]
src/contact/forms.py [new file with mode: 0644]
src/contact/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
src/contact/migrations/0001_initial.py [new file with mode: 0644]
src/contact/migrations/__init__.py [new file with mode: 0644]
src/contact/models.py [new file with mode: 0644]
src/contact/templates/admin/contact/contact/change_list.html [new file with mode: 0644]
src/contact/templates/contact/disabled_contact_form.html [new file with mode: 0644]
src/contact/templates/contact/form.html [new file with mode: 0644]
src/contact/templates/contact/mail_body.txt [new file with mode: 0644]
src/contact/templates/contact/mail_managers_body.txt [new file with mode: 0644]
src/contact/templates/contact/mail_managers_subject.txt [new file with mode: 0644]
src/contact/templates/contact/mail_subject.txt [new file with mode: 0644]
src/contact/templates/contact/thanks.html [new file with mode: 0644]
src/contact/templatetags/__init__.py [new file with mode: 0755]
src/contact/templatetags/contact_tags.py [new file with mode: 0755]
src/contact/urls.py [new file with mode: 0644]
src/contact/views.py [new file with mode: 0644]
src/contact/widgets.py [new file with mode: 0644]
src/wolnelektury/contact_forms.py [new file with mode: 0644]
src/wolnelektury/settings/__init__.py
src/wolnelektury/settings/custom.py
src/wolnelektury/static/scss/main/form.scss
src/wolnelektury/templates/main_page.html
src/wolnelektury/urls.py
src/wolnelektury/utils.py

index 447c375..7216980 100644 (file)
@@ -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 (file)
index 0000000..e69de29
diff --git a/src/chunks/templatetags/chunks.py b/src/chunks/templatetags/chunks.py
new file mode 100644 (file)
index 0000000..968d284
--- /dev/null
@@ -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 (file)
index 0000000..3bce2e3
--- /dev/null
@@ -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/<form_tag>/...).
+
+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 (file)
index 0000000..af14c6b
--- /dev/null
@@ -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 = "<a href='%(url)s'>%(url)s</a>"
+                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<form_tag>[\w-]+)/(?P<extract_type_slug>[\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 (file)
index 0000000..c2c97b3
--- /dev/null
@@ -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 (file)
index 0000000..3cdf59d
--- /dev/null
@@ -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 (file)
index 0000000..cabbe00
--- /dev/null
@@ -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 <EMAIL@ADDRESS>, 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 <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: LANGUAGE <LL@li.org>\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 (file)
index 0000000..e824a3c
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/src/contact/models.py b/src/contact/models.py
new file mode 100644 (file)
index 0000000..adffafa
--- /dev/null
@@ -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 (file)
index 0000000..3b47c97
--- /dev/null
@@ -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 %}
+            <li>
+                <a href="{% url 'admin:contact_extract' form_tag=request.GET.form_tag extract_type_slug=extract_type.slug %}?created_at__month={{request.GET.created_at__month}}&created_at__year={{request.GET.created_at__year}}">
+                    {% trans 'Extract' %}: {{extract_type.label}}
+                </a>
+            </li>
+        {% 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 (file)
index 0000000..5fbf85e
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends "base/base.html" %}
+
+{% block title %}{{ title }}{% endblock %}
+
+{% block body %}
+
+    <h1>{{ title }}</h1>
+
+    {% block contact_form_description %}
+        <p class="notice">Rejestracja została zamknięta.</p>
+    {% endblock %}
+
+{% endblock %}
diff --git a/src/contact/templates/contact/form.html b/src/contact/templates/contact/form.html
new file mode 100644 (file)
index 0000000..346de58
--- /dev/null
@@ -0,0 +1,28 @@
+{% extends form.base_template|default:"base/base.html" %}
+{% load chunks %}
+{% load honeypot %}
+
+{% block title %}{{ form.form_title }}{% endblock %}
+
+{% block body %}
+
+    <h1>{% block contact_form_title %}{{ form.form_title }}{% endblock %}</h1>
+
+    <div class="form-info">
+    {% block contact_form_description %}
+        {% chunk "contact_form__"|add:form.form_tag %}
+    {% endblock %}
+    </div>
+
+    <form method="POST" action="." enctype="multipart/form-data" class="submit-form">
+    {% csrf_token %}
+    {% render_honeypot_field %}
+    {% block form %}
+        <table>
+            {{ form.as_table }}
+            <tr><td></td><td><button>{% block contact_form_submit %}{{ form.submit_label }}{% endblock %}</button></td></tr>
+        </table>
+    {% endblock %}
+    </form>
+
+{% endblock %}
diff --git a/src/contact/templates/contact/mail_body.txt b/src/contact/templates/contact/mail_body.txt
new file mode 100644 (file)
index 0000000..5015757
--- /dev/null
@@ -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 (file)
index 0000000..b7f97cf
--- /dev/null
@@ -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 (file)
index 0000000..12d2c8e
--- /dev/null
@@ -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 (file)
index 0000000..b8f586e
--- /dev/null
@@ -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 (file)
index 0000000..00dc0fe
--- /dev/null
@@ -0,0 +1,14 @@
+{% extends base_template|default:"base.html" %}
+{% load i18n %}
+
+{% block title %}{% trans "Thank you" %}{% endblock %}
+
+{% block body %}
+
+    <h1>{% block contact_form_title %}{% trans "Thank you" %}{% endblock %}</h1>
+
+    {% block contact_form_description %}
+    <p class="notice">{% trans "Thank you for submitting the form." %}</p>
+    {% endblock %}
+
+{% endblock %}
diff --git a/src/contact/templatetags/__init__.py b/src/contact/templatetags/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/src/contact/templatetags/contact_tags.py b/src/contact/templatetags/contact_tags.py
new file mode 100755 (executable)
index 0000000..aadba16
--- /dev/null
@@ -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 (file)
index 0000000..f2ef944
--- /dev/null
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+from django.conf.urls import patterns, url
+from . import views
+
+urlpatterns = patterns(
+    'contact.views',
+    url(r'^(?P<form_tag>[^/]+)/$', views.form, name='contact_form'),
+    url(r'^(?P<form_tag>[^/]+)/thanks/$', views.thanks, name='contact_thanks'),
+    url(r'^attachment/(?P<contact_id>\d+)/(?P<tag>[^/]+)/$', views.attachment, name='contact_attachment'),
+    url(r'^results/(?P<contact_id>\d+)/(?P<digest>[0-9a-f]+)/', views.results, name='contact_results'),
+)
diff --git a/src/contact/views.py b/src/contact/views.py
new file mode 100644 (file)
index 0000000..82e0347
--- /dev/null
@@ -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 (file)
index 0000000..785e019
--- /dev/null
@@ -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('<a{0}></a>', flatatt(attrs))
diff --git a/src/wolnelektury/contact_forms.py b/src/wolnelektury/contact_forms.py
new file mode 100644 (file)
index 0000000..6ca78d6
--- /dev/null
@@ -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ę <a href="/media/chunks/attachment/Regulamin_konkursu_Trzy_strony.pdf">'
+            u'Regulamin Konkursu</a>.'),
+    )
+    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'<a href="https://creativecommons.org/licenses/by-sa/3.0/pl/">Creative Commons Uznanie autorstwa – '
+            u'Na tych samych warunkach 3.0</a>. 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.')
index a9079bf..ac36651 100644 (file)
@@ -58,6 +58,7 @@ INSTALLED_APPS_OUR = [
     'polls',
     'libraries',
     'newsletter',
+    'contact',
 ]
 
 GETPAID_BACKENDS = (
index acea28b..08ad482 100644 (file)
@@ -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'
index 42e8675..3695d28 100755 (executable)
@@ -8,8 +8,8 @@ form table {
         padding-bottom: 1em;
     }
 
-    .required th:after {
-        content: " *";
+    .required th:before {
+        content: "";
     }
 
     .errorlist {
index 7912c24..1d59c13 100755 (executable)
       <ul>
         <li><a href="https://nowoczesnapolska.org.pl/prywatnosc/">{% trans "Privacy policy" %}</a></li>
         {% infopages_on_main %}
+        <li><a href="{% url 'contact_form' 'konkurs' %}">{% trans "Konkurs Trzy strony" %}</a></li>
       </ul>
 
       <div class="social-links">
index 2a3f6c6..f8c84f8 100644 (file)
@@ -47,6 +47,7 @@ urlpatterns += [
     url(r'^chunks/', include('chunks.urls')),
     url(r'^sponsors/', include('sponsors.urls')),
     url(r'^newsletter/', include('newsletter.urls')),
+    url(r'^formularz/', include('contact.urls')),
 
     # Admin panel
     url(r'^admin/catalogue/book/import$', catalogue.views.import_book, name='import_book'),
index 72bc7d0..8c5ead6 100644 (file)
@@ -2,6 +2,9 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
+import codecs
+import csv
+import cStringIO
 import json
 import os
 from functools import wraps
@@ -115,3 +118,34 @@ def send_noreply_mail(subject, message, recipient_list, **kwargs):
         u'[WolneLektury] ' + subject,
         message + u"\n\n-- \n" + ugettext(u'Message sent automatically. Please do not reply.'),
         'no-reply@wolnelektury.pl', recipient_list, **kwargs)
+
+
+# source: https://docs.python.org/2/library/csv.html#examples
+class UnicodeCSVWriter(object):
+    """
+    A CSV writer which will write rows to CSV file "f",
+    which is encoded in the given encoding.
+    """
+
+    def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
+        # Redirect output to a queue
+        self.queue = cStringIO.StringIO()
+        self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
+        self.stream = f
+        self.encoder = codecs.getincrementalencoder(encoding)()
+
+    def writerow(self, row):
+        self.writer.writerow([s.encode("utf-8") for s in row])
+        # Fetch UTF-8 output from the queue ...
+        data = self.queue.getvalue()
+        data = data.decode("utf-8")
+        # ... and reencode it into the target encoding
+        data = self.encoder.encode(data)
+        # write to the target stream
+        self.stream.write(data)
+        # empty queue
+        self.queue.truncate(0)
+
+    def writerows(self, rows):
+        for row in rows:
+            self.writerow(row)