From d527b63f5320d32e5c598354fd60ebc00d88d7bb Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 30 Sep 2019 15:58:06 +0200 Subject: [PATCH 1/1] Make club app a little more manageable. --- src/club/admin.py | 67 +++++++++++++++- src/club/helpers.py | 4 +- .../migrations/0011_fix_notification_body.py | 24 ++++++ .../migrations/0012_auto_20190930_1510.py | 80 +++++++++++++++++++ src/club/migrations/0013_populate_payed_at.py | 26 ++++++ src/club/models.py | 9 ++- src/club/payu/models.py | 35 ++++---- src/club/payu/views.py | 2 +- src/club/templates/club/schedule.html | 2 + 9 files changed, 229 insertions(+), 20 deletions(-) create mode 100644 src/club/migrations/0011_fix_notification_body.py create mode 100644 src/club/migrations/0012_auto_20190930_1510.py create mode 100644 src/club/migrations/0013_populate_payed_at.py diff --git a/src/club/admin.py b/src/club/admin.py index ce94a094e..b0c20c2c5 100644 --- a/src/club/admin.py +++ b/src/club/admin.py @@ -1,7 +1,10 @@ # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # +import json from django.contrib import admin +from django.utils.html import conditional_escape +from django.utils.safestring import mark_safe from modeltranslation.admin import TranslationAdmin from . import models @@ -14,18 +17,31 @@ admin.site.register(models.Plan, PlanAdmin) class PayUOrderInline(admin.TabularInline): model = models.PayUOrder + fields = ['order_id', 'status', 'customer_ip'] + readonly_fields = fields extra = 0 show_change_link = True + can_delete = False + + def has_add_permission(self, request, obj): + return False class PayUCardTokenInline(admin.TabularInline): model = models.PayUCardToken + fields = ['created_at', 'disposable_token', 'reusable_token'] + readonly_fields = fields extra = 0 show_change_link = True + can_delete = False + show_change_link = True + + def has_add_permission(self, request, obj): + return False class ScheduleAdmin(admin.ModelAdmin): - list_display = ['email', 'started_at', 'expires_at', 'plan', 'amount', 'is_cancelled'] + list_display = ['email', 'started_at', 'payed_at', 'expires_at', 'plan', 'amount', 'is_cancelled'] list_search = ['email'] list_filter = ['is_cancelled'] date_hierarchy = 'started_at' @@ -37,8 +53,15 @@ 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'] + readonly_fields = fields extra = 0 show_change_link = True + can_delete = False + + def has_add_permission(self, request, obj): + return False + class MembershipAdmin(admin.ModelAdmin): list_display = ['user'] @@ -52,4 +75,44 @@ admin.site.register(models.Membership, MembershipAdmin) admin.site.register(models.ReminderEmail, TranslationAdmin) -admin.site.register(models.PayUNotification) +class PayUNotificationAdmin(admin.ModelAdmin): + list_display = ['received_at', 'order'] + fields = ['received_at', 'order', 'body_'] + readonly_fields = ['received_at', 'body_'] + raw_id_fields = ['order'] + + def body_(self, obj): + return mark_safe( + "
" +
+                conditional_escape(json.dumps(json.loads(obj.body), indent=4))
+                + "
") + + +admin.site.register(models.PayUNotification, PayUNotificationAdmin) + + +class PayUNotificationInline(admin.TabularInline): + model = models.PayUNotification + fields = ['received_at', 'body_'] + readonly_fields = fields + extra = 0 + show_change_link = True + can_delete = False + + def body_(self, obj): + return mark_safe( + "
" +
+                conditional_escape(json.dumps(json.loads(obj.body), indent=4))
+                + "
") + + def has_add_permission(self, request, obj): + return False + + +class PayUOrderAdmin(admin.ModelAdmin): + list_display = ['schedule'] + raw_id_fields = ['schedule'] + inlines = [PayUNotificationInline] + + +admin.site.register(models.PayUOrder, PayUOrderAdmin) diff --git a/src/club/helpers.py b/src/club/helpers.py index b4ef0dd7d..9bcf6dfe2 100644 --- a/src/club/helpers.py +++ b/src/club/helpers.py @@ -8,5 +8,7 @@ from .models import Schedule def get_active_schedule(user): if not user.is_authenticated: return None - return Schedule.objects.filter(membership__user=user, expires_at__gt=now()).first() + return Schedule.objects.filter( + membership__user=user + ).exclude(payed_at=None).exclude(expires_at__lt=now()).first() diff --git a/src/club/migrations/0011_fix_notification_body.py b/src/club/migrations/0011_fix_notification_body.py new file mode 100644 index 000000000..3336fe4ac --- /dev/null +++ b/src/club/migrations/0011_fix_notification_body.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.5 on 2019-09-30 13:02 + +from django.db import migrations + + +def fix_notification_body(apps, schema_editor): + PayUNotification = apps.get_model('club', 'PayUNotification') + for n in PayUNotification.objects.filter(body__startswith='b'): + n.body = n.body[2:-1] + n.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('club', '0010_auto_20190529_0946'), + ] + + operations = [ + migrations.RunPython( + fix_notification_body, + migrations.RunPython.noop, + elidable=True), + ] diff --git a/src/club/migrations/0012_auto_20190930_1510.py b/src/club/migrations/0012_auto_20190930_1510.py new file mode 100644 index 000000000..f73b3d179 --- /dev/null +++ b/src/club/migrations/0012_auto_20190930_1510.py @@ -0,0 +1,80 @@ +# Generated by Django 2.2.5 on 2019-09-30 13:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('club', '0011_fix_notification_body'), + ] + + operations = [ + migrations.AlterModelOptions( + name='payucardtoken', + options={'verbose_name': 'PayU card token', 'verbose_name_plural': 'PayU card tokens'}, + ), + migrations.AlterModelOptions( + name='payunotification', + options={'verbose_name': 'PayU notification', 'verbose_name_plural': 'PayU notifications'}, + ), + migrations.AlterModelOptions( + name='payuorder', + options={'verbose_name': 'PayU order', 'verbose_name_plural': 'PayU orders'}, + ), + migrations.AddField( + model_name='schedule', + name='payed_at', + field=models.DateTimeField(blank=True, null=True, verbose_name='payed at'), + ), + migrations.AlterField( + model_name='payucardtoken', + name='created_at', + field=models.DateTimeField(auto_now_add=True, verbose_name='created_at'), + ), + migrations.AlterField( + model_name='payucardtoken', + name='disposable_token', + field=models.CharField(max_length=255, verbose_name='disposable token'), + ), + migrations.AlterField( + model_name='payucardtoken', + name='pos_id', + field=models.CharField(max_length=255, verbose_name='POS id'), + ), + migrations.AlterField( + model_name='payucardtoken', + name='reusable_token', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='reusable token'), + ), + migrations.AlterField( + model_name='payunotification', + name='body', + field=models.TextField(verbose_name='body'), + ), + migrations.AlterField( + model_name='payunotification', + name='received_at', + field=models.DateTimeField(auto_now_add=True, verbose_name='received_at'), + ), + migrations.AlterField( + model_name='payuorder', + name='customer_ip', + field=models.GenericIPAddressField(verbose_name='customer IP'), + ), + migrations.AlterField( + model_name='payuorder', + name='order_id', + field=models.CharField(blank=True, max_length=255, verbose_name='order ID'), + ), + migrations.AlterField( + model_name='payuorder', + name='pos_id', + field=models.CharField(max_length=255, verbose_name='POS id'), + ), + migrations.AlterField( + model_name='schedule', + name='method', + field=models.CharField(choices=[('payu-re', 'PayU (płatność odnawialna)'), ('payu', 'PayU')], max_length=255, verbose_name='method'), + ), + ] diff --git a/src/club/migrations/0013_populate_payed_at.py b/src/club/migrations/0013_populate_payed_at.py new file mode 100644 index 000000000..68b335ec9 --- /dev/null +++ b/src/club/migrations/0013_populate_payed_at.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.5 on 2019-09-30 13:13 +import json +from django.db import migrations + + +def populate_payed_at(apps, schema_editor): + PayUNotification = apps.get_model('club', 'PayUNotification') + for notification in PayUNotification.objects.order_by('received_at'): + status = json.loads(notification.body)['order']['status'] + schedule = notification.order.schedule + if status == 'COMPLETED' and schedule.payed_at is None: + schedule.payed_at = notification.received_at + schedule.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('club', '0012_auto_20190930_1510'), + ] + + operations = [ + migrations.RunPython( + populate_payed_at, + migrations.RunPython.noop, + elidable=True) + ] diff --git a/src/club/models.py b/src/club/models.py index c49b888f4..40a413849 100644 --- a/src/club/models.py +++ b/src/club/models.py @@ -70,6 +70,7 @@ class Schedule(models.Model): 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_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) expires_at = models.DateTimeField(_('expires_at'), null=True, blank=True) email_sent = models.BooleanField(default=False) @@ -105,7 +106,7 @@ class Schedule(models.Model): return self.expires_at is not None and self.expires_at <= now() def is_active(self): - return self.expires_at is not None and self.expires_at > now() + return self.payed_at is not None and (self.expires_at is None or self.expires_at > now()) def send_email(self): ctx = {'schedule': self} @@ -203,8 +204,12 @@ class PayUOrder(payu_models.Order): def status_updated(self): if self.status == 'COMPLETED': - since = self.schedule.expires_at or now() + since = self.schedule.expires_at + if since is None or since < self.received_at: + since = self.received_at new_exp = self.schedule.plan.get_next_installment(since) + if self.schedule.payed_at is None: + self.schedule.payed_at = self.received_at if self.schedule.expires_at is None or self.schedule.expires_at < new_exp: self.schedule.expires_at = new_exp self.schedule.save() diff --git a/src/club/payu/models.py b/src/club/payu/models.py index 1cd689dde..1a49dd707 100644 --- a/src/club/payu/models.py +++ b/src/club/payu/models.py @@ -7,35 +7,40 @@ from urllib.request import HTTPError from django.contrib.sites.models import Site from django.db import models from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ from . import POSS class CardToken(models.Model): """ This should be attached to a payment schedule. """ - pos_id = models.CharField(max_length=255) - disposable_token = models.CharField(max_length=255) - reusable_token = models.CharField(max_length=255, null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) + pos_id = models.CharField(_('POS id'), max_length=255) + disposable_token = models.CharField(_('disposable token'), max_length=255) + reusable_token = models.CharField(_('reusable token'), max_length=255, null=True, blank=True) + created_at = models.DateTimeField(_('created_at'), auto_now_add=True) class Meta: abstract = True + verbose_name = _('PayU card token') + verbose_name_plural = _('PayU card tokens') class Order(models.Model): - pos_id = models.CharField(max_length=255) # TODO: redundant? - customer_ip = models.GenericIPAddressField() - order_id = models.CharField(max_length=255, blank=True) + pos_id = models.CharField(_('POS id'), max_length=255) # TODO: redundant? + customer_ip = models.GenericIPAddressField(_('customer IP')) + order_id = models.CharField(_('order ID'), max_length=255, blank=True) status = models.CharField(max_length=128, blank=True, choices=[ - ('PENDING', 'Pending'), - ('WAITING_FOR_CONFIRMATION', 'Waiting for confirmation'), - ('COMPLETED', 'Completed'), - ('CANCELED', 'Canceled'), - ('REJECTED', 'Rejected'), + ('PENDING', _('Pending')), + ('WAITING_FOR_CONFIRMATION', _('Waiting for confirmation')), + ('COMPLETED', _('Completed')), + ('CANCELED', _('Canceled')), + ('REJECTED', _('Rejected')), ]) class Meta: abstract = True + verbose_name = _('PayU order') + verbose_name_plural = _('PayU orders') # These need to be provided in a subclass. @@ -128,11 +133,13 @@ class Order(models.Model): class Notification(models.Model): """ Add `order` FK to real Order model. """ - body = models.TextField() - received_at = models.DateTimeField(auto_now_add=True) + body = models.TextField(_('body')) + received_at = models.DateTimeField(_('received_at'), auto_now_add=True) class Meta: abstract = True + verbose_name = _('PayU notification') + verbose_name_plural = _('PayU notifications') def get_status(self): return json.loads(self.body)['order']['status'] diff --git a/src/club/payu/views.py b/src/club/payu/views.py index 3f06b3773..91e28cc12 100644 --- a/src/club/payu/views.py +++ b/src/club/payu/views.py @@ -75,7 +75,7 @@ class NotifyView(View): return http.HttpResponseBadRequest('wrong') notification = order.notification_set.create( - body=request.body + body=request.body.decode('utf-8') ) notification.apply() diff --git a/src/club/templates/club/schedule.html b/src/club/templates/club/schedule.html index de0797f5b..bb1bf57fb 100644 --- a/src/club/templates/club/schedule.html +++ b/src/club/templates/club/schedule.html @@ -49,6 +49,7 @@ wspierasz nas kwotą {{ schedule.amount }} zł {{ schedule.plan.get_interval_dis {% else %} + {% if not schedule.payed_at %} Składka nie została jeszcze opłacona. {% if schedule.payuorder_set.exists %} Czekamy na potwierdzenie płatności. @@ -60,6 +61,7 @@ wspierasz nas kwotą {{ schedule.amount }} zł {{ schedule.plan.get_interval_dis {% endif %} + {% endif %} {% endif %} {% endif %} -- 2.20.1