fix(Issue-27): Confirmations not triggered on models with validation failures (#30)

* feat(ISSUE-27): Added some validation integration tests

* feat(ISSUE-27): Integration tests for validations

* feat(ISSUE-27): Unable to select autocomplete with selenium

* feat(ISSUE-27): Fix file caching with proper FileCache

* feat(ISSUE-27): Some minor tweaks

* feat(ISSUE-27): Try reviewdog

* feat(ISSUE-27): Remove noisy linting

* feat(ISSUE-27): Black format

* feat(ISSUE-27): Adding some simple file cache tests

* feat(ISSUE-27): Some cleaning up

Co-authored-by: Thu Trang Pham <thu@joinmodernhealth.com>
main
Thu Trang Pham 2021-04-28 12:03:16 -07:00 committed by GitHub
parent 4d6b2900d8
commit 14dc6268b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1593 additions and 1108 deletions

44
.github/workflows/lint.yml vendored 100644
View File

@ -0,0 +1,44 @@
name: Lint
on:
# Trigger the workflow on push or pull request,
# but only for the main branch
push:
branches:
- main
pull_request:
branches:
- main
release:
types:
- created
jobs:
flake8-lint:
runs-on: ubuntu-latest
name: flake8
steps:
- name: Check out source repository
uses: actions/checkout@v2
- name: Set up Python environment
uses: actions/setup-python@v2
with:
python-version: "3.8"
- name: flake8 Lint
uses: reviewdog/action-flake8@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workdir: admin_confirm
black-lint:
name: black
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: reviewdog/action-black@v2
with:
github_token: ${{ secrets.github_token }}
# Change reviewdog reporter if you need [github-pr-check, github-check].
reporter: github-pr-check
# Change reporter level if you need.
# GitHub Status Check won't become failure with a warning.
level: warning

View File

@ -14,19 +14,6 @@ on:
- created - created
jobs: jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: wemake-python-styleguide
uses: wemake-services/wemake-python-styleguide@0.15.2
with:
path: admin_confirm
reporter: 'github-pr-review'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:

View File

@ -12,6 +12,12 @@ test-all:
t: t:
python -m pytest --last-failed -x python -m pytest --last-failed -x
test-integration:
coverage run --source admin_confirm --branch -m pytest --ignore=admin_confirm/tests/unit
docker-exec:
docker-compose exec -T web ${COMMAND}
check-readme: check-readme:
python -m readme_renderer README.md -o /tmp/README.html python -m readme_renderer README.md -o /tmp/README.html

View File

@ -1,6 +1,7 @@
from typing import Dict from typing import Dict
from django.contrib.admin.exceptions import DisallowedModelAdminToField from django.contrib.admin.exceptions import DisallowedModelAdminToField
from django.contrib.admin.utils import flatten_fieldsets, unquote from django.contrib.admin.utils import flatten_fieldsets, unquote
from django.core.cache import cache
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.contrib.admin.options import TO_FIELD_VAR from django.contrib.admin.options import TO_FIELD_VAR
@ -8,11 +9,14 @@ from django.utils.translation import gettext as _
from django.contrib.admin import helpers from django.contrib.admin import helpers
from django.db.models import Model, ManyToManyField, FileField, ImageField from django.db.models import Model, ManyToManyField, FileField, ImageField
from django.forms import ModelForm from django.forms import ModelForm
from admin_confirm.utils import get_admin_change_url, snake_to_title_case from admin_confirm.utils import (
from django.core.cache import cache log,
get_admin_change_url,
snake_to_title_case,
format_cache_key,
)
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from admin_confirm.constants import ( from admin_confirm.constants import (
CACHE_TIMEOUT,
CONFIRMATION_RECEIVED, CONFIRMATION_RECEIVED,
CONFIRM_ADD, CONFIRM_ADD,
CONFIRM_CHANGE, CONFIRM_CHANGE,
@ -21,7 +25,9 @@ from admin_confirm.constants import (
CACHE_KEYS, CACHE_KEYS,
SAVE_AND_CONTINUE, SAVE_AND_CONTINUE,
SAVE_AS_NEW, SAVE_AS_NEW,
CACHE_TIMEOUT,
) )
from admin_confirm.file_cache import FileCache
class AdminConfirmMixin: class AdminConfirmMixin:
@ -38,6 +44,8 @@ class AdminConfirmMixin:
change_confirmation_template = None change_confirmation_template = None
action_confirmation_template = None action_confirmation_template = None
_file_cache = FileCache()
def get_confirmation_fields(self, request, obj=None): def get_confirmation_fields(self, request, obj=None):
""" """
Hook for specifying confirmation fields Hook for specifying confirmation fields
@ -100,6 +108,8 @@ class AdminConfirmMixin:
if (not object_id and CONFIRM_ADD in request.POST) or ( if (not object_id and CONFIRM_ADD in request.POST) or (
object_id and CONFIRM_CHANGE in request.POST object_id and CONFIRM_CHANGE in request.POST
): ):
log("confirmation is asked for")
self._file_cache.delete_all()
cache.delete_many(CACHE_KEYS.values()) cache.delete_many(CACHE_KEYS.values())
return self._change_confirmation_view( return self._change_confirmation_view(
request, object_id, form_url, extra_context request, object_id, form_url, extra_context
@ -109,14 +119,21 @@ class AdminConfirmMixin:
request, object_id, form_url, extra_context request, object_id, form_url, extra_context
) )
else: else:
self._file_cache.delete_all()
cache.delete_many(CACHE_KEYS.values()) cache.delete_many(CACHE_KEYS.values())
extra_context = { extra_context = self._add_confirmation_options_to_extra_context(extra_context)
return super().changeform_view(request, object_id, form_url, extra_context)
def _add_confirmation_options_to_extra_context(self, extra_context):
log(
f"Adding confirmation to extra_content {self.confirm_add} {self.confirm_change}"
)
return {
**(extra_context or {}), **(extra_context or {}),
"confirm_add": self.confirm_add, "confirm_add": self.confirm_add,
"confirm_change": self.confirm_change, "confirm_change": self.confirm_change,
} }
return super().changeform_view(request, object_id, form_url, extra_context)
def _get_changed_data( def _get_changed_data(
self, form: ModelForm, model: Model, obj: object, add: bool self, form: ModelForm, model: Model, obj: object, add: bool
@ -200,53 +217,58 @@ class AdminConfirmMixin:
- or save the new object and modify the request from `add` to `change` - or save the new object and modify the request from `add` to `change`
and pass the request to Django and pass the request to Django
""" """
log("Confirmation has been received")
def _reconstruct_request_files(): def _reconstruct_request_files():
""" """
Reconstruct the file(s) from the cached object (if any). Reconstruct the file(s) from the file cache (if any).
Returns a dictionary of field name to cached file Returns a dictionary of field name to cached file
""" """
reconstructed_files = {} reconstructed_files = {}
cached_object = cache.get(CACHE_KEYS["object"]) cached_object = cache.get(CACHE_KEYS["object"])
query_dict = cache.get(CACHE_KEYS["post"])
# Reconstruct the files from cached object # Reconstruct the files from cached object
if not cached_object: if not cached_object:
log("Warning: no cached_object")
return 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: if type(cached_object) != self.model:
# Do not use cache if the model doesn't match this model # Do not use cache if the model doesn't match this model
log(f"Warning: cached_object is not of type {self.model}")
return return
query_dict = request.POST
for field in self.model._meta.get_fields(): for field in self.model._meta.get_fields():
if not (isinstance(field, FileField) or isinstance(field, ImageField)): if not (isinstance(field, FileField) or isinstance(field, ImageField)):
continue continue
cached_file = getattr(cached_object, field.name) cached_file = self._file_cache.get(
format_cache_key(model=self.model.__name__, field=field.name)
)
# If a file was uploaded, the field is omitted from the POST since it's in request.FILES # 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: if not query_dict.get(field.name):
if not cached_file:
log(
f"Warning: Could not find file cached for field {field.name}"
)
else:
reconstructed_files[field.name] = cached_file reconstructed_files[field.name] = cached_file
return reconstructed_files return reconstructed_files
reconstructed_files = _reconstruct_request_files() reconstructed_files = _reconstruct_request_files()
if reconstructed_files: if reconstructed_files:
log(f"Found reconstructed files for fields: {reconstructed_files.keys()}")
obj = None obj = None
# remove the _confirm_add and _confirm_change from post # remove the _confirm_add and _confirm_change from post
modified_post = request.POST.copy() 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: if CONFIRM_ADD in modified_post:
del modified_post[CONFIRM_ADD] del modified_post[CONFIRM_ADD] # pragma: no cover
if CONFIRM_CHANGE in modified_post: if CONFIRM_CHANGE in modified_post:
del modified_post[CONFIRM_CHANGE] del modified_post[CONFIRM_CHANGE] # pragma: no cover
if object_id and SAVE_AS_NEW not in request.POST: if object_id and SAVE_AS_NEW not in request.POST:
# Update the obj with the new uploaded files # Update the obj with the new uploaded files
@ -262,6 +284,7 @@ class AdminConfirmMixin:
# No cover: __reconstruct_request_files currently checks for cached obj so obj won't be None # No cover: __reconstruct_request_files currently checks for cached obj so obj won't be None
if obj: # pragma: no cover if obj: # pragma: no cover
for field, file in reconstructed_files.items(): for field, file in reconstructed_files.items():
log(f"Setting file field {field} to file {file}")
setattr(obj, field, file) setattr(obj, field, file)
obj.save() obj.save()
object_id = str(obj.id) object_id = str(obj.id)
@ -283,7 +306,9 @@ class AdminConfirmMixin:
request.POST = modified_post request.POST = modified_post
self._file_cache.delete_all()
cache.delete_many(CACHE_KEYS.values()) cache.delete_many(CACHE_KEYS.values())
return super()._changeform_view(request, object_id, form_url, extra_context) return super()._changeform_view(request, object_id, form_url, extra_context)
def _get_cleared_fields(self, request): def _get_cleared_fields(self, request):
@ -312,6 +337,9 @@ class AdminConfirmMixin:
model = self.model model = self.model
opts = model._meta opts = model._meta
if SAVE_AS_NEW in request.POST:
object_id = None
add = object_id is None add = object_id is None
if add: if add:
if not self.has_add_permission(request): if not self.has_add_permission(request):
@ -331,7 +359,7 @@ class AdminConfirmMixin:
request, obj, change=not add, fields=flatten_fieldsets(fieldsets) request, obj, change=not add, fields=flatten_fieldsets(fieldsets)
) )
form = ModelForm(request.POST, request.FILES, obj) form = ModelForm(request.POST, request.FILES, instance=obj)
form_validated = form.is_valid() form_validated = form.is_valid()
if form_validated: if form_validated:
new_object = self.save_form(request, form, change=not add) new_object = self.save_form(request, form, change=not add)
@ -341,6 +369,16 @@ class AdminConfirmMixin:
request, new_object, change=not add request, new_object, change=not add
) )
# End code from super()._changeform_view # End code from super()._changeform_view
# form.is_valid() checks both errors and "is_bound"
# If form has errors, show the errors on the form instead of showing confirmation page
if not form_validated:
log("Invalid Form: return early")
log(form.errors)
# We must ensure that we ask for confirmation when showing errors
extra_context = self._add_confirmation_options_to_extra_context(
extra_context
)
return super()._changeform_view(request, object_id, form_url, extra_context)
add_or_new = add or SAVE_AS_NEW in request.POST add_or_new = add or SAVE_AS_NEW in request.POST
# Get changed data to show on confirmation # Get changed data to show on confirmation
@ -350,6 +388,7 @@ class AdminConfirmMixin:
self.get_confirmation_fields(request, obj) self.get_confirmation_fields(request, obj)
) & set(changed_data.keys()) ) & set(changed_data.keys())
if not bool(changed_confirmation_fields): if not bool(changed_confirmation_fields):
log("No change detected")
# No confirmation required for changed fields, continue to save # No confirmation required for changed fields, continue to save
return super()._changeform_view(request, object_id, form_url, extra_context) return super()._changeform_view(request, object_id, form_url, extra_context)
@ -363,12 +402,20 @@ class AdminConfirmMixin:
cleared_fields = [] cleared_fields = []
if form.is_multipart(): if form.is_multipart():
cache.set(CACHE_KEYS["post"], request.POST, timeout=CACHE_TIMEOUT) log("Caching files")
cache.set(CACHE_KEYS["object"], new_object, timeout=CACHE_TIMEOUT) cache.set(CACHE_KEYS["object"], new_object, CACHE_TIMEOUT)
# Save files as tempfiles
for field_name in request.FILES:
file = request.FILES[field_name]
self._file_cache.set(
format_cache_key(model=model.__name__, field=field_name), file
)
# Handle when files are cleared - since the `form` object would not hold that info # Handle when files are cleared - since the `form` object would not hold that info
cleared_fields = self._get_cleared_fields(request) cleared_fields = self._get_cleared_fields(request)
log("Render Change Confirmation")
title_action = _("adding") if add_or_new else _("changing") title_action = _("adding") if add_or_new else _("changing")
context = { context = {
**self.admin_site.each_context(request), **self.admin_site.each_context(request),

View File

@ -10,8 +10,14 @@ CONFIRM_ADD = "_confirm_add"
CONFIRM_CHANGE = "_confirm_change" CONFIRM_CHANGE = "_confirm_change"
CONFIRMATION_RECEIVED = "_confirmation_received" CONFIRMATION_RECEIVED = "_confirmation_received"
CACHE_TIMEOUT = getattr(settings, "ADMIN_CONFIRM_CACHE_TIMEOUT", 10) CACHE_TIMEOUT = getattr(settings, "ADMIN_CONFIRM_CACHE_TIMEOUT", 1000)
CACHE_KEYS = { CACHE_KEYS = {
"object": "admin_confirm__confirmation_object", "object": "admin_confirm__confirmation_object",
"post": "admin_confirm__confirmation_request_post", "post": "admin_confirm__confirmation_request_post",
} }
CACHE_KEY_PREFIX = getattr(
settings, "ADMIN_CONFIRM_CACHE_KEY_PREFIX", "admin_confirm__file_cache"
)
DEBUG = getattr(settings, "ADMIN_CONFIRM_DEBUG", False)

View File

@ -0,0 +1,110 @@
""" FileCache - caches files for ModelAdmins with confirmations.
Code modified from: https://github.com/MaistrenkoAnton/filefield-cache/blob/master/filefield_cache/cache.py
Original copy date: April 22, 2021
---
Modified to be compatible with more versions of Django/Python
---
MIT License
Copyright (c) 2020 Maistrenko Anton
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from django.core.files.uploadedfile import InMemoryUploadedFile
try:
from cStringIO import StringIO as BytesIO # noqa: WPS433
except ImportError:
from io import BytesIO # noqa: WPS433, WPS440
from django.core.cache import cache
from admin_confirm.constants import CACHE_TIMEOUT
from admin_confirm.utils import log
class FileCache(object):
"Cache file data and retain the file upon confirmation."
timeout = CACHE_TIMEOUT
def __init__(self):
self.cache = cache
self.cached_keys = []
def set(self, key, upload):
"""
Set file data to cache for 1000s
:param key: cache key
:param upload: file data
"""
try: # noqa: WPS229
state = {
"name": upload.name,
"size": upload.size,
"content_type": upload.content_type,
"charset": upload.charset,
"content": upload.file.read(),
}
upload.file.seek(0)
self.cache.set(key, state, self.timeout)
log(f"Setting file cache with {key}")
self.cached_keys.append(key)
except AttributeError: # pragma: no cover
pass # noqa: WPS420
def get(self, key):
"""
Get the file data from cache using specific cache key
:param key: cache key
:return: File data
"""
upload = None
state = self.cache.get(key)
if state:
file = BytesIO()
file.write(state["content"])
upload = InMemoryUploadedFile(
file=file,
field_name="file",
name=state["name"],
content_type=state["content_type"],
size=state["size"],
charset=state["charset"],
)
upload.file.seek(0)
log(f"Getting file cache with {key}")
return upload
def delete(self, key):
"""
Delete file data from cache
:param key: cache key
"""
self.cache.delete(key)
self.cached_keys.remove(key)
def delete_all(self):
"Delete all cached file data from cache."
self.cache.delete_many(self.cached_keys)
self.cached_keys = []

View File

@ -6,6 +6,8 @@ from django.contrib.auth.models import User
from django.test import LiveServerTestCase from django.test import LiveServerTestCase
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.by import By
class AdminConfirmTestCase(TestCase): class AdminConfirmTestCase(TestCase):
@ -69,6 +71,7 @@ class AdminConfirmTestCase(TestCase):
class AdminConfirmIntegrationTestCase(LiveServerTestCase): class AdminConfirmIntegrationTestCase(LiveServerTestCase):
# Setup/Teardown Methods
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.host = socket.gethostbyname(socket.gethostname()) cls.host = socket.gethostbyname(socket.gethostname())
@ -102,3 +105,22 @@ class AdminConfirmIntegrationTestCase(LiveServerTestCase):
def tearDownClass(cls): def tearDownClass(cls):
cls.selenium.quit() cls.selenium.quit()
super().tearDownClass() super().tearDownClass()
# Helper Methods
def set_value(self, by, by_value, value):
element = self.selenium.find_element(by, by_value)
element.clear()
element.send_keys(value)
def select_value(self, by, by_value, value):
element = Select(self.selenium.find_element(by, by_value))
element.select_by_value(value)
def select_index(self, by, by_value, index):
element = Select(self.selenium.find_element(by, by_value))
element.select_by_index(index)
def print_hidden_form(self):
hidden_form = self.selenium.find_element(By.ID, "hidden-form")
print(hidden_form.get_attribute("innerHTML"))

View File

@ -68,6 +68,7 @@ class ConfirmWithInlinesTests(AdminConfirmIntegrationTestCase):
# Change price # Change price
price = self.selenium.find_element(By.NAME, "price") price = self.selenium.find_element(By.NAME, "price")
price.send_keys(2) price.send_keys(2)
self.selenium.find_element(By.ID, "id_currency_0").click()
self.selenium.find_element(By.NAME, "_continue").click() self.selenium.find_element(By.NAME, "_continue").click()
@ -120,7 +121,7 @@ class ConfirmWithInlinesTests(AdminConfirmIntegrationTestCase):
item.refresh_from_db() item.refresh_from_db()
self.assertEqual(21, int(item.price)) self.assertEqual(21, int(item.price))
self.assertIn("screenshot.png", item.file.name) self.assertRegex(item.file.name, r"screenshot.*\.png$")
def test_should_save_file_changes(self): def test_should_save_file_changes(self):
selenium_version = pkg_resources.get_distribution("selenium").parsed_version selenium_version = pkg_resources.get_distribution("selenium").parsed_version
@ -165,7 +166,7 @@ class ConfirmWithInlinesTests(AdminConfirmIntegrationTestCase):
item.refresh_from_db() item.refresh_from_db()
self.assertEqual(21, int(item.price)) self.assertEqual(21, int(item.price))
self.assertIn("screenshot.png", item.file.name) self.assertRegex(item.file.name, r"screenshot.*\.png$")
def test_should_remove_file_if_clear_selected(self): def test_should_remove_file_if_clear_selected(self):
file = SimpleUploadedFile( file = SimpleUploadedFile(

View File

@ -69,14 +69,14 @@ class ConfirmWithInlinesTests(AdminConfirmIntegrationTestCase):
# Make a change to trigger confirmation page # Make a change to trigger confirmation page
name = self.selenium.find_element(By.NAME, "name") name = self.selenium.find_element(By.NAME, "name")
name.send_keys("New Name") name.send_keys("This is New Name")
self.selenium.find_element(By.NAME, "_continue").click() self.selenium.find_element(By.NAME, "_continue").click()
self.assertIn("Confirm", self.selenium.page_source) self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element(By.ID, "hidden-form") hidden_form = self.selenium.find_element(By.ID, "hidden-form")
hidden_form.find_element(By.NAME, "ShoppingMall_shops-TOTAL_FORMS") hidden_form.find_element(By.NAME, "ShoppingMall_shops-TOTAL_FORMS")
self.selenium.find_element(By.NAME, "_continue").click() self.selenium.find_element(By.NAME, "_continue").click()
mall.refresh_from_db() mall.refresh_from_db()

View File

@ -0,0 +1,202 @@
"""
Ensures that confirmations work with validators on the Model and on the Modelform.
"""
from logging import currentframe
from unittest import mock
from django.urls import reverse
from django.utils import timezone
from admin_confirm.tests.helpers import AdminConfirmTestCase
from tests.market.models import Checkout, ItemSale
from tests.factories import (
InventoryFactory,
ItemFactory,
ShopFactory,
TransactionFactory,
)
import pytest
import pkg_resources
from importlib import reload
from tests.factories import ShopFactory
from tests.market.models import GeneralManager, ShoppingMall, Town
from admin_confirm.tests.helpers import AdminConfirmIntegrationTestCase
from tests.market.admin import shoppingmall_admin
from admin_confirm.constants import CONFIRM_ADD, CONFIRM_CHANGE
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.by import By
class ConfirmWithValidatorsTests(AdminConfirmIntegrationTestCase):
def setUp(self):
self.admin = shoppingmall_admin.ShoppingMallAdmin
self.admin.inlines = [shoppingmall_admin.ShopInline]
super().setUp()
def tearDown(self):
reload(shoppingmall_admin)
super().tearDown()
@mock.patch("tests.market.models.ItemSale.clean")
def test_can_confirm_for_models_with_validator_on_model_field(self, _mock_clean):
# ItemSale.currency has a validator on it
ItemFactory()
TransactionFactory()
self.selenium.get(self.live_server_url + f"/admin/market/itemsale/add/")
# Should ask for confirmation of change
self.assertIn(CONFIRM_ADD, self.selenium.page_source)
self.set_value(by=By.NAME, by_value="quantity", value="1")
self.set_value(by=By.NAME, by_value="total", value="10.00")
self.set_value(by=By.NAME, by_value="currency", value="USD")
self.select_index(by=By.NAME, by_value="transaction", index=1)
self.select_index(by=By.NAME, by_value="item", index=1)
self.selenium.find_element(By.NAME, "_continue").click()
# Should have hidden form containing the updated name
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element(By.ID, "hidden-form")
currency = hidden_form.find_element(By.NAME, "currency")
self.assertIn("USD", currency.get_attribute("value"))
# Should not have been added yet
self.assertEqual(ItemSale.objects.count(), 0)
self.selenium.find_element(By.NAME, "_continue").click()
# Should persist change
self.assertEqual(ItemSale.objects.count(), 1)
def test_cannot_confirm_for_models_with_validator_on_model_field_if_validator_fails(
self,
):
# ItemSale.currency has a validator on it
shop = ShopFactory()
item = ItemFactory()
InventoryFactory(shop=shop, item=item, quantity=10)
TransactionFactory(shop=shop)
self.selenium.get(self.live_server_url + f"/admin/market/itemsale/add/")
# Should ask for confirmation of change
self.assertIn(CONFIRM_ADD, self.selenium.page_source)
self.set_value(by=By.NAME, by_value="quantity", value="1")
self.set_value(by=By.NAME, by_value="total", value="10.00")
self.set_value(by=By.NAME, by_value="currency", value="FAKE")
self.select_index(by=By.NAME, by_value="transaction", index=1)
self.select_index(by=By.NAME, by_value="item", index=1)
self.selenium.find_element(By.NAME, "_continue").click()
# Should show errors and not confirmation page
self.assertNotIn("Confirm", self.selenium.page_source)
self.assertIn("Invalid Currency", self.selenium.page_source)
self.assertIn(CONFIRM_ADD, self.selenium.page_source)
# Should not have been added yet
self.assertEqual(ItemSale.objects.count(), 0)
# Now fix the issue
self.set_value(by=By.NAME, by_value="currency", value="USD")
self.selenium.find_element(By.NAME, "_continue").click()
# Should have hidden form containing the updated currency
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element(By.ID, "hidden-form")
currency = hidden_form.find_element(By.NAME, "currency")
self.assertIn("USD", currency.get_attribute("value"))
# Should not have been added yet
self.assertEqual(ItemSale.objects.count(), 0)
self.selenium.find_element(By.NAME, "_continue").click()
# Should persist change
self.assertEqual(ItemSale.objects.count(), 1)
def test_can_confirm_for_models_with_clean_overridden(self):
shop = ShopFactory()
item = ItemFactory()
InventoryFactory(shop=shop, item=item, quantity=10)
transaction = TransactionFactory(shop=shop)
self.selenium.get(self.live_server_url + f"/admin/market/itemsale/add/")
# Should ask for confirmation of change
self.assertIn(CONFIRM_ADD, self.selenium.page_source)
self.set_value(by=By.NAME, by_value="quantity", value="9")
self.set_value(by=By.NAME, by_value="total", value="10.00")
self.set_value(by=By.NAME, by_value="currency", value="USD")
self.select_index(by=By.NAME, by_value="transaction", index=1)
self.select_index(by=By.NAME, by_value="item", index=1)
self.selenium.find_element(By.NAME, "_continue").click()
# Should not have been added yet
self.assertEqual(ItemSale.objects.count(), 0)
# Should have hidden form containing the updated currency
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element(By.ID, "hidden-form")
currency = hidden_form.find_element(By.NAME, "currency")
self.assertIn("USD", currency.get_attribute("value"))
# Confirm the change
self.selenium.find_element(By.NAME, "_continue").click()
# Should persist change
self.assertEqual(ItemSale.objects.count(), 1)
# Ensure that the date and timestamp saved correctly
item_sale = ItemSale.objects.first()
self.assertEqual(item_sale.transaction, transaction)
self.assertEqual(item_sale.item, item)
self.assertEqual(item_sale.currency, "USD")
def test_cannot_confirm_for_models_with_clean_overridden_if_clean_fails(self):
shop = ShopFactory()
item = ItemFactory()
InventoryFactory(shop=shop, item=item, quantity=1)
TransactionFactory(shop=shop)
self.selenium.get(self.live_server_url + f"/admin/market/itemsale/add/")
# Should ask for confirmation of change
self.assertIn(CONFIRM_ADD, self.selenium.page_source)
self.set_value(by=By.NAME, by_value="quantity", value="9")
self.set_value(by=By.NAME, by_value="total", value="10.00")
self.set_value(by=By.NAME, by_value="currency", value="USD")
self.select_index(by=By.NAME, by_value="transaction", index=1)
self.select_index(by=By.NAME, by_value="item", index=1)
self.selenium.find_element(By.NAME, "_continue").click()
# Should show errors and not confirmation page
self.assertNotIn("Confirm", self.selenium.page_source)
self.assertIn(
"Shop does not have enough of the item stocked", self.selenium.page_source
)
self.assertIn(CONFIRM_ADD, self.selenium.page_source)
# Should not have been added yet
self.assertEqual(ItemSale.objects.count(), 0)
# Now fix the issue
self.set_value(by=By.NAME, by_value="quantity", value="1")
self.selenium.find_element(By.NAME, "_continue").click()
# Should have hidden form containing the updated currency
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element(By.ID, "hidden-form")
quantity = hidden_form.find_element(By.NAME, "quantity")
self.assertIn("1", str(quantity.get_attribute("value")))
# Confirm change
self.selenium.find_element(By.NAME, "_continue").click()
# Should persist change
self.assertEqual(ItemSale.objects.count(), 1)

View File

@ -102,10 +102,6 @@ class TestConfirmSaveActions(AdminConfirmTestCase):
# Should have cached the unsaved item # Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"]) cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item) 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 # Should not have saved the changes yet
self.assertEqual(Item.objects.count(), 1) self.assertEqual(Item.objects.count(), 1)
@ -171,16 +167,6 @@ class TestConfirmSaveActions(AdminConfirmTestCase):
multipart_form=True, 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 # Should not have saved the item yet
self.assertEqual(Item.objects.count(), 0) self.assertEqual(Item.objects.count(), 0)
@ -205,13 +191,14 @@ class TestConfirmSaveActions(AdminConfirmTestCase):
self.assertEqual(saved_item.name, data["name"]) self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"]) self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"]) self.assertEqual(saved_item.currency, data["currency"])
self.assertEqual(saved_item.file, data["file"]) self.assertIsNotNone(saved_item.file)
self.assertEqual(saved_item.image, data["image"]) self.assertIsNotNone(saved_item.image)
self.assertEqual(saved_item.file.name, "test_file.jpg") self.assertRegex(saved_item.file.name, r"test_file.*\.jpg$")
self.assertEqual(saved_item.image.name, "test_image.jpg") self.assertRegex(saved_item.image.name, r"test_image.*\.jpg$")
# Should have cleared cache # Should have cleared cache
self.assertEqual(len(ItemAdmin._file_cache.cached_keys), 0)
for key in CACHE_KEYS.values(): for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key)) self.assertIsNone(cache.get(key))
@ -271,16 +258,6 @@ class TestConfirmSaveActions(AdminConfirmTestCase):
multipart_form=True, 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 # Should not have saved the changes yet
self.assertEqual(Item.objects.count(), 1) self.assertEqual(Item.objects.count(), 1)
item.refresh_from_db() item.refresh_from_db()
@ -312,11 +289,11 @@ class TestConfirmSaveActions(AdminConfirmTestCase):
self.assertEqual(new_item.price, data["price"]) self.assertEqual(new_item.price, data["price"])
self.assertEqual(new_item.currency, data["currency"]) self.assertEqual(new_item.currency, data["currency"])
self.assertFalse(new_item.file) self.assertFalse(new_item.file)
self.assertEqual(new_item.image, i2) self.assertIsNotNone(new_item.image)
self.assertRegex(new_item.image.name, r"test_image2.*\.jpg$")
# Should have cleared cache # Should have cleared cache
for key in CACHE_KEYS.values(): self.assertEqual(len(ItemAdmin._file_cache.cached_keys), 0)
self.assertIsNone(cache.get(key))
def test_relations_add(self): def test_relations_add(self):
gm = GeneralManager.objects.create(name="gm") gm = GeneralManager.objects.create(name="gm")
@ -379,6 +356,7 @@ class TestConfirmSaveActions(AdminConfirmTestCase):
self.assertIn(shop, shops) self.assertIn(shop, shops)
# Should have cleared cache # Should have cleared cache
self.assertEqual(len(ItemAdmin._file_cache.cached_keys), 0)
for key in CACHE_KEYS.values(): for key in CACHE_KEYS.values():
self.assertIsNone(cache.get(key)) self.assertIsNone(cache.get(key))
@ -473,5 +451,4 @@ class TestConfirmSaveActions(AdminConfirmTestCase):
self.assertIn(shop, shops2) self.assertIn(shop, shops2)
# Should have cleared cache # Should have cleared cache
for key in CACHE_KEYS.values(): self.assertEqual(len(ItemAdmin._file_cache.cached_keys), 0)
self.assertIsNone(cache.get(key))

View File

@ -102,10 +102,6 @@ class TestConfirmationCache(AdminConfirmTestCase):
# Should have cached the unsaved item # Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"]) cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item) 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 # Should not have saved the changes yet
self.assertEqual(Item.objects.count(), 1) self.assertEqual(Item.objects.count(), 1)
@ -204,11 +200,11 @@ class TestConfirmationCache(AdminConfirmTestCase):
self.assertEqual(saved_item.name, data["name"]) self.assertEqual(saved_item.name, data["name"])
self.assertEqual(saved_item.price, data["price"]) self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"]) self.assertEqual(saved_item.currency, data["currency"])
self.assertEqual(saved_item.file, data["file"]) self.assertIsNotNone(saved_item.file)
self.assertEqual(saved_item.image, data["image"]) self.assertIsNotNone(saved_item.image)
self.assertEqual(saved_item.file.name, "test_file.jpg") self.assertRegex(saved_item.file.name, r"test_file.*\.jpg$")
self.assertEqual(saved_item.image.name, "test_image.jpg") self.assertRegex(saved_item.image.name, r"test_image.*\.jpg$")
# Should have cleared cache # Should have cleared cache
for key in CACHE_KEYS.values(): for key in CACHE_KEYS.values():
@ -271,12 +267,6 @@ class TestConfirmationCache(AdminConfirmTestCase):
# Should have cached the unsaved item # Should have cached the unsaved item
cached_item = cache.get(CACHE_KEYS["object"]) cached_item = cache.get(CACHE_KEYS["object"])
self.assertIsNotNone(cached_item) 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 # Should not have saved the changes yet
self.assertEqual(Item.objects.count(), 1) self.assertEqual(Item.objects.count(), 1)
@ -301,7 +291,9 @@ class TestConfirmationCache(AdminConfirmTestCase):
self.assertEqual(saved_item.price, data["price"]) self.assertEqual(saved_item.price, data["price"])
self.assertEqual(saved_item.currency, data["currency"]) self.assertEqual(saved_item.currency, data["currency"])
self.assertFalse(saved_item.file) self.assertFalse(saved_item.file)
self.assertEqual(saved_item.image, i2) self.assertIsNotNone(saved_item.image)
self.assertRegex(saved_item.image.name, r"test_image2.*\.jpg$")
# Should have cleared cache # Should have cleared cache
for key in CACHE_KEYS.values(): for key in CACHE_KEYS.values():

View File

@ -0,0 +1,920 @@
"""
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
"""
from admin_confirm.utils import format_cache_key
from admin_confirm.file_cache import FileCache
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 TestConfirmationUsingFileCache(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,
)
file_cache = FileCache()
file_cache.set(format_cache_key(model="Item", field="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,
)
file_cache = FileCache()
file_cache.set(format_cache_key(model="Item", field="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, "/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):
# Upload new file
f2 = SimpleUploadedFile(
name="test_file2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"name": "name",
"price": 2.0,
"image": "",
"file": f2,
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_add": True,
"_save": True,
}
# 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("/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, "/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.assertIsNotNone(new_item.file)
self.assertFalse(new_item.image)
self.assertRegex(new_item.file.name, r"test_file2.*\.jpg$")
# 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):
# Upload new file
f2 = SimpleUploadedFile(
name="test_file2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"name": "name",
"price": 2.0,
"image": "",
"file": f2,
"currency": Item.VALID_CURRENCIES[0][0],
"_confirm_add": True,
"_save": True,
}
# 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("/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, "/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.assertRegex(new_item.file.name, r"test_file2.*\.jpg$")
# 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,
}
# 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("/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, "/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,
}
# 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("/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, "/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,
)
file_cache = FileCache()
file_cache.set(format_cache_key(model="Item", field="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, "/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)
self.assertIsNotNone(new_item.image)
# 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
# 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,
}
# 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, "/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
# 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,
}
# 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, "/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, "/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)
# 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
# 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, "/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,911 +1,33 @@
"""
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.files.uploadedfile import SimpleUploadedFile
from django.core.cache import cache from admin_confirm.file_cache import FileCache
from admin_confirm.tests.helpers import AdminConfirmTestCase file = SimpleUploadedFile(
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", name="test_file.jpg",
content=open(self.image_path, "rb").read(), content=open("screenshot.png", "rb").read(),
content_type="image/jpeg", 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): def test_should_set_file_cache():
item = self.item file_cache = FileCache()
# Load the Change Item Page file_cache.set("key", file)
ItemAdmin.save_as_continue = True assert "key" in file_cache.cached_keys
assert file_cache.get("key") is not None
# 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 def test_should_delete_file_cache():
cache_item = Item( file_cache = FileCache()
name=data["name"], file_cache.set("key", file)
price=data["price"], file_cache.delete("key")
currency=data["currency"], assert "key" not in file_cache.cached_keys
image=i2, assert file_cache.get("key") is None
)
cache.set(CACHE_KEYS["object"], cache_item)
cache.set(CACHE_KEYS["post"], data)
# Click "Yes, I'm Sure" def test_should_delete_all_file_cache():
del data["_confirm_change"] file_cache = FileCache()
data[CONFIRMATION_RECEIVED] = True file_cache.set("key", file)
file_cache.set("key2", file)
with mock.patch.object(ItemAdmin, "message_user") as message_user: file_cache.delete_all()
response = self.client.post( assert len(file_cache.cached_keys) == 0
f"/admin/market/item/{self.item.id}/change/", data=data assert file_cache.get("key") is None
) assert file_cache.get("key2") is None
# 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, "/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("/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, "/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("/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, "/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,
}
# 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("/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, "/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,
}
# 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("/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, "/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, "/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
# 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,
}
# 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, "/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
# 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,
}
# 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, "/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, "/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
# 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, "/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,7 +1,11 @@
""" """
Ensures that confirmations work with validators on the Model and on the Modelform. Ensures that confirmations work with validators on the Model and on the Modelform.
NOTE: These unit tests are not enough to ensure that confirmations work with validators.
Please ensure to add integration tests too!
""" """
from admin_confirm.constants import CONFIRM_ADD
from unittest import mock from unittest import mock
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -80,27 +84,8 @@ class TestWithValidators(AdminConfirmTestCase):
"_save": True, "_save": True,
} }
response = self.client.post(reverse("admin:market_itemsale_add"), data) response = self.client.post(reverse("admin:market_itemsale_add"), data)
# Should not have been added yet
self.assertEqual(ItemSale.objects.count(), 0)
# Ensure not redirected (confirmation page does not redirect) # Should show form with error and not confirmation page
self.assertEqual(response.status_code, 200)
expected_templates = [
"admin/market/itemsale/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)
# Confirmation page would not have the _confirm_add sent on submit
del data["_confirm_add"]
# Selecting to "Yes, I'm sure" on the confirmation page
# Would post to the same endpoint
response = self.client.post(reverse("admin:market_itemsale_add"), data)
# Should not have redirected, since there was an error
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected_templates = [ expected_templates = [
"admin/market/itemsale/change_form.html", "admin/market/itemsale/change_form.html",
@ -109,8 +94,10 @@ class TestWithValidators(AdminConfirmTestCase):
] ]
self.assertEqual(response.template_name, expected_templates) self.assertEqual(response.template_name, expected_templates)
self.assertEqual(ItemSale.objects.count(), 0) self.assertEqual(ItemSale.objects.count(), 0)
self.assertTrue("error" in str(response.content)) self.assertIn("error", str(response.rendered_content))
self.assertTrue("Invalid Currency" in str(response.content)) self.assertIn("Invalid Currency", str(response.rendered_content))
# Should still ask for confirmation
self.assertIn(CONFIRM_ADD, response.rendered_content)
def test_can_confirm_for_models_with_clean_overridden(self): def test_can_confirm_for_models_with_clean_overridden(self):
shop = ShopFactory() shop = ShopFactory()
@ -162,6 +149,7 @@ class TestWithValidators(AdminConfirmTestCase):
item = ItemFactory() item = ItemFactory()
InventoryFactory(shop=shop, item=item, quantity=1) InventoryFactory(shop=shop, item=item, quantity=1)
transaction = TransactionFactory(shop=shop) transaction = TransactionFactory(shop=shop)
# Asking to buy more than the shop has in stock
data = { data = {
"transaction": transaction.id, "transaction": transaction.id,
"item": item.id, "item": item.id,
@ -176,6 +164,28 @@ class TestWithValidators(AdminConfirmTestCase):
# Should not have been added yet # Should not have been added yet
self.assertEqual(ItemSale.objects.count(), 0) self.assertEqual(ItemSale.objects.count(), 0)
# Ensure it shows the form and not the confirmation page
self.assertEqual(response.status_code, 200)
expected_templates = [
"admin/market/itemsale/change_form.html",
"admin/market/change_form.html",
"admin/change_form.html",
]
self.assertEqual(response.template_name, expected_templates)
self.assertTrue("error" in str(response.content))
self.assertTrue(
"Shop does not have enough of the item stocked" in str(response.content)
)
# Should still be asking for confirmation
self.assertIn(CONFIRM_ADD, response.rendered_content)
# Fix the issue by buying only what shop has in stock
data["quantity"] = 1
# _confirm_add would still be in the POST data
response = self.client.post(reverse("admin:market_itemsale_add"), data)
# Should show confirmation page
# Ensure not redirected (confirmation page does not redirect) # Ensure not redirected (confirmation page does not redirect)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected_templates = [ expected_templates = [
@ -187,26 +197,6 @@ class TestWithValidators(AdminConfirmTestCase):
self._assertSubmitHtml(rendered_content=response.rendered_content) self._assertSubmitHtml(rendered_content=response.rendered_content)
# Confirmation page would not have the _confirm_add sent on submit
del data["_confirm_add"]
# Selecting to "Yes, I'm sure" on the confirmation page
# Would post to the same endpoint
response = self.client.post(reverse("admin:market_itemsale_add"), data)
# Should not have redirected, since there was an error
self.assertEqual(response.status_code, 200)
expected_templates = [
"admin/market/itemsale/change_form.html",
"admin/market/change_form.html",
"admin/change_form.html",
]
self.assertEqual(response.template_name, expected_templates)
self.assertEqual(ItemSale.objects.count(), 0)
self.assertTrue("error" in str(response.content))
self.assertTrue(
"Shop does not have enough of the item stocked" in str(response.content)
)
def test_can_confirm_for_modelform_with_clean_field_and_clean_overridden(self): def test_can_confirm_for_modelform_with_clean_field_and_clean_overridden(self):
shop = ShopFactory() shop = ShopFactory()
data = { data = {
@ -270,27 +260,19 @@ class TestWithValidators(AdminConfirmTestCase):
# Should not have been added yet # Should not have been added yet
self.assertEqual(Checkout.objects.count(), 0) self.assertEqual(Checkout.objects.count(), 0)
# Ensure not redirected (confirmation page does not redirect) # Should show form with error and not confirmation page
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected_templates = [ expected_templates = [
"admin/market/checkout/change_confirmation.html", "admin/market/checkout/change_form.html",
"admin/market/change_confirmation.html", "admin/market/change_form.html",
"admin/change_confirmation.html", "admin/change_form.html",
] ]
self.assertEqual(response.template_name, expected_templates) self.assertEqual(response.template_name, expected_templates)
self._assertSubmitHtml(rendered_content=response.rendered_content)
# Confirmation page would not have the _confirm_add sent on submit
del data["_confirm_add"]
# Selecting to "Yes, I'm sure" on the confirmation page
# Would post to the same endpoint
response = self.client.post(reverse("admin:market_checkout_add"), data)
print(response.content)
self.assertEqual(response.status_code, 200)
self.assertEqual(Checkout.objects.count(), 0) self.assertEqual(Checkout.objects.count(), 0)
self.assertIn("error", str(response.content)) self.assertIn("error", str(response.rendered_content))
self.assertIn("Invalid Total 111", str(response.content)) self.assertIn("Invalid Total 111", str(response.rendered_content))
# Should still ask for confirmation
self.assertIn(CONFIRM_ADD, response.rendered_content)
def test_cannot_confirm_for_modelform_with_clean_overridden_if_validation_fails( def test_cannot_confirm_for_modelform_with_clean_overridden_if_validation_fails(
self, self,
@ -311,24 +293,19 @@ class TestWithValidators(AdminConfirmTestCase):
# Should not have been added yet # Should not have been added yet
self.assertEqual(Checkout.objects.count(), 0) self.assertEqual(Checkout.objects.count(), 0)
# Ensure not redirected (confirmation page does not redirect) # Should not have been added yet
self.assertEqual(Checkout.objects.count(), 0)
# Should show form with error and not confirmation page
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
expected_templates = [ expected_templates = [
"admin/market/checkout/change_confirmation.html", "admin/market/checkout/change_form.html",
"admin/market/change_confirmation.html", "admin/market/change_form.html",
"admin/change_confirmation.html", "admin/change_form.html",
] ]
self.assertEqual(response.template_name, expected_templates) self.assertEqual(response.template_name, expected_templates)
self._assertSubmitHtml(rendered_content=response.rendered_content)
# Confirmation page would not have the _confirm_add sent on submit
del data["_confirm_add"]
# Selecting to "Yes, I'm sure" on the confirmation page
# Would post to the same endpoint
response = self.client.post(reverse("admin:market_checkout_add"), data)
print(response.content)
self.assertEqual(response.status_code, 200)
self.assertEqual(Checkout.objects.count(), 0) self.assertEqual(Checkout.objects.count(), 0)
self.assertIn("error", str(response.content)) self.assertIn("error", str(response.rendered_content))
self.assertIn("Invalid Total 222", str(response.content)) self.assertIn("Invalid Total 222", str(response.rendered_content))
# Should still ask for confirmation
self.assertIn(CONFIRM_ADD, response.rendered_content)

View File

@ -1,12 +1,27 @@
from django.urls import reverse from django.urls import reverse
from admin_confirm.constants import CACHE_KEY_PREFIX, DEBUG
def snake_to_title_case(string: str) -> str: def snake_to_title_case(string: str) -> str:
return " ".join(string.split("_")).title() return " ".join(string.split("_")).title()
def get_admin_change_url(obj): def get_admin_change_url(obj: object) -> str:
return reverse( return reverse(
"admin:%s_%s_change" % (obj._meta.app_label, obj._meta.model_name), "admin:%s_%s_change" % (obj._meta.app_label, obj._meta.model_name),
args=(obj.pk,), args=(obj.pk,),
) )
def format_cache_key(model: str, field: str) -> str:
return f"{CACHE_KEY_PREFIX}__{model}__{field}"
def log(message: str): # pragma: no cover
if DEBUG:
print(message)
def inspect(obj: object): # pragma: no cover
if DEBUG:
print(f"{str(obj): type(obj) - dir(obj)}")

View File

@ -6,6 +6,8 @@ exclude =
admin_confirm/tests/* admin_confirm/tests/*
tests/* tests/*
ignore = ignore =
D107 # Missing docstring in init
D400 # Doc-string: First line should end with a period
C812 # missing trailing comma C812 # missing trailing comma
I001 # isort found an import in the wrong position I001 # isort found an import in the wrong position
I004 # sisort found an unexpected blank line in imports I004 # sisort found an unexpected blank line in imports
@ -22,5 +24,5 @@ branch = True
[tool:pytest] [tool:pytest]
DJANGO_SETTINGS_MODULE=tests.test_project.settings.test DJANGO_SETTINGS_MODULE=tests.test_project.settings.test
addopts = --doctest-modules -ra -l --tb=short --show-capture=stdout --color=yes addopts = --doctest-modules -ra -l --tb=short --show-capture=all --color=yes
testpaths = admin_confirm testpaths = admin_confirm

View File

@ -8,6 +8,9 @@ from ..models import Checkout
class CheckoutForm(ModelForm): class CheckoutForm(ModelForm):
search_fields = ["shop", "date"]
confirm_change = True
class Meta: class Meta:
model = Checkout model = Checkout
fields = [ fields = [

View File

@ -5,6 +5,7 @@ from admin_confirm.admin import AdminConfirmMixin, confirm_action
class ShopAdmin(AdminConfirmMixin, ModelAdmin): class ShopAdmin(AdminConfirmMixin, ModelAdmin):
confirmation_fields = ["name"] confirmation_fields = ["name"]
actions = ["show_message", "show_message_no_confirmation"] actions = ["show_message", "show_message_no_confirmation"]
search_fields = ["name"]
@confirm_action @confirm_action
def show_message(modeladmin, request, queryset): def show_message(modeladmin, request, queryset):

View File

@ -7,39 +7,89 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('market', '0009_auto_20210304_0355'), ("market", "0009_auto_20210304_0355"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Transaction', name="Transaction",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('timestamp', models.DateField(auto_created=True)), "id",
('total', models.DecimalField(decimal_places=2, editable=False, max_digits=5)), models.AutoField(
('currency', models.CharField(choices=[('CAD', 'CAD'), ('USD', 'USD')], max_length=3)), auto_created=True,
('shop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='market.shop')), primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("timestamp", models.DateField(auto_created=True)),
(
"total",
models.DecimalField(decimal_places=2, editable=False, max_digits=5),
),
(
"currency",
models.CharField(
choices=[("CAD", "CAD"), ("USD", "USD")], max_length=3
),
),
(
"shop",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="market.shop"
),
),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='ItemSale', name="ItemSale",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('total', models.DecimalField(decimal_places=2, editable=False, max_digits=5)), "id",
('currency', models.CharField(choices=[('CAD', 'CAD'), ('USD', 'USD')], max_length=3)), models.AutoField(
('item', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='market.item')), auto_created=True,
('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_sales', to='market.transaction')), primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"total",
models.DecimalField(decimal_places=2, editable=False, max_digits=5),
),
(
"currency",
models.CharField(
choices=[("CAD", "CAD"), ("USD", "USD")], max_length=3
),
),
(
"item",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="market.item",
),
),
(
"transaction",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="item_sales",
to="market.transaction",
),
),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Checkout', name="Checkout",
fields=[ fields=[],
],
options={ options={
'proxy': True, "proxy": True,
'indexes': [], "indexes": [],
'constraints': [], "constraints": [],
}, },
bases=('market.transaction',), bases=("market.transaction",),
), ),
] ]

View File

@ -6,18 +6,18 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('market', '0011_auto_20210326_0130'), ("market", "0011_auto_20210326_0130"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='transaction', model_name="transaction",
name='date', name="date",
field=models.DateField(), field=models.DateField(),
), ),
migrations.AlterField( migrations.AlterField(
model_name='transaction', model_name="transaction",
name='timestamp', name="timestamp",
field=models.DateTimeField(auto_created=True), field=models.DateTimeField(auto_created=True),
), ),
] ]

View File

@ -52,7 +52,7 @@ class Town(models.Model):
class ShoppingMall(models.Model): class ShoppingMall(models.Model):
name = models.CharField(max_length=120) name = models.CharField(max_length=120)
shops = models.ManyToManyField(Shop, blank=True, null=True) shops = models.ManyToManyField(Shop, blank=True)
general_manager = models.OneToOneField( general_manager = models.OneToOneField(
GeneralManager, on_delete=models.CASCADE, null=True, blank=True GeneralManager, on_delete=models.CASCADE, null=True, blank=True
) )

View File

@ -23,11 +23,12 @@ SECRET_KEY = "=yddl-40388w3e2hl$e8)revce=n67_idi8pfejtn3!+2%!_qt"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ADMIN_CONFIRM_DEBUG = True
USE_DCOKER = os.environ.get("USE_DOCKER", "").lower() == "true" USE_DOCKER = os.environ.get("USE_DOCKER", "").lower() == "true"
ALLOWED_HOSTS = ["127.0.0.1", "localhost"] ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
if USE_DCOKER: if USE_DOCKER:
import socket import socket
ALLOWED_HOSTS += [socket.gethostbyname(socket.gethostname())] ALLOWED_HOSTS += [socket.gethostbyname(socket.gethostname())]