Skip to content

Commit

Permalink
Use cache for most fields and admin form for m2m files
Browse files Browse the repository at this point in the history
  • Loading branch information
Thu Trang Pham committed Feb 22, 2021
1 parent a498114 commit 87f4e08
Show file tree
Hide file tree
Showing 11 changed files with 697 additions and 30 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ docs/_build/

# Database
db.sqlite3

tmp/
152 changes: 125 additions & 27 deletions admin_confirm/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from itertools import chain
from typing import Dict
from django.contrib.admin.exceptions import DisallowedModelAdminToField
from django.contrib.admin.utils import flatten_fieldsets, unquote
Expand All @@ -6,11 +7,12 @@
from django.contrib.admin.options import TO_FIELD_VAR
from django.utils.translation import gettext as _
from django.contrib.admin import helpers
from django.db.models import Model, ManyToManyField
from django.db.models import Model, ManyToManyField, FileField, ImageField
from django.forms import ModelForm
from admin_confirm.utils import snake_to_title_case

SAVE_ACTIONS = ["_save", "_saveasnew", "_addanother", "_continue"]
from django.core.cache import cache
from django.forms.formsets import all_valid
from admin_confirm.constants import SAVE_ACTIONS, CACHE_KEYS


class AdminConfirmMixin:
Expand Down Expand Up @@ -89,6 +91,10 @@ def changeform_view(self, request, object_id=None, form_url="", extra_context=No
return self._change_confirmation_view(
request, object_id, form_url, extra_context
)
elif "_confirmation_received" in request.POST:
return self._confirmation_received_view(
request, object_id, form_url, extra_context
)

extra_context = {
**(extra_context or {}),
Expand All @@ -98,7 +104,7 @@ def changeform_view(self, request, object_id=None, form_url="", extra_context=No
return super().changeform_view(request, object_id, form_url, extra_context)

def _get_changed_data(
self, form: ModelForm, model: Model, obj: object, add: bool
self, form: ModelForm, model: Model, obj: object, new_object: object, add: bool
) -> Dict:
"""
Given a form, detect the changes on the form from the default values (if add) or
Expand All @@ -111,32 +117,99 @@ def _get_changed_data(
Returns a dictionary of the fields and their changed values if any
"""

def _display_for_field(value, field):
if value is None:
return ""

if isinstance(field, FileField) or isinstance(field, ImageField):
return value.name

return value

changed_data = {}
if form.is_valid():
if add:
for name, new_value in form.cleaned_data.items():
# Don't consider default values as changed for adding
default_value = model._meta.get_field(name).get_default()
if new_value is not None and new_value != default_value:
# Show what the default value is
changed_data[name] = [default_value, new_value]
else:
# Parse the changed data - Note that using form.changed_data would not work because initial is not set
for name, new_value in form.cleaned_data.items():
# Since the form considers initial as the value first shown in the form
# It could be incorrect when user hits save, and then hits "No, go back to edit"
obj.refresh_from_db()
# Note: getattr does not work on ManyToManyFields
field_object = model._meta.get_field(name)
initial_value = getattr(obj, name)
if isinstance(field_object, ManyToManyField):
initial_value = field_object.value_from_object(obj)

if initial_value != new_value:
changed_data[name] = [initial_value, new_value]
if add:
for name, new_value in form.cleaned_data.items():
# Don't consider default values as changed for adding
field_object = model._meta.get_field(name)
default_value = field_object.get_default()
if new_value is not None and new_value != default_value:
# Show what the default value is
changed_data[name] = [
_display_for_field(default_value, field_object),
_display_for_field(new_value, field_object),
]
else:
# Parse the changed data - Note that using form.changed_data would not work because initial is not set
for name, new_value in form.cleaned_data.items():

# Since the form considers initial as the value first shown in the form
# It could be incorrect when user hits save, and then hits "No, go back to edit"
obj.refresh_from_db()

field_object = model._meta.get_field(name)
initial_value = getattr(obj, name)

# Note: getattr does not work on ManyToManyFields
if isinstance(field_object, ManyToManyField):
initial_value = field_object.value_from_object(obj)

if initial_value != new_value:
changed_data[name] = [
_display_for_field(initial_value, field_object),
_display_for_field(new_value, field_object),
]

return changed_data

def _confirmation_received_view(self, request, object_id, form_url, extra_context):
"""
Save the cached object from the confirmation page. This is used because
FileField and ImageField data cannot be passed through a form.
"""
add = object_id is None
new_object = cache.get(CACHE_KEYS["object"])
change_message = cache.get(CACHE_KEYS["change_message"])

new_object.id = object_id
new_object.save()

"""
Saves the m2m values from request.POST since the cached object would not have
these stored on it
"""
# Can't use QueryDict.get() because it only returns the last value for multiselect
query_dict = {k: v for k, v in request.POST.lists()}

# Taken from _save_m2m with slight modification
# https://github.com/django/django/blob/master/django/forms/models.py#L430-L449
exclude = self.exclude
fields = self.fields
opts = new_object._meta
# Note that for historical reasons we want to include also
# private_fields here. (GenericRelation was previously a fake
# m2m field).
for f in chain(opts.many_to_many, opts.private_fields):
if not hasattr(f, "save_form_data"):
continue
if fields and f.name not in fields:
continue
if exclude and f.name in exclude:
continue
if f.name in query_dict.keys():
f.save_form_data(new_object, query_dict[f.name])
# End code from _save_m2m

# Clear the cache
cache.delete_many(CACHE_KEYS.values())

if add:
self.log_addition(request, new_object, change_message)
return self.response_add(request, new_object)
else:
self.log_change(request, new_object, change_message)
return self.response_change(request, new_object)

def _change_confirmation_view(self, request, object_id, form_url, extra_context):
# This code is taken from super()._changeform_view
# https://github.com/django/django/blob/master/django/contrib/admin/options.py#L1575-L1592
Expand Down Expand Up @@ -169,13 +242,27 @@ def _change_confirmation_view(self, request, object_id, form_url, extra_context)
)

form = ModelForm(request.POST, request.FILES, obj)
form_validated = form.is_valid()
if form_validated:
new_object = self.save_form(request, form, change=not add)
else:
new_object = form.instance
formsets, inline_instances = self._create_formsets(
request, new_object, change=not add
)

change_message = self.construct_change_message(request, form, formsets, add)
# Note to self: For inline instances see:
# https://github.com/django/django/blob/master/django/contrib/admin/options.py#L1582

# End code from super()._changeform_view

if not (form_validated and all_valid(formsets)):
# Form not valid, cannot confirm
return super()._changeform_view(request, object_id, form_url, extra_context)

# 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, new_object, add)

changed_confirmation_fields = set(
self.get_confirmation_fields(request, obj)
Expand All @@ -184,6 +271,17 @@ def _change_confirmation_view(self, request, object_id, form_url, extra_context)
# No confirmation required for changed fields, continue to save
return super()._changeform_view(request, object_id, form_url, extra_context)

print(request.POST)
print(request.FILES)
print(form.cleaned_data)
cache.set(CACHE_KEYS["object"], new_object)
# cache.set("admin_confirm__confirmation_cleaned_data", form.cleaned_data)

# print(new_object.shops.all())
# cache.set("admin_confirm__confirmation_form", form)
cache.set("admin_confirm__confirmation_formsets", formsets)
cache.set(CACHE_KEYS["change_message"], change_message)

# Parse the original save action from request
save_action = None
for key in request.POST.keys():
Expand Down
10 changes: 10 additions & 0 deletions admin_confirm/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
SAVE_ACTIONS = ["_save", "_saveasnew", "_addanother", "_continue"]

CONFIRM_ADD = "_confirm_add"
CONFIRM_CHANGE = "_confirm_change"
CONFIRMATION_RECEIVED = "_confirmation_received"

CACHE_KEYS = {
"object": "admin_confirm__confirmation_object",
"change_message": "admin_confirm__confirmation_change_message",
}
7 changes: 4 additions & 3 deletions admin_confirm/templates/admin/change_confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,20 @@
{% if add %}
<p>{% blocktrans with escaped_object=object %}Are you sure you want to add the {{ model_name }}?{% endblocktrans %}</p>
{% include "admin/change_data.html" %}
<form method="post" action="{% url opts|admin_urlname:'add'%}">{% csrf_token %}
<form {% if files %}enctype="multipart/form-data"{% endif %} method="post" action="{% url opts|admin_urlname:'add'%}">{% csrf_token %}

{% else %}

<p>{% blocktrans with escaped_object=object %}Are you sure you want to change the {{ model_name }} "{{ object_name }}"?{% endblocktrans %}</p>
{% include "admin/change_data.html" %}
<form method="post" action="{% url opts|admin_urlname:'change' object_id|admin_urlquote %}">{% csrf_token %}
{% endif %}
<div class=hidden>
{{ form }}
<div class="hidden">
{{form.as_p}}
</div>
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
<input type="hidden" name="_confirmation_received" value="True">
<div class="submit-row">
<input type="submit" value="{% trans 'Yes, I’m sure' %}" name="{{ submit_name }}">
<p class="deletelink-box">
Expand Down
5 changes: 5 additions & 0 deletions admin_confirm/tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ def _assertSubmitHtml(self, rendered_content, save_action="_save"):
# There should not be _confirm_add or _confirm_change sent in the form on confirmaiton page
self.assertNotIn("_confirm_add", rendered_content)
self.assertNotIn("_confirm_change", rendered_content)
# Should have _confirmation_received as a hidden field
self.assertIn(
'<input type="hidden" name="_confirmation_received" value="True">',
rendered_content,
)

def _assertSimpleFieldFormHtml(self, rendered_content, fields):
for k, v in fields.items():
Expand Down
3 changes: 3 additions & 0 deletions admin_confirm/tests/test_confirm_change_and_add_m2m_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ def test_m2m_field_post_change_with_confirm_change(self):
]
self.assertEqual(response.template_name, expected_templates)

# Should show two lists for the m2m current and modified values
self.assertEqual(response.rendered_content.count("<ul>"), 2)

self._assertManyToManyFormHtml(
rendered_content=response.rendered_content,
options=shops,
Expand Down

0 comments on commit 87f4e08

Please sign in to comment.