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
parent
4d6b2900d8
commit
14dc6268b0
|
|
@ -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
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
6
Makefile
6
Makefile
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
reconstructed_files[field.name] = cached_file
|
if not cached_file:
|
||||||
|
log(
|
||||||
|
f"Warning: Could not find file cached for field {field.name}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
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),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 = []
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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))
|
|
||||||
|
|
|
||||||
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
@ -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
|
name="test_file.jpg",
|
||||||
|
content=open("screenshot.png", "rb").read(),
|
||||||
from tests.market.admin import ItemAdmin
|
content_type="image/jpeg",
|
||||||
from tests.market.models import Item, Shop
|
)
|
||||||
from tests.factories import ItemFactory, ShopFactory
|
|
||||||
|
|
||||||
|
def test_should_set_file_cache():
|
||||||
class TestFileCache(AdminConfirmTestCase):
|
file_cache = FileCache()
|
||||||
def setUp(self):
|
file_cache.set("key", file)
|
||||||
# Load the Change Item Page
|
assert "key" in file_cache.cached_keys
|
||||||
ItemAdmin.confirm_change = True
|
assert file_cache.get("key") is not None
|
||||||
ItemAdmin.fields = ["name", "price", "file", "image", "currency"]
|
|
||||||
ItemAdmin.save_as = True
|
|
||||||
ItemAdmin.save_as_continue = True
|
def test_should_delete_file_cache():
|
||||||
|
file_cache = FileCache()
|
||||||
self.image_path = "screenshot.png"
|
file_cache.set("key", file)
|
||||||
f = SimpleUploadedFile(
|
file_cache.delete("key")
|
||||||
name="test_file.jpg",
|
assert "key" not in file_cache.cached_keys
|
||||||
content=open(self.image_path, "rb").read(),
|
assert file_cache.get("key") is None
|
||||||
content_type="image/jpeg",
|
|
||||||
)
|
|
||||||
i = SimpleUploadedFile(
|
def test_should_delete_all_file_cache():
|
||||||
name="test_image.jpg",
|
file_cache = FileCache()
|
||||||
content=open(self.image_path, "rb").read(),
|
file_cache.set("key", file)
|
||||||
content_type="image/jpeg",
|
file_cache.set("key2", file)
|
||||||
)
|
file_cache.delete_all()
|
||||||
self.item = ItemFactory(name="Not name", file=f, image=i)
|
assert len(file_cache.cached_keys) == 0
|
||||||
|
assert file_cache.get("key") is None
|
||||||
return super().setUp()
|
assert file_cache.get("key2") is None
|
||||||
|
|
||||||
def test_save_as_continue_true_should_not_redirect_to_changelist(self):
|
|
||||||
item = self.item
|
|
||||||
# Load the Change Item Page
|
|
||||||
ItemAdmin.save_as_continue = True
|
|
||||||
|
|
||||||
# Upload new image and remove file
|
|
||||||
i2 = SimpleUploadedFile(
|
|
||||||
name="test_image2.jpg",
|
|
||||||
content=open(self.image_path, "rb").read(),
|
|
||||||
content_type="image/jpeg",
|
|
||||||
)
|
|
||||||
# Request.POST
|
|
||||||
data = {
|
|
||||||
"id": item.id,
|
|
||||||
"name": "name",
|
|
||||||
"price": 2.0,
|
|
||||||
"file": "",
|
|
||||||
"file-clear": "on",
|
|
||||||
"currency": Item.VALID_CURRENCIES[0][0],
|
|
||||||
"_confirm_change": True,
|
|
||||||
"_saveasnew": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set cache
|
|
||||||
cache_item = Item(
|
|
||||||
name=data["name"],
|
|
||||||
price=data["price"],
|
|
||||||
currency=data["currency"],
|
|
||||||
image=i2,
|
|
||||||
)
|
|
||||||
|
|
||||||
cache.set(CACHE_KEYS["object"], cache_item)
|
|
||||||
cache.set(CACHE_KEYS["post"], data)
|
|
||||||
|
|
||||||
# Click "Yes, I'm Sure"
|
|
||||||
del data["_confirm_change"]
|
|
||||||
data[CONFIRMATION_RECEIVED] = True
|
|
||||||
|
|
||||||
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
|
||||||
response = self.client.post(
|
|
||||||
f"/admin/market/item/{self.item.id}/change/", data=data
|
|
||||||
)
|
|
||||||
# Should show message to user with correct obj and path
|
|
||||||
message_user.assert_called_once()
|
|
||||||
message = message_user.call_args[0][1]
|
|
||||||
self.assertIn("/admin/market/item/2/change/", message)
|
|
||||||
self.assertIn(data["name"], message)
|
|
||||||
self.assertIn("You may edit it again below.", message)
|
|
||||||
|
|
||||||
# Should not have redirected to changelist
|
|
||||||
self.assertEqual(response.url, f"/admin/market/item/{self.item.id + 1}/change/")
|
|
||||||
|
|
||||||
# Should not have changed existing item
|
|
||||||
item.refresh_from_db()
|
|
||||||
self.assertEqual(item.name, "Not name")
|
|
||||||
self.assertEqual(item.file.name.count("test_file"), 1)
|
|
||||||
self.assertEqual(item.image.name.count("test_image2"), 0)
|
|
||||||
self.assertEqual(item.image.name.count("test_image"), 1)
|
|
||||||
|
|
||||||
# Should have saved new item
|
|
||||||
self.assertEqual(Item.objects.count(), 2)
|
|
||||||
new_item = Item.objects.filter(id=item.id + 1).first()
|
|
||||||
self.assertIsNotNone(new_item)
|
|
||||||
self.assertEqual(new_item.name, data["name"])
|
|
||||||
self.assertEqual(new_item.price, data["price"])
|
|
||||||
self.assertEqual(new_item.currency, data["currency"])
|
|
||||||
self.assertFalse(new_item.file)
|
|
||||||
self.assertEqual(new_item.image.name.count("test_image2"), 1)
|
|
||||||
|
|
||||||
# Should have cleared cache
|
|
||||||
for key in CACHE_KEYS.values():
|
|
||||||
self.assertIsNone(cache.get(key))
|
|
||||||
|
|
||||||
def test_save_as_continue_false_should_redirect_to_changelist(self):
|
|
||||||
item = self.item
|
|
||||||
# Load the Change Item Page
|
|
||||||
ItemAdmin.save_as_continue = False
|
|
||||||
|
|
||||||
# Upload new image and remove file
|
|
||||||
i2 = SimpleUploadedFile(
|
|
||||||
name="test_image2.jpg",
|
|
||||||
content=open(self.image_path, "rb").read(),
|
|
||||||
content_type="image/jpeg",
|
|
||||||
)
|
|
||||||
# Request.POST
|
|
||||||
data = {
|
|
||||||
"id": item.id,
|
|
||||||
"name": "name",
|
|
||||||
"price": 2.0,
|
|
||||||
"file": "",
|
|
||||||
"file-clear": "on",
|
|
||||||
"currency": Item.VALID_CURRENCIES[0][0],
|
|
||||||
"_confirm_change": True,
|
|
||||||
"_saveasnew": True,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set cache
|
|
||||||
cache_item = Item(
|
|
||||||
name=data["name"],
|
|
||||||
price=data["price"],
|
|
||||||
currency=data["currency"],
|
|
||||||
image=i2,
|
|
||||||
)
|
|
||||||
|
|
||||||
cache.set(CACHE_KEYS["object"], cache_item)
|
|
||||||
cache.set(CACHE_KEYS["post"], data)
|
|
||||||
|
|
||||||
# Click "Yes, I'm Sure"
|
|
||||||
del data["_confirm_change"]
|
|
||||||
data[CONFIRMATION_RECEIVED] = True
|
|
||||||
|
|
||||||
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
|
||||||
response = self.client.post(
|
|
||||||
f"/admin/market/item/{self.item.id}/change/", data=data
|
|
||||||
)
|
|
||||||
# Should show message to user with correct obj and path
|
|
||||||
message_user.assert_called_once()
|
|
||||||
message = message_user.call_args[0][1]
|
|
||||||
self.assertIn("/admin/market/item/2/change/", message)
|
|
||||||
self.assertIn(data["name"], message)
|
|
||||||
self.assertNotIn("You may edit it again below.", message)
|
|
||||||
|
|
||||||
# Should have redirected to changelist
|
|
||||||
self.assertEqual(response.url, "/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))
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)}")
|
||||||
|
|
|
||||||
12
setup.cfg
12
setup.cfg
|
|
@ -6,10 +6,12 @@ exclude =
|
||||||
admin_confirm/tests/*
|
admin_confirm/tests/*
|
||||||
tests/*
|
tests/*
|
||||||
ignore =
|
ignore =
|
||||||
C812 # missing trailing comma
|
D107 # Missing docstring in init
|
||||||
I001 # isort found an import in the wrong position
|
D400 # Doc-string: First line should end with a period
|
||||||
I004 # sisort found an unexpected blank line in imports
|
C812 # missing trailing comma
|
||||||
Q000 # Remove bad quotes
|
I001 # isort found an import in the wrong position
|
||||||
|
I004 # sisort found an unexpected blank line in imports
|
||||||
|
Q000 # Remove bad quotes
|
||||||
WPS110 # Seems to require no one word variable names
|
WPS110 # Seems to require no one word variable names
|
||||||
WPS305 # Found f string
|
WPS305 # Found f string
|
||||||
WPS336 # Explicit string concatination
|
WPS336 # Explicit string concatination
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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",),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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())]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue