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. """