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]}