Make club app a little more manageable.
authorRadek Czajka <rczajka@rczajka.pl>
Mon, 30 Sep 2019 13:58:06 +0000 (15:58 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Mon, 30 Sep 2019 13:58:06 +0000 (15:58 +0200)
src/club/admin.py
src/club/helpers.py
src/club/migrations/0011_fix_notification_body.py [new file with mode: 0644]
src/club/migrations/0012_auto_20190930_1510.py [new file with mode: 0644]
src/club/migrations/0013_populate_payed_at.py [new file with mode: 0644]
src/club/models.py
src/club/payu/models.py
src/club/payu/views.py
src/club/templates/club/schedule.html

index ce94a09..b0c20c2 100644 (file)
@@ -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(
+                "<pre>" +
+                conditional_escape(json.dumps(json.loads(obj.body), indent=4))
+                + "</pre>")
+
+
+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(
+                "<pre>" +
+                conditional_escape(json.dumps(json.loads(obj.body), indent=4))
+                + "</pre>")
+
+    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)
index b4ef0dd..9bcf6df 100644 (file)
@@ -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 (file)
index 0000000..3336fe4
--- /dev/null
@@ -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 (file)
index 0000000..f73b3d1
--- /dev/null
@@ -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 (file)
index 0000000..68b335e
--- /dev/null
@@ -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)
+    ]
index c49b888..40a4138 100644 (file)
@@ -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()
index 1cd689d..1a49dd7 100644 (file)
@@ -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']
index 3f06b37..91e28cc 100644 (file)
@@ -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()
 
index de0797f..bb1bf57 100644 (file)
@@ -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
      </form>
 
    {% endif %}
+  {% endif %}
  {% endif %}
 {% endif %}