Verified Commit 9d0208c0 authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Move Payment model to new payments app

Redo migrations

Move pages and admin to payments app

Adjust and add tests

Update docs and fix codestyle

Fix migrations
parent ea0ef4a4
......@@ -14,6 +14,7 @@ website
merchandise
newsletters
partners
payments
photos
pizzas
pushnotifications
......
payments package
================
.. automodule:: payments
:members:
:undoc-members:
:show-inheritance:
Submodules
----------
payments\.admin module
----------------------
.. automodule:: payments.admin
:members:
:undoc-members:
:show-inheritance:
payments\.models module
-----------------------
.. automodule:: payments.models
:members:
:undoc-members:
:show-inheritance:
payments\.services module
-------------------------
.. automodule:: payments.services
:members:
:undoc-members:
:show-inheritance:
payments\.urls module
---------------------
.. automodule:: payments.urls
:members:
:undoc-members:
:show-inheritance:
payments\.views module
----------------------
.. automodule:: payments.views
:members:
:undoc-members:
:show-inheritance:
......@@ -64,6 +64,14 @@ registrations\.services module
:undoc-members:
:show-inheritance:
registrations\.signals module
-----------------------------
.. automodule:: registrations.signals
:members:
:undoc-members:
:show-inheritance:
registrations\.urls module
--------------------------
......
from django.contrib import admin, messages
from django.contrib.admin.utils import model_ngettext
from django.utils.translation import ugettext_lazy as _
from payments import services
from .models import Payment
def _show_message(admin, request, n, message, error):
if n == 0:
admin.message_user(request, error, messages.ERROR)
else:
admin.message_user(request, message % {
"count": n,
"items": model_ngettext(admin.opts, n)
}, messages.SUCCESS)
@admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin):
list_display = ('created_at', 'amount',
'processed', 'processing_date', 'type')
list_filter = ('processed', 'amount',)
date_hierarchy = 'created_at'
fields = ('created_at', 'amount',
'type', 'processed', 'processing_date')
readonly_fields = ('created_at', 'amount', 'processed',
'type', 'processing_date')
actions = ['process_cash_selected', 'process_card_selected']
def changeform_view(self, request, object_id=None, form_url='',
extra_context=None):
obj = None
if (object_id is not None and
request.user.has_perm('payments.process_payments')):
obj = Payment.objects.get(id=object_id)
if obj.processed:
obj = None
return super().changeform_view(
request, object_id, form_url, {'payment': obj})
def get_actions(self, request):
actions = super().get_actions(request)
if not request.user.has_perm('payments.process_payments'):
del(actions['process_cash_selected'])
del(actions['process_card_selected'])
return actions
def process_cash_selected(self, request, queryset):
if request.user.has_perm('payments.process_payments'):
updated_payments = services.process_payment(
queryset, Payment.CASH
)
self._process_feedback(request, updated_payments)
process_cash_selected.short_description = _(
'Process selected payments (cash)')
def process_card_selected(self, request, queryset):
if request.user.has_perm('payments.process_payments'):
updated_payments = services.process_payment(
queryset, Payment.CARD
)
self._process_feedback(request, updated_payments)
process_card_selected.short_description = _(
'Process selected payments (card)')
def _process_feedback(self, request, updated_payments):
rows_updated = len(updated_payments)
_show_message(
self, request, rows_updated,
message=_("Successfully processed %(count)d %(items)s."),
error=_('The selected payment(s) could not be processed.')
)
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-03 21:31+0100\n"
"PO-Revision-Date: 2018-02-03 21:32+0100\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Last-Translator: Sébastiaan Versteeg <se_bastiaan@outlook.com>\n"
"Language-Team: \n"
"X-Generator: Poedit 2.0.4\n"
#: admin.py:56
msgid "Process selected payments (cash)"
msgstr "Verwerk geselecteerde betalingen (contant)"
#: admin.py:65
msgid "Process selected payments (card)"
msgstr "Verwerk geselecteerde betalingen (pin)"
#: admin.py:71 tests/test_admin.py:115 tests/test_admin.py:137
#, python-format
msgid "Successfully processed %(count)d %(items)s."
msgstr "%(count)d %(items)s succesvol verwerkt."
#: admin.py:72
msgid "The selected payment(s) could not be processed."
msgstr "De geselecteerde betaling(en) konden niet worden verwerkt."
#: models.py:14
msgid "created at"
msgstr "aangemaakt op"
#: models.py:20
msgid "Cash payment"
msgstr "Contante betaling"
#: models.py:21
msgid "Card payment"
msgstr "Pin betaling"
#: models.py:26
msgid "type"
msgstr "type"
#: models.py:40
msgid "processed"
msgstr "verwerkt"
#: models.py:45
msgid "processing date"
msgstr "verwerkingsdatum"
#: models.py:62
msgid "payment"
msgstr "betaling"
#: models.py:63
msgid "payments"
msgstr "betalingen"
#: models.py:65
msgid "Process payments"
msgstr "Verwerk betalingen"
#: templates/admin/payments/change_form.html:12
msgid "Process (cash payment)"
msgstr "Verwerk (contant)"
#: templates/admin/payments/change_form.html:13
msgid "Process (card payment)"
msgstr "Verwerk (pin)"
#: tests/test_views.py:104 views.py:25
#, python-format
msgid "Successfully processed %s."
msgstr "%s succesvol verwerkt."
#: tests/test_views.py:111 views.py:28
#, python-format
msgid "Could not process %s."
msgstr "%s kon niet worden verwerkt."
# Generated by Django 2.0.1 on 2018-02-03 13:59
from django.db import migrations, models
import django.utils.timezone
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
('registrations', '0005_move_payment_model_to_app'),
]
state_operations = [
migrations.CreateModel(
name='Payment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created at')),
('type', models.CharField(blank=True, choices=[('cash_payment', 'Cash payment'), ('card_payment', 'Card payment')], max_length=20, null=True, verbose_name='type')),
('amount', models.DecimalField(decimal_places=2, max_digits=5)),
('processed', models.BooleanField(default=False, verbose_name='processed')),
('processing_date', models.DateTimeField(blank=True, null=True, verbose_name='processing date')),
],
options={
'verbose_name': 'payment',
'verbose_name_plural': 'payments',
'permissions': (('process_payments', 'Process payments'),),
},
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]
import uuid
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
class Payment(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(_('created at'), default=timezone.now)
CASH = 'cash_payment'
CARD = 'card_payment'
PAYMENT_TYPE = (
(CASH, _('Cash payment')),
(CARD, _('Card payment')),
)
type = models.CharField(
choices=PAYMENT_TYPE,
verbose_name=_('type'),
max_length=20,
blank=True,
null=True,
)
amount = models.DecimalField(
blank=False,
null=False,
max_digits=5,
decimal_places=2
)
processed = models.BooleanField(
_('processed'),
default=False,
)
processing_date = models.DateTimeField(
_('processing date'),
blank=True,
null=True,
)
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
if self.processed and not self.processing_date:
self.processing_date = timezone.now()
super().save(force_insert, force_update, using, update_fields)
def get_admin_url(self):
content_type = ContentType.objects.get_for_model(self.__class__)
return reverse("admin:%s_%s_change" % (
content_type.app_label, content_type.model), args=(self.id,))
class Meta:
verbose_name = _('payment')
verbose_name_plural = _('payments')
permissions = (
('process_payments', _("Process payments")),
)
from .models import Payment
def process_payment(queryset, pay_type=Payment.CARD):
"""
Process the payment
:param queryset: Queryset of payments that should be processed
:type queryset: QuerySet[Payment]
:param pay_type: Type of the payment
:type pay_type: String
"""
queryset = queryset.filter(processed=False)
data = []
# This should trigger post_save signals, thus a queryset update
# is not appropriate, moreover save() automatically sets
# the processing date
for payment in queryset:
payment.processed = True
payment.type = pay_type
payment.save()
data.append(payment)
return data
.submit-row a {
&.default {
text-transform: uppercase;
}
&.button {
float: right;
padding: 10px 15px;
line-height: 15px;
margin: 0 0 0 8px;
}
}
.payments-row a.button {
&.process {
background-color: #79aec8;
&:hover, &:active {
background-color: #609ab6;
}
}
}
{% extends "admin/change_form.html" %}
{% load i18n admin_urls static compress %}
{% block extrastyle %}
{{ block.super }}
{% compress css %}<link rel="stylesheet" type="text/x-scss" href="{% static 'admin/payments/css/forms.scss' %}" />{% endcompress %}
{% endblock %}
{% block submit_buttons_bottom %}
{% if payment %}
<div class="submit-row payments-row">
<a href="{% url 'payments:admin-process' pk=payment.pk type='cash_payment' %}" class="button process">{% trans "Process (cash payment)" %}</a>
<a href="{% url 'payments:admin-process' pk=payment.pk type='card_payment' %}" class="button process">{% trans "Process (card payment)" %}</a>
</div>
{% endif %}
{{ block.super }}
{% endblock %}
from unittest import mock
from unittest.mock import Mock
from django.contrib import messages
from django.contrib.admin import AdminSite
from django.contrib.admin.utils import model_ngettext
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.http import HttpRequest
from django.test import TestCase, SimpleTestCase, Client, RequestFactory
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from payments import admin
from payments.models import Payment
class GlobalAdminTest(SimpleTestCase):
@mock.patch('registrations.admin.RegistrationAdmin')
def test_show_message(self, admin_mock):
admin_mock.return_value = admin_mock
request = Mock(spec=HttpRequest)
admin._show_message(admin_mock, request, 0, 'message', 'error')
admin_mock.message_user.assert_called_once_with(
request, 'error', messages.ERROR)
admin_mock.message_user.reset_mock()
admin._show_message(admin_mock, request, 1, 'message', 'error')
admin_mock.message_user.assert_called_once_with(
request, 'message', messages.SUCCESS)
class PaymentAdminTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = get_user_model().objects.create_user(
username='username',
is_staff=True,
)
def setUp(self):
self.client = Client()
self.client.force_login(self.user)
self.factory = RequestFactory()
self.site = AdminSite()
self.admin = admin.PaymentAdmin(Payment, admin_site=self.site)
self._give_user_permissions()
process_perm = Permission.objects.get(
content_type__model='payment',
codename='process_payments')
self.user.user_permissions.remove(process_perm)
self.client.logout()
self.client.force_login(self.user)
def _give_user_permissions(self):
content_type = ContentType.objects.get_for_model(Payment)
permissions = Permission.objects.filter(
content_type__app_label=content_type.app_label,
)
for p in permissions:
self.user.user_permissions.add(p)
self.user.save()
self.client.logout()
self.client.force_login(self.user)
@mock.patch('payments.models.Payment.objects.get')
def test_changeform_view(self, payment_get):
object_id = 'c85ea333-3508-46f1-8cbb-254f8c138020'
payment = Payment.objects.create(pk=object_id,
processed=False,
amount=7.5)
payment_get.return_value = payment
response = self.client.get('/admin/payments/payment/add/')
self.assertFalse(payment_get.called)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['payment'], None)
response = self.client.get('/admin/payments/payment/{}/change/'
.format(object_id), follow=True)
self.assertFalse(payment_get.called)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['payment'], None)
self._give_user_permissions()
response = self.client.get('/admin/payments/payment/{}/change/'
.format(object_id))
self.assertTrue(payment_get.called)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['payment'], payment)
payment.processed = True
response = self.client.get('/admin/payments/payment/{}/change/'
.format(object_id))
self.assertTrue(payment_get.called)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['payment'], None)
@mock.patch('django.contrib.admin.ModelAdmin.message_user')
@mock.patch('payments.services.process_payment')
def test_process_cash(self, process_payment, message_user):
object_id = 'c85ea333-3508-46f1-8cbb-254f8c138020'
payment = Payment.objects.create(pk=object_id,
processed=False,
amount=7.5)
queryset = Payment.objects.filter(pk=object_id)
process_payment.return_value = [payment]
change_url = reverse('admin:payments_payment_changelist')
request_noperms = self.client.post(
change_url,
{'action': 'process_cash_selected',
'index': 1,
'_selected_action': [object_id]}).wsgi_request
self._give_user_permissions()
request_hasperms = self.client.post(
change_url,
{'action': 'process_cash_selected',
'index': 1,
'_selected_action': [object_id]}).wsgi_request
process_payment.reset_mock()
message_user.reset_mock()
self.admin.process_cash_selected(request_noperms, queryset)
process_payment.assert_not_called()
self.admin.process_cash_selected(request_hasperms, queryset)
process_payment.assert_called_once_with(queryset, Payment.CASH)
message_user.assert_called_once_with(
request_hasperms,
_('Successfully processed %(count)d %(items)s.')
% {
"count": 1,
"items": model_ngettext(Payment(), 1)
}, messages.SUCCESS
)
@mock.patch('django.contrib.admin.ModelAdmin.message_user')
@mock.patch('payments.services.process_payment')
def test_process_card(self, process_payment, message_user):
object_id = 'c85ea333-3508-46f1-8cbb-254f8c138020'
payment = Payment.objects.create(pk=object_id,
processed=False,
amount=7.5)
queryset = Payment.objects.filter(pk=object_id)
process_payment.return_value = [payment]
change_url = reverse('admin:payments_payment_changelist')
request_noperms = self.client.post(
change_url,
{'action': 'process_card_selected',
'index': 1,
'_selected_action': [object_id]}).wsgi_request
self._give_user_permissions()
request_hasperms = self.client.post(
change_url,
{'action': 'process_card_selected',
'index': 1,
'_selected_action': [object_id]}).wsgi_request
process_payment.reset_mock()
message_user.reset_mock()
self.admin.process_card_selected(request_noperms, queryset)
process_payment.assert_not_called()
self.admin.process_card_selected(request_hasperms, queryset)
process_payment.assert_called_once_with(queryset, Payment.CARD)
message_user.assert_called_once_with(
request_hasperms,
_('Successfully processed %(count)d %(items)s.')
% {
"count": 1,
"items": model_ngettext(Payment(), 1)
}, messages.SUCCESS
)
def test_get_actions(self):
response = self.client.get(