summaryrefslogtreecommitdiff
path: root/papillon
diff options
context:
space:
mode:
Diffstat (limited to 'papillon')
-rw-r--r--papillon/__init__.py0
-rw-r--r--papillon/locale/fr/LC_MESSAGES/django.po384
-rwxr-xr-xpapillon/manage.py11
-rwxr-xr-xpapillon/poll_cleaning.py23
-rw-r--r--papillon/polls/__init__.py0
-rw-r--r--papillon/polls/admin.py34
-rw-r--r--papillon/polls/feeds.py55
-rw-r--r--papillon/polls/forms.py132
-rw-r--r--papillon/polls/models.py227
-rw-r--r--papillon/polls/templatetags/__init__.py0
-rw-r--r--papillon/polls/templatetags/get_range.py25
-rw-r--r--papillon/polls/views.py491
-rw-r--r--papillon/settings.py102
-rw-r--r--papillon/static/bg.jpgbin0 -> 3473 bytes
-rw-r--r--papillon/static/styles.css357
-rw-r--r--papillon/static/textareas.js27
-rw-r--r--papillon/templates/base.html27
-rw-r--r--papillon/templates/category.html16
-rw-r--r--papillon/templates/create.html33
-rw-r--r--papillon/templates/edit.html62
-rw-r--r--papillon/templates/editChoices.html19
-rw-r--r--papillon/templates/editChoicesAdmin.html44
-rw-r--r--papillon/templates/editChoicesUser.html27
-rw-r--r--papillon/templates/feeds/poll_description.html8
-rw-r--r--papillon/templates/main.html22
-rw-r--r--papillon/templates/vote.html148
-rw-r--r--papillon/urls.py53
27 files changed, 2327 insertions, 0 deletions
diff --git a/papillon/__init__.py b/papillon/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/papillon/__init__.py
diff --git a/papillon/locale/fr/LC_MESSAGES/django.po b/papillon/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4d09e84
--- /dev/null
+++ b/papillon/locale/fr/LC_MESSAGES/django.po
@@ -0,0 +1,384 @@
+# Papillon
+# Copyright (C) 2008
+# This file is distributed under the same license as the papillon package.
+# Étienne Loks <etienne.loks@peacefrogs.net>, 2008.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2009-12-04 23:49+0100\n"
+"PO-Revision-Date: 2008-08-20 00:22+0200\n"
+"Last-Translator: Étienne Loks <etienne.loks@peacefrogs.net>,\n"
+"Language-Team: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: polls/feeds.py:37
+msgid "Papillon - poll : "
+msgstr "Papillon - sondage : "
+
+#: polls/forms.py:99
+msgid "Invalid poll"
+msgstr "Sondage non valide"
+
+#: polls/forms.py:114
+msgid "Invalid date format: YYYY-MM-DD HH:MM:SS"
+msgstr "Format de date invalide AAAA-MM-JJ HH:MM:SS"
+
+#: polls/models.py:44 templates/edit.html:22
+msgid ""
+"Copy this address and send it to voters who want to participate to this poll"
+msgstr "Copiez cette adresse et envoyez là aux participants à ce sondage."
+
+#: polls/models.py:46 templates/edit.html:31
+msgid "Address to modify the current poll"
+msgstr "Adresse de modification de ce sondage"
+
+#: polls/models.py:48 templates/vote.html:133
+msgid "Author name"
+msgstr "Nom de l'auteur"
+
+#: polls/models.py:49
+msgid "Name, firstname or nickname of the author"
+msgstr "Nom, prénom ou surnom de l'auteur"
+
+#: polls/models.py:51
+msgid "Poll name"
+msgstr "Nom du sondage"
+
+#: polls/models.py:52
+msgid "Global name to present the poll"
+msgstr "Nom général pour présenter le sondage"
+
+#: polls/models.py:54
+msgid "Poll description"
+msgstr "Description du sondage"
+
+#: polls/models.py:55
+msgid "Precise description of the poll"
+msgstr "Description précise du sondage"
+
+#: polls/models.py:57
+msgid "Yes/No poll"
+msgstr "Oui/Non"
+
+#: polls/models.py:58
+msgid "Yes/No/Maybe poll"
+msgstr "Oui/Non/Peut-être"
+
+#: polls/models.py:59
+msgid "One choice poll"
+msgstr "Sondage à choix unique"
+
+#: polls/models.py:60
+msgid "Valuable choice poll"
+msgstr "Sondage pondéré"
+
+#: polls/models.py:62
+msgid "Type of the poll"
+msgstr "Type du sondage"
+
+#: polls/models.py:63
+msgid ""
+"Type of the poll:\n"
+"\n"
+" - \"Yes/No poll\" is the appropriate type for a simple multi-choice poll\n"
+" - \"Yes/No/Maybe poll\" allows voters to stay undecided\n"
+" - \"One choice poll\" gives only one option to choose from\n"
+" - \"Valuable choice poll\" permit users to give a note between 0 to 9 to "
+"different choices\n"
+msgstr ""
+"Type du sondage :\n"
+"\n"
+" - \"Oui/Non\" est un sondage simple permettant de choisir entre plusieurs "
+"options\n"
+" - \"Oui/Non/Peut-être\" permet de laisser une option d'indécision aux "
+"votants\n"
+" - \"Sondage à choix unique\" ne permet que de choisir un choix parmi ceux "
+"proposés\n"
+" - \"Sondage pondéré\" permet aux utilisateurs de donner une note entre 0 et "
+"9 pour chaque choix\n"
+
+#: polls/models.py:71
+msgid "Choices are dates"
+msgstr "Les choix sont des dates"
+
+#: polls/models.py:72
+msgid "Check this option to choose between dates"
+msgstr "Cocher cette option pour choisir entre des dates"
+
+#: polls/models.py:74
+msgid "Closing date"
+msgstr "Date de fermeture"
+
+#: polls/models.py:74
+msgid "Closing date for participating to the poll"
+msgstr "Date de fermeture au vote du sondage"
+
+#: polls/models.py:78
+msgid "Display the poll on main page"
+msgstr "Afficher le sondage sur la page principale"
+
+#: polls/models.py:78
+msgid "Check this option to make the poll public"
+msgstr "Cocher cette option pour que le sondage soit publique"
+
+#: polls/models.py:81
+msgid "Allow users to add choices"
+msgstr "Permettre aux votants d'ajouter des choix"
+
+#: polls/models.py:81
+msgid "Check this option to open the poll to new choices submitted by users"
+msgstr ""
+"Cocher cette option pour permettre aux utilisateurs d'enrichir le sondage "
+"avec des nouveaux choix"
+
+#: polls/models.py:84
+msgid "Hide votes to new voters"
+msgstr "Cacher les résultats aux nouveaux votants"
+
+#: polls/models.py:84
+msgid "Check this option to hide poll results to new users"
+msgstr ""
+"Cocher cette option pour cacher, dans un premier temps, les résultats d'un "
+"sondage"
+
+#: polls/models.py:87
+msgid "State of the poll"
+msgstr "État du sondage"
+
+#: polls/models.py:87
+msgid "Uncheck this option to close the poll/check this option to reopen it"
+msgstr ""
+"Décocher cette option pour fermet le sondage aux votes/cocher cette option "
+"pour l'ouvrir de nouveau"
+
+#: polls/models.py:156
+#, python-format
+msgid "Vote from %(user)s"
+msgstr "Vote de %(user)s"
+
+#: polls/models.py:224
+msgid "Yes"
+msgstr "Oui"
+
+#: polls/models.py:225 polls/models.py:226
+msgid "No"
+msgstr "Non"
+
+#: polls/models.py:225
+msgid "Maybe"
+msgstr "Peut-être"
+
+#: polls/views.py:65
+msgid "The poll requested don't exist (anymore?)"
+msgstr "Le sondage que vous avez demandé n'existe pas (n'existe plus ?)"
+
+#: templates/category.html:8
+msgid "Polls"
+msgstr "Sondage"
+
+#: templates/create.html:11
+msgid "New poll"
+msgstr "Nouveau sondage"
+
+#: templates/create.html:28
+msgid "Create"
+msgstr "Créer"
+
+#: templates/edit.html:13
+msgid "Edit poll"
+msgstr "Éditer un sondage"
+
+#: templates/edit.html:17
+msgid "Poll url"
+msgstr "Adresse du sondage"
+
+#: templates/edit.html:26
+msgid "Administration url"
+msgstr "Adresse d'administration"
+
+#: templates/edit.html:35
+msgid "Choices administration url"
+msgstr "Adresse d'administration des choix"
+
+#: templates/edit.html:40
+msgid "Address to modify choices of the current poll."
+msgstr "Adresse de modification des choix disponibles pour ce sondage"
+
+#: templates/edit.html:57 templates/editChoicesAdmin.html:38
+#: templates/vote.html:59 templates/vote.html.py:115
+msgid "Edit"
+msgstr "Modifier"
+
+#: templates/editChoices.html:4
+msgid "New choice"
+msgstr "Nouveau choix"
+
+#: templates/editChoices.html:11
+msgid ""
+"Setting a new choice. Optionally you can set a limit of vote for this "
+"choice. This limit is usefull for limited resources allocation."
+msgstr ""
+"Ajouter un nouveau choix. Optionnellement vous pouvez ajouter une limite de "
+"vote pour ce choix. Cette limite est utile dans le cas d'attribution de "
+"ressources limitées."
+
+#: templates/editChoices.html:15 templates/editChoicesAdmin.html:35
+#: templates/editChoicesUser.html:22
+msgid "Limited to"
+msgstr "Limité à"
+
+#: templates/editChoices.html:15 templates/editChoicesAdmin.html:35
+#: templates/editChoicesUser.html:22
+msgid "vote(s)"
+msgstr "vote(s)"
+
+#: templates/editChoices.html:16
+msgid "Add"
+msgstr "Ajouter"
+
+#: templates/editChoicesAdmin.html:14
+msgid "As long as no options were added to the poll, it will not be available."
+msgstr ""
+"Tant qu'aucune option ne sera ajouté au sondage, il ne sera pas disponible."
+
+#: templates/editChoicesAdmin.html:16
+msgid "Complete/Finalise the poll"
+msgstr "Complète/Finalise le sondage"
+
+#: templates/editChoicesAdmin.html:17
+msgid "Next"
+msgstr "Suivant"
+
+#: templates/editChoicesAdmin.html:21
+msgid "Available choices"
+msgstr "Choix disponibles"
+
+#: templates/editChoicesAdmin.html:24
+msgid "Up/down"
+msgstr "Haut/bas"
+
+#: templates/editChoicesAdmin.html:25 templates/editChoicesUser.html:17
+msgid "Label"
+msgstr "Libellé"
+
+#: templates/editChoicesAdmin.html:26 templates/editChoicesUser.html:18
+msgid "Limit"
+msgstr "Limite"
+
+#: templates/editChoicesAdmin.html:27
+msgid "Delete?"
+msgstr "Supprimer ?"
+
+#: templates/editChoicesUser.html:13
+msgid "Return to the poll"
+msgstr "Retourner au sondage"
+
+#: templates/editChoicesUser.html:14
+msgid "Choices"
+msgstr "Choix"
+
+#: templates/editChoicesUser.html:21 templates/vote.html:23
+#: templates/vote.html.py:144
+msgid "DATETIME_FORMAT"
+msgstr ""
+
+#: templates/main.html:6
+msgid "Create a poll"
+msgstr "Créer un sondage"
+
+#: templates/main.html:7
+msgid ""
+"Create a new sondage for take a decision, find a date for a meeting, etc."
+msgstr ""
+"Créer un nouveau sondage pour prendre une décision, trouver une date pour "
+"une réunion, etc."
+
+#: templates/main.html:7
+msgid "It's here!"
+msgstr "C'est ici !"
+
+#: templates/main.html:9
+msgid "Public polls"
+msgstr "Sondages publics"
+
+#: templates/main.html:17
+msgid "Categories"
+msgstr "Catégories"
+
+#: templates/vote.html:15
+msgid "The current poll is closed."
+msgstr "Le sondage actuel est fermé"
+
+#: templates/vote.html:23
+msgid "max"
+msgstr "max"
+
+#: templates/vote.html:54 templates/vote.html.py:103
+msgid "Limit reached"
+msgstr "Limite atteinte"
+
+#: templates/vote.html:109
+msgid "Sum"
+msgstr "Somme"
+
+#: templates/vote.html:115
+msgid "Participate"
+msgstr "Participer"
+
+#: templates/vote.html:122
+msgid "Add a new choice to this poll?"
+msgstr "Ajouter un nouveau choix à ce sondage ?"
+
+#: templates/vote.html:124
+msgid ""
+"You have already vote? You are enough wise not to be influenced by other "
+"votes? You can display result by clicking"
+msgstr ""
+"Vous avez déjà voté ? Vous pensez être suffisament sage pour ne pas être "
+"influencé par les autres votes ? Vous pouvez afficher le résultat en cliquant"
+
+#: templates/vote.html:124
+msgid "here"
+msgstr "ici"
+
+#: templates/vote.html:125
+msgid "Remain informed of poll evolution:"
+msgstr "Restez informé de l'évolution du sondage"
+
+#: templates/vote.html:125
+msgid "syndication"
+msgstr "syndication"
+
+#: templates/vote.html:128
+msgid "Comments"
+msgstr "Commentaires"
+
+#: templates/vote.html:137
+msgid "Comment"
+msgstr "Commentaire"
+
+#: templates/vote.html:140
+msgid "Send"
+msgstr "Envoyer"
+
+#: templates/feeds/poll_description.html:2
+#, python-format
+msgid "%(voter_name)s has added/modified a vote."
+msgstr "%(voter_name)s a ajouté ou modifié un vote."
+
+#: templates/feeds/poll_description.html:3
+msgid "Current results:"
+msgstr "Résultats actuels :"
+
+#: templates/feeds/poll_description.html:6
+#, python-format
+msgid ": %(sum)s vote"
+msgid_plural ": %(sum)s votes"
+msgstr[0] " : %(sum)s vote"
+msgstr[1] " : %(sum)s votes"
+
diff --git a/papillon/manage.py b/papillon/manage.py
new file mode 100755
index 0000000..bcdd55e
--- /dev/null
+++ b/papillon/manage.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/papillon/poll_cleaning.py b/papillon/poll_cleaning.py
new file mode 100755
index 0000000..993a5b4
--- /dev/null
+++ b/papillon/poll_cleaning.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+'''
+Clean the old polls
+'''
+
+import os
+import sys
+
+# django settings path
+os.environ['DJANGO_SETTINGS_MODULE'] = 'papillon.settings'
+
+# add the parent path to sys.path
+curdir = os.path.abspath(os.curdir)
+sep = os.path.sep
+sys.path.append(sep.join(curdir.split(sep)[:-1]))
+
+
+from papillon.polls.models import Poll
+
+for poll in Poll.objects.all():
+ poll.checkForErasement()
diff --git a/papillon/polls/__init__.py b/papillon/polls/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/papillon/polls/__init__.py
diff --git a/papillon/polls/admin.py b/papillon/polls/admin.py
new file mode 100644
index 0000000..2207c60
--- /dev/null
+++ b/papillon/polls/admin.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (C) 2008 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# See the file COPYING for details.
+
+"""
+Settings for administration pages
+"""
+
+from papillon.polls.models import Poll, Category
+from django.contrib import admin
+
+class PollAdmin(admin.ModelAdmin):
+ search_fields = ("name",)
+ list_display = ('name', 'category', 'modification_date', 'public', 'open')
+ list_filter = ('public', 'open', 'category')
+
+# register of differents database fields
+admin.site.register(Category)
+admin.site.register(Poll, PollAdmin)
diff --git a/papillon/polls/feeds.py b/papillon/polls/feeds.py
new file mode 100644
index 0000000..2d52dc7
--- /dev/null
+++ b/papillon/polls/feeds.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (C) 2008 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# See the file COPYING for details.
+
+import time
+
+from django.core.exceptions import ObjectDoesNotExist
+from django.contrib.syndication.feeds import Feed
+from django.utils.translation import gettext_lazy as _
+
+from papillon.settings import BASE_SITE
+from papillon.polls.models import Poll, Vote, Voter
+
+
+class PollLatestEntries(Feed):
+ def get_object(self, poll_url):
+ if len(poll_url) < 1:
+ raise ObjectDoesNotExist
+ return Poll.objects.get(base_url=poll_url[0])
+
+ def title(self, obj):
+ return _("Papillon - poll : ") + obj.name
+
+ def link(self, obj):
+ if not obj:
+ raise FeedDoesNotExist
+ return BASE_SITE + "/poll/" + obj.base_url
+
+ def description(self, obj):
+ return obj.description
+
+ def item_link(self, voter):
+ url = "%s/poll/%s_%d" % (BASE_SITE, voter.poll.base_url,
+ time.mktime(voter.modification_date.timetuple()))
+ return url
+
+ def items(self, obj):
+ voters = Voter.objects.filter(poll__id=obj.id).\
+order_by('-modification_date')[:10]
+ return voters \ No newline at end of file
diff --git a/papillon/polls/forms.py b/papillon/polls/forms.py
new file mode 100644
index 0000000..3a151aa
--- /dev/null
+++ b/papillon/polls/forms.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (C) 2009 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# See the file COPYING for details.
+
+'''
+Forms management
+'''
+
+from datetime import datetime
+
+from django import forms
+from django.contrib.admin import widgets as adminwidgets
+from django.utils.translation import gettext_lazy as _
+
+from papillon.polls.models import Poll, Category, Choice, Comment
+from papillon import settings
+
+class TextareaWidget(forms.Textarea):
+ """
+ Manage the edition of a text using TinyMCE
+ """
+ class Media:
+ js = ["%stiny_mce.js" % settings.TINYMCE_URL,
+ "%stextareas.js" % settings.MEDIA_URL,]
+
+class PollForm(forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ super(PollForm, self).__init__(*args, **kwargs)
+ self.fields['description'].widget = TextareaWidget()
+
+class CreatePollForm(PollForm):
+ class Meta:
+ model = Poll
+ exclude = ['base_url', 'admin_url', 'open', 'author', 'enddate',
+ 'public', 'opened_admin', 'hide_choices']
+ if not Category.objects.all():
+ exclude.append('category')
+
+class CommentForm(forms.ModelForm):
+ class Meta:
+ model = Comment
+ exclude = ['date',]
+ def __init__(self, *args, **kwargs):
+ super(CommentForm, self).__init__(*args, **kwargs)
+ self.fields['text'].widget = TextareaWidget()
+
+# workaround for SplitDateTime with required=False
+class SplitDateTimeJSField(forms.SplitDateTimeField):
+ def __init__(self, *args, **kwargs):
+ super(SplitDateTimeJSField, self).__init__(*args, **kwargs)
+ self.widget.widgets[0].attrs = {'class': 'vDateField'}
+ self.widget.widgets[1].attrs = {'class': 'vTimeField'}
+
+class AdminPollForm(PollForm):
+ class Meta:
+ model = Poll
+ exclude = ['author', 'author_name', 'base_url', 'admin_url',
+ 'dated_choices', 'type']
+ if not Category.objects.all():
+ exclude.append('category')
+ enddate = SplitDateTimeJSField(widget=adminwidgets.AdminSplitDateTime(),
+ required=False, label=Poll._meta.get_field('enddate').verbose_name,
+ help_text=Poll._meta.get_field('enddate').help_text)
+
+class ChoiceForm(forms.ModelForm):
+ class Meta:
+ model = Choice
+ fields = ('name', 'limit', 'poll', 'order',)
+ def __init__(self, *args, **kwargs):
+ super(ChoiceForm, self).__init__(*args, **kwargs)
+ self.fields['poll'].widget = forms.HiddenInput()
+ self.fields['order'].widget = forms.HiddenInput()
+
+class DatedChoiceForm(ChoiceForm):
+ def __init__(self, *args, **kwargs):
+ super(DatedChoiceForm, self).__init__(*args, **kwargs)
+ self.fields['name'].widget = adminwidgets.AdminSplitDateTime()
+
+ def clean_name(self):
+ try:
+ poll_id = self.data['poll']
+ poll = Poll.objects.get(id=int(poll_id))
+ except (ValueError, Poll.DoesNotExist):
+ raise forms.ValidationError(_('Invalid poll'))
+ data = self.cleaned_data['name']
+ if poll.dated_choices:
+ # management of dates fields
+ if data.startswith('[') and data.endswith(']') and "'" in data:
+ datas = data.split("'")
+ try:
+ assert len(datas) == 5
+ time = datas[3]
+ if not time:
+ time = '00:00:00'
+ date = "%s %s" % (datas[1], time)
+ datetime.strptime(date, '%Y-%m-%d %H:%M:%S')
+ data = date
+ except (ValueError, AssertionError):
+ raise forms.ValidationError(_('Invalid date format: \
+YYYY-MM-DD HH:MM:SS'))
+ return data
+
+ def clean_limit(self):
+ """
+ data = eval(self.cleaned_data['name'])
+
+ new_limit = int(request.POST[key])
+ sum = choice.getSum()
+ if new_limit < sum:
+ response_dct['error'] = _("You cannot lower \
+%(name)s's limit to this number : there is currently %(sum)d votes for this \
+choice.") % {'name':choice.name, 'sum':sum}
+ else:
+ choice.limit = new_limit
+ choice.save()
+"""
+ pass
diff --git a/papillon/polls/models.py b/papillon/polls/models.py
new file mode 100644
index 0000000..f8b3b22
--- /dev/null
+++ b/papillon/polls/models.py
@@ -0,0 +1,227 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (C) 2008 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# See the file COPYING for details.
+
+'''
+Models management
+'''
+
+import datetime
+
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from papillon.settings import DAYS_TO_LIVE
+
+class Category(models.Model):
+ name = models.CharField(max_length=100)
+ description = models.TextField()
+ def __unicode__(self):
+ return self.name
+
+class PollUser(models.Model):
+ name = models.CharField(max_length=100)
+ email = models.CharField(max_length=100)
+ password = models.CharField(max_length=100)
+ modification_date = models.DateTimeField(auto_now=True)
+
+class Poll(models.Model):
+ base_url = models.CharField(max_length=100, help_text=_('Copy this \
+address and send it to voters who want to participate to this poll'))
+ admin_url = models.CharField(max_length=100, help_text=_("Address to \
+modify the current poll"))
+ author_name = models.CharField(verbose_name=_("Author name"),
+ max_length=100, help_text=_("Name, firstname or nickname of the author"))
+ author = models.ForeignKey(PollUser, null=True, blank=True)
+ name = models.CharField(max_length=200, verbose_name=_("Poll name"),
+ help_text=_("Global name to present the poll"))
+ description = models.CharField(max_length=1000,
+ verbose_name=_("Poll description"),
+ help_text=_("Precise description of the poll"))
+ category = models.ForeignKey(Category, null=True, blank=True)
+ TYPE = (('P', _('Yes/No poll')),
+ ('B', _('Yes/No/Maybe poll')),
+ ('O', _('One choice poll')),
+ ('V', _('Valuable choice poll')),)
+ type = models.CharField(max_length=1, choices=TYPE,
+ verbose_name=_("Type of the poll"),
+ help_text=_("""Type of the poll:
+
+ - "Yes/No poll" is the appropriate type for a simple multi-choice poll
+ - "Yes/No/Maybe poll" allows voters to stay undecided
+ - "One choice poll" gives only one option to choose from
+ - "Valuable choice poll" permit users to give a note between 0 to 9 to \
+different choices
+"""))
+ dated_choices = models.BooleanField(verbose_name=_("Choices are dates"),
+ default=False, help_text=_("Check this option to choose between dates"))
+ enddate = models.DateTimeField(null=True, blank=True,
+verbose_name=_("Closing date"), help_text=_("Closing date for participating to \
+the poll"))
+ modification_date = models.DateTimeField(auto_now=True)
+ public = models.BooleanField(default=False,
+verbose_name=_("Display the poll on main page"), help_text=_("Check this \
+option to make the poll public"))
+ opened_admin = models.BooleanField(default=False,
+verbose_name=_("Allow users to add choices"), help_text=_("Check this option \
+to open the poll to new choices submitted by users"))
+ hide_choices = models.BooleanField(default=False,
+verbose_name=_("Hide votes to new voters"), help_text=_("Check this option to \
+hide poll results to new users"))
+ open = models.BooleanField(default=True,
+verbose_name=_("State of the poll"), help_text=_("Uncheck this option to close \
+the poll/check this option to reopen it"))
+
+ def getTypeLabel(self):
+ idx = [type[0] for type in self.TYPE].index(self.type)
+ return Poll.TYPE[idx][1]
+
+ def checkForErasement(self):
+ '''Check if the poll has to be deleted'''
+ if not DAYS_TO_LIVE:
+ return
+ now = datetime.datetime.now()
+ dtl = datetime.timedelta(days=DAYS_TO_LIVE)
+ if self.modification_date + dtl > now:
+ return
+ voters = Voter.objects.filter(poll=self)
+ for voter in voters:
+ if voter.modification_date + dtl > now:
+ return
+ for voter in voters:
+ voter.user.delete()
+ voter.delete()
+ comments = Comment.objects.filter(poll=self)
+ for comment in comments:
+ comment.delete()
+ self.delete()
+
+ def getChoices(self):
+ """
+ Get choices associated to this vote"""
+ return Choice.objects.filter(poll=self)
+
+ def reorder(self):
+ """
+ Reorder choices of the poll"""
+ if not self.dated_choices:
+ return
+ choices = self.getChoices()
+ sort_fct = lambda x:datetime.datetime.strptime(x.name,
+ '%Y-%m-%d %H:%M:%S')
+ choices = sorted(choices, key=sort_fct)
+ for idx, choice in enumerate(choices):
+ choice.order = idx
+ choice.save()
+
+ class Admin:
+ pass
+ class Meta:
+ ordering = ['-modification_date']
+ def __unicode__(self):
+ return self.name
+
+class Comment(models.Model):
+ '''Comment for a poll'''
+ poll = models.ForeignKey(Poll)
+ author_name = models.CharField(max_length=100)
+ text = models.CharField(max_length=1000)
+ date = models.DateTimeField(auto_now_add=True)
+ class Meta:
+ ordering = ['date']
+
+class Voter(models.Model):
+ user = models.ForeignKey(PollUser)
+ poll = models.ForeignKey(Poll)
+ creation_date = models.DateTimeField(auto_now_add=True)
+ modification_date = models.DateTimeField(auto_now=True)
+ class Meta:
+ ordering = ['creation_date']
+ def __unicode__(self):
+ return _("Vote from %(user)s") % {'user':self.user.name}
+ def getVotes(self, choice_ids):
+ '''Get votes for a subset of choices
+ '''
+ query = Vote.objects.filter(voter=self)
+ query = query.extra(where=['choice_id IN (%s)' \
+ % ",".join([str(choice_id) for choice_id in choice_ids])])
+ return list(query.order_by('choice'))
+
+class Choice(models.Model):
+ poll = models.ForeignKey(Poll)
+ name = models.CharField(max_length=200)
+ order = models.IntegerField()
+ limit = models.IntegerField(null=True, blank=True)
+ available = models.BooleanField(default=True)
+ class Admin:
+ pass
+ class Meta:
+ ordering = ['order']
+
+ def get_date(self):
+ if not self.poll.dated_choices:
+ return self.name
+ return datetime.datetime.strptime(self.name, '%Y-%m-%d %H:%M:%S')
+
+ def set_date(self, value):
+ self._date = value
+ #if not self.poll.dated_choices:
+ # self.name = value
+ #self.name = datetime.strftime(value, '%Y-%m-%d %H:%M:%S')
+ date = property(get_date, set_date)
+
+ def getSum(self, balanced_poll=None):
+ '''Get the sum of votes for this choice'''
+ sum = 0
+ for vote in Vote.objects.filter(choice=self, value__isnull=False):
+ sum += vote.value
+ if balanced_poll:
+ return sum/2
+ return sum
+
+ def changeOrder(self, idx=1):
+ '''
+ Change a choice in the list
+ '''
+ if (self.order + idx) < 0:
+ return
+ choices = Choice.objects.filter(poll=self.poll)
+ if self.order + idx > len(choices):
+ return
+ new_order = self.order + idx
+ for choice in choices:
+ if choice == self:
+ continue
+ if idx < 0 and choice.order < self.order \
+ and choice.order >= new_order:
+ choice.order += 1
+ choice.save()
+ if idx > 0 and choice.order > self.order \
+ and choice.order <= new_order:
+ choice.order -= 1
+ choice.save()
+ self.order = new_order
+ self.save()
+
+class Vote(models.Model):
+ voter = models.ForeignKey(Voter)
+ choice = models.ForeignKey(Choice)
+ VOTE = ((1, (_('Yes'), _('Yes'))),
+ (0, (_('No'), _('Maybe')), ),
+ (-1, (_('No'), _('No'))),)
+ value = models.IntegerField(choices=VOTE, blank=True, null=True)
diff --git a/papillon/polls/templatetags/__init__.py b/papillon/polls/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/papillon/polls/templatetags/__init__.py
diff --git a/papillon/polls/templatetags/get_range.py b/papillon/polls/templatetags/get_range.py
new file mode 100644
index 0000000..b9d8328
--- /dev/null
+++ b/papillon/polls/templatetags/get_range.py
@@ -0,0 +1,25 @@
+from django.template import Library
+
+register = Library()
+
+@register.filter
+def get_range( value ):
+ """
+ Filter - returns a list containing range made from given value
+ Usage (in template):
+
+ <ul>{% for i in 3|get_range %}
+ <li>{{ i }}. Do something</li>
+{% endfor %}</ul>
+
+Results with the HTML:
+<ul>
+<li>0. Do something</li>
+<li>1. Do something</li>
+<li>2. Do something</li>
+</ul>
+
+Instead of 3 one may use the variable set in the views
+ """
+ return range(value)
+
diff --git a/papillon/polls/views.py b/papillon/polls/views.py
new file mode 100644
index 0000000..e9ef572
--- /dev/null
+++ b/papillon/polls/views.py
@@ -0,0 +1,491 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (C) 2008 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# See the file COPYING for details.
+
+'''
+Views management
+'''
+
+from random import choice as random_choice
+import string
+import time
+from datetime import datetime
+
+from django.utils.translation import gettext_lazy as _
+from django.shortcuts import render_to_response
+from django.http import HttpResponseRedirect
+
+from papillon.settings import LANGUAGES, BASE_SITE
+from papillon.polls.models import Poll, PollUser, Choice, Voter, Vote, \
+ Category, Comment
+from papillon.polls.forms import CreatePollForm, AdminPollForm, ChoiceForm, \
+ DatedChoiceForm, CommentForm
+
+def getBaseResponse(request):
+ """Manage basic fields for the template
+ If not null the second argument returned is a redirection.
+ """
+ url = BASE_SITE
+ # setting the current language and available languages
+ if 'language' in request.GET:
+ if request.GET['language'] in [language[0] for language in LANGUAGES]:
+ request.session['django_language'] = request.GET['language']
+ return None, HttpResponseRedirect(request.path)
+ languages = []
+ for language_code, language_label in LANGUAGES:
+ languages.append((language_code, language_label))
+ return {'root_url':url, 'languages':languages}, None
+
+def index(request):
+ "Main page"
+ response_dct, redirect = getBaseResponse(request)
+ if redirect:
+ return redirect
+ response_dct['polls'] = Poll.objects.filter(public=True, category=None)
+ response_dct['categories'] = Category.objects.all()
+ error = ''
+ if 'bad_poll' in request.GET:
+ response_dct['error'] = _("The poll requested don't exist (anymore?)")
+ return render_to_response('main.html', response_dct)
+
+def category(request, category_id):
+ "Page for a category"
+ response_dct, redirect = getBaseResponse(request)
+ if redirect:
+ return redirect
+ category = Category.objects.get(id=int(category_id))
+ response_dct['category'] = category
+ response_dct['polls'] = Poll.objects.filter(public=True, category=category)
+ return render_to_response('category.html', response_dct)
+
+def create(request):
+ '''Creation of a poll.
+ '''
+ def genRandomURL():
+ "Generation of a random url"
+ url = ''
+ while not url or Poll.objects.filter(base_url=url).count() or\
+ Poll.objects.filter(admin_url=url).count():
+ url = ''
+ chars = string.letters + string.digits
+ for i in xrange(6):
+ url += random_choice(chars)
+ url += str(int(time.time()))
+ return url
+
+ response_dct, redirect = getBaseResponse(request)
+ if redirect:
+ return redirect
+
+ if request.method == 'POST':
+ form = CreatePollForm(request.POST)
+ if form.is_valid():
+ poll = form.save()
+ poll.admin_url = genRandomURL()
+ poll.base_url = genRandomURL()
+ poll.save()
+ return HttpResponseRedirect('%seditChoicesAdmin/%s/' % (
+ response_dct['root_url'], poll.admin_url))
+ else:
+ form = CreatePollForm()
+ response_dct['form'] = form
+ return render_to_response('create.html', response_dct)
+
+def edit(request, admin_url):
+ '''Edition of a poll.
+ '''
+ response_dct, redirect = getBaseResponse(request)
+ if redirect:
+ return redirect
+ try:
+ poll = Poll.objects.filter(admin_url=admin_url)[0]
+ except IndexError:
+ # if the poll don't exist redirect to the creation page
+ url = response_dct['root_url']
+ return HttpResponseRedirect('%screate' % (
+ response_dct['root_url']))
+ Form = AdminPollForm
+
+ if request.method == 'POST':
+ form = Form(request.POST, instance=poll)
+ if form.is_valid():
+ poll = form.save()
+ return HttpResponseRedirect('%sedit/%s/' % (
+ response_dct['root_url'], poll.admin_url))
+ else:
+ form = Form(instance=poll)
+ response_dct['form'] = form
+ response_dct['poll'] = poll
+ return render_to_response('edit.html', response_dct)
+
+def editChoicesAdmin(request, admin_url):
+ response_dct, redirect = getBaseResponse(request)
+ if redirect:
+ return redirect
+ try:
+ poll = Poll.objects.filter(admin_url=admin_url)[0]
+ except IndexError:
+ # if the poll don't exist redirect to the main page
+ url = "/".join(request.path.split('/')[:-2])
+ return response_dct, HttpResponseRedirect(url)
+ response_dct['poll'] = poll
+ return editChoices(request, response_dct, admin=True)
+
+def editChoicesUser(request, poll_url):
+ response_dct, redirect = getBaseResponse(request)
+ if redirect:
+ return redirect
+ try:
+ poll = Poll.objects.filter(base_url=poll_url)[0]
+ except IndexError:
+ poll = None
+ if not poll or not poll.opened_admin:
+ # if the poll don't exist redirect to the main page
+ url = "/".join(request.path.split('/')[:-2])
+ return HttpResponseRedirect(url)
+ response_dct['poll'] = poll
+ return editChoices(request, response_dct)
+
+def editChoices(request, response_dct, admin=False):
+ '''Edition of choices.
+ '''
+ poll = response_dct['poll']
+ tpl = 'editChoicesAdmin.html'
+ if not admin:
+ tpl = 'editChoicesUser.html'
+ Form = ChoiceForm
+ if poll.dated_choices:
+ Form = DatedChoiceForm
+ try:
+ order = Choice.objects.order_by('-order')[0].order
+ order += 1
+ except IndexError:
+ order = 0
+ form = Form(initial={'poll':poll.id, 'order':str(order)})
+
+ if request.method == 'POST':
+ # if a new choice is submitted
+ if 'add' in request.POST and request.POST['poll'] == str(poll.id):
+ f = Form(request.POST)
+ if f.is_valid():
+ choice = f.save()
+ poll.reorder()
+ else:
+ form = f
+ if admin and 'edit' in request.POST \
+ and request.POST['poll'] == str(poll.id):
+ try:
+ choice = Choice.objects.get(id=int(request.POST['edit']))
+ if choice.poll != poll:
+ raise ValueError
+ f = Form(request.POST, instance=choice)
+ if f.is_valid():
+ choice = f.save()
+ poll.reorder()
+ except (Choice.DoesNotExist, ValueError):
+ pass
+ if admin:
+ # check if a choice has been choosen for deletion
+ for key in request.POST:
+ if key.startswith('delete_') and request.POST[key]:
+ try:
+ choice = Choice.objects.get(id=int(key[len('delete_'):]))
+ if choice.poll != poll:
+ raise ValueError
+ Vote.objects.filter(choice=choice).delete()
+ choice.delete()
+ except (Choice.DoesNotExist, ValueError):
+ pass
+ # check if the order of a choice has to be changed
+ if admin and request.method == 'GET':
+ for key in request.GET:
+ try:
+ current_url = request.path.split('?')[0]
+ if 'up_choice' in key:
+ choice = Choice.objects.get(id=int(request.GET[key]))
+ if choice.poll != poll:
+ raise ValueError
+ choice.changeOrder(-1)
+ poll.reorder()
+ # redirect in order to avoid a change with a refresh
+ return HttpResponseRedirect(current_url)
+ if 'down_choice' in key:
+ choice = Choice.objects.get(id=int(request.GET[key]))
+ if choice.poll != poll:
+ raise ValueError
+ choice.changeOrder(1)
+ poll.reorder()
+ # redirect in order to avoid a change with a refresh
+ return HttpResponseRedirect(current_url)
+ except (ValueError, Choice.DoesNotExist):
+ pass
+ choices = Choice.objects.filter(poll=poll).order_by('order')
+ for choice in choices:
+ if admin and poll.dated_choices:
+ choice.name = choice.date
+ choice.form = Form(instance=choice)
+ response_dct['choices'] = choices
+ response_dct['form_new_choice'] = form
+ return render_to_response(tpl, response_dct)
+
+def poll(request, poll_url):
+ """Display a poll
+ poll_url is given to identify the poll. If '_' is in the poll_url the second
+ part of the url is the unix time given to highlight a particular vote
+ modification
+ """
+
+ def modifyVote(request, choices):
+ "Modify user's votes"
+ try:
+ voter = Voter.objects.filter(
+ id=int(request.POST['voter']))[0]
+ except (ValueError, IndexError):
+ return
+ # if no author_name is given deletion of associated votes and
+ # author
+ if not request.POST['author_name']:
+ # verify if the author can be deleted
+ delete_user = None
+ if not voter.user.password:
+ v = Voter.objects.filter(user=voter.user)
+ if len(v) == 1 and v[0] == voter:
+ delete_user = voter.user
+ for choice in choices:
+ v = Vote.objects.filter(voter=voter, choice=choice)
+ v.delete()
+ voter.delete()
+ if delete_user:
+ delete_user.delete()
+ return
+ # update the name
+ voter.user.name = request.POST['author_name']
+ voter.user.save()
+ # update the modification date
+ voter.save()
+ selected_choices = []
+ # set the selected choices
+ for key in request.POST:
+ # modify a one choice poll
+ if key == 'vote' and request.POST[key]:
+ try:
+ id = int(request.POST[key])
+ vote = Vote.objects.filter(id=id)[0]
+ if vote.choice not in choices:
+ # bad vote id : the associated choice has
+ # probably been deleted
+ vote.delete()
+ else:
+ vote.value = 1
+ vote.save()
+ selected_choices.append(vote.choice)
+ except (ValueError, IndexError):
+ # the vote don't exist anymore
+ pass
+ # modify an existing vote
+ if key.startswith('vote_') and request.POST[key]:
+ try:
+ id = int(key.split('_')[1])
+ vote = Vote.objects.filter(id=id)[0]
+ if vote.choice not in choices:
+ # bad vote id : the associated choice has
+ # probably been deleted
+ vote.delete()
+ else:
+ # try if a specific value is specified in the form
+ # like in balanced poll
+ try:
+ value = int(request.POST[key])
+ except ValueError:
+ value = 1
+ vote.value = value
+ vote.save()
+ selected_choices.append(vote.choice)
+ except (ValueError, IndexError):
+ # the vote don't exist anymore
+ pass
+ # update non selected choices
+ for choice in choices:
+ if choice not in selected_choices:
+ try:
+ v = Vote.objects.filter(voter=voter, choice=choice)[0]
+ v.value = 0
+ except IndexError:
+ # the vote don't exist with this choice : probably
+ # a new choice
+ v = Vote(voter=voter, choice=choice, value=0)
+ v.save()
+ def newComment(request, poll):
+ "Comment the poll"
+ if 'comment_author' not in request.POST \
+ or not request.POST['comment_author'] \
+ or not request.POST['comment']:
+ return
+ c = Comment(poll=poll, author_name=request.POST['comment_author'],
+ text=request.POST['comment'])
+ c.save()
+
+ def newVote(request, choices):
+ "Create new votes"
+ if not request.POST['author_name']:
+ return
+ author = PollUser(name=request.POST['author_name'])
+ author.save()
+ voter = Voter(user=author, poll=poll)
+ voter.save()
+ selected_choices = []
+
+ # set the selected choices
+ for key in request.POST:
+ # standard vote
+ if key.startswith('choice_') and request.POST[key]:
+ try:
+ id = int(key.split('_')[1])
+ choice = Choice.objects.filter(id=id)[0]
+ if choice not in choices:
+ raise ValueError
+ # try if a specific value is specified in the form
+ # like in balanced poll
+ try:
+ value = int(request.POST[key])
+ except ValueError:
+ value = 1
+ v = Vote(voter=voter, choice=choice, value=value)
+ v.save()
+ selected_choices.append(choice)
+ except (ValueError, IndexError):
+ # bad choice id : the choice has probably been deleted
+ pass
+ # one choice vote
+ if key == 'choice' and request.POST[key]:
+ try:
+ id = int(request.POST[key])
+ choice = Choice.objects.filter(id=id)[0]
+ if choice not in choices:
+ raise ValueError
+ v = Vote(voter=voter, choice=choice, value=1)
+ v.save()
+ selected_choices.append(choice)
+ except (ValueError, IndexError):
+ # bad choice id : the choice has probably been deleted
+ pass
+ # set non selected choices
+ for choice in choices:
+ if choice not in selected_choices:
+ v = Vote(voter=voter, choice=choice, value=0)
+ v.save()
+ # results can now be displayed
+ request.session['knowned_vote_' + poll.base_url] = 1
+ response_dct, redirect = getBaseResponse(request)
+ if redirect:
+ return redirect
+ highlight_vote_date = None
+ if '_' in poll_url:
+ url_spl = poll_url.split('_')
+ if len(url_spl) == 2:
+ poll_url, highlight_vote_date = url_spl
+ try:
+ highlight_vote_date = int(highlight_vote_date)
+ except ValueError:
+ highlight_vote_date = None
+ try:
+ poll = Poll.objects.filter(base_url=poll_url)[0]
+ except IndexError:
+ poll = None
+ choices = list(Choice.objects.filter(poll=poll))
+ # if the poll don't exist or if it has no choices the user is
+ # redirected to the main page
+ if not choices or not poll:
+ url = "/".join(request.path.split('/')[:-3])
+ url += "/?bad_poll=1"
+ return HttpResponseRedirect(url)
+
+ # a vote is submitted
+ if 'author_name' in request.POST and poll.open:
+ if 'voter' in request.POST:
+ # modification of an old vote
+ modifyVote(request, choices)
+ else:
+ newVote(request, choices)
+ # update the modification date of the poll
+ poll.save()
+ if 'comment' in request.POST and poll.open:
+ # comment posted
+ newComment(request, poll)
+
+ # 'voter' is in request.GET when the edit button is pushed
+ if 'voter' in request.GET and poll.open:
+ try:
+ response_dct['current_voter_id'] = int(request.GET['voter'])
+ except ValueError:
+ pass
+
+ response_dct.update({'poll':poll,
+ 'VOTE':Vote.VOTE,})
+ response_dct['base_url'] = "/".join(request.path.split('/')[:-2]) \
+ + '/%s/' % poll.base_url
+
+ # get voters and sum for each choice for this poll
+ voters = Voter.objects.filter(poll=poll)
+ choice_ids = [choice.id for choice in choices]
+ for voter in voters:
+ # highlight a voter
+ if time.mktime(voter.modification_date.timetuple()) \
+ == highlight_vote_date:
+ voter.highlight = True
+ voter.votes = voter.getVotes(choice_ids)
+ # initialize undefined vote
+ choice_vote_ids = [vote.choice.id for vote in voter.votes]
+ for choice in choices:
+ if choice.id not in choice_vote_ids:
+ vote = Vote(voter=voter, choice=choice, value=None)
+ vote.save()
+ idx = choices.index(choice)
+ voter.votes.insert(idx, vote)
+ sums = [choice.getSum(poll.type == 'B') for choice in choices]
+ vote_max = max(sums)
+ c_idx = 0
+ while c_idx < len(choices):
+ try:
+ c_idx = sums.index(vote_max, c_idx)
+ choices[c_idx].highlight = True
+ c_idx += 1
+ except ValueError:
+ c_idx = len(choices)
+ # set non-available choices if the limit is reached for a choice
+ response_dct['limit_set'] = None
+ for choice in choices:
+ if choice.limit:
+ response_dct['limit_set'] = True
+ if choice.limit and sums[choices.index(choice)] >= choice.limit:
+ choice.available = False
+ else:
+ choice.available = True
+ choice.save()
+ response_dct['voters'] = voters
+ response_dct['choices'] = choices
+ response_dct['comments'] = Comment.objects.filter(poll=poll)
+ # verify if vote's result has to be displayed
+ response_dct['hide_vote'] = poll.hide_choices
+ if poll.hide_choices:
+ if u'display_result' in request.GET:
+ request.session['knowned_vote_' + poll.base_url] = 1
+ if 'knowned_vote_' + poll.base_url in request.session:
+ response_dct['hide_vote'] = False
+ response_dct['form_comment'] = CommentForm()
+ return render_to_response('vote.html', response_dct)
diff --git a/papillon/settings.py b/papillon/settings.py
new file mode 100644
index 0000000..85ae8c8
--- /dev/null
+++ b/papillon/settings.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Django settings for papillon project.
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ROOT_PATH = '/var/local/django/papillon/'
+SERVER_URL = 'http://localhost:8000/'
+EXTRA_URL = 'papillon/'
+BASE_SITE = SERVER_URL + EXTRA_URL
+
+TINYMCE_URL = 'http://localhost/tinymce/'
+# time to live in days
+DAYS_TO_LIVE = 30
+
+ADMINS = (
+ # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
+DATABASE_NAME = ROOT_PATH + 'papillon.db' # Or path to database file if using sqlite3.
+DATABASE_USER = '' # Not used with sqlite3.
+DATABASE_PASSWORD = '' # Not used with sqlite3.
+DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
+# although not all variations may be possible on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'Europe/Paris'
+
+# Language code for this installation. All choices can be found here:
+# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
+# http://blogs.law.harvard.edu/tech/stories/storyReader$15
+LANGUAGE_CODE = 'fr-fr'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ROOT_PATH + 'static/'
+
+# URL that handles the media served from MEDIA_ROOT.
+# Example: "http://media.lawrence.com"
+MEDIA_URL = BASE_SITE + 'static/'
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = BASE_SITE + 'media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'replace_this_with_something_else'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.load_template_source',
+ 'django.template.loaders.app_directories.load_template_source',
+# 'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.middleware.doc.XViewMiddleware',
+)
+
+ROOT_URLCONF = 'papillon.urls'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+ ROOT_PATH + 'templates',
+)
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.admin',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.markup',
+ 'papillon.polls',
+)
+
+LANGUAGES = (
+ ('fr', 'Français'),
+ ('en', 'English'),
+)
diff --git a/papillon/static/bg.jpg b/papillon/static/bg.jpg
new file mode 100644
index 0000000..7653c63
--- /dev/null
+++ b/papillon/static/bg.jpg
Binary files differ
diff --git a/papillon/static/styles.css b/papillon/static/styles.css
new file mode 100644
index 0000000..c329905
--- /dev/null
+++ b/papillon/static/styles.css
@@ -0,0 +1,357 @@
+/*
+Copyright (C) 2008 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+See the file COPYING for details.
+*/
+
+body{
+margin:0;
+font-size:12px;
+font-family: Arial, Verdana, Geneva, "Bitstream Vera Sans", Helvetica, sans-serif;
+background-color:#ced3e1;
+}
+
+pre{
+font-size:12px;
+font-family: Arial, Verdana, Geneva, "Bitstream Vera Sans", Helvetica, sans-serif;
+}
+
+a{
+color:#6f819d;
+}
+
+h1, h2, h3, h4, h5 {
+font-family: Georgia, "Bitstream Vera Serif", "New York", Palatino, serif;
+font-weight:normal;
+}
+
+h1, h1 a{
+margin:0;
+margin-top:2px;
+border:1px solid black;
+border-right:None;
+border-left:None;
+background-color:#6f819d;
+color:white;
+padding-left:10px;
+font-size:32px;
+text-decoration:None;
+}
+
+h2{
+font-style:italic;
+border-bottom:1px dashed black;
+margin-left:10px;
+padding:0;
+font-size:24px;
+}
+
+h2 a{
+color:black;
+text-decoration:None;
+}
+
+h2 a:hover{
+color:#808080;
+}
+
+h3{
+margin:10px;
+font-size:20px;
+}
+
+h3 a{
+color:black;
+text-decoration:None;
+}
+
+h3 a:hover{
+color:#808080;
+}
+
+p{
+padding:6px;
+margin:6px;
+max-width:600px;
+}
+
+td{
+font-size:12px;
+padding:4px;
+}
+
+th{
+font-size:12px;
+text-align:left;
+}
+
+label{
+font-family: Georgia, "Bitstream Vera Serif", "New York", Palatino, serif;
+font-weight:normal;
+font-style:italic;
+}
+
+hr.spacer{
+clear:both;
+height:0;
+border:0;
+}
+
+#main{
+background-color:white;
+border:1px solid;
+margin:20px;
+background-image: url(bg.jpg);
+background-repeat:no-repeat;
+background-position:top right;
+}
+
+#header{
+text-align:right;
+font-size:11px;
+color:#808080;
+}
+
+#header #languages a{
+color:#808080;
+padding-right:6px;
+text-decoration:none;
+}
+
+#footer{
+text-align:center;
+font-size:11px;
+color:#808080;
+margin:6px;
+}
+
+#footer a{
+color:#808080;
+text-decoration:none;
+}
+
+.alert, .error, .errorlist{
+color:blue;
+}
+
+.new_poll{
+width:600px;
+}
+
+.new_poll input{
+width:160px;
+}
+
+.new_poll .submit{
+width:auto;
+}
+
+.new_poll input.limit{
+width:20px;
+}
+
+.new_poll textarea{
+width:160px;
+height:100px;
+}
+
+.datetime a img{
+border:0;
+}
+
+.datetime input.vTimeField{
+width:68px;
+}
+
+.datetime input.vDateField{
+width:75px;
+}
+
+.form_description{
+background-color:#6f819d;
+color:white;
+font-size:11px;
+width:200px;
+}
+
+a.arrow{
+text-decoration:None;
+font-weight:bold;
+}
+
+#content{
+margin:5px;
+}
+
+#poll_table{
+overflow:auto;
+overflow-y:visible;
+text-align:center;
+width:100%;
+padding-bottom:16px;
+display:block;
+float:left;
+}
+
+#poll{
+text-align:center;
+}
+
+#poll a{
+color:black;
+}
+
+#poll td{
+border:1px solid black;
+padding:0;
+}
+
+#poll td.simple{
+border:None;
+background-color:#FFF;
+}
+
+#poll th{
+background-color:#ced3e1;
+border:1px solid black;
+padding:5px;
+}
+
+#poll input{
+width:100px;
+}
+
+#poll .OK{
+background-color:#9ec5d5;
+}
+
+#poll .OKO{
+background-color:#b689d5;
+}
+
+#poll .KO{
+background-color:#b9b3bd;
+}
+
+#sum th{
+background-color:white;
+border:None;
+text-align:center;
+}
+
+#sum td{
+border:None;
+}
+
+.highlight{
+font-weight:bold;
+background-color:#ced3e1;
+}
+
+tr.highlighted_voter td{
+background-color:#808080;
+color:white;
+}
+
+.footnote{
+font-size:10px;
+padding:10px;
+}
+
+.footnote p{
+padding:0;
+margin:2px;
+}
+
+.poll-description{
+margin:4px;
+padding:4px;
+border:1px solid #d3d3d3;
+}
+
+.poll-description p{
+margin:0;
+padding:2px;
+}
+
+
+.comments ul{
+list-style-type:None;
+margin:4px;
+padding:0;
+}
+
+.comments li{
+margin:4px;
+padding:4px;
+border:1px solid #d3d3d3;
+}
+
+.comments .author{
+margin:0;
+color:#6f819d;
+padding:0;
+}
+
+.comments input{
+width:160px;
+}
+
+.comments textarea{
+width:160px;
+height:100px;
+}
+
+.comments #tdsubmit{
+text-align:center;
+}
+
+.comments .submit{
+width:auto;
+}
+
+
+/* CALENDARS & CLOCKS IMPORTED FROM ADMIN */
+
+.calendarbox, .clockbox { margin: 5px auto; font-size: 11px; width: 16em; text-align: center; background: white; position: relative; border: 1px solid #444; }
+.clockbox { width: auto; }
+.clockbox h2 { margin: 0; font-size: 13px; border-bottom: 1px solid #222; padding: 3px; background-color: #EEE; }
+
+.calendar { margin: 0; padding: 0; }
+.calendar table { margin: 0; padding: 0; border-collapse: collapse; background: white; width: 100%; }
+.calendar caption, .calendarbox h2 { margin: 0; font-size: 12px; text-align: center; border-top: none; background-color: #EEE; }
+.calendar caption { height: 18px; font-weight: bold; }
+.calendar th { font-size: 10px; color: #666; padding: 2px 3px; text-align: center; background: #e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x; border-bottom: 1px solid #ddd; }
+.calendar td { font-size: 11px; text-align: center; padding: 0; border-top: 1px solid #eee; border-bottom: none; }
+.calendar td.selected a { background: #C9DBED; }
+.calendar td.nonday { background: #efefef; }
+.calendar td.today a { background: #ffc; }
+.calendar td a, .timelist a { display: block; font-weight: bold; padding: 4px; text-decoration: none; color: #444; }
+.calendar td a:hover, .timelist a:hover { background: #4A0010; color: white; }
+.calendar td a:active, .timelist a:active { background: #CCC; color: white; }
+.calendarnav { font-size: 10px; text-align: center; color: #ccc; margin: 0; padding: 1px 3px; }
+.calendarnav a:link, #calendarnav a:visited, #calendarnav a:hover { color: #999; }
+.calendar-shortcuts { background: white; font-size: 10px; line-height: 11px; border-top: 1px solid #eee; padding: 3px 0 4px; color: #ccc; }
+.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { display: block; position: absolute; font-weight: bold; font-size: 12px; background: #AAA url(../img/admin/default-bg.gif) bottom left repeat-x; padding: 1px 4px 2px 4px; color: white; }
+.calendarnav-previous:hover, .calendarnav-next:hover { background: #4A0010; }
+.calendarnav-previous { top: 0; left: 0; }
+.calendarnav-next { top: 0; right: 0; }
+.calendar-cancel { margin: 0 !important; padding: 0; font-size: 10px; background: #e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x; border-top: 1px solid #ddd; }
+.calendar-cancel a { padding: 2px; color: #999; }
+
+ul.timelist, .timelist li { list-style-type: none; margin: 0; padding: 0; }
+.timelist a { padding: 2px; }
+
diff --git a/papillon/static/textareas.js b/papillon/static/textareas.js
new file mode 100644
index 0000000..fec83b8
--- /dev/null
+++ b/papillon/static/textareas.js
@@ -0,0 +1,27 @@
+/* base function shared by some pages */
+/* Copyright (C) 2009 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+See the file COPYING for details.
+*/
+
+tinyMCE.init({
+ mode : "textareas",
+ theme : "advanced",
+ relative_urls : false,
+ theme_advanced_buttons1 : "bold,italic,underline,strikethrough,separator,bullist,numlist,separator,hr,separator,link",
+ theme_advanced_buttons2 : "",
+ theme_advanced_buttons3 : ""
+});
diff --git a/papillon/templates/base.html b/papillon/templates/base.html
new file mode 100644
index 0000000..b673f24
--- /dev/null
+++ b/papillon/templates/base.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <link rel="stylesheet" href="{{root_url}}static/styles.css" />
+ <title>{% block title %}Papillon{% endblock %}</title>
+ {% block fullscript %}{% endblock %}
+</head>
+
+<body>
+<div id="main">
+<div id="header">
+<span id='languages'>{% for language in languages%}<a href='?language={{language.0}}'>{{language.1}}</a>{% endfor %}</span>
+</div>
+<div id="top">
+ <h1><a href='{{root_url}}'>Papillon</a></h1>
+</div>
+<div id="content">
+{% block content %}{% endblock %}
+</div>
+<div id="footer">
+<a href='http://blog.peacefrogs.net/nim/papillon/'>Papillon</a> - <a href='http://www.gnu.org/licenses/gpl.html'>Copyright</a> © 2008 <a href='http://redmine.peacefrogs.net/projects/show/papillon'>Papillon project</a>
+</div>
+</div>
+</body>
+</html>
+
diff --git a/papillon/templates/category.html b/papillon/templates/category.html
new file mode 100644
index 0000000..55aa084
--- /dev/null
+++ b/papillon/templates/category.html
@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+<h2>{{category.name}}</h2>
+<p>{{category.description}}</p>
+
+{% if polls %}<h2>{%trans "Polls"%}</h2>{%endif%}
+{% for poll in polls %}
+<div class='poll-description'>
+ <p><a href='{{root_url}}poll/{{poll.base_url}}'>{{poll.name}}</a></p>
+ <p>{{poll.description|safe}}</p>
+</div>
+{% endfor %}
+
+{% endblock %}
diff --git a/papillon/templates/create.html b/papillon/templates/create.html
new file mode 100644
index 0000000..7c3d2b3
--- /dev/null
+++ b/papillon/templates/create.html
@@ -0,0 +1,33 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load markup %}
+
+{% block fullscript %}
+ {{ form.media }}
+{% endblock %}
+
+
+{% block content %}
+ <h2>{% trans "New poll" %}</h2>
+<form action="" method="post">
+<table class='new_poll'>
+ {% for field in form %}
+ {% if field.is_hidden %}
+ {{field}}
+ {% else %}
+ <tr><td colspan='3'>{{field.errors}}</td></tr>
+ <tr>
+ <td>{{field.label_tag}}</td>
+ <td>{{field}}</td>
+ <td class='form_description'>{{field.help_text|markdown}}</td>
+ </tr>
+ {% endif %}
+ {% endfor %}
+ <tr>
+ <td></td>
+ <td><input type='submit' value='{% trans "Create" %}' class='submit'/></td>
+ </tr>
+</table>
+</form>
+
+{% endblock %}
diff --git a/papillon/templates/edit.html b/papillon/templates/edit.html
new file mode 100644
index 0000000..7ef4f14
--- /dev/null
+++ b/papillon/templates/edit.html
@@ -0,0 +1,62 @@
+{% extends "base.html" %}
+{% load markup %}
+{% load i18n %}
+
+{% block fullscript %}
+<script type="text/javascript" src="{{root_url}}admin/jsi18n/"></script>
+<script type="text/javascript" src="{{root_url}}media/js/core.js"></script>
+<script type="text/javascript" src="{{root_url}}media/js/admin/RelatedObjectLookups.js"></script>
+{{ form.media }}
+{% endblock %}
+
+{% block content %}
+ <h2>{% trans "Edit poll" %}</h2>
+<form action="" method="post">
+<table class='new_poll'>
+ <tr>
+ <td><label>{% trans "Poll url" %}</label></td>
+ <td>
+<a href='{{root_url}}poll/{{poll.base_url}}'>{{root_url}}poll/{{poll.base_url}}</a>
+ </td>
+ <td class='form_description'><p>
+{% trans "Copy this address and send it to voters who want to participate to this poll" %}
+ </p></td>
+ </tr>
+ <tr>
+ <td><label>{% trans "Administration url" %}</label></td>
+ <td>
+<a href='{{root_url}}edit/{{poll.admin_url}}'>{{root_url}}edit/{{poll.admin_url}}</a>
+ </td>
+ <td class='form_description'><p>
+ {% trans "Address to modify the current poll" %}
+ </p></td>
+ </tr>
+ <tr>
+ <td><label>{% trans "Choices administration url" %}</label></td>
+ <td>
+<a href='{{root_url}}editChoicesAdmin/{{poll.admin_url}}'>{{root_url}}editChoicesAdmin/{{poll.admin_url}}</a>
+ </td>
+ <td class='form_description'><p>
+ {% trans "Address to modify choices of the current poll." %}
+ </p></td>
+ </tr>
+ {% for field in form %}
+ {% if field.is_hidden %}
+ {{field}}
+ {% else %}
+ <tr><td colspan='3'>{{field.errors}}</td></tr>
+ <tr>
+ <td>{{field.label_tag}}</td>
+ <td>{{field}}</td>
+ {% if field.help_text %}<td class='form_description'>{{field.help_text|markdown}}</td>{%endif%}
+ </tr>
+ {% endif %}
+ {% endfor %}
+ <tr>
+ <td></td>
+ <td><input type='submit' value='{% trans "Edit" %}' class='submit'/></td>
+ </tr>
+</table>
+</form>
+
+{% endblock %}
diff --git a/papillon/templates/editChoices.html b/papillon/templates/editChoices.html
new file mode 100644
index 0000000..1082d30
--- /dev/null
+++ b/papillon/templates/editChoices.html
@@ -0,0 +1,19 @@
+{% load markup %}
+{% load i18n %}
+
+<h2>{% trans "New choice" %}</h2>
+{%if form_new_choice.errors %} <p class='error'>{{form_new_choice.errors}}</p>{%endif%}
+<form action="{{admin_url}}" method="post">
+{{form_new_choice.poll}}
+{{form_new_choice.order}}
+<table class='new_poll'>
+ <tr>
+ <td class='form_description' colspan='3'><p>{% trans "Setting a new choice. Optionally you can set a limit of vote for this choice. This limit is usefull for limited resources allocation." %}</p></td>
+ </tr>
+ <tr>
+ <td>{{form_new_choice.name}}</td>
+ <td>{%trans "Limited to"%} {{form_new_choice.limit}} {%trans "vote(s)"%}</td>
+ <td><input type='hidden' name='add' value='1'/> <input type='submit' value='{% trans "Add" %}' class='submit'/></td>
+ </tr>
+</table>
+</form>
diff --git a/papillon/templates/editChoicesAdmin.html b/papillon/templates/editChoicesAdmin.html
new file mode 100644
index 0000000..a7798bf
--- /dev/null
+++ b/papillon/templates/editChoicesAdmin.html
@@ -0,0 +1,44 @@
+{% extends "base.html" %}
+{% load markup %}
+{% load i18n %}
+
+{% block fullscript %}
+<script type="text/javascript" src="{{root_url}}admin/jsi18n/"></script>
+<script type="text/javascript" src="{{root_url}}media/js/core.js"></script>
+<script type="text/javascript" src="{{root_url}}media/js/admin/RelatedObjectLookups.js"></script>
+{{ form_new_choice.media }}
+{% endblock %}
+
+{% block content %}
+{% if not choices %}<p class='error'>
+{% blocktrans %}As long as no options were added to the poll, it will not be available.{% endblocktrans %}
+</p>{% else %}
+<h2>{% trans "Complete/Finalise the poll" %}</h2>
+<p><a href='{{root_url}}edit/{{poll.admin_url}}'><button>{% trans "Next"%}</button></p>
+{% endif %}
+{% include 'editChoices.html' %}
+{% if choices %}
+<h2>{% trans "Available choices" %}</h2>
+<table class='new_poll'>
+ <tr>
+ {%if not poll.dated_choices%}<th>{% trans "Up/down" %}</th>{%endif%}
+ <th>{% trans "Label" %}</th>
+ <th>{% trans "Limit" %}</th>
+ <th>{% trans "Delete?"%}</th>
+ </tr>
+ {% for choice in choices %}
+ <form action="" method="post">
+ {{choice.form.poll}}{{choice.form.order}}<tr>
+ {%if not poll.dated_choices%}<td><a href='?up_choice={{choice.id}}' class='arrow'>+</a>
+ / <a href='?down_choice={{choice.id}}' class='arrow'>-</a></td>{%endif%}
+ <td>{{choice.form.name}}</td>
+ <td>{% trans "Limited to"%} {{choice.form.limit}} {% trans "vote(s)" %}</td>
+ <td><input type='checkbox' name='delete_{{choice.id}}'/></td>
+ <td><input type='hidden' name='edit' value='{{choice.id}}'/></td>
+ <td><input type='submit' value='{% trans "Edit" %}' class='submit'/></td>
+ </tr>
+ </form>{% endfor %}
+</table>
+{% endif %}
+
+{% endblock %}
diff --git a/papillon/templates/editChoicesUser.html b/papillon/templates/editChoicesUser.html
new file mode 100644
index 0000000..0d2b2c1
--- /dev/null
+++ b/papillon/templates/editChoicesUser.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+{% load markup %}
+{% load i18n %}
+
+{% block fullscript %}
+<script type="text/javascript" src="{{root_url}}admin/jsi18n/"></script>
+<script type="text/javascript" src="{{root_url}}media/js/core.js"></script>
+<script type="text/javascript" src="{{root_url}}media/js/admin/RelatedObjectLookups.js"></script>
+{{ form_new_choice.media }}
+{% endblock %}
+
+{% block content %}
+ <p><a href="{{root_url}}poll/{{poll.base_url}}/">{%trans "Return to the poll"%}</a></p>
+<h2>{% trans "Choices" %}</h2>
+{% if choices %}<table class='new_poll'>
+ <tr>
+ <th>{% trans "Label" %}</th>
+ <th>{% trans "Limit" %}</th>
+ </tr>
+ {% for choice in choices %}<tr>
+ <td>{%if poll.dated_choices%}{{choice.date|date:_("DATETIME_FORMAT")}}{%else%}{{choice.name}}{%endif%}</td>
+ <td>{% if choice.limit %}{% trans "Limited to"%} {{choice.limit}} {% trans "vote(s)" %}{% endif %}</td>
+ </tr>{% endfor %}
+</table>
+{% endif %}
+{% include 'editChoices.html' %}
+{% endblock %}
diff --git a/papillon/templates/feeds/poll_description.html b/papillon/templates/feeds/poll_description.html
new file mode 100644
index 0000000..7522a5a
--- /dev/null
+++ b/papillon/templates/feeds/poll_description.html
@@ -0,0 +1,8 @@
+{% load i18n %}
+<p>{% blocktrans with obj.user.name as voter_name %}{{ voter_name }} has added/modified a vote.{%endblocktrans%}</p>
+<p>{% trans "Current results:" %}</p>
+<ul>
+{% for choice in obj.poll.getChoices %}
+ <li><strong>{{choice.name}}</strong>{% blocktrans count choice.getSum as sum %}: {{sum}} vote{%plural%}: {{sum}} votes{%endblocktrans%}</li>
+{% endfor %}
+</ul> \ No newline at end of file
diff --git a/papillon/templates/main.html b/papillon/templates/main.html
new file mode 100644
index 0000000..8a09830
--- /dev/null
+++ b/papillon/templates/main.html
@@ -0,0 +1,22 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+{% if error %}<p class='error'>{{error}}</p>{%endif%}
+<h2><a href='create'>{%trans "Create a poll"%}</a></h2>
+<p>{% trans "Create a new sondage for take a decision, find a date for a meeting, etc." %} <a href='create'>{% trans "It's here!" %}</a></p>
+
+{% if polls %}<h2>{%trans "Public polls"%}</h2>{%endif%}
+{% for poll in polls %}
+<div class='poll-description'>
+ <p><a href='poll/{{poll.base_url}}'>{{poll.name}}</a></p>
+ <p>{{poll.description}}</p>
+</div>
+{% endfor %}
+
+{% if categories %}<h2>{%trans "Categories"%}</h2>{% endif %}
+{% for category in categories %}
+<h3><a href='category/{{category.id}}'>{{category.name}}</a></h3>
+{% endfor %}
+
+{% endblock %}
diff --git a/papillon/templates/vote.html b/papillon/templates/vote.html
new file mode 100644
index 0000000..eb1ae21
--- /dev/null
+++ b/papillon/templates/vote.html
@@ -0,0 +1,148 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load get_range %}
+
+{% block fullscript %}
+<script type="text/javascript" src="{{root_url}}admin/jsi18n/"></script>
+<script type="text/javascript" src="{{root_url}}media/js/core.js"></script>
+<script type="text/javascript" src="{{root_url}}media/js/admin/RelatedObjectLookups.js"></script>
+{{ form_comment.media }}
+{% endblock %}
+
+{% block content %}
+ <h2>{%if poll.category %}{{poll.category.name}} - {%endif%}{{poll.name}}</h2>
+{% if error %}<p class='alert'>{{ error }}</p>{% endif %}
+{% if not poll.open %}<p class='alert'>{% trans "The current poll is closed."%}</p>{% endif %}
+ <p>{{ poll.description|safe }}</p>
+ <form method='post' action='{{base_url}}'>
+ <div id='poll_table'>
+ <table id='poll'>
+ <tr>
+ <td class='simple'></td>
+ <td class='simple'></td>
+ {% for choice in choices %}<th>{%if poll.dated_choices%}{{choice.date|date:_("DATETIME_FORMAT")}}{%else%}{{choice.name}}{%endif%}{% if choice.limit %} ({% trans "max" %} {{choice.limit}}){%endif%}</th>
+ {% endfor %}</tr>
+ {% if not hide_vote %}
+ {% for voter in voters %}<tr{% if voter.highlight %} class='highlighted_voter'{% endif %}>
+{% ifequal current_voter_id voter.id %}
+ <input type='hidden' name='voter' value='{{voter.id}}'/>
+ <td class='simple'></td>
+ <td><input type='text' name='author_name' value='{{voter.user.name}}'/></td>
+ {% for vote in voter.votes %}<td>
+ {% if vote.choice.available or vote.value %}
+ {% ifequal poll.type 'P' %}
+ <input type='checkbox' name='vote_{{vote.id}}'{%ifequal vote.value 1%} checked='checked'{%endifequal%}/>
+ {% endifequal %}
+ {% ifequal poll.type 'O' %}
+ <input type='radio' name='vote' value='{{vote.id}}' {%ifequal vote.value 1%} checked='checked'{%endifequal%}/>
+ {% endifequal %}
+ {% ifequal poll.type 'B' %}
+ <select name='vote_{{vote.id}}'>
+ {% for vote_choice in VOTE %}
+ <option value='{{vote_choice.0}}'{%ifequal vote.value vote_choice.0%} selected='selected'{%endifequal%}>{{vote_choice.1.1}}</option>
+ {% endfor %}
+ </select>
+ {% endifequal %}
+ {% ifequal poll.type 'V' %}
+ <select name='vote_{{vote.id}}'>
+ {% for vote_choice in 10|get_range %}
+ <option value='{{vote_choice}}'{%ifequal vote.value vote_choice%} selected='selected'{%endifequal%}>{{vote_choice}}</option>
+ {% endfor %}
+ </select>
+ {% endifequal %}
+ {% else %}
+ {% trans "Limit reached" %}
+ {% endif %}
+ </td>
+ {%endfor%}
+{%else%}
+ <td class='simple'>{% if poll.open %}<a href='?voter={{voter.id}}'>{% trans "Edit" %}</a>{%else%}&nbsp;{%endif%}</td>
+ <td>{{voter.user.name}}</td>
+ {% for vote in voter.votes %}
+ {% ifequal poll.type 'V' %}
+ <td class='{%ifequal vote.value 9%}OK{%else%}{%ifequal vote.value 0%}KO{%else%}OKO{%endifequal%}{%endifequal%}'>
+ {%if vote.value%}{{vote.value}}{%else%}0{%endif%}</td>
+ {% else %}
+ <td class='{%ifequal vote.value 1%}OK{%else%}{%ifequal vote.value 0%}OKO{%else%}KO{%endifequal%}{%endifequal%}'>
+ {%ifequal poll.type 'B'%}
+ {%for VOT in VOTE%}
+ {%ifequal VOT.0 vote.value%}{{VOT.1.1}}{%endifequal%}{%endfor%}
+ {%else%}
+ {%for VOT in VOTE%}
+ {%ifequal VOT.0 vote.value%}{{VOT.1.0}}{%endifequal%}{%endfor%}
+ {%endifequal%}
+ </td>
+ {% endifequal %}
+ {%endfor%}
+ {%endifequal%}
+ </tr>{%endfor%}
+ {%endif%}
+ {%if not current_voter_id%}{% if poll.open %}
+ <tr>
+ <td class='simple'></td>
+ <td><input type='text' name='author_name'/></td>
+ {%for choice in choices%}<td>
+ {% if choice.available %}
+ {% ifequal poll.type 'P' %}
+ <input type='checkbox' name='choice_{{choice.id}}'/>{% endifequal %}
+ {% ifequal poll.type 'O' %}
+ <input type='radio' name='choice' value='{{choice.id}}'/>{% endifequal %}
+ {% ifequal poll.type 'B' %}
+ <select name='choice_{{choice.id}}'>{% for vote_choice in VOTE %}
+ <option value='{{vote_choice.0}}'{%ifequal vote_choice.0 0%} selected='selected'{%endifequal%}>{{vote_choice.1.1}}</option>{% endfor %}
+ </select>
+ {% endifequal %}
+ {% ifequal poll.type 'V' %}
+ <select name='choice_{{choice.id}}'>
+ {% for vote_choice in 10|get_range %}
+ <option value='{{vote_choice}}'>{{vote_choice}}</option>
+ {% endfor %}
+ </select>
+ {% endifequal %}
+ {% else %}
+ {% trans "Limit reached" %}
+ {% endif %}
+ </td>{%endfor%}
+ </tr>
+ {%endif%}{%endif%}
+ {% if not hide_vote %}<tr id='sum'>
+ <td class='simple'></td><th>{% trans "Sum" %}</th>
+ {% for choice in choices %}<td{%if choice.highlight %} class='highlight'{%endif%}>{{choice.getSum}}</td>
+ {% endfor %}
+ </tr>{%endif%}
+ {% if poll.open %}
+ <td class='simple'></td>
+ <td class='simple'><input type='submit' value='{%if current_voter_id%}{% trans "Edit" %}{%else%}{% trans "Participate" %}{%endif%}' class='submit'/></td>
+ {% endif %}
+ </table>
+ </div>
+ <hr class='spacer'/>
+ </form>
+ {%if poll.opened_admin%}
+ <p><a href="{{root_url}}editChoicesUser/{{poll.base_url}}/">{%trans "Add a new choice to this poll?"%}</a></p>{%endif%}
+ <div class='footnote'>
+ {%if hide_vote%}<p>{% trans "You have already vote? You are enough wise not to be influenced by other votes? You can display result by clicking" %} <a href='?display_result=1'>{% trans "here" %}</a>.</p>{%else%}
+ <p>{% trans "Remain informed of poll evolution:" %} <a href="{{root_url}}feeds/poll/{{poll.base_url}}/">{%trans "syndication"%}</a></p>{%endif%}
+ </div>
+{%if not hide_vote%}
+<h3>{%trans "Comments"%}</h3>
+<div class='comments'>
+ {%if poll.open%}<form method='post' action='{{base_url}}'>
+ <table class='comment'>
+ <tr>
+ <td><label for='comment_author'>{% trans "Author name" %}</label></td>
+ <td><input type='text' id='comment_author' name='comment_author'/></td>
+ </tr>
+ <tr>
+ <td><label for='comment'>{% trans "Comment"%}</label></td>
+ <td><textarea id='comment' name='comment' cols='' rows=''></textarea></td>
+ </tr>
+ <tr><td colspan='2' id='tdsubmit'><input type='submit' class='submit' value='{% trans "Send" %}'/></td></tr>
+ </table>
+</form>{%endif%}
+ <ul>{%for comment in comments%}
+ <li><p class='author'>{{comment.author_name}}, {{comment.date|date:_("DATETIME_FORMAT")}} :</p>
+ {{comment.text|safe}}</li>{%endfor%}
+ </ul>
+</div>{%endif%}
+{% endblock %}
diff --git a/papillon/urls.py b/papillon/urls.py
new file mode 100644
index 0000000..1e35a7b
--- /dev/null
+++ b/papillon/urls.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (C) 2008 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# See the file COPYING for details.
+
+from django.conf.urls.defaults import *
+from django.contrib import admin
+admin.autodiscover()
+
+from polls.feeds import PollLatestEntries
+
+feeds = {
+ 'poll': PollLatestEntries,
+}
+
+urlpatterns = patterns('',
+ (r'^papillon/admin/doc/', include('django.contrib.admindocs.urls')),
+ (r'^papillon/admin/jsi18n/$', 'django.views.i18n.javascript_catalog'),
+ (r'^papillon/admin/(.*)', admin.site.root),
+ (r'^papillon/$', 'papillon.polls.views.index'),
+ (r'^papillon/create$', 'papillon.polls.views.create'),
+ (r'^papillon/edit/(?P<admin_url>\w+)/$',
+ 'papillon.polls.views.edit'),
+ (r'^papillon/editChoicesAdmin/(?P<admin_url>\w+)/$',
+ 'papillon.polls.views.editChoicesAdmin'),
+ (r'^papillon/editChoicesUser/(?P<poll_url>\w+)/$',
+ 'papillon.polls.views.editChoicesUser'),
+ (r'^papillon/category/(?P<category_id>\w+)/$',
+ 'papillon.polls.views.category'),
+ (r'^papillon/poll/(?P<poll_url>\w+)/$', 'papillon.polls.views.poll'),
+ (r'^papillon/poll/(?P<poll_url>\w+)/vote$', 'papillon.polls.views.poll'),
+ (r'^papillon/feeds/(?P<url>.*)$',
+ 'django.contrib.syndication.views.feed', {'feed_dict': feeds}),
+ (r'^papillon/static/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': 'static/'}),
+ (r'^papillon/media/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': 'media/'}),
+ (r'^papillon/tinymce/', include('tinymce.urls')),
+)