From 4b86e623b0ff7a5a53bdb29df06eab039ebe4e1e Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 4 Mar 2019 21:50:33 +0100 Subject: [PATCH] Base payment scheme. --- src/club/__init__.py | 0 src/club/admin.py | 40 +++++ src/club/apps.py | 5 + src/club/forms.py | 29 ++++ src/club/helpers.py | 9 + src/club/locale/pl/LC_MESSAGES/django.mo | Bin 0 -> 2164 bytes src/club/locale/pl/LC_MESSAGES/django.po | 163 ++++++++++++++++++ src/club/migrations/0001_initial.py | 113 ++++++++++++ src/club/migrations/__init__.py | 0 src/club/models.py | 144 ++++++++++++++++ src/club/payment_methods.py | 53 ++++++ src/club/templates/club/dummy_payment.html | 24 +++ src/club/templates/club/index.html | 26 +++ src/club/templates/club/membership_form.html | 21 +++ src/club/templates/club/payment/dummy-re.html | 1 + src/club/templates/club/payment/dummy.html | 1 + .../templates/club/payment/paypal-re.html | 2 + src/club/templates/club/payment/payu-re.html | 1 + src/club/templates/club/payment/payu.html | 1 + src/club/templates/club/schedule.html | 51 ++++++ src/club/templates/club/widgets/plan.html | 25 +++ src/club/templatetags/__init__.py | 0 src/club/templatetags/club.py | 10 ++ src/club/translation.py | 13 ++ src/club/urls.py | 14 ++ src/club/views.py | 86 +++++++++ src/wolnelektury/settings/apps.py | 1 + src/wolnelektury/urls.py | 1 + 28 files changed, 834 insertions(+) create mode 100644 src/club/__init__.py create mode 100644 src/club/admin.py create mode 100644 src/club/apps.py create mode 100644 src/club/forms.py create mode 100644 src/club/helpers.py create mode 100644 src/club/locale/pl/LC_MESSAGES/django.mo create mode 100644 src/club/locale/pl/LC_MESSAGES/django.po create mode 100644 src/club/migrations/0001_initial.py create mode 100644 src/club/migrations/__init__.py create mode 100644 src/club/models.py create mode 100644 src/club/payment_methods.py create mode 100644 src/club/templates/club/dummy_payment.html create mode 100644 src/club/templates/club/index.html create mode 100644 src/club/templates/club/membership_form.html create mode 100644 src/club/templates/club/payment/dummy-re.html create mode 100644 src/club/templates/club/payment/dummy.html create mode 100644 src/club/templates/club/payment/paypal-re.html create mode 100644 src/club/templates/club/payment/payu-re.html create mode 100644 src/club/templates/club/payment/payu.html create mode 100644 src/club/templates/club/schedule.html create mode 100644 src/club/templates/club/widgets/plan.html create mode 100644 src/club/templatetags/__init__.py create mode 100644 src/club/templatetags/club.py create mode 100644 src/club/translation.py create mode 100644 src/club/urls.py create mode 100644 src/club/views.py diff --git a/src/club/__init__.py b/src/club/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/club/admin.py b/src/club/admin.py new file mode 100644 index 000000000..bfa433006 --- /dev/null +++ b/src/club/admin.py @@ -0,0 +1,40 @@ +from django.contrib import admin +from modeltranslation.admin import TranslationAdmin +from . import models + + +class PlanAdmin(admin.ModelAdmin): + list_display = ['min_amount', 'interval'] + +admin.site.register(models.Plan, PlanAdmin) + + +class PaymentInline(admin.TabularInline): + model = models.Payment + extra = 0 + readonly_fields = ['payed_at'] + + +class ScheduleAdmin(admin.ModelAdmin): + list_display = ['email', 'started_at', 'expires_at', 'plan', 'amount', 'is_active', 'is_cancelled'] + list_search = ['email'] + list_filter = ['is_active', 'is_cancelled'] + date_hierarchy = 'started_at' + inlines = [PaymentInline] + +admin.site.register(models.Schedule, ScheduleAdmin) + + +class PaymentAdmin(admin.ModelAdmin): + list_display = ['payed_at', 'schedule'] + +admin.site.register(models.Payment, PaymentAdmin) + + +class MembershipAdmin(admin.ModelAdmin): + pass + +admin.site.register(models.Membership, MembershipAdmin) + + +admin.site.register(models.ReminderEmail, TranslationAdmin) diff --git a/src/club/apps.py b/src/club/apps.py new file mode 100644 index 000000000..ee2584957 --- /dev/null +++ b/src/club/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class ClubConfig(AppConfig): + name = 'club' + verbose_name = 'Towarzystwo' diff --git a/src/club/forms.py b/src/club/forms.py new file mode 100644 index 000000000..adf8959fc --- /dev/null +++ b/src/club/forms.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 +from django import forms +from . import models +from . import widgets +from .payment_methods import method_by_slug + + +class ScheduleForm(forms.ModelForm): + class Meta: + model = models.Schedule + fields = ['plan', 'method', 'amount', 'email'] + widgets = { + 'plan': forms.RadioSelect, + 'method': forms.RadioSelect, + } + + def __init__(self, *args, **kwargs): + super(ScheduleForm, self).__init__(*args, **kwargs) + self.fields['plan'].empty_label = None + + def clean(self): + cleaned_data = super(ScheduleForm, self).clean() + if 'method' in cleaned_data: + method = method_by_slug[cleaned_data['method']] + if method not in cleaned_data['plan'].payment_methods(): + self.add_error('method', 'Metoda płatności niedostępna dla tego planu.') + if cleaned_data['amount'] < cleaned_data['plan'].min_amount: + self.add_error('amount', 'Minimalna kwota dla tego planu to %d zł.' % cleaned_data['plan'].min_amount) + diff --git a/src/club/helpers.py b/src/club/helpers.py new file mode 100644 index 000000000..b7592fadd --- /dev/null +++ b/src/club/helpers.py @@ -0,0 +1,9 @@ +from django.utils.timezone import now +from .models import Schedule + + +def get_active_schedule(user): + if not user.is_authenticated: + return None + return Schedule.objects.filter(membership__user=user, is_active=True).exclude(expires_at__lt=now()).first() + diff --git a/src/club/locale/pl/LC_MESSAGES/django.mo b/src/club/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..576eae04ac72dba637460082dc1058dc5a82265a GIT binary patch literal 2164 zcmb`HO^6&t6vs=A(Tp0C7{8*?N?=2lwKqMx>ypgwBw*Y~AWN385g}rz>8_oro$ji( zs(L3qMhHR3N${YEL_LWIZyqEUJ-KuB>P0*WdKE8v5dFWN-kk}n2f?DNe)Zm~SMR;* zuKw}Bt`8X6QS@igU$}>{6X4>#7|?zPN&W+@gOm3$_5gSV+zmz**TKDz-vl28Z&dpC zz+I3(1owa+gEjDT@FDO^@P6={O8+hR2;}cTit_`Ahy8?s{C)w+?>CU*|5fRCBgli0 z_k-kjsFII@c-RyMiW7pA=R8PxE`j8C8KnH)04Yufq z@BrjZkm~v#r2T&e>AZfe_$Ns9>_O2KXD|2!co3v~r$AbN7Nq?zg0$}Siml4t0dXAL zjXoFhJ6LI`=B@S&CbS3TMUBp%+Vew{=TA9Mduj;AE7G@sS6sFu7#~A_4t+m5y{D(q z_n}j+R6jL3H~N;Tew15&+lN4^`!G7y$pnu@!Nr=Fmdk6IvcfB)*+fiwx6?3O-;!&_ z%71d$X%Rd%+IJACkis$%d9@*#ND|ZIMoaEhx+Pi}<<=_QW+FAY_N-;%f<;0{GD&32 zA}fWLF&Cbd*$sm+nF^J#GJEN+!l*RQq|Kzyl`kN9xgiqPl?6*>+LG3FR5mtsnEQ^2 zS*rBaDsm=@VIWLWiBu{&mL)=$gJV|0Bc_upGq_uH%taj;=ZV~&K;?z4DsXvgT}Gbe zPFl8N4GF>dICvHL;T1G^?UKxl^}(`ht2lTmZ@XaCK)rG?xFk0eEec-7c{KP;SU(kn zvq3n=>-EO$+*DW(!`cPme6VVTc8T!D!t;XA?Oe2Fuqs75CO3GN)RxaLFO7oNr^DJ= zyf>5{tQHx9czMmAK=Dd1@TeoKlfHR*^;~drw2oq}NgFKb$i#S!4Suqvd~GGktw@4% z#-^^pbykY5Ik$kYWN9AP&1RjS;URDE<9ebVo^Hc8u4AD`X$Rx%wA^KWJf^?%w@ zB^_1L*Ge*Lh7?_2^mM_p!A;?{8QhK(Uzf2qR`g9z-ldOhP%#VA+gH14r|yNh8myiW&PWG2N);7NySxl@lTfghb|TamJ{ Xc`>~6%;O({%RczJ@Lki>s>}Wc-6R}c literal 0 HcmV?d00001 diff --git a/src/club/locale/pl/LC_MESSAGES/django.po b/src/club/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 000000000..0cb3e2495 --- /dev/null +++ b/src/club/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,163 @@ +# 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 , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-03-04 11:36+0100\n" +"PO-Revision-Date: 2019-03-04 11:34+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" +"%100<12 || n%100>=14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" +"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" +"X-Generator: Poedit 2.0.6\n" + +#: models.py:25 +msgid "a month" +msgstr "miesięcznie" + +#: models.py:26 +msgid "a year" +msgstr "rocznie" + +#: models.py:27 +msgid "in perpetuity" +msgstr "na zawsze" + +#: models.py:30 +msgid "inteval" +msgstr "okres" + +#: models.py:31 +msgid "min_amount" +msgstr "minimalna kwota" + +#: models.py:32 +msgid "allow recurring" +msgstr "płatności cykliczne" + +#: models.py:33 +msgid "allow one time" +msgstr "płatności jednorazowe" + +#: models.py:36 models.py:69 +msgid "plan" +msgstr "plan" + +#: models.py:37 +msgid "plans" +msgstr "plany" + +#: models.py:66 +msgid "key" +msgstr "klucz" + +#: models.py:67 +msgid "email" +msgstr "email" + +#: models.py:68 models.py:123 +msgid "membership" +msgstr "członkostwo" + +#: models.py:70 +msgid "amount" +msgstr "kwota" + +#: models.py:71 +msgid "method" +msgstr "metoda płatności" + +#: models.py:72 +msgid "active" +msgstr "aktywny" + +#: models.py:73 +msgid "cancelled" +msgstr "anulowany" + +#: models.py:74 +msgid "started at" +msgstr "start" + +#: models.py:75 +msgid "expires_at" +msgstr "wygasa" + +#: models.py:79 models.py:105 +msgid "schedule" +msgstr "harmonogram" + +#: models.py:80 +msgid "schedules" +msgstr "harmonogramy" + +#: models.py:106 models.py:120 +msgid "created at" +msgstr "utworzone" + +#: models.py:107 +msgid "payed at" +msgstr "opłacona" + +#: models.py:110 +msgid "payment" +msgstr "płatność" + +#: models.py:111 +msgid "payments" +msgstr "płatności" + +#: models.py:119 +msgid "user" +msgstr "użytkownik" + +#: models.py:124 +msgid "memberships" +msgstr "członkostwa" + +#: models.py:131 +msgid "days before" +msgstr "dni przed" + +#: models.py:132 +msgid "subject" +msgstr "temat" + +#: models.py:133 +msgid "body" +msgstr "treść" + +#: models.py:136 +msgid "reminder email" +msgstr "email z przypomnieniem" + +#: models.py:137 +msgid "reminder emails" +msgstr "emaile z przypomnieniem" + +#: models.py:142 +#, python-format +msgid "a day before expiration" +msgid_plural "%d days before expiration" +msgstr[0] "%d dzień przed wygaśnięciem" +msgstr[1] "%d dni przed wygaśnięciem" +msgstr[2] "%d dni przed wygaśnięciem" +msgstr[3] "%d dni przed wygaśnięciem" + +#: models.py:144 +#, python-format +msgid "a day after expiration" +msgid_plural "%d days after expiration" +msgstr[0] "%d dzień po wygaśnięciu" +msgstr[1] "%d dni po wygaśnięciu" +msgstr[2] "%d dni po wygaśnięciu" +msgstr[3] "%d dni przed wygaśnięciem" diff --git a/src/club/migrations/0001_initial.py b/src/club/migrations/0001_initial.py new file mode 100644 index 000000000..a3b7455e5 --- /dev/null +++ b/src/club/migrations/0001_initial.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-04 20:50 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Membership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + options={ + 'verbose_name': 'membership', + 'verbose_name_plural': 'memberships', + }, + ), + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('payed_at', models.DateTimeField(blank=True, null=True, verbose_name='payed at')), + ], + options={ + 'verbose_name': 'payment', + 'verbose_name_plural': 'payments', + }, + ), + migrations.CreateModel( + name='Plan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('interval', models.SmallIntegerField(choices=[(30, 'a month'), (365, 'a year'), (999, 'in perpetuity')], verbose_name='inteval')), + ('min_amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='min_amount')), + ('allow_recurring', models.BooleanField(verbose_name='allow recurring')), + ('allow_one_time', models.BooleanField(verbose_name='allow one time')), + ], + options={ + 'ordering': ('interval',), + }, + ), + migrations.CreateModel( + name='ReminderEmail', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('days_before', models.SmallIntegerField(verbose_name='days before')), + ('subject', models.CharField(max_length=1024, verbose_name='subject')), + ('subject_de', models.CharField(max_length=1024, null=True, verbose_name='subject')), + ('subject_en', models.CharField(max_length=1024, null=True, verbose_name='subject')), + ('subject_es', models.CharField(max_length=1024, null=True, verbose_name='subject')), + ('subject_fr', models.CharField(max_length=1024, null=True, verbose_name='subject')), + ('subject_it', models.CharField(max_length=1024, null=True, verbose_name='subject')), + ('subject_lt', models.CharField(max_length=1024, null=True, verbose_name='subject')), + ('subject_pl', models.CharField(max_length=1024, null=True, verbose_name='subject')), + ('subject_ru', models.CharField(max_length=1024, null=True, verbose_name='subject')), + ('subject_uk', models.CharField(max_length=1024, null=True, verbose_name='subject')), + ('body', models.TextField(verbose_name='body')), + ('body_de', models.TextField(null=True, verbose_name='body')), + ('body_en', models.TextField(null=True, verbose_name='body')), + ('body_es', models.TextField(null=True, verbose_name='body')), + ('body_fr', models.TextField(null=True, verbose_name='body')), + ('body_it', models.TextField(null=True, verbose_name='body')), + ('body_lt', models.TextField(null=True, verbose_name='body')), + ('body_pl', models.TextField(null=True, verbose_name='body')), + ('body_ru', models.TextField(null=True, verbose_name='body')), + ('body_uk', models.TextField(null=True, verbose_name='body')), + ], + options={ + 'ordering': ['days_before'], + 'verbose_name': 'reminder email', + 'verbose_name_plural': 'reminder emails', + }, + ), + migrations.CreateModel( + name='Schedule', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=255, unique=True, verbose_name='key')), + ('email', models.EmailField(max_length=254, verbose_name='email')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='amount')), + ('method', models.CharField(choices=[(b'payu', b'PayU'), (b'payu-re', b'PayU Recurring'), (b'paypal-re', b'PayPal Recurring')], max_length=255, verbose_name='method')), + ('is_active', models.BooleanField(default=False, verbose_name='active')), + ('is_cancelled', models.BooleanField(default=False, verbose_name='cancelled')), + ('started_at', models.DateTimeField(auto_now_add=True, verbose_name='started at')), + ('expires_at', models.DateTimeField(blank=True, null=True, verbose_name='expires_at')), + ('membership', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='club.Membership', verbose_name='membership')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='club.Plan', verbose_name='plan')), + ], + options={ + 'verbose_name': 'schedule', + 'verbose_name_plural': 'schedules', + }, + ), + migrations.AddField( + model_name='payment', + name='schedule', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='club.Schedule', verbose_name='schedule'), + ), + ] diff --git a/src/club/migrations/__init__.py b/src/club/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/club/models.py b/src/club/models.py new file mode 100644 index 000000000..a8b4089c7 --- /dev/null +++ b/src/club/models.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 +from __future__ import unicode_literals + +from datetime import timedelta +from django.conf import settings +from django.urls import reverse +from django.db import models +from django.utils.timezone import now +from django.utils.translation import ugettext_lazy as _, ungettext +from catalogue.utils import get_random_hash +from .payment_methods import methods, method_by_slug + + +class Plan(models.Model): + """ Plans are set up by administrators. """ + MONTH = 30 + YEAR = 365 + PERPETUAL = 999 + intervals = [ + (MONTH, _('a month')), + (YEAR, _('a year')), + (PERPETUAL, _('in perpetuity')), + ] + + interval = models.SmallIntegerField(_('inteval'), choices=intervals) + min_amount = models.DecimalField(_('min_amount'), max_digits=10, decimal_places=2) + allow_recurring = models.BooleanField(_('allow recurring')) + allow_one_time = models.BooleanField(_('allow one time')) + + class Meta: + verbose_name = _('plan') + verbose_name_plural = _('plans') + + def __unicode__(self): + return "%s %s" % (self.min_amount, self.get_interval_display()) + + class Meta: + ordering = ('interval',) + + def payment_methods(self): + for method in methods: + if self.allow_recurring and method.is_recurring or self.allow_one_time and not method.is_recurring: + yield method + + def get_next_installment(self, date): + if self.interval == self.PERPETUAL: + return None + elif self.interval == self.YEAR: + return date.replace(year=date.year + 1) + elif self.interval == self.MONTH: + day = date.day + date = (date.replace(day=1) + timedelta(31)).replace(day=1) + timedelta(day - 1) + if date.day != day: + date = date.replace(day=1) + return date + + + +class Schedule(models.Model): + """ Represents someone taking up a plan. """ + key = models.CharField(_('key'), max_length=255, unique=True) + email = models.EmailField(_('email')) + membership = models.ForeignKey('Membership', verbose_name=_('membership'), null=True, blank=True, on_delete=models.PROTECT) + plan = models.ForeignKey(Plan, verbose_name=_('plan'), on_delete=models.PROTECT) + amount = models.DecimalField(_('amount'), max_digits=10, decimal_places=2) + method = models.CharField(_('method'), max_length=255, choices=[(method.slug, method.name) for method in methods]) + is_active = models.BooleanField(_('active'), default=False) + is_cancelled = models.BooleanField(_('cancelled'), default=False) + started_at = models.DateTimeField(_('started at'), auto_now_add=True) + expires_at = models.DateTimeField(_('expires_at'), null=True, blank=True) + # extra info? + + class Meta: + verbose_name = _('schedule') + verbose_name_plural = _('schedules') + + def __unicode__(self): + return self.key + + def save(self, *args, **kwargs): + if not self.key: + self.key = get_random_hash(self.email) + return super(Schedule, self).save(*args, **kwargs) + + def get_absolute_url(self): + return reverse('club_schedule', args=[self.key]) + + def get_payment_method(self): + return method_by_slug[self.method] + + + def is_expired(self): + return self.expires_at is not None and self.expires_at < now() + + def create_payment(self): + n = now() + self.expires_at = self.plan.get_next_installment(n) + self.is_active = True + self.save() + self.payment_set.create(payed_at=n) + + +class Payment(models.Model): + schedule = models.ForeignKey(Schedule, verbose_name=_('schedule'), on_delete=models.PROTECT) + created_at = models.DateTimeField(_('created at'), auto_now_add=True) + payed_at = models.DateTimeField(_('payed at'), null=True, blank=True) + + class Meta: + verbose_name = _('payment') + verbose_name_plural = _('payments') + + def __unicode__(self): + return "%s %s" % (self.schedule, self.payed_at) + + +class Membership(models.Model): + """ Represents a user being recognized as a member of the club. """ + user = models.OneToOneField(settings.AUTH_USER_MODEL, verbose_name=_('user'), on_delete=models.CASCADE) + created_at = models.DateTimeField(_('created at'), auto_now_add=True) + + class Meta: + verbose_name = _('membership') + verbose_name_plural = _('memberships') + + def __unicode__(self): + return u'tow. ' + unicode(self.user) + + +class ReminderEmail(models.Model): + days_before = models.SmallIntegerField(_('days before')) + subject = models.CharField(_('subject'), max_length=1024) + body = models.TextField(_('body')) + + class Meta: + verbose_name = _('reminder email') + verbose_name_plural = _('reminder emails') + ordering = ['days_before'] + + def __unicode__(self): + if self.days_before >= 0: + return ungettext('a day before expiration', '%d days before expiration', n=self.days_before) + else: + return ungettext('a day after expiration', '%d days after expiration', n=-self.days_before) + diff --git a/src/club/payment_methods.py b/src/club/payment_methods.py new file mode 100644 index 000000000..363d4451c --- /dev/null +++ b/src/club/payment_methods.py @@ -0,0 +1,53 @@ +from django.urls import reverse + + +class PaymentMethod(object): + is_recurring = False + + @classmethod + def get_payment_url(cls, schedule): + return reverse('club_dummy_payment', args=[schedule.key]) + + +class PayU(PaymentMethod): + slug = 'payu' + name = 'PayU' + template_name = 'club/payment/payu.html' + + @classmethod + def get_payment_url(cls, schedule): + return reverse('club_dummy_payment', args=[schedule.key]) + + +class PayURe(PaymentMethod): + slug='payu-re' + name = 'PayU Recurring' + template_name = 'club/payment/payu-re.html' + is_recurring = True + + @classmethod + def get_payment_url(cls, schedule): + return reverse('club_dummy_payment', args=[schedule.key]) + + +class PayPalRe(PaymentMethod): + slug='paypal-re' + name = 'PayPal Recurring' + template_name = 'club/payment/paypal-re.html' + is_recurring = True + + @classmethod + def get_payment_url(cls, schedule): + return reverse('club_dummy_payment', args=[schedule.key]) + + +methods = [ + PayU, + PayURe, + PayPalRe, +] + +method_by_slug = { + m.slug: m + for m in methods +} diff --git a/src/club/templates/club/dummy_payment.html b/src/club/templates/club/dummy_payment.html new file mode 100644 index 000000000..a24e603a6 --- /dev/null +++ b/src/club/templates/club/dummy_payment.html @@ -0,0 +1,24 @@ +{% extends "base/base.html" %} + + +{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %} + + +{% block body %} +
+ +

Testowa płatność

+ +

{{ schedule.email }}

+

{{ schedule.amount }}

+

{{ schedule.plan.get_interval_display }}

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +
+ +{% endblock %} diff --git a/src/club/templates/club/index.html b/src/club/templates/club/index.html new file mode 100644 index 000000000..fd36aad4e --- /dev/null +++ b/src/club/templates/club/index.html @@ -0,0 +1,26 @@ +{% extends "base/base.html" %} +{% load active_schedule from club %} + + +{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %} + + +{% block body %} +
+ +

Towarzystwo Wolnych Lektur

+ +

Towarzystwo jest fajne.

+ +{% with schedule=request.user|active_schedule %} +{% if schedule %} +

Jesteś już w Towarzystwie!

+{% else %} +Dołącz do Towarzystwa. +{% endif %} +{% endwith %} + + +
+ +{% endblock %} diff --git a/src/club/templates/club/membership_form.html b/src/club/templates/club/membership_form.html new file mode 100644 index 000000000..d0c3250e5 --- /dev/null +++ b/src/club/templates/club/membership_form.html @@ -0,0 +1,21 @@ +{% extends "base/base.html" %} + + +{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %} + + +{% block body %} +
+ +

{% if membership %}Odnów swoje członkostwo w Towarzystwie Wolnych Lektur{% else %}Dołącz do Towarzystwa Wolnych Lektur{% endif %}

+ +
+ {% csrf_token %} + + {{ form.as_p }} + +
+ +
+ +{% endblock %} diff --git a/src/club/templates/club/payment/dummy-re.html b/src/club/templates/club/payment/dummy-re.html new file mode 100644 index 000000000..8bc9f9004 --- /dev/null +++ b/src/club/templates/club/payment/dummy-re.html @@ -0,0 +1 @@ +dummy re diff --git a/src/club/templates/club/payment/dummy.html b/src/club/templates/club/payment/dummy.html new file mode 100644 index 000000000..421376db9 --- /dev/null +++ b/src/club/templates/club/payment/dummy.html @@ -0,0 +1 @@ +dummy diff --git a/src/club/templates/club/payment/paypal-re.html b/src/club/templates/club/payment/paypal-re.html new file mode 100644 index 000000000..42563e809 --- /dev/null +++ b/src/club/templates/club/payment/paypal-re.html @@ -0,0 +1,2 @@ +PayPal + diff --git a/src/club/templates/club/payment/payu-re.html b/src/club/templates/club/payment/payu-re.html new file mode 100644 index 000000000..28b693635 --- /dev/null +++ b/src/club/templates/club/payment/payu-re.html @@ -0,0 +1 @@ +Kartą diff --git a/src/club/templates/club/payment/payu.html b/src/club/templates/club/payment/payu.html new file mode 100644 index 000000000..6da90e30f --- /dev/null +++ b/src/club/templates/club/payment/payu.html @@ -0,0 +1 @@ +Czekolada / karta płatnicza diff --git a/src/club/templates/club/schedule.html b/src/club/templates/club/schedule.html new file mode 100644 index 000000000..6baa9a4fc --- /dev/null +++ b/src/club/templates/club/schedule.html @@ -0,0 +1,51 @@ +{% extends "base/base.html" %} + + +{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %} + + +{% block body %} +
+ +

Plan płatności

+ +
{{ schedule.email }}
+
{{ schedule.amount }}
+
{{ schedule.plan.get_interval_display }}
+ +
{{ schedule.is_active|yesno:"Aktywne,Nieaktywne" }}
+ +
Start: {{ schedule.started_at }}
+
Opłacone do: {{ schedule.expires_at|default:"brak" }} {% if schedule.is_cancelled %}(anulowana){% endif %}
+ +{% if schedule.expires_at and not schedule.is_cancelled %} +
+ {% csrf_token %} + +
+{% endif %} + +{% if schedule.is_expired %} + Płatność wygasła. Wykonaj nową płatność. +{% endif %} + +{% if schedule.membership %} +

+ Członek/członkini Towarzystwa nr {{ schedule.membership.id }} ({{ schedule.membership.user }}). +

+{% else %} +

+Płatność nie przypisana do członkostwa.
+Przypisz +

+{% endif %} + + + + + + + +
+ +{% endblock %} diff --git a/src/club/templates/club/widgets/plan.html b/src/club/templates/club/widgets/plan.html new file mode 100644 index 000000000..fe0e4a8fa --- /dev/null +++ b/src/club/templates/club/widgets/plan.html @@ -0,0 +1,25 @@ + +
+{% for _1, optgroup, _2 in widget.optgroups %} + {% for option in optgroup %} +
+ {% with plan=option.label %} + {{ plan.min_amount }} zł
+ {{ plan.get_interval_display }} + + {% for pm in plan.payment_methods %} +
+ + +
+ {% endfor %} + + {% endwith %} +
+ {% endfor %} +{% endfor %} +
+ + diff --git a/src/club/templatetags/__init__.py b/src/club/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/club/templatetags/club.py b/src/club/templatetags/club.py new file mode 100644 index 000000000..1a3416971 --- /dev/null +++ b/src/club/templatetags/club.py @@ -0,0 +1,10 @@ +from django import template +from ..helpers import get_active_schedule + + +register = template.Library() + + +@register.filter +def active_schedule(user): + return get_active_schedule(user) diff --git a/src/club/translation.py b/src/club/translation.py new file mode 100644 index 000000000..38772a70b --- /dev/null +++ b/src/club/translation.py @@ -0,0 +1,13 @@ +# -*- 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 modeltranslation.translator import translator, TranslationOptions +from .models import ReminderEmail + + +class ReminderEmailTranslationOptions(TranslationOptions): + fields = ('subject', 'body') + +translator.register(ReminderEmail, ReminderEmailTranslationOptions) diff --git a/src/club/urls.py b/src/club/urls.py new file mode 100644 index 000000000..0a8b53a4e --- /dev/null +++ b/src/club/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls import url +from . import views + + +urlpatterns = [ + url(r'^$', views.ClubView.as_view()), + url(r'^dolacz/$', views.JoinView.as_view(), name='club_join'), + + url(r'^plan/(?P[-a-z0-9]+)/$', views.ScheduleView.as_view(), name='club_schedule'), + url(r'^przylacz/(?P[-a-z0-9]+)/$', views.claim, name='club_claim'), + url(r'^anuluj/(?P[-a-z0-9]+)/$', views.cancel, name='club_cancel'), + + url(r'^testowa-platnosc/(?P[-a-z0-9]+)/$', views.DummyPaymentView.as_view(), name='club_dummy_payment'), +] diff --git a/src/club/views.py b/src/club/views.py new file mode 100644 index 000000000..e6dc996e0 --- /dev/null +++ b/src/club/views.py @@ -0,0 +1,86 @@ +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.views.generic import FormView, CreateView, TemplateView +from django.views import View +from .forms import ScheduleForm +from . import models +from .helpers import get_active_schedule + + +class ClubView(TemplateView): + template_name = 'club/index.html' + + +class JoinView(CreateView): + template_name = 'club/membership_form.html' + form_class = ScheduleForm + + def get(self, request): + schedule = get_active_schedule(request.user) + if schedule is not None: + return HttpResponseRedirect(schedule.get_absolute_url()) + else: + return super(JoinView, self).get(request) + + def get_context_data(self): + c = super(JoinView, self).get_context_data() + c['membership'] = getattr(self.request.user, 'membership', None) + return c + + def get_initial(self): + if self.request.user.is_authenticated and self.request.user.email: + return { + 'email': self.request.user.email, + } + + def form_valid(self, form): + retval = super(JoinView, self).form_valid(form) + if self.request.user.is_authenticated: + form.instance.membership, created = models.Membership.objects.get_or_create(user=self.request.user) + form.instance.save() + return retval + + +class ScheduleView(View): + def get(self, request, key): + schedule = models.Schedule.objects.get(key=key) + if not schedule.is_active: + return HttpResponseRedirect(schedule.get_payment_method().get_payment_url(schedule)) + else: + return render( + request, + 'club/schedule.html', + { + 'schedule': schedule, + } + ) + + +@login_required +def claim(request, key): + schedule = models.Schedule.objects.get(key=key, membership=None) + schedule.membership, created = models.Membership.objects.get_or_create(user=request.user) + schedule.save() + return HttpResponseRedirect(schedule.get_absolute_url()) + + +def cancel(request, key): + schedule = models.Schedule.objects.get(key=key) + schedule.is_cancelled = True + schedule.save() + return HttpResponseRedirect(schedule.get_absolute_url()) + + +class DummyPaymentView(TemplateView): + template_name = 'club/dummy_payment.html' + + def get_context_data(self, key): + return { + 'schedule': models.Schedule.objects.get(key=key), + } + + def post(self, request, key): + schedule = models.Schedule.objects.get(key=key) + schedule.create_payment() + return HttpResponseRedirect(schedule.get_absolute_url()) diff --git a/src/wolnelektury/settings/apps.py b/src/wolnelektury/settings/apps.py index 07f7ff1fc..106e94fe3 100644 --- a/src/wolnelektury/settings/apps.py +++ b/src/wolnelektury/settings/apps.py @@ -61,6 +61,7 @@ INSTALLED_APPS_CONTRIB = [ 'django_extensions', 'raven.contrib.django.raven_compat', + 'club.apps.ClubConfig', 'migdal', 'django_comments', 'django_comments_xtd', diff --git a/src/wolnelektury/urls.py b/src/wolnelektury/urls.py index eb6a01785..fbfd226ea 100644 --- a/src/wolnelektury/urls.py +++ b/src/wolnelektury/urls.py @@ -53,6 +53,7 @@ urlpatterns += [ url(r'^isbn/', include('isbn.urls')), url(r'^paypal/', include('paypal.urls')), url(r'^powiadomienie/', include('push.urls')), + url(r'^towarzystwo/', include('club.urls')), # Admin panel url(r'^admin/catalogue/book/import$', catalogue.views.import_book, name='import_book'), -- 2.20.1