Base payment scheme.
authorRadek Czajka <rczajka@rczajka.pl>
Mon, 4 Mar 2019 20:50:33 +0000 (21:50 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Mon, 4 Mar 2019 20:50:33 +0000 (21:50 +0100)
28 files changed:
src/club/__init__.py [new file with mode: 0644]
src/club/admin.py [new file with mode: 0644]
src/club/apps.py [new file with mode: 0644]
src/club/forms.py [new file with mode: 0644]
src/club/helpers.py [new file with mode: 0644]
src/club/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
src/club/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
src/club/migrations/0001_initial.py [new file with mode: 0644]
src/club/migrations/__init__.py [new file with mode: 0644]
src/club/models.py [new file with mode: 0644]
src/club/payment_methods.py [new file with mode: 0644]
src/club/templates/club/dummy_payment.html [new file with mode: 0644]
src/club/templates/club/index.html [new file with mode: 0644]
src/club/templates/club/membership_form.html [new file with mode: 0644]
src/club/templates/club/payment/dummy-re.html [new file with mode: 0644]
src/club/templates/club/payment/dummy.html [new file with mode: 0644]
src/club/templates/club/payment/paypal-re.html [new file with mode: 0644]
src/club/templates/club/payment/payu-re.html [new file with mode: 0644]
src/club/templates/club/payment/payu.html [new file with mode: 0644]
src/club/templates/club/schedule.html [new file with mode: 0644]
src/club/templates/club/widgets/plan.html [new file with mode: 0644]
src/club/templatetags/__init__.py [new file with mode: 0644]
src/club/templatetags/club.py [new file with mode: 0644]
src/club/translation.py [new file with mode: 0644]
src/club/urls.py [new file with mode: 0644]
src/club/views.py [new file with mode: 0644]
src/wolnelektury/settings/apps.py
src/wolnelektury/urls.py

diff --git a/src/club/__init__.py b/src/club/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/club/admin.py b/src/club/admin.py
new file mode 100644 (file)
index 0000000..bfa4330
--- /dev/null
@@ -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 (file)
index 0000000..ee25849
--- /dev/null
@@ -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 (file)
index 0000000..adf8959
--- /dev/null
@@ -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 (file)
index 0000000..b7592fa
--- /dev/null
@@ -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 (file)
index 0000000..576eae0
Binary files /dev/null and b/src/club/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/src/club/locale/pl/LC_MESSAGES/django.po b/src/club/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..0cb3e24
--- /dev/null
@@ -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 <EMAIL@ADDRESS>, 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 (file)
index 0000000..a3b7455
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/src/club/models.py b/src/club/models.py
new file mode 100644 (file)
index 0000000..a8b4089
--- /dev/null
@@ -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 (file)
index 0000000..363d445
--- /dev/null
@@ -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 (file)
index 0000000..a24e603
--- /dev/null
@@ -0,0 +1,24 @@
+{% extends "base/base.html" %}
+
+
+{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %}
+
+
+{% block body %}
+<div class="white-box normal-text">
+
+       <h1>Testowa płatność</h1>
+
+       <p>     {{ schedule.email }}</p>
+       <p>     {{ schedule.amount }}</p>
+       <p>     {{ schedule.plan.get_interval_display }}</p>
+
+<form method="POST" action="">
+  {% csrf_token %}
+  {{ form.as_p }}
+  <button type='submit'>Zapłać</button>
+</form>
+
+</div>
+
+{% endblock %}
diff --git a/src/club/templates/club/index.html b/src/club/templates/club/index.html
new file mode 100644 (file)
index 0000000..fd36aad
--- /dev/null
@@ -0,0 +1,26 @@
+{% extends "base/base.html" %}
+{% load active_schedule from club %}
+
+
+{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %}
+
+
+{% block body %}
+<div class="white-box normal-text">
+
+       <h1>Towarzystwo Wolnych Lektur</h1>
+
+       <p>Towarzystwo jest fajne.</p>
+
+{% with schedule=request.user|active_schedule %}
+{% if schedule %}
+<p><a href="{{ schedule.get_absolute_url }}">Jesteś już w Towarzystwie</a>!</p>
+{% else %}
+<a href="{% url 'club_join' %}">Dołącz do Towarzystwa</a>.
+{% endif %}
+{% endwith %}
+
+
+</div>
+
+{% endblock %}
diff --git a/src/club/templates/club/membership_form.html b/src/club/templates/club/membership_form.html
new file mode 100644 (file)
index 0000000..d0c3250
--- /dev/null
@@ -0,0 +1,21 @@
+{% extends "base/base.html" %}
+
+
+{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %}
+
+
+{% block body %}
+<div class="white-box normal-text">
+
+       <h1>{% if membership %}Odnów swoje członkostwo w Towarzystwie Wolnych Lektur{% else %}Dołącz do Towarzystwa Wolnych Lektur{% endif %}</h1>
+
+<form method="POST" action="">
+  {% csrf_token %}
+
+  {{ form.as_p }}
+  <button type='submit'>Dołącz</button>
+</form>
+
+</div>
+
+{% 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 (file)
index 0000000..8bc9f90
--- /dev/null
@@ -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 (file)
index 0000000..421376d
--- /dev/null
@@ -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 (file)
index 0000000..42563e8
--- /dev/null
@@ -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 (file)
index 0000000..28b6936
--- /dev/null
@@ -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 (file)
index 0000000..6da90e3
--- /dev/null
@@ -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 (file)
index 0000000..6baa9a4
--- /dev/null
@@ -0,0 +1,51 @@
+{% extends "base/base.html" %}
+
+
+{% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %}
+
+
+{% block body %}
+<div class="white-box normal-text">
+
+<h1>Plan płatności</h1>
+
+<div>{{ schedule.email }}</div>
+<div>{{ schedule.amount }}</div>
+<div>{{ schedule.plan.get_interval_display }}</div>
+
+<div>{{ schedule.is_active|yesno:"Aktywne,Nieaktywne" }}</div>
+
+<div>Start: {{ schedule.started_at }}</div>
+<div>Opłacone do: {{ schedule.expires_at|default:"brak" }} {% if schedule.is_cancelled %}(anulowana){% endif %}</div>
+
+{% if schedule.expires_at and not schedule.is_cancelled %}
+<form method='post' action="{% url 'club_cancel' schedule.key %}">
+       {% csrf_token %}
+       <button type="submit">Anuluj płatność</button>
+</form>
+{% endif %}
+
+{% if schedule.is_expired %}
+  Płatność wygasła. <a href="{% url 'club_join' %}">Wykonaj nową płatność</a>.
+{% endif %}
+
+{% if schedule.membership %}
+  <p>
+  Członek/członkini Towarzystwa nr {{ schedule.membership.id }} ({{ schedule.membership.user }}).
+  </p>
+{% else %}
+<p>
+Płatność nie przypisana do członkostwa.<br>
+<a href="{% url 'club_claim' schedule.key %}">Przypisz</a>
+</p>
+{% endif %}
+
+
+
+
+       
+<a href="">
+
+</div>
+
+{% endblock %}
diff --git a/src/club/templates/club/widgets/plan.html b/src/club/templates/club/widgets/plan.html
new file mode 100644 (file)
index 0000000..fe0e4a8
--- /dev/null
@@ -0,0 +1,25 @@
+
+<div style="display:flex">
+{% for _1, optgroup, _2 in widget.optgroups %}
+  {% for option in optgroup %}
+    <div style="display: flex-item;">
+    {% with plan=option.label %}
+      <big>{{ plan.min_amount }} zł</big><br>
+      {{ plan.get_interval_display }}
+
+      {% for pm in plan.payment_methods %}
+        <div>
+               <input id="id_{{ option.value }}_{{ pm.slug }}" type="radio" name="payment_method" value="{{ option.value }}_{{ pm.slug }}">
+               <label for="id_{{ option.value }}_{{ pm.slug }}">
+        {% include pm.template_name %}
+               </label>
+       </div>
+      {% endfor %}
+
+    {% endwith %}
+    </div>
+  {% endfor %}
+{% endfor %}
+</div>
+
+
diff --git a/src/club/templatetags/__init__.py b/src/club/templatetags/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/club/templatetags/club.py b/src/club/templatetags/club.py
new file mode 100644 (file)
index 0000000..1a34169
--- /dev/null
@@ -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 (file)
index 0000000..38772a7
--- /dev/null
@@ -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 (file)
index 0000000..0a8b53a
--- /dev/null
@@ -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<key>[-a-z0-9]+)/$', views.ScheduleView.as_view(), name='club_schedule'),
+    url(r'^przylacz/(?P<key>[-a-z0-9]+)/$', views.claim, name='club_claim'),
+    url(r'^anuluj/(?P<key>[-a-z0-9]+)/$', views.cancel, name='club_cancel'),
+
+    url(r'^testowa-platnosc/(?P<key>[-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 (file)
index 0000000..e6dc996
--- /dev/null
@@ -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())
index 07f7ff1..106e94f 100644 (file)
@@ -61,6 +61,7 @@ INSTALLED_APPS_CONTRIB = [
     'django_extensions',
     'raven.contrib.django.raven_compat',
 
+    'club.apps.ClubConfig',
     'migdal',
     'django_comments',
     'django_comments_xtd',
index eb6a017..fbfd226 100644 (file)
@@ -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'),