'events',
'migdal',
'questions',
+ 'shop',
'gravatar',
'south',
'honeypot',
'taggit',
'taggit_autosuggest',
+ 'getpaid',
+ 'getpaid.backends.payu',
'django.contrib.auth',
'django.contrib.contenttypes',
--- /dev/null
+{% extends "base.html" %}
+{% load comments i18n %}
+{% load fnp_common migdal_tags fnp_share shop_tags %}
+
+
+{% 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">
+{{ entry.body }}
+
+
+
+
+{% if entry.offer_set.all.exists %}
+
+{% order_form_for entry.offer_set.all.0 form %}
+
+{% endif %}
+
+
+
+
+{% 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 %}
+
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:
django-taggit
django-taggit-autosuggest
+
+django-getpaid>=1.4,<1.5
+django-celery>=3.0.11
+django-kombu
--- /dev/null
+# -*- 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')
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.utils.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']
+ #~ search_fields = ['entry__title_pl']
+
+
+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)
--- /dev/null
+# -*- coding: utf-8 -*-
+# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django import forms
+from django.utils import formats
+from django.utils.translation import ugettext_lazy as _, ugettext, get_language
+from .models import Order
+from . import app_settings
+
+
+class OrderForm(forms.Form):
+ required_css_class = 'required'
+ backend = 'getpaid.backends.payu'
+
+ name = forms.CharField(label=_("Name"))
+ email = forms.EmailField(label=_("Contact e-mail"))
+ address = forms.CharField(label=_("Address"), widget=forms.Textarea)
+ consent = forms.CharField(label=_("Consent"), widget=forms.Textarea,
+ help_text=_('I hereby consent'))
+
+ def __init__(self, offer, *args, **kwargs):
+ print 'o:', offer
+ self.offer = offer
+ super(OrderForm, self).__init__(*args, **kwargs)
+
+ def save(self):
+ order = Order.objects.create(
+ offer=self.offer,
+ name=self.cleaned_data['name'],
+ email=self.cleaned_data['email'],
+ address=self.cleaned_data['address'],
+ language_code = get_language(),
+ )
+ return order
+
--- /dev/null
+# -*- 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.ForeignKey')(to=orm['migdal.Entry'])),
+ ('price', self.gf('django.db.models.fields.DecimalField')(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'])),
+ ('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__title']", 'object_name': 'Offer'},
+ 'entry': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['migdal.Entry']"}),
+ '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'}),
+ '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
--- /dev/null
+# -*- 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
+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.ForeignKey(Entry) # filter publications!
+ price = models.DecimalField(_('price'), decimal_places=2, max_digits=6)
+
+ 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 sum(self):
+ """ The money gathered. """
+ return self.order_payed().aggregate(s=models.Sum('amount'))['s'] or 0
+
+
+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'))
+ 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 unicode(self.offer)
+
+ def get_absolute_url(self):
+ return self.offer.get_absolute_url()
+
+ 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
+ )
+
+# 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.offer.price
+ 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'
+ )
+getpaid.signals.payment_status_changed.connect(payment_status_changed_listener)
--- /dev/null
+{% autoescape off %}{% load i18n %}{% trans 'Hi' %}{{ order.name }}{% endif %},
+{% block body %}
+{% endblock %}
+{% blocktrans %}Cheers,
+Right to Culture team{% endblocktrans %}
--- /dev/null
+{% extends "shop/email/base.txt" %}
+{% load i18n %}
+
+
+{% block body %}
+{% blocktrans %}Your payment has been successfully completed.{% endblocktrans %}
+{% endblock %}
--- /dev/null
+{% extends "base.html" %}
+{% load i18n %}
+{% load fnp_share %}
+
+{% block titleextra %}{% trans "Payment failed" %}{% endblock %}
+
+{% block "body" %}
+
+<h1>{% trans "Payment failed" %}</h1>
+
+<p>{% trans "You're support has not been processed successfully." %}</p>
+
+
+
+{% endblock %}
--- /dev/null
+{% extends "migdal/entry/publications/entry_detail.html" %}
--- /dev/null
+{% load i18n %}
+{% load url from future %}
+
+<form action="{% url 'shop_buy' form.offer.entry.slug %}" method="post">
+ <table>
+ {{ form.as_table }}
+ <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>
--- /dev/null
+{% extends "base.html" %}
+{% load i18n %}
+{% load fnp_share %}
+
+{% block titleextra %}{% trans "Payment successful" %}{% endblock %}
+
+{% block "body" %}
+
+<h1>{% trans "Payment successful" %}</h1>
+
+<p>{% trans "Your payment has been successfully completed." %}</p>
+
+{% endblock %}
--- /dev/null
+# -*- 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 form is None:
+ form = OrderForm(offer)
+ return {'form': form}
--- /dev/null
+# -*- 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 .views import ThanksView, NoThanksView, OfferDetailView
+
+
+urlpatterns = patterns('',
+ url(r'^kup/(?P<slug>[^/]+)/$', OfferDetailView.as_view(), name='shop_buy'),
+ url(r'^dziekujemy/$', ThanksView.as_view(), name='shop_thanks'),
+ url(r'^niepowodzenie/$', NoThanksView.as_view(), name='shop_nothanks'),
+ url(r'^getpaid/', include('getpaid.urls')),
+)
--- /dev/null
+# -*- 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_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):
+ if self.request.method == 'POST':
+ return form_class(self.object, self.request.POST)
+ else:
+ return form_class(self.object)
+
+ 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(TemplateView):
+ template_name = "shop/thanks.html"
+
+
+class NoThanksView(TemplateView):
+ template_name = "shop/no_thanks.html"