Commit d8074d57 authored by Luko van der Maas's avatar Luko van der Maas
Browse files

Merge branch 'feature/pizza-payments' into 'master'

Move pizza admin to backend and migrate to payments app for payment registration

Closes #840

See merge request !1225
parents ae0d3abf 2f947c3c
......@@ -24,18 +24,18 @@ pizzas.admin module
:undoc-members:
:show-inheritance:
pizzas.apps module
------------------
pizzas.admin\_views module
--------------------------
.. automodule:: pizzas.apps
.. automodule:: pizzas.admin_views
:members:
:undoc-members:
:show-inheritance:
pizzas.forms module
-------------------
pizzas.apps module
------------------
.. automodule:: pizzas.forms
.. automodule:: pizzas.apps
:members:
:undoc-members:
:show-inheritance:
......
......@@ -19,6 +19,14 @@ Subpackages
Submodules
----------
utils.admin module
------------------
.. automodule:: utils.admin
:members:
:undoc-members:
:show-inheritance:
utils.countries module
----------------------
......
# -*- coding: utf-8 -*-
"""Registers admin interfaces for the events module"""
from django.contrib import admin
from django.core.exceptions import DisallowedRedirect, PermissionDenied
from django.core.exceptions import PermissionDenied
from django.db.models import Max, Min
from django.forms import Field
from django.http import HttpResponseRedirect
from django.template.defaultfilters import date as _date
from django.urls import reverse, path
from django.utils import timezone
from django.utils.datetime_safe import date
from django.utils.html import format_html
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _
import events.admin_views as admin_views
from activemembers.models import MemberGroup
from events import services
from events.forms import RegistrationAdminForm
from members.models import Member
from payments.widgets import PaymentWidget
from pizzas.models import PizzaEvent
from utils.admin import DoNextTranslatedModelAdmin
from utils.snippets import datetime_to_lectureyear
from utils.translation import TranslatedModelAdmin
from . import forms, models
import events.admin_views as admin_views
def _do_next(request, response):
"""See DoNextModelAdmin"""
if 'next' in request.GET:
if not is_safe_url(request.GET['next'],
allowed_hosts={request.get_host()}):
raise DisallowedRedirect
elif '_save' in request.POST:
return HttpResponseRedirect(request.GET['next'])
elif response is not None:
return HttpResponseRedirect('{}?{}'.format(
response.url, request.GET.urlencode()))
return response
class DoNextModelAdmin(TranslatedModelAdmin):
"""
This class adds processing of a `next` parameter in the urls
of the add and change admin forms. If it is set and safe this
override will redirect the user to the provided url.
"""
def response_add(self, request, obj):
res = super().response_add(request, obj)
return _do_next(request, res)
def response_change(self, request, obj):
res = super().response_change(request, obj)
return _do_next(request, res)
class RegistrationInformationFieldInline(admin.StackedInline):
......@@ -109,7 +77,7 @@ class LectureYearFilter(admin.SimpleListFilter):
@admin.register(models.Event)
class EventAdmin(DoNextModelAdmin):
class EventAdmin(DoNextTranslatedModelAdmin):
"""Manage the events"""
inlines = (RegistrationInformationFieldInline, PizzaEventInline,)
fields = ('title', 'description', 'start', 'end', 'organiser', 'category',
......@@ -267,7 +235,7 @@ class EventAdmin(DoNextModelAdmin):
@admin.register(models.Registration)
class RegistrationAdmin(DoNextModelAdmin):
class RegistrationAdmin(DoNextTranslatedModelAdmin):
"""Custom admin for registrations"""
form = RegistrationAdminForm
......
......@@ -7,6 +7,7 @@ from html import unescape
from rest_framework import serializers
from rest_framework.fields import empty
from payments.api.fields import PaymentTypeField
from payments.models import Payment
from thaliawebsite.api.services import create_image_thumbnail_dict
from events import services
......@@ -229,14 +230,6 @@ class RegistrationListSerializer(serializers.ModelSerializer):
size_large='800x800')
class PaymentTypeField(serializers.ChoiceField):
def get_attribute(self, instance):
if not instance.payment:
return Payment.NONE
return super().get_attribute(instance)
class RegistrationAdminListSerializer(RegistrationListSerializer):
"""Custom registration admin list serializer"""
class Meta:
......
......@@ -10,19 +10,19 @@ from freezegun import freeze_time
from activemembers.models import Committee, MemberGroupMembership
from events.admin import (
DoNextModelAdmin,
RegistrationInformationFieldInline,
EventAdmin
)
from events.models import Event, RegistrationInformationField
from members.models import Member
from utils.admin import DoNextTranslatedModelAdmin
class DoNextModelAdminTest(TestCase):
def setUp(self):
self.site = AdminSite()
self.admin = DoNextModelAdmin(Event, admin_site=self.site)
self.admin = DoNextTranslatedModelAdmin(Event, admin_site=self.site)
self.rf = RequestFactory()
@mock.patch('utils.translation.TranslatedModelAdmin.response_add')
......@@ -196,7 +196,7 @@ class EventAdminTest(TestCase):
'<a href="/admin/events/event/1/details/">'
'testevent</a>')
@mock.patch('events.admin.DoNextModelAdmin.has_change_permission')
@mock.patch('utils.admin.DoNextTranslatedModelAdmin.has_change_permission')
@mock.patch('events.services.is_organiser')
def test_has_change_permission(self, organiser_mock, permission_mock):
permission_mock.return_value = None
......
from rest_framework import serializers
from payments.models import Payment
class PaymentTypeField(serializers.ChoiceField):
def get_attribute(self, instance):
if not instance.payment:
return Payment.NONE
return super().get_attribute(instance)
from django.contrib import admin
from django.core.exceptions import PermissionDenied
from django.urls import reverse
from django.urls import reverse, path
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from pizzas import admin_views
from utils.admin import DoNextModelAdmin
from .models import Order, PizzaEvent, Product
from events.models import Event
from events.services import is_organiser
......@@ -22,9 +24,9 @@ class PizzaEventAdmin(admin.ModelAdmin):
exclude = ('end_reminder',)
def orders(self, obj):
return format_html(_('<strong><a href="{link}">Orders</a></strong>'),
link=reverse('pizzas:orders',
kwargs={'event_pk': obj.pk}))
url = reverse('admin:pizzas_pizzaevent_details', kwargs={'pk': obj.pk})
return format_html('<a href="{url}">{text}</a>',
url=url, text=_("Orders"))
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "event":
......@@ -33,10 +35,26 @@ class PizzaEventAdmin(admin.ModelAdmin):
return super(PizzaEventAdmin, self).formfield_for_foreignkey(
db_field, request, **kwargs)
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('<int:pk>/details/',
self.admin_site.admin_view(
admin_views.PizzaOrderDetails.as_view(admin=self)),
name='pizzas_pizzaevent_details'),
path('<int:pk>/overview/',
self.admin_site.admin_view(
admin_views.PizzaOrderSummary.as_view(admin=self)),
name='pizzas_pizzaevent_overview'),
]
return custom_urls + urls
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ('pizza_event', 'member_name', 'product', 'paid')
class OrderAdmin(DoNextModelAdmin):
list_display = ('pizza_event', 'member_first_name',
'member_last_name', 'product', 'payment')
exclude = ('payment', )
def save_model(self, request, obj, form, change):
if not is_organiser(request.member, obj.pizza_event.event):
......
"""Admin views provided by the pizzas package"""
from django.shortcuts import get_object_or_404
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView
from payments.models import Payment
from pizzas.models import PizzaEvent, Order
class PizzaOrderSummary(TemplateView):
template_name = 'pizzas/admin/summary.html'
admin = None
def get_context_data(self, **kwargs):
event = get_object_or_404(PizzaEvent, pk=kwargs.get('pk'))
context = super().get_context_data(**kwargs)
context.update({
**self.admin.admin_site.each_context(self.request),
'has_delete_permission': False,
'has_editable_inline_admin_formsets': False,
'app_label': 'pizzas',
'opts': PizzaEvent._meta,
'is_popup': False,
'save_as': False,
'save_on_top': False,
'title': capfirst(_('pizza order summary')),
'original': capfirst(_('summary')),
'pizza_event': event
})
product_list = {}
orders = Order.objects.filter(
pizza_event=event).prefetch_related('product')
for order in orders:
if order.product.id not in product_list:
product_list[order.product.id] = {
'name': order.product.name,
'price': order.product.price,
'amount': 0,
'total': 0
}
product_list[order.product.id]['amount'] += 1
product_list[order.product.id]['total'] += order.product.price
product_list = sorted(product_list.values(), key=lambda x: x['name'])
context.update({
'event': event,
'product_list': product_list,
'total_money': sum(map(lambda x: x['total'], product_list)),
'total_products': len(orders)
})
return context
class PizzaOrderDetails(TemplateView):
template_name = 'pizzas/admin/orders.html'
admin = None
def get_context_data(self, **kwargs):
event = get_object_or_404(PizzaEvent, pk=kwargs.get('pk'))
context = super().get_context_data(**kwargs)
context.update({
**self.admin.admin_site.each_context(self.request),
'has_delete_permission': False,
'has_editable_inline_admin_formsets': False,
'app_label': 'pizzas',
'opts': PizzaEvent._meta,
'is_popup': False,
'save_as': False,
'save_on_top': False,
'title': capfirst(_('pizza order overview')),
'original': str(event),
'pizza_event': event
})
context.update({
'event': event,
'payment': Payment,
'orders': (Order.objects
.filter(pizza_event=event)
.prefetch_related('member', 'product')
.order_by('member__first_name'))
})
return context
from typing import Any
from django.db.models import Model
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from payments.api.fields import PaymentTypeField
from payments.models import Payment
from pizzas.models import Product, PizzaEvent, Order
from pizzas.services import can_change_order
......@@ -28,14 +33,20 @@ class PizzaEventSerializer(serializers.ModelSerializer):
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ('pk', 'paid', 'product', 'name', 'member')
read_only_fields = ('pk', 'paid', 'name', 'member')
fields = ('pk', 'payment', 'product', 'name', 'member')
read_only_fields = ('pk', 'payment', 'name', 'member')
payment = PaymentTypeField(source='payment.type',
choices=Payment.PAYMENT_TYPE)
class AdminOrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ('pk', 'paid', 'product', 'name', 'member')
fields = ('pk', 'payment', 'product', 'name', 'member')
payment = PaymentTypeField(source='payment.type',
choices=Payment.PAYMENT_TYPE)
def validate(self, attrs):
if attrs.get('member') and attrs.get('name'):
......@@ -46,3 +57,12 @@ class AdminOrderSerializer(serializers.ModelSerializer):
if not (attrs.get('member') or attrs.get('name')) and not self.partial:
attrs['member'] = self.context['request'].member
return super().validate(attrs)
def update(self, instance: Model, validated_data: Any) -> Any:
if validated_data.get(
'payment', {}
).get('type', instance.payment.type) != instance.payment.type:
instance.payment.type = validated_data['payment']['type']
instance.payment.save()
del validated_data['payment']
return super().update(instance, validated_data)
from django.forms import ModelForm
from .models import Order, Product
class AddOrderForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['product'].queryset = Product.available_products.all()
class Meta:
model = Order
fields = ['name', 'product']
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2017-12-13 19:01
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pizzas', '0005_auto_20171213_1954'),
]
operations = [
migrations.AddField(
model_name='product',
name='restricted',
field=models.BooleanField(default=False),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2017-12-13 19:17
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pizzas', '0006_product_restricted'),
]
operations = [
migrations.AlterModelOptions(
name='product',
options={'ordering': ('name',), 'permissions': (('order_restricted_products', 'Order restricted products'),)},
),
migrations.AlterModelManagers(
name='product',
managers=[
],
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2018-01-02 15:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pizzas', '0007_auto_20171213_2017'),
]
operations = [
migrations.AlterField(
model_name='product',
name='restricted',
field=models.BooleanField(default=False, help_text="Only allow to be ordered by people with the 'order restricted products' permission."),
),
]
# Generated by Django 2.2 on 2019-04-13 20:21
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('payments', '0003_auto_20190204_2111'),
('pizzas', '0007_auto_20181219_2032'),
]
operations = [
migrations.AddField(
model_name='order',
name='payment',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pizzas_order', to='payments.Payment', verbose_name='payment'),
),
migrations.AlterField(
model_name='order',
name='name',
field=models.CharField(blank=True, help_text='Use this for non-members', max_length=50, null=True, verbose_name='name'),
),
migrations.AlterField(
model_name='order',
name='pizza_event',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pizzas.PizzaEvent', verbose_name='event'),
),
migrations.AlterField(
model_name='order',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='pizzas.Product', verbose_name='product'),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pizzas', '0008_payment_registration'),
]
def forwards_func(apps, schema_editor):
Order = apps.get_model('pizzas', 'order')
Payment = apps.get_model('payments', 'payment')
db_alias = schema_editor.connection.alias
for order in Order.objects.using(db_alias).all():
name = order.name
if order.member is not None:
name = '{} {}'.format(order.member.first_name,
order.member.last_name)
order.payment = Payment.objects.create(
created_at=order.pizza_event.end,
type='cash_payment' if order.paid else 'no_payment',
amount=order.product.price,
paid_by=order.member,
processing_date=None,
notes=(f'Pizza order by {name} '
f'for {order.pizza_event.event.title_en}')
)
order.save()
def reverse_func(apps, schema_editor):
Order = apps.get_model('pizzas', 'order')
db_alias = schema_editor.connection.alias
for order in Order.objects.using(db_alias).all():
payment = order.payment
order.paid = order.payment.type != 'no_payment'
order.payment = None
order.save()
payment.delete()
operations = [
migrations.RunPython(forwards_func, reverse_func),
]
# Generated by Django 2.2 on 2019-04-13 20:33
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('pizzas', '0009_payment_registration'),
]
operations = [
migrations.RemoveField(
model_name='order',
name='paid',
),
migrations.AlterField(
model_name='order',
name='payment',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='pizzas_order', to='payments.Payment', verbose_name='payment'),
),
]
from django.core.exceptions import ValidationError
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.db import models
from django.db.models import Q
from django.utils import timezone
......@@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from events.models import Event
import members
from members.models import Member
from payments.models import Payment
from pushnotifications.models import ScheduledMessage, Category
from utils.translation import ModelTranslateMeta, MultilingualField
......@@ -165,17 +166,32 @@ class Order(models.Model):
null=True,
)
paid = models.BooleanField(default=False)
name = models.CharField(
verbose_name=_('name'),
max_length=50,
help_text=_('Use this for non-members'),
null=True,
blank=True,
)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
pizza_event = models.ForeignKey(PizzaEvent, on_delete=models.CASCADE)