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.')
)
This diff was suppressed by a .gitattributes entry.
# 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(
reverse('admin:payments_payment_changelist'))
actions = self.admin.get_actions(response.wsgi_request)
self.assertCountEqual(actions, ['delete_selected'])
self._give_user_permissions()
response = self.client.get(
reverse('admin:payments_payment_changelist'))
actions = self.admin.get_actions(response.wsgi_request)
self.assertCountEqual(actions, ['delete_selected',
'process_cash_selected',
'process_card_selected'])
from django.test import TestCase