Merge branch 'master' into localizedfloatfield

This commit is contained in:
kgeorgy 2017-06-26 11:26:56 +02:00 committed by GitHub
commit c737fa0f8c
24 changed files with 259 additions and 33 deletions

View File

@ -300,7 +300,7 @@ Add the package to your `INSTALLED_APPS` and modify your REST framework settings
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.ext.rest_framework.OAuth2Authentication',
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
)
}

View File

@ -230,5 +230,5 @@ The generic views use the `raise_exception=True` flag, which means that you can
By default this exception results in a response with the HTTP status code "400 Bad Request".
[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html
[cite]: https://doughellmann.com/blog/2009/06/19/python-exception-handling-techniques/
[authentication]: authentication.md

View File

@ -390,4 +390,4 @@ The [django-rest-framework-bulk package][django-rest-framework-bulk] implements
[UpdateModelMixin]: #updatemodelmixin
[DestroyModelMixin]: #destroymodelmixin
[django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk
[django-rest-multiple-models]: https://github.com/Axiologue/DjangoRestMultipleModels
[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels

View File

@ -194,7 +194,7 @@ As with `SimpleRouter` the trailing slashes on the URL routes can be removed by
# Custom Routers
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset. The `.routes` attribute is a list of `Route` named tuples.

View File

@ -107,6 +107,8 @@ add a schema to your API, depending on exactly what you need.
The simplest way to include a schema in your project is to use the
`get_schema_view()` function.
from rest_framework.schemas import get_schema_view
schema_view = get_schema_view(title="Server Monitoring API")
urlpatterns = [
@ -161,6 +163,7 @@ ROOT_URLCONF setting.
May be used to pass the set of renderer classes that can be used to render the API root endpoint.
from rest_framework.schemas import get_schema_view
from rest_framework.renderers import CoreJSONRenderer
from my_custom_package import APIBlueprintRenderer
@ -185,6 +188,12 @@ to be exposed in the schema:
patterns=schema_url_patterns,
)
#### `generator_class`
May be used to specify a `SchemaGenerator` subclass to be passed to the
`SchemaView`.
## Using an explicit schema view

View File

@ -329,7 +329,7 @@ For further enquires please contact <a href=mailto:funding@django-rest-framework
## Accountability
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/march-2017) and regularly include financial reports and cost breakdowns.
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/may-2017) and regularly include financial reports and cost breakdowns.
<!-- Begin MailChimp Signup Form -->
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">
@ -360,6 +360,25 @@ In an effort to keep the project as transparent as possible, we are releasing [m
---
## Frequently asked questions
**Q: Can you issue monthly invoices?**
A: Yes, we are happy to issue monthly invoices. Please just <a href=mailto:funding@django-rest-framework.org>email us</a> and let us know who to issue the invoice to (name and address) and which email address to send it to each month.
**Q: Does sponsorship include VAT?**
A: Sponsorship is VAT exempt.
**Q: Do I have to sign up for a certain time period?**
A: No, we appreciate your support for any time period that is convenient for you. Also, you can cancel your sponsorship anytime.
**Q: Can I pay yearly? Can I pay upfront fox X amount of months at a time?**
A: We are currently only set up to accept monthly payments. However, if you'd like to support Django REST framework and you can only do yearly/upfront payments, we are happy to work with you and figure out a convenient solution.
**Q: Are you only looking for corporate sponsors?**
A: No, we value individual sponsors just as much as corporate sponsors and appreciate any kind of support.
---
## Our sponsors
<div id="fundingInclude"></div>

View File

@ -296,7 +296,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[drf-compound-fields]: https://github.com/estebistec/drf-compound-fields
[django-extra-fields]: https://github.com/Hipo/drf-extra-fields
[djangorestframework-bulk]: https://github.com/miki725/django-rest-framework-bulk
[django-rest-multiple-models]: https://github.com/Axiologue/DjangoRestMultipleModels
[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
[wq.db.rest]: http://wq.io/docs/about-rest
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack

View File

@ -35,6 +35,7 @@ There are a wide range of resources available for learning and using Django REST
### Talks
* [Level Up! Rethinking the Web API Framework][pycon-us-2017]
* [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-tookit]
* [Django REST API - So Easy You Can Learn It in 25 Minutes][django-rest-api-so-easy]
* [Tom Christie about Django Rest Framework at Django: Under The Hood][django-under-hood-2014]
@ -114,3 +115,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
[building-a-restful-api-with-drf]: http://agiliq.com/blog/2014/12/building-a-restful-api-with-django-rest-framework/
[submit-pr]: https://github.com/encode/django-rest-framework
[anna-email]: mailto:anna@django-rest-framework.org
[pycon-us-2017]: https://www.youtube.com/watch?v=Rk6MHZdust4

View File

@ -48,7 +48,7 @@ if django_filters:
warnings.warn(
"The built in 'rest_framework.filters.FilterSet' is deprecated. "
"You should use 'django_filters.rest_framework.FilterSet' instead.",
DeprecationWarning
DeprecationWarning, stacklevel=2
)
return super(FilterSet, self).__init__(*args, **kwargs)
@ -72,7 +72,7 @@ class DjangoFilterBackend(DFBase):
warnings.warn(
"The built in 'rest_framework.filters.DjangoFilterBackend' is deprecated. "
"You should use 'django_filters.rest_framework.DjangoFilterBackend' instead.",
DeprecationWarning
DeprecationWarning, stacklevel=2
)
return super(DjangoFilterBackend, cls).__new__(cls, *args, **kwargs)

View File

@ -6,6 +6,7 @@ on the request, such as form content or json encoded data.
"""
from __future__ import unicode_literals
import codecs
import json
from django.conf import settings
@ -61,8 +62,8 @@ class JSONParser(BaseParser):
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
try:
data = stream.read().decode(encoding)
return json.loads(data)
decoded_stream = codecs.getreader(encoding)(stream)
return json.load(decoded_stream)
except ValueError as exc:
raise ParseError('JSON parse error - %s' % six.text_type(exc))

View File

@ -75,7 +75,8 @@ class PKOnlyObject(object):
# rather than the parent serializer.
MANY_RELATION_KWARGS = (
'read_only', 'write_only', 'required', 'default', 'initial', 'source',
'label', 'help_text', 'style', 'error_messages', 'allow_empty'
'label', 'help_text', 'style', 'error_messages', 'allow_empty',
'html_cutoff', 'html_cutoff_text'
)
@ -86,10 +87,12 @@ class RelatedField(Field):
def __init__(self, **kwargs):
self.queryset = kwargs.pop('queryset', self.queryset)
self.html_cutoff = kwargs.pop(
'html_cutoff',
self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF)
)
cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF
if cutoff_from_settings is not None:
cutoff_from_settings = int(cutoff_from_settings)
self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings)
self.html_cutoff_text = kwargs.pop(
'html_cutoff_text',
self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
@ -466,10 +469,12 @@ class ManyRelatedField(Field):
def __init__(self, child_relation=None, *args, **kwargs):
self.child_relation = child_relation
self.allow_empty = kwargs.pop('allow_empty', True)
self.html_cutoff = kwargs.pop(
'html_cutoff',
self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF)
)
cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF
if cutoff_from_settings is not None:
cutoff_from_settings = int(cutoff_from_settings)
self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings)
self.html_cutoff_text = kwargs.pop(
'html_cutoff_text',
self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)

View File

@ -335,7 +335,6 @@ class Request(object):
"""
Attempt to authenticate the request using each authentication instance
in turn.
Returns a three-tuple of (authenticator, user, authtoken).
"""
for authenticator in self.authenticators:
try:

View File

@ -35,6 +35,15 @@ DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwarg
DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs'])
def escape_curly_brackets(url_path):
"""
Double brackets in regex of url_path for escape string formatting
"""
if ('{' and '}') in url_path:
url_path = url_path.replace('{', '{{').replace('}', '}}')
return url_path
def replace_methodname(format_string, methodname):
"""
Partially format a format_string, swapping out any
@ -178,6 +187,7 @@ class SimpleRouter(BaseRouter):
initkwargs = route.initkwargs.copy()
initkwargs.update(method_kwargs)
url_path = initkwargs.pop("url_path", None) or methodname
url_path = escape_curly_brackets(url_path)
url_name = initkwargs.pop("url_name", None) or url_path
ret.append(Route(
url=replace_methodname(route.url, url_path),
@ -323,7 +333,7 @@ class DefaultRouter(SimpleRouter):
warnings.warn(
"Including a schema directly via a router is now deprecated. "
"Use `get_schema_view()` instead.",
DeprecationWarning
DeprecationWarning, stacklevel=2
)
if 'schema_renderers' in kwargs:
assert 'schema_title' in kwargs, 'Missing "schema_title" argument.'

View File

@ -1159,6 +1159,11 @@ class ModelSerializer(Serializer):
field_class = field_mapping[model_field]
field_kwargs = get_field_kwargs(field_name, model_field)
# Special case to handle when a OneToOneField is also the primary key
if model_field.one_to_one and model_field.primary_key:
field_class = self.serializer_related_field
field_kwargs['queryset'] = model_field.related_model.objects
if 'choices' in field_kwargs:
# Fields with choices get coerced into `ChoiceField`
# instead of using their regular typed field.

View File

@ -102,7 +102,7 @@ $(function () {
var entry = entries[i]
var paramKey = entry[0]
var paramValue = entry[1]
var $elem = $form.find('[name=' + paramKey + ']')
var $elem = $form.find('[name="' + paramKey + '"]')
var dataType = $elem.data('type') || 'string'
if (dataType === 'integer' && paramValue) {

View File

@ -37,6 +37,21 @@ function doAjaxSubmit(e) {
if (contentType) {
data = form.find('[data-override="content"]').val() || ''
if (contentType === 'multipart/form-data') {
// We need to add a boundary parameter to the header
// We assume the first valid-looking boundary line in the body is correct
// regex is from RFC 2046 appendix A
var boundaryCharNoSpace = "0-9A-Z'()+_,-./:=?";
var boundaryChar = boundaryCharNoSpace + ' ';
var re = new RegExp('^--([' + boundaryChar + ']{0,69}[' + boundaryCharNoSpace + '])[\\s]*?$', 'im');
var boundary = data.match(re);
if (boundary !== null) {
contentType += '; boundary="' + boundary[1] + '"';
}
// Fix textarea.value EOL normalisation (multipart/form-data should use CR+NL, not NL)
data = data.replace(/\n/g, '\r\n');
}
} else {
contentType = form.attr('enctype') || form.attr('encoding')

View File

@ -32,7 +32,8 @@
<div class="wrapper">
{% block navbar %}
<div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
<div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"
role="navigation" aria-label="{% trans "navbar" %}">
<div class="container">
<span>
{% block branding %}
@ -70,9 +71,10 @@
{% endblock %}
<!-- Content -->
<div id="content">
<div id="content" role="main" aria-label="{% trans "content" %}">
{% block content %}
<div class="region" aria-label="{% trans "request form" %}">
{% if 'GET' in allowed_methods %}
<form id="get-form" class="pull-right">
<fieldset>
@ -131,8 +133,9 @@
{% trans "Filters" %}
</button>
{% endif %}
</div>
<div class="content-main">
<div class="content-main" role="main" aria-label="{% trans "main content" %}">
<div class="page-header">
<h1>{{ name }}</h1>
</div>
@ -148,11 +151,11 @@
</nav>
{% endif %}
<div class="request-info" style="clear: both" >
<div class="request-info" style="clear: both" aria-label="{% trans "request info" %}">
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
</div>
<div class="response-info">
<div class="response-info" aria-label="{% trans "response info" %}">
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}{% for key, val in response_headers|items %}
<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>{% endfor %}

View File

@ -192,7 +192,8 @@ def get_field_kwargs(field_name, model_field):
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
if max_length is not None and (isinstance(model_field, models.CharField) or
isinstance(model_field, models.TextField)):
isinstance(model_field, models.TextField) or
isinstance(model_field, models.FileField)):
kwargs['max_length'] = max_length
validator_kwarg = [
validator for validator in validator_kwarg

View File

@ -88,3 +88,11 @@ class NullableOneToOneSource(RESTFrameworkModel):
target = models.OneToOneField(
OneToOneTarget, null=True, blank=True,
related_name='nullable_source', on_delete=models.CASCADE)
class OneToOnePKSource(RESTFrameworkModel):
""" Test model where the primary key is a OneToOneField with another model. """
name = models.CharField(max_length=100)
target = models.OneToOneField(
OneToOneTarget, primary_key=True,
related_name='required_source', on_delete=models.CASCADE)

View File

@ -63,6 +63,7 @@ class RegularFieldsModel(models.Model):
slug_field = models.SlugField(max_length=100)
small_integer_field = models.SmallIntegerField()
text_field = models.TextField(max_length=100)
file_field = models.FileField(max_length=100)
time_field = models.TimeField()
url_field = models.URLField(max_length=100)
custom_field = CustomField()
@ -181,6 +182,7 @@ class TestRegularFieldMappings(TestCase):
slug_field = SlugField(max_length=100)
small_integer_field = IntegerField()
text_field = CharField(max_length=100, style={'base_template': 'textarea.html'})
file_field = FileField(max_length=100)
time_field = TimeField()
url_field = URLField(max_length=100)
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)

View File

@ -1,12 +1,13 @@
import uuid
import pytest
from _pytest.monkeypatch import MonkeyPatch
from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings
from django.utils.datastructures import MultiValueDict
from rest_framework import serializers
from rest_framework import relations, serializers
from rest_framework.fields import empty
from rest_framework.test import APISimpleTestCase
@ -25,6 +26,61 @@ class TestStringRelatedField(APISimpleTestCase):
assert representation == '<MockObject name=foo, pk=1>'
class MockApiSettings(object):
def __init__(self, cutoff, cutoff_text):
self.HTML_SELECT_CUTOFF = cutoff
self.HTML_SELECT_CUTOFF_TEXT = cutoff_text
class TestRelatedFieldHTMLCutoff(APISimpleTestCase):
def setUp(self):
self.queryset = MockQueryset([
MockObject(pk=i, name=str(i)) for i in range(0, 1100)
])
self.monkeypatch = MonkeyPatch()
def test_no_settings(self):
# The default is 1,000, so sans settings it should be 1,000 plus one.
for many in (False, True):
field = serializers.PrimaryKeyRelatedField(queryset=self.queryset,
many=many)
options = list(field.iter_options())
assert len(options) == 1001
assert options[-1].display_text == "More than 1000 items..."
def test_settings_cutoff(self):
self.monkeypatch.setattr(relations, "api_settings",
MockApiSettings(2, "Cut Off"))
for many in (False, True):
field = serializers.PrimaryKeyRelatedField(queryset=self.queryset,
many=many)
options = list(field.iter_options())
assert len(options) == 3 # 2 real items plus the 'Cut Off' item.
assert options[-1].display_text == "Cut Off"
def test_settings_cutoff_none(self):
# Setting it to None should mean no limit; the default limit is 1,000.
self.monkeypatch.setattr(relations, "api_settings",
MockApiSettings(None, "Cut Off"))
for many in (False, True):
field = serializers.PrimaryKeyRelatedField(queryset=self.queryset,
many=many)
options = list(field.iter_options())
assert len(options) == 1100
def test_settings_kwargs_cutoff(self):
# The explicit argument should override the settings.
self.monkeypatch.setattr(relations, "api_settings",
MockApiSettings(2, "Cut Off"))
for many in (False, True):
field = serializers.PrimaryKeyRelatedField(queryset=self.queryset,
many=many,
html_cutoff=100)
options = list(field.iter_options())
assert len(options) == 101
assert options[-1].display_text == "Cut Off"
class TestPrimaryKeyRelatedField(APISimpleTestCase):
def setUp(self):
self.queryset = MockQueryset([
@ -96,7 +152,7 @@ class TestHyperlinkedRelatedField(APISimpleTestCase):
def setUp(self):
self.queryset = MockQueryset([
MockObject(pk=1, name='foobar'),
MockObject(pk=2, name='baz qux'),
MockObject(pk=2, name='bazABCqux'),
])
self.field = serializers.HyperlinkedRelatedField(
view_name='example',
@ -116,7 +172,7 @@ class TestHyperlinkedRelatedField(APISimpleTestCase):
assert instance is self.queryset.items[0]
def test_hyperlinked_related_lookup_url_encoded_exists(self):
instance = self.field.to_internal_value('http://example.org/example/baz%20qux/')
instance = self.field.to_internal_value('http://example.org/example/baz%41%42%43qux/')
assert instance is self.queryset.items[1]
def test_hyperlinked_related_lookup_does_not_exist(self):

View File

@ -6,8 +6,8 @@ from django.utils import six
from rest_framework import serializers
from tests.models import (
ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget,
NullableForeignKeySource, NullableOneToOneSource,
NullableUUIDForeignKeySource, OneToOneTarget, UUIDForeignKeyTarget
NullableForeignKeySource, NullableOneToOneSource, NullableUUIDForeignKeySource,
OneToOnePKSource, OneToOneTarget, UUIDForeignKeyTarget
)
@ -63,6 +63,13 @@ class NullableOneToOneTargetSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'nullable_source')
class OneToOnePKSourceSerializer(serializers.ModelSerializer):
class Meta:
model = OneToOnePKSource
fields = '__all__'
# TODO: Add test that .data cannot be accessed prior to .is_valid
class PKManyToManyTests(TestCase):
@ -486,3 +493,51 @@ class PKNullableOneToOneTests(TestCase):
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
]
assert serializer.data == expected
class OneToOnePrimaryKeyTests(TestCase):
def setUp(self):
# Given: Some target models already exist
self.target = target = OneToOneTarget(name='target-1')
target.save()
self.alt_target = alt_target = OneToOneTarget(name='target-2')
alt_target.save()
def test_one_to_one_when_primary_key(self):
# When: Creating a Source pointing at the id of the second Target
target_pk = self.alt_target.id
source = OneToOnePKSourceSerializer(data={'name': 'source-2', 'target': target_pk})
# Then: The source is valid with the serializer
if not source.is_valid():
self.fail("Expected OneToOnePKTargetSerializer to be valid but had errors: {}".format(source.errors))
# Then: Saving the serializer creates a new object
new_source = source.save()
# Then: The new object has the same pk as the target object
self.assertEqual(new_source.pk, target_pk)
def test_one_to_one_when_primary_key_no_duplicates(self):
# When: Creating a Source pointing at the id of the second Target
target_pk = self.target.id
data = {'name': 'source-1', 'target': target_pk}
source = OneToOnePKSourceSerializer(data=data)
# Then: The source is valid with the serializer
self.assertTrue(source.is_valid())
# Then: Saving the serializer creates a new object
new_source = source.save()
# Then: The new object has the same pk as the target object
self.assertEqual(new_source.pk, target_pk)
# When: Trying to create a second object
second_source = OneToOnePKSourceSerializer(data=data)
self.assertFalse(second_source.is_valid())
expected = {'target': [u'one to one pk source with this target already exists.']}
self.assertDictEqual(second_source.errors, expected)
def test_one_to_one_when_primary_key_does_not_exist(self):
# Given: a target PK that does not exist
target_pk = self.target.pk + self.alt_target.pk
source = OneToOnePKSourceSerializer(data={'name': 'source-2', 'target': target_pk})
# Then: The source is not valid with the serializer
self.assertFalse(source.is_valid())
self.assertIn("Invalid pk", source.errors['target'][0])
self.assertIn("object does not exist", source.errors['target'][0])

View File

@ -65,6 +65,19 @@ class EmptyPrefixViewSet(viewsets.ModelViewSet):
return self.queryset[index]
class RegexUrlPathViewSet(viewsets.ViewSet):
@list_route(url_path='list/(?P<kwarg>[0-9]{4})')
def regex_url_path_list(self, request, *args, **kwargs):
kwarg = self.kwargs.get('kwarg', '')
return Response({'kwarg': kwarg})
@detail_route(url_path='detail/(?P<kwarg>[0-9]{4})')
def regex_url_path_detail(self, request, *args, **kwargs):
pk = self.kwargs.get('pk', '')
kwarg = self.kwargs.get('kwarg', '')
return Response({'pk': pk, 'kwarg': kwarg})
notes_router = SimpleRouter()
notes_router.register(r'notes', NoteViewSet)
@ -80,6 +93,9 @@ empty_prefix_urls = [
url(r'^', include(empty_prefix_router.urls)),
]
regex_url_path_router = SimpleRouter()
regex_url_path_router.register(r'', RegexUrlPathViewSet, base_name='regex')
urlpatterns = [
url(r'^non-namespaced/', include(namespaced_router.urls)),
url(r'^namespaced/', include(namespaced_router.urls, namespace='example', app_name='example')),
@ -87,6 +103,7 @@ urlpatterns = [
url(r'^example2/', include(kwarged_notes_router.urls)),
url(r'^empty-prefix/', include(empty_prefix_urls)),
url(r'^regex/', include(regex_url_path_router.urls))
]
@ -402,3 +419,19 @@ class TestEmptyPrefix(TestCase):
response = self.client.get('/empty-prefix/1/')
assert response.status_code == 200
assert json.loads(response.content.decode('utf-8')) == {'uuid': '111', 'text': 'First'}
@override_settings(ROOT_URLCONF='tests.test_routers')
class TestRegexUrlPath(TestCase):
def test_regex_url_path_list(self):
kwarg = '1234'
response = self.client.get('/regex/list/{}/'.format(kwarg))
assert response.status_code == 200
assert json.loads(response.content.decode('utf-8')) == {'kwarg': kwarg}
def test_regex_url_path_detail(self):
pk = '1'
kwarg = '1234'
response = self.client.get('/regex/{}/detail/{}/'.format(pk, kwarg))
assert response.status_code == 200
assert json.loads(response.content.decode('utf-8')) == {'pk': pk, 'kwarg': kwarg}

View File

@ -20,6 +20,9 @@ class MockQueryset(object):
def __init__(self, iterable):
self.items = iterable
def __getitem__(self, val):
return self.items[val]
def get(self, **lookup):
for item in self.items:
if all([