├── .gitignore ├── MANIFEST.in ├── simple_search ├── views.py ├── models.py ├── __init__.py ├── utils.py └── tests.py ├── setup.py └── README.rst /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /simple_search/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /simple_search/models.py: -------------------------------------------------------------------------------- 1 | # this file has been intentionally left blank -------------------------------------------------------------------------------- /simple_search/__init__.py: -------------------------------------------------------------------------------- 1 | from utils import perform_search, generic_search 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read() 5 | 6 | # allow setup.py to be run from any path 7 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 8 | 9 | setup( 10 | name='django-simple-search', 11 | version='0.1', 12 | packages=['simple_search'], 13 | include_package_data=True, 14 | description='A simple Django app to search models.', 15 | long_description=README, 16 | url='https://github.com/esistgut/django-simple-search', 17 | author='Giacomo Graziosi', 18 | author_email='g.graziosi@gmail.com', 19 | classifiers=[ 20 | 'Environment :: Web Environment', 21 | 'Framework :: Django', 22 | 'Intended Audience :: Developers', 23 | 'Operating System :: OS Independent', 24 | 'Programming Language :: Python', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.2', 27 | 'Programming Language :: Python :: 3.3', 28 | 'Topic :: Internet :: WWW/HTTP', 29 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /simple_search/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import operator 3 | 4 | try: 5 | from functools import reduce 6 | except ImportError: 7 | # In Python 2 reduce is in builtins 8 | pass 9 | 10 | from django.db.models import Q, query 11 | 12 | 13 | def normalize_query(query_string, 14 | findterms=re.compile(r'"([^"]+)"|(\S+)').findall, 15 | normspace=re.compile(r'\s{2,}').sub): 16 | """ 17 | Splits the query string in invidual keywords, getting rid of unecessary 18 | spaces and grouping quoted words together. 19 | Example: 20 | 21 | >>> normalize_query(' some random words "with quotes " and spaces') 22 | ['some', 'random', 'words', 'with quotes', 'and', 'spaces'] 23 | """ 24 | return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] 25 | 26 | 27 | def build_query(query_string, search_fields): 28 | """ 29 | Returns a query, that is a combination of Q objects. That combination 30 | aims to search keywords within a model by testing the given search fields. 31 | """ 32 | terms = normalize_query(query_string) 33 | 34 | if not terms: 35 | return None 36 | 37 | query = reduce( 38 | operator.__and__, 39 | (reduce( 40 | operator.__or__, 41 | (Q(**{"%s__icontains" % field_name: term}) for field_name in search_fields) 42 | ) for term in terms), 43 | ) 44 | return query 45 | 46 | 47 | def perform_search(query_string, model, fields): 48 | """ 49 | Perform a search in the given fields of a model or queryset 50 | """ 51 | # Ensure we're dealing with a queryset 52 | queryset = model 53 | if not isinstance(queryset, query.QuerySet): 54 | queryset = model.objects.all() 55 | 56 | if not query_string: 57 | return queryset 58 | 59 | entry_query = build_query(query_string, fields) 60 | return queryset.filter(entry_query) 61 | 62 | 63 | def generic_search(request, model, fields, query_param="q"): 64 | """ 65 | Look up a search string in the request GET, and perform a search in the 66 | given fields of a model or queryset 67 | """ 68 | query_string = request.GET.get(query_param, "").strip() 69 | return perform_search(query_string, model, fields) 70 | -------------------------------------------------------------------------------- /simple_search/tests.py: -------------------------------------------------------------------------------- 1 | 2 | from django.db import models 3 | from django.test import TestCase,TransactionTestCase 4 | from utils import * 5 | 6 | class NormalizeTest(TestCase): 7 | def test_normalize(self): 8 | """ 9 | tests to normalize the query 10 | """ 11 | self.assertEquals( 12 | normalize_query(' some random words "with quotes " and spaces'), 13 | ['some', 'random', 'words', 'with quotes', 'and', 'spaces'] 14 | ) 15 | 16 | self.assertEquals( 17 | normalize_query(' adsf add a & and a | "for good%measure"'), 18 | ['adsf','add','a','&','and','a','|',"for good%measure"] 19 | ) 20 | 21 | def test_empty_string(self, ): 22 | """ 23 | """ 24 | self.assertEquals(normalize_query(""),[]) 25 | 26 | class BuildQueryTest(TestCase): 27 | 28 | def test_find_items_in_database(self, ): 29 | """ 30 | test to find the items based on fields 31 | """ 32 | 33 | query = build_query("now is the time",['name','description']) 34 | 35 | self.assertEquals(len(query),4) 36 | 37 | self.assertEquals( 38 | str(query), 39 | "(AND: (OR: ('name__icontains', 'now'), ('description__icontains', 'now')), (OR: ('name__icontains', 'is'), ('description__icontains', 'is')), (OR: ('name__icontains', 'the'), ('description__icontains', 'the')), (OR: ('name__icontains', 'time'), ('description__icontains', 'time')))" 40 | ) 41 | 42 | def test_return_none_for_empty_string(self): 43 | """ 44 | If string withouth terms is provided to build_query then return None 45 | 46 | """ 47 | query = build_query(' ', ['name', 'description']) 48 | 49 | self.assertIsNone(query) 50 | 51 | 52 | class SearchTest(TransactionTestCase): 53 | """ 54 | """ 55 | 56 | def setUp(self, ): 57 | 58 | self.entry=Entry.objects.get_or_create(name="Now", description="is the time")[0] 59 | 60 | Entry.objects.get_or_create(name="Item 2",description="do we have time?") 61 | 62 | self.freds = [ 63 | Entry.objects.get_or_create(name="Is",description="fred flintstone")[0], 64 | Entry.objects.get_or_create(name="barney rubble",description="fred's neighbor")[0], 65 | ] 66 | 67 | 68 | def test_generic_query(self, ): 69 | """ 70 | """ 71 | 72 | request = MockRequest({"q":"now is the time"}) 73 | 74 | result = generic_search(request,Entry,["name","description"]) 75 | 76 | self.assertEquals(list(result), 77 | [self.entry,] 78 | ) 79 | 80 | def test_generic_empty_query_returns_all(self,): 81 | """ 82 | """ 83 | request = MockRequest({}) 84 | result = generic_search(request, Entry, ["name","description"]) 85 | 86 | self.assertEquals(set(result), 87 | set(Entry.objects.all()) 88 | ) 89 | 90 | def test_multiple_records(self, ): 91 | """ 92 | """ 93 | result = perform_search("fred", Entry, ["name","description"]) 94 | 95 | self.assertEquals(list(result), 96 | self.freds 97 | ) 98 | 99 | def test_queryset(self,): 100 | result = perform_search( 101 | "time", Entry.objects.filter(name="Now"), ['description'], 102 | ) 103 | self.assertEquals(list(result), 104 | [self.entry,] 105 | ) 106 | 107 | 108 | class MockRequest(object): 109 | 110 | def __init__(self, get_params): 111 | """ 112 | """ 113 | self.GET=get_params 114 | 115 | 116 | class Entry(models.Model): 117 | 118 | name=models.CharField(max_length=50) 119 | description=models.TextField() 120 | 121 | def __unicode__(self, ): 122 | """ 123 | """ 124 | return self.name 125 | 126 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | django-simple-search 3 | ==================== 4 | 5 | This application provides a portable, simple way to do search in a Django project. 6 | 7 | This fork is a rudimentary packaged version of the application provided by Mike Hostetler: 8 | 9 | https://github.com/squarepegsys/django-simple-search 10 | 11 | that is based on an article by Julien Phalip: 12 | 13 | http://julienphalip.com/post/2825034077/adding-search-to-a-django-site-in-a-snap 14 | 15 | 16 | Usage 17 | ===== 18 | 19 | At the top of your view, import the ``simple_search`` search module. It exposes 20 | two functions: 21 | 22 | ``simple_search.generic_search(request, model, fields, query_param)`` 23 | This takes 3 arguments (``query_param`` is optional) and returns a queryset 24 | of search results. It is the easiest way to perform a search. 25 | 26 | Arguments: 27 | ``request`` 28 | The request object, as passed to your view. 29 | 30 | ``model`` 31 | The model or queryset to search. A model will be converted to a 32 | queryset by using ``model.objects.all()`` 33 | 34 | ``fields`` 35 | A list of fields to search in the model. 36 | 37 | ``query_param`` (optional) 38 | The name of the GET parameter which contains the search string. 39 | 40 | Default: ``q`` 41 | 42 | Returns: 43 | ``queryset`` 44 | The queryset containing the results. If the search string was 45 | empty, the original queryset (or all objects) will be returned. 46 | 47 | ``simple_search.perform_search(query_string, model, fields)`` 48 | This takes 3 arguments and returns a queryset of search results. 49 | 50 | Use this function if you want to use the stripped search query string 51 | elsewhere in your view. 52 | 53 | Arguments: 54 | ``query_string`` 55 | The string to search the model for. You should call ``strip()`` on 56 | it before passing it to the function. 57 | 58 | Example: 59 | query_string = request.GET.get("q", "").strip() 60 | 61 | ``model`` 62 | The model or queryset to search. A model will be converted to a 63 | queryset by using ``model.objects.all()`` 64 | 65 | ``fields`` 66 | A list of fields to search in the model. 67 | 68 | Returns: 69 | ``queryset`` 70 | The queryset containing the results. If the search string was 71 | empty, the original queryset (or all objects) will be returned. 72 | 73 | 74 | Example 75 | ======= 76 | 77 | Generic search example:: 78 | 79 | ### views.py 80 | from simple_search import generic_search, perform_search 81 | from books.models import Author, Book 82 | from django.shortcuts import render_to_response, redirect, get_object_or_404 83 | 84 | 85 | QUERY = "search-query" 86 | 87 | MODEL_MAP = { 88 | Author: ["first_name", "last_name", ], 89 | Book: ["title", "summary"], 90 | } 91 | 92 | 93 | def search(request): 94 | """Search Author and Book""" 95 | objects = [] 96 | for model, fields in MODEL_MAP.iteritems(): 97 | objects += generic_search(request, model, fields, QUERY) 98 | 99 | return render_to_response("search_results.html", 100 | { 101 | "objects": objects, 102 | "search_string": request.GET.get(QUERY, ""), 103 | }) 104 | 105 | def list_books(request, author_pk): 106 | """List books by a specific author, with optional search""" 107 | query_string = request.GET.get(QUERY, "").strip() 108 | author = get_object_or_404(Author, pk=author_pk) 109 | books = Books.objects.filter(author=author) 110 | books = perform_search(query_string, books, MODEL_MAP['Book']) 111 | 112 | return render_to_response("list_books.html", 113 | { 114 | "books": books, 115 | "search_string": query_string, 116 | }) 117 | --------------------------------------------------------------------------------