fix(MR-16): Use cache for FileField and ImageField (#17)
* Remove casting to list in _get_form_data * Use cache for most fields and admin form for m2m files * MR comments/clean up * Cache should obey exclude and fields * Some more tests and docs * Only use cache for image files * Even more tests and handle save as new * fix test * More tests * minor refactor * Improve test coverage * Add no cover for some places * V0.2.3.dev7 * Adding tests for fieldsets * Added cache timeout * Added another test for an edge case * Fix issue with ManagementForm tampered with * Update cache to only set when form is_multipart * Even more testing changes * Update based on comments on MR and clean up a bit * make test names better Co-authored-by: Thu Trang Pham <thu@joinmodernhealth.com>main
parent
cc36492bfe
commit
06d3e1a208
|
|
@ -1,2 +1,4 @@
|
||||||
[run]
|
[run]
|
||||||
relative_files = True
|
relative_files = True
|
||||||
|
omit = admin_confirm/tests/*
|
||||||
|
branch = True
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,5 @@ docs/_build/
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
|
||||||
|
tmp/
|
||||||
|
|
|
||||||
1
Makefile
1
Makefile
|
|
@ -3,6 +3,7 @@ run:
|
||||||
|
|
||||||
test:
|
test:
|
||||||
coverage run --source admin_confirm --branch -m pytest
|
coverage run --source admin_confirm --branch -m pytest
|
||||||
|
coverage report -m
|
||||||
|
|
||||||
check-readme:
|
check-readme:
|
||||||
python -m readme_renderer README.md -o /tmp/README.html
|
python -m readme_renderer README.md -o /tmp/README.html
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,12 @@ Typical Usage:
|
||||||
confirmation_fields = ['field1', 'field2']
|
confirmation_fields = ['field1', 'field2']
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
Be aware that not all possible combinations of ModelAdmin have been tested, even if test coverage is high.
|
||||||
|
|
||||||
|
See [testing readme](admin_confirm/tests/README.md) for more details
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install django-admin-confirm by running:
|
Install django-admin-confirm by running:
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,23 @@ from django.template.response import TemplateResponse
|
||||||
from django.contrib.admin.options import TO_FIELD_VAR
|
from django.contrib.admin.options import TO_FIELD_VAR
|
||||||
from django.utils.translation import gettext as _
|
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
|
from django.db.models import Model, ManyToManyField, FileField, ImageField
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from admin_confirm.utils import snake_to_title_case
|
from admin_confirm.utils import get_admin_change_url, snake_to_title_case
|
||||||
|
from django.core.cache import cache
|
||||||
SAVE_ACTIONS = ["_save", "_saveasnew", "_addanother", "_continue"]
|
from django.views.decorators.cache import cache_control
|
||||||
|
from django.forms.formsets import all_valid
|
||||||
|
from admin_confirm.constants import (
|
||||||
|
CACHE_TIMEOUT,
|
||||||
|
CONFIRMATION_RECEIVED,
|
||||||
|
CONFIRM_ADD,
|
||||||
|
CONFIRM_CHANGE,
|
||||||
|
SAVE,
|
||||||
|
SAVE_ACTIONS,
|
||||||
|
CACHE_KEYS,
|
||||||
|
SAVE_AND_CONTINUE,
|
||||||
|
SAVE_AS_NEW,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdminConfirmMixin:
|
class AdminConfirmMixin:
|
||||||
|
|
@ -34,7 +46,9 @@ class AdminConfirmMixin:
|
||||||
if self.confirmation_fields is not None:
|
if self.confirmation_fields is not None:
|
||||||
return self.confirmation_fields
|
return self.confirmation_fields
|
||||||
|
|
||||||
return flatten_fieldsets(self.get_fieldsets(request, obj))
|
model_fields = set([field.name for field in self.model._meta.fields])
|
||||||
|
admin_fields = set(flatten_fieldsets(self.get_fieldsets(request, obj)))
|
||||||
|
return list(model_fields & admin_fields)
|
||||||
|
|
||||||
def render_change_confirmation(self, request, context):
|
def render_change_confirmation(self, request, context):
|
||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
|
|
@ -81,14 +95,22 @@ class AdminConfirmMixin:
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cache_control(private=True)
|
||||||
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
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
|
||||||
):
|
):
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
elif CONFIRMATION_RECEIVED in request.POST:
|
||||||
|
return self._confirmation_received_view(
|
||||||
|
request, object_id, form_url, extra_context
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cache.delete_many(CACHE_KEYS.values())
|
||||||
|
|
||||||
extra_context = {
|
extra_context = {
|
||||||
**(extra_context or {}),
|
**(extra_context or {}),
|
||||||
|
|
@ -111,32 +133,160 @@ class AdminConfirmMixin:
|
||||||
|
|
||||||
Returns a dictionary of the fields and their changed values if any
|
Returns a dictionary of the fields and their changed values if any
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _display_for_changed_data(field, initial_value, new_value):
|
||||||
|
if not (isinstance(field, FileField) or isinstance(field, ImageField)):
|
||||||
|
return [initial_value, new_value]
|
||||||
|
|
||||||
|
if initial_value:
|
||||||
|
if new_value == False:
|
||||||
|
# Clear has been selected
|
||||||
|
return [initial_value.name, None]
|
||||||
|
elif new_value:
|
||||||
|
return [initial_value.name, new_value.name]
|
||||||
|
else:
|
||||||
|
# No cover: Technically doesn't get called in current code because
|
||||||
|
# This function is only called if there was a difference in the data
|
||||||
|
return [initial_value.name, initial_value.name] # pragma: no cover
|
||||||
|
|
||||||
|
if new_value:
|
||||||
|
return [None, new_value.name]
|
||||||
|
|
||||||
|
return [None, None]
|
||||||
|
|
||||||
changed_data = {}
|
changed_data = {}
|
||||||
if form.is_valid():
|
|
||||||
if add:
|
if add:
|
||||||
for name, new_value in form.cleaned_data.items():
|
for name, new_value in form.cleaned_data.items():
|
||||||
# Don't consider default values as changed for adding
|
# Don't consider default values as changed for adding
|
||||||
default_value = model._meta.get_field(name).get_default()
|
field_object = model._meta.get_field(name)
|
||||||
|
default_value = field_object.get_default()
|
||||||
if new_value is not None and new_value != default_value:
|
if new_value is not None and new_value != default_value:
|
||||||
# Show what the default value is
|
# Show what the default value is
|
||||||
changed_data[name] = [default_value, new_value]
|
changed_data[name] = _display_for_changed_data(
|
||||||
|
field_object, default_value, new_value
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Parse the changed data - Note that using form.changed_data would not work because initial is not set
|
# Parse the changed data - Note that using form.changed_data would not work because initial is not set
|
||||||
for name, new_value in form.cleaned_data.items():
|
for name, new_value in form.cleaned_data.items():
|
||||||
|
|
||||||
# Since the form considers initial as the value first shown in the form
|
# Since the form considers initial as the value first shown in the form
|
||||||
# It could be incorrect when user hits save, and then hits "No, go back to edit"
|
# It could be incorrect when user hits save, and then hits "No, go back to edit"
|
||||||
obj.refresh_from_db()
|
obj.refresh_from_db()
|
||||||
# Note: getattr does not work on ManyToManyFields
|
|
||||||
field_object = model._meta.get_field(name)
|
field_object = model._meta.get_field(name)
|
||||||
initial_value = getattr(obj, name)
|
initial_value = getattr(obj, name)
|
||||||
|
|
||||||
|
# Note: getattr does not work on ManyToManyFields
|
||||||
if isinstance(field_object, ManyToManyField):
|
if isinstance(field_object, ManyToManyField):
|
||||||
initial_value = field_object.value_from_object(obj)
|
initial_value = field_object.value_from_object(obj)
|
||||||
|
|
||||||
if initial_value != new_value:
|
if initial_value != new_value:
|
||||||
changed_data[name] = [initial_value, new_value]
|
changed_data[name] = _display_for_changed_data(
|
||||||
|
field_object, initial_value, new_value
|
||||||
|
)
|
||||||
|
|
||||||
return changed_data
|
return changed_data
|
||||||
|
|
||||||
|
def _confirmation_received_view(self, request, object_id, form_url, extra_context):
|
||||||
|
"""
|
||||||
|
When the form is a multipart form, the object and POST are cached
|
||||||
|
This is required because file(s) cannot be programmically uploaded
|
||||||
|
ie. There is no way to set a file on the html form
|
||||||
|
|
||||||
|
If the form isn't multipart, this function would not be called.
|
||||||
|
If there are no file changes, do nothing to the request and send to Django.
|
||||||
|
|
||||||
|
If there are files uploaded, save the files from cached object to either:
|
||||||
|
- the object instance if already exists
|
||||||
|
- or save the new object and modify the request from `add` to `change`
|
||||||
|
and pass the request to Django
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _reconstruct_request_files():
|
||||||
|
"""
|
||||||
|
Reconstruct the file(s) from the cached object (if any).
|
||||||
|
Returns a dictionary of field name to cached file
|
||||||
|
"""
|
||||||
|
reconstructed_files = {}
|
||||||
|
|
||||||
|
cached_object = cache.get(CACHE_KEYS["object"])
|
||||||
|
query_dict = cache.get(CACHE_KEYS["post"])
|
||||||
|
# Reconstruct the files from cached object
|
||||||
|
if not cached_object:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not query_dict:
|
||||||
|
# Use the current POST, since it should mirror cached POST
|
||||||
|
query_dict = request.POST
|
||||||
|
|
||||||
|
if type(cached_object) != self.model:
|
||||||
|
# Do not use cache if the model doesn't match this model
|
||||||
|
return
|
||||||
|
|
||||||
|
for field in self.model._meta.get_fields():
|
||||||
|
if not (isinstance(field, FileField) or isinstance(field, ImageField)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
cached_file = getattr(cached_object, field.name)
|
||||||
|
# If a file was uploaded, the field is omitted from the POST since it's in request.FILES
|
||||||
|
if not query_dict.get(field.name) and cached_file:
|
||||||
|
reconstructed_files[field.name] = cached_file
|
||||||
|
|
||||||
|
return reconstructed_files
|
||||||
|
|
||||||
|
reconstructed_files = _reconstruct_request_files()
|
||||||
|
if reconstructed_files:
|
||||||
|
obj = None
|
||||||
|
|
||||||
|
# remove the _confirm_add and _confirm_change from post
|
||||||
|
modified_post = request.POST.copy()
|
||||||
|
cached_post = cache.get(CACHE_KEYS["post"])
|
||||||
|
# No cover: __reconstruct_request_files currently checks for cached post so cached_post won't be None
|
||||||
|
if cached_post: # pragma: no cover
|
||||||
|
modified_post = cached_post.copy()
|
||||||
|
if CONFIRM_ADD in modified_post:
|
||||||
|
del modified_post[CONFIRM_ADD]
|
||||||
|
if CONFIRM_CHANGE in modified_post:
|
||||||
|
del modified_post[CONFIRM_CHANGE]
|
||||||
|
|
||||||
|
if object_id and not SAVE_AS_NEW in request.POST:
|
||||||
|
# Update the obj with the new uploaded files
|
||||||
|
# then pass rest of changes to Django
|
||||||
|
obj = self.model.objects.filter(id=object_id).first()
|
||||||
|
else:
|
||||||
|
# Create the obj and pass the rest as changes to Django
|
||||||
|
# (Since we are not handling the formsets/inlines)
|
||||||
|
# Note that this results in the "Yes, I'm Sure" submission
|
||||||
|
# act as a `change` not an `add`
|
||||||
|
obj = cache.get(CACHE_KEYS["object"])
|
||||||
|
|
||||||
|
# No cover: __reconstruct_request_files currently checks for cached obj so obj won't be None
|
||||||
|
if obj: # pragma: no cover
|
||||||
|
for field, file in reconstructed_files.items():
|
||||||
|
setattr(obj, field, file)
|
||||||
|
obj.save()
|
||||||
|
object_id = str(obj.id)
|
||||||
|
# Update the request path, used in the message to user and redirect
|
||||||
|
# Used in `self.response_change`
|
||||||
|
request.path = get_admin_change_url(obj)
|
||||||
|
|
||||||
|
if SAVE_AS_NEW in request.POST:
|
||||||
|
# We have already saved the new object
|
||||||
|
# So change action to _continue
|
||||||
|
del modified_post[SAVE_AS_NEW]
|
||||||
|
if self.save_as_continue:
|
||||||
|
modified_post[SAVE_AND_CONTINUE] = True
|
||||||
|
else:
|
||||||
|
modified_post[SAVE] = True
|
||||||
|
if "id" in modified_post:
|
||||||
|
del modified_post["id"]
|
||||||
|
modified_post["id"] = object_id
|
||||||
|
|
||||||
|
request.POST = modified_post
|
||||||
|
|
||||||
|
cache.delete_many(CACHE_KEYS.values())
|
||||||
|
return super()._changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
def _change_confirmation_view(self, request, object_id, form_url, extra_context):
|
def _change_confirmation_view(self, request, object_id, form_url, extra_context):
|
||||||
# This code is taken from super()._changeform_view
|
# This code is taken from super()._changeform_view
|
||||||
# https://github.com/django/django/blob/master/django/contrib/admin/options.py#L1575-L1592
|
# https://github.com/django/django/blob/master/django/contrib/admin/options.py#L1575-L1592
|
||||||
|
|
@ -169,13 +319,19 @@ class AdminConfirmMixin:
|
||||||
)
|
)
|
||||||
|
|
||||||
form = ModelForm(request.POST, request.FILES, obj)
|
form = ModelForm(request.POST, request.FILES, obj)
|
||||||
# Note to self: For inline instances see:
|
form_validated = form.is_valid()
|
||||||
# https://github.com/django/django/blob/master/django/contrib/admin/options.py#L1582
|
if form_validated:
|
||||||
|
new_object = self.save_form(request, form, change=not add)
|
||||||
|
else:
|
||||||
|
new_object = form.instance
|
||||||
|
formsets, inline_instances = self._create_formsets(
|
||||||
|
request, new_object, change=not add
|
||||||
|
)
|
||||||
# End code from super()._changeform_view
|
# End code from super()._changeform_view
|
||||||
|
|
||||||
|
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
|
||||||
changed_data = self._get_changed_data(form, model, obj, add)
|
changed_data = self._get_changed_data(form, model, obj, add_or_new)
|
||||||
|
|
||||||
changed_confirmation_fields = set(
|
changed_confirmation_fields = set(
|
||||||
self.get_confirmation_fields(request, obj)
|
self.get_confirmation_fields(request, obj)
|
||||||
|
|
@ -186,13 +342,17 @@ class AdminConfirmMixin:
|
||||||
|
|
||||||
# Parse the original save action from request
|
# Parse the original save action from request
|
||||||
save_action = None
|
save_action = None
|
||||||
for key in request.POST.keys():
|
# No cover: There would not be a case of not request.POST.keys() and form is valid
|
||||||
|
for key in request.POST.keys(): # pragma: no cover
|
||||||
if key in SAVE_ACTIONS:
|
if key in SAVE_ACTIONS:
|
||||||
save_action = key
|
save_action = key
|
||||||
break
|
break
|
||||||
|
|
||||||
title_action = _("adding") if add else _("changing")
|
if form.is_multipart():
|
||||||
|
cache.set(CACHE_KEYS["post"], request.POST, timeout=CACHE_TIMEOUT)
|
||||||
|
cache.set(CACHE_KEYS["object"], new_object, timeout=CACHE_TIMEOUT)
|
||||||
|
|
||||||
|
title_action = _("adding") if add_or_new else _("changing")
|
||||||
context = {
|
context = {
|
||||||
**self.admin_site.each_context(request),
|
**self.admin_site.each_context(request),
|
||||||
"preserved_filters": self.get_preserved_filters(request),
|
"preserved_filters": self.get_preserved_filters(request),
|
||||||
|
|
@ -205,8 +365,10 @@ class AdminConfirmMixin:
|
||||||
"opts": opts,
|
"opts": opts,
|
||||||
"changed_data": changed_data,
|
"changed_data": changed_data,
|
||||||
"add": add,
|
"add": add,
|
||||||
|
"save_as_new": SAVE_AS_NEW in request.POST,
|
||||||
"submit_name": save_action,
|
"submit_name": save_action,
|
||||||
"form": form,
|
"form": form,
|
||||||
|
"formsets": formsets,
|
||||||
**(extra_context or {}),
|
**(extra_context or {}),
|
||||||
}
|
}
|
||||||
return self.render_change_confirmation(request, context)
|
return self.render_change_confirmation(request, context)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
SAVE = "_save"
|
||||||
|
SAVE_AS_NEW = "_saveasnew"
|
||||||
|
ADD_ANOTHER = "_addanother"
|
||||||
|
SAVE_AND_CONTINUE = "_continue"
|
||||||
|
SAVE_ACTIONS = [SAVE, SAVE_AS_NEW, ADD_ANOTHER, SAVE_AND_CONTINUE]
|
||||||
|
|
||||||
|
CONFIRM_ADD = "_confirm_add"
|
||||||
|
CONFIRM_CHANGE = "_confirm_change"
|
||||||
|
CONFIRMATION_RECEIVED = "_confirmation_received"
|
||||||
|
|
||||||
|
CACHE_TIMEOUT = getattr(settings, "ADMIN_CONFIRM_CACHE_TIMEOUT", 10)
|
||||||
|
CACHE_KEYS = {
|
||||||
|
"object": "admin_confirm__confirmation_object",
|
||||||
|
"post": "admin_confirm__confirmation_request_post",
|
||||||
|
}
|
||||||
|
|
@ -31,22 +31,24 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if add %}
|
{% if add or save_as_new %}
|
||||||
<p>{% blocktrans with escaped_object=object %}Are you sure you want to add the {{ model_name }}?{% endblocktrans %}</p>
|
<p>{% blocktrans with escaped_object=object %}Are you sure you want to add the {{ model_name }}?{% endblocktrans %}</p>
|
||||||
{% include "admin/change_data.html" %}
|
|
||||||
<form method="post" action="{% url opts|admin_urlname:'add'%}">{% csrf_token %}
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<p>{% blocktrans with escaped_object=object %}Are you sure you want to change the {{ model_name }} "{{ object_name }}"?{% endblocktrans %}</p>
|
<p>{% blocktrans with escaped_object=object %}Are you sure you want to change the {{ model_name }} "{{ object_name }}"?{% endblocktrans %}</p>
|
||||||
{% include "admin/change_data.html" %}
|
|
||||||
<form method="post" action="{% url opts|admin_urlname:'change' object_id|admin_urlquote %}">{% csrf_token %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class=hidden>
|
|
||||||
{{ form }}
|
{% include "admin/change_data.html" %}
|
||||||
|
|
||||||
|
<form {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} method="post" {% if add %}action="{% url opts|admin_urlname:'add'%}" {% else %}action="{% url opts|admin_urlname:'change' object_id|admin_urlquote %}"{% endif %}>{% csrf_token %}
|
||||||
|
<div class="hidden">
|
||||||
|
{{form.as_p}}
|
||||||
|
{% for formset in formsets %}
|
||||||
|
{{ formset.as_p }}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
|
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
|
||||||
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
|
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
|
||||||
|
{% if form.is_multipart %}<input type="hidden" name=CONFIRMATION_RECEIVED value="True">{% endif %}
|
||||||
<div class="submit-row">
|
<div class="submit-row">
|
||||||
<input type="submit" value="{% trans 'Yes, I’m sure' %}" name="{{ submit_name }}">
|
<input type="submit" value="{% trans 'Yes, I’m sure' %}" name="{{ submit_name }}">
|
||||||
<p class="deletelink-box">
|
<p class="deletelink-box">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Testing Documentation/Notes
|
||||||
|
|
||||||
|
[](https://coveralls.io/github/TrangPham/django-admin-confirm)
|
||||||
|
|
||||||
|
Hello, friend! You have found the list of test cases that this package can benefit from.
|
||||||
|
|
||||||
|
You seem concerned about the stability and reliability of this package. You're probably wondering if you should include it in your production codebase. Well, although I have tried very hard to get 100% code coverage, there are so many permutations of ModelAdmins in the wild. And I'm only one person.
|
||||||
|
|
||||||
|
So if you want to include this package in your production codebase, be aware that AdminConfirmMixin works best with simple unmodified ModelAdmins.
|
||||||
|
|
||||||
|
## Save Options
|
||||||
|
|
||||||
|
- [x] Save
|
||||||
|
- [x] Conitnue
|
||||||
|
- [x] Save As New
|
||||||
|
- [x] Add another
|
||||||
|
|
||||||
|
### Field types
|
||||||
|
|
||||||
|
- [x] CharField
|
||||||
|
- [x] PositiveIntegerField
|
||||||
|
- [x] DecimalField
|
||||||
|
- [x] TextField
|
||||||
|
- [x] ImageField
|
||||||
|
- [x] FileField
|
||||||
|
- [x] ManyToManyField
|
||||||
|
- [x] OneToOneField
|
||||||
|
- [x] ForeignKey
|
||||||
|
|
||||||
|
- [x] Custom Readonly fields
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
- [x] .exclude
|
||||||
|
- [x] .fields
|
||||||
|
- [x] .readonly_fields
|
||||||
|
- [x] Actions
|
||||||
|
|
||||||
|
### Options to test
|
||||||
|
|
||||||
|
- [x] ModelAdmin.fieldsets
|
||||||
|
- [ ] ModelAdmin.form
|
||||||
|
- [ ] ModelAdmin.raw_id_fields
|
||||||
|
- [ ] ModelAdmin.radio_fields
|
||||||
|
- [ ] ModelAdmin.autocomplete_fields
|
||||||
|
- [ ] ModelAdmin.prepopulated_fields
|
||||||
|
|
||||||
|
## ModelAdmin form template overrides?
|
||||||
|
|
||||||
|
https://docs.djangoproject.com/en/3.1/ref/contrib/admin/#custom-template-options
|
||||||
|
(Maybe??? IDK this is esoteric)
|
||||||
|
|
||||||
|
## Function overrides to test
|
||||||
|
|
||||||
|
- [ ] .save_model()
|
||||||
|
- [ ] .get_readonly_fields()
|
||||||
|
- [ ] .get_fields()
|
||||||
|
- [ ] .get_excludes()
|
||||||
|
- [ ] .get_form()
|
||||||
|
- [ ] .get_autocomplete_fields()
|
||||||
|
- [ ] .get_prepopulated_fields()
|
||||||
|
- [x] .get_fieldsets()
|
||||||
|
- [ ] ModelAdmin.formfield_for_manytomany()
|
||||||
|
- [ ] ModelAdmin.formfield_for_foreignkey()
|
||||||
|
- [ ] ModelAdmin.formfield_for_choice_field()
|
||||||
|
- [ ] ModelAdmin.get_changeform_initial_data()
|
||||||
|
|
||||||
|
## Inline instance support??
|
||||||
|
|
||||||
|
Confirmation on inline changes is not a current feature of this project.
|
||||||
|
|
||||||
|
Confirmation on add/change of ModelAdmin that includes inlines needs to be tested. Use AdminConfirmMixin with ModelAdmin containing inlines at your own risk.
|
||||||
|
|
||||||
|
- [ ] .inlines
|
||||||
|
- [ ] .get_inline_instances()
|
||||||
|
- [ ] .get_inlines() (New in Django 3.0)
|
||||||
|
- [ ] .get_formsets_with_inlines()
|
||||||
|
|
||||||
|
#### Options for inlines
|
||||||
|
|
||||||
|
- [ ] classes of inlines: Tabular, Stacked, etc
|
||||||
|
- [ ] extra
|
||||||
|
- [ ] action on the inline: add or change
|
||||||
|
- [ ] clicking add another on the inline
|
||||||
|
|
||||||
|
## IDK if we want to support these
|
||||||
|
|
||||||
|
- [ ] .get_changelist_form()
|
||||||
|
- [ ] ModelAdmin.list_editable
|
||||||
|
- [ ] ModelAdmin.changelist_view()
|
||||||
|
|
||||||
|
- [ ] ModelAdmin.add_view(request, form_url='', extra_context=None)
|
||||||
|
- [ ] ModelAdmin.change_view(request, object_id, form_url='', extra_context=None)
|
||||||
|
|
||||||
|
## More tests for these?
|
||||||
|
|
||||||
|
Note: Currently the code always calls super().\_changeform_view(), which would ensure permissions correct as well
|
||||||
|
|
||||||
|
- [x] ModelAdmin.has_add_permission
|
||||||
|
- [x] ModelAdmin.has_change_permission
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
|
from django.core.cache import cache
|
||||||
from django.test import TestCase, RequestFactory
|
from django.test import TestCase, RequestFactory
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
class ConfirmAdminTestCase(TestCase):
|
class AdminConfirmTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Helper TestCase class and common associated assertions
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.superuser = User.objects.create_superuser(
|
cls.superuser = User.objects.create_superuser(
|
||||||
|
|
@ -10,6 +15,7 @@ class ConfirmAdminTestCase(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
cache.clear()
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
|
@ -24,7 +30,9 @@ class ConfirmAdminTestCase(TestCase):
|
||||||
# ManyToManyField should be embedded
|
# ManyToManyField should be embedded
|
||||||
self.assertIn("related-widget-wrapper", rendered_content)
|
self.assertIn("related-widget-wrapper", rendered_content)
|
||||||
|
|
||||||
def _assertSubmitHtml(self, rendered_content, save_action="_save"):
|
def _assertSubmitHtml(
|
||||||
|
self, rendered_content, save_action="_save", multipart_form=False
|
||||||
|
):
|
||||||
# Submit should conserve the save action
|
# Submit should conserve the save action
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
f'<input type="submit" value="Yes, I’m sure" name="{save_action}">',
|
f'<input type="submit" value="Yes, I’m sure" name="{save_action}">',
|
||||||
|
|
@ -34,6 +42,16 @@ class ConfirmAdminTestCase(TestCase):
|
||||||
self.assertNotIn("_confirm_add", rendered_content)
|
self.assertNotIn("_confirm_add", rendered_content)
|
||||||
self.assertNotIn("_confirm_change", rendered_content)
|
self.assertNotIn("_confirm_change", rendered_content)
|
||||||
|
|
||||||
|
confirmation_received_html = (
|
||||||
|
'<input type="hidden" name=CONFIRMATION_RECEIVED value="True">'
|
||||||
|
)
|
||||||
|
|
||||||
|
if multipart_form:
|
||||||
|
# Should have _confirmation_received as a hidden field
|
||||||
|
self.assertIn(confirmation_received_html, rendered_content)
|
||||||
|
else:
|
||||||
|
self.assertNotIn(confirmation_received_html, rendered_content)
|
||||||
|
|
||||||
def _assertSimpleFieldFormHtml(self, rendered_content, fields):
|
def _assertSimpleFieldFormHtml(self, rendered_content, fields):
|
||||||
for k, v in fields.items():
|
for k, v in fields.items():
|
||||||
self.assertIn(f'name="{k}"', rendered_content)
|
self.assertIn(f'name="{k}"', rendered_content)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
from unittest import mock
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from admin_confirm.tests.helpers import AdminConfirmTestCase
|
||||||
|
from tests.market.admin import ShoppingMallAdmin
|
||||||
|
from tests.market.models import GeneralManager, ShoppingMall, Town
|
||||||
|
from tests.factories import ShopFactory
|
||||||
|
|
||||||
|
from admin_confirm.constants import CACHE_KEYS, CONFIRMATION_RECEIVED
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
|
||||||
|
class TestAdminOptions(AdminConfirmTestCase):
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "fields", ["name", "town"])
|
||||||
|
def test_change_model_with_m2m_field_without_input_for_m2m_field_should_work(self):
|
||||||
|
gm = GeneralManager.objects.create(name="gm")
|
||||||
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
town = Town.objects.create(name="town")
|
||||||
|
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
|
||||||
|
mall.shops.set(shops)
|
||||||
|
|
||||||
|
# new values
|
||||||
|
gm2 = GeneralManager.objects.create(name="gm2")
|
||||||
|
shops2 = [ShopFactory() for i in range(3)]
|
||||||
|
town2 = Town.objects.create(name="town2")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": mall.id,
|
||||||
|
"name": "name",
|
||||||
|
"town": town2.id,
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content, save_action="_continue"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have cached the unsaved obj
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNone(cached_item)
|
||||||
|
|
||||||
|
# Should not have saved changes yet
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
mall.refresh_from_db()
|
||||||
|
self.assertEqual(mall.name, "mall")
|
||||||
|
self.assertEqual(mall.general_manager, gm)
|
||||||
|
self.assertEqual(mall.town, town)
|
||||||
|
for shop in mall.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
confirmation_received_data = data
|
||||||
|
del confirmation_received_data["_confirm_change"]
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/",
|
||||||
|
data=confirmation_received_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved obj
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
saved_item = ShoppingMall.objects.all().first()
|
||||||
|
# should have updated fields that were in form
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.town, town2)
|
||||||
|
# should have presevered the fields that are not in form
|
||||||
|
self.assertEqual(saved_item.general_manager, gm)
|
||||||
|
for shop in saved_item.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "exclude", ["shops"])
|
||||||
|
def test_when_m2m_field_in_exclude_changes_to_field_should_not_be_saved(self):
|
||||||
|
gm = GeneralManager.objects.create(name="gm")
|
||||||
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
town = Town.objects.create(name="town")
|
||||||
|
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
|
||||||
|
mall.shops.set(shops)
|
||||||
|
|
||||||
|
# new values
|
||||||
|
gm2 = GeneralManager.objects.create(name="gm2")
|
||||||
|
shops2 = [ShopFactory() for i in range(3)]
|
||||||
|
town2 = Town.objects.create(name="town2")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": mall.id,
|
||||||
|
"name": "name",
|
||||||
|
"general_manager": gm2.id,
|
||||||
|
"shops": [1],
|
||||||
|
"town": town2.id,
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content, save_action="_continue"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have cached the unsaved obj
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNone(cached_item)
|
||||||
|
|
||||||
|
# Should not have saved changes yet
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
mall.refresh_from_db()
|
||||||
|
self.assertEqual(mall.name, "mall")
|
||||||
|
self.assertEqual(mall.general_manager, gm)
|
||||||
|
self.assertEqual(mall.town, town)
|
||||||
|
for shop in mall.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
confirmation_received_data = data
|
||||||
|
del confirmation_received_data["_confirm_change"]
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/",
|
||||||
|
data=confirmation_received_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved obj
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
saved_item = ShoppingMall.objects.all().first()
|
||||||
|
# should have updated fields that were in form
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.town, town2)
|
||||||
|
self.assertEqual(saved_item.general_manager, gm2)
|
||||||
|
# should have presevered the fields that are not in form (exclude)
|
||||||
|
for shop in saved_item.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "exclude", ["shops", "name"])
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
|
||||||
|
def test_if_confirmation_fields_in_exclude_should_not_trigger_confirmation(self):
|
||||||
|
gm = GeneralManager.objects.create(name="gm")
|
||||||
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
town = Town.objects.create(name="town")
|
||||||
|
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
|
||||||
|
mall.shops.set(shops)
|
||||||
|
|
||||||
|
# new values
|
||||||
|
gm2 = GeneralManager.objects.create(name="gm2")
|
||||||
|
shops2 = [ShopFactory() for i in range(3)]
|
||||||
|
town2 = Town.objects.create(name="town2")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": mall.id,
|
||||||
|
"name": "name",
|
||||||
|
"general_manager": gm2.id,
|
||||||
|
"shops": [1],
|
||||||
|
"town": town2.id,
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should not be shown confirmation page
|
||||||
|
# SInce we used "Save and Continue", should show change page
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved the non excluded fields
|
||||||
|
mall.refresh_from_db()
|
||||||
|
for shop in shops:
|
||||||
|
self.assertIn(shop, mall.shops.all())
|
||||||
|
self.assertEqual(mall.name, "mall")
|
||||||
|
# Should have saved other fields
|
||||||
|
self.assertEqual(mall.town, town2)
|
||||||
|
self.assertEqual(mall.general_manager, gm2)
|
||||||
|
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "readonly_fields", ["shops", "name"])
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
|
||||||
|
def test_if_confirmation_fields_in_readonly_should_not_trigger_confirmation(self):
|
||||||
|
gm = GeneralManager.objects.create(name="gm")
|
||||||
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
town = Town.objects.create(name="town")
|
||||||
|
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
|
||||||
|
mall.shops.set(shops)
|
||||||
|
|
||||||
|
# new values
|
||||||
|
gm2 = GeneralManager.objects.create(name="gm2")
|
||||||
|
shops2 = [ShopFactory() for i in range(3)]
|
||||||
|
town2 = Town.objects.create(name="town2")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": mall.id,
|
||||||
|
"name": "name",
|
||||||
|
"general_manager": gm2.id,
|
||||||
|
"shops": [1],
|
||||||
|
"town": town2.id,
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should not be shown confirmation page
|
||||||
|
# SInce we used "Save and Continue", should show change page
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved the non excluded fields
|
||||||
|
mall.refresh_from_db()
|
||||||
|
for shop in shops:
|
||||||
|
self.assertIn(shop, mall.shops.all())
|
||||||
|
self.assertEqual(mall.name, "mall")
|
||||||
|
# Should have saved other fields
|
||||||
|
self.assertEqual(mall.town, town2)
|
||||||
|
self.assertEqual(mall.general_manager, gm2)
|
||||||
|
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "confirmation_fields", ["name"])
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "readonly_fields", ["shops"])
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
|
||||||
|
def test_readonly_fields_should_not_change(self):
|
||||||
|
gm = GeneralManager.objects.create(name="gm")
|
||||||
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
town = Town.objects.create(name="town")
|
||||||
|
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
|
||||||
|
mall.shops.set(shops)
|
||||||
|
|
||||||
|
# new values
|
||||||
|
gm2 = GeneralManager.objects.create(name="gm2")
|
||||||
|
shops2 = [ShopFactory() for i in range(3)]
|
||||||
|
town2 = Town.objects.create(name="town2")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": mall.id,
|
||||||
|
"name": "name",
|
||||||
|
"general_manager": gm2.id,
|
||||||
|
"shops": [1],
|
||||||
|
"town": town2.id,
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content, save_action="_continue"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have cached the unsaved obj
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNone(cached_item)
|
||||||
|
|
||||||
|
# Should not have saved changes yet
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
mall.refresh_from_db()
|
||||||
|
self.assertEqual(mall.name, "mall")
|
||||||
|
self.assertEqual(mall.general_manager, gm)
|
||||||
|
self.assertEqual(mall.town, town)
|
||||||
|
for shop in mall.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
confirmation_received_data = data
|
||||||
|
del confirmation_received_data["_confirm_change"]
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/",
|
||||||
|
data=confirmation_received_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved obj
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
saved_item = ShoppingMall.objects.all().first()
|
||||||
|
# should have updated fields that were in form
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.town, town2)
|
||||||
|
self.assertEqual(saved_item.general_manager, gm2)
|
||||||
|
# should have presevered the fields that are not in form (exclude)
|
||||||
|
for shop in saved_item.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
|
from unittest import mock
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from django.contrib.admin.options import TO_FIELD_VAR
|
from django.contrib.admin.options import TO_FIELD_VAR
|
||||||
from django.http import HttpResponseForbidden, HttpResponseBadRequest
|
from django.http import HttpResponseForbidden, HttpResponseBadRequest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from admin_confirm.tests.helpers import ConfirmAdminTestCase
|
from admin_confirm.tests.helpers import AdminConfirmTestCase
|
||||||
from tests.market.admin import ItemAdmin, InventoryAdmin
|
from tests.market.admin import ItemAdmin, InventoryAdmin, ShoppingMallAdmin
|
||||||
from tests.market.models import Item, Inventory
|
from tests.market.models import Item, Inventory, ShoppingMall
|
||||||
from tests.factories import ItemFactory, ShopFactory, InventoryFactory
|
from tests.factories import ItemFactory, ShopFactory, InventoryFactory
|
||||||
|
|
||||||
|
|
||||||
class TestConfirmChangeAndAdd(ConfirmAdminTestCase):
|
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
|
||||||
|
class TestConfirmChangeAndAdd(AdminConfirmTestCase):
|
||||||
def test_get_add_without_confirm_add(self):
|
def test_get_add_without_confirm_add(self):
|
||||||
|
ItemAdmin.confirm_add = False
|
||||||
response = self.client.get(reverse("admin:market_item_add"))
|
response = self.client.get(reverse("admin:market_item_add"))
|
||||||
self.assertFalse(response.context_data.get("confirm_add"))
|
self.assertFalse(response.context_data.get("confirm_add"))
|
||||||
self.assertNotIn("_confirm_add", response.rendered_content)
|
self.assertNotIn("_confirm_add", response.rendered_content)
|
||||||
|
|
@ -70,6 +73,36 @@ class TestConfirmChangeAndAdd(ConfirmAdminTestCase):
|
||||||
# Should not have been added yet
|
# Should not have been added yet
|
||||||
self.assertEqual(Inventory.objects.count(), 0)
|
self.assertEqual(Inventory.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_post_change_with_confirm_change_shoppingmall_name(self):
|
||||||
|
# When testing found that even though name was in confirmation_fields
|
||||||
|
# When only name changed, `form.is_valid` = False, and thus didn't trigger
|
||||||
|
# confirmation page previously, even though it should have
|
||||||
|
|
||||||
|
mall = ShoppingMall.objects.create(name="name")
|
||||||
|
data = {
|
||||||
|
"id": mall.id,
|
||||||
|
"name": "new name",
|
||||||
|
"_confirm_change": True,
|
||||||
|
"csrfmiddlewaretoken": "fake token",
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/", data
|
||||||
|
)
|
||||||
|
# Ensure not redirected (confirmation page does not redirect)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
expected_templates = [
|
||||||
|
"admin/market/shoppingmall/change_confirmation.html",
|
||||||
|
"admin/market/change_confirmation.html",
|
||||||
|
"admin/change_confirmation.html",
|
||||||
|
]
|
||||||
|
self.assertEqual(response.template_name, expected_templates)
|
||||||
|
self._assertSubmitHtml(rendered_content=response.rendered_content)
|
||||||
|
|
||||||
|
# Hasn't changed item yet
|
||||||
|
mall.refresh_from_db()
|
||||||
|
self.assertEqual(mall.name, "name")
|
||||||
|
|
||||||
def test_post_change_with_confirm_change(self):
|
def test_post_change_with_confirm_change(self):
|
||||||
item = ItemFactory(name="item")
|
item = ItemFactory(name="item")
|
||||||
data = {
|
data = {
|
||||||
|
|
@ -100,7 +133,9 @@ class TestConfirmChangeAndAdd(ConfirmAdminTestCase):
|
||||||
self._assertSimpleFieldFormHtml(
|
self._assertSimpleFieldFormHtml(
|
||||||
rendered_content=response.rendered_content, fields=form_data
|
rendered_content=response.rendered_content, fields=form_data
|
||||||
)
|
)
|
||||||
self._assertSubmitHtml(rendered_content=response.rendered_content)
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content, multipart_form=True
|
||||||
|
)
|
||||||
|
|
||||||
# Hasn't changed item yet
|
# Hasn't changed item yet
|
||||||
item.refresh_from_db()
|
item.refresh_from_db()
|
||||||
|
|
@ -120,9 +155,22 @@ class TestConfirmChangeAndAdd(ConfirmAdminTestCase):
|
||||||
def test_get_confirmation_fields_should_default_if_not_set(self):
|
def test_get_confirmation_fields_should_default_if_not_set(self):
|
||||||
expected_fields = [f.name for f in Item._meta.fields if f.name != "id"]
|
expected_fields = [f.name for f in Item._meta.fields if f.name != "id"]
|
||||||
ItemAdmin.confirmation_fields = None
|
ItemAdmin.confirmation_fields = None
|
||||||
|
ItemAdmin.fields = expected_fields
|
||||||
admin = ItemAdmin(Item, AdminSite())
|
admin = ItemAdmin(Item, AdminSite())
|
||||||
actual_fields = admin.get_confirmation_fields(self.factory.request())
|
actual_fields = admin.get_confirmation_fields(self.factory.request())
|
||||||
self.assertEqual(expected_fields, actual_fields)
|
for field in expected_fields:
|
||||||
|
self.assertIn(field, actual_fields)
|
||||||
|
|
||||||
|
def test_get_confirmation_fields_default_should_only_include_fields_shown_on_admin(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
admin_fields = ["name", "price"]
|
||||||
|
ItemAdmin.confirmation_fields = None
|
||||||
|
ItemAdmin.fields = admin_fields
|
||||||
|
admin = ItemAdmin(Item, AdminSite())
|
||||||
|
actual_fields = admin.get_confirmation_fields(self.factory.request())
|
||||||
|
for field in admin_fields:
|
||||||
|
self.assertIn(field, actual_fields)
|
||||||
|
|
||||||
def test_get_confirmation_fields_if_set(self):
|
def test_get_confirmation_fields_if_set(self):
|
||||||
expected_fields = ["name", "currency"]
|
expected_fields = ["name", "currency"]
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
|
from unittest import mock
|
||||||
|
from admin_confirm.admin import AdminConfirmMixin
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from admin_confirm.tests.helpers import ConfirmAdminTestCase
|
from admin_confirm.tests.helpers import AdminConfirmTestCase
|
||||||
from tests.market.admin import ShoppingMallAdmin
|
from tests.market.admin import ShoppingMallAdmin
|
||||||
from tests.market.models import ShoppingMall
|
from tests.market.models import ShoppingMall
|
||||||
from tests.factories import ShopFactory
|
from tests.factories import ShopFactory
|
||||||
|
|
||||||
|
|
||||||
class TestConfirmChangeAndAddM2MField(ConfirmAdminTestCase):
|
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
|
||||||
|
class TestConfirmChangeAndAddM2MField(AdminConfirmTestCase):
|
||||||
def test_post_add_without_confirm_add_m2m(self):
|
def test_post_add_without_confirm_add_m2m(self):
|
||||||
shops = [ShopFactory() for i in range(3)]
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
|
||||||
|
|
@ -84,6 +87,9 @@ class TestConfirmChangeAndAddM2MField(ConfirmAdminTestCase):
|
||||||
]
|
]
|
||||||
self.assertEqual(response.template_name, expected_templates)
|
self.assertEqual(response.template_name, expected_templates)
|
||||||
|
|
||||||
|
# Should show two lists for the m2m current and modified values
|
||||||
|
self.assertEqual(response.rendered_content.count("<ul>"), 2)
|
||||||
|
|
||||||
self._assertManyToManyFormHtml(
|
self._assertManyToManyFormHtml(
|
||||||
rendered_content=response.rendered_content,
|
rendered_content=response.rendered_content,
|
||||||
options=shops,
|
options=shops,
|
||||||
|
|
@ -0,0 +1,477 @@
|
||||||
|
from unittest import mock
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from admin_confirm.tests.helpers import AdminConfirmTestCase
|
||||||
|
from tests.market.admin import ItemAdmin, ShoppingMallAdmin
|
||||||
|
from tests.market.models import GeneralManager, Item, ShoppingMall, Town
|
||||||
|
from tests.factories import ItemFactory, ShopFactory
|
||||||
|
|
||||||
|
from admin_confirm.constants import CACHE_KEYS, CONFIRMATION_RECEIVED
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
|
||||||
|
class TestConfirmSaveActions(AdminConfirmTestCase):
|
||||||
|
def test_simple_add_with_save(self):
|
||||||
|
# Load the Add Item Page
|
||||||
|
ItemAdmin.confirm_add = True
|
||||||
|
response = self.client.get(reverse("admin:market_item_add"))
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_add"))
|
||||||
|
self.assertIn("_confirm_add", response.rendered_content)
|
||||||
|
|
||||||
|
# Click "Save"
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(reverse("admin:market_item_add"), data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
save_action="_save",
|
||||||
|
multipart_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have cached the unsaved item
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNotNone(cached_item)
|
||||||
|
self.assertIsNone(cached_item.id)
|
||||||
|
self.assertEqual(cached_item.name, data["name"])
|
||||||
|
self.assertEqual(cached_item.price, data["price"])
|
||||||
|
self.assertEqual(cached_item.currency, data["currency"])
|
||||||
|
|
||||||
|
# Should not have saved the item yet
|
||||||
|
self.assertEqual(Item.objects.count(), 0)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_add"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = self.client.post(reverse("admin:market_item_add"), data=data)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/market/item/")
|
||||||
|
|
||||||
|
# Should have saved item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
saved_item = Item.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.price, data["price"])
|
||||||
|
self.assertEqual(saved_item.currency, data["currency"])
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_simple_change_with_continue(self):
|
||||||
|
item = ItemFactory(name="Not name")
|
||||||
|
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.confirm_change = True
|
||||||
|
response = self.client.get(f"/admin/market/item/{item.id}/change/")
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_change"))
|
||||||
|
self.assertIn("_confirm_change", response.rendered_content)
|
||||||
|
|
||||||
|
# Click "Save And Continue"
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
save_action="_continue",
|
||||||
|
multipart_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have cached the unsaved item
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNotNone(cached_item)
|
||||||
|
self.assertIsNone(cached_item.id)
|
||||||
|
self.assertEqual(cached_item.name, data["name"])
|
||||||
|
self.assertEqual(cached_item.price, data["price"])
|
||||||
|
self.assertEqual(cached_item.currency, data["currency"])
|
||||||
|
|
||||||
|
# Should not have saved the changes yet
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/{item.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
saved_item = Item.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.price, data["price"])
|
||||||
|
self.assertEqual(saved_item.currency, data["currency"])
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_file_and_image_add_addanother(self):
|
||||||
|
# Load the Add Item Page
|
||||||
|
ItemAdmin.confirm_add = True
|
||||||
|
response = self.client.get(reverse("admin:market_item_add"))
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_add"))
|
||||||
|
self.assertIn("_confirm_add", response.rendered_content)
|
||||||
|
|
||||||
|
# Select files
|
||||||
|
image_path = "screenshot.png"
|
||||||
|
f = SimpleUploadedFile(
|
||||||
|
name="test_file.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
i = SimpleUploadedFile(
|
||||||
|
name="test_image.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Click "Save"
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"file": f,
|
||||||
|
"image": i,
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_addanother": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(reverse("admin:market_item_add"), data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
save_action="_addanother",
|
||||||
|
multipart_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have cached the unsaved item
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNotNone(cached_item)
|
||||||
|
self.assertIsNone(cached_item.id)
|
||||||
|
self.assertEqual(cached_item.name, data["name"])
|
||||||
|
self.assertEqual(cached_item.price, data["price"])
|
||||||
|
self.assertEqual(cached_item.currency, data["currency"])
|
||||||
|
self.assertEqual(cached_item.file, data["file"])
|
||||||
|
self.assertEqual(cached_item.image, data["image"])
|
||||||
|
|
||||||
|
# Should not have saved the item yet
|
||||||
|
self.assertEqual(Item.objects.count(), 0)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
confirmation_data = data.copy()
|
||||||
|
del confirmation_data["_confirm_add"]
|
||||||
|
del confirmation_data["image"]
|
||||||
|
del confirmation_data["file"]
|
||||||
|
confirmation_data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("admin:market_item_add"), data=confirmation_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
# Should show add page since "add another" was selected
|
||||||
|
self.assertEqual(response.url, "/admin/market/item/add/")
|
||||||
|
|
||||||
|
# Should have saved item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
saved_item = Item.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.price, data["price"])
|
||||||
|
self.assertEqual(saved_item.currency, data["currency"])
|
||||||
|
self.assertEqual(saved_item.file, data["file"])
|
||||||
|
self.assertEqual(saved_item.image, data["image"])
|
||||||
|
|
||||||
|
self.assertEqual(saved_item.file.name, "test_file.jpg")
|
||||||
|
self.assertEqual(saved_item.image.name, "test_image.jpg")
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_file_and_image_change_with_saveasnew(self):
|
||||||
|
item = ItemFactory(name="Not name")
|
||||||
|
# Select files
|
||||||
|
image_path = "screenshot.png"
|
||||||
|
f = SimpleUploadedFile(
|
||||||
|
name="test_file.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
i = SimpleUploadedFile(
|
||||||
|
name="test_image.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
item.file = f
|
||||||
|
item.image = i
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.confirm_change = True
|
||||||
|
ItemAdmin.fields = ["name", "price", "file", "image", "currency"]
|
||||||
|
ItemAdmin.save_as = True
|
||||||
|
ItemAdmin.save_as_continue = True
|
||||||
|
response = self.client.get(f"/admin/market/item/{item.id}/change/")
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_change"))
|
||||||
|
self.assertIn("_confirm_change", response.rendered_content)
|
||||||
|
|
||||||
|
# Upload new image and remove file
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="test_image2.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Click "Save And Continue"
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"image": i2,
|
||||||
|
"file": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_saveasnew": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
save_action="_saveasnew",
|
||||||
|
multipart_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have cached the unsaved item
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNotNone(cached_item)
|
||||||
|
self.assertIsNone(cached_item.id)
|
||||||
|
self.assertEqual(cached_item.name, data["name"])
|
||||||
|
self.assertEqual(cached_item.price, data["price"])
|
||||||
|
self.assertEqual(cached_item.currency, data["currency"])
|
||||||
|
self.assertFalse(cached_item.file.name)
|
||||||
|
self.assertEqual(cached_item.image, i2)
|
||||||
|
|
||||||
|
# Should not have saved the changes yet
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertIsNotNone(item.file)
|
||||||
|
self.assertIsNotNone(item.image)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data["image"] = ""
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/{item.id + 1}/change/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertEqual(item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
self.assertFalse(new_item.file)
|
||||||
|
self.assertEqual(new_item.image, i2)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_relations_add(self):
|
||||||
|
gm = GeneralManager.objects.create(name="gm")
|
||||||
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
town = Town.objects.create(name="town")
|
||||||
|
|
||||||
|
# Load the Add ShoppingMall Page
|
||||||
|
ShoppingMallAdmin.confirm_add = True
|
||||||
|
response = self.client.get(reverse("admin:market_shoppingmall_add"))
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_add"))
|
||||||
|
self.assertIn("_confirm_add", response.rendered_content)
|
||||||
|
|
||||||
|
# Click "Save"
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"shops": [s.id for s in shops],
|
||||||
|
"general_manager": gm.id,
|
||||||
|
"town": town.id,
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(reverse("admin:market_shoppingmall_add"), data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertManyToManyFormHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
options=shops,
|
||||||
|
selected_ids=data["shops"],
|
||||||
|
)
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content, save_action="_save"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have cached the unsaved object
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNone(cached_item)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
confirmation_received_data = data
|
||||||
|
del confirmation_received_data["_confirm_add"]
|
||||||
|
confirmation_received_data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("admin:market_shoppingmall_add"), data=confirmation_received_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/market/shoppingmall/")
|
||||||
|
|
||||||
|
# Should have saved object
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
saved_item = ShoppingMall.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.general_manager, gm)
|
||||||
|
self.assertEqual(saved_item.town, town)
|
||||||
|
for shop in saved_item.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_relation_change_with_saveasnew(self):
|
||||||
|
gm = GeneralManager.objects.create(name="gm")
|
||||||
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
town = Town.objects.create(name="town")
|
||||||
|
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
|
||||||
|
mall.shops.set(shops)
|
||||||
|
|
||||||
|
# new values
|
||||||
|
gm2 = GeneralManager.objects.create(name="gm2")
|
||||||
|
shops2 = [ShopFactory() for i in range(3)]
|
||||||
|
town2 = Town.objects.create(name="town2")
|
||||||
|
|
||||||
|
# Load the Change ShoppingMall Page
|
||||||
|
ShoppingMallAdmin.confirm_change = True
|
||||||
|
response = self.client.get(f"/admin/market/shoppingmall/{mall.id}/change/")
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_change"))
|
||||||
|
self.assertIn("_confirm_change", response.rendered_content)
|
||||||
|
|
||||||
|
# Click "Save"
|
||||||
|
data = {
|
||||||
|
"id": mall.id,
|
||||||
|
"name": "name",
|
||||||
|
"shops": [s.id for s in shops2],
|
||||||
|
"general_manager": gm2.id,
|
||||||
|
"town": town2.id,
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_saveasnew": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertManyToManyFormHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
options=shops,
|
||||||
|
selected_ids=data["shops"],
|
||||||
|
)
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content, save_action="_saveasnew"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have cached the unsaved obj
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNone(cached_item)
|
||||||
|
|
||||||
|
# Should not have saved changes yet
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
mall.refresh_from_db()
|
||||||
|
self.assertEqual(mall.name, "mall")
|
||||||
|
self.assertEqual(mall.general_manager, gm)
|
||||||
|
self.assertEqual(mall.town, town)
|
||||||
|
for shop in mall.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
confirmation_received_data = data.copy()
|
||||||
|
del confirmation_received_data["_confirm_change"]
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/",
|
||||||
|
data=confirmation_received_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(
|
||||||
|
response.url, f"/admin/market/shoppingmall/{mall.id + 1}/change/"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have saved obj
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 2)
|
||||||
|
# Should not have changed old obj
|
||||||
|
mall.refresh_from_db()
|
||||||
|
self.assertEqual(mall.name, "mall")
|
||||||
|
self.assertEqual(mall.general_manager, gm)
|
||||||
|
self.assertEqual(mall.town, town)
|
||||||
|
for shop in mall.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Should have created new obj
|
||||||
|
saved_item = ShoppingMall.objects.filter(id=mall.id + 1).first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.general_manager, gm2)
|
||||||
|
self.assertEqual(saved_item.town, town2)
|
||||||
|
|
||||||
|
for shop in saved_item.shops.all():
|
||||||
|
self.assertIn(shop, shops2)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
@ -0,0 +1,458 @@
|
||||||
|
from unittest import mock
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from admin_confirm.tests.helpers import AdminConfirmTestCase
|
||||||
|
from tests.market.admin import ItemAdmin, ShoppingMallAdmin
|
||||||
|
from tests.market.models import GeneralManager, Item, ShoppingMall, Town
|
||||||
|
from tests.factories import ItemFactory, ShopFactory
|
||||||
|
|
||||||
|
from admin_confirm.constants import CACHE_KEYS, CONFIRMATION_RECEIVED
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(ShoppingMallAdmin, "inlines", [])
|
||||||
|
class TestConfirmationCache(AdminConfirmTestCase):
|
||||||
|
def test_simple_add(self):
|
||||||
|
# Load the Add Item Page
|
||||||
|
ItemAdmin.confirm_add = True
|
||||||
|
response = self.client.get(reverse("admin:market_item_add"))
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_add"))
|
||||||
|
self.assertIn("_confirm_add", response.rendered_content)
|
||||||
|
|
||||||
|
# Click "Save"
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(reverse("admin:market_item_add"), data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
save_action="_save",
|
||||||
|
multipart_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have cached the unsaved item
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNotNone(cached_item)
|
||||||
|
self.assertIsNone(cached_item.id)
|
||||||
|
self.assertEqual(cached_item.name, data["name"])
|
||||||
|
self.assertEqual(cached_item.price, data["price"])
|
||||||
|
self.assertEqual(cached_item.currency, data["currency"])
|
||||||
|
|
||||||
|
# Should not have saved the item yet
|
||||||
|
self.assertEqual(Item.objects.count(), 0)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_add"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = self.client.post(reverse("admin:market_item_add"), data=data)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/market/item/")
|
||||||
|
|
||||||
|
# Should have saved item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
saved_item = Item.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.price, data["price"])
|
||||||
|
self.assertEqual(saved_item.currency, data["currency"])
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_simple_change(self):
|
||||||
|
item = ItemFactory(name="Not name")
|
||||||
|
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.confirm_change = True
|
||||||
|
response = self.client.get(f"/admin/market/item/{item.id}/change/")
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_change"))
|
||||||
|
self.assertIn("_confirm_change", response.rendered_content)
|
||||||
|
|
||||||
|
# Click "Save And Continue"
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
save_action="_continue",
|
||||||
|
multipart_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have cached the unsaved item
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNotNone(cached_item)
|
||||||
|
self.assertIsNone(cached_item.id)
|
||||||
|
self.assertEqual(cached_item.name, data["name"])
|
||||||
|
self.assertEqual(cached_item.price, data["price"])
|
||||||
|
self.assertEqual(cached_item.currency, data["currency"])
|
||||||
|
|
||||||
|
# Should not have saved the changes yet
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/{item.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
saved_item = Item.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.price, data["price"])
|
||||||
|
self.assertEqual(saved_item.currency, data["currency"])
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_file_and_image_add(self):
|
||||||
|
# Load the Add Item Page
|
||||||
|
ItemAdmin.confirm_add = True
|
||||||
|
response = self.client.get(reverse("admin:market_item_add"))
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_add"))
|
||||||
|
self.assertIn("_confirm_add", response.rendered_content)
|
||||||
|
|
||||||
|
# Select files
|
||||||
|
image_path = "screenshot.png"
|
||||||
|
f = SimpleUploadedFile(
|
||||||
|
name="test_file.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
i = SimpleUploadedFile(
|
||||||
|
name="test_image.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Click "Save"
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"file": f,
|
||||||
|
"image": i,
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(reverse("admin:market_item_add"), data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
save_action="_save",
|
||||||
|
multipart_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have cached the unsaved item
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNotNone(cached_item)
|
||||||
|
self.assertIsNone(cached_item.id)
|
||||||
|
self.assertEqual(cached_item.name, data["name"])
|
||||||
|
self.assertEqual(cached_item.price, data["price"])
|
||||||
|
self.assertEqual(cached_item.currency, data["currency"])
|
||||||
|
self.assertEqual(cached_item.file, data["file"])
|
||||||
|
self.assertEqual(cached_item.image, data["image"])
|
||||||
|
|
||||||
|
# Should not have saved the item yet
|
||||||
|
self.assertEqual(Item.objects.count(), 0)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
confirmation_data = data.copy()
|
||||||
|
del confirmation_data["_confirm_add"]
|
||||||
|
del confirmation_data["image"]
|
||||||
|
del confirmation_data["file"]
|
||||||
|
confirmation_data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("admin:market_item_add"), data=confirmation_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/market/item/")
|
||||||
|
|
||||||
|
# Should have saved item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
saved_item = Item.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.price, data["price"])
|
||||||
|
self.assertEqual(saved_item.currency, data["currency"])
|
||||||
|
self.assertEqual(saved_item.file, data["file"])
|
||||||
|
self.assertEqual(saved_item.image, data["image"])
|
||||||
|
|
||||||
|
self.assertEqual(saved_item.file.name, "test_file.jpg")
|
||||||
|
self.assertEqual(saved_item.image.name, "test_image.jpg")
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_file_and_image_change(self):
|
||||||
|
item = ItemFactory(name="Not name")
|
||||||
|
# Select files
|
||||||
|
image_path = "screenshot.png"
|
||||||
|
f = SimpleUploadedFile(
|
||||||
|
name="test_file.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
i = SimpleUploadedFile(
|
||||||
|
name="test_image.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
item.file = f
|
||||||
|
item.image = i
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.confirm_change = True
|
||||||
|
ItemAdmin.fields = ["name", "price", "file", "image", "currency"]
|
||||||
|
response = self.client.get(f"/admin/market/item/{item.id}/change/")
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_change"))
|
||||||
|
self.assertIn("_confirm_change", response.rendered_content)
|
||||||
|
|
||||||
|
# Upload new image and remove file
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="test_image2.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Click "Save And Continue"
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"image": i2,
|
||||||
|
"file": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
save_action="_continue",
|
||||||
|
multipart_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have cached the unsaved item
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNotNone(cached_item)
|
||||||
|
self.assertIsNone(cached_item.id)
|
||||||
|
self.assertEqual(cached_item.name, data["name"])
|
||||||
|
self.assertEqual(cached_item.price, data["price"])
|
||||||
|
self.assertEqual(cached_item.currency, data["currency"])
|
||||||
|
self.assertFalse(cached_item.file.name)
|
||||||
|
self.assertEqual(cached_item.image, i2)
|
||||||
|
|
||||||
|
# Should not have saved the changes yet
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertIsNotNone(item.file)
|
||||||
|
self.assertIsNotNone(item.image)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data["image"] = ""
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/{item.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
saved_item = Item.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.price, data["price"])
|
||||||
|
self.assertEqual(saved_item.currency, data["currency"])
|
||||||
|
self.assertFalse(saved_item.file)
|
||||||
|
self.assertEqual(saved_item.image, i2)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_relations_add(self):
|
||||||
|
gm = GeneralManager.objects.create(name="gm")
|
||||||
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
town = Town.objects.create(name="town")
|
||||||
|
|
||||||
|
# Load the Add ShoppingMall Page
|
||||||
|
ShoppingMallAdmin.confirm_add = True
|
||||||
|
response = self.client.get(reverse("admin:market_shoppingmall_add"))
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_add"))
|
||||||
|
self.assertIn("_confirm_add", response.rendered_content)
|
||||||
|
|
||||||
|
# Click "Save"
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"shops": [s.id for s in shops],
|
||||||
|
"general_manager": gm.id,
|
||||||
|
"town": town.id,
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(reverse("admin:market_shoppingmall_add"), data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertManyToManyFormHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
options=shops,
|
||||||
|
selected_ids=data["shops"],
|
||||||
|
)
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content, save_action="_save"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have cached the unsaved object
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNone(cached_item)
|
||||||
|
|
||||||
|
# Should not have saved the object yet
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 0)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
confirmation_received_data = data
|
||||||
|
del confirmation_received_data["_confirm_add"]
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("admin:market_shoppingmall_add"), data=confirmation_received_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/admin/market/shoppingmall/")
|
||||||
|
|
||||||
|
# Should have saved object
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
saved_item = ShoppingMall.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.general_manager, gm)
|
||||||
|
self.assertEqual(saved_item.town, town)
|
||||||
|
for shop in saved_item.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_relation_change(self):
|
||||||
|
gm = GeneralManager.objects.create(name="gm")
|
||||||
|
shops = [ShopFactory() for i in range(3)]
|
||||||
|
town = Town.objects.create(name="town")
|
||||||
|
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
|
||||||
|
mall.shops.set(shops)
|
||||||
|
|
||||||
|
# new values
|
||||||
|
gm2 = GeneralManager.objects.create(name="gm2")
|
||||||
|
shops2 = [ShopFactory() for i in range(3)]
|
||||||
|
town2 = Town.objects.create(name="town2")
|
||||||
|
|
||||||
|
# Load the Change ShoppingMall Page
|
||||||
|
ShoppingMallAdmin.confirm_change = True
|
||||||
|
response = self.client.get(f"/admin/market/shoppingmall/{mall.id}/change/")
|
||||||
|
|
||||||
|
# Should be asked for confirmation
|
||||||
|
self.assertTrue(response.context_data.get("confirm_change"))
|
||||||
|
self.assertIn("_confirm_change", response.rendered_content)
|
||||||
|
|
||||||
|
# Click "Save"
|
||||||
|
data = {
|
||||||
|
"id": mall.id,
|
||||||
|
"name": "name",
|
||||||
|
"shops": [s.id for s in shops2],
|
||||||
|
"general_manager": gm2.id,
|
||||||
|
"town": town2.id,
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/", data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertManyToManyFormHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
options=shops,
|
||||||
|
selected_ids=data["shops"],
|
||||||
|
)
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content, save_action="_continue"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have cached the unsaved obj
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNone(cached_item)
|
||||||
|
|
||||||
|
# Should not have saved changes yet
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
mall.refresh_from_db()
|
||||||
|
self.assertEqual(mall.name, "mall")
|
||||||
|
self.assertEqual(mall.general_manager, gm)
|
||||||
|
self.assertEqual(mall.town, town)
|
||||||
|
for shop in mall.shops.all():
|
||||||
|
self.assertIn(shop, shops)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
confirmation_received_data = data
|
||||||
|
del confirmation_received_data["_confirm_change"]
|
||||||
|
confirmation_received_data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/shoppingmall/{mall.id}/change/",
|
||||||
|
data=confirmation_received_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/shoppingmall/{mall.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved obj
|
||||||
|
self.assertEqual(ShoppingMall.objects.count(), 1)
|
||||||
|
saved_item = ShoppingMall.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.general_manager, gm2)
|
||||||
|
self.assertEqual(saved_item.town, town2)
|
||||||
|
|
||||||
|
for shop in saved_item.shops.all():
|
||||||
|
self.assertIn(shop, shops2)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
"""
|
||||||
|
Tests ModelAdmin with fieldsets custom configured through one of the possible methods
|
||||||
|
Ensures that AdminConfirmMixin works correctly when implimenting class alters default fieldsets
|
||||||
|
|
||||||
|
Test Matrix
|
||||||
|
method: `.fieldsets =`, `def get_fieldsets()`
|
||||||
|
action: change, add
|
||||||
|
fieldset: simple, with readonly fields, with custom fields
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from importlib import reload
|
||||||
|
from tests.market.admin import item_admin
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.admin import AdminSite
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
|
||||||
|
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from admin_confirm.constants import CACHE_KEYS, CONFIRM_CHANGE, CONFIRMATION_RECEIVED
|
||||||
|
|
||||||
|
from tests.market.models import Item
|
||||||
|
from tests.factories import ItemFactory
|
||||||
|
|
||||||
|
|
||||||
|
def fs_simple(admin):
|
||||||
|
return (
|
||||||
|
(None, {"fields": ("name", "price", "image")}),
|
||||||
|
(
|
||||||
|
"Advanced options",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": ("currency", "file"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fs_w_readonly(admin):
|
||||||
|
admin.readonly_fields = ["description", "image"]
|
||||||
|
return fs_simple(admin)
|
||||||
|
|
||||||
|
|
||||||
|
def fs_w_custom(admin):
|
||||||
|
admin.one = lambda self, obj: "ReadOnly"
|
||||||
|
admin.two = lambda self, obj: "ReadOnly"
|
||||||
|
admin.three = lambda self, obj: "ReadOnly"
|
||||||
|
admin.readonly_fields = ["one", "two", "three"]
|
||||||
|
return (
|
||||||
|
(None, {"fields": ("name", "price", "image", "one")}),
|
||||||
|
(
|
||||||
|
"Advanced options",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": ("currency", "two", "file"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("More Info", {"fields": ("three", "description")}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_fieldsets(admin, fieldset):
|
||||||
|
admin.fieldsets = fieldset
|
||||||
|
|
||||||
|
|
||||||
|
def override_get_fieldsets(admin, fieldset):
|
||||||
|
admin.get_fieldsets = lambda self, request, obj=None: fieldset
|
||||||
|
|
||||||
|
|
||||||
|
methods = [set_fieldsets, override_get_fieldsets]
|
||||||
|
actions = ["_confirm_add", "_confirm_change"]
|
||||||
|
fieldsets = [fs_simple, fs_w_readonly, fs_w_custom]
|
||||||
|
|
||||||
|
param_matrix = []
|
||||||
|
for method in methods:
|
||||||
|
for fieldset in fieldsets:
|
||||||
|
for action in actions:
|
||||||
|
param_matrix.append((method, fieldset, action))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db()
|
||||||
|
@pytest.mark.parametrize("method,get_fieldset,action", param_matrix)
|
||||||
|
def test_fieldsets(client, method, get_fieldset, action):
|
||||||
|
reload(item_admin)
|
||||||
|
|
||||||
|
admin = item_admin.ItemAdmin
|
||||||
|
fs = get_fieldset(admin)
|
||||||
|
# set fieldsets via one of the methods
|
||||||
|
method(admin, fs)
|
||||||
|
|
||||||
|
admin_instance = admin(admin_site=AdminSite(), model=Item)
|
||||||
|
request = RequestFactory().request
|
||||||
|
assert admin_instance.get_fieldsets(request) == fs
|
||||||
|
|
||||||
|
user = User.objects.create_superuser(
|
||||||
|
username="super", email="super@email.org", password="pass"
|
||||||
|
)
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
url = "/admin/market/item/add/"
|
||||||
|
image_path = "screenshot.png"
|
||||||
|
f2 = SimpleUploadedFile(
|
||||||
|
name="new_file.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="new_image.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
"name": "new name",
|
||||||
|
"price": 2,
|
||||||
|
"currency": "USD",
|
||||||
|
"image": i2,
|
||||||
|
"file": f2,
|
||||||
|
action: True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
for f in admin.readonly_fields:
|
||||||
|
if f in data.keys():
|
||||||
|
del data[f]
|
||||||
|
if action == CONFIRM_CHANGE:
|
||||||
|
url = "/admin/market/item/1/change/"
|
||||||
|
f = SimpleUploadedFile(
|
||||||
|
name="old_file.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
i = SimpleUploadedFile(
|
||||||
|
name="old_image.jpg",
|
||||||
|
content=open(image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
item = ItemFactory(name="old name", price=1, currency="CAD", file=f, image=i)
|
||||||
|
data["id"] = item.id
|
||||||
|
|
||||||
|
cache_item = Item()
|
||||||
|
for f in ["name", "price", "currency", "image", "file"]:
|
||||||
|
if f not in admin.readonly_fields:
|
||||||
|
setattr(cache_item, f, data[f])
|
||||||
|
|
||||||
|
cache.set(CACHE_KEYS["object"], cache_item)
|
||||||
|
cache.set(CACHE_KEYS["post"], data)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data[action]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = client.post(url, data=data)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
# assert response.status_code == 302
|
||||||
|
assert response.url == "/admin/market/item/"
|
||||||
|
|
||||||
|
# Should have saved item
|
||||||
|
assert Item.objects.count() == 1
|
||||||
|
saved_item = Item.objects.all().first()
|
||||||
|
for f in ["name", "price", "currency"]:
|
||||||
|
if f not in admin.readonly_fields:
|
||||||
|
assert getattr(saved_item, f) == data[f]
|
||||||
|
if "file" not in admin.readonly_fields:
|
||||||
|
assert "new_file" in saved_item.file.name
|
||||||
|
if "image" not in admin.readonly_fields:
|
||||||
|
assert "new_image" in saved_item.image.name
|
||||||
|
|
||||||
|
reload(item_admin)
|
||||||
|
|
@ -0,0 +1,967 @@
|
||||||
|
"""
|
||||||
|
Ensure that files are saved during confirmation
|
||||||
|
Without file changes, Django is relied on
|
||||||
|
|
||||||
|
With file changes, we cache the object, save it with
|
||||||
|
the files if new, or add files to existing obj and save
|
||||||
|
|
||||||
|
Then send the rest of the changes to Django to handle
|
||||||
|
|
||||||
|
This is arguably the most we fiddle with the Django request
|
||||||
|
Thus we should test it extensively
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from admin_confirm.tests.helpers import AdminConfirmTestCase
|
||||||
|
from admin_confirm.constants import CACHE_KEYS, CONFIRMATION_RECEIVED
|
||||||
|
|
||||||
|
from tests.market.admin import ItemAdmin
|
||||||
|
from tests.market.models import Item, Shop
|
||||||
|
from tests.factories import ItemFactory, ShopFactory
|
||||||
|
|
||||||
|
|
||||||
|
class TestFileCache(AdminConfirmTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.confirm_change = True
|
||||||
|
ItemAdmin.fields = ["name", "price", "file", "image", "currency"]
|
||||||
|
ItemAdmin.save_as = True
|
||||||
|
ItemAdmin.save_as_continue = True
|
||||||
|
|
||||||
|
self.image_path = "screenshot.png"
|
||||||
|
f = SimpleUploadedFile(
|
||||||
|
name="test_file.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
i = SimpleUploadedFile(
|
||||||
|
name="test_image.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
self.item = ItemFactory(name="Not name", file=f, image=i)
|
||||||
|
|
||||||
|
return super().setUp()
|
||||||
|
|
||||||
|
def test_save_as_continue_true_should_not_redirect_to_changelist(self):
|
||||||
|
item = self.item
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.save_as_continue = True
|
||||||
|
|
||||||
|
# Upload new image and remove file
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="test_image2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"file": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_saveasnew": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"],
|
||||||
|
price=data["price"],
|
||||||
|
currency=data["currency"],
|
||||||
|
image=i2,
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.set(CACHE_KEYS["object"], cache_item)
|
||||||
|
cache.set(CACHE_KEYS["post"], data)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/item/{self.item.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/{self.item.id + 1}/change/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertEqual(item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
self.assertFalse(new_item.file)
|
||||||
|
self.assertEqual(new_item.image.name.count("test_image2"), 1)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_save_as_continue_false_should_redirect_to_changelist(self):
|
||||||
|
item = self.item
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.save_as_continue = False
|
||||||
|
|
||||||
|
# Upload new image and remove file
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="test_image2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"file": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_saveasnew": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"],
|
||||||
|
price=data["price"],
|
||||||
|
currency=data["currency"],
|
||||||
|
image=i2,
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.set(CACHE_KEYS["object"], cache_item)
|
||||||
|
cache.set(CACHE_KEYS["post"], data)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/item/{self.item.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertEqual(item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
self.assertFalse(new_item.file)
|
||||||
|
self.assertEqual(new_item.image.name.count("test_image2"), 1)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_saveasnew_without_any_file_changes_should_save_new_instance_without_files(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
item = self.item
|
||||||
|
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"file": "",
|
||||||
|
"image": "",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_saveasnew": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"],
|
||||||
|
price=data["price"],
|
||||||
|
currency=data["currency"],
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.set(CACHE_KEYS["object"], cache_item)
|
||||||
|
cache.set(CACHE_KEYS["post"], data)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/item/{self.item.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/{self.item.id + 1}/change/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertEqual(item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
# In Django (by default), the save as new does not transfer over the files
|
||||||
|
self.assertFalse(new_item.file)
|
||||||
|
self.assertFalse(new_item.image)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_add_with_upload_file_should_save_new_instance_with_files(self):
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"image": "",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload new file
|
||||||
|
f2 = SimpleUploadedFile(
|
||||||
|
name="test_file2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"], price=data["price"], currency=data["currency"], file=f2
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.set(CACHE_KEYS["object"], cache_item)
|
||||||
|
cache.set(CACHE_KEYS["post"], data)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_add"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(f"/admin/market/item/add/", data=data)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
self.item.refresh_from_db()
|
||||||
|
self.assertEqual(self.item.name, "Not name")
|
||||||
|
self.assertEqual(self.item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(self.item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(self.item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=self.item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
self.assertIn("test_file2", new_item.file.name)
|
||||||
|
self.assertFalse(new_item.image)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_add_without_cached_post_should_save_new_instance_with_file(self):
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"image": "",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload new file
|
||||||
|
f2 = SimpleUploadedFile(
|
||||||
|
name="test_file2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"], price=data["price"], currency=data["currency"], file=f2
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.set(CACHE_KEYS["object"], cache_item)
|
||||||
|
# Make sure there's no post cached post
|
||||||
|
cache.delete(CACHE_KEYS["post"])
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_add"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(f"/admin/market/item/add/", data=data)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
self.item.refresh_from_db()
|
||||||
|
self.assertEqual(self.item.name, "Not name")
|
||||||
|
self.assertEqual(self.item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(self.item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(self.item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=self.item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
self.assertFalse(new_item.image)
|
||||||
|
|
||||||
|
# Able to save the cached file since cached object was there even though cached post was not
|
||||||
|
self.assertIn("test_file2", new_item.file.name)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_add_without_cached_object_should_save_new_instance_but_not_have_file(self):
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"image": "",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload new file
|
||||||
|
f2 = SimpleUploadedFile(
|
||||||
|
name="test_file2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"], price=data["price"], currency=data["currency"], file=f2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure there's no post cached obj
|
||||||
|
cache.delete(CACHE_KEYS["object"])
|
||||||
|
cache.set(CACHE_KEYS["post"], data)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_add"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(f"/admin/market/item/add/", data=data)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
self.item.refresh_from_db()
|
||||||
|
self.assertEqual(self.item.name, "Not name")
|
||||||
|
self.assertEqual(self.item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(self.item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(self.item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=self.item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
self.assertFalse(new_item.image)
|
||||||
|
|
||||||
|
# FAILED to save the file, because cached item was not there
|
||||||
|
self.assertFalse(new_item.file)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_add_without_any_cache_should_save_new_instance_but_not_have_file(self):
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"image": "",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_add": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload new file
|
||||||
|
f2 = SimpleUploadedFile(
|
||||||
|
name="test_file2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"], price=data["price"], currency=data["currency"], file=f2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure there's no cache
|
||||||
|
cache.delete(CACHE_KEYS["object"])
|
||||||
|
cache.delete(CACHE_KEYS["post"])
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_add"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(f"/admin/market/item/add/", data=data)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
self.item.refresh_from_db()
|
||||||
|
self.assertEqual(self.item.name, "Not name")
|
||||||
|
self.assertEqual(self.item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(self.item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(self.item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=self.item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
self.assertFalse(new_item.image)
|
||||||
|
|
||||||
|
# FAILED to save the file, because cached item was not there
|
||||||
|
self.assertFalse(new_item.file)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_change_without_cached_post_should_save_file_changes(self):
|
||||||
|
item = self.item
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.save_as_continue = False
|
||||||
|
|
||||||
|
# Upload new image and remove file
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="test_image2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"image": i2,
|
||||||
|
"file": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_saveasnew": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"],
|
||||||
|
price=data["price"],
|
||||||
|
currency=data["currency"],
|
||||||
|
image=i2,
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.set(CACHE_KEYS["object"], cache_item)
|
||||||
|
# Ensure no cached post
|
||||||
|
cache.delete(CACHE_KEYS["post"])
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
# Image would have been in FILES and not in POST
|
||||||
|
del data["image"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/item/{self.item.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertEqual(item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
# Should have cleared `file` since clear was selected
|
||||||
|
self.assertFalse(new_item.file)
|
||||||
|
# Saved cached file from cached obj even if cached post was missing
|
||||||
|
self.assertIn("test_image2", new_item.image.name)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_change_without_cached_object_should_save_but_without_file_changes(self):
|
||||||
|
item = self.item
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.save_as_continue = False
|
||||||
|
|
||||||
|
# Upload new image and remove file
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="test_image2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"file": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_saveasnew": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"],
|
||||||
|
price=data["price"],
|
||||||
|
currency=data["currency"],
|
||||||
|
image=i2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure no cached obj
|
||||||
|
cache.delete(CACHE_KEYS["object"])
|
||||||
|
cache.set(CACHE_KEYS["post"], data)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/item/{self.item.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertEqual(item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
self.assertFalse(new_item.file)
|
||||||
|
# FAILED to save image
|
||||||
|
self.assertFalse(new_item.image)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_change_without_any_cache_should_save_but_not_have_file_changes(self):
|
||||||
|
item = self.item
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.save_as_continue = False
|
||||||
|
|
||||||
|
# Upload new image and remove file
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="test_image2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"file": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_saveasnew": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"],
|
||||||
|
price=data["price"],
|
||||||
|
currency=data["currency"],
|
||||||
|
image=i2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure no cache
|
||||||
|
cache.delete(CACHE_KEYS["object"])
|
||||||
|
cache.delete(CACHE_KEYS["post"])
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/item/{self.item.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/2/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should not have changed existing item
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertEqual(item.file.name.count("test_file"), 1)
|
||||||
|
self.assertEqual(item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have saved new item
|
||||||
|
self.assertEqual(Item.objects.count(), 2)
|
||||||
|
new_item = Item.objects.filter(id=item.id + 1).first()
|
||||||
|
self.assertIsNotNone(new_item)
|
||||||
|
self.assertEqual(new_item.name, data["name"])
|
||||||
|
self.assertEqual(new_item.price, data["price"])
|
||||||
|
self.assertEqual(new_item.currency, data["currency"])
|
||||||
|
self.assertFalse(new_item.file)
|
||||||
|
# FAILED to save image
|
||||||
|
self.assertFalse(new_item.image)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_change_without_changing_file_should_save_changes(self):
|
||||||
|
item = self.item
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.save_as_continue = False
|
||||||
|
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"file": "",
|
||||||
|
"image": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cache
|
||||||
|
cache_item = Item(
|
||||||
|
name=data["name"],
|
||||||
|
price=data["price"],
|
||||||
|
currency=data["currency"],
|
||||||
|
)
|
||||||
|
|
||||||
|
cache.get(CACHE_KEYS["object"], cache_item)
|
||||||
|
cache.get(CACHE_KEYS["post"], data)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/item/{self.item.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/1/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should have changed existing item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "name")
|
||||||
|
# Should have cleared if requested
|
||||||
|
self.assertFalse(item.file.name)
|
||||||
|
self.assertEqual(item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
@mock.patch("admin_confirm.admin.CACHE_TIMEOUT", 1)
|
||||||
|
def test_old_cache_should_not_be_used(self):
|
||||||
|
item = self.item
|
||||||
|
|
||||||
|
# Upload new image and remove file
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="test_image2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Click "Save And Continue"
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"image": i2,
|
||||||
|
"file": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content,
|
||||||
|
save_action="_continue",
|
||||||
|
multipart_form=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should have cached the unsaved item
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNotNone(cached_item)
|
||||||
|
self.assertIsNone(cached_item.id)
|
||||||
|
self.assertEqual(cached_item.name, data["name"])
|
||||||
|
self.assertEqual(cached_item.price, data["price"])
|
||||||
|
self.assertEqual(cached_item.currency, data["currency"])
|
||||||
|
self.assertFalse(cached_item.file.name)
|
||||||
|
self.assertEqual(cached_item.image, i2)
|
||||||
|
|
||||||
|
# Should not have saved the changes yet
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "Not name")
|
||||||
|
self.assertIsNotNone(item.file)
|
||||||
|
self.assertIsNotNone(item.image)
|
||||||
|
|
||||||
|
# Wait for cache to time out
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Check that it did time out
|
||||||
|
cached_item = cache.get(CACHE_KEYS["object"])
|
||||||
|
self.assertIsNone(cached_item)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data["image"] = ""
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
response = self.client.post(f"/admin/market/item/{item.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should not have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/{item.id}/change/")
|
||||||
|
|
||||||
|
# Should have saved item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
saved_item = Item.objects.all().first()
|
||||||
|
self.assertEqual(saved_item.name, data["name"])
|
||||||
|
self.assertEqual(saved_item.price, data["price"])
|
||||||
|
self.assertEqual(saved_item.currency, data["currency"])
|
||||||
|
self.assertFalse(saved_item.file)
|
||||||
|
|
||||||
|
# SHOULD not have saved image since it was in the old cache
|
||||||
|
self.assertNotIn("test_image2", saved_item.image)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_cache_with_incorrect_model_should_not_be_used(self):
|
||||||
|
item = self.item
|
||||||
|
# Load the Change Item Page
|
||||||
|
ItemAdmin.save_as_continue = False
|
||||||
|
|
||||||
|
# Upload new image and remove file
|
||||||
|
i2 = SimpleUploadedFile(
|
||||||
|
name="test_image2.jpg",
|
||||||
|
content=open(self.image_path, "rb").read(),
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
# Request.POST
|
||||||
|
data = {
|
||||||
|
"id": item.id,
|
||||||
|
"name": "name",
|
||||||
|
"price": 2.0,
|
||||||
|
"file": "",
|
||||||
|
"file-clear": "on",
|
||||||
|
"currency": Item.VALID_CURRENCIES[0][0],
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_save": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cache to incorrect model
|
||||||
|
cache_obj = Shop(name="ShopName")
|
||||||
|
|
||||||
|
cache.set(CACHE_KEYS["object"], cache_obj)
|
||||||
|
cache.set(CACHE_KEYS["post"], data)
|
||||||
|
|
||||||
|
# Click "Yes, I'm Sure"
|
||||||
|
del data["_confirm_change"]
|
||||||
|
data[CONFIRMATION_RECEIVED] = True
|
||||||
|
|
||||||
|
with mock.patch.object(ItemAdmin, "message_user") as message_user:
|
||||||
|
response = self.client.post(
|
||||||
|
f"/admin/market/item/{self.item.id}/change/", data=data
|
||||||
|
)
|
||||||
|
# Should show message to user with correct obj and path
|
||||||
|
message_user.assert_called_once()
|
||||||
|
message = message_user.call_args[0][1]
|
||||||
|
self.assertIn("/admin/market/item/1/change/", message)
|
||||||
|
self.assertIn(data["name"], message)
|
||||||
|
self.assertNotIn("You may edit it again below.", message)
|
||||||
|
|
||||||
|
# Should have redirected to changelist
|
||||||
|
self.assertEqual(response.url, f"/admin/market/item/")
|
||||||
|
|
||||||
|
# Should have changed existing item
|
||||||
|
self.assertEqual(Item.objects.count(), 1)
|
||||||
|
item.refresh_from_db()
|
||||||
|
self.assertEqual(item.name, "name")
|
||||||
|
# Should have cleared if requested
|
||||||
|
self.assertFalse(item.file.name)
|
||||||
|
self.assertEqual(item.image.name.count("test_image2"), 0)
|
||||||
|
self.assertEqual(item.image.name.count("test_image"), 1)
|
||||||
|
|
||||||
|
# Should have cleared cache
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
||||||
|
def test_form_without_files_should_not_use_cache(self):
|
||||||
|
cache.delete_many(CACHE_KEYS.values())
|
||||||
|
shop = ShopFactory()
|
||||||
|
# Click "Save And Continue"
|
||||||
|
data = {
|
||||||
|
"id": shop.id,
|
||||||
|
"name": "name",
|
||||||
|
"_confirm_change": True,
|
||||||
|
"_continue": True,
|
||||||
|
}
|
||||||
|
response = self.client.post(f"/admin/market/shop/{shop.id}/change/", data=data)
|
||||||
|
|
||||||
|
# Should be shown confirmation page
|
||||||
|
self._assertSubmitHtml(
|
||||||
|
rendered_content=response.rendered_content, save_action="_continue"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should not have set cache since not multipart form
|
||||||
|
for key in CACHE_KEYS.values():
|
||||||
|
self.assertIsNone(cache.get(key))
|
||||||
|
|
@ -1,2 +1,12 @@
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
|
return reverse(
|
||||||
|
"admin:%s_%s_change" % (obj._meta.app_label, obj._meta.model_name),
|
||||||
|
args=(obj.pk,),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,5 @@ pytest-django~=4.1.0
|
||||||
readme-renderer~=28.0
|
readme-renderer~=28.0
|
||||||
twine~=3.3.0
|
twine~=3.3.0
|
||||||
coveralls~=3.0.0
|
coveralls~=3.0.0
|
||||||
|
Pillow~=8.1.0 # For ImageField
|
||||||
|
selenium~=3.141.0
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -6,7 +6,7 @@ README = open(os.path.join(here, "README.md")).read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="django-admin-confirm",
|
name="django-admin-confirm",
|
||||||
version="0.2.3.dev5",
|
version="0.2.3.dev9",
|
||||||
packages=["admin_confirm"],
|
packages=["admin_confirm"],
|
||||||
description="Adds confirmation to Django Admin changes, additions and actions",
|
description="Adds confirmation to Django Admin changes, additions and actions",
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from admin_confirm.admin import AdminConfirmMixin, confirm_action
|
|
||||||
|
|
||||||
from .models import Item, Inventory, Shop, ShoppingMall
|
|
||||||
|
|
||||||
|
|
||||||
class ItemAdmin(AdminConfirmMixin, admin.ModelAdmin):
|
|
||||||
list_display = ("name", "price", "currency")
|
|
||||||
confirm_change = True
|
|
||||||
|
|
||||||
|
|
||||||
class InventoryAdmin(AdminConfirmMixin, admin.ModelAdmin):
|
|
||||||
list_display = ("shop", "item", "quantity")
|
|
||||||
confirm_change = True
|
|
||||||
confirm_add = True
|
|
||||||
confirmation_fields = ["quantity"]
|
|
||||||
|
|
||||||
|
|
||||||
class ShopAdmin(AdminConfirmMixin, admin.ModelAdmin):
|
|
||||||
confirmation_fields = ["name"]
|
|
||||||
actions = ["show_message", "show_message_no_confirmation"]
|
|
||||||
|
|
||||||
@confirm_action
|
|
||||||
def show_message(modeladmin, request, queryset):
|
|
||||||
shops = ", ".join(shop.name for shop in queryset)
|
|
||||||
modeladmin.message_user(request, f"You selected with confirmation: {shops}")
|
|
||||||
|
|
||||||
show_message.allowed_permissions = ("delete",)
|
|
||||||
|
|
||||||
def show_message_no_confirmation(modeladmin, request, queryset):
|
|
||||||
shops = ", ".join(shop.name for shop in queryset)
|
|
||||||
modeladmin.message_user(request, f"You selected without confirmation: {shops}")
|
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
|
||||||
return request.user.is_superuser
|
|
||||||
|
|
||||||
|
|
||||||
class ShoppingMallAdmin(AdminConfirmMixin, admin.ModelAdmin):
|
|
||||||
confirm_add = True
|
|
||||||
confirm_change = True
|
|
||||||
confirmation_fields = ["name"]
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Item, ItemAdmin)
|
|
||||||
admin.site.register(Inventory, InventoryAdmin)
|
|
||||||
admin.site.register(Shop, ShopAdmin)
|
|
||||||
admin.site.register(ShoppingMall, ShoppingMallAdmin)
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from ..models import GeneralManager, Item, Inventory, Shop, ShoppingMall
|
||||||
|
|
||||||
|
from .item_admin import ItemAdmin
|
||||||
|
from .inventory_admin import InventoryAdmin
|
||||||
|
from .shop_admin import ShopAdmin
|
||||||
|
from .shoppingmall_admin import ShoppingMallAdmin
|
||||||
|
from .generalmanager_admin import GeneralManagerAdmin
|
||||||
|
|
||||||
|
admin.site.register(Item, ItemAdmin)
|
||||||
|
admin.site.register(Inventory, InventoryAdmin)
|
||||||
|
admin.site.register(Shop, ShopAdmin)
|
||||||
|
admin.site.register(ShoppingMall, ShoppingMallAdmin)
|
||||||
|
admin.site.register(GeneralManager, GeneralManagerAdmin)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.contrib.admin import ModelAdmin
|
||||||
|
|
||||||
|
|
||||||
|
class GeneralManagerAdmin(ModelAdmin):
|
||||||
|
save_as = True
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from django.contrib.admin import ModelAdmin
|
||||||
|
from admin_confirm.admin import AdminConfirmMixin
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryAdmin(AdminConfirmMixin, ModelAdmin):
|
||||||
|
list_display = ("shop", "item", "quantity")
|
||||||
|
|
||||||
|
confirm_change = True
|
||||||
|
confirm_add = True
|
||||||
|
confirmation_fields = ["quantity"]
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
from django.contrib.admin import ModelAdmin
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from admin_confirm.admin import AdminConfirmMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ItemAdmin(AdminConfirmMixin, ModelAdmin):
|
||||||
|
confirm_change = True
|
||||||
|
confirm_add = True
|
||||||
|
confirmation_fields = ["price"]
|
||||||
|
|
||||||
|
list_display = ("name", "price", "currency")
|
||||||
|
readonly_fields = ["image_preview"]
|
||||||
|
|
||||||
|
save_as = True
|
||||||
|
save_as_continue = False
|
||||||
|
|
||||||
|
def image_preview(self, obj):
|
||||||
|
if obj.image:
|
||||||
|
return mark_safe('<img src="{obj.image.url}" />')
|
||||||
|
|
||||||
|
# def one(self, obj):
|
||||||
|
# return "Read Only"
|
||||||
|
|
||||||
|
# def two(self, obj):
|
||||||
|
# return "Read Only"
|
||||||
|
|
||||||
|
# def three(self, obj):
|
||||||
|
# return "Read Only"
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
from django.contrib.admin import ModelAdmin
|
||||||
|
from admin_confirm.admin import AdminConfirmMixin, confirm_action
|
||||||
|
|
||||||
|
|
||||||
|
class ShopAdmin(AdminConfirmMixin, ModelAdmin):
|
||||||
|
confirmation_fields = ["name"]
|
||||||
|
actions = ["show_message", "show_message_no_confirmation"]
|
||||||
|
|
||||||
|
@confirm_action
|
||||||
|
def show_message(modeladmin, request, queryset):
|
||||||
|
shops = ", ".join(shop.name for shop in queryset)
|
||||||
|
modeladmin.message_user(request, f"You selected with confirmation: {shops}")
|
||||||
|
|
||||||
|
show_message.allowed_permissions = ("delete",)
|
||||||
|
|
||||||
|
def show_message_no_confirmation(modeladmin, request, queryset):
|
||||||
|
shops = ", ".join(shop.name for shop in queryset)
|
||||||
|
modeladmin.message_user(request, f"You selected without confirmation: {shops}")
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from ..models import ShoppingMall
|
||||||
|
from django.contrib.admin import ModelAdmin
|
||||||
|
from django.contrib.admin.options import StackedInline
|
||||||
|
from admin_confirm.admin import AdminConfirmMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ShopInline(StackedInline):
|
||||||
|
model = ShoppingMall.shops.through
|
||||||
|
|
||||||
|
|
||||||
|
class ShoppingMallAdmin(AdminConfirmMixin, ModelAdmin):
|
||||||
|
confirm_add = True
|
||||||
|
confirm_change = True
|
||||||
|
confirmation_fields = ["name"]
|
||||||
|
|
||||||
|
inlines = [ShopInline]
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Generated by Django 3.1.6 on 2021-02-22 03:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('market', '0005_shoppingmall'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='GeneralManager',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=120)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Town',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=120)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='file',
|
||||||
|
field=models.FileField(blank=True, null=True, upload_to='tmp/files'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='image',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='tmp/items'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shoppingmall',
|
||||||
|
name='general_manager',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='market.generalmanager'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shoppingmall',
|
||||||
|
name='town',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='market.town'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.1.6 on 2021-02-24 01:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('market', '0006_auto_20210222_0312'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='generalmanager',
|
||||||
|
name='headshot',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='tmp/gm/headshots'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.1.6 on 2021-02-24 08:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('market', '0007_generalmanager_headshot'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='item',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -9,6 +9,9 @@ class Item(models.Model):
|
||||||
name = models.CharField(max_length=120)
|
name = models.CharField(max_length=120)
|
||||||
price = models.DecimalField(max_digits=5, decimal_places=2)
|
price = models.DecimalField(max_digits=5, decimal_places=2)
|
||||||
currency = models.CharField(max_length=3, choices=VALID_CURRENCIES)
|
currency = models.CharField(max_length=3, choices=VALID_CURRENCIES)
|
||||||
|
image = models.ImageField(upload_to="tmp/items", null=True, blank=True)
|
||||||
|
file = models.FileField(upload_to="tmp/files", null=True, blank=True)
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
@ -18,7 +21,7 @@ class Shop(models.Model):
|
||||||
name = models.CharField(max_length=120)
|
name = models.CharField(max_length=120)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return str(self.name)
|
||||||
|
|
||||||
|
|
||||||
class Inventory(models.Model):
|
class Inventory(models.Model):
|
||||||
|
|
@ -35,9 +38,22 @@ class Inventory(models.Model):
|
||||||
notes = models.TextField(default="This is the default", null=True, blank=True)
|
notes = models.TextField(default="This is the default", null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class GeneralManager(models.Model):
|
||||||
|
name = models.CharField(max_length=120)
|
||||||
|
headshot = models.ImageField(upload_to="tmp/gm/headshots", null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Town(models.Model):
|
||||||
|
name = models.CharField(max_length=120)
|
||||||
|
|
||||||
|
|
||||||
class ShoppingMall(models.Model):
|
class ShoppingMall(models.Model):
|
||||||
name = models.CharField(max_length=120)
|
name = models.CharField(max_length=120)
|
||||||
shops = models.ManyToManyField(Shop)
|
shops = models.ManyToManyField(Shop)
|
||||||
|
general_manager = models.OneToOneField(
|
||||||
|
GeneralManager, on_delete=models.CASCADE, null=True, blank=True
|
||||||
|
)
|
||||||
|
town = models.ForeignKey(Town, on_delete=models.CASCADE, null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue