From 97c8c6d7c7961976172bece182832d01c9c0fc4b Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Thu, 28 Nov 2019 22:59:12 +0100 Subject: [PATCH 1/1] Payment overhaul. --- src/club/admin.py | 12 +- src/club/forms.py | 39 +-- src/club/locale/pl/LC_MESSAGES/django.mo | Bin 3010 -> 3317 bytes src/club/locale/pl/LC_MESSAGES/django.po | 162 +++++++---- src/club/management/commands/prolong.py | 2 +- src/club/migrations/0014_club.py | 23 ++ .../migrations/0015_auto_20191127_0947.py | 23 ++ src/club/migrations/0016_migrate_plans.py | 25 ++ .../migrations/0017_auto_20191127_0959.py | 25 ++ .../migrations/0018_auto_20191127_1002.py | 20 ++ .../migrations/0019_remove_schedule_method.py | 17 ++ .../migrations/0020_auto_20191127_1519.py | 26 ++ .../migrations/0021_auto_20191127_1545.py | 37 +++ src/club/models.py | 95 ++++--- src/club/payment_methods.py | 23 +- src/club/templates/club/membership_form.html | 269 +++++++++++++----- src/club/templates/club/schedule.html | 4 +- src/club/templates/payu/rec_payment.html | 6 +- src/club/utils.py | 13 + src/club/views.py | 12 +- src/wolnelektury/abtests.py | 8 +- src/wolnelektury/static/js/base.js | 77 +++++ 22 files changed, 664 insertions(+), 254 deletions(-) create mode 100644 src/club/migrations/0014_club.py create mode 100644 src/club/migrations/0015_auto_20191127_0947.py create mode 100644 src/club/migrations/0016_migrate_plans.py create mode 100644 src/club/migrations/0017_auto_20191127_0959.py create mode 100644 src/club/migrations/0018_auto_20191127_1002.py create mode 100644 src/club/migrations/0019_remove_schedule_method.py create mode 100644 src/club/migrations/0020_auto_20191127_1519.py create mode 100644 src/club/migrations/0021_auto_20191127_1545.py create mode 100644 src/club/utils.py diff --git a/src/club/admin.py b/src/club/admin.py index b0c20c2c5..e0beaeada 100644 --- a/src/club/admin.py +++ b/src/club/admin.py @@ -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) diff --git a/src/club/forms.py b/src/club/forms.py index 1906bc9fe..8df778338 100644 --- a/src/club/forms.py +++ b/src/club/forms.py @@ -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): diff --git a/src/club/locale/pl/LC_MESSAGES/django.mo b/src/club/locale/pl/LC_MESSAGES/django.mo index 83c0057d7771da1a3f44b1ee2eccda5895bf7c1a..821ce3c27615219f61a0b85ca2af0b7478807086 100644 GIT binary patch literal 3317 zcmb`IO^g&p6vqoe)K&aIL{!8=TwujsW`@Nq0|U`rAR8fUa92H;n40d|-QMo5npAZ& z^n{CK4SF$#$U&D2F;Nf1M2&b6O_YgV2nRzvm}tZ!As!8${J-uVW?+|tiIu7TRlTbB z>b+MV^V`a0rx?n1)U~J=mN2#-442}A^7b;u?grlh?*UJO4}hn^2f?%8L*RLEC3p$! z1FwMhg4e*i!0RB{S#l?1YrsBmE4TqHgD-=m_ZCQc?}5v~(;!sYM`z#E*TA z59ysR_!CI_KNtKRT#f!!@DcD17$v{gfDo|(ko32L6mqrb?*_@v7<7<)8^9kyvUe3Edw+wJ$K^0a{na4Z=?5v!L6H0!2JvGf_|SSU z6&wd)f=z;7Vb5L%NpB@Sq_-BN{oDjT366pkcfFu3I9Ko#NPd3|;>XV7LwP#~PJtIe z0}jDBjei2J06zyw?>i7Kuy!$i5u|v3Dfnk0|Eu72ko;MSNn~#oxEWjrZUXm%bgnc= z_TL04-V;UtUGP5i-!FIuqeZPZORJ8c}LNv7^xr~Ss~wO zey2QEG{^_KC!IpMpz}<3h)M~S{H5|7D&0kjmkRCwQ&}sIaaGYKP|lu4rL#hLphEYc zA9WCwqdtsEcb1BSicr`ysE?!4nH)l0hf0NVLHoBJmCpSZRC-(JtZYNwh)VgTy|18B zPAN}pL?~B=(qkh!YJ@UIYh0w0+!e;-u@0oVwQqBQ(y>3~yCU{=F{M{hOwz`aCZC@* zA(qPX)igUSXQfN4y(;_|0zRb;ceR@GO_bXd+!HAmQ?WE$9&Pw07x#N4w+jdI`E?0H zaxsO4BOozUgI#DsJYcFMPmLAHF8z%MaW0`BDfa_euo#q?VOl+(pI6F4Z zG7s|wnJ1@262?5zDxL`uQEt8?wg^6yH>AF8Xe;WFv0HaWdPyi*r(v0mg(*^RlR6zC z7R#eCODxVAMHi-Q+-Mqb#ysa0WC`c9%KHw>hBmQt&`$fF^I|e>oe2%`_#x-8JmS+N zXB3-S<&{!-hf^*)m0@0~RJT39rBp7J`VI&iI}=7I8;V#PXdV!1IuX;-nUEst=~sCp z>^nGiaBp|<@?fcN1iO!vITL9Ei;&M-8=>&kPVUYKV`W^MoY?0KcjwVsQ_?tlm8ZF3x8xBa;1%E z>1N9E2Z%Ck1q;H&Z85jiKCV@uGw*vkB7a!L4S#O!(}^%TfVkr#Qlvzy>DxfFu~FVV zX@Y6;9H&XJmN!d>MCPxk$_{@PaC-ZIvuAn1)Wt&9o~+{y5qYG%MF%5o=m-ZEzbF^W zh4MUA*yK?+bA2#5>AngHwT;!51ZYGSwn3FS-yz0=b@>Fr@M zW6VYqO$-np9byc4F;No(k+43Z0TT^-(U^GhM7Zch62nPvKEA)9R-iKf{&d%LSN;E0 z-G7dLzh?g0isETQn;;$_&J~P#2A^EUf%aU{n7eTrOZXz*iwAKzMmU16;0k;T2k}EJ z<9QswuTkTChimZ%+-c0b`IVDhbgb;VdE>L#&-ILVKMr$!05$Qf@9!dynpaVYy@8te zZSNUW0v~%XqQ?0amH1EC$Nc6RCwlNVya#WfGX5L22z_Lu|4XO=SD_LNeE$|yWfR^f zP>D?;LzrpQb1z`85bPDAYvwmyPBg%qs7l^NO?VcS$S0@)Kf^EBp3hPD&GL}$%TQZ4 zkL&O(s={x)KYFiv|3D@756)|#B8U5M7?0p)Oz|WR;lHQ~hL}wWt;IDM;A-598t{4l z|9<2VbI^Ol+e9VS_17l`S${3XX*%>0eS{k5E2Kzs88z`0-~Y4s7gSfxdAxUd4 z4W_3|({@}fu$gH_Htrqf^6UM!*qU=`eqdl~xH38ZSS8rC`|--;_R7S<`GLcQWtAPf z7nYaaE6g-PHy2)czuJvMXHwe@YDthL^_H>qtkaG=rn&f<&EjP7Of?KSXn#9fx)EAC=zH5&!@I diff --git a/src/club/locale/pl/LC_MESSAGES/django.po b/src/club/locale/pl/LC_MESSAGES/django.po index 8f9964259..440b7b94e 100644 --- a/src/club/locale/pl/LC_MESSAGES/django.po +++ b/src/club/locale/pl/LC_MESSAGES/django.po @@ -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ść" diff --git a/src/club/management/commands/prolong.py b/src/club/management/commands/prolong.py index 3c8b36cb1..34ebe63ad 100644 --- a/src/club/management/commands/prolong.py +++ b/src/club/management/commands/prolong.py @@ -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 index 000000000..1183bc3e0 --- /dev/null +++ b/src/club/migrations/0014_club.py @@ -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 index 000000000..0bd22be37 --- /dev/null +++ b/src/club/migrations/0015_auto_20191127_0947.py @@ -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 index 000000000..fd2a9f905 --- /dev/null +++ b/src/club/migrations/0016_migrate_plans.py @@ -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 index 000000000..ba2ed0752 --- /dev/null +++ b/src/club/migrations/0017_auto_20191127_0959.py @@ -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 index 000000000..22e150d8e --- /dev/null +++ b/src/club/migrations/0018_auto_20191127_1002.py @@ -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 index 000000000..6a3283476 --- /dev/null +++ b/src/club/migrations/0019_remove_schedule_method.py @@ -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 index 000000000..1129a3e7a --- /dev/null +++ b/src/club/migrations/0020_auto_20191127_1519.py @@ -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 index 000000000..e377d461f --- /dev/null +++ b/src/club/migrations/0021_auto_20191127_1545.py @@ -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'), + ), + ] diff --git a/src/club/models.py b/src/club/models.py index 62c1069f2..8c26d03a1 100644 --- a/src/club/models.py +++ b/src/club/models.py @@ -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') + + diff --git a/src/club/payment_methods.py b/src/club/payment_methods.py index 99694ef47..7657701ab 100644 --- a/src/club/payment_methods.py +++ b/src/club/payment_methods.py @@ -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 diff --git a/src/club/templates/club/membership_form.html b/src/club/templates/club/membership_form.html index ecba87760..6e64de1d0 100644 --- a/src/club/templates/club/membership_form.html +++ b/src/club/templates/club/membership_form.html @@ -1,91 +1,208 @@ {% extends request.session.from_app|yesno:"base/app.html,base/base.html" %} {% load chunks %} - +{% load thumbnail %} {% block titleextra %}Towarzystwo Wolnych Lektur{% endblock %} {% block body %} - -
- -

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

- -{% chunk 'club_form_top' %} - -
- {% csrf_token %} - -

Zadeklaruj, jak często i jaką kwotą chcesz nas wspierać:

- -
    - {% for e in form.non_field_errors %} -
  • {{ e }}
  • - {% endfor %} - {% for e in form.plan.errors %} -
  • {{ e }}
  • - {% endfor %} - {% for e in form.amount.errors %} -
  • {{ e }}
  • - {% endfor %} -
- - {% for plan in form.plans %} - -
- - +
+ +

Wspieraj Wolne Lektury

+

+ {% if membership %}Dziękujemy za Twoje dotychczasowe zaangażowanie! Wesprzyj nas ponownie!{% else %}Dziękujemy, że chcesz razem z nami uwalniać książki!{% endif %}

+ + {% chunk 'club_form_top' %} +
+ + + {% csrf_token %} +

Zadeklaruj, jak często i jaką kwotą chcesz nas wspierać:

+ +
    + {% for e in form.non_field_errors %} +
  • {{ e }}
  • + {% endfor %} + {% for e in form.plan.errors %} +
  • {{ e }}
  • + {% endfor %} + {% for e in form.amount.errors %} +
  • {{ e }}
  • + {% endfor %} +
+ + {{ form.amount }} + {{ form.monthly }} +
+ jednorazowo + miesięcznie +
+ + + + +
+ {% for amount in club.proposed_monthly_amounts %} + {{ amount }} + {% endfor %} + + + inna kwota + + +
+ +

+ Podaj nam swój adres e-mail, żebyśmy mogli się z Tobą skontaktować: +

+ +

+ {{ form.email }}

+ + + +
+ {% if ambassador %} +
+
+ + {{ ambassador.text }} + +
{{ ambassador.name }}
+
+ {% if ambassador.photo %} + + {% endif %} +
+ {% endif %} +
+
+ {% chunk 'club-form-info-monthly' %} +
+ +
- - {% endfor %} - -

Wybierz metodę płatności:

- -
    - {% for e in form.method.errors %} -
  • {{ e }}
  • - {% endfor %} -
- - {% for payment_method in form.payment_methods %} -
- - -
- {% endfor %} - -

- Podaj nam swój adres e-mail, żebyśmy mogli się z Tobą skontaktować: -

- -

- {{ form.email }}

- - - -{% chunk 'club_form_bottom' %} +
+
diff --git a/src/club/templates/club/schedule.html b/src/club/templates/club/schedule.html index bb1bf57fb..bfae8c56c 100644 --- a/src/club/templates/club/schedule.html +++ b/src/club/templates/club/schedule.html @@ -15,11 +15,11 @@ Od {{ schedule.started_at.date }} {% if schedule.expires_at %} do {{ schedule.expires_at.date }} {% 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 %}.

{% if schedule.is_active %} - {% if schedule.get_payment_method.is_recurring %} + {% if schedule.is_recurring %} {% if schedule.is_cancelled %}

Płatność anulowana.

diff --git a/src/club/templates/payu/rec_payment.html b/src/club/templates/payu/rec_payment.html index 56425e4f7..254c2bc46 100644 --- a/src/club/templates/payu/rec_payment.html +++ b/src/club/templates/payu/rec_payment.html @@ -7,11 +7,9 @@ {% block body %}

-

Płatność cykliczna przez PayU

+

Wspierasz Wolne Lektury

-

{{ schedule.email }}

-

{{ schedule.amount }}

-

{{ schedule.plan.get_interval_display }}

+

Zlecasz comiesięczną płatność w wysokości {{ schedule.amount}} zł. Dziękujemy!

{% csrf_token %} diff --git a/src/club/utils.py b/src/club/utils.py new file mode 100644 index 000000000..699f5030c --- /dev/null +++ b/src/club/utils.py @@ -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 + diff --git a/src/club/views.py b/src/club/views.py index e469345fc..32daffde1 100644 --- a/src/club/views.py +++ b/src/club/views.py @@ -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): diff --git a/src/wolnelektury/abtests.py b/src/wolnelektury/abtests.py index 7fde579b0..6dd0ea2fc 100644 --- a/src/wolnelektury/abtests.py +++ b/src/wolnelektury/abtests.py @@ -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} diff --git a/src/wolnelektury/static/js/base.js b/src/wolnelektury/static/js/base.js index 2d0ebbf64..2c2daa5f4 100644 --- a/src/wolnelektury/static/js/base.js +++ b/src/wolnelektury/static/js/base.js @@ -270,6 +270,83 @@ 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); -- 2.20.1