Changeset - c153097ca199
[Not reviewed]
default
0 3 0
Branko Majic (branko) - 11 years ago 2013-07-24 21:25:10
branko@majic.rs
CONNT-14: Implemented the LocationForm model form. Updated tests.
3 files changed with 18 insertions and 62 deletions:
0 comments (0 inline, 0 general)
conntrackt/forms.py
Show inline comments
 
# Django imports.
 
from django.forms import ModelForm
 

	
 
# Application imports.
 
from .models import Project, Entity, Interface, Communication
 
from .models import Project, Location, Entity, Interface, Communication
 

	
 

	
 
class WidgetCSSClassFormMixin(object):
 
    """
 
    Helper form mixin that can be used for applying additional custom CSS
 
    classes to form field widgets.
 

	
 
    The mixin accepts the following class options:
 

	
 
        widget_css_classes - Dictionary describing which additional CSS classes
 
        should be applied to which form widget. Dictinoary keys should be equal
 
        to form widget names, while the value should be a string containing the
 
        extra CSS classes that should be applied to it. In order to apply the
 
        CSS classes to every widget of the form, use the key "ALL"
 
    """
 

	
 
    def __init__(self, *args, **kwargs):
 
        """
 
        Assigns the custom CSS classes form widgets, as configured by the
 
        widget_css_classes property.
 
        """
 

	
 
        super(WidgetCSSClassFormMixin, self).__init__(*args, **kwargs)
 

	
 
        for field_name, css_class in self.widget_css_classes.iteritems():
 
            if field_name == "ALL":
 
                for field in self.fields.values():
 
                    if "class" in field.widget.attrs:
 
                        field.widget.attrs["class"] += " " + css_class
 
                    else:
 
                        field.widget.attrs["class"] = css_class
 
            else:
 
                field = self.fields[field_name]
 
                if "class" in field.widget.attrs:
 
                    field.widget.attrs["class"] += " " + css_class
 
                else:
 
                    field.widget.attrs["class"] = css_class
 

	
 

	
 
class PlaceholderFormMixin(object):
 
    """
 
    Helper form mixin that can be used to set-up placeholders for text widgets.
 

	
 
    The mixin accepts the following class options:
 

	
 
        widget_placeholders - Dictionary describing which placeholders should be
 
        applied to which widgets. Keys should be equal to form widget names,
 
        while the values should be the strings that should be set as
 
        placeholders.
 
    """
 

	
 
    def __init__(self, *args, **kwargs):
 
        """
 
        Assigns the placeholders to text form widgets, as configured by the
 
        widget_placeholders property.
 
        """
 

	
 
        super(PlaceholderFormMixin, self).__init__(*args, **kwargs)
 

	
 
        for field_name, placeholder in self.widget_placeholders.iteritems():
 
            self.fields[field_name].widget.attrs["placeholder"] = placeholder
 

	
 

	
 
class EntityForm(WidgetCSSClassFormMixin, PlaceholderFormMixin, ModelForm):
 
    """
 
    Implements a custom model form for entities with some styling changes.
 
    """
 

	
 
    class Meta:
 
        model = Entity
 

	
 
    widget_placeholders = {"name": "Entity name",
 
                           "description": "Entity description"}
 
    widget_css_classes = {"ALL": "span6"}
 

	
 

	
 
class InterfaceForm(WidgetCSSClassFormMixin, PlaceholderFormMixin, ModelForm):
 
    """
 
    Implements a custom model form for interfaces with some styling changes.
 
    """
 

	
 
    class Meta:
 
        model = Interface
 

	
 
    widget_placeholders = {"name": "Interface name",
 
                           "description": "Interface description",
 
                           "address": "IP address of interface",
 
                           "netmask": "IP address netmask"}
 

	
 
    widget_css_classes = {"ALL": "span6"}
 

	
 

	
 
class CommunicationForm(WidgetCSSClassFormMixin, PlaceholderFormMixin, ModelForm):
 
    """
 
    Implements a custom model form for communications with some styling changes.
 
    """
 

	
 
    class Meta:
 
        model = Communication
 

	
 
    widget_placeholders = {"port": "Port used for communication",
 
                           "description": "Communication description"}
 

	
 
    widget_css_classes = {"ALL": "span6"}
 

	
 

	
 
class ProjectForm(WidgetCSSClassFormMixin, PlaceholderFormMixin, ModelForm):
 
    """
 
    Implements a custom model form for projects with some styling changes.
 
    """
 

	
 
    class Meta:
 
        model = Project
 

	
 
    widget_placeholders = {"name": "Project name",
 
                           "description": "Project description"}
 

	
 
    widget_css_classes = {"ALL": "span6"}
 

	
 

	
 
class LocationForm(WidgetCSSClassFormMixin, PlaceholderFormMixin, ModelForm):
 
    """
 
    Implements a custom model form for projects with some styling changes.
 
    """
 

	
 
    class Meta:
 
        model = Location
 

	
 
    widget_placeholders = {"name": "Location name",
 
                           "description": "Location description"}
 

	
 
    widget_css_classes = {"ALL": "span6"}
conntrackt/tests/test_views.py
Show inline comments
 
# Standard library imports.
 
from StringIO import StringIO
 
from zipfile import ZipFile, ZIP_DEFLATED
 

	
 
# Python third-party library imports.
 
import mock
 

	
 
# Django imports.
 
from django.core.urlresolvers import reverse
 
from django.http import Http404
 
from django.test import RequestFactory
 
from django.test import TestCase
 

	
 
# Application imports
 
from conntrackt.models import Project, Location, Entity, Interface, Communication
 

	
 
from conntrackt.views import IndexView
 
from conntrackt.views import entity_iptables, project_iptables
 

	
 
from conntrackt.views import ProjectView, ProjectCreateView, ProjectUpdateView, ProjectDeleteView
 
from conntrackt.views import LocationCreateView, LocationUpdateView, LocationDeleteView
 
from conntrackt.views import EntityView, EntityCreateView, EntityUpdateView, EntityDeleteView
 
from conntrackt.views import InterfaceCreateView, InterfaceUpdateView, InterfaceDeleteView
 
from conntrackt.views import CommunicationCreateView, CommunicationUpdateView, CommunicationDeleteView
 

	
 
from helpers import PermissionTestMixin, create_get_request, generate_get_response, FakeMessages
 

	
 

	
 
class IndexViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    sufficient_permissions = ("view",)
 
    view_class = IndexView
 

	
 
    def test_context_no_projects(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called and
 
        no projects are available.
 
        """
 

	
 
        Project.objects.all().delete()
 

	
 
        # Get the view.
 
        view = IndexView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view)
 

	
 
        # Validate the response.
 
        self.assertQuerysetEqual(response.context_data["projects"], [])
 

	
 
    def test_context_no_locations(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called and
 
        no locations are available.
 
        """
 

	
 
        Location.objects.all().delete()
 

	
 
        # Get the view.
 
        view = IndexView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view)
 

	
 
        # Validate the response.
 
        self.assertQuerysetEqual(response.context_data["locations"], [])
 

	
 
    def test_context_projects(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called and
 
        there's multiple projects available.
 
        """
 

	
 
        # Get the view.
 
        view = IndexView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view)
 

	
 
        self.assertQuerysetEqual(response.context_data["projects"], ["<Project: Test Project 1>", "<Project: Test Project 2>"])
 

	
 
    def test_locations_available(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called and
 
        there's multiple locationsg available.
 
        """
 

	
 
        # Get the view.
 
        view = IndexView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view)
 

	
 
        # Validate the response.
 
        self.assertQuerysetEqual(response.context_data["locations"], ["<Location: Test Location 1>", "<Location: Test Location 2>"])
 

	
 

	
 
class ProjectViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    sufficient_permissions = ("view",)
 
    permission_test_view_kwargs = {"pk": "1"}
 
    view_class = ProjectView
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called.
 
        """
 

	
 
        # Get the view.
 
        view = ProjectView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, pk=1)
 

	
 
        # Fetch context data from response.
 
        location, entities = response.context_data["location_entities"][0]
 

	
 
        # Set-up expected context data values.
 
        expected_entities = ["<Entity: Test Entity 1 (Test Project 1 - Test Location 1)>",
 
                             "<Entity: Test Entity 2 (Test Project 1 - Test Location 1)>"]
 

	
 
        # Validate context data.
 
        self.assertEqual(location.name, "Test Location 1")
 
        self.assertQuerysetEqual(entities, expected_entities)
 

	
 
        # Fetch context data from response.
 
        location, entities = response.context_data["location_entities"][1]
 

	
 
        # Set-up expected context data values.
 
        expected_entities = ["<Entity: Test Entity 3 (Test Project 1 - Test Location 2)>",
 
                             "<Entity: Test Subnet (Test Project 1 - Test Location 2)>"]
 

	
 
        # Validate context data.
 
        self.assertEqual(location.name, "Test Location 2")
 
        self.assertQuerysetEqual(entities, expected_entities)
 

	
 
        # Validate context data.
 
        self.assertEqual(str(response.context_data["project"]), "Test Project 1")
 

	
 

	
 
class EntityViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = EntityView
 
    sufficient_permissions = ("view",)
 
    permission_test_view_kwargs = {"pk": "1"}
 

	
 
    def test_context(self):
 
        """
 
        Tests if the form comes pre-populated with proper content.
 
        """
 

	
 
        # Get the view.
 
        view = EntityView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, pk=1)
 

	
 
        # Set-up expected context data.
 
        expected_entity = Entity.objects.get(pk=1)
 

	
 
        expected_incoming_communications = ["<Communication: Test Entity 2 -> Test Entity 1 (TCP:22)>",
 
                                            "<Communication: Test Entity 2 -> Test Entity 1 (ICMP:8)>",
 
                                            "<Communication: Test Entity 3 -> Test Entity 1 (TCP:3306)>",
 
                                            "<Communication: Test Subnet -> Test Entity 1 (TCP:22)>"]
 

	
 
        expected_outgoing_communications = ["<Communication: Test Entity 1 -> Test Entity 2 (UDP:123)>",
 
                                            "<Communication: Test Entity 1 -> Test Entity 3 (UDP:53)>"]
 

	
 
        expected_interfaces = ["<Interface: Test Entity 1 (192.168.1.1)>"]
 

	
 
        # Validate the response.
 
        self.assertQuerysetEqual(response.context_data["interfaces"], expected_interfaces)
 
        self.assertQuerysetEqual(response.context_data["incoming_communications"], expected_incoming_communications)
 
        self.assertQuerysetEqual(response.context_data["outgoing_communications"], expected_outgoing_communications)
 
        self.assertEqual(response.context_data["entity"], expected_entity)
 
        self.assertTrue("entity_iptables" in response.context_data)
 

	
 

	
 
class EntityIptablesTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_function = staticmethod(entity_iptables)
 
    sufficient_permissions = ("view",)
 
    permission_test_view_kwargs = {"pk": "1"}
 

	
 
    def test_invalid_entity(self):
 
        """
 
        Tests if a 404 is returned if no entity was found (invalid ID).
 
        """
 

	
 
        # Set-up a request.
 
        request = create_get_request()
 

	
 
        # Get the view.
 
        view = entity_iptables
 

	
 
        # Validate the response.
 
        self.assertRaises(Http404, view, request, pk=200)
 

	
 
    def test_content_type(self):
 
        """
 
        Test if correct content type is being returned by the response.
 
        """
 

	
 
        # Get the view.
 
        view = entity_iptables
 

	
 
        # Get the response.
 
        response = generate_get_response(view, pk=1)
 

	
 
        self.assertEqual(response['Content-Type'], "text/plain")
 

	
 
    def test_content_disposition(self):
 
        """
 
        Test if the correct content disposition has been set.
 
        """
 

	
 
        # Get the view.
 
        view = entity_iptables
 

	
 
        # Get the response.
 
        response = generate_get_response(view, pk=1)
 

	
 
        self.assertEqual(response['Content-Disposition'], "attachment; filename=test_entity_1-iptables.conf")
 

	
 
    def test_content(self):
 
        """
 
        Tests content produced by the view.
 
        """
 

	
 
        # Get the view.
 
        view = entity_iptables
 

	
 
        # Get the response.
 
        response = generate_get_response(view, pk=1)
 

	
 
        self.assertContains(response, ":INPUT")
 
        self.assertContains(response, ":OUTPUT")
 
        self.assertContains(response, ":FORWARD")
 

	
 

	
 
class ProjectIptablesTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_function = staticmethod(project_iptables)
 
    sufficient_permissions = ("view",)
 
    permission_test_view_kwargs = {"project_id": 1}
 

	
 
    def test_invalid_project(self):
 
        """
 
        Tests if a 404 is returned if no project was found (invalid ID).
 
        """
 

	
 
        # Set-up a request.
 
        request = create_get_request()
 

	
 
        # Get the view.
 
        view = project_iptables
 

	
 
        # Request iptables for whole project.
 
        self.assertRaises(Http404, view, request, 200)
 
        # Request iptables for project location
 
        self.assertRaises(Http404, view, request, 200, 1)
 

	
 
    def test_invalid_location(self):
 
        """
 
        Tests if a 404 is returned if no location was found (invalid ID).
 
        """
 

	
 
        # Set-up a request.
 
        request = create_get_request()
 

	
 
        # Get the view.
 
        view = project_iptables
 

	
 
        # Request iptables for project location
 
        self.assertRaises(Http404, view, request, 1, 200)
 

	
 
    def test_content_type(self):
 
        """
 
        Test if correct content type is being returned by the response.
 
        """
 

	
 
        # Get the view.
 
        view = project_iptables
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, 1)
 

	
 
        # Validate the response.
 
        self.assertEqual(response['Content-Type'], "application/zip")
 

	
 
    def test_content_disposition(self):
 
        """
 
        Test if the correct content disposition has been set.
 
        """
 

	
 
        # Get the view.
 
        view = project_iptables
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, 1)
 
        self.assertEqual(response['Content-Disposition'], 'attachment; filename="test_project_1-iptables.zip"')
 

	
 
        response = generate_get_response(view, None, 1, 1)
 
        self.assertEqual(response['Content-Disposition'], 'attachment; filename="test_project_1-test_location_1-iptables.zip"')
 

	
 
    def test_content_project(self):
 
        """
 
        Verifies that the content is properly generated when the view is called
 
        for an entire project.
 
        """
 

	
 
        # Get the view.
 
        view = project_iptables
 

	
 
        # Get the response.
 
        response = generate_get_response(project_iptables, None, 1)
 

	
 
        buff = StringIO(response.content)
 

	
 
        zipped_iptables = ZipFile(buff, "r", ZIP_DEFLATED)
 
        expected_zip_files = ["test_entity_1-iptables.conf",
 
                              "test_entity_2-iptables.conf",
 
                              "test_entity_3-iptables.conf",
 
                              "test_subnet-iptables.conf"]
 

	
 
        self.assertEqual(len(zipped_iptables.namelist()), 4)
 
        self.assertEqual(zipped_iptables.namelist(), expected_zip_files)
 

	
 
        for filename in expected_zip_files:
 
            iptables_file = zipped_iptables.read(filename)
 
            self.assertIn(":INPUT", iptables_file)
 
            self.assertIn(":OUTPUT", iptables_file)
 
            self.assertIn(":FORWARD", iptables_file)
 

	
 
        zipped_iptables.close()
 

	
 
    def test_content_location(self):
 
        """
 
        Verifies that the content is properly generated when the view is called
 
        for an entire project.
 
        """
 

	
 
        # Get the view.
 
        view = project_iptables
 

	
 
        # Get the response.
 
        response = generate_get_response(project_iptables, None, 1, 1)
 

	
 
        buff = StringIO(response.content)
 

	
 
        zipped_iptables = ZipFile(buff, "r", ZIP_DEFLATED)
 
        expected_zip_files = ["test_entity_1-iptables.conf",
 
                              "test_entity_2-iptables.conf"]
 

	
 
        self.assertEqual(len(zipped_iptables.namelist()), 2)
 
        self.assertEqual(zipped_iptables.namelist(), expected_zip_files)
 

	
 
        for filename in expected_zip_files:
 
            iptables_file = zipped_iptables.read(filename)
 
            self.assertIn(":INPUT", iptables_file)
 
            self.assertIn(":OUTPUT", iptables_file)
 
            self.assertIn(":FORWARD", iptables_file)
 

	
 
        zipped_iptables.close()
 

	
 

	
 
class ProjectCreateViewTest(PermissionTestMixin, TestCase):
 

	
 
    view_class = ProjectCreateView
 
    sufficient_permissions = ("add_project",)
 

	
 

	
 
class ProjectUpdateViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = ProjectUpdateView
 
    sufficient_permissions = ("change_project",)
 
    permission_test_view_kwargs = {"pk": 1}
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called for
 
        specific project.
 
        """
 

	
 
        # Get the view.
 
        view = ProjectUpdateView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, pk=1)
 

	
 
        self.assertEqual(response.context_data["project"].name, "Test Project 1")
 

	
 

	
 
class ProjectDeleteViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = ProjectDeleteView
 
    sufficient_permissions = ("delete_project",)
 
    permission_test_view_kwargs = {"pk": "1"}
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called for
 
        specific project.
 
        """
 

	
 
        # Get the expected project.
 
        project = Project.objects.get(pk=1)
 

	
 
        # Get the view.
 
        view = ProjectDeleteView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, pk=1)
 

	
 
        self.assertEqual(response.context_data["project"], project)
 

	
 
    def test_message(self):
 
        """
 
        Tests if the message gets added when the project is deleted.
 
        """
 

	
 
        # Get the view.
 
        view = ProjectDeleteView.as_view()
 

	
 
        # Generate the request.
 
        request = RequestFactory().post("/fake-path/")
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 
        request._messages = FakeMessages()
 

	
 
        # Get the response.
 
        response = view(request, pk=1)
 

	
 
        self.assertIn("Project Test Project 1 has been removed.", request._messages.messages)
 

	
 

	
 
class LocationCreateViewTest(PermissionTestMixin, TestCase):
 

	
 
    view_class = LocationCreateView
 
    sufficient_permissions = ("add_location",)
 

	
 
    def test_form_styling(self):
 
        """
 
        Tests if proper form styling is being applied.
 
        """
 

	
 
        # Get the view.
 
        view = LocationCreateView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view)
 

	
 
        self.assertContains(response, 'class="span6 textinput')
 
        self.assertContains(response, 'class="span6 textarea')
 
        self.assertContains(response, 'placeholder="New Location"')
 
        self.assertContains(response, 'placeholder="Description for new location."')
 

	
 

	
 
class LocationUpdateViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = LocationUpdateView
 
    sufficient_permissions = ("change_location",)
 
    permission_test_view_kwargs = {"pk": 1}
 

	
 
    def test_form_styling(self):
 
        """
 
        Tests if proper form styling is being applied.
 
        """
 

	
 
        # Get the view.
 
        view = LocationUpdateView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, pk=1)
 

	
 
        self.assertContains(response, 'class="span6 textinput')
 
        self.assertContains(response, 'class="span6 textarea')
 
        self.assertContains(response, 'placeholder="Location name"')
 
        self.assertContains(response, 'placeholder="Description for location."')
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called for
 
        specific location.
 
        """
 

	
 
        # Get the view.
 
        view = LocationUpdateView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, pk=1)
 

	
 
        self.assertEqual(response.context_data["location"].name, "Test Location 1")
 

	
 

	
 
class LocationDeleteViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = LocationDeleteView
 
    sufficient_permissions = ("delete_location",)
 
    permission_test_view_kwargs = {"pk": "1"}
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called for
 
        specific location.
 
        """
 

	
 
        # Get the expected location.
 
        location = Location.objects.get(pk=1)
 

	
 
        # Get the view.
 
        view = LocationDeleteView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, pk=1)
 

	
 
        self.assertEqual(response.context_data["location"], location)
 

	
 
    def test_message(self):
 
        """
 
        Tests if the message gets added when the location is deleted.
 
        """
 

	
 
        # Get the view.
 
        view = LocationDeleteView.as_view()
 

	
 
        # Generate the request.
 
        request = RequestFactory().post("/fake-path/")
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        request._messages = FakeMessages()
 

	
 
        # Get the response.
 
        response = view(request, pk=1)
 

	
 
        self.assertIn("Location Test Location 1 has been removed.", request._messages.messages)
 

	
 

	
 
class EntityCreateViewTest(PermissionTestMixin, TestCase):
 

	
 
    view_class = EntityCreateView
 
    sufficient_permissions = ("add_entity",)
 

	
 
    def setUp(self):
 
        """
 
        Sets-up some data necessary for testing.
 
        """
 

	
 
        # Set-up some data for testing.
 
        Project.objects.create(name="Test Project 1", description="This is test project 1.")
 
        Project.objects.create(name="Test Project 2", description="This is test project 2.")
 
        Location.objects.create(name="Test Location 1", description="This is test location 1.")
 
        Location.objects.create(name="Test Location 2", description="This is test location 2.")
 

	
 
    def test_form_project_limit(self):
 
        """
 
        Tests if the queryset is properly limitted to specific project if GET
 
        parameters is passed.
 
        """
 

	
 
        # Set-up the view.
 
        view = EntityCreateView()
 
        view.request = RequestFactory().get("/fake-path?project=1")
 
        view.object = None
 

	
 
        # Get the form.
 
        form = view.get_form(view.get_form_class())
 

	
 
        self.assertQuerysetEqual(form.fields["project"].queryset, ["<Project: Test Project 1>"])
 

	
 
    def test_form_location_limit(self):
 
        """
 
        Tests if the queryset is properly limitted to specific location if GET
 
        parameters is passed.
 
        """
 

	
 
        # Set-up the view.
 
        view = EntityCreateView()
 
        view.request = RequestFactory().get("/fake-path?location=1")
 
        view.object = None
 

	
 
        # Get the form.
 
        form = view.get_form(view.get_form_class())
 

	
 
        self.assertQuerysetEqual(form.fields["location"].queryset, ["<Location: Test Location 1>"])
 

	
 
    def test_initial_project(self):
 
        """
 
        Tests if the choice field for project is defaulted to project passed as
 
        part of GET parameters.
 
        """
 

	
 
        view = EntityCreateView()
 
        view.request = RequestFactory().get("/fake-path?project=1")
 
        view.object = None
 

	
 
        initial = view.get_initial()
 

	
 
        self.assertDictContainsSubset({"project": "1"}, initial)
 

	
 
    def test_initial_location(self):
 
        """
 
        Tests if the choice field for location is defaulted to location passed
 
        as part of GET parameters.
 
        """
 

	
 
        view = EntityCreateView()
 
        view.request = RequestFactory().get("/fake-path?location=1")
 
        view.object = None
 

	
 
        initial = view.get_initial()
 

	
 
        self.assertDictContainsSubset({"location": "1"}, initial)
 

	
 

	
 
class EntityDeleteViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = EntityDeleteView
 
    sufficient_permissions = ("delete_entity",)
 
    permission_test_view_kwargs = {"pk": 1}
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called for
 
        specific entity.
 
        """
 

	
 
        # Get the expected entity.
 
        entity = Entity.objects.get(pk=1)
 

	
 
        # Get the view.
 
        view = EntityDeleteView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, pk=1)
 

	
 
        self.assertEqual(response.context_data["entity"], entity)
 

	
 
    def test_message(self):
 
        """
 
        Tests if the message gets added when the entity is deleted.
 
        """
 

	
 
        # Get the view.
 
        view = EntityDeleteView.as_view()
 

	
 
        # Generate the request.
 
        request = RequestFactory().post("/fake-path/")
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        request._messages = FakeMessages()
 

	
 
        # Get the response.
 
        response = view(request, pk=1)
 

	
 
        self.assertIn("Entity Test Entity 1 has been removed.", request._messages.messages)
 

	
 
    def test_success_url(self):
 
        """
 
        Validate that the success URL is set properly after delete.
 
        """
 

	
 
        # Get the view.
 
        view = EntityDeleteView.as_view()
 

	
 
        # Generate the request
 
        request = RequestFactory().post("/fake-path/")
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 
        request._messages = FakeMessages()
 

	
 
        # Get the response.
 
        response = view(request, pk=1)
 

	
 
        self.assertEqual(response["Location"], reverse("project", args=(1,)))
 

	
 

	
 
class EntityUpdateViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = EntityUpdateView
 
    sufficient_permissions = ("change_entity",)
 
    permission_test_view_kwargs = {"pk": 1}
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called for
 
        specific entity.
 
        """
 

	
 
        # Get the view.
 
        view = EntityUpdateView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, pk=1)
 

	
 
        self.assertEqual(response.context_data["entity"].name, "Test Entity 1")
 

	
 

	
 
class InterfaceCreateViewTest(PermissionTestMixin, TestCase):
 

	
 
    view_class = InterfaceCreateView
 
    sufficient_permissions = ("add_interface",)
 

	
 
    def setUp(self):
 
        """
 
        Sets-up some data necessary for testing.
 
        """
 

	
 
        # Set-up some data for testing.
 
        project = Project.objects.create(name="Test Project", description="This is test project.")
 
        location = Location.objects.create(name="Test Location", description="This is test location.")
 
        Entity.objects.create(name="Test Entity 1", description="This is test entity 1.", project=project, location=location)
 
        Entity.objects.create(name="Test Entity 2", description="This is test entity 2.", project=project, location=location)
 

	
 
    def test_form_entity_limit(self):
 
        """
 
        Tests if the queryset is properly limitted to specific entity if GET
 
        parameter is passed.
 
        """
 

	
 
        # Set-up the view.
 
        view = InterfaceCreateView()
 
        view.request = RequestFactory().get("/fake-path?entity=1")
 
        view.object = None
 

	
 
        # Get the form.
 
        form = view.get_form(view.get_form_class())
 

	
 
        self.assertQuerysetEqual(form.fields["entity"].queryset, ["<Entity: Test Entity 1 (Test Project - Test Location)>"])
 

	
 
    def test_initial_project(self):
 
        """
 
        Tests if the choice field for entity is defaulted to entity passed as
 
        part of GET parameters.
 
        """
 

	
 
        view = InterfaceCreateView()
 
        view.request = RequestFactory().get("/fake-path?entity=1")
 
        view.object = None
 

	
 
        initial = view.get_initial()
 

	
 
        self.assertDictContainsSubset({"entity": "1"}, initial)
 

	
 
    def test_success_url(self):
 
        """
 
        Validate that the success URL is set properly after interface is
 
        created.
 
        """
 

	
 
        # Get the view.
 
        view = InterfaceCreateView.as_view()
 

	
 
        # Generate the request.
 
        post_data = {"name": "eth0", "description": "Main interface.",
 
                     "entity": "1", "address": "192.168.1.1",
 
                     "netmask": "255.255.255.255"}
 
        request = RequestFactory().post("/fake-path/", data=post_data)
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        # Get the response.
 
        response = view(request, pk=1)
 

	
 
        self.assertEqual(response["Location"], reverse("entity", args=(1,)))
 
        self.assertEqual(response.status_code, 302)
 

	
 

	
 
class InterfaceUpdateViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = InterfaceUpdateView
 
    sufficient_permissions = ("change_interface",)
 
    permission_test_view_kwargs = {"pk": 1}
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called for
 
        specific entity.
 
        """
 

	
 
        # Get the view.
 
        view = InterfaceUpdateView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, pk=1)
 

	
 
        # Set-up expected interface.
 
        interface = Interface.objects.get(pk=1)
 

	
 
        self.assertEqual(response.context_data["interface"], interface)
 

	
 
    def test_form_entity_limit(self):
 
        """
 
        Tests if the queryset is properly limitted to specific project's
 
        entities.
 
        """
 

	
 
        # Set-up the view.
 
        view = InterfaceUpdateView()
 
        view.request = RequestFactory().get("/fake-path/1")
 
        view.object = Interface.objects.get(pk=1)
 

	
 
        # Get the form.
 
        form = view.get_form(view.get_form_class())
 

	
 
        expected_entities = ["<Entity: Test Entity 1 (Test Project 1 - Test Location 1)>",
 
                             "<Entity: Test Entity 2 (Test Project 1 - Test Location 1)>",
 
                             "<Entity: Test Entity 3 (Test Project 1 - Test Location 2)>",
 
                             "<Entity: Test Subnet (Test Project 1 - Test Location 2)>"]
 

	
 
        self.assertQuerysetEqual(form.fields["entity"].queryset, expected_entities)
 

	
 
    def test_success_url(self):
 
        """
 
        Validate that the success URL is set properly after update.
 
        """
 

	
 
        # Get the view.
 
        view = InterfaceUpdateView.as_view()
 

	
 
        # Get the interface object.
 
        interface = Interface.objects.get(pk=1)
 

	
 
        # Generate the request.
 
        post_data = {"name": interface.name, "description": interface.name,
 
                     "entity": "1", "address": "192.168.1.1",
 
                     "netmask": "255.255.255.255"}
 
        request = RequestFactory().post("/fake-path/", data=post_data)
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        # Get the response.
 
        response = view(request, pk=1)
 

	
 
        self.assertEqual(response["Location"], reverse("entity", args=(1,)))
 
        self.assertEqual(response.status_code, 302)
 

	
 

	
 
class InterfaceDeleteViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = InterfaceDeleteView
 
    sufficient_permissions = ("delete_interface",)
 
    permission_test_view_kwargs = {"pk": 1}
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up when the view is called for
 
        specific interface.
 
        """
 

	
 
        # Get the expected entity.
 
        interface = Interface.objects.get(pk=1)
 

	
 
        # Get the view.
 
        view = InterfaceDeleteView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, pk=1)
 

	
 
        self.assertEqual(response.context_data["interface"], interface)
 

	
 
    def test_message(self):
 
        """
 
        Tests if the message gets added when the interface is deleted.
 
        """
 

	
 
        # Get the view.
 
        view = InterfaceDeleteView.as_view()
 

	
 
        # Generate the request.
 
        request = RequestFactory().post("/fake-path/")
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        request._messages = FakeMessages()
 

	
 
        # Get the response.
 
        response = view(request, pk=1)
 

	
 
        self.assertIn("Interface eth0 has been removed.", request._messages.messages)
 

	
 
    def test_success_url(self):
 
        """
 
        Validate that the success URL is set properly after delete.
 
        """
 

	
 
        # Get the view.
 
        view = InterfaceDeleteView.as_view()
 

	
 
        # Generate the request
 
        request = RequestFactory().post("/fake-path/")
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 
        request._messages = FakeMessages()
 

	
 
        # Get the response.
 
        response = view(request, pk=1)
 

	
 
        self.assertEqual(response["Location"], reverse("entity", args=(1,)))
 

	
 

	
 
class CommunicationCreateViewTest(PermissionTestMixin, TestCase):
 

	
 
    view_class = CommunicationCreateView
 
    sufficient_permissions = ("add_communication",)
 

	
 
    def setUp(self):
 
        """
 
        Sets-up some data necessary for testing.
 
        """
 

	
 
        # Set-up some data for testing.
 
        project1 = Project.objects.create(name="Test Project 1", description="This is test project 1.")
 
        project2 = Project.objects.create(name="Test Project 2", description="This is test project 2.")
 
        location = Location.objects.create(name="Test Location", description="This is test location.")
 
        entity1 = Entity.objects.create(name="Test Entity 1", description="This is test entity 1.", project=project1, location=location)
 
        entity2 = Entity.objects.create(name="Test Entity 2", description="This is test entity 2.", project=project1, location=location)
 
        entity3 = Entity.objects.create(name="Test Entity 3", description="This is test entity 3.", project=project2, location=location)
 
        Interface.objects.create(name="eth0", description="Main interface", entity=entity1, address="192.168.1.1", netmask="255.255.255.255")
 
        Interface.objects.create(name="eth0", description="Main interface", entity=entity2, address="192.168.1.2", netmask="255.255.255.255")
 
        Interface.objects.create(name="eth0", description="Main interface", entity=entity3, address="192.168.1.3", netmask="255.255.255.255")
 

	
 
    def test_interface_limit_from_entity(self):
 
        """
 
        Tests if the queryset is properly limitted if GET parameter is passed.
 
        """
 

	
 
        # Set-up the view.
 
        view = CommunicationCreateView()
 
        view.request = RequestFactory().get("/fake-path?from_entity=1")
 
        view.object = None
 

	
 
        # Get the form.
 
        form = view.get_form(view.get_form_class())
 

	
 
        # Set-up expected interfaces.
 
        expected_interfaces = ["<Interface: Test Entity 1 (192.168.1.1)>",
 
                               "<Interface: Test Entity 2 (192.168.1.2)>"]
 

	
 
        self.assertQuerysetEqual(form.fields["source"].queryset, expected_interfaces)
 
        self.assertQuerysetEqual(form.fields["destination"].queryset, expected_interfaces)
 

	
 
    def test_interface_limit_to_entity(self):
 
        """
 
        Tests if the queryset is properly limitted if GET parameter is passed.
 
        """
 

	
 
        # Set-up the view.
 
        view = CommunicationCreateView()
 
        view.request = RequestFactory().get("/fake-path?to_entity=1")
 
        view.object = None
 

	
 
        # Get the form.
 
        form = view.get_form(view.get_form_class())
 

	
 
        # Set-up expected interfaces.
 
        expected_interfaces = ["<Interface: Test Entity 1 (192.168.1.1)>",
 
                               "<Interface: Test Entity 2 (192.168.1.2)>"]
 

	
 
        self.assertQuerysetEqual(form.fields["source"].queryset, expected_interfaces)
 
        self.assertQuerysetEqual(form.fields["destination"].queryset, expected_interfaces)
 

	
 
    def test_interface_limit_project(self):
 
        """
 
        Tests if the queryset is properly limitted if GET parameter is passed.
 
        """
 

	
 
        # Set-up the view.
 
        view = CommunicationCreateView()
 
        view.request = RequestFactory().get("/fake-path?project=1")
 
        view.object = None
 

	
 
        # Get the form.
 
        form = view.get_form(view.get_form_class())
 

	
 
        # Set-up expected interfaces.
 
        expected_interfaces = ["<Interface: Test Entity 1 (192.168.1.1)>",
 
                               "<Interface: Test Entity 2 (192.168.1.2)>"]
 

	
 
        self.assertQuerysetEqual(form.fields["source"].queryset, expected_interfaces)
 
        self.assertQuerysetEqual(form.fields["destination"].queryset, expected_interfaces)
 

	
 
    def test_initial_from_entity(self):
 
        """
 
        Tests if the choice field for interface is defaulted to first interface
 
        of entity passed as part of GET parameters.
 
        """
 

	
 
        # Set-up the view.
 
        view = CommunicationCreateView()
 
        view.request = RequestFactory().get("/fake-path?from_entity=1")
 
        view.object = None
 

	
 
        # Get the expected interface ID.
 
        interface = Entity.objects.get(pk=1).interface_set.all()[0]
 

	
 
        # Fetch the initial values.
 
        initial = view.get_initial()
 

	
 
        self.assertDictContainsSubset({"source": interface.pk}, initial)
 

	
 
    def test_initial_to_entity(self):
 
        """
 
        Tests if the choice field for interface is defaulted to first interface
 
        of entity passed as part of GET parameters.
 
        """
 

	
 
        # Set-up the view.
 
        view = CommunicationCreateView()
 
        view.request = RequestFactory().get("/fake-path?to_entity=1")
 
        view.object = None
 

	
 
        # Get the expected interface ID.
 
        interface = Entity.objects.get(pk=1).interface_set.all()[0]
 

	
 
        # Fetch the initial value.
 
        initial = view.get_initial()
 

	
 
        self.assertDictContainsSubset({"destination": interface.pk}, initial)
 

	
 
    def test_initial_invalid_from_entity(self):
 
        """
 
        Tests if the choice fields for source and destination interfaces are not
 
        defaulted in case invalid entity ID is passed as GET parameter.
 
        """
 

	
 
        # Set-up the view.
 
        view = CommunicationCreateView()
 
        view.request = RequestFactory().get("/fake-path?from_entity=10")
 
        view.object = None
 

	
 
        # Get the initial values.
 
        initial = view.get_initial()
 

	
 
        self.assertEqual(len(initial), 0)
 

	
 
    def test_initial_invalid_to_entity(self):
 
        """
 
        Tests if the choice fields for source and destination interfaces are not
 
        defaulted in case invalid entity ID is passed as GET parameter.
 
        """
 

	
 
        # Set-up the view.
 
        view = CommunicationCreateView()
 
        view.request = RequestFactory().get("/fake-path?to_entity=10")
 
        view.object = None
 

	
 
        # Get the initial values.
 
        initial = view.get_initial()
 

	
 
        self.assertEqual(len(initial), 0)
 

	
 
    def test_initial_invalid_project(self):
 
        """
 
        Tests if the choice fields for source and destination interfaces are not
 
        defaulted in case invalid project ID is passed as GET parameter.
 
        """
 

	
 
        # Set-up the view.
 
        view = CommunicationCreateView()
 
        view.request = RequestFactory().get("/fake-path?project=10")
 
        view.object = None
 

	
 
        # Get the initial values.
 
        initial = view.get_initial()
 

	
 
        self.assertEqual(len(initial), 0)
 

	
 
    def test_success_url_from_entity(self):
 
        """
 
        Validate that the success URL is set properly after communication is
 
        created if origin entity is provided.
 
        """
 

	
 
        # Get the view.
 
        view = CommunicationCreateView.as_view()
 

	
 
        # Generate the request.
 
        source = Interface.objects.get(pk=1)
 
        destination = Interface.objects.get(pk=2)
 
        post_data = {"source": source.pk,
 
                     "destination": destination.pk,
 
                     "protocol": "TCP",
 
                     "port": "22",
 
                     "description": "SSH."}
 
        request = RequestFactory().post("/fake-path?from_entity=1", data=post_data)
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        # Get the response.
 
        response = view(request)
 

	
 
        self.assertEqual(response["Location"], reverse("entity", args=(1,)))
 
        self.assertEqual(response.status_code, 302)
 

	
 
    def test_success_url_to_entity(self):
 
        """
 
        Validate that the success URL is set properly after communication is
 
        created if destination entity is provided.
 
        """
 

	
 
        # Get the view.
 
        view = CommunicationCreateView.as_view()
 

	
 
        # Generate the request.
 
        source = Interface.objects.get(pk=1)
 
        destination = Interface.objects.get(pk=2)
 
        post_data = {"source": source.pk,
 
                     "destination": destination.pk,
 
                     "protocol": "TCP",
 
                     "port": "22",
 
                     "description": "SSH."}
 
        request = RequestFactory().post("/fake-path?to_entity=2", data=post_data)
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        # Get the response.
 
        response = view(request)
 

	
 
        self.assertEqual(response["Location"], reverse("entity", args=(2,)))
 
        self.assertEqual(response.status_code, 302)
 

	
 
    def test_success_url_no_entity(self):
 
        """
 
        Validate that the success URL is set properly after communication is
 
        created if no entity source/destinantion is passed on.
 
        """
 

	
 
        # Get the view.
 
        view = CommunicationCreateView.as_view()
 

	
 
        # Generate the request.
 
        source = Interface.objects.get(pk=1)
 
        destination = Interface.objects.get(pk=2)
 
        post_data = {"source": source.pk,
 
                     "destination": destination.pk,
 
                     "protocol": "TCP",
 
                     "port": "22",
 
                     "description": "SSH."}
 
        request = RequestFactory().post("/fake-path", data=post_data)
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        # Get the response.
 
        response = view(request)
 

	
 
        self.assertEqual(response["Location"], reverse("entity", args=(1,)))
 
        self.assertEqual(response.status_code, 302)
 

	
 

	
 
class CommunicationUpdateViewTest(PermissionTestMixin, TestCase):
 

	
 
    fixtures = ['test-data.json']
 

	
 
    view_class = CommunicationUpdateView
 
    sufficient_permissions = ("change_communication",)
 
    permission_test_view_kwargs = {"pk": 1}
 

	
 
    def test_context(self):
 
        """
 
        Verifies that the context is properly set-up.
 
        """
 

	
 
        # Get the view.
 
        view = CommunicationUpdateView.as_view()
 

	
 
        # Get the response.
 
        response = generate_get_response(view, None, pk=1)
 

	
 
        # Set-up expected interface.
 
        communication = Communication.objects.get(pk=1)
 

	
 
        self.assertEqual(response.context_data["communication"], communication)
 

	
 
    def test_form_interface_limit(self):
 
        """
 
        Tests if the queryset is properly limitted to specific project's
 
        entity interfaces.
 
        """
 

	
 
        # Set-up the view.
 
        view = CommunicationUpdateView()
 
        view.request = RequestFactory().get("/fake-path/1")
 
        view.object = Communication.objects.get(pk=1)
 

	
 
        # Get the form.
 
        form = view.get_form(view.get_form_class())
 

	
 
        expected_interfaces = ["<Interface: Test Entity 1 (192.168.1.1)>",
 
                               "<Interface: Test Entity 2 (192.168.1.2)>",
 
                               "<Interface: Test Entity 3 (192.168.1.3)>",
 
                               "<Interface: Test Subnet (192.168.2.0/255.255.255.0)>"]
 

	
 
        self.assertQuerysetEqual(form.fields["source"].queryset, expected_interfaces)
 
        self.assertQuerysetEqual(form.fields["destination"].queryset, expected_interfaces)
 

	
 
    def test_success_url_from_entity(self):
 
        """
 
        Validate that the success URL is set properly after update.
 
        """
 

	
 
        # Get the view.
 
        view = CommunicationUpdateView.as_view()
 

	
 
        # Get the communication object.
 
        communication = Communication.objects.get(pk=1)
 

	
 
        # Generate the request.
 
        post_data = {"source": communication.source.pk,
 
                     "destination": communication.destination.pk,
 
                     "protocol": communication.protocol,
 
                     "port": communication.port}
 
        request = RequestFactory().post("/fake-path?from_entity=1", data=post_data)
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        # Get the response.
 
        response = view(request, pk=1)
 

	
 
        self.assertEqual(response["Location"], reverse("entity", args=(1,)))
 
        self.assertEqual(response.status_code, 302)
 

	
 
    def test_success_url_to_entity(self):
 
        """
 
        Validate that the success URL is set properly after update.
 
        """
 

	
 
        # Get the view.
 
        view = CommunicationUpdateView.as_view()
 

	
 
        # Get the communication object.
 
        communication = Communication.objects.get(pk=1)
 

	
 
        # Generate the request.
 
        post_data = {"source": communication.source.pk,
 
                     "destination": communication.destination.pk,
conntrackt/views.py
Show inline comments
 
# Standard library imports.
 
from StringIO import StringIO
 
from zipfile import ZipFile, ZIP_DEFLATED
 

	
 
# Django imports.
 
from django.contrib.auth.decorators import permission_required
 
from django.contrib import messages
 
from django.core.urlresolvers import reverse, reverse_lazy
 
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
 

	
 
# Third-party application imports.
 
from braces.views import MultiplePermissionsRequiredMixin
 

	
 
# Application imports.
 
from .forms import EntityForm, InterfaceForm, CommunicationForm, ProjectForm
 
from .forms import ProjectForm, LocationForm, EntityForm, InterfaceForm, CommunicationForm
 
from .models import Project, Entity, Location, Interface, Communication
 
from .utils import generate_entity_iptables
 

	
 

	
 
class IndexView(MultiplePermissionsRequiredMixin, TemplateView):
 
    """
 
    Custom view used for rendering the index page.
 
    """
 

	
 
    template_name = 'conntrackt/index.html'
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.view",),
 
        }
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def get_context_data(self, **kwargs):
 
        """
 
        Returns the context data that should be used for rendering of the
 
        template.
 

	
 
        Adds the 'projects' context object containing all of the projects
 
        available sorted aplhabetically by name.
 
        """
 

	
 
        # Set the context using the parent class.
 
        context = super(IndexView, self).get_context_data(**kwargs)
 

	
 
        # Store information about all projcts in context.
 
        context['projects'] = Project.objects.all().order_by('name')
 

	
 
        # Store information about all locations in context.
 
        context['locations'] = Location.objects.all().order_by('name')
 

	
 
        return context
 

	
 

	
 
class ProjectView(MultiplePermissionsRequiredMixin, DetailView):
 
    """
 
    Custom view for presenting the project information.
 
    """
 

	
 
    model = Project
 

	
 
    permissions = {
 
        "all": ("conntrackt.view",),
 
        }
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def get_context_data(self, **kwargs):
 
        """
 
        Returns the context data that should be used for rendering of the
 
        template.
 

	
 
        Adds the 'location_entities' context object that contains tuples of the
 
        form '(location, entities)', where location is an instance of a
 
        Location, and entities is a query set of entities that belong to that
 
        particular location, and to related project.
 
        """
 

	
 
        # Set the context using the parent class.
 
        context = super(ProjectView, self).get_context_data(**kwargs)
 

	
 
        # Set-up an array that will contaion (location, entities) tuples.
 
        location_entities = []
 

	
 
        # Add the (location, entities) tuple for each location that has entities
 
        # belonging to the related project.
 
        for location in Location.objects.filter(entity__project=self.object).distinct().order_by("name"):
 
            entities = Entity.objects.filter(project=self.object, location=location)
 
            location_entities.append((location, entities))
 

	
 
        # Add the (location, entities) tuples to context.
 
        context['location_entities'] = location_entities
 

	
 
        # Finally return the context.
 
        return context
 

	
 

	
 
class EntityView(MultiplePermissionsRequiredMixin, DetailView):
 
    """
 
    Custom view for presenting entity information.
 
    """
 

	
 
    # Optimise the query to fetch the related data from reverse relationships.
 
    queryset = Entity.objects.all()
 
    queryset = queryset.prefetch_related('interface_set__destination_set__source__entity')
 
    queryset = queryset.prefetch_related('interface_set__destination_set__destination__entity')
 
    queryset = queryset.prefetch_related('interface_set__source_set__source__entity')
 
    queryset = queryset.prefetch_related('interface_set__source_set__destination__entity')
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.view",),
 
        }
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def get_context_data(self, **kwargs):
 
        """
 
        Returns the context data that should be used for rendering of the
 
        template.
 

	
 
        Adds the 'entity_iptables' context object that contains full iptables
 
        rules generated for the entity.
 
        """
 

	
 
        # Call the parent class method.
 
        context = super(EntityView, self).get_context_data(**kwargs)
 

	
 
        # Add the rendered iptables rules to the context.
 
        context['entity_iptables'] = generate_entity_iptables(self.object)
 

	
 
        # Add the incoming and outgoing commmunication to the context.
 
        context["incoming_communications"] = self.object.incoming_communications()
 
        context["outgoing_communications"] = self.object.outgoing_communications()
 

	
 
        # Add the interfaces to the context.
 
        context["interfaces"] = self.object.interface_set.all().order_by("name")
 

	
 
        # Add project/location to the context.
 
        context["project"] = self.object.project
 
        context["location"] = self.object.location
 

	
 
        return context
 

	
 

	
 
@permission_required("conntrackt.view", raise_exception=True)
 
def entity_iptables(request, pk):
 
    """
 
    Custom view that returns response containing iptables rules generated for an
 
    entity.
 

	
 
    Makes sure to set the Content-Disposition of a response in order to
 
    signal the browser it should start download of this view's response
 
    immediately. Also sets the suggested filename for it.
 

	
 
    Arguments:
 

	
 
        pk - Primary key of the Entity object for which the rules should be
 
        generated.
 

	
 
    Returns:
 

	
 
        Response object that contains the iptables rules for specified entity.
 
    """
 

	
 
    # Fetch the entity, and construct the response with iptables rules as
 
    # content.
 
    entity = get_object_or_404(Entity, pk=pk)
 
    content = generate_entity_iptables(entity)
 
    response = HttpResponse(content, mimetype='text/plain')
 

	
 
    # Add the Content-Disposition information for the browser, telling the
 
    # browser to download the file with suggested filename.
 
    response['Content-Disposition'] = "attachment; filename=%s-iptables.conf" % entity.name.lower().replace(" ", "_")
 

	
 
    return response
 

	
 

	
 
@permission_required("conntrackt.view", raise_exception=True)
 
def project_iptables(request, project_id, location_id=None):
 
    """
 
    Custom view for obtaining iptables for all entities of a project or project
 
    location in a single ZIP file.
 

	
 
    Arguments:
 

	
 
        request - Request object.
 

	
 
        project_id - Unique ID of the project for whose entities the iptables
 
        rules should be generated.
 

	
 
        location_id - Optional unique ID of the project location for whose
 
        entities the iptables rules should be generated. Default is None, which
 
        means generate rules for _all_ entities in a project.
 

	
 
    Returns:
 

	
 
        Response object that contains the ZIP file and Content-Disposition
 
        information.
 
    """
 

	
 
    # Fetch the project.
 
    project = get_object_or_404(Project, pk=project_id)
 

	
 
    # Set-up a string IO object to which we'll write the ZIP file (in-memory).
 
    buff = StringIO()
 

	
 
    # Create a new ZIP file in-memory.
 
    zipped_iptables = ZipFile(buff, "w", ZIP_DEFLATED)
 

	
 
    # Create the response object, setting the mime type so browser could offer
 
    # to open the file with program as well.
 
    response = HttpResponse(mimetype='application/zip')
 

	
 
    # If specific location was specified, get the entities that are part of that
 
    # project location only, otherwise fetch all of the project's entities. Also
 
    # set-up the filename that will be suggested to the browser.
 
    if location_id:
 
        location = get_object_or_404(Location, pk=location_id)
 
        entities = project.entity_set.filter(location=location)
 
        filename = '%s-%s-iptables.zip' % (project.name.lower().replace(" ", "_"), location.name.lower().replace(" ", "_"))
 
    else:
 
        entities = project.entity_set.all()
 
        filename = '%s-iptables.zip' % (project.name.lower().replace(" ", "_"))
 

	
 
    # Render iptables rules for each entity, placing them in the ZIP archive.
 
    for entity in entities:
 
        entity_iptables = generate_entity_iptables(entity)
 
        zipped_iptables.writestr("%s-iptables.conf" % entity.name.lower().replace(" ", "_"), entity_iptables)
 

	
 
    # Close the archive, and flush the buffer.
 
    zipped_iptables.close()
 
    buff.flush()
 

	
 
    # Write the contents of our buffer (ZIP archive) to response content, and
 
    # close the IO string.
 
    response.write(buff.getvalue())
 
    buff.close()
 

	
 
    # Set the Content-Disposition so the browser would know it should download
 
    # the archive, and suggest the filename.
 
    response['Content-Disposition'] = 'attachment; filename="%s"' % filename
 

	
 
    # Finally return the response object.
 
    return response
 

	
 

	
 
class ProjectCreateView(MultiplePermissionsRequiredMixin, CreateView):
 
    """
 
    View for creating a new project.
 
    """
 

	
 
    model = Project
 
    form_class = ProjectForm
 
    template_name_suffix = "_create_form"
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.add_project",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 

	
 
class ProjectUpdateView(MultiplePermissionsRequiredMixin, UpdateView):
 
    """
 
    View for modifying an existing project.
 
    """
 

	
 
    model = Project
 
    form_class = ProjectForm
 
    template_name_suffix = "_update_form"
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.change_project",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 

	
 
class ProjectDeleteView(MultiplePermissionsRequiredMixin, DeleteView):
 
    """
 
    View for deleting a project.
 
    """
 

	
 
    model = Project
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.delete_project",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    success_url = reverse_lazy("index")
 

	
 
    def post(self, *args, **kwargs):
 
        """
 
        Add a success message that will be displayed to the user to confirm the
 
        project deletion.
 
        """
 

	
 
        messages.success(self.request, "Project %s has been removed." % self.get_object().name, extra_tags="alert alert-success")
 

	
 
        return super(ProjectDeleteView, self).post(*args, **kwargs)
 

	
 

	
 
class LocationCreateView(MultiplePermissionsRequiredMixin, CreateView):
 
    """
 
    View for creating a new location.
 
    """
 

	
 
    model = Location
 
    form_class = LocationForm
 
    template_name_suffix = "_create_form"
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.add_location",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    success_url = reverse_lazy("index")
 

	
 
    def get_form(self, form_class):
 
        """
 
        Implements an override for the default form constructed for the create
 
        view that includes some better styling of input widgets.
 
        """
 

	
 
        form = super(LocationCreateView, self).get_form(form_class)
 
        form.fields["name"].widget.attrs["class"] = "span6"
 
        form.fields["name"].widget.attrs["placeholder"] = "New Location"
 
        form.fields["description"].widget.attrs["class"] = "span6"
 
        form.fields["description"].widget.attrs["placeholder"] = "Description for new location."
 

	
 
        return form
 

	
 

	
 
class LocationUpdateView(MultiplePermissionsRequiredMixin, UpdateView):
 
    """
 
    View for modifying an existing location.
 
    """
 

	
 
    model = Location
 
    form_class = LocationForm
 
    template_name_suffix = "_update_form"
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.change_location",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    success_url = reverse_lazy("index")
 

	
 
    def get_form(self, form_class):
 
        """
 
        Implements an override for the default form constructed for the create
 
        view that includes some better styling of input widgets.
 
        """
 

	
 
        form = super(LocationUpdateView, self).get_form(form_class)
 
        form.fields["name"].widget.attrs["class"] = "span6"
 
        form.fields["name"].widget.attrs["placeholder"] = "Location name"
 
        form.fields["description"].widget.attrs["class"] = "span6"
 
        form.fields["description"].widget.attrs["placeholder"] = "Description for location."
 

	
 
        return form
 

	
 

	
 
class LocationDeleteView(MultiplePermissionsRequiredMixin, DeleteView):
 
    """
 
    View for deleting a location.
 
    """
 

	
 
    model = Location
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.delete_location",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    success_url = reverse_lazy("index")
 

	
 
    def post(self, *args, **kwargs):
 
        """
 
        Add a success message that will be displayed to the user to confirm the
 
        location deletion.
 
        """
 

	
 
        messages.success(self.request, "Location %s has been removed." % self.get_object().name, extra_tags="alert alert-success")
 

	
 
        return super(LocationDeleteView, self).post(*args, **kwargs)
 

	
 

	
 
class EntityCreateView(MultiplePermissionsRequiredMixin, CreateView):
 
    """
 
    View for creating a new entity.
 
    """
 

	
 
    model = Entity
 
    form_class = EntityForm
 
    template_name_suffix = "_create_form"
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.add_entity",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def get_form(self, form_class):
 
        """
 
        Returns an instance of form that can be used by the view.
 

	
 
        The method will limit the project or location select inputs if request
 
        contained this information.
 
        """
 

	
 
        form = super(EntityCreateView, self).get_form(form_class)
 

	
 
        # Limit the project selection if required.
 
        project_id = self.request.GET.get("project", None)
 
        if project_id:
 
            form.fields["project"].queryset = Project.objects.filter(pk=project_id)
 
            form.fields["project"].widget.attrs["readonly"] = True
 

	
 
        # Limit the location selection if required.
 
        location_id = self.request.GET.get("location", None)
 
        if location_id:
 
            form.fields["location"].queryset = Location.objects.filter(pk=location_id)
 
            form.fields["location"].widget.attrs["readonly"] = True
 

	
 
        return form
 

	
 
    def get_initial(self):
 
        """
 
        Returns initial values that should be pre-selected (if they were
 
        specified through a GET parameter).
 
        """
 

	
 
        initial = super(EntityCreateView, self).get_initial()
 

	
 
        initial["project"] = self.request.GET.get("project", None)
 
        initial["location"] = self.request.GET.get("location", None)
 

	
 
        return initial
 

	
 

	
 
class EntityUpdateView(MultiplePermissionsRequiredMixin, UpdateView):
 
    """
 
    View for updating an existing entity.
 
    """
 

	
 
    model = Entity
 
    form_class = EntityForm
 
    template_name_suffix = "_update_form"
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.change_entity",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 

	
 
class EntityDeleteView(MultiplePermissionsRequiredMixin, DeleteView):
 
    """
 
    View for deleting an entity.
 
    """
 

	
 
    model = Entity
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.delete_entity",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def post(self, *args, **kwargs):
 
        """
 
        Add a success message that will be displayed to the user to confirm the
 
        entity deletion.
 
        """
 

	
 
        messages.success(self.request, "Entity %s has been removed." % self.get_object().name, extra_tags="alert alert-success")
 

	
 
        return super(EntityDeleteView, self).post(*args, **kwargs)
 

	
 
    def delete(self, *args, **kwargs):
 
        """
 
        Deletes the object. This method is overridden in order to obtain the
 
        project ID for success URL.
 

	
 
        @TODO: Fix this once Django 1.6 comes out with fix from ticket 19044.
 
        """
 

	
 
        self.success_url = reverse("project", args=(self.get_object().project.id,))
 

	
 
        return super(EntityDeleteView, self).delete(*args, **kwargs)
 

	
 

	
 
class InterfaceCreateView(MultiplePermissionsRequiredMixin, CreateView):
 
    """
 
    View for creating a new interface.
 
    """
 

	
 
    model = Interface
 
    form_class = InterfaceForm
 
    template_name_suffix = "_create_form"
 

	
 
    # Required permissions
 
    permissions = {
 
        "all": ("conntrackt.add_interface",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def get_form(self, form_class):
 
        """
 
        Returns an instance of form that can be used by the view.
 

	
 
        The method will limit the entity select input if request contained this
 
        information.
 
        """
 

	
 
        form = super(InterfaceCreateView, self).get_form(form_class)
 

	
 
        # Limit the entity selection if required.
 
        entity_id = self.request.GET.get("entity", None)
 
        if entity_id:
 
            form.fields["entity"].queryset = Entity.objects.filter(pk=entity_id)
 
            form.fields["entity"].widget.attrs["readonly"] = True
 

	
 
        return form
 

	
 
    def get_initial(self):
 
        """
 
        Returns initial values that should be pre-selected (if they were
 
        specified through a GET parameter).
 
        """
 

	
 
        initial = super(InterfaceCreateView, self).get_initial()
 

	
 
        initial["entity"] = self.request.GET.get("entity", None)
 

	
 
        return initial
 

	
 
    def get_success_url(self):
 
        """
 
        Returns the URL to which the user should be redirected after an
 
        interface has been created.
 

	
 
        The URL in this case will be set to entity's details page.
 
        """
 

	
 
        return reverse("entity", args=(self.object.entity.pk,))
 

	
 

	
 
class InterfaceUpdateView(MultiplePermissionsRequiredMixin, UpdateView):
 
    """
 
    View for updating an existing interface.
 
    """
 

	
 
    model = Interface
 
    form_class = InterfaceForm
 
    template_name_suffix = "_update_form"
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.change_interface",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def get_form(self, form_class):
 
        """
 
        Returns an instance of form that can be used by the view.
 

	
 
        The method will limit the entities that can be selected for the
 
        interface to the ones that belong to the same project as the currently
 
        set entity.
 
        """
 

	
 
        form = super(InterfaceUpdateView, self).get_form(form_class)
 

	
 
        # Limit the entities to same project.
 
        form.fields["entity"].queryset = Entity.objects.filter(project=self.object.entity.project)
 

	
 
        return form
 

	
 
    def get_success_url(self):
 
        """
 
        Returns the URL to which the user should be redirected after an
 
        interface has been updated.
 

	
 
        The URL in this case will be set to entity's details page.
 
        """
 

	
 
        return reverse("entity", args=(self.object.entity.pk,))
 

	
 

	
 
class InterfaceDeleteView(MultiplePermissionsRequiredMixin, DeleteView):
 
    """
 
    View for deleting an interface.
 
    """
 

	
 
    model = Interface
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.delete_interface",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def post(self, *args, **kwargs):
 
        """
 
        Add a success message that will be displayed to the user to confirm the
 
        interface deletion.
 
        """
 

	
 
        messages.success(self.request, "Interface %s has been removed." % self.get_object().name, extra_tags="alert alert-success")
 

	
 
        return super(InterfaceDeleteView, self).post(*args, **kwargs)
 

	
 
    def delete(self, *args, **kwargs):
 
        """
 
        Deletes the object. This method is overridden in order to obtain the
 
        entity ID for success URL.
 

	
 
        @TODO: Fix this once Django 1.6 comes out with fix from ticket 19044.
 
        """
 

	
 
        self.success_url = reverse("entity", args=(self.get_object().entity.id,))
 

	
 
        return super(InterfaceDeleteView, self).delete(*args, **kwargs)
 

	
 

	
 
class CommunicationCreateView(MultiplePermissionsRequiredMixin, CreateView):
 
    """
 
    View for creating a new communication.
 
    """
 

	
 
    model = Communication
 
    form_class = CommunicationForm
 
    template_name_suffix = "_create_form"
 

	
 
    # Required permissions
 
    permissions = {
 
        "all": ("conntrackt.add_communication",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def get_form(self, form_class):
 
        """
 
        Returns an instance of form that can be used by the view.
 

	
 
        The method will limit the source and destination interface selection to
 
        interfaces belonging to the same project as provided entity ID (if any
 
        was provided).
 
        """
 

	
 
        form = super(CommunicationCreateView, self).get_form(form_class)
 

	
 
        # Limit the interface selection based on provided source entity,
 
        # destination entity, or project.
 
        entity_id = self.request.GET.get("from_entity", None) or self.request.GET.get("to_entity", None)
 
        project_id = self.request.GET.get("project", None)
 

	
 
        if project_id:
 
            form.fields["source"].queryset = Interface.objects.filter(entity__project=project_id)
 
            form.fields["destination"].queryset = Interface.objects.filter(entity__project=project_id)
 
        elif entity_id:
 
            entity = Entity.objects.get(pk=1)
 
            form.fields["source"].queryset = Interface.objects.filter(entity__project=entity.project)
 
            form.fields["destination"].queryset = Interface.objects.filter(entity__project=entity.project)
 

	
 
        return form
 

	
 
    def get_initial(self):
 
        """
 
        Returns initial values that should be pre-selected (if they were
 
        specified through a GET parameter).
 
        """
 

	
 
        initial = super(CommunicationCreateView, self).get_initial()
 

	
 
        # If source or destination entity were specified in request, fetch the
 
        # first interface from them and use it as initial source and destination.
 
        from_entity = self.request.GET.get("from_entity", None)
 
        to_entity = self.request.GET.get("to_entity", None)
 

	
 
        if from_entity:
 
            try:
 
                interface = Interface.objects.filter(entity=from_entity)[0]
 
                initial["source"] = interface.id
 
            except IndexError:
 
                pass
 

	
 
        if to_entity:
 
            try:
 
                interface = Interface.objects.filter(entity=to_entity)[0]
 
                initial["destination"] = interface.id
 
            except IndexError:
 
                pass
 

	
 
        return initial
 

	
 
    def get_success_url(self):
 
        """
 
        Returns the URL to which the user should be redirected after a
 
        communication has been created.
 

	
 
        The URL will be set to entity details page of an entity that was
 
        provided as part of the from/to GET request (in that order), or as a
 
        fallback it'll direct the user to source interface's entity details
 
        page.
 
        """
 

	
 
        entity_id = self.request.GET.get("from_entity", None) or self.request.GET.get("to_entity", None) or self.object.source.entity.pk
 

	
 
        return reverse("entity", args=(entity_id,))
 

	
 

	
 
class CommunicationUpdateView(MultiplePermissionsRequiredMixin, UpdateView):
 
    """
 
    View for updating an existing communication.
 
    """
 

	
 
    model = Communication
 
    form_class = CommunicationForm
 
    template_name_suffix = "_update_form"
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.change_communication",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def get_form(self, form_class):
 
        """
 
        Returns an instance of form that can be used by the view.
 

	
 
        The method will limit the source and destination interfaces that can be
 
        selected for the communication. Both will be limited to interfaces
 
        coming from entities that belong to the same project as current
 
        communication's source interface.
 
        """
 

	
 
        form = super(CommunicationUpdateView, self).get_form(form_class)
 

	
 
        project = self.object.source.entity.project
 

	
 
        form.fields["source"].queryset = Interface.objects.filter(entity__project=project)
 
        form.fields["destination"].queryset = Interface.objects.filter(entity__project=project)
 

	
 
        return form
 

	
 
    def get_success_url(self):
 
        """
 
        Returns the URL to which the user should be redirected after a
 
        communication has been created.
 

	
 
        The URL will be set to entity details page of an entity that was
 
        provided as part of the from/to GET request (in that order), or as a
 
        fallback it'll direct the user to source interface's entity details
 
        page.
 
        """
 

	
 
        entity_id = self.request.GET.get("from_entity", None) or self.request.GET.get("to_entity", None) or self.object.source.entity.pk
 

	
 
        return reverse("entity", args=(entity_id,))
 

	
 

	
 
class CommunicationDeleteView(MultiplePermissionsRequiredMixin, DeleteView):
 
    """
 
    View for deleting an communication.
 
    """
 

	
 
    model = Communication
 

	
 
    # Required permissions.
 
    permissions = {
 
        "all": ("conntrackt.delete_communication",),
 
        }
 

	
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def post(self, *args, **kwargs):
 
        """
 
        Add a success message that will be displayed to the user to confirm the
 
        communication deletion.
 
        """
 

	
 
        messages.success(self.request, "Communication %s has been removed." % self.get_object(), extra_tags="alert alert-success")
 

	
 
        return super(CommunicationDeleteView, self).post(*args, **kwargs)
 

	
 
    def delete(self, *args, **kwargs):
 
        """
 
        Deletes the object. This method is overridden in order to obtain the
 
        entity ID for success URL.
 

	
 
        @TODO: Fix this once Django 1.6 comes out with fix from ticket 19044.
 
        """
 

	
 
        entity_id = self.request.GET.get("from_entity", None) or self.request.GET.get("to_entity", None) or self.get_object().source.entity.pk
 

	
 
        self.success_url = reverse("entity", args=(entity_id,))
 

	
 
        return super(CommunicationDeleteView, self).delete(*args, **kwargs)
0 comments (0 inline, 0 general)