Merge branch 'api'
authorJan Szejko <janek37@gmail.com>
Thu, 14 Dec 2017 08:43:33 +0000 (09:43 +0100)
committerJan Szejko <janek37@gmail.com>
Thu, 14 Dec 2017 08:43:33 +0000 (09:43 +0100)
src/api/handlers.py
src/api/urls.py
src/catalogue/migrations/0016_auto_20171031_1232.py [new file with mode: 0644]
src/catalogue/models/book.py
src/catalogue/tasks.py
src/contact/views.py
src/stats/utils.py
src/wolnelektury/contact_forms.py
src/wolnelektury/static/scss/main/form.scss

index 827cd7c..eb56e20 100644 (file)
@@ -7,6 +7,7 @@ import json
 from django.contrib.sites.models import Site
 from django.core.urlresolvers import reverse
 from django.utils.functional import lazy
+from django.db import models
 from piston.handler import AnonymousBaseHandler, BaseHandler
 from piston.utils import rc
 from sorl.thumbnail import default
@@ -40,13 +41,25 @@ for k, v in category_singular.items():
 book_tag_categories = ['author', 'epoch', 'kind', 'genre']
 
 
-def read_tags(tags, allowed):
+def read_tags(tags, request, allowed):
     """ Reads a path of filtering tags.
 
     :param str tags: a path of category and slug pairs, like: authors/an-author/...
     :returns: list of Tag objects
     :raises: ValueError when tags can't be found
     """
+
+    def process(category, slug):
+        if category == 'book':
+            try:
+                books.append(Book.objects.get(slug=slug))
+            except Book.DoesNotExist:
+                raise ValueError('Unknown book.')
+        try:
+            real_tags.append(Tag.objects.get(category=category, slug=slug))
+        except Tag.DoesNotExist:
+            raise ValueError('Tag not found')
+
     if not tags:
         return [], []
 
@@ -64,17 +77,14 @@ def read_tags(tags, allowed):
 
         if category not in allowed:
             raise ValueError('Category not allowed.')
-
-        if category == 'book':
-            try:
-                books.append(Book.objects.get(slug=slug))
-            except Book.DoesNotExist:
-                raise ValueError('Unknown book.')
-
-        try:
-            real_tags.append(Tag.objects.get(category=category, slug=slug))
-        except Tag.DoesNotExist:
-            raise ValueError('Tag not found')
+        process(category, slug)
+
+    for key in request.GET:
+        if key in category_singular:
+            category = category_singular[key]
+            if category in allowed:
+                for slug in request.GET.getlist(key):
+                    process(category, slug)
     return real_tags, books
 
 
@@ -113,14 +123,12 @@ class BookDetails(object):
     @classmethod
     def url(cls, book):
         """ Returns Book's URL on the site. """
-
         return WL_BASE + book.get_absolute_url()
 
     @classmethod
     def children(cls, book):
         """ Returns all children for a book. """
-
-        return book.children.all()
+        return book.children.order_by('parent_number', 'sort_key')
 
     @classmethod
     def media(cls, book):
@@ -136,6 +144,11 @@ class BookDetails(object):
         return MEDIA_BASE + default.backend.get_thumbnail(
                     book.cover, "139x193").url if book.cover else ''
 
+    @classmethod
+    def cover_source_image(cls, book):
+        url = book.cover_source()
+        return url.rstrip('/') + '/file/'
+
 
 class BookDetailHandler(BaseHandler, BookDetails):
     """ Main handler for Book objects.
@@ -144,7 +157,7 @@ class BookDetailHandler(BaseHandler, BookDetails):
     """
     allowed_methods = ['GET']
     fields = ['title', 'parent', 'children'] + Book.formats + [
-        'media', 'url', 'cover', 'cover_thumb'] + [
+        'media', 'url', 'cover', 'cover_thumb', 'fragment_data'] + [
             category_plural[c] for c in book_tag_categories]
 
     @piwik_track
@@ -163,7 +176,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
     """
     allowed_methods = ('GET',)
     model = Book
-    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
+    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
 
     @classmethod
     def genres(cls, book):
@@ -171,7 +184,9 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
         return book.tags.filter(category='genre')
 
     @piwik_track
-    def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None):
+    def read(self, request, tags=None, top_level=False, audiobooks=False, daisy=False, pk=None,
+             recommended=False, newest=False, books=None,
+             after=None, before=None, count=None):
         """ Lists all books with given tags.
 
         :param tags: filtering tags; should be a path of categories
@@ -187,10 +202,17 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
                 return rc.NOT_FOUND
 
         try:
-            tags, _ancestors = read_tags(tags, allowed=book_tag_categories)
+            tags, _ancestors = read_tags(tags, request, allowed=book_tag_categories)
         except ValueError:
             return rc.NOT_FOUND
 
+        if 'after' in request.GET:
+            after = request.GET['after']
+        if 'before' in request.GET:
+            before = request.GET['before']
+        if 'count' in request.GET:
+            count = request.GET['count']
+
         if tags:
             if top_level:
                 books = Book.tagged_top_level(tags)
@@ -198,7 +220,8 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
             else:
                 books = Book.tagged.with_all(tags)
         else:
-            books = Book.objects.all()
+            books = books if books is not None else Book.objects.all()
+        books = books.order_by('slug')
 
         if top_level:
             books = books.filter(parent=None)
@@ -206,14 +229,27 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
             books = books.filter(media__type='mp3').distinct()
         if daisy:
             books = books.filter(media__type='daisy').distinct()
+        if recommended:
+            books = books.filter(recommended=True)
+        if newest:
+            books = books.order_by('-created_at')
+
+        if after:
+            books = books.filter(slug__gt=after)
+        if before:
+            books = books.filter(slug__lt=before)
 
         books = books.only('slug', 'title', 'cover', 'cover_thumb')
         for category in book_tag_categories:
             books = prefetch_relations(books, category)
-        if books:
-            return books
-        else:
-            return rc.NOT_FOUND
+
+        if count:
+            if before:
+                books = list(reversed(books.order_by('-slug')[:count]))
+            else:
+                books = books[:count]
+
+        return books
 
     def create(self, request, *args, **kwargs):
         return rc.FORBIDDEN
@@ -222,7 +258,7 @@ class AnonymousBooksHandler(AnonymousBaseHandler, BookDetails):
 class BooksHandler(BookDetailHandler):
     allowed_methods = ('GET', 'POST')
     model = Book
-    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb']
+    fields = book_tag_categories + ['href', 'title', 'url', 'cover', 'cover_thumb', 'slug']
     anonymous = AnonymousBooksHandler
 
     def create(self, request, *args, **kwargs):
@@ -239,7 +275,95 @@ class BooksHandler(BookDetailHandler):
 
 
 class EBooksHandler(AnonymousBooksHandler):
-    fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats)
+    fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',)
+
+
+class BookProxy(models.Model):
+    def __init__(self, book, key):
+        self.book = book
+        self.key = key
+
+    def __getattr__(self, item):
+        if item not in ('book', 'key'):
+            return self.book.__getattribute__(item)
+        else:
+            return self.__getattribute__(item)
+
+
+class QuerySetProxy(models.QuerySet):
+    def __init__(self, l):
+        self.list = l
+
+    def __iter__(self):
+        return iter(self.list)
+
+
+class FilterBooksHandler(AnonymousBooksHandler):
+    fields = book_tag_categories + [
+        'href', 'title', 'url', 'cover', 'cover_thumb', 'key', 'cover_source_image']
+
+    def read(self, request):
+        key_sep = '$'
+        search_string = request.GET.get('search')
+        is_lektura = request.GET.get('lektura')
+        is_audiobook = request.GET.get('audiobook')
+
+        after = request.GET.get('after')
+        count = int(request.GET.get('count', 50))
+        if is_lektura in ('true', 'false'):
+            is_lektura = is_lektura == 'true'
+        else:
+            is_lektura = None
+        if is_audiobook in ('true', 'false'):
+            is_audiobook = is_audiobook == 'true'
+        books = Book.objects.distinct().order_by('slug')
+        if is_lektura is not None:
+            books = books.filter(has_audience=is_lektura)
+        if is_audiobook is not None:
+            if is_audiobook:
+                books = books.filter(media__type='mp3')
+            else:
+                books = books.exclude(media__type='mp3')
+        for key in request.GET:
+            if key in category_singular:
+                category = category_singular[key]
+                if category in book_tag_categories:
+                    slugs = request.GET[key].split(',')
+                    tags = Tag.objects.filter(category=category, slug__in=slugs)
+                    books = Book.tagged.with_any(tags, books)
+        if (search_string is not None) and len(search_string) < 3:
+            search_string = None
+        if search_string:
+            books_author = books.filter(cached_author__iregex='\m' + search_string)
+            books_title = books.filter(title__iregex='\m' + search_string)
+            books_title = books_title.exclude(id__in=list(books_author.values_list('id', flat=True)))
+            if after and (key_sep in after):
+                which, slug = after.split(key_sep, 1)
+                if which == 'title':
+                    book_lists = [(books_title.filter(slug__gt=slug), 'title')]
+                else:  # which == 'author'
+                    book_lists = [(books_author.filter(slug__gt=slug), 'author'), (books_title, 'title')]
+            else:
+                book_lists = [(books_author, 'author'), (books_title, 'title')]
+        else:
+            if after and key_sep in after:
+                which, slug = after.split(key_sep, 1)
+                books = books.filter(slug__gt=slug)
+            book_lists = [(books, 'book')]
+
+        filtered_books = []
+        for book_list, label in book_lists:
+            book_list = book_list.only('slug', 'title', 'cover', 'cover_thumb')
+            for category in book_tag_categories:
+                book_list = prefetch_relations(book_list, category)
+            remaining_count = count - len(filtered_books)
+            new_books = [BookProxy(book, '%s%s%s' % (label, key_sep, book.slug))
+                         for book in book_list[:remaining_count]]
+            filtered_books += new_books
+            if len(filtered_books) == count:
+                break
+
+        return QuerySetProxy(filtered_books)
 
 
 # add categorized tags fields for Book
@@ -375,7 +499,7 @@ class TagsHandler(BaseHandler, TagDetails):
     """
     allowed_methods = ('GET',)
     model = Tag
-    fields = ['name', 'href', 'url']
+    fields = ['name', 'href', 'url', 'slug']
 
     @piwik_track
     def read(self, request, category=None, pk=None):
@@ -391,11 +515,24 @@ class TagsHandler(BaseHandler, TagDetails):
         except KeyError:
             return rc.NOT_FOUND
 
-        tags = Tag.objects.filter(category=category_sng).exclude(items=None)
-        if tags.exists():
-            return tags
-        else:
-            return rc.NOT_FOUND
+        after = request.GET.get('after')
+        before = request.GET.get('before')
+        count = request.GET.get('count')
+
+        tags = Tag.objects.filter(category=category_sng).exclude(items=None).order_by('slug')
+
+        if after:
+            tags = tags.filter(slug__gt=after)
+        if before:
+            tags = tags.filter(slug__lt=before)
+
+        if count:
+            if before:
+                tags = list(reversed(tags.order_by('-slug')[:count]))
+            else:
+                tags = tags[:count]
+
+        return tags
 
 
 class FragmentDetails(object):
index 4452aa2..310460e 100644 (file)
@@ -17,6 +17,7 @@ book_list_resource = CsrfExemptResource(handler=handlers.BooksHandler, authentic
 ebook_list_resource = Resource(handler=handlers.EBooksHandler)
 # book_list_resource = Resource(handler=handlers.BooksHandler)
 book_resource = Resource(handler=handlers.BookDetailHandler)
+filter_book_resource = Resource(handler=handlers.FilterBooksHandler)
 
 collection_resource = Resource(handler=handlers.CollectionDetailHandler)
 collection_list_resource = Resource(handler=handlers.CollectionsHandler)
@@ -30,6 +31,10 @@ fragment_list_resource = Resource(handler=handlers.FragmentsHandler)
 picture_resource = CsrfExemptResource(handler=handlers.PictureHandler, authentication=auth)
 
 
+tags_re = r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){0,6})'
+paginate_re = r'(?:before/(?P<before>[a-z0-9-]+)/)?(?:after/(?P<after>[a-z0-9-]+)/)?(?:count/(?P<count>[0-9]+)/)?$'
+
+
 @ssi_included
 def incl(request, model, pk, emitter_format):
     resource = {
@@ -73,19 +78,23 @@ urlpatterns = patterns(
         fragment_resource, name="api_fragment"),
 
     # books by tags
-    url(r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){0,6})books/$',
+    url(tags_re + r'books/' + paginate_re,
         book_list_resource, name='api_book_list'),
-    url(r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){0,6})ebooks/$',
+    url(tags_re + r'ebooks/' + paginate_re,
         ebook_list_resource, name='api_ebook_list'),
-    url(r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){0,6})parent_books/$',
+    url(tags_re + r'parent_books/' + paginate_re,
         book_list_resource, {"top_level": True}, name='api_parent_book_list'),
-    url(r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){0,6})parent_ebooks/$',
+    url(tags_re + r'parent_ebooks/' + paginate_re,
         ebook_list_resource, {"top_level": True}, name='api_parent_ebook_list'),
-    url(r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){0,6})audiobooks/$',
+    url(tags_re + r'audiobooks/' + paginate_re,
         book_list_resource, {"audiobooks": True}, name='api_audiobook_list'),
-    url(r'^(?P<tags>(?:(?:[a-z0-9-]+/){2}){0,6})daisy/$',
+    url(tags_re + r'daisy/' + paginate_re,
         book_list_resource, {"daisy": True}, name='api_daisy_list'),
 
+    url(r'^recommended/' + paginate_re, book_list_resource, {"recommended": True}, name='api_recommended_list'),
+    url(r'^newest/', book_list_resource, {"newest": True, "count": 20}, name='api_newest_list'),
+    url(r'^filter-books/', filter_book_resource, name='api_filter_books'),
+
     url(r'^pictures/$', picture_resource),
 
     # fragments by book, tags, themes
diff --git a/src/catalogue/migrations/0016_auto_20171031_1232.py b/src/catalogue/migrations/0016_auto_20171031_1232.py
new file mode 100644 (file)
index 0000000..5892a29
--- /dev/null
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+def refresh_books(apps, schema_editor):
+    Book = apps.get_model('catalogue', 'Book')
+    TagRelation = apps.get_model('catalogue', 'TagRelation')
+    db_alias = schema_editor.connection.alias
+    for book in Book.objects.using(db_alias).all():
+        book.cached_author = ', '.join(
+            TagRelation.objects.filter(content_type__model='book', object_id=book.id, tag__category='author')
+            .values_list('tag__name', flat=True))
+        book.has_audience = 'audience' in book.extra_info
+        book.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0015_book_recommended'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='book',
+            name='cached_author',
+            field=models.CharField(db_index=True, max_length=240, blank=True),
+        ),
+        migrations.AddField(
+            model_name='book',
+            name='has_audience',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.RunPython(refresh_books, migrations.RunPython.noop),
+    ]
index 02278de..140ba50 100644 (file)
@@ -81,6 +81,9 @@ class Book(models.Model):
     parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
     ancestor = models.ManyToManyField('self', blank=True, editable=False, related_name='descendant', symmetrical=False)
 
+    cached_author = models.CharField(blank=True, max_length=240, db_index=True)
+    has_audience = models.BooleanField(default=False)
+
     objects = models.Manager()
     tagged = managers.ModelTaggedItemManager(Tag)
     tags = managers.TagDescriptor(Tag)
@@ -123,7 +126,7 @@ class Book(models.Model):
         return split_tags(self.tags.exclude(category__in=('set', 'theme')))
 
     def author_unicode(self):
-        return self.tag_unicode('author')
+        return self.cached_author
 
     def translator(self):
         translators = self.extra_info.get('translators')
@@ -136,6 +139,9 @@ class Book(models.Model):
             others = ''
         return ', '.join(u'\xa0'.join(reversed(translator.split(', ', 1))) for translator in translators) + others
 
+    def cover_source(self):
+        return self.extra_info.get('cover_source', self.parent.cover_source() if self.parent else '')
+
     def save(self, force_insert=False, force_update=False, **kwargs):
         from sortify import sortify
 
@@ -148,6 +154,9 @@ class Book(models.Model):
             author = u''
         self.sort_key_author = author
 
+        self.cached_author = self.tag_unicode('author')
+        self.has_audience = 'audience' in self.extra_info
+
         ret = super(Book, self).save(force_insert, force_update, **kwargs)
 
         return ret
@@ -636,6 +645,13 @@ class Book(models.Model):
         else:
             return None
 
+    def fragment_data(self):
+        fragment = self.choose_fragment()
+        if fragment:
+            return {'title': fragment.book.pretty_title(), 'html': fragment.get_short_text()}
+        else:
+            return None
+
     def update_popularity(self):
         count = self.tags.filter(category='set').values('user').order_by('user').distinct().count()
         try:
index 30bc55f..265897f 100644 (file)
@@ -2,14 +2,13 @@
 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
-from datetime import datetime
 from traceback import print_exc
 from celery.task import task
 from celery.utils.log import get_task_logger
 from django.conf import settings
+from django.utils import timezone
 
 from catalogue.utils import gallery_path
-from wolnelektury.utils import localtime_to_utc
 from waiter.models import WaitedFile
 
 task_logger = get_task_logger(__name__)
@@ -18,7 +17,7 @@ task_logger = get_task_logger(__name__)
 # TODO: move to model?
 def touch_tag(tag):
     update_dict = {
-        'changed_at': localtime_to_utc(datetime.now()),
+        'changed_at': timezone.now(),
     }
 
     type(tag).objects.filter(pk=tag.pk).update(**update_dict)
index 82e0347..7ec0505 100644 (file)
@@ -1,28 +1,37 @@
 # -*- coding: utf-8 -*-
 from urllib import unquote
 
+from datetime import datetime
 from django.contrib.auth.decorators import permission_required
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect, render
+from django.utils import timezone
+from django.views.decorators.cache import never_cache
 from fnpdjango.utils.views import serve_file
 from honeypot.decorators import check_honeypot
 
+from wolnelektury.utils import localtime_to_utc
 from .forms import contact_forms
 from .models import Attachment, Contact
 
 
 @check_honeypot
+@never_cache
 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 not (force_enabled and request.user.is_superuser):
+        disabled = getattr(form_class, 'disabled', False)
+        end_tuple = getattr(form_class, 'ends_on', None)
+        end_time = localtime_to_utc(datetime(*end_tuple)) if end_tuple else None
+        expired = end_time and end_time < timezone.now()
+        if disabled or expired:
+            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:
index 474d1b5..fc473a6 100644 (file)
@@ -9,6 +9,9 @@ from functools import update_wrapper
 import urllib
 from random import random
 from inspect import isclass
+
+from django.utils.encoding import force_str
+
 from .tasks import track_request
 
 logger = logging.getLogger(__name__)
@@ -18,10 +21,10 @@ def piwik_url(request):
     return urllib.urlencode(dict(
         idsite=getattr(settings, 'PIWIK_SITE_ID', '0'),
         rec=1,
-        url='http://%s%s' % (request.META['HTTP_HOST'], request.path),
+        url=force_str('http://%s%s' % (request.META['HTTP_HOST'], request.path)),
         rand=int(random() * 0x10000),
         apiv=PIWIK_API_VERSION,
-        urlref=request.META.get('HTTP_REFERER', ''),
+        urlref=force_str(request.META.get('HTTP_REFERER', '')),
         ua=request.META.get('HTTP_USER_AGENT', ''),
         lang=request.META.get('HTTP_ACCEPT_LANGUAGE', ''),
         token_auth=getattr(settings, 'PIWIK_TOKEN', ''),
index 6ca78d6..f280dec 100644 (file)
@@ -16,6 +16,8 @@ class KonkursForm(ContactForm):
     form_tag = 'konkurs'
     form_title = u"Konkurs Trzy strony"
     admin_list = ['podpis', 'contact', 'temat']
+    ends_on = (2017, 11, 8)
+    disabled_template = 'contact/disabled_contact_form.html'
 
     opiekun_header = HeaderField(label=u'Dane\xa0Opiekuna/Opiekunki')
     opiekun_nazwisko = forms.CharField(label=u'Imię i nazwisko', max_length=128)
@@ -66,3 +68,34 @@ class KonkursForm(ContactForm):
         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.')
+
+
+class WorkshopsForm(ContactForm):
+    form_tag = 'warsztaty'
+    form_title = u"Wolne Lektury Fest"
+    nazwisko = forms.CharField(label=u'Imię i nazwisko uczestnika', max_length=128)
+    instytucja = forms.CharField(label=u'Instytucja/organizacja', max_length=128, required=False)
+    contact = forms.EmailField(label=u'Adres e-mail', max_length=128)
+    tel = forms.CharField(label=u'Numer telefonu', max_length=32)
+    warsztat = forms.ChoiceField(choices=(
+        ('skad-i-jak', u'Skąd i jak bezpiecznie korzystać z darmowych i wolnych wideo i zdjęć w sieci? '
+                       u'Jak wykorzystać wolne licencje by zwiększyć zasięg Twoich publikacji?'),
+        ('jak-badac', u'Jak badać wykorzystanie zbiorów domeny publicznej?'),
+        ('kultura', u'Kultura dostępna dla wszystkich')),
+        widget=forms.RadioSelect,
+    )
+    agree_header = HeaderField(label=mark_safe_lazy(u'<strong>Oświadczenia</strong>'))
+    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 warsztatów w ramach wydarzenia '
+              u'„WOLNE LEKTURY FEST”. Jednocześnie oświadczam, że zostałam/em poinformowana/y o tym, że mam prawo '
+              u'wglądu w treść swoich danych i możliwość ich poprawiania oraz że ich podanie jest dobrowolne, '
+              u'ale niezbędne do dokonania zgłoszenia.')
+    agree_wizerunek = forms.BooleanField(
+        label='Rozpowszechnianie wizerunku',
+        help_text=u'Wyrażam zgodę na fotografowanie i nagrywanie podczas warsztatów „WOLNE LEKTURY FEST” '
+                  u'24.11.2017 roku i następnie rozpowszechnianie mojego wizerunku w celach promocyjnych.')
+    agree_gala = forms.BooleanField(
+        label=u'Wezmę udział w uroczystej gali o godz. 19.00.', required=False)
index 3695d28..d3c7638 100755 (executable)
@@ -23,4 +23,9 @@ form table {
         font-size: .9em;
         font-style: italic;
     }
+
+    ul {
+        list-style: none;
+        padding-left: 0;
+    }
 }