Skip to content

Commit

Permalink
auto-generate a default Searchable.searchable attribute. closes #1
Browse files Browse the repository at this point in the history
  • Loading branch information
knipknap committed Nov 5, 2018
1 parent 118c056 commit f33ecaf
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 40 deletions.
53 changes: 30 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,11 @@ For this, a JSON-based search functionality is provided:
django-find is smart in figuring out how to join those models
together and return a useful result.

## Quick start

Enabling the functionality is as simple as the following:

1. Make sure your model inherits the `Searchable` mixin. A word of
caution: Inherit from models.Model first, then Searchable.
2. Add a "searchable" attribute to your models, that lists the
aliases and maps them to a Django field using Django's selector
syntax (underscore-separated field names).
## Quick start

Example:
Enabling the functionality is as simple as adding the "Searchable"
decorator. Example:

```python
from django.db import models
Expand All @@ -62,21 +56,11 @@ from django_find import Searchable
class Author(models.Model, Searchable):
name = models.CharField("Author Name", max_length=10)

searchable = [
('author', 'name'),
('name', 'name'),
]

class Book(models.Model, Searchable):
author = models.ForeignKey(Author, on_delete=models.CASCADE)
title = models.CharField("Title", max_length=10)
rating = models.IntegerField()

searchable = [
('author', 'author__name'), # Note the selector syntax
('title', 'title'),
('rating', 'rating'),
]
author = models.ForeignKey(Author, on_delete=models.CASCADE, verbose_name='Author')
title = models.CharField("Title", max_length=80)
rating = models.IntegerField("Rating")
internal_id = models.CharField(max_length=10)
```

That is all, your models now provide the following methods:
Expand All @@ -103,6 +87,29 @@ You can pass the PaginatedRawQuerySet to Django templates as you
would with a Django QuerySet, as it supports slicing and
pagination.

In most cases, you also want to specify some other, related
fields that can be searched, or exclude some columns from the search.
The following example shows how to do that.

```python
class Book(models.Model, Searchable):
author = models.ForeignKey(Author, on_delete=models.CASCADE, verbose_name='Author')
title = models.CharField("Title", max_length=10)
rating = models.IntegerField("Rating")
internal_id = models.CharField(max_length=10)

searchable = [
('author', 'author__name'), # Search the name instead of the id of the related model. Note the selector syntax
('stars', 'rating'), # Add an extra alias for "rating" that can be used in a query.
('internal_id', False), # Exclude from search
]
```

In other words, add a "searchable" attribute to your models, that lists the
aliases and maps them to a Django field using Django's selector syntax
(underscore-separated field names).


## Installation

django-find is installed like any other Django app:
Expand Down
22 changes: 17 additions & 5 deletions django_find/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ class Searchable(object):

searchable_labels = {}

@classmethod
def get_default_searchable(cls):
return OrderedDict((f.name, f.name) for f in cls._meta.get_fields()
if not f.auto_created)

@classmethod
def get_searchable(cls):
result = cls.get_default_searchable()
if hasattr(cls, 'searchable'):
result.update(OrderedDict(cls.searchable))
return tuple(i for i in result.items() if i[1])

@classmethod
def get_caption_from_target_name(cls, target_name):
caption = cls.searchable_labels.get(target_name)
Expand Down Expand Up @@ -77,7 +89,7 @@ def get_target_type_from_field(cls, field):

@classmethod
def get_field_names(cls):
return OrderedDict(cls.searchable).keys()
return OrderedDict(cls.get_searchable()).keys()

@classmethod
def get_full_field_names(cls):
Expand All @@ -91,7 +103,7 @@ def get_full_field_names(cls):
def table_headers(cls):
targets = set()
result = []
for item in cls.searchable:
for item in cls.get_searchable():
target = item[1]
if target in targets:
continue
Expand All @@ -103,7 +115,7 @@ def table_headers(cls):
def search_criteria(cls):
targets = set()
result = []
for alias, target_name in cls.searchable:
for alias, target_name in cls.get_searchable():
if target_name in targets:
continue
targets.add(target_name)
Expand Down Expand Up @@ -156,7 +168,7 @@ def get_target_name_from_name(cls, name):
@type name: str
@param name: e.g. 'address', or 'name'
"""
return dict(cls.searchable)[name]
return dict(cls.get_searchable())[name]

@classmethod
def get_object_vector_to(cls, search_cls):
Expand Down Expand Up @@ -248,7 +260,7 @@ def by_field_names(cls, field_names):
@classmethod
def dom_from_query(cls, query):
fields = {}
for alias, target_name in cls.searchable:
for alias, target_name in cls.get_searchable():
fields[alias] = cls.__name__+'.'+alias
default_fields = cls.get_field_names()
query_parser = QueryParser(fields, default_fields)
Expand Down
17 changes: 7 additions & 10 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ class DummyModel(models.Model, Searchable):

searchable = [
('host', 'hostname'),
('hostname', 'hostname'),
('address', 'address'),
('model', 'model'),
]

class Meta:
Expand All @@ -22,8 +19,6 @@ class Author(models.Model, Searchable):

searchable = [
('author', 'name'),
('name', 'name'),
('rating', 'rating'),
]

class Meta:
Expand All @@ -41,9 +36,6 @@ class Book(models.Model, Searchable):

searchable = [
('author', 'author__name'),
('title', 'title'),
('comment', 'comment'),
('rating', 'rating'),
]

class Meta:
Expand All @@ -56,8 +48,6 @@ class Chapter(models.Model, Searchable):

searchable = [
('book', 'book__title'),
('title', 'title'),
('comment', 'comment'),
]

class Meta:
Expand All @@ -74,3 +64,10 @@ class SecondAuthor(models.Model, Searchable):

class Meta:
app_label = 'search_tests'

class SimpleModel(models.Model, Searchable):
title = models.CharField(max_length=10)
comment = models.CharField(max_length=10)

class Meta:
app_label = 'search_tests'
10 changes: 8 additions & 2 deletions tests/test_searchable.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import absolute_import
from django.test import TestCase
from .models import Author, DerivedAuthor, SecondAuthor, Book, Chapter, \
DummyModel
DummyModel, SimpleModel

class SearchableTest(TestCase):
def setUp(self):
Expand Down Expand Up @@ -63,12 +63,18 @@ def testGetQFromQuery(self):
self.assertTrue('%testme%' in query)

def testByQueryRaw(self):
query, fields = SimpleModel.by_query_raw('testme AND comment:foo')
self.assertTrue('WHERE ' in query.raw_query)
self.assertTrue('%testme%' in query.raw_query)
self.failIf('SimpleModel' in query.raw_query)
self.assertEqual(['SimpleModel.title', 'SimpleModel.comment'], fields)

query, fields = Author.by_query_raw('testme AND name:foo')
self.assertTrue('WHERE ' in query.raw_query)
self.assertTrue('%testme%' in query.raw_query)
self.failIf('Author' in query.raw_query)
self.failIf('Book' in query.raw_query)
self.assertEqual(['Author.author', 'Author.name', 'Author.rating'], fields)
self.assertEqual(['Author.name', 'Author.rating', 'Author.author'], fields)

query, fields = Book.by_query_raw('rating:5')
self.assertTrue('WHERE ' in query.raw_query)
Expand Down

0 comments on commit f33ecaf

Please sign in to comment.