From 91117520dbe0336cd5acc91c6d8e23d81141f9df Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 23 Sep 2013 16:55:33 +0200 Subject: [PATCH] Basic book shop support. --- prawokultury/settings.d/30-apps.py | 3 + prawokultury/static/img/payu.png | Bin 0 -> 6311 bytes .../entry/publications/entry_detail.html | 58 ++++++++++ prawokultury/urls.py | 1 + requirements.txt | 4 + shop/__init__.py | 14 +++ shop/admin.py | 37 +++++++ shop/forms.py | 36 ++++++ shop/migrations/0001_initial.py | 103 ++++++++++++++++++ shop/migrations/__init__.py | 0 shop/models.py | 102 +++++++++++++++++ shop/templates/shop/email/base.txt | 5 + shop/templates/shop/email/payed.txt | 7 ++ shop/templates/shop/no_thanks.html | 15 +++ shop/templates/shop/offer_detail.html | 1 + shop/templates/shop/snippets/order_form.html | 13 +++ shop/templates/shop/thanks.html | 13 +++ shop/templatetags/__init__.py | 0 shop/templatetags/shop_tags.py | 15 +++ shop/urls.py | 16 +++ shop/views.py | 58 ++++++++++ 21 files changed, 501 insertions(+) create mode 100644 prawokultury/static/img/payu.png create mode 100755 prawokultury/templates/migdal/entry/publications/entry_detail.html create mode 100644 shop/__init__.py create mode 100644 shop/admin.py create mode 100644 shop/forms.py create mode 100644 shop/migrations/0001_initial.py create mode 100644 shop/migrations/__init__.py create mode 100644 shop/models.py create mode 100755 shop/templates/shop/email/base.txt create mode 100644 shop/templates/shop/email/payed.txt create mode 100644 shop/templates/shop/no_thanks.html create mode 100644 shop/templates/shop/offer_detail.html create mode 100755 shop/templates/shop/snippets/order_form.html create mode 100644 shop/templates/shop/thanks.html create mode 100755 shop/templatetags/__init__.py create mode 100755 shop/templatetags/shop_tags.py create mode 100644 shop/urls.py create mode 100644 shop/views.py diff --git a/prawokultury/settings.d/30-apps.py b/prawokultury/settings.d/30-apps.py index 1877989..578e9dd 100644 --- a/prawokultury/settings.d/30-apps.py +++ b/prawokultury/settings.d/30-apps.py @@ -5,6 +5,7 @@ INSTALLED_APPS = ( 'events', 'migdal', 'questions', + 'shop', 'gravatar', 'south', @@ -19,6 +20,8 @@ INSTALLED_APPS = ( 'honeypot', 'taggit', 'taggit_autosuggest', + 'getpaid', + 'getpaid.backends.payu', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/prawokultury/static/img/payu.png b/prawokultury/static/img/payu.png new file mode 100644 index 0000000000000000000000000000000000000000..2e8abf48001123d5b6b750377652c11d1ad8ddea GIT binary patch literal 6311 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 literal 0 HcmV?d00001 diff --git a/prawokultury/templates/migdal/entry/publications/entry_detail.html b/prawokultury/templates/migdal/entry/publications/entry_detail.html new file mode 100755 index 0000000..0cae1fa --- /dev/null +++ b/prawokultury/templates/migdal/entry/publications/entry_detail.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% load comments i18n %} +{% load fnp_common migdal_tags fnp_share shop_tags %} + + +{% block "body" %} + +{% if not entry.published %} +

{% trans "This entry hasn't been published yet." %}

+{% endif %} + +
+
+ +{% entry_begin entry 1 %} +
+{{ 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 %} +
+ {{ inline_html|safe }} +
+{% endfor %} + +
+ +
+ +
+ +
+ +
+ +{% if entry.get_type.commentable %} + {% render_comment_list for entry %} +
+ {% entry_comment_form entry %} +
+{% endif %} +
+
+{% endblock %} + diff --git a/prawokultury/urls.py b/prawokultury/urls.py index e167926..848d885 100644 --- a/prawokultury/urls.py +++ b/prawokultury/urls.py @@ -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: diff --git a/requirements.txt b/requirements.txt index e962daf..c0f07de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 index 0000000..ccd7a85 --- /dev/null +++ b/shop/__init__.py @@ -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 index 0000000..6c84a94 --- /dev/null +++ b/shop/admin.py @@ -0,0 +1,37 @@ +# -*- 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) diff --git a/shop/forms.py b/shop/forms.py new file mode 100644 index 0000000..ce8a48f --- /dev/null +++ b/shop/forms.py @@ -0,0 +1,36 @@ +# -*- 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 + diff --git a/shop/migrations/0001_initial.py b/shop/migrations/0001_initial.py new file mode 100644 index 0000000..0bc1dc2 --- /dev/null +++ b/shop/migrations/0001_initial.py @@ -0,0 +1,103 @@ +# -*- 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 diff --git a/shop/migrations/__init__.py b/shop/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shop/models.py b/shop/models.py new file mode 100644 index 0000000..72d599c --- /dev/null +++ b/shop/models.py @@ -0,0 +1,102 @@ +# -*- 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) diff --git a/shop/templates/shop/email/base.txt b/shop/templates/shop/email/base.txt new file mode 100755 index 0000000..5cf1b9a --- /dev/null +++ b/shop/templates/shop/email/base.txt @@ -0,0 +1,5 @@ +{% autoescape off %}{% load i18n %}{% trans 'Hi' %}{{ order.name }}{% endif %}, +{% block body %} +{% endblock %} +{% blocktrans %}Cheers, +Right to Culture team{% endblocktrans %} diff --git a/shop/templates/shop/email/payed.txt b/shop/templates/shop/email/payed.txt new file mode 100644 index 0000000..4697ea9 --- /dev/null +++ b/shop/templates/shop/email/payed.txt @@ -0,0 +1,7 @@ +{% extends "shop/email/base.txt" %} +{% load i18n %} + + +{% block body %} +{% blocktrans %}Your payment has been successfully completed.{% endblocktrans %} +{% endblock %} diff --git a/shop/templates/shop/no_thanks.html b/shop/templates/shop/no_thanks.html new file mode 100644 index 0000000..007c28d --- /dev/null +++ b/shop/templates/shop/no_thanks.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% load i18n %} +{% load fnp_share %} + +{% block titleextra %}{% trans "Payment failed" %}{% endblock %} + +{% block "body" %} + +

{% trans "Payment failed" %}

+ +

{% trans "You're support has not been processed successfully." %}

+ + + +{% endblock %} diff --git a/shop/templates/shop/offer_detail.html b/shop/templates/shop/offer_detail.html new file mode 100644 index 0000000..a52f589 --- /dev/null +++ b/shop/templates/shop/offer_detail.html @@ -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 index 0000000..c513025 --- /dev/null +++ b/shop/templates/shop/snippets/order_form.html @@ -0,0 +1,13 @@ +{% load i18n %} +{% load url from future %} + +
+ + {{ form.as_table }} + +
+ +
+
diff --git a/shop/templates/shop/thanks.html b/shop/templates/shop/thanks.html new file mode 100644 index 0000000..fe21d98 --- /dev/null +++ b/shop/templates/shop/thanks.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% load i18n %} +{% load fnp_share %} + +{% block titleextra %}{% trans "Payment successful" %}{% endblock %} + +{% block "body" %} + +

{% trans "Payment successful" %}

+ +

{% trans "Your payment has been successfully completed." %}

+ +{% endblock %} diff --git a/shop/templatetags/__init__.py b/shop/templatetags/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/shop/templatetags/shop_tags.py b/shop/templatetags/shop_tags.py new file mode 100755 index 0000000..10c53ae --- /dev/null +++ b/shop/templatetags/shop_tags.py @@ -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 form is None: + form = OrderForm(offer) + return {'form': form} diff --git a/shop/urls.py b/shop/urls.py new file mode 100644 index 0000000..ff1c847 --- /dev/null +++ b/shop/urls.py @@ -0,0 +1,16 @@ +# -*- 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[^/]+)/$', 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')), +) diff --git a/shop/views.py b/shop/views.py new file mode 100644 index 0000000..16634e7 --- /dev/null +++ b/shop/views.py @@ -0,0 +1,58 @@ +# -*- 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" -- 2.20.1