fix(MR-16): Use cache for FileField and ImageField (#17)

* Remove casting to list in _get_form_data

* Use cache for most fields and admin form for m2m files

* MR comments/clean up

* Cache should obey exclude and fields

* Some more tests and docs

* Only use cache for image files

* Even more tests and handle save as new

* fix test

* More tests

* minor refactor

* Improve test coverage

* Add no cover for some places

* V0.2.3.dev7

* Adding tests for fieldsets

* Added cache timeout

* Added another test for an edge case

* Fix issue with ManagementForm tampered with

* Update cache to only set when form is_multipart

* Even more testing changes

* Update based on comments on MR and clean up a bit

* make test names better

Co-authored-by: Thu Trang Pham <thu@joinmodernhealth.com>
main
Thu Trang Pham 2021-02-27 15:39:01 -08:00 committed by GitHub
parent cc36492bfe
commit 06d3e1a208
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 3017 additions and 116 deletions

View File

@ -1,2 +1,4 @@
[run]
relative_files = True
omit = admin_confirm/tests/*
branch = True

2
.gitignore vendored
View File

@ -35,3 +35,5 @@ docs/_build/
# Database
db.sqlite3
tmp/

View File

@ -3,6 +3,7 @@ run:
test:
coverage run --source admin_confirm --branch -m pytest
coverage report -m
check-readme:
python -m readme_renderer README.md -o /tmp/README.html

View File

@ -27,6 +27,12 @@ Typical Usage:
confirmation_fields = ['field1', 'field2']
```
## Disclaimer
Be aware that not all possible combinations of ModelAdmin have been tested, even if test coverage is high.
See [testing readme](admin_confirm/tests/README.md) for more details
## Installation
Install django-admin-confirm by running:

View File

@ -6,11 +6,23 @@ from django.template.response import TemplateResponse
from django.contrib.admin.options import TO_FIELD_VAR
from django.utils.translation import gettext as _
from django.contrib.admin import helpers
from django.db.models import Model, ManyToManyField
from django.db.models import Model, ManyToManyField, FileField, ImageField
from django.forms import ModelForm
from admin_confirm.utils import snake_to_title_case
SAVE_ACTIONS = ["_save", "_saveasnew", "_addanother", "_continue"]
from admin_confirm.utils import get_admin_change_url, snake_to_title_case
from django.core.cache import cache
from django.views.decorators.cache import cache_control
from django.forms.formsets import all_valid
from admin_confirm.constants import (
CACHE_TIMEOUT,
CONFIRMATION_RECEIVED,
CONFIRM_ADD,
CONFIRM_CHANGE,
SAVE,
SAVE_ACTIONS,
CACHE_KEYS,
SAVE_AND_CONTINUE,
SAVE_AS_NEW,
)
class AdminConfirmMixin:
@ -34,7 +46,9 @@ class AdminConfirmMixin:
if self.confirmation_fields is not None:
return self.confirmation_fields
return flatten_fieldsets(self.get_fieldsets(request, obj))
model_fields = set([field.name for field in self.model._meta.fields])
admin_fields = set(flatten_fieldsets(self.get_fieldsets(request, obj)))
return list(model_fields & admin_fields)
def render_change_confirmation(self, request, context):
opts = self.model._meta
@ -81,14 +95,22 @@ class AdminConfirmMixin:
context,
)
@cache_control(private=True)
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
if request.method == "POST":
if (not object_id and "_confirm_add" in request.POST) or (
object_id and "_confirm_change" in request.POST
if (not object_id and CONFIRM_ADD in request.POST) or (
object_id and CONFIRM_CHANGE in request.POST
):
cache.delete_many(CACHE_KEYS.values())
return self._change_confirmation_view(
request, object_id, form_url, extra_context
)
elif CONFIRMATION_RECEIVED in request.POST:
return self._confirmation_received_view(
request, object_id, form_url, extra_context
)
else:
cache.delete_many(CACHE_KEYS.values())
extra_context = {
**(extra_context or {}),
@ -111,32 +133,160 @@ class AdminConfirmMixin:
Returns a dictionary of the fields and their changed values if any
"""
changed_data = {}
if form.is_valid():
if add:
for name, new_value in form.cleaned_data.items():
# Don't consider default values as changed for adding
default_value = model._meta.get_field(name).get_default()
if new_value is not None and new_value != default_value:
# Show what the default value is
changed_data[name] = [default_value, new_value]
else:
# Parse the changed data - Note that using form.changed_data would not work because initial is not set
for name, new_value in form.cleaned_data.items():
# Since the form considers initial as the value first shown in the form
# It could be incorrect when user hits save, and then hits "No, go back to edit"
obj.refresh_from_db()
# Note: getattr does not work on ManyToManyFields
field_object = model._meta.get_field(name)
initial_value = getattr(obj, name)
if isinstance(field_object, ManyToManyField):
initial_value = field_object.value_from_object(obj)
if initial_value != new_value:
changed_data[name] = [initial_value, new_value]
def _display_for_changed_data(field, initial_value, new_value):
if not (isinstance(field, FileField) or isinstance(field, ImageField)):
return [initial_value, new_value]
if initial_value:
if new_value == False:
# Clear has been selected
return [initial_value.name, None]
elif new_value:
return [initial_value.name, new_value.name]
else:
# No cover: Technically doesn't get called in current code because
# This function is only called if there was a difference in the data
return [initial_value.name, initial_value.name] # pragma: no cover
if new_value:
return [None, new_value.name]
return [None, None]
changed_data = {}
if add:
for name, new_value in form.cleaned_data.items():
# Don't consider default values as changed for adding
field_object = model._meta.get_field(name)
default_value = field_object.get_default()
if new_value is not None and new_value != default_value:
# Show what the default value is
changed_data[name] = _display_for_changed_data(
field_object, default_value, new_value
)
else:
# Parse the changed data - Note that using form.changed_data would not work because initial is not set
for name, new_value in form.cleaned_data.items():
# Since the form considers initial as the value first shown in the form
# It could be incorrect when user hits save, and then hits "No, go back to edit"
obj.refresh_from_db()
field_object = model._meta.get_field(name)
initial_value = getattr(obj, name)
# Note: getattr does not work on ManyToManyFields
if isinstance(field_object, ManyToManyField):
initial_value = field_object.value_from_object(obj)
if initial_value != new_value:
changed_data[name] = _display_for_changed_data(
field_object, initial_value, new_value
)
return changed_data
def _confirmation_received_view(self, request, object_id, form_url, extra_context):
"""
When the form is a multipart form, the object and POST are cached
This is required because file(s) cannot be programmically uploaded
ie. There is no way to set a file on the html form
If the form isn't multipart, this function would not be called.
If there are no file changes, do nothing to the request and send to Django.
If there are files uploaded, save the files from cached object to either:
- the object instance if already exists
- or save the new object and modify the request from `add` to `change`
and pass the request to Django
"""
def _reconstruct_request_files():
"""
Reconstruct the file(s) from the cached object (if any).
Returns a dictionary of field name to cached file
"""
reconstructed_files = {}
cached_object = cache.get(CACHE_KEYS["object"])
query_dict = cache.get(CACHE_KEYS["post"])
# Reconstruct the files from cached object
if not cached_object:
return
if not query_dict:
# Use the current POST, since it should mirror cached POST
query_dict = request.POST
if type(cached_object) != self.model:
# Do not use cache if the model doesn't match this model
return
for field in self.model._meta.get_fields():
if not (isinstance(field, FileField) or isinstance(field, ImageField)):
continue
cached_file = getattr(cached_object, field.name)
# If a file was uploaded, the field is omitted from the POST since it's in request.FILES
if not query_dict.get(field.name) and cached_file:
reconstructed_files[field.name] = cached_file
return reconstructed_files
reconstructed_files = _reconstruct_request_files()
if reconstructed_files:
obj = None
# remove the _confirm_add and _confirm_change from post
modified_post = request.POST.copy()
cached_post = cache.get(CACHE_KEYS["post"])
# No cover: __reconstruct_request_files currently checks for cached post so cached_post won't be None
if cached_post: # pragma: no cover
modified_post = cached_post.copy()
if CONFIRM_ADD in modified_post:
del modified_post[CONFIRM_ADD]
if CONFIRM_CHANGE in modified_post:
del modified_post[CONFIRM_CHANGE]
if object_id and not SAVE_AS_NEW in request.POST:
# Update the obj with the new uploaded files
# then pass rest of changes to Django
obj = self.model.objects.filter(id=object_id).first()
else:
# Create the obj and pass the rest as changes to Django
# (Since we are not handling the formsets/inlines)
# Note that this results in the "Yes, I'm Sure" submission
# act as a `change` not an `add`
obj = cache.get(CACHE_KEYS["object"])
# No cover: __reconstruct_request_files currently checks for cached obj so obj won't be None
if obj: # pragma: no cover
for field, file in reconstructed_files.items():
setattr(obj, field, file)
obj.save()
object_id = str(obj.id)
# Update the request path, used in the message to user and redirect
# Used in `self.response_change`
request.path = get_admin_change_url(obj)
if SAVE_AS_NEW in request.POST:
# We have already saved the new object
# So change action to _continue
del modified_post[SAVE_AS_NEW]
if self.save_as_continue:
modified_post[SAVE_AND_CONTINUE] = True
else:
modified_post[SAVE] = True
if "id" in modified_post:
del modified_post["id"]
modified_post["id"] = object_id
request.POST = modified_post
cache.delete_many(CACHE_KEYS.values())
return super()._changeform_view(request, object_id, form_url, extra_context)
def _change_confirmation_view(self, request, object_id, form_url, extra_context):
# This code is taken from super()._changeform_view
# https://github.com/django/django/blob/master/django/contrib/admin/options.py#L1575-L1592
@ -169,13 +319,19 @@ class AdminConfirmMixin:
)
form = ModelForm(request.POST, request.FILES, obj)
# Note to self: For inline instances see:
# https://github.com/django/django/blob/master/django/contrib/admin/options.py#L1582
form_validated = form.is_valid()
if form_validated:
new_object = self.save_form(request, form, change=not add)
else:
new_object = form.instance
formsets, inline_instances = self._create_formsets(
request, new_object, change=not add
)
# End code from super()._changeform_view
add_or_new = add or SAVE_AS_NEW in request.POST
# Get changed data to show on confirmation
changed_data = self._get_changed_data(form, model, obj, add)
changed_data = self._get_changed_data(form, model, obj, add_or_new)
changed_confirmation_fields = set(
self.get_confirmation_fields(request, obj)
@ -186,13 +342,17 @@ class AdminConfirmMixin:
# Parse the original save action from request
save_action = None
for key in request.POST.keys():
# No cover: There would not be a case of not request.POST.keys() and form is valid
for key in request.POST.keys(): # pragma: no cover
if key in SAVE_ACTIONS:
save_action = key
break
title_action = _("adding") if add else _("changing")
if form.is_multipart():
cache.set(CACHE_KEYS["post"], request.POST, timeout=CACHE_TIMEOUT)
cache.set(CACHE_KEYS["object"], new_object, timeout=CACHE_TIMEOUT)
title_action = _("adding") if add_or_new else _("changing")
context = {
**self.admin_site.each_context(request),
"preserved_filters": self.get_preserved_filters(request),
@ -205,8 +365,10 @@ class AdminConfirmMixin:
"opts": opts,
"changed_data": changed_data,
"add": add,
"save_as_new": SAVE_AS_NEW in request.POST,
"submit_name": save_action,
"form": form,
"formsets": formsets,
**(extra_context or {}),
}
return self.render_change_confirmation(request, context)

View File

@ -0,0 +1,17 @@
from django.conf import settings
SAVE = "_save"
SAVE_AS_NEW = "_saveasnew"
ADD_ANOTHER = "_addanother"
SAVE_AND_CONTINUE = "_continue"
SAVE_ACTIONS = [SAVE, SAVE_AS_NEW, ADD_ANOTHER, SAVE_AND_CONTINUE]
CONFIRM_ADD = "_confirm_add"
CONFIRM_CHANGE = "_confirm_change"
CONFIRMATION_RECEIVED = "_confirmation_received"
CACHE_TIMEOUT = getattr(settings, "ADMIN_CONFIRM_CACHE_TIMEOUT", 10)
CACHE_KEYS = {
"object": "admin_confirm__confirmation_object",
"post": "admin_confirm__confirmation_request_post",
}

View File

@ -31,27 +31,29 @@
{% block content %}
{% if add %}
<p>{% blocktrans with escaped_object=object %}Are you sure you want to add the {{ model_name }}?{% endblocktrans %}</p>
{% include "admin/change_data.html" %}
<form method="post" action="{% url opts|admin_urlname:'add'%}">{% csrf_token %}
{% if add or save_as_new %}
<p>{% blocktrans with escaped_object=object %}Are you sure you want to add the {{ model_name }}?{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans with escaped_object=object %}Are you sure you want to change the {{ model_name }} "{{ object_name }}"?{% endblocktrans %}</p>
{% endif %}
{% else %}
<p>{% blocktrans with escaped_object=object %}Are you sure you want to change the {{ model_name }} "{{ object_name }}"?{% endblocktrans %}</p>
{% include "admin/change_data.html" %}
<form method="post" action="{% url opts|admin_urlname:'change' object_id|admin_urlquote %}">{% csrf_token %}
{% endif %}
<div class=hidden>
{{ form }}
</div>
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
<div class="submit-row">
<input type="submit" value="{% trans 'Yes, Im sure' %}" name="{{ submit_name }}">
<p class="deletelink-box">
<a href="#" class="button cancel-link">{% trans "No, continue to edit" %}</a>
</p>
</div>
</form>
<form {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} method="post" {% if add %}action="{% url opts|admin_urlname:'add'%}" {% else %}action="{% url opts|admin_urlname:'change' object_id|admin_urlquote %}"{% endif %}>{% csrf_token %}
<div class="hidden">
{{form.as_p}}
{% for formset in formsets %}
{{ formset.as_p }}
{% endfor %}
</div>
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
{% if form.is_multipart %}<input type="hidden" name=CONFIRMATION_RECEIVED value="True">{% endif %}
<div class="submit-row">
<input type="submit" value="{% trans 'Yes, Im sure' %}" name="{{ submit_name }}">
<p class="deletelink-box">
<a href="#" class="button cancel-link">{% trans "No, continue to edit" %}</a>
</p>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,100 @@
# Testing Documentation/Notes
[![Coverage Status](https://coveralls.io/repos/github/TrangPham/django-admin-confirm/badge.svg)](https://coveralls.io/github/TrangPham/django-admin-confirm)
Hello, friend! You have found the list of test cases that this package can benefit from.
You seem concerned about the stability and reliability of this package. You're probably wondering if you should include it in your production codebase. Well, although I have tried very hard to get 100% code coverage, there are so many permutations of ModelAdmins in the wild. And I'm only one person.
So if you want to include this package in your production codebase, be aware that AdminConfirmMixin works best with simple unmodified ModelAdmins.
## Save Options
- [x] Save
- [x] Conitnue
- [x] Save As New
- [x] Add another
### Field types
- [x] CharField
- [x] PositiveIntegerField
- [x] DecimalField
- [x] TextField
- [x] ImageField
- [x] FileField
- [x] ManyToManyField
- [x] OneToOneField
- [x] ForeignKey
- [x] Custom Readonly fields
### Options
- [x] .exclude
- [x] .fields
- [x] .readonly_fields
- [x] Actions
### Options to test
- [x] ModelAdmin.fieldsets
- [ ] ModelAdmin.form
- [ ] ModelAdmin.raw_id_fields
- [ ] ModelAdmin.radio_fields
- [ ] ModelAdmin.autocomplete_fields
- [ ] ModelAdmin.prepopulated_fields
## ModelAdmin form template overrides?
https://docs.djangoproject.com/en/3.1/ref/contrib/admin/#custom-template-options
(Maybe??? IDK this is esoteric)
## Function overrides to test
- [ ] .save_model()
- [ ] .get_readonly_fields()
- [ ] .get_fields()
- [ ] .get_excludes()
- [ ] .get_form()
- [ ] .get_autocomplete_fields()
- [ ] .get_prepopulated_fields()
- [x] .get_fieldsets()
- [ ] ModelAdmin.formfield_for_manytomany()
- [ ] ModelAdmin.formfield_for_foreignkey()
- [ ] ModelAdmin.formfield_for_choice_field()
- [ ] ModelAdmin.get_changeform_initial_data()
## Inline instance support??
Confirmation on inline changes is not a current feature of this project.
Confirmation on add/change of ModelAdmin that includes inlines needs to be tested. Use AdminConfirmMixin with ModelAdmin containing inlines at your own risk.
- [ ] .inlines
- [ ] .get_inline_instances()
- [ ] .get_inlines() (New in Django 3.0)
- [ ] .get_formsets_with_inlines()
#### Options for inlines
- [ ] classes of inlines: Tabular, Stacked, etc
- [ ] extra
- [ ] action on the inline: add or change
- [ ] clicking add another on the inline
## IDK if we want to support these
- [ ] .get_changelist_form()
- [ ] ModelAdmin.list_editable
- [ ] ModelAdmin.changelist_view()
- [ ] ModelAdmin.add_view(request, form_url='', extra_context=None)
- [ ] ModelAdmin.change_view(request, object_id, form_url='', extra_context=None)
## More tests for these?
Note: Currently the code always calls super().\_changeform_view(), which would ensure permissions correct as well
- [x] ModelAdmin.has_add_permission
- [x] ModelAdmin.has_change_permission

View File

@ -1,8 +1,13 @@
from django.core.cache import cache
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
class ConfirmAdminTestCase(TestCase):
class AdminConfirmTestCase(TestCase):
"""
Helper TestCase class and common associated assertions
"""
@classmethod
def setUpTestData(cls):
cls.superuser = User.objects.create_superuser(
@ -10,6 +15,7 @@ class ConfirmAdminTestCase(TestCase):
)
def setUp(self):
cache.clear()
self.client.force_login(self.superuser)
self.factory = RequestFactory()
@ -24,7 +30,9 @@ class ConfirmAdminTestCase(TestCase):
# ManyToManyField should be embedded
self.assertIn("related-widget-wrapper", rendered_content)
def _assertSubmitHtml(self, rendered_content, save_action="_save"):
def _assertSubmitHtml(
self, rendered_content, save_action="_save", multipart_form=False
):
# Submit should conserve the save action
self.assertIn(
f'<input type="submit" value="Yes, Im sure" name="{save_action}">',
@ -34,6 +42,16 @@ class ConfirmAdminTestCase(TestCase):
self.assertNotIn("_confirm_add", rendered_content)
self.assertNotIn("_confirm_change", rendered_content)
confirmation_received_html = (
'<input type="hidden" name=CONFIRMATION_RECEIVED value="True">'
)
if multipart_form:
# Should have _confirmation_received as a hidden field
self.assertIn(confirmation_received_html, rendered_content)
else:
self.assertNotIn(confirmation_received_html, rendered_content)
def _assertSimpleFieldFormHtml(self, rendered_content, fields):
for k, v in fields.items():
self.assertIn(f'name="{k}"', rendered_content)

View File

@ -0,0 +1,307 @@
from unittest import mock
from django.core.cache import cache
from admin_confirm.tests.helpers import AdminConfirmTestCase
from tests.market.admin import ShoppingMallAdmin
from tests.market.models import GeneralManager, ShoppingMall, Town
from tests.factories import ShopFactory
from admin_confirm.constants import CACHE_KEYS, CONFIRMATION_RECEIVED
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
class TestAdminOptions(AdminConfirmTestCase):
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
@mock.patch.object(ShoppingMallAdmin, "fields", ["name", "town"])
def test_change_model_with_m2m_field_without_input_for_m2m_field_should_work(self):
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory() for i in range(3)]
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
mall.shops.set(shops)
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {
"id": mall.id,
"name": "name",
"town": town2.id,
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content, save_action="_continue"
)
# Should not have cached the unsaved obj
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNone(cached_item)
# Should not have saved changes yet
self.assertEqual(ShoppingMall.objects.count(), 1)
mall.refresh_from_db()
self.assertEqual(mall.name, "mall")
self.assertEqual(mall.general_manager, gm)
self.assertEqual(mall.town, town)
for shop in mall.shops.all():
self.assertIn(shop, shops)
# Click "Yes, I'm Sure"
confirmation_received_data = data
del confirmation_received_data["_confirm_change"]
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/",
data=confirmation_received_data,
)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
# Should have saved obj
self.assertEqual(ShoppingMall.objects.count(), 1)
saved_item = ShoppingMall.objects.all().first()
# should have updated fields that were in form
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.town, town2)
# should have presevered the fields that are not in form
self.assertEqual(saved_item.general_manager, gm)
for shop in saved_item.shops.all():
self.assertIn(shop, shops)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
@mock.patch.object(ShoppingMallAdmin, "exclude", ["shops"])
def test_when_m2m_field_in_exclude_changes_to_field_should_not_be_saved(self):
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory() for i in range(3)]
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
mall.shops.set(shops)
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {
"id": mall.id,
"name": "name",
"general_manager": gm2.id,
"shops": [1],
"town": town2.id,
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content, save_action="_continue"
)
# Should not have cached the unsaved obj
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNone(cached_item)
# Should not have saved changes yet
self.assertEqual(ShoppingMall.objects.count(), 1)
mall.refresh_from_db()
self.assertEqual(mall.name, "mall")
self.assertEqual(mall.general_manager, gm)
self.assertEqual(mall.town, town)
for shop in mall.shops.all():
self.assertIn(shop, shops)
# Click "Yes, I'm Sure"
confirmation_received_data = data
del confirmation_received_data["_confirm_change"]
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/",
data=confirmation_received_data,
)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
# Should have saved obj
self.assertEqual(ShoppingMall.objects.count(), 1)
saved_item = ShoppingMall.objects.all().first()
# should have updated fields that were in form
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.town, town2)
self.assertEqual(saved_item.general_manager, gm2)
# should have presevered the fields that are not in form (exclude)
for shop in saved_item.shops.all():
self.assertIn(shop, shops)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
@mock.patch.object(ShoppingMallAdmin, "exclude", ["shops", "name"])
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
def test_if_confirmation_fields_in_exclude_should_not_trigger_confirmation(self):
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory() for i in range(3)]
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
mall.shops.set(shops)
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {
"id": mall.id,
"name": "name",
"general_manager": gm2.id,
"shops": [1],
"town": town2.id,
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
)
# Should not be shown confirmation page
# SInce we used "Save and Continue", should show change page
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
# Should have saved the non excluded fields
mall.refresh_from_db()
for shop in shops:
self.assertIn(shop, mall.shops.all())
self.assertEqual(mall.name, "mall")
# Should have saved other fields
self.assertEqual(mall.town, town2)
self.assertEqual(mall.general_manager, gm2)
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
@mock.patch.object(ShoppingMallAdmin, "readonly_fields", ["shops", "name"])
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
def test_if_confirmation_fields_in_readonly_should_not_trigger_confirmation(self):
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory() for i in range(3)]
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
mall.shops.set(shops)
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {
"id": mall.id,
"name": "name",
"general_manager": gm2.id,
"shops": [1],
"town": town2.id,
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
)
# Should not be shown confirmation page
# SInce we used "Save and Continue", should show change page
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
# Should have saved the non excluded fields
mall.refresh_from_db()
for shop in shops:
self.assertIn(shop, mall.shops.all())
self.assertEqual(mall.name, "mall")
# Should have saved other fields
self.assertEqual(mall.town, town2)
self.assertEqual(mall.general_manager, gm2)
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
@mock.patch.object(ShoppingMallAdmin, "readonly_fields", ["shops"])
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
def test_readonly_fields_should_not_change(self):
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory() for i in range(3)]
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
mall.shops.set(shops)
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {
"id": mall.id,
"name": "name",
"general_manager": gm2.id,
"shops": [1],
"town": town2.id,
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content, save_action="_continue"
)
# Should not have cached the unsaved obj
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNone(cached_item)
# Should not have saved changes yet
self.assertEqual(ShoppingMall.objects.count(), 1)
mall.refresh_from_db()
self.assertEqual(mall.name, "mall")
self.assertEqual(mall.general_manager, gm)
self.assertEqual(mall.town, town)
for shop in mall.shops.all():
self.assertIn(shop, shops)
# Click "Yes, I'm Sure"
confirmation_received_data = data
del confirmation_received_data["_confirm_change"]
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/",
data=confirmation_received_data,
)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
# Should have saved obj
self.assertEqual(ShoppingMall.objects.count(), 1)
saved_item = ShoppingMall.objects.all().first()
# should have updated fields that were in form
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.town, town2)
self.assertEqual(saved_item.general_manager, gm2)
# should have presevered the fields that are not in form (exclude)
for shop in saved_item.shops.all():
self.assertIn(shop, shops)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))

View File

@ -1,17 +1,20 @@
from unittest import mock
from django.contrib.auth.models import User
from django.contrib.admin.sites import AdminSite
from django.contrib.admin.options import TO_FIELD_VAR
from django.http import HttpResponseForbidden, HttpResponseBadRequest
from django.urls import reverse
from admin_confirm.tests.helpers import ConfirmAdminTestCase
from tests.market.admin import ItemAdmin, InventoryAdmin
from tests.market.models import Item, Inventory
from admin_confirm.tests.helpers import AdminConfirmTestCase
from tests.market.admin import ItemAdmin, InventoryAdmin, ShoppingMallAdmin
from tests.market.models import Item, Inventory, ShoppingMall
from tests.factories import ItemFactory, ShopFactory, InventoryFactory
class TestConfirmChangeAndAdd(ConfirmAdminTestCase):
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
class TestConfirmChangeAndAdd(AdminConfirmTestCase):
def test_get_add_without_confirm_add(self):
ItemAdmin.confirm_add = False
response = self.client.get(reverse("admin:market_item_add"))
self.assertFalse(response.context_data.get("confirm_add"))
self.assertNotIn("_confirm_add", response.rendered_content)
@ -70,6 +73,36 @@ class TestConfirmChangeAndAdd(ConfirmAdminTestCase):
# Should not have been added yet
self.assertEqual(Inventory.objects.count(), 0)
def test_post_change_with_confirm_change_shoppingmall_name(self):
# When testing found that even though name was in confirmation_fields
# When only name changed, `form.is_valid` = False, and thus didn't trigger
# confirmation page previously, even though it should have
mall = ShoppingMall.objects.create(name="name")
data = {
"id": mall.id,
"name": "new name",
"_confirm_change": True,
"csrfmiddlewaretoken": "fake token",
"_save": True,
}
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/", data
)
# Ensure not redirected (confirmation page does not redirect)
self.assertEqual(response.status_code, 200)
expected_templates = [
"admin/market/shoppingmall/change_confirmation.html",
"admin/market/change_confirmation.html",
"admin/change_confirmation.html",
]
self.assertEqual(response.template_name, expected_templates)
self._assertSubmitHtml(rendered_content=response.rendered_content)
# Hasn't changed item yet
mall.refresh_from_db()
self.assertEqual(mall.name, "name")
def test_post_change_with_confirm_change(self):
item = ItemFactory(name="item")
data = {
@ -100,7 +133,9 @@ class TestConfirmChangeAndAdd(ConfirmAdminTestCase):
self._assertSimpleFieldFormHtml(
rendered_content=response.rendered_content, fields=form_data
)
self._assertSubmitHtml(rendered_content=response.rendered_content)
self._assertSubmitHtml(
rendered_content=response.rendered_content, multipart_form=True
)
# Hasn't changed item yet
item.refresh_from_db()
@ -120,9 +155,22 @@ class TestConfirmChangeAndAdd(ConfirmAdminTestCase):
def test_get_confirmation_fields_should_default_if_not_set(self):
expected_fields = [f.name for f in Item._meta.fields if f.name != "id"]
ItemAdmin.confirmation_fields = None
ItemAdmin.fields = expected_fields
admin = ItemAdmin(Item, AdminSite())
actual_fields = admin.get_confirmation_fields(self.factory.request())
self.assertEqual(expected_fields, actual_fields)
for field in expected_fields:
self.assertIn(field, actual_fields)
def test_get_confirmation_fields_default_should_only_include_fields_shown_on_admin(
self,
):
admin_fields = ["name", "price"]
ItemAdmin.confirmation_fields = None
ItemAdmin.fields = admin_fields
admin = ItemAdmin(Item, AdminSite())
actual_fields = admin.get_confirmation_fields(self.factory.request())
for field in admin_fields:
self.assertIn(field, actual_fields)
def test_get_confirmation_fields_if_set(self):
expected_fields = ["name", "currency"]

View File

@ -1,12 +1,15 @@
from unittest import mock
from admin_confirm.admin import AdminConfirmMixin
from django.urls import reverse
from admin_confirm.tests.helpers import ConfirmAdminTestCase
from admin_confirm.tests.helpers import AdminConfirmTestCase
from tests.market.admin import ShoppingMallAdmin
from tests.market.models import ShoppingMall
from tests.factories import ShopFactory
class TestConfirmChangeAndAddM2MField(ConfirmAdminTestCase):
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
class TestConfirmChangeAndAddM2MField(AdminConfirmTestCase):
def test_post_add_without_confirm_add_m2m(self):
shops = [ShopFactory() for i in range(3)]
@ -84,6 +87,9 @@ class TestConfirmChangeAndAddM2MField(ConfirmAdminTestCase):
]
self.assertEqual(response.template_name, expected_templates)
# Should show two lists for the m2m current and modified values
self.assertEqual(response.rendered_content.count("<ul>"), 2)
self._assertManyToManyFormHtml(
rendered_content=response.rendered_content,
options=shops,

View File

@ -0,0 +1,477 @@
from unittest import mock
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.cache import cache
from django.urls import reverse
from admin_confirm.tests.helpers import AdminConfirmTestCase
from tests.market.admin import ItemAdmin, ShoppingMallAdmin
from tests.market.models import GeneralManager, Item, ShoppingMall, Town
from tests.factories import ItemFactory, ShopFactory
from admin_confirm.constants import CACHE_KEYS, CONFIRMATION_RECEIVED
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
class TestConfirmSaveActions(AdminConfirmTestCase):
def test_simple_add_with_save(self):
# Load the Add Item Page
ItemAdmin.confirm_add = True
response = self.client.get(reverse("admin:market_item_add"))
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_add"))
self.assertIn("_confirm_add", response.rendered_content)
# Click "Save"
data = {
"name": "name",
"price": 2.0,
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_add": True,
"_save": True,
}
response = self.client.post(reverse("admin:market_item_add"), data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content,
save_action="_save",
multipart_form=True,
)
# Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item)
self.assertIsNone(cached_item.id)
self.assertEqual(cached_item.name, data["name"])
self.assertEqual(cached_item.price, data["price"])
self.assertEqual(cached_item.currency, data["currency"])
# Should not have saved the item yet
self.assertEqual(Item.objects.count(), 0)
# Click "Yes, I'm Sure"
del data["_confirm_add"]
data[CONFIRMATION_RECEIVED] = True
response = self.client.post(reverse("admin:market_item_add"), data=data)
# Should have redirected to changelist
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/market/item/")
# Should have saved item
self.assertEqual(Item.objects.count(), 1)
saved_item = Item.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"])
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_simple_change_with_continue(self):
item = ItemFactory(name="Not name")
# Load the Change Item Page
ItemAdmin.confirm_change = True
response = self.client.get(f"/admin/market/item/{item.id}/change/")
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_change"))
self.assertIn("_confirm_change", response.rendered_content)
# Click "Save And Continue"
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content,
save_action="_continue",
multipart_form=True,
)
# Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item)
self.assertIsNone(cached_item.id)
self.assertEqual(cached_item.name, data["name"])
self.assertEqual(cached_item.price, data["price"])
self.assertEqual(cached_item.currency, data["currency"])
# Should not have saved the changes yet
self.assertEqual(Item.objects.count(), 1)
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data[CONFIRMATION_RECEIVED] = True
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/{item.id}/change/")
# Should have saved item
self.assertEqual(Item.objects.count(), 1)
saved_item = Item.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"])
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_file_and_image_add_addanother(self):
# Load the Add Item Page
ItemAdmin.confirm_add = True
response = self.client.get(reverse("admin:market_item_add"))
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_add"))
self.assertIn("_confirm_add", response.rendered_content)
# Select files
image_path = "screenshot.png"
f = SimpleUploadedFile(
name="test_file.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
i = SimpleUploadedFile(
name="test_image.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
# Click "Save"
data = {
"name": "name",
"price": 2.0,
"currency": Item.VALID_CURRENCIES[0][0],
"file": f,
"image": i,
"_confirm_add": True,
"_addanother": True,
}
response = self.client.post(reverse("admin:market_item_add"), data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content,
save_action="_addanother",
multipart_form=True,
)
# Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item)
self.assertIsNone(cached_item.id)
self.assertEqual(cached_item.name, data["name"])
self.assertEqual(cached_item.price, data["price"])
self.assertEqual(cached_item.currency, data["currency"])
self.assertEqual(cached_item.file, data["file"])
self.assertEqual(cached_item.image, data["image"])
# Should not have saved the item yet
self.assertEqual(Item.objects.count(), 0)
# Click "Yes, I'm Sure"
confirmation_data = data.copy()
del confirmation_data["_confirm_add"]
del confirmation_data["image"]
del confirmation_data["file"]
confirmation_data[CONFIRMATION_RECEIVED] = True
response = self.client.post(
reverse("admin:market_item_add"), data=confirmation_data
)
# Should have redirected to changelist
self.assertEqual(response.status_code, 302)
# Should show add page since "add another" was selected
self.assertEqual(response.url, "/admin/market/item/add/")
# Should have saved item
self.assertEqual(Item.objects.count(), 1)
saved_item = Item.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"])
self.assertEqual(saved_item.file, data["file"])
self.assertEqual(saved_item.image, data["image"])
self.assertEqual(saved_item.file.name, "test_file.jpg")
self.assertEqual(saved_item.image.name, "test_image.jpg")
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_file_and_image_change_with_saveasnew(self):
item = ItemFactory(name="Not name")
# Select files
image_path = "screenshot.png"
f = SimpleUploadedFile(
name="test_file.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
i = SimpleUploadedFile(
name="test_image.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
item.file = f
item.image = i
item.save()
# Load the Change Item Page
ItemAdmin.confirm_change = True
ItemAdmin.fields = ["name", "price", "file", "image", "currency"]
ItemAdmin.save_as = True
ItemAdmin.save_as_continue = True
response = self.client.get(f"/admin/market/item/{item.id}/change/")
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_change"))
self.assertIn("_confirm_change", response.rendered_content)
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
# Click "Save And Continue"
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"image": i2,
"file": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_saveasnew": True,
}
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content,
save_action="_saveasnew",
multipart_form=True,
)
# Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item)
self.assertIsNone(cached_item.id)
self.assertEqual(cached_item.name, data["name"])
self.assertEqual(cached_item.price, data["price"])
self.assertEqual(cached_item.currency, data["currency"])
self.assertFalse(cached_item.file.name)
self.assertEqual(cached_item.image, i2)
# Should not have saved the changes yet
self.assertEqual(Item.objects.count(), 1)
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertIsNotNone(item.file)
self.assertIsNotNone(item.image)
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data["image"] = ""
data[CONFIRMATION_RECEIVED] = True
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/{item.id + 1}/change/")
# Should not have changed existing item
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertEqual(item.file.name.count("test_file"), 1)
self.assertEqual(item.image.name.count("test_image2"), 0)
self.assertEqual(item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
self.assertFalse(new_item.file)
self.assertEqual(new_item.image, i2)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_relations_add(self):
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory() for i in range(3)]
town = Town.objects.create(name="town")
# Load the Add ShoppingMall Page
ShoppingMallAdmin.confirm_add = True
response = self.client.get(reverse("admin:market_shoppingmall_add"))
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_add"))
self.assertIn("_confirm_add", response.rendered_content)
# Click "Save"
data = {
"name": "name",
"shops": [s.id for s in shops],
"general_manager": gm.id,
"town": town.id,
"_confirm_add": True,
"_save": True,
}
response = self.client.post(reverse("admin:market_shoppingmall_add"), data=data)
# Should be shown confirmation page
self._assertManyToManyFormHtml(
rendered_content=response.rendered_content,
options=shops,
selected_ids=data["shops"],
)
self._assertSubmitHtml(
rendered_content=response.rendered_content, save_action="_save"
)
# Should not have cached the unsaved object
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNone(cached_item)
# Click "Yes, I'm Sure"
confirmation_received_data = data
del confirmation_received_data["_confirm_add"]
confirmation_received_data[CONFIRMATION_RECEIVED] = True
response = self.client.post(
reverse("admin:market_shoppingmall_add"), data=confirmation_received_data
)
# Should have redirected to changelist
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/market/shoppingmall/")
# Should have saved object
self.assertEqual(ShoppingMall.objects.count(), 1)
saved_item = ShoppingMall.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.general_manager, gm)
self.assertEqual(saved_item.town, town)
for shop in saved_item.shops.all():
self.assertIn(shop, shops)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_relation_change_with_saveasnew(self):
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory() for i in range(3)]
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
mall.shops.set(shops)
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
# Load the Change ShoppingMall Page
ShoppingMallAdmin.confirm_change = True
response = self.client.get(f"/admin/market/shoppingmall/{mall.id}/change/")
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_change"))
self.assertIn("_confirm_change", response.rendered_content)
# Click "Save"
data = {
"id": mall.id,
"name": "name",
"shops": [s.id for s in shops2],
"general_manager": gm2.id,
"town": town2.id,
"_confirm_change": True,
"_saveasnew": True,
}
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
)
# Should be shown confirmation page
self._assertManyToManyFormHtml(
rendered_content=response.rendered_content,
options=shops,
selected_ids=data["shops"],
)
self._assertSubmitHtml(
rendered_content=response.rendered_content, save_action="_saveasnew"
)
# Should not have cached the unsaved obj
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNone(cached_item)
# Should not have saved changes yet
self.assertEqual(ShoppingMall.objects.count(), 1)
mall.refresh_from_db()
self.assertEqual(mall.name, "mall")
self.assertEqual(mall.general_manager, gm)
self.assertEqual(mall.town, town)
for shop in mall.shops.all():
self.assertIn(shop, shops)
# Click "Yes, I'm Sure"
confirmation_received_data = data.copy()
del confirmation_received_data["_confirm_change"]
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/",
data=confirmation_received_data,
)
# Should not have redirected to changelist
self.assertEqual(
response.url, f"/admin/market/shoppingmall/{mall.id + 1}/change/"
)
# Should have saved obj
self.assertEqual(ShoppingMall.objects.count(), 2)
# Should not have changed old obj
mall.refresh_from_db()
self.assertEqual(mall.name, "mall")
self.assertEqual(mall.general_manager, gm)
self.assertEqual(mall.town, town)
for shop in mall.shops.all():
self.assertIn(shop, shops)
# Should have created new obj
saved_item = ShoppingMall.objects.filter(id=mall.id + 1).first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.general_manager, gm2)
self.assertEqual(saved_item.town, town2)
for shop in saved_item.shops.all():
self.assertIn(shop, shops2)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))

View File

@ -0,0 +1,458 @@
from unittest import mock
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.cache import cache
from django.urls import reverse
from admin_confirm.tests.helpers import AdminConfirmTestCase
from tests.market.admin import ItemAdmin, ShoppingMallAdmin
from tests.market.models import GeneralManager, Item, ShoppingMall, Town
from tests.factories import ItemFactory, ShopFactory
from admin_confirm.constants import CACHE_KEYS, CONFIRMATION_RECEIVED
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
class TestConfirmationCache(AdminConfirmTestCase):
def test_simple_add(self):
# Load the Add Item Page
ItemAdmin.confirm_add = True
response = self.client.get(reverse("admin:market_item_add"))
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_add"))
self.assertIn("_confirm_add", response.rendered_content)
# Click "Save"
data = {
"name": "name",
"price": 2.0,
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_add": True,
"_save": True,
}
response = self.client.post(reverse("admin:market_item_add"), data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content,
save_action="_save",
multipart_form=True,
)
# Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item)
self.assertIsNone(cached_item.id)
self.assertEqual(cached_item.name, data["name"])
self.assertEqual(cached_item.price, data["price"])
self.assertEqual(cached_item.currency, data["currency"])
# Should not have saved the item yet
self.assertEqual(Item.objects.count(), 0)
# Click "Yes, I'm Sure"
del data["_confirm_add"]
data[CONFIRMATION_RECEIVED] = True
response = self.client.post(reverse("admin:market_item_add"), data=data)
# Should have redirected to changelist
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/market/item/")
# Should have saved item
self.assertEqual(Item.objects.count(), 1)
saved_item = Item.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"])
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_simple_change(self):
item = ItemFactory(name="Not name")
# Load the Change Item Page
ItemAdmin.confirm_change = True
response = self.client.get(f"/admin/market/item/{item.id}/change/")
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_change"))
self.assertIn("_confirm_change", response.rendered_content)
# Click "Save And Continue"
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content,
save_action="_continue",
multipart_form=True,
)
# Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item)
self.assertIsNone(cached_item.id)
self.assertEqual(cached_item.name, data["name"])
self.assertEqual(cached_item.price, data["price"])
self.assertEqual(cached_item.currency, data["currency"])
# Should not have saved the changes yet
self.assertEqual(Item.objects.count(), 1)
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data[CONFIRMATION_RECEIVED] = True
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/{item.id}/change/")
# Should have saved item
self.assertEqual(Item.objects.count(), 1)
saved_item = Item.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"])
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_file_and_image_add(self):
# Load the Add Item Page
ItemAdmin.confirm_add = True
response = self.client.get(reverse("admin:market_item_add"))
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_add"))
self.assertIn("_confirm_add", response.rendered_content)
# Select files
image_path = "screenshot.png"
f = SimpleUploadedFile(
name="test_file.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
i = SimpleUploadedFile(
name="test_image.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
# Click "Save"
data = {
"name": "name",
"price": 2.0,
"currency": Item.VALID_CURRENCIES[0][0],
"file": f,
"image": i,
"_confirm_add": True,
"_save": True,
}
response = self.client.post(reverse("admin:market_item_add"), data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content,
save_action="_save",
multipart_form=True,
)
# Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item)
self.assertIsNone(cached_item.id)
self.assertEqual(cached_item.name, data["name"])
self.assertEqual(cached_item.price, data["price"])
self.assertEqual(cached_item.currency, data["currency"])
self.assertEqual(cached_item.file, data["file"])
self.assertEqual(cached_item.image, data["image"])
# Should not have saved the item yet
self.assertEqual(Item.objects.count(), 0)
# Click "Yes, I'm Sure"
confirmation_data = data.copy()
del confirmation_data["_confirm_add"]
del confirmation_data["image"]
del confirmation_data["file"]
confirmation_data[CONFIRMATION_RECEIVED] = True
response = self.client.post(
reverse("admin:market_item_add"), data=confirmation_data
)
# Should have redirected to changelist
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/market/item/")
# Should have saved item
self.assertEqual(Item.objects.count(), 1)
saved_item = Item.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"])
self.assertEqual(saved_item.file, data["file"])
self.assertEqual(saved_item.image, data["image"])
self.assertEqual(saved_item.file.name, "test_file.jpg")
self.assertEqual(saved_item.image.name, "test_image.jpg")
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_file_and_image_change(self):
item = ItemFactory(name="Not name")
# Select files
image_path = "screenshot.png"
f = SimpleUploadedFile(
name="test_file.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
i = SimpleUploadedFile(
name="test_image.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
item.file = f
item.image = i
item.save()
# Load the Change Item Page
ItemAdmin.confirm_change = True
ItemAdmin.fields = ["name", "price", "file", "image", "currency"]
response = self.client.get(f"/admin/market/item/{item.id}/change/")
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_change"))
self.assertIn("_confirm_change", response.rendered_content)
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
# Click "Save And Continue"
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"image": i2,
"file": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content,
save_action="_continue",
multipart_form=True,
)
# Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item)
self.assertIsNone(cached_item.id)
self.assertEqual(cached_item.name, data["name"])
self.assertEqual(cached_item.price, data["price"])
self.assertEqual(cached_item.currency, data["currency"])
self.assertFalse(cached_item.file.name)
self.assertEqual(cached_item.image, i2)
# Should not have saved the changes yet
self.assertEqual(Item.objects.count(), 1)
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertIsNotNone(item.file)
self.assertIsNotNone(item.image)
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data["image"] = ""
data[CONFIRMATION_RECEIVED] = True
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/{item.id}/change/")
# Should have saved item
self.assertEqual(Item.objects.count(), 1)
saved_item = Item.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"])
self.assertFalse(saved_item.file)
self.assertEqual(saved_item.image, i2)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_relations_add(self):
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory() for i in range(3)]
town = Town.objects.create(name="town")
# Load the Add ShoppingMall Page
ShoppingMallAdmin.confirm_add = True
response = self.client.get(reverse("admin:market_shoppingmall_add"))
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_add"))
self.assertIn("_confirm_add", response.rendered_content)
# Click "Save"
data = {
"name": "name",
"shops": [s.id for s in shops],
"general_manager": gm.id,
"town": town.id,
"_confirm_add": True,
"_save": True,
}
response = self.client.post(reverse("admin:market_shoppingmall_add"), data=data)
# Should be shown confirmation page
self._assertManyToManyFormHtml(
rendered_content=response.rendered_content,
options=shops,
selected_ids=data["shops"],
)
self._assertSubmitHtml(
rendered_content=response.rendered_content, save_action="_save"
)
# Should not have cached the unsaved object
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNone(cached_item)
# Should not have saved the object yet
self.assertEqual(ShoppingMall.objects.count(), 0)
# Click "Yes, I'm Sure"
confirmation_received_data = data
del confirmation_received_data["_confirm_add"]
response = self.client.post(
reverse("admin:market_shoppingmall_add"), data=confirmation_received_data
)
# Should have redirected to changelist
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/market/shoppingmall/")
# Should have saved object
self.assertEqual(ShoppingMall.objects.count(), 1)
saved_item = ShoppingMall.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.general_manager, gm)
self.assertEqual(saved_item.town, town)
for shop in saved_item.shops.all():
self.assertIn(shop, shops)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_relation_change(self):
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory() for i in range(3)]
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
mall.shops.set(shops)
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
# Load the Change ShoppingMall Page
ShoppingMallAdmin.confirm_change = True
response = self.client.get(f"/admin/market/shoppingmall/{mall.id}/change/")
# Should be asked for confirmation
self.assertTrue(response.context_data.get("confirm_change"))
self.assertIn("_confirm_change", response.rendered_content)
# Click "Save"
data = {
"id": mall.id,
"name": "name",
"shops": [s.id for s in shops2],
"general_manager": gm2.id,
"town": town2.id,
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
)
# Should be shown confirmation page
self._assertManyToManyFormHtml(
rendered_content=response.rendered_content,
options=shops,
selected_ids=data["shops"],
)
self._assertSubmitHtml(
rendered_content=response.rendered_content, save_action="_continue"
)
# Should not have cached the unsaved obj
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNone(cached_item)
# Should not have saved changes yet
self.assertEqual(ShoppingMall.objects.count(), 1)
mall.refresh_from_db()
self.assertEqual(mall.name, "mall")
self.assertEqual(mall.general_manager, gm)
self.assertEqual(mall.town, town)
for shop in mall.shops.all():
self.assertIn(shop, shops)
# Click "Yes, I'm Sure"
confirmation_received_data = data
del confirmation_received_data["_confirm_change"]
confirmation_received_data[CONFIRMATION_RECEIVED] = True
response = self.client.post(
f"/admin/market/shoppingmall/{mall.id}/change/",
data=confirmation_received_data,
)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
# Should have saved obj
self.assertEqual(ShoppingMall.objects.count(), 1)
saved_item = ShoppingMall.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.general_manager, gm2)
self.assertEqual(saved_item.town, town2)
for shop in saved_item.shops.all():
self.assertIn(shop, shops2)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))

View File

@ -0,0 +1,169 @@
"""
Tests ModelAdmin with fieldsets custom configured through one of the possible methods
Ensures that AdminConfirmMixin works correctly when implimenting class alters default fieldsets
Test Matrix
method: `.fieldsets =`, `def get_fieldsets()`
action: change, add
fieldset: simple, with readonly fields, with custom fields
"""
import pytest
from importlib import reload
from tests.market.admin import item_admin
from django.contrib.auth.models import User
from django.contrib.admin import AdminSite
from django.test.client import RequestFactory
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.cache import cache
from admin_confirm.constants import CACHE_KEYS, CONFIRM_CHANGE, CONFIRMATION_RECEIVED
from tests.market.models import Item
from tests.factories import ItemFactory
def fs_simple(admin):
return (
(None, {"fields": ("name", "price", "image")}),
(
"Advanced options",
{
"classes": ("collapse",),
"fields": ("currency", "file"),
},
),
)
def fs_w_readonly(admin):
admin.readonly_fields = ["description", "image"]
return fs_simple(admin)
def fs_w_custom(admin):
admin.one = lambda self, obj: "ReadOnly"
admin.two = lambda self, obj: "ReadOnly"
admin.three = lambda self, obj: "ReadOnly"
admin.readonly_fields = ["one", "two", "three"]
return (
(None, {"fields": ("name", "price", "image", "one")}),
(
"Advanced options",
{
"classes": ("collapse",),
"fields": ("currency", "two", "file"),
},
),
("More Info", {"fields": ("three", "description")}),
)
def set_fieldsets(admin, fieldset):
admin.fieldsets = fieldset
def override_get_fieldsets(admin, fieldset):
admin.get_fieldsets = lambda self, request, obj=None: fieldset
methods = [set_fieldsets, override_get_fieldsets]
actions = ["_confirm_add", "_confirm_change"]
fieldsets = [fs_simple, fs_w_readonly, fs_w_custom]
param_matrix = []
for method in methods:
for fieldset in fieldsets:
for action in actions:
param_matrix.append((method, fieldset, action))
@pytest.mark.django_db()
@pytest.mark.parametrize("method,get_fieldset,action", param_matrix)
def test_fieldsets(client, method, get_fieldset, action):
reload(item_admin)
admin = item_admin.ItemAdmin
fs = get_fieldset(admin)
# set fieldsets via one of the methods
method(admin, fs)
admin_instance = admin(admin_site=AdminSite(), model=Item)
request = RequestFactory().request
assert admin_instance.get_fieldsets(request) == fs
user = User.objects.create_superuser(
username="super", email="super@email.org", password="pass"
)
client.force_login(user)
url = "/admin/market/item/add/"
image_path = "screenshot.png"
f2 = SimpleUploadedFile(
name="new_file.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
i2 = SimpleUploadedFile(
name="new_image.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
data = {
"name": "new name",
"price": 2,
"currency": "USD",
"image": i2,
"file": f2,
action: True,
"_save": True,
}
for f in admin.readonly_fields:
if f in data.keys():
del data[f]
if action == CONFIRM_CHANGE:
url = "/admin/market/item/1/change/"
f = SimpleUploadedFile(
name="old_file.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
i = SimpleUploadedFile(
name="old_image.jpg",
content=open(image_path, "rb").read(),
content_type="image/jpeg",
)
item = ItemFactory(name="old name", price=1, currency="CAD", file=f, image=i)
data["id"] = item.id
cache_item = Item()
for f in ["name", "price", "currency", "image", "file"]:
if f not in admin.readonly_fields:
setattr(cache_item, f, data[f])
cache.set(CACHE_KEYS["object"], cache_item)
cache.set(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure"
del data[action]
data[CONFIRMATION_RECEIVED] = True
response = client.post(url, data=data)
# Should have redirected to changelist
# assert response.status_code == 302
assert response.url == "/admin/market/item/"
# Should have saved item
assert Item.objects.count() == 1
saved_item = Item.objects.all().first()
for f in ["name", "price", "currency"]:
if f not in admin.readonly_fields:
assert getattr(saved_item, f) == data[f]
if "file" not in admin.readonly_fields:
assert "new_file" in saved_item.file.name
if "image" not in admin.readonly_fields:
assert "new_image" in saved_item.image.name
reload(item_admin)

View File

@ -0,0 +1,967 @@
"""
Ensure that files are saved during confirmation
Without file changes, Django is relied on
With file changes, we cache the object, save it with
the files if new, or add files to existing obj and save
Then send the rest of the changes to Django to handle
This is arguably the most we fiddle with the Django request
Thus we should test it extensively
"""
import time
from unittest import mock
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.cache import cache
from admin_confirm.tests.helpers import AdminConfirmTestCase
from admin_confirm.constants import CACHE_KEYS, CONFIRMATION_RECEIVED
from tests.market.admin import ItemAdmin
from tests.market.models import Item, Shop
from tests.factories import ItemFactory, ShopFactory
class TestFileCache(AdminConfirmTestCase):
def setUp(self):
# Load the Change Item Page
ItemAdmin.confirm_change = True
ItemAdmin.fields = ["name", "price", "file", "image", "currency"]
ItemAdmin.save_as = True
ItemAdmin.save_as_continue = True
self.image_path = "screenshot.png"
f = SimpleUploadedFile(
name="test_file.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
i = SimpleUploadedFile(
name="test_image.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
self.item = ItemFactory(name="Not name", file=f, image=i)
return super().setUp()
def test_save_as_continue_true_should_not_redirect_to_changelist(self):
item = self.item
# Load the Change Item Page
ItemAdmin.save_as_continue = True
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"file": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_saveasnew": True,
}
# Set cache
cache_item = Item(
name=data["name"],
price=data["price"],
currency=data["currency"],
image=i2,
)
cache.set(CACHE_KEYS["object"], cache_item)
cache.set(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(
f"/admin/market/item/{self.item.id}/change/", data=data
)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/{self.item.id + 1}/change/")
# Should not have changed existing item
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertEqual(item.file.name.count("test_file"), 1)
self.assertEqual(item.image.name.count("test_image2"), 0)
self.assertEqual(item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
self.assertFalse(new_item.file)
self.assertEqual(new_item.image.name.count("test_image2"), 1)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_save_as_continue_false_should_redirect_to_changelist(self):
item = self.item
# Load the Change Item Page
ItemAdmin.save_as_continue = False
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"file": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_saveasnew": True,
}
# Set cache
cache_item = Item(
name=data["name"],
price=data["price"],
currency=data["currency"],
image=i2,
)
cache.set(CACHE_KEYS["object"], cache_item)
cache.set(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(
f"/admin/market/item/{self.item.id}/change/", data=data
)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should not have changed existing item
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertEqual(item.file.name.count("test_file"), 1)
self.assertEqual(item.image.name.count("test_image2"), 0)
self.assertEqual(item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
self.assertFalse(new_item.file)
self.assertEqual(new_item.image.name.count("test_image2"), 1)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_saveasnew_without_any_file_changes_should_save_new_instance_without_files(
self,
):
item = self.item
# Request.POST
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"file": "",
"image": "",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_saveasnew": True,
}
# Set cache
cache_item = Item(
name=data["name"],
price=data["price"],
currency=data["currency"],
)
cache.set(CACHE_KEYS["object"], cache_item)
cache.set(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(
f"/admin/market/item/{self.item.id}/change/", data=data
)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/{self.item.id + 1}/change/")
# Should not have changed existing item
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertEqual(item.file.name.count("test_file"), 1)
self.assertEqual(item.image.name.count("test_image2"), 0)
self.assertEqual(item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
# In Django (by default), the save as new does not transfer over the files
self.assertFalse(new_item.file)
self.assertFalse(new_item.image)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_add_with_upload_file_should_save_new_instance_with_files(self):
# Request.POST
data = {
"name": "name",
"price": 2.0,
"image": "",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_add": True,
"_save": True,
}
# Upload new file
f2 = SimpleUploadedFile(
name="test_file2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Set cache
cache_item = Item(
name=data["name"], price=data["price"], currency=data["currency"], file=f2
)
cache.set(CACHE_KEYS["object"], cache_item)
cache.set(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure"
del data["_confirm_add"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(f"/admin/market/item/add/", data=data)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should not have changed existing item
self.item.refresh_from_db()
self.assertEqual(self.item.name, "Not name")
self.assertEqual(self.item.file.name.count("test_file"), 1)
self.assertEqual(self.item.image.name.count("test_image2"), 0)
self.assertEqual(self.item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=self.item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
self.assertIn("test_file2", new_item.file.name)
self.assertFalse(new_item.image)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_add_without_cached_post_should_save_new_instance_with_file(self):
# Request.POST
data = {
"name": "name",
"price": 2.0,
"image": "",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_add": True,
"_save": True,
}
# Upload new file
f2 = SimpleUploadedFile(
name="test_file2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Set cache
cache_item = Item(
name=data["name"], price=data["price"], currency=data["currency"], file=f2
)
cache.set(CACHE_KEYS["object"], cache_item)
# Make sure there's no post cached post
cache.delete(CACHE_KEYS["post"])
# Click "Yes, I'm Sure"
del data["_confirm_add"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(f"/admin/market/item/add/", data=data)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should not have changed existing item
self.item.refresh_from_db()
self.assertEqual(self.item.name, "Not name")
self.assertEqual(self.item.file.name.count("test_file"), 1)
self.assertEqual(self.item.image.name.count("test_image2"), 0)
self.assertEqual(self.item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=self.item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
self.assertFalse(new_item.image)
# Able to save the cached file since cached object was there even though cached post was not
self.assertIn("test_file2", new_item.file.name)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_add_without_cached_object_should_save_new_instance_but_not_have_file(self):
# Request.POST
data = {
"name": "name",
"price": 2.0,
"image": "",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_add": True,
"_save": True,
}
# Upload new file
f2 = SimpleUploadedFile(
name="test_file2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Set cache
cache_item = Item(
name=data["name"], price=data["price"], currency=data["currency"], file=f2
)
# Make sure there's no post cached obj
cache.delete(CACHE_KEYS["object"])
cache.set(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure"
del data["_confirm_add"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(f"/admin/market/item/add/", data=data)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should not have changed existing item
self.item.refresh_from_db()
self.assertEqual(self.item.name, "Not name")
self.assertEqual(self.item.file.name.count("test_file"), 1)
self.assertEqual(self.item.image.name.count("test_image2"), 0)
self.assertEqual(self.item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=self.item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
self.assertFalse(new_item.image)
# FAILED to save the file, because cached item was not there
self.assertFalse(new_item.file)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_add_without_any_cache_should_save_new_instance_but_not_have_file(self):
# Request.POST
data = {
"name": "name",
"price": 2.0,
"image": "",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_add": True,
"_save": True,
}
# Upload new file
f2 = SimpleUploadedFile(
name="test_file2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Set cache
cache_item = Item(
name=data["name"], price=data["price"], currency=data["currency"], file=f2
)
# Make sure there's no cache
cache.delete(CACHE_KEYS["object"])
cache.delete(CACHE_KEYS["post"])
# Click "Yes, I'm Sure"
del data["_confirm_add"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(f"/admin/market/item/add/", data=data)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should not have changed existing item
self.item.refresh_from_db()
self.assertEqual(self.item.name, "Not name")
self.assertEqual(self.item.file.name.count("test_file"), 1)
self.assertEqual(self.item.image.name.count("test_image2"), 0)
self.assertEqual(self.item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=self.item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
self.assertFalse(new_item.image)
# FAILED to save the file, because cached item was not there
self.assertFalse(new_item.file)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_change_without_cached_post_should_save_file_changes(self):
item = self.item
# Load the Change Item Page
ItemAdmin.save_as_continue = False
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"image": i2,
"file": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_saveasnew": True,
}
# Set cache
cache_item = Item(
name=data["name"],
price=data["price"],
currency=data["currency"],
image=i2,
)
cache.set(CACHE_KEYS["object"], cache_item)
# Ensure no cached post
cache.delete(CACHE_KEYS["post"])
# Click "Yes, I'm Sure"
del data["_confirm_change"]
# Image would have been in FILES and not in POST
del data["image"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(
f"/admin/market/item/{self.item.id}/change/", data=data
)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should not have changed existing item
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertEqual(item.file.name.count("test_file"), 1)
self.assertEqual(item.image.name.count("test_image2"), 0)
self.assertEqual(item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
# Should have cleared `file` since clear was selected
self.assertFalse(new_item.file)
# Saved cached file from cached obj even if cached post was missing
self.assertIn("test_image2", new_item.image.name)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_change_without_cached_object_should_save_but_without_file_changes(self):
item = self.item
# Load the Change Item Page
ItemAdmin.save_as_continue = False
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"file": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_saveasnew": True,
}
# Set cache
cache_item = Item(
name=data["name"],
price=data["price"],
currency=data["currency"],
image=i2,
)
# Ensure no cached obj
cache.delete(CACHE_KEYS["object"])
cache.set(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(
f"/admin/market/item/{self.item.id}/change/", data=data
)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should not have changed existing item
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertEqual(item.file.name.count("test_file"), 1)
self.assertEqual(item.image.name.count("test_image2"), 0)
self.assertEqual(item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
self.assertFalse(new_item.file)
# FAILED to save image
self.assertFalse(new_item.image)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_change_without_any_cache_should_save_but_not_have_file_changes(self):
item = self.item
# Load the Change Item Page
ItemAdmin.save_as_continue = False
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"file": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_saveasnew": True,
}
# Set cache
cache_item = Item(
name=data["name"],
price=data["price"],
currency=data["currency"],
image=i2,
)
# Ensure no cache
cache.delete(CACHE_KEYS["object"])
cache.delete(CACHE_KEYS["post"])
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(
f"/admin/market/item/{self.item.id}/change/", data=data
)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/2/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should not have changed existing item
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertEqual(item.file.name.count("test_file"), 1)
self.assertEqual(item.image.name.count("test_image2"), 0)
self.assertEqual(item.image.name.count("test_image"), 1)
# Should have saved new item
self.assertEqual(Item.objects.count(), 2)
new_item = Item.objects.filter(id=item.id + 1).first()
self.assertIsNotNone(new_item)
self.assertEqual(new_item.name, data["name"])
self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"])
self.assertFalse(new_item.file)
# FAILED to save image
self.assertFalse(new_item.image)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_change_without_changing_file_should_save_changes(self):
item = self.item
# Load the Change Item Page
ItemAdmin.save_as_continue = False
# Request.POST
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"file": "",
"image": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_save": True,
}
# Set cache
cache_item = Item(
name=data["name"],
price=data["price"],
currency=data["currency"],
)
cache.get(CACHE_KEYS["object"], cache_item)
cache.get(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(
f"/admin/market/item/{self.item.id}/change/", data=data
)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/1/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should have changed existing item
self.assertEqual(Item.objects.count(), 1)
item.refresh_from_db()
self.assertEqual(item.name, "name")
# Should have cleared if requested
self.assertFalse(item.file.name)
self.assertEqual(item.image.name.count("test_image2"), 0)
self.assertEqual(item.image.name.count("test_image"), 1)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
@mock.patch("admin_confirm.admin.CACHE_TIMEOUT", 1)
def test_old_cache_should_not_be_used(self):
item = self.item
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Click "Save And Continue"
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"image": i2,
"file": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content,
save_action="_continue",
multipart_form=True,
)
# Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item)
self.assertIsNone(cached_item.id)
self.assertEqual(cached_item.name, data["name"])
self.assertEqual(cached_item.price, data["price"])
self.assertEqual(cached_item.currency, data["currency"])
self.assertFalse(cached_item.file.name)
self.assertEqual(cached_item.image, i2)
# Should not have saved the changes yet
self.assertEqual(Item.objects.count(), 1)
item.refresh_from_db()
self.assertEqual(item.name, "Not name")
self.assertIsNotNone(item.file)
self.assertIsNotNone(item.image)
# Wait for cache to time out
time.sleep(1)
# Check that it did time out
cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNone(cached_item)
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data["image"] = ""
data[CONFIRMATION_RECEIVED] = True
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/{item.id}/change/")
# Should have saved item
self.assertEqual(Item.objects.count(), 1)
saved_item = Item.objects.all().first()
self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"])
self.assertFalse(saved_item.file)
# SHOULD not have saved image since it was in the old cache
self.assertNotIn("test_image2", saved_item.image)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_cache_with_incorrect_model_should_not_be_used(self):
item = self.item
# Load the Change Item Page
ItemAdmin.save_as_continue = False
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"id": item.id,
"name": "name",
"price": 2.0,
"file": "",
"file-clear": "on",
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_change": True,
"_save": True,
}
# Set cache to incorrect model
cache_obj = Shop(name="ShopName")
cache.set(CACHE_KEYS["object"], cache_obj)
cache.set(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure"
del data["_confirm_change"]
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(
f"/admin/market/item/{self.item.id}/change/", data=data
)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
self.assertIn("/admin/market/item/1/change/", message)
self.assertIn(data["name"], message)
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
# Should have changed existing item
self.assertEqual(Item.objects.count(), 1)
item.refresh_from_db()
self.assertEqual(item.name, "name")
# Should have cleared if requested
self.assertFalse(item.file.name)
self.assertEqual(item.image.name.count("test_image2"), 0)
self.assertEqual(item.image.name.count("test_image"), 1)
# Should have cleared cache
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))
def test_form_without_files_should_not_use_cache(self):
cache.delete_many(CACHE_KEYS.values())
shop = ShopFactory()
# Click "Save And Continue"
data = {
"id": shop.id,
"name": "name",
"_confirm_change": True,
"_continue": True,
}
response = self.client.post(f"/admin/market/shop/{shop.id}/change/", data=data)
# Should be shown confirmation page
self._assertSubmitHtml(
rendered_content=response.rendered_content, save_action="_continue"
)
# Should not have set cache since not multipart form
for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key))

View File

@ -1,2 +1,12 @@
from django.urls import reverse
def snake_to_title_case(string: str) -> str:
return " ".join(string.split("_")).title()
def get_admin_change_url(obj):
return reverse(
"admin:%s_%s_change" % (obj._meta.app_label, obj._meta.model_name),
args=(obj.pk,),
)

View File

@ -7,3 +7,5 @@ pytest-django~=4.1.0
readme-renderer~=28.0
twine~=3.3.0
coveralls~=3.0.0
Pillow~=8.1.0 # For ImageField
selenium~=3.141.0

View File

@ -6,7 +6,7 @@ README = open(os.path.join(here, "README.md")).read()
setup(
name="django-admin-confirm",
version="0.2.3.dev5",
version="0.2.3.dev9",
packages=["admin_confirm"],
description="Adds confirmation to Django Admin changes, additions and actions",
long_description_content_type="text/markdown",

View File

@ -1,48 +0,0 @@
from django.contrib import admin
from admin_confirm.admin import AdminConfirmMixin, confirm_action
from .models import Item, Inventory, Shop, ShoppingMall
class ItemAdmin(AdminConfirmMixin, admin.ModelAdmin):
list_display = ("name", "price", "currency")
confirm_change = True
class InventoryAdmin(AdminConfirmMixin, admin.ModelAdmin):
list_display = ("shop", "item", "quantity")
confirm_change = True
confirm_add = True
confirmation_fields = ["quantity"]
class ShopAdmin(AdminConfirmMixin, admin.ModelAdmin):
confirmation_fields = ["name"]
actions = ["show_message", "show_message_no_confirmation"]
@confirm_action
def show_message(modeladmin, request, queryset):
shops = ", ".join(shop.name for shop in queryset)
modeladmin.message_user(request, f"You selected with confirmation: {shops}")
show_message.allowed_permissions = ("delete",)
def show_message_no_confirmation(modeladmin, request, queryset):
shops = ", ".join(shop.name for shop in queryset)
modeladmin.message_user(request, f"You selected without confirmation: {shops}")
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
class ShoppingMallAdmin(AdminConfirmMixin, admin.ModelAdmin):
confirm_add = True
confirm_change = True
confirmation_fields = ["name"]
admin.site.register(Item, ItemAdmin)
admin.site.register(Inventory, InventoryAdmin)
admin.site.register(Shop, ShopAdmin)
admin.site.register(ShoppingMall, ShoppingMallAdmin)

View File

@ -0,0 +1,15 @@
from django.contrib import admin
from ..models import GeneralManager, Item, Inventory, Shop, ShoppingMall
from .item_admin import ItemAdmin
from .inventory_admin import InventoryAdmin
from .shop_admin import ShopAdmin
from .shoppingmall_admin import ShoppingMallAdmin
from .generalmanager_admin import GeneralManagerAdmin
admin.site.register(Item, ItemAdmin)
admin.site.register(Inventory, InventoryAdmin)
admin.site.register(Shop, ShopAdmin)
admin.site.register(ShoppingMall, ShoppingMallAdmin)
admin.site.register(GeneralManager, GeneralManagerAdmin)

View File

@ -0,0 +1,5 @@
from django.contrib.admin import ModelAdmin
class GeneralManagerAdmin(ModelAdmin):
save_as = True

View File

@ -0,0 +1,10 @@
from django.contrib.admin import ModelAdmin
from admin_confirm.admin import AdminConfirmMixin
class InventoryAdmin(AdminConfirmMixin, ModelAdmin):
list_display = ("shop", "item", "quantity")
confirm_change = True
confirm_add = True
confirmation_fields = ["quantity"]

View File

@ -0,0 +1,28 @@
from django.contrib.admin import ModelAdmin
from django.utils.safestring import mark_safe
from admin_confirm.admin import AdminConfirmMixin
class ItemAdmin(AdminConfirmMixin, ModelAdmin):
confirm_change = True
confirm_add = True
confirmation_fields = ["price"]
list_display = ("name", "price", "currency")
readonly_fields = ["image_preview"]
save_as = True
save_as_continue = False
def image_preview(self, obj):
if obj.image:
return mark_safe('<img src="{obj.image.url}" />')
# def one(self, obj):
# return "Read Only"
# def two(self, obj):
# return "Read Only"
# def three(self, obj):
# return "Read Only"

View File

@ -0,0 +1,21 @@
from django.contrib.admin import ModelAdmin
from admin_confirm.admin import AdminConfirmMixin, confirm_action
class ShopAdmin(AdminConfirmMixin, ModelAdmin):
confirmation_fields = ["name"]
actions = ["show_message", "show_message_no_confirmation"]
@confirm_action
def show_message(modeladmin, request, queryset):
shops = ", ".join(shop.name for shop in queryset)
modeladmin.message_user(request, f"You selected with confirmation: {shops}")
show_message.allowed_permissions = ("delete",)
def show_message_no_confirmation(modeladmin, request, queryset):
shops = ", ".join(shop.name for shop in queryset)
modeladmin.message_user(request, f"You selected without confirmation: {shops}")
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser

View File

@ -0,0 +1,16 @@
from ..models import ShoppingMall
from django.contrib.admin import ModelAdmin
from django.contrib.admin.options import StackedInline
from admin_confirm.admin import AdminConfirmMixin
class ShopInline(StackedInline):
model = ShoppingMall.shops.through
class ShoppingMallAdmin(AdminConfirmMixin, ModelAdmin):
confirm_add = True
confirm_change = True
confirmation_fields = ["name"]
inlines = [ShopInline]

View File

@ -0,0 +1,48 @@
# Generated by Django 3.1.6 on 2021-02-22 03:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('market', '0005_shoppingmall'),
]
operations = [
migrations.CreateModel(
name='GeneralManager',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120)),
],
),
migrations.CreateModel(
name='Town',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120)),
],
),
migrations.AddField(
model_name='item',
name='file',
field=models.FileField(blank=True, null=True, upload_to='tmp/files'),
),
migrations.AddField(
model_name='item',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='tmp/items'),
),
migrations.AddField(
model_name='shoppingmall',
name='general_manager',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='market.generalmanager'),
),
migrations.AddField(
model_name='shoppingmall',
name='town',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='market.town'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-02-24 01:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('market', '0006_auto_20210222_0312'),
]
operations = [
migrations.AddField(
model_name='generalmanager',
name='headshot',
field=models.ImageField(blank=True, null=True, upload_to='tmp/gm/headshots'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-02-24 08:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('market', '0007_generalmanager_headshot'),
]
operations = [
migrations.AddField(
model_name='item',
name='description',
field=models.TextField(blank=True, null=True),
),
]

View File

@ -9,6 +9,9 @@ class Item(models.Model):
name = models.CharField(max_length=120)
price = models.DecimalField(max_digits=5, decimal_places=2)
currency = models.CharField(max_length=3, choices=VALID_CURRENCIES)
image = models.ImageField(upload_to="tmp/items", null=True, blank=True)
file = models.FileField(upload_to="tmp/files", null=True, blank=True)
description = models.TextField(null=True, blank=True)
def __str__(self):
return self.name
@ -18,7 +21,7 @@ class Shop(models.Model):
name = models.CharField(max_length=120)
def __str__(self):
return self.name
return str(self.name)
class Inventory(models.Model):
@ -35,9 +38,22 @@ class Inventory(models.Model):
notes = models.TextField(default="This is the default", null=True, blank=True)
class GeneralManager(models.Model):
name = models.CharField(max_length=120)
headshot = models.ImageField(upload_to="tmp/gm/headshots", null=True, blank=True)
class Town(models.Model):
name = models.CharField(max_length=120)
class ShoppingMall(models.Model):
name = models.CharField(max_length=120)
shops = models.ManyToManyField(Shop)
general_manager = models.OneToOneField(
GeneralManager, on_delete=models.CASCADE, null=True, blank=True
)
town = models.ForeignKey(Town, on_delete=models.CASCADE, null=True, blank=True)
def __str__(self):
return self.name