# HG changeset patch # User Branko Majic # Date 2013-11-03 12:03:58 # Node ID 5193ae7fc0e165998028b4e506b8717dbb915b70 # Parent b2d842037a6342a13bf817af34fbb7e0b69cd490 CONNT-20: Implemented output of related items that will get removed as part of cascading on the confirmation page. Includes one custom model and view mixin, and small changes to the delete confirmation template. diff --git a/conntrackt/models.py b/conntrackt/models.py --- a/conntrackt/models.py +++ b/conntrackt/models.py @@ -20,10 +20,13 @@ # Django imports. +from django.contrib.admin.util import NestedObjects from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models from django.db.models.query_utils import Q +from django.utils.html import format_html +from django.utils.text import capfirst class SearchManager(models.Manager): @@ -47,7 +50,94 @@ class SearchManager(models.Manager): return self.filter(Q(name__icontains=search_term) | Q(description__icontains=search_term)) -class Project(models.Model): +class RelatedCollectorMixin(object): + """ + Implements model mixin for easily obtainning related items of a model + instance. + + The mixin can be used for obtaining all model objects that are directly or + indirectly linked to the calling model object (through foreign key + relationships). + + This mixin is very useful in delete views for warning the user about all of + the related items that will be deleted if the calling item is deleted as + well. + """ + + def get_dependant_objects(self): + """ + Creates a list of model objects that depend (reference), both directly + and indirectly, on calling model object. The calling model object is + included as the first element of the list. This method call can be used + in order to obtain a list of model objects that would get deleted in + case the calling model object gets deleted. + + Returns: + Nested list of model objects that depend (reference) calling model + object. + """ + + collector = NestedObjects(using='default') + + collector.collect([self]) + + return collector.nested() + + def get_dependant_objects_representation(self): + """ + Creates a nested list of object representations that depend (reference), + both directly and indirectly, calling model object. This method call can + be used in order to obtain a list of string representations of model + objects that would get deleted in case the calling model object gets + deleted. + + The resulting nested list can be shown to the user for + warning/notification purposes using the unordered_list template tag. + + Each non-list element will be a string of format: + + MODEL_NAME: OBJECT_REPRESENTATION + + If object has a callable get_absolute_url method, the object + representation will be surrouned by HTML anchor tag () where + target (href) is set to the value of get_absolute_url() method call. + + Returns: + Nested list of representations of model objects that depend + (reference) calling model object. + """ + + collector = NestedObjects(using='default') + + collector.collect([self]) + + def formatter_callback(obj): + """ + Creates model object representation in format: + + MODEL_NAME: OBJECT_REPRESENTATION + + If passed object has a callable get_absolute_url method, the + instance representation will be surrouned by an HTML anchor + () where target is set to value of the get_absolute_url() + method call. + + Arguments: + obj - Model object whose representation should be returned. + + Returns: + String represenation of passed model object. + """ + + try: + return format_html('{0}: {2}', capfirst(obj._meta.verbose_name), obj.get_absolute_url(), str(obj)) + except AttributeError: + return format_html('{0}: {1}', capfirst(obj._meta.verbose_name), str(obj)) + + return collector.nested(formatter_callback) + + +class Project(RelatedCollectorMixin, models.Model): """ Implements a model with information about a project. A project has some basic settings, and mainly serves the purpose of grouping entities for @@ -62,6 +152,7 @@ class Project(models.Model): name = models.CharField(max_length=100, unique=True) description = models.TextField(blank=True) objects = SearchManager() + deletion_collect_models = ["Entity", "Interface"] class Meta: permissions = (("view", "Can view information"),) @@ -82,7 +173,7 @@ class Project(models.Model): return reverse("project", kwargs={'pk': self.pk}) -class Location(models.Model): +class Location(RelatedCollectorMixin, models.Model): """ Implements a model with information about location. Locations can further be assigned to entities, letting the user group different servers and equipment @@ -118,7 +209,7 @@ class Location(models.Model): return self.name -class Entity(models.Model): +class Entity(RelatedCollectorMixin, models.Model): """ Models an entity in a project. An entity can be a server, router, or any other piece of networking equipment that has its own IP address. @@ -215,7 +306,7 @@ class Entity(models.Model): raise ValidationError("The entity cannot be moved to different project as long as it has valid communications with entities in current project.") -class Interface(models.Model): +class Interface(RelatedCollectorMixin, models.Model): """ Models a representation of an interface on an entity. It can be used for representing the subnets as well. @@ -261,7 +352,7 @@ class Interface(models.Model): return '%s (%s/%s)' % (self.entity.name, self.address, self.netmask) -class Communication(models.Model): +class Communication(RelatedCollectorMixin, models.Model): """ Models a representation of allowed network communication. This lets the user display the possible network connections that should be allowed. Information diff --git a/conntrackt/templates/conntrackt/delete_form.html b/conntrackt/templates/conntrackt/delete_form.html --- a/conntrackt/templates/conntrackt/delete_form.html +++ b/conntrackt/templates/conntrackt/delete_form.html @@ -15,7 +15,11 @@
{% csrf_token %} {{ form }} - Are you sure you want to remove this project? + The following entries will be removed: + + Are you sure you want to remove them?

diff --git a/conntrackt/views.py b/conntrackt/views.py --- a/conntrackt/views.py +++ b/conntrackt/views.py @@ -30,6 +30,7 @@ from django.contrib import messages from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse, reverse_lazy from django.db.models import Q +from django.db.models.deletion import Collector from django.http import HttpResponse from django.shortcuts import render_to_response, get_object_or_404 from django.views.generic import TemplateView, DetailView, CreateView, UpdateView, DeleteView, View @@ -65,6 +66,43 @@ class RedirectToNextMixin(object): return self.request.GET.get(self.next_parameter, super(RedirectToNextMixin, self).get_success_url()) +class RelatedItemsMixin(object): + """ + View mixin that adds related items of a referenced model object to context + data in form of a nested list, including the reference model object as the + first element of a list. + + The context data will be passed using the "related_items" key. + + This data can be used in the template by passing it through the + unordered_list template tag. + + The reference object is accessed via "object" property of calling view + (i.e. self.object). + + For more details on implementation, see: + + RelatedCollectorMixin.get_dependant_objects_representation method + """ + + def get_context_data(self, **kwargs): + """ + Adds the related items of a reference model object to context data. + + Returns: + Context data. + """ + + # Set the context using the parent class. + context = super(RelatedItemsMixin, self).get_context_data(**kwargs) + + # Add to context the nested list of string representations of related + # items. + context["related_items"] = self.object.get_dependant_objects_representation() + + return context + + class IndexView(MultiplePermissionsRequiredMixin, TemplateView): """ Custom view used for rendering the index page. @@ -337,7 +375,7 @@ class ProjectUpdateView(RedirectToNextMi return "Update project %s" % self.object.name -class ProjectDeleteView(RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): +class ProjectDeleteView(RelatedItemsMixin, RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): """ View for deleting a project. """ @@ -421,7 +459,7 @@ class LocationUpdateView(RedirectToNextM return "Update location %s" % self.object.name -class LocationDeleteView(RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): +class LocationDeleteView(RelatedItemsMixin, RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): """ View for deleting a location. """ @@ -538,7 +576,7 @@ class EntityUpdateView(RedirectToNextMix return "Update entity %s" % self.object.name -class EntityDeleteView(RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): +class EntityDeleteView(RelatedItemsMixin, RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): """ View for deleting an entity. """ @@ -694,7 +732,7 @@ class InterfaceUpdateView(RedirectToNext return "Update interface %s" % self.object.name -class InterfaceDeleteView(RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): +class InterfaceDeleteView(RelatedItemsMixin, RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): """ View for deleting an interface. """ @@ -887,7 +925,7 @@ class CommunicationUpdateView(RedirectTo return "Update communication %s" % self.object -class CommunicationDeleteView(RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): +class CommunicationDeleteView(RelatedItemsMixin, RedirectToNextMixin, SetHeadlineMixin, MultiplePermissionsRequiredMixin, DeleteView): """ View for deleting an communication. """