├── wagtailpolls ├── migrations │ ├── __init__.py │ ├── 0002_auto_20161025_1513.py │ ├── 0003_auto_20170511_1620.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── wagtailpolls_tags.py ├── views │ ├── __init__.py │ ├── results.py │ ├── vote.py │ ├── editor.py │ └── chooser.py ├── __init__.py ├── templates │ ├── wagtailpolls │ │ ├── vote_success.html │ │ ├── chosen.js │ │ ├── list.html │ │ ├── search_results.html │ │ ├── delete.html │ │ ├── copy.html │ │ ├── choose.html │ │ ├── results.html │ │ ├── edit.html │ │ ├── create.html │ │ ├── search.html │ │ ├── poll_results.html │ │ ├── choose.js │ │ ├── index.html │ │ └── poll_list.html │ └── widgets │ │ └── poll_chooser.html ├── locale │ └── fr │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── pagination.py ├── wagtail_hooks.py ├── static │ └── js │ │ └── poll_chooser.js ├── urls.py ├── edit_handlers.py ├── widgets.py ├── forms.py └── models.py ├── .gitignore ├── setup.cfg ├── CONTRIBUTORS ├── MANIFEST.in ├── setup.py ├── LICENSE └── README.rst /wagtailpolls/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wagtailpolls/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wagtailpolls/views/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | dist/ 4 | -------------------------------------------------------------------------------- /wagtailpolls/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3.0' 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/vote_success.html: -------------------------------------------------------------------------------- 1 |

{% blocktrans %}You have successfully voted{% endblocktrans %}

2 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Liam Brenner - 2 | Tim Heap - 3 | François Guérin - 4 | -------------------------------------------------------------------------------- /wagtailpolls/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neon-jungle/wagtailpolls/HEAD/wagtailpolls/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/chosen.js: -------------------------------------------------------------------------------- 1 | function(modal) { 2 | modal.respond('pollChosen', {{ snippet_json|safe }}); 3 | modal.close(); 4 | } 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | 4 | recursive-include wagtailpolls/templates * 5 | recursive-include wagtailpolls/static * 6 | recursive-include wagtailpolls/locale * 7 | -------------------------------------------------------------------------------- /wagtailpolls/templates/widgets/poll_chooser.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/widgets/chooser.html" %} 2 | 3 | {% block chooser_class %}poll-chooser{% endblock %} 4 | 5 | {% block chosen_state_view %} 6 | {{ item }} 7 | {% endblock %} 8 | 9 | {% block edit_chosen_item_url %}{% url 'wagtailpolls_edit' poll_pk=item.pk %}{% endblock %} 10 | -------------------------------------------------------------------------------- /wagtailpolls/views/results.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import permission_required 2 | from django.shortcuts import render, get_object_or_404 3 | 4 | 5 | @permission_required('wagtailadmin.access_admin') 6 | def results(request, poll_pk): 7 | from ..models import Poll, Vote 8 | 9 | poll = get_object_or_404(Poll, pk=poll_pk) 10 | votes = Vote.objects.filter(question__poll=poll) 11 | total_votes = votes.count() 12 | 13 | return render(request, 'wagtailpolls/poll_results.html', { 14 | 'poll': poll, 15 | 'total_votes': total_votes, 16 | }) 17 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for snippet in items %} 13 | 14 | 21 | 22 | {% endfor %} 23 | 24 |
{% trans "Title" %}
15 | {% if choosing %} 16 |

{{ snippet }}

17 | {% else %} 18 |

{{ snippet }}

19 | {% endif %} 20 |
25 | -------------------------------------------------------------------------------- /wagtailpolls/pagination.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.paginator import Paginator, EmptyPage 3 | 4 | 5 | def get_per_page(): 6 | if hasattr(settings, 'DEFAULT_PER_PAGE'): 7 | per_page = settings.DEFAULT_PER_PAGE 8 | else: 9 | per_page = 12 10 | return per_page 11 | 12 | 13 | def paginate(request, items, per_page=get_per_page(), 14 | page_key='page'): 15 | paginator = Paginator(items, per_page) 16 | 17 | try: 18 | page_number = int(request.GET[page_key]) 19 | page = paginator.page(page_number) 20 | except (ValueError, KeyError, EmptyPage): 21 | page = paginator.page(1) 22 | 23 | return paginator, page 24 | -------------------------------------------------------------------------------- /wagtailpolls/templatetags/wagtailpolls_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.core.urlresolvers import reverse 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag(takes_context=True) 8 | def querystring(context, **kwargs): 9 | get = context['request'].GET.copy() 10 | for key, val in kwargs.items(): 11 | if val is None: 12 | get.pop(key, None) 13 | else: 14 | get[key] = val 15 | 16 | return get.urlencode() 17 | 18 | 19 | @register.simple_tag(takes_context=True) 20 | def vote(context, poll): 21 | if poll is None: 22 | return reverse('wagtailpolls_vote', kwargs={'poll_pk': None}) 23 | return reverse('wagtailpolls_vote', kwargs={'poll_pk': poll.pk}) 24 | -------------------------------------------------------------------------------- /wagtailpolls/wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, absolute_import 2 | 3 | from django.conf.urls import include, url 4 | from django.core import urlresolvers 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from wagtail.wagtailcore import hooks 8 | from . import urls 9 | from wagtail.wagtailadmin.menu import MenuItem 10 | 11 | 12 | @hooks.register('register_admin_urls') 13 | def register_admin_urls(): 14 | return [ 15 | url(r'^polls/', include(urls)), 16 | ] 17 | 18 | 19 | @hooks.register('construct_main_menu') 20 | def construct_main_menu(request, menu_items): 21 | menu_items.append( 22 | MenuItem(_('Polls'), urlresolvers.reverse('wagtailpolls_index'), 23 | classnames='icon icon-group', order=250) 24 | ) 25 | -------------------------------------------------------------------------------- /wagtailpolls/migrations/0002_auto_20161025_1513.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.2 on 2016-10-25 13:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('wagtailpolls', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='poll', 17 | name='title', 18 | field=models.CharField(max_length=128, verbose_name='Titre'), 19 | ), 20 | migrations.AlterField( 21 | model_name='question', 22 | name='question', 23 | field=models.CharField(max_length=128, verbose_name='Question'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/search_results.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if items %} 3 | {% if is_searching %} 4 |

5 | {% blocktrans count counter=items.paginator.count %} 6 | There is one match 7 | {% plural %} 8 | There are {{ counter }} matches 9 | {% endblocktrans %} 10 |

11 | {% endif %} 12 | 13 | {% include "wagtailpolls/list.html" %} 14 | 15 | {% include "wagtailadmin/shared/pagination_nav.html" with items=items is_searching=is_searching %} 16 | {% else %} 17 | {% if is_searching %} 18 |

{% blocktrans %}Sorry, no polls match "{{ query_string }}"{% endblocktrans %}

19 | {% else %} 20 | {% url 'wagtailpolls_create' as wagtailpolls_create_url %} 21 |

{% blocktrans %}No polls have been created. Why not add one?{% endblocktrans %}

22 | {% endif %} 23 | {% endif %} 24 | -------------------------------------------------------------------------------- /wagtailpolls/static/js/poll_chooser.js: -------------------------------------------------------------------------------- 1 | function createPollChooser(id) { 2 | console.log('got here'); 3 | var chooserElement = $('#' + id + '-chooser'); 4 | var docTitle = chooserElement.find('.title'); 5 | var input = $('#' + id); 6 | var editLink = chooserElement.find('.edit-link'); 7 | console.log(editLink); 8 | var pollChooser = "/admin/polls/choose/"; 9 | 10 | $('.action-choose', chooserElement).click(function() { 11 | console.log('whoop'); 12 | ModalWorkflow({ 13 | url: pollChooser, 14 | responses: { 15 | pollChosen: function(pollData) { 16 | input.val(pollData.id); 17 | docTitle.text(pollData.string); 18 | chooserElement.removeClass('blank'); 19 | editLink.attr('href', pollData.edit_link); 20 | } 21 | } 22 | }); 23 | }); 24 | 25 | $('.action-clear', chooserElement).click(function() { 26 | input.val(''); 27 | chooserElement.addClass('blank'); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% block titletag %}{% trans "Polls" %}{% endblock %} 4 | {% block bodyclass %}menu-poll{% endblock %} 5 | {% block content %} 6 | 7 | {% trans "Delete" as new_str %} 8 | {% include "wagtailadmin/shared/header.html" with title=new_str subtitle=poll icon="grip" %} 9 | 10 |
11 |
12 | {% csrf_token %} 13 |

{% blocktrans %}Are you sure you want to delete this poll?{% endblocktrans %}

14 | 15 |
16 |
17 | {% endblock %} 18 | 19 | {% block extra_css %} 20 | {% include "wagtailadmin/pages/_editor_css.html" %} 21 | {% endblock %} 22 | {% block extra_js %} 23 | {% include "wagtailadmin/pages/_editor_js.html" %} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/copy.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% block titletag %}{% trans "Polls" %}{% endblock %} 4 | {% block bodyclass %}menu-poll{% endblock %} 5 | {% block content %} 6 | 7 | {% trans "Copy" as new_str %} 8 | {% include "wagtailadmin/shared/header.html" with title=new_str subtitle=poll icon="grip" %} 9 | 10 |
11 |
12 | {% csrf_token %} 13 |

{% blocktrans %}Are you sure you want to create a duplicate of this poll?{% endblocktrans %}

14 | 15 |
16 |
17 | {% endblock %} 18 | 19 | {% block extra_css %} 20 | {% include "wagtailadmin/pages/_editor_css.html" %} 21 | {% endblock %} 22 | {% block extra_js %} 23 | {% include "wagtailadmin/pages/_editor_js.html" %} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/choose.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% trans "Choose a" as choose_str %} 3 | {% include "wagtailadmin/shared/header.html" with title=choose_str subtitle=snippet_type_name icon="group" %} 4 | 5 |
6 | {# Need to keep the form in the HTML, even if the snippet is not searchable #} 7 | {# This is to allow pagination links to be generated from the form action URL #} 8 | 18 | 19 |
20 | {% include "wagtailpolls/results.html" with choosing=1 %} 21 |
22 |
23 | -------------------------------------------------------------------------------- /wagtailpolls/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.conf.urls import url 4 | from .views import chooser, editor, results 5 | 6 | 7 | urlpatterns = [ 8 | # Choosers 9 | url(r'^choose/$', chooser.choose, 10 | name='wagtailpolls_choose'), 11 | url(r'^choose/(\w+)/(\w+)/$', chooser.choose, name='wagtailpolls_choose_specific'), 12 | url(r'^choose/(\d+)/$', chooser.chosen, name='wagtailpolls_chosen'), 13 | # General Urls 14 | url(r'^$', chooser.index, 15 | name='wagtailpolls_index'), 16 | url(r'^search/$', chooser.search, 17 | name='wagtailpolls_search'), 18 | url(r'^create/$', editor.create, 19 | name='wagtailpolls_create'), 20 | url(r'^edit/(?P.*)/$', editor.edit, 21 | name='wagtailpolls_edit'), 22 | url(r'^delete/(?P.*)/$', editor.delete, 23 | name='wagtailpolls_delete'), 24 | url(r'^copy/(?P.*)/$', editor.copy, 25 | name='wagtailpolls_copy'), 26 | url(r'^results/(?P.*)/$', results.results, 27 | name='wagtailpolls_results'), 28 | ] 29 | -------------------------------------------------------------------------------- /wagtailpolls/migrations/0003_auto_20170511_1620.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.1 on 2017-05-11 14:20 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('wagtailpolls', '0002_auto_20161025_1513'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='poll', 17 | options={'verbose_name': 'poll', 'verbose_name_plural': 'polls'}, 18 | ), 19 | migrations.AlterModelOptions( 20 | name='question', 21 | options={'verbose_name': 'question', 'verbose_name_plural': 'questions'}, 22 | ), 23 | migrations.AlterModelOptions( 24 | name='vote', 25 | options={'verbose_name': 'vote', 'verbose_name_plural': 'votes'}, 26 | ), 27 | migrations.AlterField( 28 | model_name='poll', 29 | name='title', 30 | field=models.CharField(max_length=128, verbose_name='Title'), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/results.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if items %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for snippet in items %} 14 | 15 | 22 | 23 | {% endfor %} 24 | 25 |
{% trans "Title" %}
16 | {% if choosing %} 17 |

{{ snippet }}

18 | {% else %} 19 |

{{ snippet }}

20 | {% endif %} 21 |
26 | {% else %} 27 | {% url 'wagtailpolls_create' as wagtailpolls_create_url %} 28 |

{% blocktrans %}No polls have been created. Why not add one?{% endblocktrans %}

29 | {% endif %} 30 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% block titletag %}{% trans "Polls" %}{% endblock %} 4 | {% block bodyclass %}menu-poll{% endblock %} 5 | {% block content %} 6 | 7 | {% trans "Edit" as new_str %} 8 | {% include "wagtailadmin/shared/header.html" with title=new_str subtitle=poll icon="group" %} 9 | 10 |
11 | {% csrf_token %} 12 | {{ edit_handler.render_form_content }} 13 | 14 |
15 |
    16 |
  • 17 | 23 |
  • 24 |
25 |
26 |
27 | {% endblock %} 28 | 29 | {% block extra_css %} 30 | {% include "wagtailadmin/pages/_editor_css.html" %} 31 | {% endblock %} 32 | {% block extra_js %} 33 | {% include "wagtailadmin/pages/_editor_js.html" %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/create.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% block titletag %}{% blocktrans %}New poll{% endblocktrans %}{% endblock %} 4 | {% block bodyclass %}menu-poll{% endblock %} 5 | {% block content %} 6 | {% trans "New" as new_str %} 7 | {% trans "Poll" as pollpost_str %} 8 | {% include "wagtailadmin/shared/header.html" with title=new_str subtitle=pollpost_str icon="group" %} 9 | 10 |
11 | {% csrf_token %} 12 | {{ edit_handler.render_form_content }} 13 | 14 |
15 |
    16 |
  • 17 | 23 |
  • 24 |
25 |
26 |
27 | {% endblock %} 28 | 29 | {% block extra_css %} 30 | {% include "wagtailadmin/pages/_editor_css.html" %} 31 | {% endblock %} 32 | {% block extra_js %} 33 | {% include "wagtailadmin/pages/_editor_js.html" %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/search.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% block titletag %}{% trans "Polls" %}{% endblock %} 4 | {% block bodyclass %}menu-poll{% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 |
10 |
11 |
12 |

{% blocktrans %}Poll{% endblocktrans %}

13 |
14 |
15 | {{search_form}} 16 |
  • 17 |
    18 |
    19 | 20 | 25 |
    26 |
    27 | {% if poll_list.exists %} 28 | {% include 'wagtailpolls/poll_list.html' %} 29 | {% else %} 30 |
    {% blocktrans %}Your search results didn't return anything!{% endblocktrans %}
    31 | {% endif %} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Install wagtailpolls using setuptools 4 | """ 5 | 6 | from wagtailpolls import __version__ 7 | 8 | with open('README.rst', 'r') as f: 9 | readme = f.read() 10 | 11 | try: 12 | from setuptools import setup, find_packages 13 | except ImportError: 14 | from ez_setup import use_setuptools 15 | use_setuptools() 16 | from setuptools import setup, find_packages 17 | 18 | setup( 19 | name='wagtailpolls', 20 | version=__version__, 21 | description='A polling plugin for the Wagtail CMS', 22 | long_description=readme, 23 | author='Takeflight', 24 | author_email='liam@takeflight.com.au', 25 | url='https://github.com/takeflight/wagtailpolls', 26 | 27 | install_requires=[ 28 | 'wagtail>=1.3', 29 | 'django-ipware==1.1.2', 30 | ], 31 | zip_safe=False, 32 | license='BSD License', 33 | 34 | packages=find_packages(), 35 | 36 | include_package_data=True, 37 | package_data={}, 38 | 39 | classifiers=[ 40 | 'Environment :: Web Environment', 41 | 'Intended Audience :: Developers', 42 | 'Operating System :: OS Independent', 43 | 'Programming Language :: Python', 44 | 'Framework :: Django', 45 | 'License :: OSI Approved :: BSD License', 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/poll_results.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% block titletag %}{% trans "Polls: Viewing Results" %}{% endblock %} 4 | {% block bodyclass %}menu-poll{% endblock %} 5 | 6 | {% block content %} 7 | 8 |
    9 |
    10 |
    11 |
    12 |

    {% blocktrans %}{{poll}} - Total Votes {{total_votes}}{% endblocktrans %}

    13 |
    14 |
    15 | 16 |
    17 |
    18 |
    19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for question in poll.questions.all %} 28 | 29 | 32 | 33 | 34 | {% endfor %} 35 | 36 |
    {% blocktrans %}Question{% endblocktrans %}{% blocktrans %}Votes{% endblocktrans %}
    30 |

    {{question}}

    31 |
    {{question.votes.all.count}}
    37 |
    38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/choose.js: -------------------------------------------------------------------------------- 1 | function initModal(modal) { 2 | 3 | function ajaxifyLinks(context) { 4 | $('a.snippet-choice', modal.body).click(function() { 5 | modal.loadUrl(this.href); 6 | return false; 7 | }); 8 | 9 | $('.pagination a', context).click(function() { 10 | var page = this.getAttribute('data-page'); 11 | setPage(page); 12 | return false; 13 | }); 14 | } 15 | 16 | var searchUrl = $('form.snippet-search', modal.body).attr('action'); 17 | 18 | function search() { 19 | $.ajax({ 20 | url: searchUrl, 21 | data: {q: $('#id_q').val(), results: 'true'}, 22 | success: function(data, status) { 23 | $('#search-results').html(data); 24 | ajaxifyLinks($('#search-results')); 25 | } 26 | }); 27 | return false; 28 | } 29 | 30 | function setPage(page) { 31 | var dataObj = {p: page, results: 'true'}; 32 | 33 | if ($('#id_q').length && $('#id_q').val().length) { 34 | dataObj.q = $('#id_q').val(); 35 | } 36 | 37 | $.ajax({ 38 | url: searchUrl, 39 | data: dataObj, 40 | success: function(data, status) { 41 | $('#search-results').html(data); 42 | ajaxifyLinks($('#search-results')); 43 | } 44 | }); 45 | return false; 46 | } 47 | 48 | $('form.snippet-search', modal.body).submit(search); 49 | 50 | $('#id_q').on('input', function() { 51 | clearTimeout($.data(this, 'timer')); 52 | var wait = setTimeout(search, 50); 53 | $(this).data('timer', wait); 54 | }); 55 | 56 | ajaxifyLinks(modal.body); 57 | } 58 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/index.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load i18n %} 3 | {% block titletag %}{% trans "Polls" %}{% endblock %} 4 | {% block bodyclass %}menu-poll{% endblock %} 5 | 6 | {% block extra_js %} 7 | 21 | {% endblock %} 22 | 23 | {% block content %} 24 | 25 |
    26 |
    27 |
    28 |
    29 |

    {% blocktrans %}Polls{% endblocktrans %}

    30 |
    31 |
    32 | {{search_form}} 33 |
  • 34 |
    35 |
    36 | 37 | 42 |
    43 |
    44 | {% if poll_list.exists %} 45 | {% include 'wagtailpolls/poll_list.html' %} 46 | {% else %} 47 |
    {% blocktrans %}No polls yet!{% endblocktrans %}
    48 | {% endif %} 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Takeflight 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of wagtailpolls nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /wagtailpolls/edit_handlers.py: -------------------------------------------------------------------------------- 1 | from django.template.loader import render_to_string 2 | from django.utils.encoding import force_text 3 | from django.utils.safestring import mark_safe 4 | from wagtail.wagtailadmin.edit_handlers import BaseChooserPanel 5 | 6 | from .widgets import AdminPollChooser 7 | 8 | 9 | class BasePollChooserPanel(BaseChooserPanel): 10 | object_type_name = 'item' 11 | 12 | _target_model = None 13 | 14 | @classmethod 15 | def widget_overrides(cls): 16 | return {cls.field_name: AdminPollChooser(model=cls.target_model())} 17 | 18 | @classmethod 19 | def target_model(cls): 20 | if cls._target_model is None: 21 | cls._target_model = cls.model._meta.get_field(cls.field_name).rel.model 22 | 23 | return cls._target_model 24 | 25 | def render_as_field(self): 26 | instance_obj = self.get_chosen_item() 27 | return mark_safe(render_to_string(self.field_template, { 28 | 'field': self.bound_field, 29 | self.object_type_name: instance_obj, 30 | 'snippet_type_name': self.get_snippet_type_name(), 31 | })) 32 | 33 | @classmethod 34 | def get_snippet_type_name(cls): 35 | return force_text(cls.target_model()._meta.verbose_name) 36 | 37 | 38 | class PollChooserPanel(object): 39 | def __init__(self, field_name, snippet_type=None): 40 | self.field_name = field_name 41 | self.snippet_type = snippet_type 42 | 43 | def bind_to_model(self, model): 44 | return type(str('_PollChooserPanel'), (BasePollChooserPanel,), { 45 | 'model': model, 46 | 'field_name': self.field_name, 47 | 'snippet_type': self.snippet_type, 48 | }) 49 | -------------------------------------------------------------------------------- /wagtailpolls/widgets.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import json 4 | from django.contrib.contenttypes.models import ContentType 5 | 6 | from django.template.loader import render_to_string 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | from wagtail.wagtailadmin.widgets import AdminChooser 10 | 11 | 12 | class AdminPollChooser(AdminChooser): 13 | target_content_type = None 14 | 15 | class Media: 16 | js = ['js/poll_chooser.js'] 17 | 18 | def __init__(self, content_type=None, **kwargs): 19 | model = kwargs.pop('model', None) 20 | self.choose_one_text = (_('Choose a poll')) 21 | self.choose_another_text = (_('Choose another poll')) 22 | self.link_to_chosen_text = (_('Edit this poll')) 23 | 24 | super(AdminPollChooser, self).__init__(**kwargs) 25 | if content_type is not None: 26 | self.target_content_type = content_type 27 | elif model is not None: 28 | self.target_content_type = ContentType.objects.get_for_model(model) 29 | else: 30 | raise RuntimeError("Unable to set model from both content_type and model") 31 | 32 | def render_html(self, name, value, attrs): 33 | model_class = self.target_content_type.model_class() 34 | instance, value = self.get_instance_and_id(model_class, value) 35 | 36 | original_field_html = super(AdminPollChooser, self).render_html(name, value, attrs) 37 | 38 | return render_to_string("widgets/poll_chooser.html", { 39 | 'widget': self, 40 | 'original_field_html': original_field_html, 41 | 'attrs': attrs, 42 | 'value': value, 43 | 'item': instance, 44 | }) 45 | 46 | def render_js_init(self, id_, name, value): 47 | return "createPollChooser({id});".format(id=json.dumps(id_)) 48 | -------------------------------------------------------------------------------- /wagtailpolls/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import modelcluster.fields 6 | import django.utils.timezone 7 | import wagtail.wagtailsearch.index 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Poll', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)), 20 | ('title', models.CharField(max_length=128)), 21 | ('date_created', models.DateTimeField(default=django.utils.timezone.now)), 22 | ], 23 | options={ 24 | 'abstract': False, 25 | }, 26 | bases=(models.Model, wagtail.wagtailsearch.index.Indexed), 27 | ), 28 | migrations.CreateModel( 29 | name='Question', 30 | fields=[ 31 | ('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)), 32 | ('question', models.CharField(max_length=128)), 33 | ('poll', modelcluster.fields.ParentalKey(to='wagtailpolls.Poll', related_name='questions')), 34 | ], 35 | options={ 36 | 'abstract': False, 37 | }, 38 | ), 39 | migrations.CreateModel( 40 | name='Vote', 41 | fields=[ 42 | ('id', models.AutoField(verbose_name='ID', auto_created=True, primary_key=True, serialize=False)), 43 | ('ip', models.GenericIPAddressField()), 44 | ('time', models.DateTimeField(auto_now_add=True)), 45 | ('question', modelcluster.fields.ParentalKey(to='wagtailpolls.Question', related_name='votes')), 46 | ], 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /wagtailpolls/views/vote.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.decorators import permission_required 3 | from django.http import Http404, HttpResponse, JsonResponse 4 | from django.shortcuts import get_object_or_404 5 | from django.utils.translation import ugettext_lazy as _ 6 | from wagtailpolls.forms import VoteForm 7 | from wagtailpolls.models import Poll, Vote 8 | 9 | 10 | def vote_data(poll): 11 | questions = poll.questions.all() 12 | votes = Vote.objects.filter(question__poll=poll) 13 | _vote_data = { 14 | 'poll': poll.title, 15 | 'total_questions': questions.count(), 16 | 'total_votes': votes.count(), 17 | 'votes': { 18 | question.question: question.votes.count() 19 | for question in questions 20 | } 21 | } 22 | return _vote_data 23 | 24 | 25 | def _vote(request, poll_pk): 26 | """ 27 | Performs the vote 28 | 29 | :param request: http request 30 | :param poll_pk: poll identifier 31 | :returns: JSON response 32 | """ 33 | try: 34 | int(poll_pk) 35 | except ValueError: 36 | raise Http404("

    {0}

    ".format('Oops, there is no poll to vote on!')) 37 | 38 | poll = get_object_or_404(Poll, pk=poll_pk) 39 | 40 | form = VoteForm(data=request.POST, poll=poll, request=request) 41 | 42 | if 'polls' in request.session: 43 | if poll.pk in request.session['polls']: 44 | return JsonResponse(vote_data(poll)) 45 | else: 46 | request.session['polls'] = [] 47 | 48 | if form.is_valid(): 49 | form.save() 50 | request.session['polls'].append(poll.pk) 51 | request.session.modified = True 52 | return JsonResponse(vote_data(poll)) 53 | 54 | else: 55 | data = vote_data(poll) 56 | data.update({'form_error': form.errors}) 57 | return JsonResponse(data) 58 | 59 | 60 | if getattr(settings, 'WAGTAILPOLLS_VOTE_REQUIRE_PERMS', None): 61 | vote = permission_required(settings.POLLS_VOTE_REQUIRE_PERMS)(_vote) 62 | else: 63 | vote = _vote 64 | -------------------------------------------------------------------------------- /wagtailpolls/forms.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django import forms 4 | from django.conf import settings 5 | from django.utils import timezone 6 | from django.utils.translation import ugettext_lazy as _ 7 | from django.utils.translation import ungettext_lazy as __ 8 | from ipware.ip import get_ip, get_real_ip 9 | 10 | from .models import Vote 11 | 12 | 13 | # stop votes from an ip for 1 min after each vote 14 | 15 | 16 | class SearchForm(forms.Form): 17 | query = forms.CharField(required=False) 18 | 19 | 20 | class VoteForm(forms.ModelForm): 21 | question = forms.ModelChoiceField(Vote.objects.none(), widget=forms.RadioSelect, empty_label=None) 22 | 23 | class Meta: 24 | model = Vote 25 | fields = ['question'] 26 | 27 | def __init__(self, poll, request=None, data=None, **kwargs): 28 | super(VoteForm, self).__init__(data, **kwargs) 29 | self.poll = poll 30 | self.request = request 31 | self.fields['question'].queryset = poll.questions.all() 32 | if request: 33 | self.ip = get_real_ip(self.request) or get_ip(self.request) 34 | 35 | def recent_vote(self): 36 | vote = Vote.objects.filter(ip=self.ip).order_by('-time') 37 | return vote.first() 38 | 39 | def clean(self): 40 | recent_vote = self.recent_vote() 41 | cooldown = getattr(settings, 'WAGTAILPOLLS_VOTE_COOLDOWN', 10) 42 | if not self.ip: 43 | self.add_error(None, _('Sorry, we were not able to obtain your ip address')) 44 | if recent_vote is not None: 45 | if timezone.now() - recent_vote.time < datetime.timedelta(minutes=cooldown): 46 | error_string = __( 47 | 'Sorry, you can not vote twice in a minute', 48 | 'Sorry, you can not vote twice in %(cooldown)s minutes', 49 | cooldown) % { 50 | 'cooldown': cooldown 51 | } 52 | self.add_error(None, error_string) 53 | 54 | def save(self, commit=True): 55 | instance = super(VoteForm, self).save(commit=False) 56 | instance.ip = self.ip # TODO 57 | if commit: 58 | instance.save() 59 | return instance 60 | -------------------------------------------------------------------------------- /wagtailpolls/templates/wagtailpolls/poll_list.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load wagtailpolls_tags %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% for poll in page %} 12 | 13 | 25 | 32 | 35 | 36 | {% endfor %} 37 | 38 |
    {% blocktrans %}Name{% endblocktrans %}{% blocktrans %}Date created{% endblocktrans %}
    14 |

    15 | 16 | {{poll}} 17 | 18 |

    19 | 24 |
    26 | {% if poll.date_created %} 27 |
    28 | {% blocktrans with when=poll.date_created|timesince %}{{ when }} ago{% endblocktrans %} 29 |
    30 | {% endif %} 31 |
    33 | {% blocktrans %}Results{% endblocktrans %} 34 |
    39 | 62 | -------------------------------------------------------------------------------- /wagtailpolls/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from six import text_type 4 | 5 | from django.db import models 6 | from django.db.models.query import QuerySet 7 | from django.utils import timezone 8 | from django.utils.encoding import python_2_unicode_compatible 9 | from django.utils.text import slugify 10 | from django.utils.translation import ugettext_lazy as _ 11 | from modelcluster.fields import ParentalKey 12 | from modelcluster.models import ClusterableModel 13 | from wagtail.wagtailadmin.edit_handlers import FieldPanel, InlinePanel 14 | from wagtail.wagtailsearch import index 15 | from wagtail.wagtailsearch.backends import get_search_backend 16 | 17 | 18 | class PollQuerySet(QuerySet): 19 | def search(self, query_string, fields=None, backend='default'): 20 | """ 21 | This runs a search query on all the pages in the QuerySet 22 | """ 23 | search_backend = get_search_backend(backend) 24 | return search_backend.search(query_string, self) 25 | 26 | 27 | @python_2_unicode_compatible 28 | class Vote(models.Model): 29 | question = ParentalKey('Question', related_name='votes') 30 | ip = models.GenericIPAddressField() 31 | time = models.DateTimeField(auto_now_add=True) 32 | 33 | def __str__(self): 34 | return self.question.question 35 | 36 | class Meta: 37 | verbose_name = _('vote') 38 | verbose_name_plural = _('votes') 39 | 40 | 41 | @python_2_unicode_compatible 42 | class Question(ClusterableModel, models.Model): 43 | poll = ParentalKey('Poll', related_name='questions') 44 | question = models.CharField(max_length=128, verbose_name=_('Question')) 45 | 46 | def __str__(self): 47 | return self.question 48 | 49 | class Meta: 50 | verbose_name = _('question') 51 | verbose_name_plural = _('questions') 52 | 53 | 54 | @python_2_unicode_compatible 55 | class Poll(ClusterableModel, models.Model, index.Indexed): 56 | title = models.CharField(max_length=128, verbose_name=_('Title')) 57 | date_created = models.DateTimeField(default=timezone.now) 58 | 59 | class Meta: 60 | verbose_name = _('poll') 61 | verbose_name_plural = _('polls') 62 | 63 | panels = [ 64 | FieldPanel('title'), 65 | InlinePanel('questions', label=_('Questions'), min_num=1) 66 | ] 67 | 68 | search_fields = ( 69 | index.SearchField('title', partial_match=True, boost=5), 70 | index.SearchField('id', boost=10), 71 | ) 72 | 73 | objects = PollQuerySet.as_manager() 74 | 75 | def get_nice_url(self): 76 | return slugify(text_type(self)) 77 | 78 | def get_template(self, request): 79 | try: 80 | return self.template 81 | except AttributeError: 82 | return '{0}/{1}.html'.format(self._meta.app_label, self._meta.model_name) 83 | 84 | def form(self): 85 | # Stops circular import 86 | from .forms import VoteForm 87 | return VoteForm(self) 88 | 89 | def __str__(self): 90 | return self.title 91 | -------------------------------------------------------------------------------- /wagtailpolls/views/editor.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.decorators import permission_required 3 | from django.shortcuts import redirect, render, get_object_or_404 4 | from django.utils.translation import ugettext_lazy as _ 5 | from django.utils import timezone 6 | from django.utils.lru_cache import lru_cache 7 | 8 | from wagtail.wagtailadmin.edit_handlers import ( 9 | ObjectList, extract_panel_definitions_from_model_class) 10 | from wagtail.wagtailcore.models import Page 11 | 12 | 13 | @lru_cache(maxsize=None) 14 | def get_poll_edit_handler(Poll): 15 | panels = extract_panel_definitions_from_model_class(Poll) 16 | EditHandler = ObjectList(panels).bind_to_model(Poll) 17 | return EditHandler 18 | 19 | 20 | @permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view 21 | def create(request): 22 | from ..models import Poll 23 | 24 | poll = Poll() 25 | EditHandler = get_poll_edit_handler(Poll) 26 | EditForm = EditHandler.get_form_class(Poll) 27 | 28 | if request.method == 'POST': 29 | form = EditForm(request.POST, request.FILES, instance=poll) 30 | if form.is_valid(): 31 | poll = form.save() 32 | poll.save() 33 | messages.success(request, _('The poll "{0!s}" has been added').format(poll)) 34 | return redirect('wagtailpolls_index') 35 | 36 | else: 37 | messages.error(request, _('The poll could not be created due to validation errors')) 38 | edit_handler = EditHandler(instance=poll, form=form) 39 | 40 | else: 41 | form = EditForm(instance=poll) 42 | edit_handler = EditHandler(instance=poll, form=form) 43 | 44 | return render(request, 'wagtailpolls/create.html', { 45 | 'form': form, 46 | 'edit_handler': edit_handler, 47 | }) 48 | 49 | 50 | @permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view 51 | def edit(request, poll_pk): 52 | from ..models import Poll 53 | 54 | poll = get_object_or_404(Poll, pk=poll_pk) 55 | 56 | EditHandler = get_poll_edit_handler(Poll) 57 | EditForm = EditHandler.get_form_class(Poll) 58 | 59 | if request.method == 'POST': 60 | form = EditForm(request.POST, request.FILES, instance=poll) 61 | 62 | if form.is_valid(): 63 | poll = form.save() 64 | poll.save() 65 | messages.success(request, _('The poll "{0!s}" has been updated').format(poll)) 66 | return redirect('wagtailpolls_index') 67 | 68 | else: 69 | messages.error(request, _('The poll could not be updated due to validation errors')) 70 | edit_handler = EditHandler(instance=poll, form=form) 71 | else: 72 | form = EditForm(instance=poll) 73 | edit_handler = EditHandler(instance=poll, form=form) 74 | 75 | return render(request, 'wagtailpolls/edit.html', { 76 | 'poll': poll, 77 | 'form': form, 78 | 'edit_handler': edit_handler, 79 | }) 80 | 81 | 82 | @permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view 83 | def delete(request, poll_pk): 84 | from ..models import Poll 85 | 86 | poll = get_object_or_404(Poll, pk=poll_pk) 87 | 88 | if request.method == 'POST': 89 | poll.delete() 90 | return redirect('wagtailpolls_index') 91 | 92 | return render(request, 'wagtailpolls/delete.html', { 93 | 'poll': poll, 94 | }) 95 | 96 | 97 | @permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view 98 | def copy(request, poll_pk): 99 | from ..models import Poll 100 | 101 | poll = Poll.objects.get(id=poll_pk) 102 | 103 | if request.method == 'POST': 104 | poll.pk = None 105 | poll.save() 106 | return redirect('wagtailpolls_index') 107 | 108 | return render(request, 'wagtailpolls/copy.html', { 109 | 'poll': poll, 110 | }) 111 | -------------------------------------------------------------------------------- /wagtailpolls/views/chooser.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import json 4 | 5 | from django.contrib.auth.decorators import permission_required 6 | from django.shortcuts import get_object_or_404, render 7 | from django.utils.six import text_type 8 | from django.utils.translation import ugettext as _ 9 | from wagtail.wagtailadmin.forms import SearchForm as AdminSearchForm 10 | from wagtail.wagtailadmin.modal_workflow import render_modal_workflow 11 | from wagtail.wagtailsearch.backends import get_search_backend 12 | 13 | from ..forms import SearchForm 14 | from ..models import Poll 15 | from ..pagination import paginate 16 | 17 | 18 | @permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view 19 | def index(request): 20 | poll_list = Poll.objects.all() 21 | search_form = SearchForm() 22 | 23 | paginator, page = paginate( 24 | request, 25 | Poll.objects.all(), 26 | per_page=8) 27 | 28 | return render(request, 'wagtailpolls/index.html', { 29 | 'page': page, 30 | 'paginator': paginator, 31 | 'poll_list': poll_list, 32 | 'search_form': search_form, 33 | }) 34 | 35 | 36 | @permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view 37 | def search(request): 38 | poll_list = Poll.objects.all() 39 | search_form = SearchForm(request.GET or None) 40 | 41 | if search_form.is_valid(): 42 | query = search_form.cleaned_data['query'] 43 | poll_list = poll_list.search(query) 44 | 45 | else: 46 | paginator, page = paginate( 47 | request, 48 | Poll.objects.all(), 49 | per_page=8) 50 | 51 | paginator, page = paginate( 52 | request, 53 | poll_list, 54 | per_page=20) 55 | 56 | return render(request, 'wagtailpolls/search.html', { 57 | 'page': page, 58 | 'paginator': paginator, 59 | 'poll_list': poll_list, 60 | 'search_form': search_form, 61 | }) 62 | 63 | 64 | def choose(request): 65 | items = Poll.objects.all() 66 | 67 | # Search 68 | is_searching = False 69 | search_query = None 70 | if 'q' in request.GET: 71 | search_form = AdminSearchForm(request.GET, placeholder=_("Search %(snippet_type_name)s") % { 72 | 'snippet_type_name': 'Polls' 73 | }) 74 | 75 | if search_form.is_valid(): 76 | search_query = search_form.cleaned_data['q'] 77 | 78 | search_backend = get_search_backend() 79 | items = search_backend.search(search_query, items) 80 | is_searching = True 81 | 82 | else: 83 | search_form = AdminSearchForm() 84 | 85 | # Pagination 86 | paginator, paginated_items = paginate(request, items, per_page=25) 87 | 88 | # If paginating or searching, render "results.html" 89 | if request.GET.get('results', None) == 'true': 90 | return render(request, "wagtailpolls/search_results.html", { 91 | 'items': paginated_items, 92 | 'query_string': search_query, 93 | 'is_searching': is_searching, 94 | }) 95 | 96 | return render_modal_workflow( 97 | request, 98 | 'wagtailpolls/choose.html', 'wagtailpolls/choose.js', 99 | { 100 | 'snippet_type_name': 'Poll', 101 | 'items': paginated_items, 102 | 'is_searchable': True, 103 | 'search_form': search_form, 104 | 'query_string': search_query, 105 | 'is_searching': is_searching, 106 | } 107 | ) 108 | 109 | 110 | def chosen(request, id): 111 | model = Poll 112 | item = get_object_or_404(model, id=id) 113 | 114 | snippet_json = json.dumps({ 115 | 'id': item.id, 116 | 'string': text_type(item), 117 | }) 118 | 119 | return render_modal_workflow( 120 | request, 121 | None, 'wagtailpolls/chosen.js', 122 | { 123 | 'snippet_json': snippet_json, 124 | } 125 | ) 126 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | wagtailpolls 3 | =============== 4 | 5 | A plugin for Wagtail that provides polling functionality. 6 | 7 | Installing 8 | ========== 9 | 10 | Install using pip:: 11 | 12 | pip install wagtailpolls 13 | 14 | It works with Wagtail 1.3 and upwards. 15 | 16 | Using 17 | ===== 18 | 19 | Add ``wagtailpolls`` to your ``INSTALLED_APPS``. 20 | 21 | Ensure you add the line ``from wagtailpolls.views.vote import vote`` to your ``urls.py`` and include the URL ``url(r'^vote/(?P.*)/$', vote, name='wagtailpolls_vote')``. 22 | 23 | Define a foreign key referring to ``wagtailpolls.Poll`` and use the ``PollChooserPanel``: 24 | 25 | .. code-block:: python 26 | 27 | from django.db import models 28 | from wagtailpolls.edit_handlers import PollChooserPanel 29 | from wagtail.wagtailadmin.edit_handlers import FieldPanel 30 | 31 | class Content(Page): 32 | body = models.TextField() 33 | poll = models.ForeignKey( 34 | 'wagtailpolls.Poll', 35 | null=True, 36 | blank=True, 37 | on_delete=models.SET_NULL 38 | ) 39 | 40 | content_panels = [ 41 | FieldPanel('body', classname="full"), 42 | PollChooserPanel('poll'), 43 | ] 44 | 45 | 46 | Then, in your editor, ensure that you have added some polls in the polls section in wagtail admin. You will be able to select a poll from there accessable in the template as you would expect. 47 | 48 | Templating & Display 49 | ==================== 50 | There are many ways in which you may want to display your poll. ``wagtailpolls`` comes with a template tag to assist with this, as well as certain attributes accessible via templating to render each question as a form. Here is an example using all of the tools provided: 51 | 52 | .. code-block:: html 53 | 54 | {% extends "layouts/page.html" %} 55 | {% load wagtailpolls_tags %} 56 | {% block content %} 57 |

    {{ self.title }}

    58 |
    59 | {% if self.poll %} 60 |
    61 | {% csrf_token %} 62 | {{self.poll.form}} 63 |

    64 | 65 |
    66 | {% else %} 67 | No polls added to this page yet! 68 | {% endif %} 69 | {% endblock %} 70 | 71 | As shown, the ``{% vote %}`` template tag will need to be passed a poll instance to function correctly. You will also need to ``{% load wagtailpolls_tags %}`` at the top of the file where this template tag is used. 72 | The poll can be rendered with all questions using ``.form`` at the end. ``.form_as_ul`` and all other form types will also work. 73 | 74 | If you do select a poll for a page, no fields will display on the form and, upon voting, a message stating that there is no poll to vote on will be displayed. 75 | 76 | Voting 77 | ====== 78 | When a vote has been submitted, the server will return a ``JsonResponse`` something like: 79 | 80 | .. code-block:: json 81 | 82 | {"total_votes": 11, "total_questions": 3, "poll": "Test Poll", "votes": {"Nah": 10, "Yeah": 1, "Maybe": 0}} 83 | 84 | With javascript, this data can be used to create a frontend for your poll to your own liking. 85 | 86 | The voting form also performs some validation. If the voting form is unable to obtain your IP it will return something like: 87 | 88 | .. code-block:: json 89 | 90 | {"poll": "Test Poll", "total_questions": 3, "total_votes": 11, "votes": {"Yeah": 1, "Maybe": 0, "Nah": 10}, "form_error": {"__all__": ["Sorry, we were not able to obtain your ip address"]}} 91 | 92 | There is also a ``WAGTAILPOLLS_VOTE_COOLDOWN`` which is set in your settings. This will only allow users on the same IP to vote at an interval of your choosing. If this is caught, the error will be present in the ``JsonResponse`` much like the error above. 93 | 94 | Additionally, information will be added to the django session (basically cookies will be set) that will help make sure devices are not able to vote twice. When a vote is rejected due to this reason, the vote simply won't register with no error being returned in the ``JsonResponse``. 95 | 96 | Settings 97 | ======== 98 | 99 | The following settings can to be set in your ``settings.py`` file. 100 | 101 | ``WAGTAILPOLLS_VOTE_COOLDOWN`` `This is to be an integer representing minutes, the default is 10 minutes.` 102 | 103 | ``WAGTAILPOLLS_VOTE_REQUIRE_PERMS`` `A string or list of strings representing the permissions to vote, aka. 'wagtailadmin.access_admin'` 104 | -------------------------------------------------------------------------------- /wagtailpolls/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2016-11-07 11:23+1100\n" 11 | "PO-Revision-Date: 2016-10-25 15:11+0200\n" 12 | "Last-Translator: François GUÉRIN \n" 13 | "Language-Team: \n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | "X-Generator: Poedit 1.8.10\n" 20 | 21 | #: forms.py:43 22 | msgid "Sorry, we were not able to obtain your ip address" 23 | msgstr "" 24 | 25 | #: models.py:37 26 | msgid "vote" 27 | msgstr "" 28 | 29 | #: models.py:38 30 | msgid "votes" 31 | msgstr "" 32 | 33 | #: models.py:44 templates/wagtailpolls/poll_results.html:22 34 | msgid "Question" 35 | msgstr "Question" 36 | 37 | #: models.py:50 38 | #, fuzzy 39 | #| msgid "Question" 40 | msgid "question" 41 | msgstr "Question" 42 | 43 | #: models.py:51 44 | #, fuzzy 45 | #| msgid "Questions" 46 | msgid "questions" 47 | msgstr "Questions" 48 | 49 | #: models.py:56 templates/wagtailpolls/list.html:8 50 | #: templates/wagtailpolls/results.html:8 51 | msgid "Title" 52 | msgstr "Titre" 53 | 54 | #: models.py:60 55 | #, fuzzy 56 | #| msgid "New poll" 57 | msgid "poll" 58 | msgstr "Nouveau sondage" 59 | 60 | #: models.py:61 61 | #, fuzzy 62 | #| msgid "Polls" 63 | msgid "polls" 64 | msgstr "Sondages" 65 | 66 | #: models.py:65 67 | msgid "Questions" 68 | msgstr "Questions" 69 | 70 | #: templates/wagtailpolls/choose.html:2 71 | msgid "Choose a" 72 | msgstr "Choisir un" 73 | 74 | #: templates/wagtailpolls/choose.html:14 templates/wagtailpolls/index.html:33 75 | msgid "Search" 76 | msgstr "Rechercher" 77 | 78 | #: templates/wagtailpolls/copy.html:3 templates/wagtailpolls/delete.html:3 79 | #: templates/wagtailpolls/edit.html:3 templates/wagtailpolls/index.html:3 80 | #: templates/wagtailpolls/index.html:29 templates/wagtailpolls/search.html:3 81 | #: templates/wagtailpolls/search.html:12 wagtail_hooks.py:22 82 | msgid "Polls" 83 | msgstr "Sondages" 84 | 85 | #: templates/wagtailpolls/copy.html:7 templates/wagtailpolls/poll_list.html:21 86 | msgid "Copy" 87 | msgstr "Copier" 88 | 89 | #: templates/wagtailpolls/copy.html:13 90 | msgid "Are you sure you want to create a duplicate of this poll?" 91 | msgstr "Êtes vous sur de vouloir créer une copie de ce sondage ?" 92 | 93 | #: templates/wagtailpolls/copy.html:14 94 | msgid "Yes, create duplicate" 95 | msgstr "Oui, créer une copie" 96 | 97 | #: templates/wagtailpolls/create.html:3 98 | msgid "New poll" 99 | msgstr "Nouveau sondage" 100 | 101 | #: templates/wagtailpolls/create.html:6 102 | msgid "New" 103 | msgstr "Nouveau" 104 | 105 | #: templates/wagtailpolls/create.html:7 106 | msgid "Poll" 107 | msgstr "Sondage" 108 | 109 | #: templates/wagtailpolls/create.html:18 110 | msgid "Save" 111 | msgstr "Enregistrer" 112 | 113 | #: templates/wagtailpolls/delete.html:7 114 | #: templates/wagtailpolls/poll_list.html:22 115 | msgid "Delete" 116 | msgstr "Supprimer" 117 | 118 | #: templates/wagtailpolls/delete.html:13 119 | msgid "Are you sure you want to delete this poll?" 120 | msgstr "Êtes vous sûr de supprimer ce sondage ?" 121 | 122 | #: templates/wagtailpolls/delete.html:14 123 | msgid "Yes, delete" 124 | msgstr "Oui, suprimer" 125 | 126 | #: templates/wagtailpolls/edit.html:7 templates/wagtailpolls/poll_list.html:20 127 | msgid "Edit" 128 | msgstr "Éditer" 129 | 130 | #: templates/wagtailpolls/edit.html:18 131 | msgid "Update" 132 | msgstr "Mettre à jour" 133 | 134 | #: templates/wagtailpolls/index.html:39 templates/wagtailpolls/search.html:22 135 | msgid "Add poll" 136 | msgstr "Ajouter un sondage" 137 | 138 | #: templates/wagtailpolls/index.html:47 139 | msgid "No polls yet!" 140 | msgstr "" 141 | 142 | #: templates/wagtailpolls/poll_list.html:6 143 | msgid "Name" 144 | msgstr "" 145 | 146 | #: templates/wagtailpolls/poll_list.html:7 147 | msgid "Date created" 148 | msgstr "" 149 | 150 | #: templates/wagtailpolls/poll_list.html:28 151 | #, python-format 152 | msgid "%(when)s ago" 153 | msgstr "" 154 | 155 | #: templates/wagtailpolls/poll_list.html:33 156 | msgid "Check out the poll results for '{{poll}}'" 157 | msgstr "" 158 | 159 | #: templates/wagtailpolls/poll_list.html:33 160 | msgid "Results" 161 | msgstr "" 162 | 163 | #: templates/wagtailpolls/poll_list.html:40 164 | #, python-format 165 | msgid "Page %(number)s of %(total)s" 166 | msgstr "" 167 | 168 | #: templates/wagtailpolls/poll_list.html:46 169 | msgid "Previous" 170 | msgstr "" 171 | 172 | #: templates/wagtailpolls/poll_list.html:55 173 | msgid "Next" 174 | msgstr "" 175 | 176 | #: templates/wagtailpolls/poll_results.html:3 177 | msgid "Polls: Viewing Results" 178 | msgstr "Sondages : Voir les résultats " 179 | 180 | #: templates/wagtailpolls/poll_results.html:12 181 | #, python-format 182 | msgid "%(poll)s - Total Votes %(total_votes)s" 183 | msgstr "%(poll)s - Total des votes %(total_votes)s" 184 | 185 | #: templates/wagtailpolls/poll_results.html:23 186 | msgid "Votes" 187 | msgstr "" 188 | 189 | #: templates/wagtailpolls/search.html:30 190 | msgid "Your search results didn't return anything!" 191 | msgstr "" 192 | 193 | #: templates/wagtailpolls/search_results.html:5 194 | #, python-format 195 | msgid "" 196 | "\n" 197 | "\t\t\tThere is one match\n" 198 | "\t\t" 199 | msgid_plural "" 200 | "\n" 201 | "\t\t\tThere are %(counter)s matches\n" 202 | "\t\t" 203 | msgstr[0] "" 204 | "\n" 205 | "\t\t\tIl y a une correspondance\n" 206 | "\t\t" 207 | msgstr[1] "" 208 | "\n" 209 | "\t\t\tIl y a %(counter)s correspondances\n" 210 | "\t\t" 211 | 212 | #: templates/wagtailpolls/search_results.html:18 213 | #, python-format 214 | msgid "Sorry, no snippets match \"%(query_string)s\"" 215 | msgstr "Désolé, aucune bride ne correspond à \"%(query_string)s\"" 216 | 217 | #: templates/wagtailpolls/search_results.html:21 218 | #, python-format 219 | msgid "" 220 | "No %(snippet_type_name_plural)s have been created. Why not add one?" 222 | msgstr "" 223 | "Aucun %(snippet_type_name_plural)s n'a été créé. Pourquoi ne pas en ajouter un ?" 225 | 226 | #: templates/wagtailpolls/vote_success.html:1 227 | msgid "You have successfully voted" 228 | msgstr "" 229 | 230 | #: views/chooser.py:71 231 | #, python-format 232 | msgid "Search %(snippet_type_name)s" 233 | msgstr "Rechercher un %(snippet_type_name)s" 234 | 235 | #: views/editor.py:33 236 | msgid "The poll \"{0!s}\" has been added" 237 | msgstr "Le sondage \"{0!s}\" a été ajouté" 238 | 239 | #: views/editor.py:37 240 | msgid "The poll could not be created due to validation errors" 241 | msgstr "Le sondage ne peut pas être créé, à cause d'une erreur de validation" 242 | 243 | #: views/editor.py:65 244 | msgid "The poll \"{0!s}\" has been updated" 245 | msgstr "Le sondage \"{0!s}\" a été mis à jour" 246 | 247 | #: views/editor.py:69 248 | msgid "The poll could not be updated due to validation errors" 249 | msgstr "Ce sondage ne peut être mise à jour à cause d'une erreur de validation" 250 | 251 | #: widgets.py:20 252 | #, python-format 253 | msgid "Choose %(snippet_type_name)s" 254 | msgstr "Choisir un %(snippet_type_name)s" 255 | 256 | #: widgets.py:22 257 | #, python-format 258 | msgid "Choose another %(snippet_type_name)s" 259 | msgstr "Choisir un autre %(snippet_type_name)s" 260 | 261 | #: widgets.py:24 262 | #, python-format 263 | msgid "Edit this %(snippet_type_name)s" 264 | msgstr "Éditer un %(snippet_type_name)s" 265 | 266 | #~ msgid "Edit this %s" 267 | #~ msgstr "Éditer ce %s" 268 | --------------------------------------------------------------------------------