Payment overhaul.
authorRadek Czajka <rczajka@rczajka.pl>
Thu, 28 Nov 2019 21:59:12 +0000 (22:59 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Thu, 28 Nov 2019 21:59:12 +0000 (22:59 +0100)
22 files changed:
src/club/admin.py
src/club/forms.py
src/club/locale/pl/LC_MESSAGES/django.mo
src/club/locale/pl/LC_MESSAGES/django.po
src/club/management/commands/prolong.py
src/club/migrations/0014_club.py [new file with mode: 0644]
src/club/migrations/0015_auto_20191127_0947.py [new file with mode: 0644]
src/club/migrations/0016_migrate_plans.py [new file with mode: 0644]
src/club/migrations/0017_auto_20191127_0959.py [new file with mode: 0644]
src/club/migrations/0018_auto_20191127_1002.py [new file with mode: 0644]
src/club/migrations/0019_remove_schedule_method.py [new file with mode: 0644]
src/club/migrations/0020_auto_20191127_1519.py [new file with mode: 0644]
src/club/migrations/0021_auto_20191127_1545.py [new file with mode: 0644]
src/club/models.py
src/club/payment_methods.py
src/club/templates/club/membership_form.html
src/club/templates/club/schedule.html
src/club/templates/payu/rec_payment.html
src/club/utils.py [new file with mode: 0644]
src/club/views.py
src/wolnelektury/abtests.py
src/wolnelektury/static/js/base.js

index b0c20c2..e0beaea 100644 (file)
@@ -9,10 +9,7 @@ from modeltranslation.admin import TranslationAdmin
 from . import models
 
 
-class PlanAdmin(admin.ModelAdmin):
-    list_display = ['min_amount', 'interval']
-
-admin.site.register(models.Plan, PlanAdmin)
+admin.site.register(models.Club)
 
 
 class PayUOrderInline(admin.TabularInline):
@@ -41,7 +38,7 @@ class PayUCardTokenInline(admin.TabularInline):
 
 
 class ScheduleAdmin(admin.ModelAdmin):
-    list_display = ['email', 'started_at', 'payed_at', 'expires_at', 'plan', 'amount', 'is_cancelled']
+    list_display = ['email', 'started_at', 'payed_at', 'expires_at', 'amount', 'monthly', 'yearly', 'is_cancelled']
     list_search = ['email']
     list_filter = ['is_cancelled']
     date_hierarchy = 'started_at'
@@ -53,7 +50,7 @@ admin.site.register(models.Schedule, ScheduleAdmin)
 
 class ScheduleInline(admin.TabularInline):
     model = models.Schedule
-    fields = ['email', 'plan', 'amount', 'method', 'is_cancelled', 'started_at', 'payed_at', 'expires_at', 'email_sent']
+    fields = ['email', 'amount', 'is_cancelled', 'started_at', 'payed_at', 'expires_at', 'email_sent']
     readonly_fields = fields
     extra = 0
     show_change_link = True
@@ -116,3 +113,6 @@ class PayUOrderAdmin(admin.ModelAdmin):
 
 
 admin.site.register(models.PayUOrder, PayUOrderAdmin)
+
+
+admin.site.register(models.Ambassador)
index 1906bc9..8df7783 100644 (file)
@@ -4,45 +4,24 @@
 from decimal import Decimal
 from django import forms
 from . import models
-from .payment_methods import method_by_slug, methods
 from .payu.forms import CardTokenForm
 
 
 class ScheduleForm(forms.ModelForm):
     class Meta:
         model = models.Schedule
-        fields = ['plan', 'method', 'amount', 'email']
+        fields = ['monthly', 'amount', 'email']
         widgets = {
-            'plan': forms.RadioSelect,
-            'method': forms.RadioSelect,
+            'amount': forms.HiddenInput,
+            'monthly': forms.HiddenInput,
         }
 
-    def __init__(self, *args, request=None, **kwargs):
-        super(ScheduleForm, self).__init__(*args, **kwargs)
-        self.request = request
-        self.plans = models.Plan.objects.all()
-        self.payment_methods = methods
-        self.fields['amount'].required = False
-
-    def clean(self):
-        cleaned_data = super(ScheduleForm, self).clean()
-
-        if 'plan' in cleaned_data:
-            cleaned_data['amount'] = self.fields['amount'].clean(
-                self.request.POST['amount-{}'.format(cleaned_data['plan'].id)]
-            )
-
-            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
-                )
-
-        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', 'Wybrana metoda płatności nie jest dostępna dla tego planu.')
-
+    def clean_amount(self):
+        value = self.cleaned_data['amount']
+        club = models.Club.objects.first()
+        if club and value < club.min_amount:
+            raise forms.ValidationError('Minimalna kwota to %d zł.' % club.min_amount)
+        return value
 
 
 class PayUCardTokenForm(CardTokenForm):
index 83c0057..821ce3c 100644 (file)
Binary files a/src/club/locale/pl/LC_MESSAGES/django.mo and b/src/club/locale/pl/LC_MESSAGES/django.mo differ
index 8f99642..440b7b9 100644 (file)
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-09-30 15:58+0200\n"
-"PO-Revision-Date: 2019-09-30 16:09+0200\n"
+"POT-Creation-Date: 2019-11-28 22:34+0100\n"
+"PO-Revision-Date: 2019-11-28 22:36+0100\n"
 "Last-Translator: \n"
 "Language-Team: \n"
 "Language: pl\n"
@@ -18,129 +18,121 @@ msgstr ""
 "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"
+"X-Generator: Poedit 2.2.4\n"
 
-#: models.py:24
-msgid "a month"
-msgstr "miesięcznie"
-
-#: models.py:25
-msgid "a year"
-msgstr "raz do roku"
-
-#: models.py:26
-msgid "in perpetuity"
-msgstr "jednorazowo"
-
-#: models.py:29
-msgid "inteval"
-msgstr "okres"
-
-#: models.py:30
-msgid "min amount"
+#: models.py:20
+msgid "minimum amount"
 msgstr "minimalna kwota"
 
-#: models.py:31
-msgid "default amount"
-msgstr "domyślna kwota"
+#: models.py:21
+msgid "minimum amount for year"
+msgstr "minimalna kwota na rok"
 
-#: models.py:32
-msgid "allow recurring"
-msgstr "płatności cykliczne"
+#: models.py:22
+msgid "proposed amounts for single payment"
+msgstr "proponowane kwoty dla pojedynczej wpłaty"
 
-#: models.py:33
-msgid "allow one time"
-msgstr "płatności jednorazowe"
+#: models.py:23
+msgid "default single amount"
+msgstr "domyślna kwota dla pojedynczej wpłaty"
 
-#: models.py:34
-msgid "active"
-msgstr "aktywny"
+#: models.py:24
+msgid "proposed amounts for monthly payments"
+msgstr "proponowane kwoty dla miesięcznych wpłat"
+
+#: models.py:25
+msgid "default monthly amount"
+msgstr "domyślna kwota dla miesięcznych wpłat"
 
-#: models.py:37 models.py:69
-msgid "plan"
-msgstr "plan"
+#: models.py:28
+msgid "club"
+msgstr "towarzystwo"
 
-#: models.py:38
-msgid "plans"
-msgstr "plany"
+#: models.py:29
+msgid "clubs"
+msgstr "towarzystwa"
 
-#: models.py:66
+#: models.py:43
 msgid "key"
 msgstr "klucz"
 
-#: models.py:67
+#: models.py:44
 msgid "email"
 msgstr "email"
 
-#: models.py:68 models.py:129
+#: models.py:45 models.py:120
 msgid "membership"
 msgstr "członkostwo"
 
-#: models.py:70
+#: models.py:46
 msgid "amount"
 msgstr "kwota"
 
-#: models.py:71
-msgid "method"
-msgstr "metoda płatności"
+#: models.py:47
+msgid "monthly"
+msgstr "miesięcznie"
+
+#: models.py:48
+msgid "yearly"
+msgstr "rocznie"
 
-#: models.py:72
+#: models.py:50
 msgid "cancelled"
 msgstr "anulowany"
 
-#: models.py:73
+#: models.py:51
 msgid "payed at"
 msgstr "opłacona"
 
-#: models.py:74
+#: models.py:52
 msgid "started at"
 msgstr "start"
 
-#: models.py:75
+#: models.py:53
 msgid "expires_at"
 msgstr "wygasa"
 
-#: models.py:79
+#: models.py:57
 msgid "schedule"
 msgstr "harmonogram"
 
-#: models.py:80
+#: models.py:58
 msgid "schedules"
 msgstr "harmonogramy"
 
-#: models.py:123
+#: models.py:114
 msgid "user"
 msgstr "użytkownik"
 
-#: models.py:124
+#: models.py:115
 msgid "created at"
 msgstr "utworzone"
 
-#: models.py:130
+#: models.py:121
 msgid "memberships"
 msgstr "członkostwa"
 
-#: models.py:152
+#: models.py:143
 msgid "days before"
 msgstr "dni przed"
 
-#: models.py:153
+#: models.py:144
 msgid "subject"
 msgstr "temat"
 
-#: models.py:154 payu/models.py:136
+#: models.py:145 payu/models.py:136
 msgid "body"
 msgstr "treść"
 
-#: models.py:157
+#: models.py:148
 msgid "reminder email"
 msgstr "email z przypomnieniem"
 
-#: models.py:158
+#: models.py:149
 msgid "reminder emails"
 msgstr "emaile z przypomnieniem"
 
-#: models.py:163
+#: models.py:154
 #, python-format
 msgid "a day before expiration"
 msgid_plural "%d days before expiration"
@@ -149,7 +141,7 @@ 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:165
+#: models.py:156
 #, python-format
 msgid "a day after expiration"
 msgid_plural "%d days after expiration"
@@ -158,7 +150,27 @@ msgstr[1] "%d dni po wygaśnięciu"
 msgstr[2] "%d dni po wygaśnięciu"
 msgstr[3] "%d dni przed wygaśnięciem"
 
-#: models.py:192
+#: models.py:160
+msgid "name"
+msgstr "nazwisko"
+
+#: models.py:161
+msgid "photo"
+msgstr "zdjęcie"
+
+#: models.py:162
+msgid "text"
+msgstr "tekst"
+
+#: models.py:165
+msgid "ambassador"
+msgstr "ambasador"
+
+#: models.py:166
+msgid "ambassadors"
+msgstr "ambasadorowie"
+
+#: models.py:197
 msgid "Towarzystwo Wolnych Lektur"
 msgstr ""
 
@@ -234,6 +246,30 @@ msgstr "notyfikacja PayU"
 msgid "PayU notifications"
 msgstr "notyfikacje PayU"
 
+#~ msgid "in perpetuity"
+#~ msgstr "jednorazowo"
+
+#~ msgid "inteval"
+#~ msgstr "okres"
+
+#~ msgid "allow recurring"
+#~ msgstr "płatności cykliczne"
+
+#~ msgid "allow one time"
+#~ msgstr "płatności jednorazowe"
+
+#~ msgid "active"
+#~ msgstr "aktywny"
+
+#~ msgid "plan"
+#~ msgstr "plan"
+
+#~ msgid "plans"
+#~ msgstr "plany"
+
+#~ msgid "method"
+#~ msgstr "metoda płatności"
+
 #~ msgid "payment"
 #~ msgstr "płatność"
 
index 3c8b36c..34ebe63 100644 (file)
@@ -9,7 +9,7 @@ from club.models import Schedule
 
 class Command(BaseCommand):
     def handle(self, *args, **options):
-        for s in Schedule.objects.filter(is_cancelled=False, expires_at__lt=now() + timedelta(1)):
+        for s in Schedule.objects.exclude(monthly=False, yearly=False).filter(is_cancelled=False, expires_at__lt=now() + timedelta(1)):
             print(s, s.email, s.expires_at)
             s.pay(None)
 
diff --git a/src/club/migrations/0014_club.py b/src/club/migrations/0014_club.py
new file mode 100644 (file)
index 0000000..1183bc3
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.6 on 2019-11-20 08:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0013_populate_payed_at'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Club',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('min_amount', models.IntegerField(verbose_name='minimum amount')),
+                ('min_for_year', models.IntegerField(verbose_name='minimum amount for year')),
+                ('single_amounts', models.CharField(max_length=255, verbose_name='proposed amounts for single payment')),
+                ('monthly_amounts', models.CharField(max_length=255, verbose_name='proposed amounts for monthly payments')),
+            ],
+        ),
+    ]
diff --git a/src/club/migrations/0015_auto_20191127_0947.py b/src/club/migrations/0015_auto_20191127_0947.py
new file mode 100644 (file)
index 0000000..0bd22be
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.6 on 2019-11-27 08:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0014_club'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='schedule',
+            name='monthly',
+            field=models.BooleanField(default=False, verbose_name='monthly'),
+        ),
+        migrations.AddField(
+            model_name='schedule',
+            name='yearly',
+            field=models.BooleanField(default=False, verbose_name='yearly'),
+        ),
+    ]
diff --git a/src/club/migrations/0016_migrate_plans.py b/src/club/migrations/0016_migrate_plans.py
new file mode 100644 (file)
index 0000000..fd2a9f9
--- /dev/null
@@ -0,0 +1,25 @@
+# Generated by Django 2.2.6 on 2019-11-27 08:49
+
+from django.db import migrations
+
+
+def migrate_plans(apps, schema_editor):
+    Schedule = apps.get_model('club', 'Schedule')
+    schedules = Schedule.objects.filter(method='payu-re')
+    schedules.filter(plan__interval=30).update(monthly=True)
+    schedules.filter(plan__interval=365).update(yearly=True)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0015_auto_20191127_0947'),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            migrate_plans,
+            migrations.RunPython.noop,
+            elidable=True,
+        )
+    ]
diff --git a/src/club/migrations/0017_auto_20191127_0959.py b/src/club/migrations/0017_auto_20191127_0959.py
new file mode 100644 (file)
index 0000000..ba2ed07
--- /dev/null
@@ -0,0 +1,25 @@
+# Generated by Django 2.2.6 on 2019-11-27 08:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0016_migrate_plans'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='club',
+            name='default_monthly_amount',
+            field=models.IntegerField(default=10, verbose_name='default monthly amount'),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='club',
+            name='default_single_amount',
+            field=models.IntegerField(default=100, verbose_name='default single amount'),
+            preserve_default=False,
+        ),
+    ]
diff --git a/src/club/migrations/0018_auto_20191127_1002.py b/src/club/migrations/0018_auto_20191127_1002.py
new file mode 100644 (file)
index 0000000..22e150d
--- /dev/null
@@ -0,0 +1,20 @@
+# Generated by Django 2.2.6 on 2019-11-27 09:02
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0017_auto_20191127_0959'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='schedule',
+            name='plan',
+        ),
+        migrations.DeleteModel(
+            name='Plan',
+        ),
+    ]
diff --git a/src/club/migrations/0019_remove_schedule_method.py b/src/club/migrations/0019_remove_schedule_method.py
new file mode 100644 (file)
index 0000000..6a32834
--- /dev/null
@@ -0,0 +1,17 @@
+# Generated by Django 2.2.6 on 2019-11-27 10:02
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0018_auto_20191127_1002'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='schedule',
+            name='method',
+        ),
+    ]
diff --git a/src/club/migrations/0020_auto_20191127_1519.py b/src/club/migrations/0020_auto_20191127_1519.py
new file mode 100644 (file)
index 0000000..1129a3e
--- /dev/null
@@ -0,0 +1,26 @@
+# Generated by Django 2.2.6 on 2019-11-27 14:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0019_remove_schedule_method'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Ambassador',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=255)),
+                ('photo', models.ImageField(blank=True, upload_to='')),
+            ],
+        ),
+        migrations.AlterField(
+            model_name='schedule',
+            name='monthly',
+            field=models.BooleanField(default=True, verbose_name='monthly'),
+        ),
+    ]
diff --git a/src/club/migrations/0021_auto_20191127_1545.py b/src/club/migrations/0021_auto_20191127_1545.py
new file mode 100644 (file)
index 0000000..e377d46
--- /dev/null
@@ -0,0 +1,37 @@
+# Generated by Django 2.2.6 on 2019-11-27 14:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('club', '0020_auto_20191127_1519'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='ambassador',
+            options={'ordering': ['name'], 'verbose_name': 'ambassador', 'verbose_name_plural': 'ambassadors'},
+        ),
+        migrations.AlterModelOptions(
+            name='club',
+            options={'verbose_name': 'club', 'verbose_name_plural': 'clubs'},
+        ),
+        migrations.AddField(
+            model_name='ambassador',
+            name='text',
+            field=models.CharField(default='', max_length=1024, verbose_name='text'),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='ambassador',
+            name='name',
+            field=models.CharField(max_length=255, verbose_name='name'),
+        ),
+        migrations.AlterField(
+            model_name='ambassador',
+            name='photo',
+            field=models.ImageField(blank=True, upload_to='', verbose_name='photo'),
+        ),
+    ]
index 62c1069..8c26d03 100644 (file)
@@ -11,64 +11,42 @@ from django import template
 from django.utils.timezone import now
 from django.utils.translation import ugettext_lazy as _, ungettext, ugettext, get_language
 from catalogue.utils import get_random_hash
-from .payment_methods import methods, method_by_slug
+from .payment_methods import recurring_payment_method, single_payment_method
 from .payu import models as payu_models
+from . import utils
 
 
-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)
-    default_amount = models.DecimalField(_('default amount'), max_digits=10, decimal_places=2)
-    allow_recurring = models.BooleanField(_('allow recurring'))
-    allow_one_time = models.BooleanField(_('allow one time'))
-    active = models.BooleanField(_('active'), default=True)
+class Club(models.Model):
+    min_amount = models.IntegerField(_('minimum amount'))
+    min_for_year = models.IntegerField(_('minimum amount for year'))
+    single_amounts = models.CharField(_('proposed amounts for single payment'), max_length=255)
+    default_single_amount = models.IntegerField(_('default single amount'))
+    monthly_amounts = models.CharField(_('proposed amounts for monthly payments'), max_length=255)
+    default_monthly_amount = models.IntegerField(_('default monthly amount'))
 
     class Meta:
-        verbose_name = _('plan')
-        verbose_name_plural = _('plans')
-
+        verbose_name = _('club')
+        verbose_name_plural = _('clubs')
+    
     def __str__(self):
-        return "%s %s" % (self.min_amount, self.get_interval_display())
+        return 'Klub'
     
-    class Meta:
-        ordering = ('interval',)
+    def proposed_single_amounts(self):
+        return [int(x) for x in self.single_amounts.split(',')]
 
-    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 proposed_monthly_amounts(self):
+        return [int(x) for x in self.monthly_amounts.split(',')]
 
-    def get_next_installment(self, date):
-        if self.interval == self.PERPETUAL:
-            return datetime.max - timedelta(1)
-        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])
+    monthly = models.BooleanField(_('monthly'), default=True)
+    yearly = models.BooleanField(_('yearly'), default=False)
+    
     is_cancelled = models.BooleanField(_('cancelled'), default=False)
     payed_at = models.DateTimeField(_('payed at'), null=True, blank=True)
     started_at = models.DateTimeField(_('started at'), auto_now_add=True)
@@ -100,7 +78,7 @@ class Schedule(models.Model):
         return reverse('club_thanks', args=[self.key])
 
     def get_payment_method(self):
-        return method_by_slug[self.method]
+        return recurring_payment_method if self.monthly or self.yearly else single_payment_method
 
     def is_expired(self):
         return self.expires_at is not None and self.expires_at <= now()
@@ -108,6 +86,19 @@ class Schedule(models.Model):
     def is_active(self):
         return self.payed_at is not None and (self.expires_at is None or self.expires_at > now())
 
+    def is_recurring(self):
+        return self.monthly or self.yearly
+
+    def get_next_installment(self, date):
+        if self.yearly:
+            return utils.add_year(date)
+        if self.monthly:
+            return utils.add_month(date)
+        club = Club.objects.first()
+        if club is not None and self.amount >= club.min_for_year:
+            return utils.add_year(date)
+        return utils.add_month(date)
+
     def send_email(self):
         ctx = {'schedule': self}
         send_mail(
@@ -165,6 +156,20 @@ class ReminderEmail(models.Model):
             return ungettext('a day after expiration', '%d days after expiration', n=-self.days_before)
 
 
+class Ambassador(models.Model):
+    name = models.CharField(_('name'), max_length=255)
+    photo = models.ImageField(_('photo'), blank=True)
+    text = models.CharField(_('text'), max_length=1024)
+
+    class Meta:
+        verbose_name = _('ambassador')
+        verbose_name_plural = _('ambassadors')
+        ordering = ['name']
+    
+    def __str__(self):
+        return self.name
+
+        
 ########
 #      #
 # PayU #
@@ -208,7 +213,7 @@ class PayUOrder(payu_models.Order):
             n = now()
             if since is None or since < n:
                 since = n
-            new_exp = self.schedule.plan.get_next_installment(since)
+            new_exp = self.schedule.get_next_installment(since)
             if self.schedule.payed_at is None:
                 self.schedule.payed_at = n
             if self.schedule.expires_at is None or self.schedule.expires_at < new_exp:
@@ -225,3 +230,5 @@ class PayUCardToken(payu_models.CardToken):
 
 class PayUNotification(payu_models.Notification):
     order = models.ForeignKey(PayUOrder, models.CASCADE, related_name='notification_set')
+
+
index 99694ef..7657701 100644 (file)
@@ -71,27 +71,14 @@ class PayPal(PaymentMethod):
         return reverse('club_dummy_payment', args=[schedule.key])
 
 
-methods = []
-
-pos= getattr(settings, 'CLUB_PAYU_RECURRING_POS', None)
+pos = getattr(settings, 'CLUB_PAYU_RECURRING_POS', None)
 if pos:
-    payure_method = PayURe(pos)
-    methods.append(payure_method)
+    recurring_payment_method = PayURe(pos)
 else:
-    payure_method = None
+    recurring_payment_method = None
 
 pos = getattr(settings, 'CLUB_PAYU_POS', None)
 if pos:
-    payu_method = PayU(pos)
-    methods.append(payu_method)
+    single_payment_method = PayU(pos)
 else:
-    payu_method = None
-
-
-#methods.append(PayPal())
-
-
-method_by_slug = {
-    m.slug: m
-    for m in methods
-}
+    single_payment_method = None
index ecba877..6e64de1 100644 (file)
 {% extends request.session.from_app|yesno:"base/app.html,base/base.html" %}
 {% load chunks %}
-
+{% load thumbnail %}
 
 {% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %}
 
 
 {% block body %}
 
-<style>
-  .payment-method.disabled {
-  opacity: .5;
-  filter: grayscale(100%);
-  }
-  
+  <style>
+   .payment-method.disabled {
+       opacity: .5;
+       filter: grayscale(100%);
+   }
+
+   .button {
+       box-sizing: border-box;
+       display: inline-block;
+       text-align: center;
+   }
+   .kwota, .inna .button, .plan-toggle, .inna input {
+       border: 1px solid black;
+       background: none;
+       cursor: pointer;
+       padding: 10px 0;
+       margin-right: 3%;
+       margin-bottom: 10px;
+       line-height: 3em;
+   }
+   .plan-toggle {
+       width: 46.5%;
+   }
+   .kwota {
+       width: 30%;
+   }
+   .kwota:after {
+       content: " zł";
+   }
+   .kwota.yearly {
+       background: orange;
+
+   }
+   .inna .button {
+       width: 63%;
+   }
+   .inna input {
+       width: 60%;
+       padding-left: 1.5%;
+       padding-right: 1.5%;
+       height: 3em !important;
+       text-align: center;
+   }
+   .chunk-alt {
+       position: relative;
+       overflow: hidden;
+   }
+   .chunk-alt .chunk {
+       top: 0;
+   }
+
+   .kwota.active, .plan-toggle.active {
+       background: black;
+       color: white;
+   }
+   .inna input {display: none;}
+   .inna.active input {display: inline;}
+   .inna.active .button {display: none;}
+
+   .ambassador {
+       display: flex;
+       justify-content: end;
+       padding: 2em;
+       margin-bottom: 1em;
+       border: 1px solid #ddd;
+   }
+   .ambassador img {
+       border-radius: 100%;
+       margin-left: 1em;
+       align-self: center;
+   }
+   .ambassador div {
+       text-align: right;
+       align-self: center;
+       line-height: 1.5em;
+   }
+
+   .club-form-info {
+       line-height: 1.5em;
+   }
+
+   @media screen and (max-width: 1023px) {
+       .club-form-info {
+           margin-top: 2em;
+       }
+   }
+
+
+   @media screen and (min-width: 1024px) {
+       .club-form-info {
+           border-left: 1px solid #ddd;
+           padding-left: 2em;
+           margin-left: 2em;
+       }
+       .twocol {
+           display: flex;
+       }
+       .twocol > form, .twocol > div {
+           flex: 1;
+       }
+   }
+
   </style>
 
 
-<div class="white-box normal-text">
-
-    <h1>{% if membership %}Odnów swoje członkostwo w Towarzystwie Przyjaciół Wolnych Lektur{% else %}Dołącz do Towarzystwa Przyjaciół Wolnych Lektur{% endif %}</h1>
-
-{% chunk 'club_form_top' %}
-
-<form method="POST" action="" id="payment-form" class="wlform">
-  {% csrf_token %}
-
-  <h2>Zadeklaruj, jak często i jaką kwotą chcesz nas wspierać:</h2>
-
-  <ul class="errorlist">
-  {% for e in  form.non_field_errors %}
-    <li>{{ e }}</li>
-  {% endfor %}
-    {% for e in  form.plan.errors %}
-    <li>{{ e }}</li>
-  {% endfor %}
-    {% for e in  form.amount.errors %}
-    <li>{{ e }}</li>
-    {% endfor %}
-  </ul>
-
-  {% for plan in form.plans %}
-  
-  <div>
-    <input class="plan" type="radio" name="plan" value="{{ plan.id }}" id="plan{{ plan.id }}" data-methods="{% for m in plan.payment_methods %}{{ m.slug }} {% endfor %}">
-    <label for="plan{{ plan.id }}">
-      <input
-        name="amount-{{ plan.id }}"
-        type="number"
-        placeholder="min. {{ plan.min_amount|floatformat:0 }}"
-        value="{{ plan.default_amount|floatformat:0 }}"
-        min="{{ plan.min_amount|floatformat:0 }}"
-        step="1"
-        style="width: 5em;"
-        > zł
-      {{ plan.get_interval_display }}
-    </label>
+  <div class="white-box normal-text">
+
+    <h1>Wspieraj Wolne Lektury</h1>
+    <h2 style="margin-bottom:2em;">
+      {% if membership %}Dziękujemy za Twoje dotychczasowe zaangażowanie! Wesprzyj nas ponownie!{% else %}Dziękujemy, że chcesz razem z nami uwalniać książki!{% endif %}</h2>
+
+    {% chunk 'club_form_top' %}
+    <div class='twocol'>
+
+      <form method="POST" action="" id="payment-form" class="wlform">
+        {% csrf_token %}
+        <h3>Zadeklaruj, jak często i jaką kwotą chcesz nas wspierać:</h3>
+
+        <ul class="errorlist">
+          {% for e in  form.non_field_errors %}
+            <li>{{ e }}</li>
+          {% endfor %}
+          {% for e in  form.plan.errors %}
+            <li>{{ e }}</li>
+          {% endfor %}
+          {% for e in  form.amount.errors %}
+            <li>{{ e }}</li>
+          {% endfor %}
+        </ul>
+
+        {{ form.amount }}
+        {{ form.monthly }}
+        <div>
+          <span class="button plan-toggle" data-plan="plan-single" data-monthly="False">jednorazowo</span>
+          <span class="button plan-toggle active" data-plan="plan-monthly" data-monthly="True">miesięcznie</span>
+        </div>
+
+        <div class="plan" id="plan-single" style="display:none;" data-monthly="False" data-min-for-year="{{ club.min_for_year }}" data-amount="{{ club.default_single_amount }}">
+          {% for amount in club.proposed_single_amounts %}
+            <span class="button kwota{% if amount == club.default_single_amount %} active{% endif %}{% if amount >= club.min_for_year %} yearly{% endif %}">{{ amount }}</span>
+          {% endfor %}
+
+          <span class="inna">
+            <span class="button">inna kwota</span>
+            <input type="number" min="{{ club.min_amount }}">
+          </span>
+        </div>
+
+
+        <div class="plan" id="plan-monthly" data-monthly="True" data-amount="{{ club.default_monthly_amount }}">
+          {% for amount in club.proposed_monthly_amounts %}
+            <span class="button kwota{% if amount == club.default_monthly_amount %} active{% endif %}">{{ amount }}</span>
+          {% endfor %}
+
+          <span class="inna">
+            <span class="button">inna kwota</span>
+            <input type="number" min="{{ club.min_amount }}">
+          </span>
+        </div>
+
+        <h3>
+          Podaj nam swój adres e-mail, żebyśmy mogli się z Tobą skontaktować:
+        </h3>
+
+        <p>
+          {{ form.email }}</p>
+        <button class="submit" type='submit'>Dołącz</button>
+      </form>
+
+      <div class="club-form-info">
+        {% if ambassador %}
+          <div class="ambassador">
+            <div>
+              <em>
+                {{ ambassador.text }}
+              </em>
+              <div style="font-size: 1.2em">{{ ambassador.name }}</div>
+            </div>
+            {% if ambassador.photo %}
+              <img src="{% thumbnail ambassador.photo "100x100" as thumb %}{{ thumb.url }}{% empty %}{{ ambassador.photo.url }}{% endthumbnail %}">
+            {% endif %}
+          </div>
+        {% endif %}
+        <div class="chunk-alt">
+          <div class="chunk chunk-monthly">
+            {% chunk 'club-form-info-monthly' %}
+          </div>
+          <div class="chunk chunk-single" style="display: none;">
+            {% chunk 'club-form-info-single' %}
+          </div>
+          <div class="chunk chunk-single-year" style="display: none;">
+            {% chunk 'club-form-info-single-year' %}
+          </div>
   </div>
-
-  {% endfor %}
-
-  <h2>Wybierz metodę płatności:</h2>
-  
-  <ul class="errorlist">
-  {% for e in form.method.errors %}
-  <li>{{ e }}</li>
-  {% endfor %}
-  </ul>
-  
-  {% for payment_method in form.payment_methods %}
-  <div class="payment-method" id="payment-method-{{ payment_method.slug }}">
-    <input type="radio" id="method{{ payment_method.slug }}" name="method" value="{{ payment_method.slug }}">
-    <label for="method{{ payment_method.slug }}">
-      {% include payment_method.template_name %}
-    </label>
-  </div>
-  {% endfor %}
-
-  <h2>
-    Podaj nam swój adres e-mail, żebyśmy mogli się z Tobą skontaktować:
-  </h2>
-
-  <p>
-    {{ form.email }}</p>
-  <button class="submit" type='submit'>Dołącz</button>
-</form>
-
-{% chunk 'club_form_bottom' %}
+</div>
+</div>
 
 </div>
 
index bb1bf57..bfae8c5 100644 (file)
@@ -15,11 +15,11 @@ Od <strong>{{ schedule.started_at.date }}</strong>
 {% if schedule.expires_at %}
   do <strong>{{ schedule.expires_at.date }}</strong>
 {% endif %}
-wspierasz nas kwotą {{ schedule.amount }} zł {{ schedule.plan.get_interval_display }}.
+wspierasz nas kwotą {{ schedule.amount }} zł{% if schedule.monthly %} miesięcznie{% endif %}{% if schedule.yearly %} rocznie{% endif %}.
 </p>
 
 {% if schedule.is_active %}
-  {% if schedule.get_payment_method.is_recurring %}
+  {% if schedule.is_recurring %}
      {% if schedule.is_cancelled %}
          <p><strong>Płatność anulowana.</strong></p>
          <p>
index 56425e4..254c2bc 100644 (file)
@@ -7,11 +7,9 @@
 {% block body %}
 <div class="white-box normal-text">
 
-       <h1>Płatność cykliczna przez PayU</h1>
+       <h1>Wspierasz Wolne Lektury</h1>
 
-       <p>     {{ schedule.email }}</p>
-       <p>     {{ schedule.amount }}</p>
-       <p>     {{ schedule.plan.get_interval_display }}</p>
+       <p>Zlecasz comiesięczną płatność w wysokości {{ schedule.amount}} zł. Dziękujemy!</p>
 
        <form id="theform" method='POST'>
                {% csrf_token %}
diff --git a/src/club/utils.py b/src/club/utils.py
new file mode 100644 (file)
index 0000000..699f503
--- /dev/null
@@ -0,0 +1,13 @@
+from datetime import timedelta
+
+
+def add_year(date):
+    return date.replace(year=date.year + 1)
+
+def add_month(date):
+    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
+
index e469345..32daffd 100644 (file)
@@ -14,7 +14,7 @@ from .payu import views as payu_views
 from .forms import ScheduleForm, PayUCardTokenForm
 from . import models
 from .helpers import get_active_schedule
-from .payment_methods import payure_method
+from .payment_methods import recurring_payment_method
 
 
 class ClubView(TemplateView):
@@ -48,15 +48,13 @@ class JoinView(CreateView):
         else:
             return super(JoinView, self).get(request)
 
-    def get_form_kwargs(self):
-        kwargs = super().get_form_kwargs()
-        kwargs['request'] = self.request
-        return kwargs
-
     def get_context_data(self, **kwargs):
         c = super(JoinView, self).get_context_data(**kwargs)
         c['membership'] = getattr(self.request.user, 'membership', None)
         c['active_menu_item'] = 'club'
+        c['club'] = models.Club.objects.first()
+
+        c['ambassador'] = models.Ambassador.objects.all().order_by('?').first()
         return c
 
     def get_initial(self):
@@ -132,7 +130,7 @@ class PayURecPayment(payu_views.RecPayment):
         return get_object_or_404(models.Schedule, key=self.kwargs['key'])
 
     def get_pos(self):
-        pos_id = payure_method.pos_id
+        pos_id = recurring_payment_method.pos_id
         return POSS[pos_id]
 
     def get_success_url(self):
index 7fde579..6dd0ea2 100644 (file)
@@ -4,10 +4,12 @@ from django.conf import settings
 
 def context_processor(request):
     ab = {}
+    overrides = getattr(settings, 'AB_TESTS_OVERRIDES', {})
     for abtest, nvalues in settings.AB_TESTS.items():
-        print(abtest, nvalues)
-        ab[abtest] = hashlib.md5(
+        ab[abtest] = overrides.get(
+            abtest,
+            hashlib.md5(
                 (abtest + request.META['REMOTE_ADDR']).encode('utf-8')
             ).digest()[0] % nvalues
-    print(ab)
+        )
     return {'AB': ab}
index 2d0ebbf..2c2daa5 100644 (file)
           p.prev('.read-more-show').removeClass('hide'); // Hide only the preceding "Read More"
           e.preventDefault();
         });
+
+
+        function update_info() {
+            var amount = parseInt($("#id_amount").val());
+            var monthly =  $("#id_monthly").val() == 'True';
+            if (monthly) slug = "monthly";
+            else if (amount >= parseInt($("#plan-single").attr('data-min-for-year'))) slug = 'single-year';
+            else slug = 'single';
+
+            var chunk = $('.club-form-info .chunk-' + slug);
+            if (chunk.css('display') == 'none') {
+                $('.chunk-alt').css('height', $('.chunk-alt').height());
+                $('.chunk-alt .chunk').css('position', 'absolute');
+
+                $('.club-form-info .chunk').fadeOut();
+                $('.club-form-info .chunk.chunk-' + slug).fadeIn(function() {
+                    $('.chunk-alt .chunk').css('position', 'static');
+                    $('.chunk-alt').css('height', 'auto');
+                });
+                $('.chunk-alt').animate({height: chunk.height()}, 100);
+            }
+        }
+        
+        $("#id_amount").val($("#plan-monthly").attr('data-amount'));
+        
+        $(".button.kwota").click(function() {
+            var plan = $(this).closest('.plan');
+            $('.kwota', plan).removeClass('active')
+            $('.inna', plan).removeClass('active')
+            $(this).addClass('active');
+
+            var amount = $(this).text();
+            plan.attr("data-amount", amount);
+            $("#id_amount").val(amount);
+
+            update_info();
+            return false;
+        });
+
+        $(".plan-toggle").click(function() {
+            $(".plan-toggle").removeClass('active');
+            $(this).addClass('active')
+            $(".plan").hide();
+            var plan = $("#" + $(this).attr('data-plan'));
+            plan.show();
+            $("#id_amount").val(plan.attr('data-amount'));
+            $("#id_monthly").val(plan.attr('data-monthly'));
+
+            update_info();
+            return false;
+        });
+
+        $(".inna .button").click(function() {
+            var plan = $(this).closest('.plan');
+            $('.kwota', plan).removeClass('active');
+            $(this).parent().addClass('active');
+            $('input', plan).focus();
+
+            var amount = $('input', $(this).parent()).val();
+            plan.attr("data-amount", amount);
+            $("#id_amount").val(amount);
+
+            update_info();
+            return false;
+        });
+        
+        $(".inna input").on('input', function() {
+            var plan = $(this).closest('.plan');
+            $('.kwota', plan).removeClass('active');
+            var amount = $(this).val();
+            plan.attr("data-amount", amount);
+            $("#id_amount").val(amount);
+
+            update_info();
+            return false;
+        });
+
     });
 })(jQuery);