Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
thalia
concrexit
Commits
2f947c3c
Verified
Commit
2f947c3c
authored
Apr 14, 2019
by
Sébastiaan Versteeg
Browse files
Move pizza admin to backend and migrate to payments app for payment registration
parent
11163857
Changes
29
Hide whitespace changes
Inline
Side-by-side
docs/pizzas.rst
View file @
2f947c3c
...
...
@@ -24,18 +24,18 @@ pizzas.admin module
:undoc-members:
:show-inheritance:
pizzas.a
pp
s module
------------------
pizzas.a
dmin\_view
s module
------------------
--------
.. automodule:: pizzas.a
pp
s
.. automodule:: pizzas.a
dmin_view
s
:members:
:undoc-members:
:show-inheritance:
pizzas.
form
s module
------------------
-
pizzas.
app
s module
------------------
.. automodule:: pizzas.
form
s
.. automodule:: pizzas.
app
s
:members:
:undoc-members:
:show-inheritance:
...
...
docs/utils.rst
View file @
2f947c3c
...
...
@@ -19,6 +19,14 @@ Subpackages
Submodules
----------
utils.admin module
------------------
.. automodule:: utils.admin
:members:
:undoc-members:
:show-inheritance:
utils.countries module
----------------------
...
...
website/events/admin.py
View file @
2f947c3c
# -*- 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
(
DoNext
Translated
ModelAdmin
):
"""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
(
DoNext
Translated
ModelAdmin
):
"""Custom admin for registrations"""
form
=
RegistrationAdminForm
...
...
website/events/api/serializers.py
View file @
2f947c3c
...
...
@@ -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
...
...
@@ -226,14 +227,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
:
...
...
website/events/tests/test_admin.py
View file @
2f947c3c
...
...
@@ -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
=
DoNext
Translated
ModelAdmin
(
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
(
'
event
s.admin.DoNextModelAdmin.has_change_permission'
)
@
mock
.
patch
(
'
util
s.admin.DoNext
Translated
ModelAdmin.has_change_permission'
)
@
mock
.
patch
(
'events.services.is_organiser'
)
def
test_has_change_permission
(
self
,
organiser_mock
,
permission_mock
):
permission_mock
.
return_value
=
None
...
...
website/payments/api/fields.py
0 → 100644
View file @
2f947c3c
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
)
website/pizzas/admin.py
View file @
2f947c3c
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
):
...
...
website/pizzas/admin_views.py
0 → 100644
View file @
2f947c3c
"""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
website/pizzas/api/serializers.py
View file @
2f947c3c
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
)
website/pizzas/forms.py
deleted
100644 → 0
View file @
11163857
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'
]
website/pizzas/migrations/0006_product_restricted.py
deleted
100644 → 0
View file @
11163857
# -*- 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
),
),
]
website/pizzas/migrations/0007_auto_20171213_2017.py
deleted
100644 → 0
View file @
11163857
# -*- 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
=
[
],
),
]
website/pizzas/migrations/0008_auto_20180102_1635.py
deleted
100644 → 0
View file @
11163857
# -*- 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."
),
),
]
website/pizzas/migrations/0008_payment_registration.py
0 → 100644
View file @
2f947c3c
# 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'
),
),
]
website/pizzas/migrations/0009_payment_registration.py
0 → 100644
View file @
2f947c3c
# -*- 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
),
]
website/pizzas/migrations/0010_payment_registration.py
0 → 100644
View file @
2f947c3c
# 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'
),
),
]
website/pizzas/models.py
View file @
2f947c3c
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
)