diff --git a/.gitignore b/.gitignore index cbb74dd..95bd76d 100755 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ django_mass_edit.egg-info .idea .coverage .tox +.spyproject diff --git a/massadmin/massadmin.py b/massadmin/massadmin.py index 4eb51db..6f07dc9 100644 --- a/massadmin/massadmin.py +++ b/massadmin/massadmin.py @@ -31,13 +31,13 @@ import types import sys -from django.contrib import admin +from django.contrib import admin, messages from django.core.exceptions import PermissionDenied, ValidationError try: from django.urls import reverse except ImportError: # Django<2.0 from django.core.urlresolvers import reverse -from django.db import transaction +from django.db import transaction, DatabaseError, IntegrityError try: # Django>=1.9 from django.apps import apps get_model = apps.get_model @@ -64,6 +64,13 @@ from . import settings +def url_to_edit_object(obj): + url = reverse('admin:%s_%s_change' % ( + obj._meta.app_label, obj._meta.model_name), args=[obj.pk]) + return '%s #%s' % ( + url, obj._meta.verbose_name, obj.pk) + + def mass_change_selected(modeladmin, request, queryset): selected = queryset.values_list('pk', flat=True) @@ -248,6 +255,7 @@ def mass_change_view( with transaction.atomic(): objects_count = 0 changed_count = 0 + forms_errors = [] # accumulate across all forms objects = queryset.filter(pk__in=object_ids) for obj in objects: objects_count += 1 @@ -271,6 +279,7 @@ def mass_change_view( form, change=True) else: + forms_errors.append({obj: form.errors}) form_validated = False new_object = obj prefixes = {} @@ -289,36 +298,77 @@ def mass_change_view( if all_valid(formsets) and form_validated: # self.admin_obj.save_model(request, new_object, form, change=True) - self.save_model( - request, - new_object, - form, - change=True) - form.save_m2m() - for formset in formsets: - self.save_formset( - request, - form, - formset, - change=True) - - change_message = self.construct_change_message( - request, - form, - formsets) - self.log_change( - request, - new_object, - change_message) - changed_count += 1 + # second transaction.atomic added to avoid: + # "You can't execute queries ..." error + with transaction.atomic(): + + try: + self.save_model( + request, + new_object, + form, + change=True) + form.save_m2m() + for formset in formsets: + self.save_formset( + request, + form, + formset, + change=True) + change_message = self.construct_change_message( + request, + form, + formsets) + self.log_change( + request, + new_object, + change_message) + changed_count += 1 + + except IntegrityError: + obj_url = url_to_edit_object(obj) + hint = '(Either run the mass edit without'\ + ' this; or remove the existing record)' + msg = f'

Cannot modify {obj_url}: a '\ + 'record already exists in the database'\ + f' {hint}

' + messages.add_message( + request, messages.ERROR, mark_safe(msg)) + + except DatabaseError as err: + detail = str(err.__cause__ or str(err) or '') + obj_url = url_to_edit_object(obj) + hint = 'Please attempt a manual change.' + msg = f'

Cannot modify {obj_url}: {detail} {hint}

' + messages.add_message( + request, messages.ERROR, mark_safe(msg)) + + except Exception as err: + detail = str(err.__cause__ or str(err) or '') + obj_url = url_to_edit_object(obj) + hint = 'Please attempt a manual change.' + msg = f'

Cannot modify {obj_url}: {detail} {hint}

' + messages.add_message( + request, messages.ERROR, mark_safe(msg)) if changed_count == objects_count: + msg = _('%s out of %s records were successfully edited.' % + (changed_count, objects_count)) + messages.add_message(request, messages.INFO, msg) return self.response_change(request, new_object) else: errors = form.errors errors_list = helpers.AdminErrorList(form, formsets) + # Create consolidated feedback on errors + msg = _('%s out of %s records cannot be successfully edited' % + (objects_count - changed_count, objects_count)) + messages.add_message(request, messages.ERROR, msg) + for err in forms_errors: + msg = f'

{list(err.keys())[0]}:
' \ + f'{list(err.values())[0]}

'.replace('__all__', 'General') + messages.add_message(request, messages.ERROR, mark_safe(msg)) # Raise error for rollback transaction in atomic block - raise ValidationError("Not all forms is correct") + raise ValidationError("The mass edit could not be executed!") except Exception: general_error = sys.exc_info()[1] diff --git a/massadmin/templates/admin/includes/mass_fieldset.html b/massadmin/templates/admin/includes/mass_fieldset.html index 93ada00..8be4d69 100644 --- a/massadmin/templates/admin/includes/mass_fieldset.html +++ b/massadmin/templates/admin/includes/mass_fieldset.html @@ -3,15 +3,15 @@
{% if fieldset.name %}

{{ fieldset.name }} Mass Edit

{% endif %} {% if fieldset.description %}

{{ fieldset.description|safe }}

{% endif %} - + {% if general_error %}
{{ general_error}}
{% endif %} - + {% endif %} - + {% for field in line %} {% if field.field.name in adminform.readonly_fields %} {% else %}{% if field.field.name in unique_fields %} {% endif %}{% endif %} {% endfor %} {% endfor %}
- {% trans "Mass Update?" %} + {% trans "Update?" %} {% trans "Field" %} @@ -25,19 +25,19 @@
- + {{ field.field.name }} {% trans "is read only." %} - + {{ field.field.name }} {% trans "is unique." %} @@ -49,20 +49,23 @@ {% if field.is_checkbox %} - {{ field.field }}{{ field.label_tag }} +
{{ field.field }}
+
 {{ field.label_tag }}
{% else %}
- {{ field.label_tag }} + {{ field.label_tag }} 
{{ field.field }}
{% endif %} - {% if field.field.field.help_text %}

{{ field.field.field.help_text|safe }}

{% endif %} + {% if field.field.field.help_text %} +

 {{ field.field.field.help_text|safe }}

+ {% endif %}
-
\ No newline at end of file +