From e7fd166048de8017c11152b102a80602a80d20c1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Sep 2016 21:29:21 +0100 Subject: [PATCH 1/6] Docs tweaks --- docs/api-guide/relations.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 8695b2c1e..42533823c 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -286,7 +286,7 @@ Would serialize to a nested representation like this: ], } -# Writable nested serializers +## Writable nested serializers By default nested serializers are read-only. If you want to support write-operations to a nested serializer field you'll need to create `create()` and/or `update()` methods in order to explicitly specify how the child relationships should be saved. @@ -324,8 +324,14 @@ By default nested serializers are read-only. If you want to support write-operat >>> serializer.save() +--- + # Custom relational fields +In rare cases where none of the existing relational styles fit the representation you need, +you can implement a completely custom relational field, that describes exactly how the +output representation should be generated from the model instance. + To implement a custom relational field, you should override `RelatedField`, and implement the `.to_representation(self, value)` method. This method takes the target of the field as the `value` argument, and should return the representation that should be used to serialize the target. The `value` argument will typically be a model instance. If you want to implement a read-write relational field, you must also implement the `.to_internal_value(self, data)` method. From 73fd1ff4e71bec2cb70e8e0bd659712af98fa6d4 Mon Sep 17 00:00:00 2001 From: Manjit Kumar Date: Fri, 30 Sep 2016 16:32:23 +0530 Subject: [PATCH 2/6] add drf-url-filters app to django-rest-framework in filtering docs (#4528) - enabling validtions on incoming query params with the ease of adding new filters as easy as adding a new key in a dict. --- docs/api-guide/filtering.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 0ccd51dd3..bc502e970 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -432,6 +432,10 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [django-url-filter][django-url-filter] provides a safe way to filter data via human-friendly URLs. It works very similar to DRF serializers and fields in a sense that they can be nested except they are called filtersets and filters. That provides easy way to filter related data. Also this library is generic-purpose so it can be used to filter other sources of data and not only Django `QuerySet`s. +## drf-url-filters + +[drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values. A beautiful python package `Voluptouos` is being used for validations on the incoming query parameters. The best part about voluptouos is you can define your own validations as per your query params requirements. + [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.io/en/latest/index.html @@ -443,3 +447,5 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters [django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter [django-url-filter]: https://github.com/miki725/django-url-filter +[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters + From 1b882f7281b475a8cb6eb993299d3ec2248dc535 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Fri, 30 Sep 2016 18:06:36 +0200 Subject: [PATCH 3/6] Add drf-dynamic-fields to third party packages (#4530) --- docs/api-guide/serializers.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 5772d940a..290e32f4f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -1089,6 +1089,7 @@ The following third party packages are also available. The [django-rest-marshmallow][django-rest-marshmallow] package provides an alternative implementation for serializers, using the python [marshmallow][marshmallow] library. It exposes the same API as the REST framework serializers, and can be used as a drop-in replacement in some use-cases. ## Serpy + The [serpy][serpy] package is an alternative implementation for serializers that is built for speed. [Serpy][serpy] serializes complex datatypes to simple native types. The native types can be easily converted to JSON or any other format needed. ## MongoengineModelSerializer @@ -1107,7 +1108,12 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide The [dynamic-rest][dynamic-rest] package extends the ModelSerializer and ModelViewSet interfaces, adding API query parameters for filtering, sorting, and including / excluding all fields and relationships defined by your serializers. +## Dynamic Fields Mixin + +The [drf-dynamic-fields][drf-dynamic-fields] package provides a mixin to dynamically limit the fields per serializer to a subset specified by an URL parameter. + ## HTML JSON Forms + The [html-json-forms][html-json-forms] package provides an algorithm and serializer for processing `
` submissions per the (inactive) [HTML JSON Form specification][json-form-spec]. The serializer facilitates processing of arbitrarily nested JSON structures within HTML. For example, `` will be interpreted as `{"items": [{"id": "5"}]}`. [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion @@ -1124,3 +1130,4 @@ The [html-json-forms][html-json-forms] package provides an algorithm and seriali [dynamic-rest]: https://github.com/AltSchool/dynamic-rest [html-json-forms]: https://github.com/wq/html-json-forms [json-form-spec]: https://www.w3.org/TR/html-json-forms/ +[drf-dynamic-fields]: https://github.com/dbrgn/drf-dynamic-fields From b8fcb7807a11f37c88e9bfc6eaf999875e392114 Mon Sep 17 00:00:00 2001 From: Angel Velasquez Date: Sun, 2 Oct 2016 03:11:40 -0300 Subject: [PATCH 4/6] Update Django security release 1.10.2 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8c20faa2b..48cecccf7 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ setenv = deps = django18: Django==1.8.15 django19: Django==1.9.10 - django110: Django==1.10 + django110: Django==1.10.2 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 883efbc19fb124db7741caad512afb5b482e291d Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Tue, 4 Oct 2016 14:44:50 +0200 Subject: [PATCH 5/6] Case insensitive uniqueness validation (#4534) --- docs/api-guide/validators.md | 1 + rest_framework/validators.py | 5 +++-- tests/test_validators.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index f04c74c3c..9df15ec15 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -61,6 +61,7 @@ It takes a single required argument, and an optional `messages` argument: * `queryset` *required* - This is the queryset against which uniqueness should be enforced. * `message` - The error message that should be used when validation fails. +* `lookup` - The lookup used to find an existing instance with the value being validated. Defaults to `'exact'`. This validator should be applied to *serializer fields*, like so: diff --git a/rest_framework/validators.py b/rest_framework/validators.py index ef23b9bd7..84af0b9d5 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -42,10 +42,11 @@ class UniqueValidator(object): """ message = _('This field must be unique.') - def __init__(self, queryset, message=None): + def __init__(self, queryset, message=None, lookup='exact'): self.queryset = queryset self.serializer_field = None self.message = message or self.message + self.lookup = lookup def set_context(self, serializer_field): """ @@ -62,7 +63,7 @@ class UniqueValidator(object): """ Filter the queryset to all instances matching the given attribute. """ - filter_kwargs = {self.field_name: value} + filter_kwargs = {'%s__%s' % (self.field_name, self.lookup): value} return qs_filter(queryset, **filter_kwargs) def exclude_current_instance(self, queryset): diff --git a/tests/test_validators.py b/tests/test_validators.py index ebc8b899f..ab2c87c11 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -31,7 +31,7 @@ class RelatedModel(models.Model): class RelatedModelSerializer(serializers.ModelSerializer): username = serializers.CharField(source='user.username', - validators=[UniqueValidator(queryset=UniquenessModel.objects.all())]) # NOQA + validators=[UniqueValidator(queryset=UniquenessModel.objects.all(), lookup='iexact')]) # NOQA class Meta: model = RelatedModel @@ -103,7 +103,7 @@ class TestUniquenessValidation(TestCase): AnotherUniquenessModel._meta.get_field('code').validators, []) def test_related_model_is_unique(self): - data = {'username': 'existing', 'email': 'new-email@example.com'} + data = {'username': 'Existing', 'email': 'new-email@example.com'} rs = RelatedModelSerializer(data=data) self.assertFalse(rs.is_valid()) self.assertEqual(rs.errors, From 915ac22aeb5cce447ea20863c594387d71159826 Mon Sep 17 00:00:00 2001 From: Alex Kahan Date: Tue, 4 Oct 2016 16:22:56 -0400 Subject: [PATCH 6/6] Adding tests for rest_framework.py (#4523) --- tests/test_templatetags.py | 145 ++++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index ac218df21..28390320b 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -3,14 +3,24 @@ from __future__ import unicode_literals from django.test import TestCase +from rest_framework.relations import Hyperlink from rest_framework.templatetags.rest_framework import ( - add_query_param, urlize_quoted_links + add_nested_class, add_query_param, format_value, urlize_quoted_links ) from rest_framework.test import APIRequestFactory factory = APIRequestFactory() +def format_html(html): + """ + Helper function that formats HTML in order for easier comparison + :param html: raw HTML text to be formatted + :return: Cleaned HTML with no newlines or spaces + """ + return html.replace('\n', '').replace(' ', '') + + class TemplateTagTests(TestCase): def test_add_query_param_with_non_latin_character(self): @@ -22,6 +32,139 @@ class TemplateTagTests(TestCase): self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url) self.assertIn("format=json", json_url) + def test_format_value_boolean_or_none(self): + """ + Tests format_value with booleans and None + """ + self.assertEqual(format_value(True), 'true') + self.assertEqual(format_value(False), 'false') + self.assertEqual(format_value(None), 'null') + + def test_format_value_hyperlink(self): + url = 'http://url.com' + name = 'name_of_url' + hyperlink = Hyperlink(url, name) + self.assertEqual(format_value(hyperlink), '%s' % (url, name)) + + def test_format_value_list(self): + """ + Tests format_value with a list of strings + """ + list_items = ['item1', 'item2', 'item3'] + self.assertEqual(format_value(list_items), '\n item1, item2, item3\n') + self.assertEqual(format_value([]), '\n\n') + + def test_format_value_table(self): + """ + Tests format_value with a list of lists/dicts + """ + list_of_lists = [['list1'], ['list2'], ['list3']] + expected_list_format = """ + + + + 0 + list1 + + + 1 + list2 + + + 2 + list3 + + + """ + self.assertEqual( + format_html(format_value(list_of_lists)), + format_html(expected_list_format) + ) + + expected_dict_format = """ + + + + 0 + + + + 1 + + + + 2 + + + + """ + + list_of_dicts = [{'item1': 'value1'}, {'item2': 'value2'}, {'item3': 'value3'}] + self.assertEqual( + format_html(format_value(list_of_dicts)), + format_html(expected_dict_format) + ) + + def test_format_value_simple_string(self): + """ + Tests format_value with a simple string + """ + simple_string = 'this is an example of a string' + self.assertEqual(format_value(simple_string), simple_string) + + def test_format_value_string_hyperlink(self): + """ + Tests format_value with a url + """ + url = 'http://www.example.com' + self.assertEqual(format_value(url), 'http://www.example.com') + + def test_format_value_string_email(self): + """ + Tests format_value with an email address + """ + email = 'something@somewhere.com' + self.assertEqual(format_value(email), 'something@somewhere.com') + + def test_format_value_string_newlines(self): + """ + Tests format_value with a string with newline characters + :return: + """ + text = 'Dear user, \n this is a message \n from,\nsomeone' + self.assertEqual(format_value(text), '
Dear user, \n this is a message \n from,\nsomeone
') + + def test_format_value_object(self): + """ + Tests that format_value with a object returns the object's __str__ method + """ + obj = object() + self.assertEqual(format_value(obj), obj.__str__()) + + def test_add_nested_class(self): + """ + Tests that add_nested_class returns the proper class + """ + positive_cases = [ + [['item']], + [{'item1': 'value1'}], + {'item1': 'value1'} + ] + + negative_cases = [ + ['list'], + '', + None, + True, + False + ] + + for case in positive_cases: + self.assertEqual(add_nested_class(case), 'class=nested') + + for case in negative_cases: + self.assertEqual(add_nested_class(case), '') + class Issue1386Tests(TestCase): """