Merge branch 'shop'
authorRadek Czajka <radekczajka@nowoczesnapolska.org.pl>
Thu, 31 Oct 2013 10:35:36 +0000 (11:35 +0100)
committerRadek Czajka <radekczajka@nowoczesnapolska.org.pl>
Thu, 31 Oct 2013 10:35:36 +0000 (11:35 +0100)
34 files changed:
prawokultury/locale/pl/LC_MESSAGES/django.mo
prawokultury/locale/pl/LC_MESSAGES/django.po
prawokultury/middleware.py [new file with mode: 0755]
prawokultury/settings.d/30-apps.py
prawokultury/settings.d/40-middleware.py
prawokultury/settings.d/50-contrib.py
prawokultury/settings.d/50-static.py
prawokultury/static/css/forms.css
prawokultury/static/css/forms.scss
prawokultury/static/img/payu.png [new file with mode: 0644]
prawokultury/templates/migdal/entry/publications/entry_detail.html [new file with mode: 0755]
prawokultury/urls.py
requirements.txt
shop/__init__.py [new file with mode: 0644]
shop/admin.py [new file with mode: 0644]
shop/forms.py [new file with mode: 0644]
shop/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
shop/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
shop/migrations/0001_initial.py [new file with mode: 0644]
shop/migrations/__init__.py [new file with mode: 0644]
shop/models.py [new file with mode: 0644]
shop/static/shop/shop.js [new file with mode: 0755]
shop/templates/shop/email/base.txt [new file with mode: 0755]
shop/templates/shop/email/payed.txt [new file with mode: 0644]
shop/templates/shop/email/payed_managers.txt [new file with mode: 0755]
shop/templates/shop/no_thanks.html [new file with mode: 0644]
shop/templates/shop/offer_detail.html [new file with mode: 0644]
shop/templates/shop/snippets/order_form.html [new file with mode: 0755]
shop/templates/shop/thanks.html [new file with mode: 0644]
shop/templatetags/__init__.py [new file with mode: 0755]
shop/templatetags/shop_tags.py [new file with mode: 0755]
shop/urls.py [new file with mode: 0644]
shop/views.py [new file with mode: 0644]
shop/widgets.py [new file with mode: 0755]

index 5859207..41f6877 100644 (file)
Binary files a/prawokultury/locale/pl/LC_MESSAGES/django.mo and b/prawokultury/locale/pl/LC_MESSAGES/django.mo differ
index 261bf54..ff1d3f9 100644 (file)
@@ -7,16 +7,16 @@ msgid ""
 msgstr ""
 "Project-Id-Version: prawokultury\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-05-17 10:41+0200\n"
-"PO-Revision-Date: 2013-01-14 16:09+0100\n"
+"POT-Creation-Date: 2013-10-31 11:29+0100\n"
+"PO-Revision-Date: 2013-10-31 11:30+0100\n"
 "Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
 "Language-Team: FNP <fundacja@nowoczesnapolska.org.pl>\n"
-"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
-"|| n%100>=20) ? 1 : 2)\n"
+"|| n%100>=20) ? 1 : 2);\n"
+"X-Generator: Poedit 1.5.4\n"
 
 #: menu_items.py:15
 msgid "Publications"
@@ -38,10 +38,14 @@ msgstr "Pierwsza pomoc"
 msgid "Guide"
 msgstr "Przewodnik"
 
-#: urls.py:42
+#: urls.py:47
 msgid "events"
 msgstr "wydarzenia"
 
+#: urls.py:50
+msgid "shop"
+msgstr "sklep"
+
 #: settings.d/60-custom.py:5
 msgid "news"
 msgstr "newsy"
@@ -62,44 +66,44 @@ msgstr "Strona nie znaleziona"
 msgid "The page you were looking for doesn't exist."
 msgstr "Strona, której szukasz, nie istnieje."
 
-#: templates/base.html:8 templates/base.html.py:14 templates/base.html:15
-#: templates/base.html.py:27 templates/base.html:79
+#: templates/base.html:9 templates/base.html.py:15 templates/base.html:16
+#: templates/base.html.py:28 templates/base.html:80
 msgid "Right to Culture"
 msgstr "Prawo kultury"
 
-#: templates/base.html:32
+#: templates/base.html:33
 msgid "Organizer"
 msgstr "Organizator"
 
-#: templates/base.html:35
+#: templates/base.html:36
 msgid "Modern Poland Foundation"
 msgstr "Fundacja Nowoczesna Polska"
 
-#: templates/base.html:37
+#: templates/base.html:38
 msgid "Sponsor"
 msgstr "Wspierane przez"
 
-#: templates/base.html:40
+#: templates/base.html:41
 msgid "Trust for Civil Society in Central and Eastern Europe"
 msgstr ""
 
-#: templates/base.html:49
+#: templates/base.html:50
 msgid "Search"
 msgstr "Szukaj"
 
-#: templates/base.html:65
+#: templates/base.html:66
 msgid "Upcoming events"
 msgstr "Nadchodzące wydarzenia"
 
-#: templates/base.html:69
+#: templates/base.html:70
 msgid "Ask a lawyer"
 msgstr "Zapytaj prawnika"
 
-#: templates/base.html:83
+#: templates/base.html:84
 msgid "Latest comments"
 msgstr "Ostatnie komentarze"
 
-#: templates/base.html:112
+#: templates/base.html:113
 msgid ""
 "If not explicitly stated otherwise, all texts are licensed under the <a "
 "href='http://creativecommons.org/licenses/by-sa/3.0/'>Creative Commons "
@@ -109,6 +113,14 @@ msgstr ""
 "href='http://creativecommons.org/licenses/by-sa/3.0/deed.pl'>Creative "
 "Commons Uznanie autorstwa – Na tych samych warunkach</a>."
 
+#: templates/migdal/entry/publications/entry_detail.html:10
+msgid "This entry hasn't been published yet."
+msgstr "Ten wpis nie został jeszcze opublikowany."
+
+#: templates/migdal/entry/publications/entry_detail.html:28
+msgid "Price"
+msgstr "Cena"
+
 #~ msgid "First aid in copyright"
 #~ msgstr "Pierwsza pomoc w prawie autorskim"
 
diff --git a/prawokultury/middleware.py b/prawokultury/middleware.py
new file mode 100755 (executable)
index 0000000..0911a69
--- /dev/null
@@ -0,0 +1,12 @@
+from honeypot.middleware import HoneypotViewMiddleware
+
+def honeypot_exempt(view):
+    view.honeypot_exempt = True
+    return view
+
+class ExemptableHoneypotViewMiddleware(HoneypotViewMiddleware):
+    def process_view(self, request, callback, callback_args, callback_kwargs):
+        if hasattr(callback, 'honeypot_exempt'):
+            return None
+        return super(ExemptableHoneypotViewMiddleware, self).process_view(
+            request, callback, callback_args, callback_kwargs)
index 1877989..1e87f11 100644 (file)
@@ -5,6 +5,7 @@ INSTALLED_APPS = (
     'events',
     'migdal',
     'questions',
+    'shop',
 
     'gravatar',
     'south',
@@ -19,6 +20,11 @@ INSTALLED_APPS = (
     'honeypot',
     'taggit',
     'taggit_autosuggest',
+    'getpaid',
+    'getpaid.backends.payu',
+    'djcelery',
+    'djkombu',
+
 
     'django.contrib.auth',
     'django.contrib.contenttypes',
index c8a63b6..f0229cd 100644 (file)
@@ -5,7 +5,8 @@ MIDDLEWARE_CLASSES = (
 
     'django.middleware.common.CommonMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
-    'honeypot.middleware.HoneypotMiddleware',
+    'prawokultury.middleware.ExemptableHoneypotViewMiddleware',
+    'honeypot.middleware.HoneypotResponseMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
 )
 
index 4423184..4625e79 100644 (file)
@@ -19,3 +19,21 @@ CAS_VERSION = '1'
 HONEYPOT_FIELD_NAME='miut'
 
 TAGGIT_AUTOSUGGEST_MODEL = ('questions', 'Tag')
+
+GETPAID_BACKENDS = (
+    'getpaid.backends.payu',
+)
+
+
+import djcelery
+djcelery.setup_loader()
+
+BROKER_BACKEND = "djkombu.transport.DatabaseTransport"
+BROKER_HOST = "localhost"
+BROKER_PORT = 5672
+BROKER_USER = "guest"
+BROKER_PASSWORD = "guest"
+BROKER_VHOST = "/"
+
+CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
+CELERY_SEND_TASK_ERROR_EMAILS = True
index 8f514da..f43274b 100644 (file)
@@ -42,6 +42,7 @@ PIPELINE_JS = {
     'base': {
         'source_filenames': (
             'js/promobox.js',
+            'shop/shop.js',
 
         ),
         'output_filename': 'compressed/base.js',
index 4acb30f..70930a0 100644 (file)
@@ -1,10 +1,13 @@
+.entry-wrapped .submit-form {
+  font-size: .9em; }
+
 .form-info {
   font-size: 1.2em; }
 
 .submit-form {
   margin-top: 3em; }
   .submit-form table {
-    border-spacing: 0 .5em; }
+    border-spacing: 0 0.5em; }
   .submit-form tr {
     background-color: #fdfdfd; }
   .submit-form label {
@@ -14,7 +17,7 @@
     color: #e41b13;
     font-size: 1.2em;
     padding: 0 0 0 1.3em;
-    margin: 0 0 .5em 0;
+    margin: 0 0 0.5em 0;
     list-style: url("/static/img/read-more.png"); }
   .submit-form input, .submit-form textarea, .submit-form select {
     font-size: 1.2em;
@@ -23,7 +26,7 @@
     color: #363A3B;
     width: 100%;
     margin-bottom: .5em;
-    border: 1px solid #EDECE7; }
+    border: 1px solid #edece7; }
   .submit-form th, .submit-form td {
     text-align: left;
     font-weight: normal;
index 9539415..1f3488a 100644 (file)
@@ -1,3 +1,7 @@
+.entry-wrapped .submit-form {
+    font-size: .9em;
+}
+
 .form-info {
     font-size: 1.2em;
 }
diff --git a/prawokultury/static/img/payu.png b/prawokultury/static/img/payu.png
new file mode 100644 (file)
index 0000000..2e8abf4
Binary files /dev/null and b/prawokultury/static/img/payu.png differ
diff --git a/prawokultury/templates/migdal/entry/publications/entry_detail.html b/prawokultury/templates/migdal/entry/publications/entry_detail.html
new file mode 100755 (executable)
index 0000000..b231954
--- /dev/null
@@ -0,0 +1,68 @@
+{% extends "base.html" %}
+{% load comments i18n %}
+{% load fnp_common migdal_tags fnp_share shop_tags %}
+{% load url from future %}
+
+
+{% block "body" %}
+
+{% if not entry.published %}
+    <p class="warning">{% trans "This entry hasn't been published yet." %}</p>
+{% endif %}
+
+<div class="entry entry-detail entry-{{ entry.type }}">
+<div class="entry-wrapped">
+
+{% entry_begin entry 1 %}
+<div class="body">
+
+{% if entry.offer %}
+<div style="float:left;clear:left;">
+<a href="{% url 'migdal_entry_info' 'regulamin-sklepu' %}">Regulamin sklepu</a>
+</div>
+{% endif %}
+
+
+{% if entry.offer %}
+
+<p>{% trans "Price" %}: {{ entry.offer.price|floatformat:"-2" }} PLN</p>
+
+
+{% order_form_for entry.offer form %}
+
+{% endif %}
+
+
+{{ entry.body }}
+
+
+
+
+{% for inline_html in entry.inline_html %}
+<div class="inline_html">
+    {{ inline_html|safe }}
+</div>
+{% endfor %}
+
+</div>
+
+<div style="clear: both;"></div>
+
+<div class="toolbar">
+<div class="social">
+    {% share object.get_absolute_url object.title %}
+</div>
+</div>
+
+<div style="clear: both"></div>
+
+{% if entry.get_type.commentable %}
+    {% render_comment_list for entry %}
+    <div class="comments">
+    {% entry_comment_form entry %}
+    </div>
+{% endif %}
+</div>
+</div>
+{% endblock %}
+
index e167926..848d885 100644 (file)
@@ -47,6 +47,7 @@ urlpatterns += i18n_patterns('',
     url(string_concat(r'^', _('events'), r'/'), include('events.urls')),
     url(r'^comments/', include('django_comments_xtd.urls')),
     url(r'^pierwsza-pomoc/', include('questions.urls')),
+    url(string_concat(r'^', _('shop'), r'/'), include('shop.urls')),
 ) + migdal_urlpatterns 
 
 if settings.DEBUG:
index e962daf..c0f07de 100644 (file)
@@ -27,3 +27,7 @@ piwik
 
 django-taggit
 django-taggit-autosuggest
+
+django-getpaid>=1.4,<1.5
+django-celery>=3.0.11
+django-kombu
diff --git a/shop/__init__.py b/shop/__init__.py
new file mode 100644 (file)
index 0000000..ccd7a85
--- /dev/null
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# This file is part of PrawoKultury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf import settings as settings
+from fnpdjango.utils.app import AppSettings
+
+
+class Settings(AppSettings):
+    """Default settings for funding app."""
+    DEFAULT_LANGUAGE = u'pl'
+
+
+app_settings = Settings('SHOP')
diff --git a/shop/admin.py b/shop/admin.py
new file mode 100644 (file)
index 0000000..58fd842
--- /dev/null
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# This file is part of PrawoKultury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.utils.translation import ugettext_lazy as _
+from django.contrib import admin
+from .models import Offer, Order
+
+
+class OfferAdmin(admin.ModelAdmin):
+    model = Offer
+    list_display = ['entry', 'price']
+
+
+class PayedFilter(admin.SimpleListFilter):
+    title = _('payment complete')
+    parameter_name = 'payed'
+    def lookups(self, request, model_admin):
+        return (
+            ('yes', _('Yes')),
+            ('no', _('No')),
+        )
+    def queryset(self, request, queryset):
+        if self.value() == 'yes':
+            return queryset.exclude(payed_at=None)
+        elif self.value() == 'no':
+            return queryset.filter(payed_at=None)
+
+class OrderAdmin(admin.ModelAdmin):
+    model = Order
+    list_display = ['payed_at', 'offer', 'name', 'email']
+    search_fields = ['name', 'email', 'offer']
+    list_filter = [PayedFilter, 'offer']
+
+admin.site.register(Offer, OfferAdmin)
+admin.site.register(Order, OrderAdmin)
diff --git a/shop/forms.py b/shop/forms.py
new file mode 100644 (file)
index 0000000..fedd152
--- /dev/null
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# This file is part of PrawoKultury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django import forms
+from django.utils import formats
+from django.utils.translation import ugettext_lazy as _, ugettext, get_language
+from . import app_settings
+from .models import Order
+from .widgets import NumberInput
+
+
+class OrderForm(forms.Form):
+    required_css_class = 'required'
+    backend = 'getpaid.backends.payu'
+    items = forms.IntegerField(label=_("Items"), min_value=1, initial=1,
+        widget=NumberInput(attrs={'min': '1', 'step': '1', 'class': 'cost-items'}))
+    name = forms.CharField(label=_("Name"))
+    email = forms.EmailField(label=_("Contact e-mail"))
+    address = forms.CharField(label=_("Address"), widget=forms.Textarea)
+
+    accept = forms.BooleanField(label=_("Accept terms"),
+        help_text='''Akceptuję <a href='/info/regulamin-sklepu/'>regulamin sklepu</a>.''')
+
+    consent = forms.BooleanField(label=_("Consent to the processing of data"),
+        help_text='''Wyrażam zgodę na przetwarzanie moich danych osobowych w celu realizacji
+zamówienia. Administratorem danych osobowych jest Fundacja Nowoczesna
+Polska, ul. Marszałkowska 84/92, lok. 125, 00-514 Warszawa.
+Zapoznałem/zapoznałam się
+z&nbsp;<a href="http://nowoczesnapolska.org.pl/prywatnosc/">polityką prywatności Fundacji</a>.
+Jestem świadom/świadoma, iż moja zgoda może być odwołana w każdym czasie, co skutkować będzie
+usunięciem mojego adresu e-mail z bazy danych.''')
+
+    def __init__(self, offer, *args, **kwargs):
+        self.offer = offer
+        super(OrderForm, self).__init__(*args, **kwargs)
+        self.fields['items'].widget.attrs.update({
+            'data-cost-price': self.offer.price,
+            'data-cost-per-item': self.offer.cost_per_item,
+            'data-cost-const': self.offer.cost_const,
+            'data-decimal-separator': formats.get_format("DECIMAL_SEPARATOR"),
+            })
+
+    def save(self):
+        order = Order.objects.create(
+            offer=self.offer,
+            items=self.cleaned_data['items'],
+            name=self.cleaned_data['name'],
+            email=self.cleaned_data['email'],
+            address=self.cleaned_data['address'],
+            language_code = get_language(),
+        )
+        return order
+
diff --git a/shop/locale/pl/LC_MESSAGES/django.mo b/shop/locale/pl/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..24fe2e9
Binary files /dev/null and b/shop/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/shop/locale/pl/LC_MESSAGES/django.po b/shop/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..b523584
--- /dev/null
@@ -0,0 +1,143 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-10-31 11:13+0100\n"
+"PO-Revision-Date: 2013-10-31 11:25+0100\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+"X-Generator: Poedit 1.5.4\n"
+
+#: admin.py:16
+msgid "payment complete"
+msgstr "płatność wykonana"
+
+#: admin.py:20
+msgid "Yes"
+msgstr "Tak"
+
+#: admin.py:21
+msgid "No"
+msgstr "Nie"
+
+#: forms.py:16
+msgid "Items"
+msgstr "Liczba egzemplarzy"
+
+#: forms.py:18
+msgid "Name"
+msgstr "Imię i nazwisko"
+
+#: forms.py:19
+msgid "Contact e-mail"
+msgstr "E-mail kontaktowy"
+
+#: forms.py:20
+msgid "Address"
+msgstr "Adres"
+
+#: forms.py:22
+msgid "Accept terms"
+msgstr "Akceptacja regulaminu"
+
+#: forms.py:25
+msgid "Consent to the processing of data"
+msgstr "Zgoda na przetwarzanie danych"
+
+#: models.py:20
+msgid "price"
+msgstr "cena"
+
+#: models.py:25 models.py:45
+msgid "offer"
+msgstr "oferta"
+
+#: models.py:26
+msgid "offers"
+msgstr "oferty"
+
+#: models.py:46
+msgid "items"
+msgstr "liczba egzemplarzy"
+
+#: models.py:47
+msgid "name"
+msgstr "nazwisko"
+
+#: models.py:48
+msgid "email"
+msgstr "e-mail"
+
+#: models.py:49
+msgid "address"
+msgstr "adres"
+
+#: models.py:50
+msgid "payed at"
+msgstr "zapłacono"
+
+#: models.py:54
+msgid "order"
+msgstr "zamówienie"
+
+#: models.py:55
+msgid "orders"
+msgstr "zamówienie"
+
+#: models.py:113
+msgid "Your payment has been completed."
+msgstr "Twoja płatność została wykonana."
+
+#: models.py:117
+msgid "New order has been placed."
+msgstr "Zostało złożone nowe zamówienie."
+
+#: templates/shop/no_thanks.html:5 templates/shop/no_thanks.html.py:9
+msgid "Payment failed"
+msgstr "Płatność zakończona niepowodzeniem"
+
+#: templates/shop/no_thanks.html:13
+msgid "You're support has not been processed successfully."
+msgstr "Twoja płatność nie została zakończona powodzeniem."
+
+#: templates/shop/no_thanks.html:15 templates/shop/thanks.html:15
+msgid "Go back to:"
+msgstr "Wróć do:"
+
+#: templates/shop/thanks.html:5 templates/shop/thanks.html.py:9
+msgid "Payment successful"
+msgstr "Płatność wykonana."
+
+#: templates/shop/thanks.html:13 templates/shop/email/payed.txt:6
+msgid "Your payment has been successfully completed."
+msgstr "Twoja płatność została wykonana."
+
+#: templates/shop/email/base.txt:1
+msgid "Hi"
+msgstr "Cześć"
+
+#: templates/shop/email/base.txt:4
+msgid ""
+"Cheers,\n"
+"Right to Culture team"
+msgstr ""
+"Pozdrowienia,\n"
+"Zespół Prawa Kultury"
+
+#: templates/shop/email/payed.txt:8
+msgid "Your order is now being processed: "
+msgstr "Twoje zamówienie jest przetwarzane:"
+
+#: templates/shop/snippets/order_form.html:18
+msgid "Donate!"
+msgstr "Wesprzyj!"
diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..471a911
--- /dev/null
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'Offer'
+        db.create_table('shop_offer', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('entry', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['migdal.Entry'], unique=True)),
+            ('price', self.gf('django.db.models.fields.DecimalField')(max_digits=6, decimal_places=2)),
+            ('cost_const', self.gf('django.db.models.fields.DecimalField')(max_digits=6, decimal_places=2)),
+            ('cost_per_item', self.gf('django.db.models.fields.DecimalField')(default=0, max_digits=6, decimal_places=2)),
+        ))
+        db.send_create_signal('shop', ['Offer'])
+
+        # Adding model 'Order'
+        db.create_table('shop_order', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('offer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['shop.Offer'])),
+            ('items', self.gf('django.db.models.fields.IntegerField')(default=1)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=127, blank=True)),
+            ('email', self.gf('django.db.models.fields.EmailField')(max_length=75, db_index=True)),
+            ('address', self.gf('django.db.models.fields.TextField')(db_index=True)),
+            ('payed_at', self.gf('django.db.models.fields.DateTimeField')(db_index=True, null=True, blank=True)),
+            ('language_code', self.gf('django.db.models.fields.CharField')(max_length=2, null=True, blank=True)),
+        ))
+        db.send_create_signal('shop', ['Order'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'Offer'
+        db.delete_table('shop_offer')
+
+        # Deleting model 'Order'
+        db.delete_table('shop_order')
+
+
+    models = {
+        'migdal.category': {
+            'Meta': {'object_name': 'Category'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'slug_en': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}),
+            'slug_pl': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}),
+            'taxonomy': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'title_en': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64', 'db_index': 'True'}),
+            'title_pl': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64', 'db_index': 'True'})
+        },
+        'migdal.entry': {
+            'Meta': {'ordering': "['-date']", 'object_name': 'Entry'},
+            '_body_en_rendered': ('django.db.models.fields.TextField', [], {}),
+            '_body_pl_rendered': ('django.db.models.fields.TextField', [], {}),
+            '_lead_en_rendered': ('django.db.models.fields.TextField', [], {}),
+            '_lead_pl_rendered': ('django.db.models.fields.TextField', [], {}),
+            'author': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'author_email': ('django.db.models.fields.EmailField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'body_en': ('markupfield.fields.MarkupField', [], {'null': 'True', 'rendered_field': 'True', 'blank': 'True'}),
+            'body_en_markup_type': ('django.db.models.fields.CharField', [], {'default': "'textile_pl'", 'max_length': '30', 'blank': 'True'}),
+            'body_pl': ('markupfield.fields.MarkupField', [], {'null': 'True', 'rendered_field': 'True', 'blank': 'True'}),
+            'body_pl_markup_type': ('django.db.models.fields.CharField', [], {'default': "'textile_pl'", 'max_length': '30', 'blank': 'True'}),
+            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['migdal.Category']", 'null': 'True', 'blank': 'True'}),
+            'changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'first_published_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+            'in_stream': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'lead_en': ('markupfield.fields.MarkupField', [], {'null': 'True', 'rendered_field': 'True', 'blank': 'True'}),
+            'lead_en_markup_type': ('django.db.models.fields.CharField', [], {'default': "'textile_pl'", 'max_length': '30', 'blank': 'True'}),
+            'lead_pl': ('markupfield.fields.MarkupField', [], {'null': 'True', 'rendered_field': 'True', 'blank': 'True'}),
+            'lead_pl_markup_type': ('django.db.models.fields.CharField', [], {'default': "'textile_pl'", 'max_length': '30', 'blank': 'True'}),
+            'needed_en': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1', 'db_index': 'True'}),
+            'promo': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'published_at_en': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'published_at_pl': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'published_en': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'published_pl': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'slug_en': ('migdal.fields.SlugNullField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+            'slug_pl': ('migdal.fields.SlugNullField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+            'title_en': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title_pl': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '16', 'db_index': 'True'})
+        },
+        'shop.offer': {
+            'Meta': {'ordering': "['entry']", 'object_name': 'Offer'},
+            'cost_const': ('django.db.models.fields.DecimalField', [], {'max_digits': '6', 'decimal_places': '2'}),
+            'cost_per_item': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '6', 'decimal_places': '2'}),
+            'entry': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['migdal.Entry']", 'unique': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'price': ('django.db.models.fields.DecimalField', [], {'max_digits': '6', 'decimal_places': '2'})
+        },
+        'shop.order': {
+            'Meta': {'ordering': "['-payed_at']", 'object_name': 'Order'},
+            'address': ('django.db.models.fields.TextField', [], {'db_index': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'items': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+            'language_code': ('django.db.models.fields.CharField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '127', 'blank': 'True'}),
+            'offer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shop.Offer']"}),
+            'payed_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['shop']
\ No newline at end of file
diff --git a/shop/migrations/__init__.py b/shop/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/shop/models.py b/shop/models.py
new file mode 100644 (file)
index 0000000..349cf43
--- /dev/null
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+# This file is part of PrawoKultury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from datetime import datetime
+from django.core.mail import send_mail, mail_managers
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.db import models
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _, override
+import getpaid
+from migdal.models import Entry
+from . import app_settings
+
+
+class Offer(models.Model):
+    """ A fundraiser for a particular book. """
+    entry = models.OneToOneField(Entry)  # filter publications!
+    price = models.DecimalField(_('price'), decimal_places=2, max_digits=6)
+    cost_const = models.DecimalField(decimal_places=2, max_digits=6)
+    cost_per_item = models.DecimalField(decimal_places=2, max_digits=6, default=0)
+
+    class Meta:
+        verbose_name = _('offer')
+        verbose_name_plural = _('offers')
+        ordering = ['entry']
+
+    def __unicode__(self):
+        return self.entry.title
+
+    def get_absolute_url(self):
+        return self.entry.get_absolute_url()
+
+    def total_per_item(self):
+        return self.price + self.cost_per_item
+
+    def price_per_items(self, items):
+        return self.cost_const + items * self.total_per_item()
+
+
+class Order(models.Model):
+    """ A person paying for a book.
+
+    The payment was completed if and only if payed_at is set.
+
+    """
+    offer = models.ForeignKey(Offer, verbose_name=_('offer'))
+    items = models.IntegerField(verbose_name=_('items'), default=1)
+    name = models.CharField(_('name'), max_length=127, blank=True)
+    email = models.EmailField(_('email'), db_index=True)
+    address = models.TextField(_('address'), db_index=True)
+    payed_at = models.DateTimeField(_('payed at'), null=True, blank=True, db_index=True)
+    language_code = models.CharField(max_length = 2, null = True, blank = True)
+
+    class Meta:
+        verbose_name = _('order')
+        verbose_name_plural = _('orders')
+        ordering = ['-payed_at']
+
+    def __unicode__(self):
+        return "%s (%d egz.)" % (unicode(self.offer), self.items)
+
+    def get_absolute_url(self):
+        return self.offer.get_absolute_url()
+
+    def amount(self):
+        return self.offer.price_per_items(self.items)
+
+    def notify(self, subject, template_name, extra_context=None):
+        context = {
+            'order': self,
+            'site': Site.objects.get_current(),
+        }
+        if extra_context:
+            context.update(extra_context)
+        with override(self.language_code or app_settings.DEFAULT_LANGUAGE):
+            send_mail(subject,
+                render_to_string(template_name, context),
+                getattr(settings, 'CONTACT_EMAIL', 'prawokultury@nowoczesnapolska.org.pl'),
+                [self.email],
+                fail_silently=False
+            )
+
+    def notify_managers(self, subject, template_name, extra_context=None):
+        context = {
+            'order': self,
+            'site': Site.objects.get_current(),
+        }
+        if extra_context:
+            context.update(extra_context)
+        with override(app_settings.DEFAULT_LANGUAGE):
+            mail_managers(subject, render_to_string(template_name, context))
+
+# Register the Order model with django-getpaid for payments.
+getpaid.register_to_payment(Order, unique=False, related_name='payment')
+
+
+def new_payment_query_listener(sender, order=None, payment=None, **kwargs):
+    """ Set payment details for getpaid. """
+    payment.amount = order.amount()
+    payment.currency = 'PLN'
+getpaid.signals.new_payment_query.connect(new_payment_query_listener)
+
+
+def user_data_query_listener(sender, order, user_data, **kwargs):
+    """ Set user data for payment. """
+    user_data['email'] = order.email
+getpaid.signals.user_data_query.connect(user_data_query_listener)
+
+def payment_status_changed_listener(sender, instance, old_status, new_status, **kwargs):
+    """ React to status changes from getpaid. """
+    if old_status != 'paid' and new_status == 'paid':
+        instance.order.payed_at = datetime.now()
+        instance.order.save()
+        instance.order.notify(
+            _('Your payment has been completed.'),
+            'shop/email/payed.txt'
+        )
+        instance.order.notify_managers(
+            _('New order has been placed.'),
+            'shop/email/payed_managers.txt'
+        )
+getpaid.signals.payment_status_changed.connect(payment_status_changed_listener)
diff --git a/shop/static/shop/shop.js b/shop/static/shop/shop.js
new file mode 100755 (executable)
index 0000000..96b9f9d
--- /dev/null
@@ -0,0 +1,31 @@
+$(function() {
+
+    $('.cost-items').each(function() {
+
+        var $items = $(this);
+        var price = parseFloat($items.attr('data-cost-price')) * 100;
+        var cost_const = parseFloat($items.attr('data-cost-const')) * 100;
+        var cost_per_item = parseFloat($items.attr('data-cost-per-item')) * 100;
+
+        var decimal_separator = $items.attr('data-decimal-separator');
+
+        var money = function(amount) {
+            return amount.toFixed(2).replace(".", decimal_separator);
+        }
+
+        var update_costs = function() {
+            var items = $items.val();
+            if (items < 1)
+                items = 1;
+            var total_costs = cost_per_item * items + cost_const;
+            var final = price * items + total_costs;
+            $("#cost-costs").text(money(total_costs / 100) + " zł");
+            $("#cost-final").text(money(final / 100) + " zł");
+        }
+
+        $items.change(update_costs);
+        update_costs();
+
+    });
+
+});
diff --git a/shop/templates/shop/email/base.txt b/shop/templates/shop/email/base.txt
new file mode 100755 (executable)
index 0000000..a0fd074
--- /dev/null
@@ -0,0 +1,5 @@
+{% autoescape off %}{% load i18n %}{% trans 'Hi' %} {{ order.name }},
+{% block body %}
+{% endblock %}
+{% blocktrans %}Cheers,
+Right to Culture team{% endblocktrans %}{% endautoescape %}
diff --git a/shop/templates/shop/email/payed.txt b/shop/templates/shop/email/payed.txt
new file mode 100644 (file)
index 0000000..387b6ba
--- /dev/null
@@ -0,0 +1,14 @@
+{% extends "shop/email/base.txt" %}
+{% load i18n %}
+
+
+{% block body %}
+{% blocktrans %}Your payment has been successfully completed.{% endblocktrans %}
+
+{% blocktrans %}Your order is now being processed: {% endblocktrans %}
+
+{{ order }}
+{{ order.name }}
+{{ order.address }}
+
+{% endblock %}
diff --git a/shop/templates/shop/email/payed_managers.txt b/shop/templates/shop/email/payed_managers.txt
new file mode 100755 (executable)
index 0000000..748d4d9
--- /dev/null
@@ -0,0 +1,18 @@
+{% extends "shop/email/base.txt" %}
+{% load i18n %}
+{% load url from future %}
+
+
+{% block body %}
+Następujące zamówienie zostało złożone i opłacone:
+
+{{ order }}
+
+{{ order.email }}
+{{ order.name }}
+
+{{ order.address }}
+
+http://{{ site.domain }}{% url 'admin:shop_order_change' order.pk %}
+
+{% endblock %}
diff --git a/shop/templates/shop/no_thanks.html b/shop/templates/shop/no_thanks.html
new file mode 100644 (file)
index 0000000..ad2d65e
--- /dev/null
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load fnp_share %}
+
+{% block titleextra %}{% trans "Payment failed" %}{% endblock %}
+
+{% block "body" %}
+
+<h1>{% trans "Payment failed" %}</h1>
+
+<div class="normal">
+
+<p>{% trans "You're support has not been processed successfully." %}</p>
+
+<p>{% trans "Go back to:" %} <a href="{{ object.order.get_absolute_url }}">{{ object.order.offer }}</a>.</p>
+</div>
+
+{% endblock %}
diff --git a/shop/templates/shop/offer_detail.html b/shop/templates/shop/offer_detail.html
new file mode 100644 (file)
index 0000000..a52f589
--- /dev/null
@@ -0,0 +1 @@
+{% extends "migdal/entry/publications/entry_detail.html" %}
diff --git a/shop/templates/shop/snippets/order_form.html b/shop/templates/shop/snippets/order_form.html
new file mode 100755 (executable)
index 0000000..a1f00e9
--- /dev/null
@@ -0,0 +1,22 @@
+{% load i18n staticfiles %}
+{% load url from future %}
+
+<form class="submit-form" action="{% url 'shop_buy' form.offer.entry.slug %}" method="post">
+    <table>
+        {{ form.as_table }}
+
+        <tr><th>{% trans "Payment and shipping costs" %}:</th><td style="font-size:1.2em;" id="cost-costs">
+                {{ form.offer.cost_const|floatformat:"-2" }} zł
+                + {{ form.offer.cost_per_item|floatformat:"-2" }} zł {% trans "for each copy" %}
+            </td></tr>
+        <tr><th>{% trans "Final cost" %}:</th><td id="cost-final" style="font-size:1.5em;">
+                {{ form.offer.total_per_item|floatformat:"-2" }} zł {% trans "for each copy" %}
+                + {{ form.offer.cost_const|floatformat:"-2" }} zł
+            </td></tr>
+        <tr><td></td><td>
+            <button type="submit" style="border: none; background: none; cursor: pointer">
+            <img alt="{% trans 'Donate!' %}" src="{% static 'img/payu.png' %}" />
+            </button>
+        </td></tr>
+    </table>
+</form>
diff --git a/shop/templates/shop/thanks.html b/shop/templates/shop/thanks.html
new file mode 100644 (file)
index 0000000..c08447c
--- /dev/null
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+{% load i18n %}
+{% load fnp_share %}
+
+{% block titleextra %}{% trans "Payment successful" %}{% endblock %}
+
+{% block "body" %}
+
+<h1>{% trans "Payment successful" %}</h1>
+
+<div class="normal">
+
+<p>{% trans "Your payment has been successfully completed." %}</p>
+
+<p>{% trans "Go back to:" %} <a href="{{ object.order.get_absolute_url }}">{{ object.order.offer }}</a>.</p>
+
+</div>
+
+{% endblock %}
diff --git a/shop/templatetags/__init__.py b/shop/templatetags/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/shop/templatetags/shop_tags.py b/shop/templatetags/shop_tags.py
new file mode 100755 (executable)
index 0000000..9ecf196
--- /dev/null
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# This file is part of PrawoKultury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django import template
+from shop.forms import OrderForm
+
+register = template.Library()
+
+
+@register.inclusion_tag('shop/snippets/order_form.html', takes_context=True)
+def order_form_for(context, offer, form=None):
+    if not form:
+        form = OrderForm(offer)
+    return {'form': form}
diff --git a/shop/urls.py b/shop/urls.py
new file mode 100644 (file)
index 0000000..debea33
--- /dev/null
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# This file is part of PrawoKultury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf.urls import patterns, url, include
+from django.utils.translation import ugettext_lazy as _
+from django.views.decorators.csrf import csrf_exempt
+from getpaid.backends.payu.views import OnlineView
+from prawokultury.middleware import honeypot_exempt
+from .views import ThanksView, NoThanksView, OfferDetailView
+
+
+urlpatterns = patterns('',
+    url(r'^kup/(?P<slug>[^/]+)/$', OfferDetailView.as_view(), name='shop_buy'),
+    url(r'^dziekujemy/(?P<pk>\d+)/$', ThanksView.as_view(), name='shop_thanks'),
+    url(r'^niepowodzenie/(?P<pk>\d+)/$', NoThanksView.as_view(), name='shop_nothanks'),
+    url(r'^getpaid/getpaid.backends.payu/online/$', 
+            honeypot_exempt(csrf_exempt(OnlineView.as_view())),
+            name='getpaid-payu-online'),
+    url(r'^getpaid/', include('getpaid.urls')),
+)
diff --git a/shop/views.py b/shop/views.py
new file mode 100644 (file)
index 0000000..ef73d66
--- /dev/null
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# This file is part of PrawoKultury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from datetime import date
+from django.views.decorators.cache import never_cache
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect, get_object_or_404
+from django.views.decorators.csrf import csrf_exempt
+from django.views.generic import TemplateView, FormView, DetailView, ListView
+import getpaid.backends.payu
+from getpaid.models import Payment
+from . import app_settings
+from .forms import OrderForm
+from .models import Offer, Order
+
+
+
+class OfferDetailView(FormView):
+    form_class = OrderForm
+    template_name = "shop/offer_detail.html"
+    backend = 'getpaid.backends.payu'
+
+    @csrf_exempt
+    def dispatch(self, request, slug):
+        if getattr(self, 'object', None) is None:
+            lang = request.LANGUAGE_CODE
+            args = {'entry__slug_%s' % lang: slug}
+            self.object = get_object_or_404(Offer, **args)
+        return super(OfferDetailView, self).dispatch(request, slug)
+
+    def get(self, *args, **kwargs):
+        return redirect(self.object.get_absolute_url())
+
+    def get_context_data(self, *args, **kwargs):
+        ctx = super(OfferDetailView, self).get_context_data(*args, **kwargs)
+        ctx['entry'] = self.object.entry
+        return ctx
+
+    def get_form(self, form_class):
+        return form_class(self.object, self.request.POST)
+
+    def form_valid(self, form):
+        order = form.save()
+        # Skip getpaid.forms.PaymentMethodForm, go directly to the broker.
+        payment = Payment.create(order, self.backend)
+        gateway_url_tuple = payment.get_processor()(payment).get_gateway_url(self.request)
+        payment.change_status('in_progress')
+        return redirect(gateway_url_tuple[0])
+
+
+class ThanksView(DetailView):
+    model = Payment
+    template_name = "shop/thanks.html"
+
+
+class NoThanksView(DetailView):
+    model = Payment
+    template_name = "shop/no_thanks.html"
diff --git a/shop/widgets.py b/shop/widgets.py
new file mode 100755 (executable)
index 0000000..5690be3
--- /dev/null
@@ -0,0 +1,4 @@
+from django.forms.widgets import TextInput
+
+class NumberInput(TextInput):
+    input_type = 'number'