Changeset - b2d842037a63
[Not reviewed]
default
0 3 0
Branko Majic (branko) - 10 years ago 2013-10-26 23:14:24
branko@majic.rs
CONNT-23: Limit the as-you-type search results to 4 entities and 4 projects maximum. Fixed permission handling in APISearchView to raise exception. Search term is now defauled to empty string (easier to test). Implemented test for APISearchView.
3 files changed with 172 insertions and 3 deletions:
0 comments (0 inline, 0 general)
conntrackt/static/conntrackt.js
Show inline comments
 
@@ -28,25 +28,25 @@
 

	
 
    // Set-up the search suggestions as the user types-in the search query.
 
    searchField.keyup(function(){
 
        // Fetch the needed elements. The search suggestions element is kept
 
        // invisble by default via CSS.
 
        var searchField = $("#search");
 
        var searchSuggestions = $("#search-suggestions");
 
        var searchSuggestionsList = searchSuggestions.find("ul");
 

	
 
        // Show the search suggestions only if the user has provided 2 or more
 
        // characters.
 
        if (searchField.val().length >= 2) {
 
            request = $.getJSON(conntrackt_api_url + "/search/" + searchField.val())
 
            request = $.getJSON(conntrackt_api_url + "/search/" + searchField.val(), {"limit": 4})
 
                .done(function(data) {
 

	
 
                    // Process the retrieved JSON response, setting-up list of
 
                    // items that will be offered as suggestion.
 
                    var items = [];
 
                    $.each(data, function(index, item) {
 
                        if (item.type == "project") {
 
                            items.push('<li><a class="search-option" href="' + item.url + '">' + item.name + '<br/><small>Project</small></a></li>');
 
                        } else if (item.type == "entity") {
 
                            items.push('<li><a class="search-option" href="' + item.url + '">' + item.name + '<br/><small>from ' + item.project + '<small></a></li>');
 
                        }
 
                    });
conntrackt/tests/test_views.py
Show inline comments
 
@@ -11,41 +11,43 @@
 
#
 
# Django Conntrackt is distributed in the hope that it will be useful, but
 
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
# details.
 
#
 
# You should have received a copy of the GNU General Public License along with
 
# Django Conntrackt.  If not, see <http://www.gnu.org/licenses/>.
 
#
 

	
 

	
 
# Standard library imports.
 
import json
 
from StringIO import StringIO
 
from zipfile import ZipFile, ZIP_DEFLATED
 

	
 
# Python third-party library imports.
 
import mock
 

	
 
# Django imports.
 
from django.core.exceptions import ValidationError
 
from django.core.urlresolvers import reverse
 
from django.http import Http404
 
from django.test import RequestFactory
 
from django.test import TestCase
 
from django.utils.http import urlquote
 

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

	
 
from conntrackt.views import IndexView, SearchView
 
from conntrackt.views import IndexView, SearchView, APISearchView
 
from conntrackt.views import entity_iptables, project_iptables, project_diagram
 

	
 
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
 

	
 
# Test imports.
 
from .forms import FormWithWidgetCSSClassFormMixin, FormWithPlaceholderFormMixin
 
from .helpers import PermissionTestMixin, create_get_request, generate_get_response, FakeMessages
 
from .views import RedirectToNextMixinView
 
@@ -1595,12 +1597,165 @@ class SearchViewTest(PermissionTestMixin
 

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

	
 
        # Set-up a request.
 
        response = generate_get_response(view)
 

	
 
        self.assertNotIn("entities", response.context_data)
 
        self.assertNotIn("projects", response.context_data)
 
        self.assertNotIn("search_term", response.context_data)
 
        # Only the "view" context variable should be present.
 
        self.assertEqual(1, len(response.context_data))
 

	
 

	
 
class APISearchViewTest(PermissionTestMixin, TestCase):
 

	
 
    sufficient_permissions = ("view",)
 
    view_class = APISearchView
 

	
 
    def setUp(self):
 
        """
 
        Set-up some test data.
 
        """
 

	
 
        setup_test_data()
 

	
 
    def test_limit_negative(self):
 
        """
 
        Test if an exception is raised in case a negative limit is requested.
 
        """
 

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

	
 
        # Generate the request.
 
        request = RequestFactory().get("/fake-path?limit=-1")
 
        request.user = mock.Mock()
 
        request._dont_enforce_csrf_checks = True
 

	
 
        # Validate the response.
 
        self.assertRaisesRegexp(ValidationError, "Limit may not be a negative value.", view, request, search_term="test")
 

	
 
    def test_empty_query(self):
 
        """
 
        Test that the response is empty if empty query was sent.
 
        """
 

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

	
 
        # Get the response.
 
        response = generate_get_response(view, search_term="")
 

	
 
        self.assertEqual(response['Content-Type'], "application/json")
 
        self.assertEqual(response.content, "[]")
 

	
 
    def test_strip_search_term(self):
 
        """
 
        Verifies that the search term is stripped when search is performed.
 
        """
 

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

	
 
        # Get the response.
 
        response = generate_get_response(view, search_term="Test Entity 1")
 

	
 
        # Validate the response.
 
        expected_content = """[{"project": "Test Project 1", "url": "/conntrackt/entity/1/", "type": "entity", "name": "Test Entity 1"}]"""
 
        self.assertEqual(response.content, expected_content)
 

	
 
    def test_no_items(self):
 
        """
 
        Test the response if no items are found.
 
        """
 

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

	
 
        # Get the response.
 
        response = generate_get_response(view, search_term="string that does not exist")
 

	
 
        self.assertEqual(response['Content-Type'], "application/json")
 
        self.assertEqual(response.content, "[]")
 

	
 
    def test_entity_found(self):
 
        """
 
        Test the response if a single entity is found.
 
        """
 

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

	
 
        # Get the response.
 
        response = generate_get_response(view, search_term="Test Entity 1")
 

	
 
        expected_content = """[{"project": "Test Project 1", "url": "/conntrackt/entity/1/", "type": "entity", "name": "Test Entity 1"}]"""
 

	
 
        self.assertEqual(response['Content-Type'], "application/json")
 
        self.assertEqual(response.content, expected_content)
 

	
 
    def test_project_found(self):
 
        """
 
        Test the response if a single project is found.
 
        """
 

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

	
 
        # Get the response.
 
        response = generate_get_response(view, search_term="Test Project 1")
 

	
 
        expected_content = """[{"project": "Test Project 1", "url": "/conntrackt/project/1/", "type": "project", "name": "Test Project 1"}]"""
 

	
 
        self.assertEqual(response['Content-Type'], "application/json")
 
        self.assertEqual(response.content, expected_content)
 

	
 
    def test_multiple_items_found(self):
 
        """
 
        Test the response if multiple items are found.
 
        """
 

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

	
 
        # Get the response.
 
        response = generate_get_response(view, search_term="Test")
 

	
 
        # Verify that the JSON reply is valid.
 
        try:
 
            items = json.loads(response.content)
 
        except ValueError:
 
            self.fail("Parsing of resulting JSON has failed")
 

	
 
        # Verify that a list of items was returned.
 
        self.assertTrue(isinstance(items, list))
 

	
 
        # Verify each item.
 
        for item in items:
 
            # Every item must be a dictionary.
 
            self.assertTrue(isinstance(item, dict))
 
            keys = item.keys()
 
            # Verify that 4 specific keys are present in dictionary (project,
 
            #  url, name, type).
 
            self.assertEqual(len(keys), 4)
 
            self.assertIn("project", keys)
 
            self.assertIn("name", keys)
 
            self.assertIn("url", keys)
 
            self.assertIn("type", keys)
 
            # Verify the type associated with item.
 
            self.assertIn(item["type"], ["project", "entity"])
 

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

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

	
 
        # Get the response.
 
        response = generate_get_response(view, search_term="test")
 

	
 
        self.assertEqual(response['Content-Type'], "application/json")
 

	
conntrackt/views.py
Show inline comments
 
@@ -18,24 +18,25 @@
 
# Django Conntrackt.  If not, see <http://www.gnu.org/licenses/>.
 
#
 

	
 

	
 
# Standard library imports.
 
from StringIO import StringIO
 
from zipfile import ZipFile, ZIP_DEFLATED
 
import json
 

	
 
# Django imports.
 
from django.contrib.auth.decorators import permission_required
 
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.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
 

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

	
 
# Application imports.
 
from .forms import ProjectForm, LocationForm, EntityForm, InterfaceForm, CommunicationForm
 
from .models import Project, Entity, Location, Interface, Communication
 
@@ -1030,43 +1031,56 @@ class APISearchView(MultiplePermissionsR
 

	
 
      - name (name of the matched item)
 
      - project (project to which the item belongs)
 
      - type (type of the matched item)
 
      - url (URL towards the matched item)
 
    """
 

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

	
 
    def get(self, request, search_term):
 
    # Raise authorisation denied exception for unmet permissions.
 
    raise_exception = True
 

	
 
    def get(self, request, search_term=""):
 
        """
 
        Implements response handling for a GET request.
 
        """
 

	
 
        # Retrieve the search term, and strip it if it was provided.
 
        if search_term:
 
            search_term = search_term.strip()
 

	
 
        # Set-up a list that will contain found items.
 
        items = []
 

	
 
        # Fetch the maximum number of items that should be returned.
 
        limit = int(request.GET.get("limit", 0))
 
        if limit < 0:
 
            raise ValidationError("Limit may not be a negative value.")
 

	
 
        # Don't perform search with empty search term.
 
        if search_term != "":
 

	
 
            # Run the search on entities and projects.
 
            entities = Entity.objects.search(search_term).select_related("project")
 
            projects = Project.objects.search(search_term)
 

	
 
            # If maximum number of items was provided, narrow-down the results.
 
            if limit > 0:
 
                entities = entities[:limit]
 
                projects = projects[:limit]
 

	
 
            # Add found entities.
 
            for entity in entities:
 
                items.append({"name": entity.name,
 
                              "project": entity.project.name,
 
                              "type": "entity",
 
                              "url": entity.get_absolute_url(),})
 

	
 
            # Add found projects.
 
            for project in projects:
 
                items.append({"name": project.name,
 
                              "project": project.name,
 
                              "type": "project",
0 comments (0 inline, 0 general)