diff --git a/orchestra/contrib/accounts/actions.py b/orchestra/contrib/accounts/actions.py index 9c93ff07..032af361 100644 --- a/orchestra/contrib/accounts/actions.py +++ b/orchestra/contrib/accounts/actions.py @@ -10,7 +10,7 @@ from django.utils import timezone from django.utils.encoding import force_text from django.utils.html import format_html from django.utils.text import capfirst -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext, ugettext_lazy as _ from orchestra.core import services @@ -146,16 +146,24 @@ def delete_related_services(modeladmin, request, queryset): # The user has already confirmed the deletion. # Do the deletion and return a None to display the change list view again. if request.POST.get('post'): - n = queryset.count() - if n: + accounts = len(queryset) + msg = _("Related services deleted and account disabled.") + for account in queryset: + account.is_active = False + account.save(update_fields=('is_active',)) + modeladmin.log_change(request, account, msg) + if accounts: + relateds = len(to_delete) for obj in to_delete: obj_display = force_text(obj) modeladmin.log_deletion(request, obj, obj_display) - # TODO This probably will fail in certain conditions, just capture exception obj.delete() - modeladmin.message_user(request, _("Successfully deleted %(count)d %(items)s.") % { - "count": n, "items": model_ngettext(modeladmin.opts, n) - }, messages.SUCCESS) + context = { + 'accounts': accounts, + 'relateds': relateds, + } + msg = _("Successfully disabled %(accounts)d account and deleted %(relateds)d related services.") % context + modeladmin.message_user(request, msg, messages.SUCCESS) # Return None to display the change list page again. return None @@ -165,7 +173,7 @@ def delete_related_services(modeladmin, request, queryset): objects_name = force_text(opts.verbose_name_plural) context = dict( - modeladmin.admin_site.each_context(request), + admin_site.each_context(request), title=_("Are you sure?"), objects_name=objects_name, deletable_objects=[related_services], @@ -174,11 +182,85 @@ def delete_related_services(modeladmin, request, queryset): opts=opts, action_checkbox_name=helpers.ACTION_CHECKBOX_NAME, ) - request.current_app = modeladmin.admin_site.name + request.current_app = admin_site.name # Display the confirmation page - return TemplateResponse(request, modeladmin.delete_selected_confirmation_template or [ - "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.model_name), - "admin/%s/delete_selected_confirmation.html" % app_label, - "admin/delete_selected_confirmation.html" - ], context) + template = 'admin/%s/%s/delete_related_services_confirmation.html' % (app_label, opts.model_name) + return TemplateResponse(request, template, context) delete_related_services.short_description = _("Delete related services") + + +def disable_selected(modeladmin, request, queryset): + opts = modeladmin.model._meta + app_label = opts.app_label + + # The user has already confirmed the deletion. + # Do the disable and return a None to display the change list view again. + if request.POST.get('post'): + n = 0 + for account in queryset: + account.disable() + modeladmin.log_change(request, account, _("Disabled")) + n += 1 + modeladmin.message_user(request, ungettext( + _("One account has been successfully disabled."), + _("%i accounts have been successfully disabled.") % n, + n) + ) + return None + + user = request.user + admin_site = modeladmin.admin_site + + def format(obj): + has_admin = obj.__class__ in admin_site._registry + opts = obj._meta + no_edit_link = '%s: %s' % (capfirst(opts.verbose_name), force_text(obj)) + if has_admin: + try: + admin_url = reverse( + 'admin:%s_%s_change' % (opts.app_label, opts.model_name), + None, + (quote(obj._get_pk_val()),) + ) + except NoReverseMatch: + # Change url doesn't exist -- don't display link to edit + return no_edit_link + + p = '%s.%s' % (opts.app_label, get_permission_codename('delete', opts)) + if not user.has_perm(p): + perms_needed.add(opts.verbose_name) + # Display a link to the admin page. + context = (capfirst(opts.verbose_name), admin_url, obj) + return format_html('{}: {}', *context) + else: + # Don't display link to edit, because it either has no + # admin or is edited inline. + return no_edit_link + + display = [] + for account in queryset: + current = [] + for related in account.get_services_to_disable(): + current.append(format(related)) + display.append([format(account), current]) + + if len(queryset) == 1: + objects_name = force_text(opts.verbose_name) + else: + objects_name = force_text(opts.verbose_name_plural) + + context = dict( + admin_site.each_context(request), + title=_("Are you sure?"), + objects_name=objects_name, + deletable_objects=display, + queryset=queryset, + opts=opts, + action_checkbox_name=helpers.ACTION_CHECKBOX_NAME, + ) + request.current_app = admin_site.name + template = 'admin/%s/%s/disable_selected_confirmation.html' % (app_label, opts.model_name) + return TemplateResponse(request, template, context) +disable_selected.short_description = _("Disable selected accounts") +disable_selected.url = 'disable' +disable_selected.tool_description = _("Disable") diff --git a/orchestra/contrib/accounts/admin.py b/orchestra/contrib/accounts/admin.py index 4584e880..4cadc51b 100644 --- a/orchestra/contrib/accounts/admin.py +++ b/orchestra/contrib/accounts/admin.py @@ -13,12 +13,12 @@ from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin -from orchestra.admin.actions import SendEmail, disable +from orchestra.admin.actions import SendEmail from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query, change_url from orchestra.core import services, accounts from orchestra.forms import UserChangeForm -from .actions import list_contacts, service_report, delete_related_services +from .actions import list_contacts, service_report, delete_related_services, disable_selected from .filters import HasMainUserListFilter from .forms import AccountCreationForm from .models import Account @@ -61,8 +61,10 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) filter_horizontal = () change_readonly_fields = ('username', 'main_systemuser_link', 'is_active') change_form_template = 'admin/accounts/account/change_form.html' - actions = [disable, list_contacts, service_report, SendEmail(), delete_related_services] - change_view_actions = [disable, service_report] + actions = ( + disable_selected, delete_related_services, list_contacts, service_report, SendEmail() + ) + change_view_actions = (disable_selected, service_report) ordering = () main_systemuser_link = admin_link('main_systemuser') @@ -108,6 +110,12 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) form.save_related(obj) else: super(AccountAdmin, self).save_model(request, obj, form, change) + + def get_actions(self, request): + actions = super(AccountAdmin, self).get_actions(request) + if 'delete_selected' in actions: + del actions['delete_selected'] + return actions admin.site.register(Account, AccountAdmin) diff --git a/orchestra/contrib/accounts/models.py b/orchestra/contrib/accounts/models.py index 84e0b74f..4977427d 100644 --- a/orchestra/contrib/accounts/models.py +++ b/orchestra/contrib/accounts/models.py @@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.orchestration.middlewares import OperationsMiddleware from orchestra.contrib.orchestration import Operation +from orchestra.core import services from orchestra.utils.mail import send_email_template from . import settings @@ -82,13 +83,17 @@ class Account(auth.AbstractBaseUser): self.save(update_fields=['is_active']) self.notify_related() - def notify_related(self): - # Trigger save() on related objects that depend on this account + def get_services_to_disable(self): for rel in self._meta.get_all_related_objects(): source = getattr(rel, 'related_model', rel.model) if source in services and hasattr(source, 'active'): for obj in getattr(self, rel.get_accessor_name()).all(): - OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=[]) + yield obj + + def notify_related(self): + """ Trigger save() on related objects that depend on this account """ + for obj in self.get_services_to_disable(): + OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=[]) def send_email(self, template, context, email_from=None, contacts=[], attachments=[], html=None): contacts = self.contacts.filter(email_usages=contacts) diff --git a/orchestra/contrib/accounts/templates/admin/accounts/account/delete_related_services_confirmation.html b/orchestra/contrib/accounts/templates/admin/accounts/account/delete_related_services_confirmation.html new file mode 100644 index 00000000..b25aee0b --- /dev/null +++ b/orchestra/contrib/accounts/templates/admin/accounts/account/delete_related_services_confirmation.html @@ -0,0 +1,39 @@ +{% extends "admin/delete_selected_confirmation.html" %} +{% load i18n l10n admin_urls %} + +{% block content %} +{% if perms_lacking %} +
{% blocktrans %}Deleting the selected {{ objects_name }} would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}
+{% blocktrans %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktrans %}
+{% blocktrans %}Are you sure you want to delete the selected {{ objects_name }}? All of the following objects and their related items will be deleted:{% endblocktrans %}
+ {% include "admin/includes/object_delete_summary.html" %} +{% blocktrans %}Disabling the selected {{ objects_name }} would result in disabling related objects, but your account doesn't have permission to disable the following types of objects:{% endblocktrans %}
+{% blocktrans %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktrans %}
+{% blocktrans %}Are you sure you want to disable the selected {{ objects_name }}? All of the following objects and their related items will be disabled:{% endblocktrans %}
+