Skip to content

Commit

Permalink
Merge pull request #105 from peopledoc/104_can_skip_item
Browse files Browse the repository at this point in the history
Add a way to skip item
  • Loading branch information
brunobord authored Oct 16, 2018
2 parents a5c7dd7 + 3c9e6a8 commit b160c4c
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 4 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Changelog for django-agnocomplete
master (unreleased)
==================

Nothing here yet
* Add a way in UrlProxy widget to filter value with python (#104)
* Provide a FieldMixin in order to use with UrlProxy Autocomplete for efficient value validation (#107)

0.13.0 (2018-10-02)
===================
Expand Down
31 changes: 30 additions & 1 deletion agnocomplete/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from .constants import AGNOCOMPLETE_DEFAULT_QUERYSIZE
from .constants import AGNOCOMPLETE_MIN_QUERYSIZE
from .exceptions import AuthenticationRequiredAgnocompleteException
from .exceptions import SkipItem
from .exceptions import ItemNotFound


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -618,7 +620,10 @@ def items(self, query=None, **kwargs):
result = []
for item in http_result:
# Eventual result reshaping.
result.append(self.item(item))
try:
result.append(self.item(item))
except SkipItem:
continue
return result

def selected(self, ids):
Expand All @@ -638,3 +643,27 @@ def selected(self, ids):
)
)
return data

def validate(self, value):
"""
From a value available on the remote server, the method returns the
complete item matching the value.
If case the value is not available on the server side or filtered
through :meth:`item`, the class:`agnocomplete.exceptions.ItemNotFound`
is raised.
"""

url = self.get_item_url(value)
try:
data = self.http_call(url=url)
except requests.HTTPError:
raise ItemNotFound()

data = self.get_http_result(data)

try:
self.item(data)
except SkipItem:
raise ItemNotFound()

return value
14 changes: 14 additions & 0 deletions agnocomplete/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,17 @@ class HTTPError(HTTPError):
Occurs when the 3rd party API returns an error code
"""
pass


class SkipItem(Exception):
"""
Occurs when Item has to be skipped when building the final Payload
"""
pass


class ItemNotFound(Exception):
"""
Occurs while searching an unexisting item on autocomplete choices.
"""
pass
17 changes: 17 additions & 0 deletions agnocomplete/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .constants import AGNOCOMPLETE_USER_ATTRIBUTE
from .widgets import AgnocompleteSelect, AgnocompleteMultiSelect
from .register import get_agnocomplete_registry
from .exceptions import ItemNotFound
from .exceptions import UnregisteredAgnocompleteException


Expand Down Expand Up @@ -246,3 +247,19 @@ def clean(self, value):
qs = super(AgnocompleteModelMultipleField, self).clean(pks)

return qs


class AgnocompleteUrlProxyMixin(object):
"""
This mixin can be used with a field which actually using
:class:`agnocomplete.core.AutocompletUrlProxy`. The main purpose is to
provide a mechanism to validate the value through the Autocomplete.
"""

def valid_value(self, value):
try:
self.agnocomplete.validate(value)
except ItemNotFound:
return False

return True
19 changes: 19 additions & 0 deletions demo/autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
from django.utils.encoding import force_text as text
from django.conf import settings

from agnocomplete.exceptions import SkipItem
from agnocomplete.register import register
from agnocomplete.core import (
AgnocompleteChoices,
AgnocompleteModel,
AgnocompleteUrlProxy,
)

from .models import Person, Tag, ContextTag
from .common import COLORS
from . import GOODAUTHTOKEN
Expand Down Expand Up @@ -257,6 +259,22 @@ def items(self, query=None, **kwargs):
return super(AutocompleteUrlSimple, self).items(query, **kwargs)


class AutocompleteUrlSkipItem(AutocompleteUrlSimple):

data_key = 'data'

def item(self, obj):
if obj['label'] == 'first person':
raise SkipItem
return super(AutocompleteUrlSkipItem, self).item(obj)

def get_item_url(self, pk):
return '{}{}'.format(
getattr(settings, 'HTTP_HOST', ''),
reverse_lazy('url-proxy:atomic-item', args=[pk])
)


# Registration
register(AutocompleteColor)
register(AutocompleteColorExtra)
Expand All @@ -282,3 +300,4 @@ def items(self, query=None, **kwargs):
register(AutocompleteUrlSimplePost)
register(AutocompleteUrlErrors)
register(AutocompleteUrlSimpleWithExtra)
register(AutocompleteUrlSkipItem)
7 changes: 7 additions & 0 deletions demo/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ def create_item(self, **kwargs):
Return the created model instance.
"""
return self.queryset.model.objects.create(**kwargs)


class AgnocompleteUrlProxyField(fields.AgnocompleteUrlProxyMixin,
fields.AgnocompleteField):
"""
Demo Field to use AutocompleteUrlProxy
"""
3 changes: 2 additions & 1 deletion demo/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class LoaddataLiveTestCase(LoaddataMixin, LiveServerTestCase):
class RegistryTestGeneric(LoaddataTestCase):

def _test_registry_keys(self, keys):
self.assertEqual(len(keys), 23)
self.assertEqual(len(keys), 24)
self.assertIn("AutocompleteColor", keys)
self.assertIn("AutocompleteColorExtra", keys)
self.assertIn("AutocompletePerson", keys)
Expand Down Expand Up @@ -64,6 +64,7 @@ def _test_registry_keys(self, keys):
self.assertIn("AutocompleteUrlHeadersAuth", keys)
self.assertIn("AutocompleteUrlErrors", keys)
self.assertIn("AutocompleteUrlSimpleWithExtra", keys)
self.assertIn("AutocompleteUrlSkipItem", keys)


class MockRequestUser(object):
Expand Down
30 changes: 29 additions & 1 deletion demo/tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

from django import forms, get_version
from django.urls import reverse
from django.test import TestCase
from django.test import LiveServerTestCase, TestCase, override_settings
from django.core.exceptions import ValidationError
import six

import mock

from agnocomplete import fields
from agnocomplete.exceptions import UnregisteredAgnocompleteException
from agnocomplete.fields import (
Expand All @@ -19,7 +22,9 @@
HiddenAutocompleteURLReverse,
AutocompleteTag,
AutocompletePersonDomain,
AutocompleteUrlSkipItem,
)
from demo.fields import AgnocompleteUrlProxyField
from demo.models import Tag, Person
from demo.tests import LoaddataTestCase

Expand Down Expand Up @@ -241,3 +246,26 @@ def test_render_no_selection(self):
)
html_form = "{}".format(form)
self.assertNotIn('<option', html_form)


@override_settings(HTTP_HOST='')
class FieldUrlProxyTest(LiveServerTestCase):

def test_clean_method(self):
person = AgnocompleteUrlProxyField(AutocompleteUrlSkipItem)
search_url = person.agnocomplete.get_item_url('2')
with mock.patch('demo.autocomplete.AutocompleteUrlSkipItem'
'.get_item_url') as mock_auto:
mock_auto.return_value = self.live_server_url + search_url
item = person.clean('2')

self.assertEqual(item, '2')

def test_clean_method_value_not_found(self):
# Value is filter through the `item` method
person = AgnocompleteUrlProxyField(AutocompleteUrlSkipItem)
search_url = person.agnocomplete.get_item_url('1')
with mock.patch('demo.autocomplete.AutocompleteUrlSkipItem'
'.get_item_url') as mock_auto:
mock_auto.return_value = self.live_server_url + search_url
self.assertRaises(ValidationError, person.clean, '1')
24 changes: 24 additions & 0 deletions demo/tests/test_url_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
AutocompleteUrlHeadersAuth,
AutocompleteUrlSimplePost,
AutocompleteUrlSimpleWithExtra,
AutocompleteUrlSkipItem,
)
from .. import DATABASE, GOODAUTHTOKEN
RESULT_DICT = [{'value': text(item['pk']), 'label': text(item['name'])} for item in DATABASE] # noqa
Expand Down Expand Up @@ -291,3 +292,26 @@ def test_search_extra(self):
{'value': 'moo', 'label': 'moo'}
]
)


@override_settings(HTTP_HOST='')
class AutocompleteUrlSkipItemTest(LiveServerTestCase):
def test_search(self):
instance = AutocompleteUrlSkipItem()
# "mock" Change URL by adding the host
search_url = instance.search_url
with mock.patch('demo.autocomplete.AutocompleteUrlSkipItem'
'.get_search_url') as mock_auto:
mock_auto.return_value = self.live_server_url + search_url
search_result = instance.items(query='person')
self.assertEqual(
list(search_result),
[
{"value": '2', 'label': 'second person'},
{"value": '3', 'label': 'third person'},
{"value": '4', 'label': 'fourth person'},
{"value": '5', 'label': 'fifth person'},
{"value": '6', 'label': 'sixth person'},
{"value": '7', 'label': 'seventh person'}
],
)
2 changes: 2 additions & 0 deletions demo/urls_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

urlpatterns = [
url(r'^item/(?P<pk>[0-9]+)$', views_proxy.item, name='item'),
url(r'^atomicitem/(?P<pk>[0-9]+)$', views_proxy.atomic_item,
name='atomic-item'),
url(r'^simple/$', views_proxy.simple, name='simple'),
url(r'^simple-post/$', views_proxy.simple_post, name='simple-post'),
url(r'^convert/$', views_proxy.convert, name='convert'),
Expand Down
19 changes: 19 additions & 0 deletions demo/views_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,22 @@ def item(request, pk):
response = json.dumps(result)
logger.debug('response: `%s`', response)
return HttpResponse(response)


@require_GET
def atomic_item(request, pk):
"""
Similar to `item` but does not return a list
"""
data = None
for item in DATABASE:
if text(item['pk']) == text(pk):
data = convert_data(item)
break
if not data:
raise Http404("Unknown item `{}`".format(pk))
logger.debug('3rd item search: `%s`', pk)
result = {'data': data}
response = json.dumps(result)
logger.debug('response: `%s`', response)
return HttpResponse(response)
22 changes: 22 additions & 0 deletions docs/url-proxy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,28 @@ or, if things are going more complicated:
label='{} {}'.format(current_item['label1'], current_item['label2']),
)
If you need to skip fields
++++++++++++++++++++++++++


Sometimes, passing in a query may not provide sufficient filtering to meet requirements. In such cases, :class:`agnocomplete.exceptions.SkipItem` can be raised to prevent a given item from appearing in the final result. Please note, however, that this is not a very efficient approach, and so should only be used as a last resort. It is generally preferable to apply ``server-side`` filtering if at all possible.

.. code-block:: python
from agnocomplete.exceptions import SkipItem
class AutocompleteUrlConvert(AgnocompleteUrlProxy):
def item(self, current_item):
if not current_item['meta'].get('is_required'):
raise SkipItem()
return dict(
value=current_item[current_item['meta']['value_field']],
label='{} {}'.format(current_item['label1'], current_item['label2']),
)
If the result doesn't follow standard schema
++++++++++++++++++++++++++++++++++++++++++++

Expand Down

0 comments on commit b160c4c

Please sign in to comment.