diff options
| -rw-r--r-- | CONTRIBUTING.md | 17 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | docs/api-guide/fields.md | 49 | ||||
| -rw-r--r-- | docs/api-guide/serializers.md | 21 | ||||
| -rw-r--r-- | docs/index.md | 4 | ||||
| -rw-r--r-- | docs/topics/2.3-announcement.md | 6 | ||||
| -rw-r--r-- | docs/topics/contributing.md | 1 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 4 | ||||
| -rw-r--r-- | docs/topics/third-party-resources.md | 2 | ||||
| -rw-r--r-- | docs/tutorial/6-viewsets-and-routers.md | 4 | ||||
| -rw-r--r-- | docs/tutorial/quickstart.md | 2 | ||||
| -rw-r--r-- | requirements-test.txt | 15 | ||||
| -rw-r--r-- | requirements.txt | 17 | ||||
| -rw-r--r-- | rest_framework/parsers.py | 13 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 27 | ||||
| -rw-r--r-- | rest_framework/utils/serializer_helpers.py | 14 | ||||
| -rw-r--r-- | tests/models.py | 111 | ||||
| -rw-r--r-- | tests/test_generics.py | 56 | ||||
| -rw-r--r-- | tests/test_model_serializer.py | 24 | ||||
| -rw-r--r-- | tests/test_parsers.py | 32 |
20 files changed, 238 insertions, 193 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69802995..96e55161 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ If you use REST framework, we'd love you to be vocal about your experiences with Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. -When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. +When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. ## Code of conduct @@ -38,7 +38,7 @@ Some tips on good issue reporting: ## Triaging issues -Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to +Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to * Read through the ticket - does it make sense, is it missing any context that would help explain it better? * Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? @@ -62,7 +62,6 @@ To run the tests, clone the repository, and then: virtualenv env env/bin/activate pip install -r requirements.txt - pip install -r requirements-test.txt # Run the tests ./runtests.py @@ -130,8 +129,8 @@ There are a couple of conventions you should follow when working on the document Headers should use the hash style. For example: ### Some important topic - -The underline style should not be used. **Don't do this:** + +The underline style should not be used. **Don't do this:** Some important topic ==================== @@ -141,9 +140,9 @@ The underline style should not be used. **Don't do this:** Links should always use the reference style, with the referenced hyperlinks kept at the end of the document. Here is a link to [some other thing][other-thing]. - + More text... - + [other-thing]: http://example.com/other/thing This style helps keep the documentation source consistent and readable. @@ -159,9 +158,9 @@ Linking in this style means you'll be able to click the hyperlink in your markdo If you want to draw attention to a note or warning, use a pair of enclosing lines, like so: --- - + **Note:** A useful documentation note. - + --- # Third party packages @@ -3,13 +3,13 @@ [![build-status-image]][travis] [![pypi-version]][pypi] -**Awesome web-browseable Web APIs.** +**Awesome web-browsable Web APIs.** Full documentation for the project is available at [http://www.django-rest-framework.org][docs]. --- -**Note**: The incoming 3.0 version has now been merged to the `master` branch on GitHub. For the source of the currently available PyPI version, please see the `2.4.4` tag. +**Note**: We have now released Django REST framework 3.0. For older codebases you may want to refer to the version 2.4.4 [source code](https://github.com/tomchristie/django-rest-framework/tree/version-2.4.x), and [documentation](http://tomchristie.github.io/rest-framework-2-docs/). --- @@ -19,7 +19,7 @@ Django REST framework is a powerful and flexible toolkit for building Web APIs. Some reasons you might want to use REST framework: -* The [Web browseable API][sandbox] is a huge useability win for your developers. +* The [Web browsable API][sandbox] is a huge usability win for your developers. * [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box. * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. @@ -27,7 +27,7 @@ Some reasons you might want to use REST framework: There is a live example API for testing purposes, [available here][sandbox]. -**Below**: *Screenshot from the browseable API* +**Below**: *Screenshot from the browsable API* ![Screenshot][image] @@ -86,7 +86,7 @@ router.register(r'users', UserViewSet) # Wire up our API using automatic URL routing. -# Additionally, we include login URLs for the browseable API. +# Additionally, we include login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) @@ -180,7 +180,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master +[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.svg?branch=master [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [pypi-version]: https://pypip.in/version/djangorestframework/badge.svg [pypi]: https://pypi.python.org/pypi/djangorestframework diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 391a52e5..aa5cc84e 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -453,7 +453,7 @@ If you want to create a custom field, you'll need to subclass `Field` and then o The `.to_representation()` method is called to convert the initial datatype into a primitive, serializable datatype. -The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. +The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. This method should raise a `serializer.ValidationError` if the data is invalid. Note that the `WritableField` class that was present in version 2.x no longer exists. You should subclass `Field` and override `to_internal_value()` if the field supports data input. @@ -498,6 +498,53 @@ As an example, let's create a field that can be used represent the class name of """ return obj.__class__.__name__ +#### Raising validation errors + +Our `ColorField` class above currently does not perform any data validation. +To indicate invalid data, we should raise a `serializers.ValidationError`, like so: + + def to_internal_value(self, data): + if not isinstance(data, six.text_type): + msg = 'Incorrect type. Expected a string, but got %s' + raise ValidationError(msg % type(data).__name__) + + if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data): + raise ValidationError('Incorrect format. Expected `rgb(#,#,#)`.') + + data = data.strip('rgb(').rstrip(')') + red, green, blue = [int(col) for col in data.split(',')] + + if any([col > 255 or col < 0 for col in (red, green, blue)]): + raise ValidationError('Value out of range. Must be between 0 and 255.') + + return Color(red, green, blue) + +The `.fail()` method is a shortcut for raising `ValidationError` that takes a message string from the `error_messages` dictionary. For example: + + default_error_messages = { + 'incorrect_type': 'Incorrect type. Expected a string, but got {input_type}', + 'incorrect_format': 'Incorrect format. Expected `rgb(#,#,#)`.', + 'out_of_range': 'Value out of range. Must be between 0 and 255.' + } + + def to_internal_value(self, data): + if not isinstance(data, six.text_type): + msg = 'Incorrect type. Expected a string, but got %s' + self.fail('incorrect_type', input_type=type(data).__name__) + + if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data): + self.fail('incorrect_format') + + data = data.strip('rgb(').rstrip(')') + red, green, blue = [int(col) for col in data.split(',')] + + if any([col > 255 or col < 0 for col in (red, green, blue)]): + self.fail('out_of_range') + + return Color(red, green, blue) + +This style keeps you error messages more cleanly separated from your code, and should be preferred. + # Third party packages The following third party packages are also available. diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 0ee80d53..1779c863 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -96,7 +96,7 @@ If we want to be able to return complete object instances based on the validated If your object instances correspond to Django models you'll also want to ensure that these methods save the object to the database. For example, if `Comment` was a Django model, the methods might look like this: def create(self, validated_data): - return Comment.objcts.create(**validated_data) + return Comment.objects.create(**validated_data) def update(self, instance, validated_data): instance.email = validated_data.get('email', instance.email) @@ -567,13 +567,13 @@ There needs to be a way of determining which views should be used for hyperlinki By default hyperlinks are expected to correspond to a view name that matches the style `'{model_name}-detail'`, and looks up the instance by a `pk` keyword argument. -You can override a URL field view name and lookup field by using either, or both of, the `view_name` and `lookup_field` options in the `extra_field_kwargs` setting, like so: +You can override a URL field view name and lookup field by using either, or both of, the `view_name` and `lookup_field` options in the `extra_kwargs` setting, like so: class AccountSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Account fields = ('account_url', 'account_name', 'users', 'created') - extra_field_kwargs = { + extra_kwargs = { 'url': {'view_name': 'accounts', 'lookup_field': 'account_name'} 'users': {'lookup_field': 'username'} } @@ -689,6 +689,21 @@ Here's an example of how you might choose to implement multiple updates: It is possible that a third party package may be included alongside the 3.1 release that provides some automatic support for multiple update operations, similar to the `allow_add_remove` behavior that was present in REST framework 2. +#### Customizing ListSerializer initialization + +When a serializer with `many=True` is instantiated, we need to determine which arguments and keyword arguments should be passed to the `.__init__()` method for both the child `Serializer` class, and for the parent `ListSerializer` class. + +The default implementation is to pass all arguments to both classes, except for `validators`, and any custom keyword arguments, both of which are assumed to be intended for the child serializer class. + +Occasionally you might need to explicitly specify how the child and parent classes should be instantiated when `many=True` is passed. You can do so by using the `many_init` class method. + + @classmethod + def many_init(cls, *args, **kwargs): + # Instantiate the child serializer. + kwargs['child'] = cls() + # Instantiate the parent list serializer. + return CustomListSerializer(*args, **kwargs) + --- # BaseSerializer diff --git a/docs/index.md b/docs/index.md index 7631be1e..e0ba2332 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,7 +33,7 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b Some reasons you might want to use REST framework: -* The [Web browseable API][sandbox] is a huge usability win for your developers. +* The [Web browsable API][sandbox] is a huge usability win for your developers. * [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box. * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. @@ -134,7 +134,7 @@ Here's our project's root `urls.py` module: router.register(r'users', UserViewSet) # Wire up our API using automatic URL routing. - # Additionally, we include login URLs for the browseable API. + # Additionally, we include login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) diff --git a/docs/topics/2.3-announcement.md b/docs/topics/2.3-announcement.md index 9c9f3e9f..66e46865 100644 --- a/docs/topics/2.3-announcement.md +++ b/docs/topics/2.3-announcement.md @@ -35,7 +35,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w # Wire up our API using automatic URL routing. - # Additionally, we include login URLs for the browseable API. + # Additionally, we include login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) @@ -207,9 +207,9 @@ The old-style signature will continue to function but will raise a `PendingDepre ## View names and descriptions -The mechanics of how the names and descriptions used in the browseable API are generated has been modified and cleaned up somewhat. +The mechanics of how the names and descriptions used in the browsable API are generated has been modified and cleaned up somewhat. -If you've been customizing this behavior, for example perhaps to use `rst` markup for the browseable API, then you'll need to take a look at the implementation to see what updates you need to make. +If you've been customizing this behavior, for example perhaps to use `rst` markup for the browsable API, then you'll need to take a look at the implementation to see what updates you need to make. Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated. diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 99f4fc3c..c9626ebf 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -62,7 +62,6 @@ To run the tests, clone the repository, and then: virtualenv env source env/bin/activate pip install -r requirements.txt - pip install -r requirements-test.txt # Run the tests ./runtests.py diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 53187589..19dfbb98 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -121,7 +121,7 @@ You can determine your currently installed version using `pip freeze`: * Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode. * Fix `parse_header` argument convertion. * Fix mediatype detection under Python 3. -* Web browseable API now offers blank option on dropdown when the field is not required. +* Web browsable API now offers blank option on dropdown when the field is not required. * `APIException` representation improved for logging purposes. * Allow source="*" within nested serializers. * Better support for custom oauth2 provider backends. @@ -200,7 +200,7 @@ You can determine your currently installed version using `pip freeze`: * Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. * Added `cache` attribute to throttles to allow overriding of default cache. * 'Raw data' tab in browsable API now contains pre-populated data. -* 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views. +* 'Raw data' and 'HTML form' tab preference in browsable API now saved between page views. * Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending empty string instead of file now clears `FileField`. diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index efa0b91f..0358d614 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -93,7 +93,7 @@ The cookiecutter template includes a `runtests.py` which uses the `pytest` packa Before running, you'll need to install a couple test requirements. - $ pip install -r requirements-test.txt + $ pip install -r requirements.txt Once requirements installed, you can run `runtests.py`. diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 3fad509a..816e9da6 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -112,7 +112,7 @@ Here's our re-wired `urls.py` file. router.register(r'users', views.UserViewSet) # The API URLs are now determined automatically by the router. - # Additionally, we include the login URLs for the browseable API. + # Additionally, we include the login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) @@ -130,7 +130,7 @@ That doesn't mean it's always the right approach to take. There's a similar set ## Reviewing our work -With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats. +With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, and comes complete with authentication, per-object permissions, and multiple renderer formats. We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views. diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 1c398c1f..3e1ce0a9 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -100,7 +100,7 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`... router.register(r'groups', views.GroupViewSet) # Wire up our API using automatic URL routing. - # Additionally, we include login URLs for the browseable API. + # Additionally, we include login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index 06c8849a..00000000 --- a/requirements-test.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Test requirements -pytest-django==2.6 -pytest==2.5.2 -pytest-cov==1.6 -flake8==2.2.2 - -# Optional packages -markdown>=2.1.0 -PyYAML>=3.10 -defusedxml>=0.3 -django-guardian==1.2.4 -django-filter>=0.5.4 -django-oauth-plus>=2.2.1 -oauth2>=1.5.211 -django-oauth2-provider>=0.2.4 diff --git a/requirements.txt b/requirements.txt index f284644a..f282d3ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,18 @@ +# Minimum Django version Django>=1.4.11 + +# Test requirements +pytest-django==2.6 +pytest==2.5.2 +pytest-cov==1.6 +flake8==2.2.2 + +# Optional packages +markdown>=2.1.0 +PyYAML>=3.10 +defusedxml>=0.3 +django-guardian==1.2.4 +django-filter>=0.5.4 +django-oauth-plus>=2.2.1 +oauth2>=1.5.211 +django-oauth2-provider>=0.2.4 diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index ccb82f03..d229abec 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -256,23 +256,24 @@ class FileUploadParser(BaseParser): chunks = ChunkIter(stream, chunk_size) counters = [0] * len(upload_handlers) - for handler in upload_handlers: + for index, handler in enumerate(upload_handlers): try: handler.new_file(None, filename, content_type, content_length, encoding) except StopFutureHandlers: + upload_handlers = upload_handlers[:index + 1] break for chunk in chunks: - for i, handler in enumerate(upload_handlers): + for index, handler in enumerate(upload_handlers): chunk_length = len(chunk) - chunk = handler.receive_data_chunk(chunk, counters[i]) - counters[i] += chunk_length + chunk = handler.receive_data_chunk(chunk, counters[index]) + counters[index] += chunk_length if chunk is None: break - for i, handler in enumerate(upload_handlers): - file_obj = handler.file_complete(counters[i]) + for index, handler in enumerate(upload_handlers): + file_obj = handler.file_complete(counters[index]) if file_obj: return DataAndFiles(None, {'file': file_obj}) raise ParseError("FileUpload parse error - " diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0d0a4d9a..d417ca80 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -127,6 +127,14 @@ class BaseSerializer(Field): (self.__class__.__module__, self.__class__.__name__) ) + assert hasattr(self, '_errors'), ( + 'You must call `.is_valid()` before calling `.save()`.' + ) + + assert not self.errors, ( + 'You cannot call `.save()` on a serializer with invalid data.' + ) + validated_data = dict( list(self.validated_data.items()) + list(kwargs.items()) @@ -645,7 +653,24 @@ class ModelSerializer(Serializer): if relation_info.to_many and (field_name in validated_attrs): many_to_many[field_name] = validated_attrs.pop(field_name) - instance = ModelClass.objects.create(**validated_attrs) + try: + instance = ModelClass.objects.create(**validated_attrs) + except TypeError as exc: + msg = ( + 'Got a `TypeError` when calling `%s.objects.create()`. ' + 'This may be because you have a writable field on the ' + 'serializer class that is not a valid argument to ' + '`%s.objects.create()`. You may need to make the field ' + 'read-only, or override the %s.create() method to handle ' + 'this correctly.\nOriginal exception text was: %s.' % + ( + ModelClass.__name__, + ModelClass.__name__, + self.__class__.__name__, + exc + ) + ) + raise TypeError(msg) # Save many-to-many relationships after the instance is created. if many_to_many: diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 92d19857..277cf649 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,3 +1,4 @@ +import collections from rest_framework.compat import OrderedDict @@ -70,7 +71,7 @@ class NestedBoundField(BoundField): return BoundField(field, value, error, prefix=self.name + '.') -class BindingDict(object): +class BindingDict(collections.MutableMapping): """ This dict-like object is used to store fields on a serializer. @@ -92,11 +93,8 @@ class BindingDict(object): def __delitem__(self, key): del self.fields[key] - def items(self): - return self.fields.items() - - def keys(self): - return self.fields.keys() + def __iter__(self): + return iter(self.fields) - def values(self): - return self.fields.values() + def __len__(self): + return len(self.fields) diff --git a/tests/models.py b/tests/models.py index 06ec5a22..456b0a0b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -3,62 +3,20 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -def foobar(): - return 'foobar' - - -class CustomField(models.CharField): - - def __init__(self, *args, **kwargs): - kwargs['max_length'] = 12 - super(CustomField, self).__init__(*args, **kwargs) - - class RESTFrameworkModel(models.Model): """ Base for test models that sets app_label, so they play nicely. """ + class Meta: app_label = 'tests' abstract = True -class HasPositiveIntegerAsChoice(RESTFrameworkModel): - some_choices = ((1, 'A'), (2, 'B'), (3, 'C')) - some_integer = models.PositiveIntegerField(choices=some_choices) - - -class Anchor(RESTFrameworkModel): - text = models.CharField(max_length=100, default='anchor') - - class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description.")) -class SlugBasedModel(RESTFrameworkModel): - text = models.CharField(max_length=100) - slug = models.SlugField(max_length=32) - - -class DefaultValueModel(RESTFrameworkModel): - text = models.CharField(default='foobar', max_length=100) - extra = models.CharField(blank=True, null=True, max_length=100) - - -class CallableDefaultValueModel(RESTFrameworkModel): - text = models.CharField(default=foobar, max_length=100) - - -class ManyToManyModel(RESTFrameworkModel): - rel = models.ManyToManyField(Anchor, help_text='Some help text.') - - -class ReadOnlyManyToManyModel(RESTFrameworkModel): - text = models.CharField(max_length=100, default='anchor') - rel = models.ManyToManyField(Anchor) - - class BaseFilterableItem(RESTFrameworkModel): text = models.CharField(max_length=100) @@ -71,73 +29,6 @@ class FilterableItem(BaseFilterableItem): date = models.DateField() -# Model for regression test for #285 - -class Comment(RESTFrameworkModel): - email = models.EmailField() - content = models.CharField(max_length=200) - created = models.DateTimeField(auto_now_add=True) - - -class ActionItem(RESTFrameworkModel): - title = models.CharField(max_length=200) - started = models.NullBooleanField(default=False) - done = models.BooleanField(default=False) - info = CustomField(default='---', max_length=12) - - -# Models for reverse relations -class Person(RESTFrameworkModel): - name = models.CharField(max_length=10) - age = models.IntegerField(null=True, blank=True) - - @property - def info(self): - return { - 'name': self.name, - 'age': self.age, - } - - -class BlogPost(RESTFrameworkModel): - title = models.CharField(max_length=100) - writer = models.ForeignKey(Person, null=True, blank=True) - - def get_first_comment(self): - return self.blogpostcomment_set.all()[0] - - -class BlogPostComment(RESTFrameworkModel): - text = models.TextField() - blog_post = models.ForeignKey(BlogPost) - - -class Album(RESTFrameworkModel): - title = models.CharField(max_length=100, unique=True) - ref = models.CharField(max_length=10, unique=True, null=True, blank=True) - - -class Photo(RESTFrameworkModel): - description = models.TextField() - album = models.ForeignKey(Album) - - -# Model for issue #324 -class BlankFieldModel(RESTFrameworkModel): - title = models.CharField(max_length=100, blank=True, null=False, - default="title") - - -# Model for issue #380 -class OptionalRelationModel(RESTFrameworkModel): - other = models.ForeignKey('OptionalRelationModel', blank=True, null=True) - - -# Model for RegexField -class Book(RESTFrameworkModel): - isbn = models.CharField(max_length=13) - - # Models for relations tests # ManyToMany class ManyToManyTarget(RESTFrameworkModel): diff --git a/tests/test_generics.py b/tests/test_generics.py index 2690fb47..94023c30 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -6,12 +6,26 @@ from django.test import TestCase from django.utils import six from rest_framework import generics, renderers, serializers, status from rest_framework.test import APIRequestFactory -from tests.models import BasicModel, Comment, SlugBasedModel +from tests.models import BasicModel, RESTFrameworkModel from tests.models import ForeignKeySource, ForeignKeyTarget factory = APIRequestFactory() +# Models +class SlugBasedModel(RESTFrameworkModel): + text = models.CharField(max_length=100) + slug = models.SlugField(max_length=32) + + +# Model for regression test for #285 +class Comment(RESTFrameworkModel): + email = models.EmailField() + content = models.CharField(max_length=200) + created = models.DateTimeField(auto_now_add=True) + + +# Serializers class BasicSerializer(serializers.ModelSerializer): class Meta: model = BasicModel @@ -22,6 +36,15 @@ class ForeignKeySerializer(serializers.ModelSerializer): model = ForeignKeySource +class SlugSerializer(serializers.ModelSerializer): + slug = serializers.ReadOnlyField() + + class Meta: + model = SlugBasedModel + fields = ('text', 'slug') + + +# Views class RootView(generics.ListCreateAPIView): queryset = BasicModel.objects.all() serializer_class = BasicSerializer @@ -37,14 +60,6 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView): serializer_class = ForeignKeySerializer -class SlugSerializer(serializers.ModelSerializer): - slug = serializers.ReadOnlyField() - - class Meta: - model = SlugBasedModel - fields = ('text', 'slug') - - class SlugBasedInstanceView(InstanceView): """ A model with a slug-field. @@ -54,6 +69,7 @@ class SlugBasedInstanceView(InstanceView): lookup_field = 'slug' +# Tests class TestRootView(TestCase): def setUp(self): """ @@ -127,13 +143,13 @@ class TestRootView(TestCase): self.assertEqual(created.text, 'foobar') -EXPECTED_QUERYS_FOR_PUT = 3 if django.VERSION < (1, 6) else 2 +EXPECTED_QUERIES_FOR_PUT = 3 if django.VERSION < (1, 6) else 2 class TestInstanceView(TestCase): def setUp(self): """ - Create 3 BasicModel intances. + Create 3 BasicModel instances. """ items = ['foo', 'bar', 'baz', 'filtered out'] for item in items: @@ -173,7 +189,7 @@ class TestInstanceView(TestCase): """ data = {'text': 'foobar'} request = factory.put('/1', data, format='json') - with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT): + with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk='1').render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'}) @@ -187,7 +203,7 @@ class TestInstanceView(TestCase): data = {'text': 'foobar'} request = factory.patch('/1', data, format='json') - with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT): + with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) @@ -222,7 +238,7 @@ class TestInstanceView(TestCase): """ data = {'id': 999, 'text': 'foobar'} request = factory.put('/1', data, format='json') - with self.assertNumQueries(EXPECTED_QUERYS_FOR_PUT): + with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) @@ -288,9 +304,10 @@ class TestOverriddenGetObject(TestCase): Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the queryset/model mechanism but instead overrides get_object() """ + def setUp(self): """ - Create 3 BasicModel intances. + Create 3 BasicModel instances. """ items = ['foo', 'bar', 'baz'] for item in items: @@ -363,11 +380,11 @@ class ClassB(models.Model): class ClassA(models.Model): name = models.CharField(max_length=255) - childs = models.ManyToManyField(ClassB, blank=True, null=True) + children = models.ManyToManyField(ClassB, blank=True, null=True) class ClassASerializer(serializers.ModelSerializer): - childs = serializers.PrimaryKeyRelatedField( + children = serializers.PrimaryKeyRelatedField( many=True, queryset=ClassB.objects.all() ) @@ -380,8 +397,8 @@ class ExampleView(generics.ListCreateAPIView): queryset = ClassA.objects.all() -class TestM2MBrowseableAPI(TestCase): - def test_m2m_in_browseable_api(self): +class TestM2MBrowsableAPI(TestCase): + def test_m2m_in_browsable_api(self): """ Test for particularly ugly regression with m2m in browsable API """ @@ -424,7 +441,6 @@ class DynamicSerializerView(generics.ListCreateAPIView): class TestFilterBackendAppliedToViews(TestCase): - def setUp(self): """ Create 3 BasicModel instances to filter on. diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 3aec0da0..1bcd58e0 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -26,6 +26,10 @@ class CustomField(models.Field): pass +class OneFieldModel(models.Model): + char_field = models.CharField(max_length=100) + + class RegularFieldsModel(models.Model): """ A model class for testing regular flat fields. @@ -68,6 +72,26 @@ class FieldOptionsModel(models.Model): choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES) +class TestModelSerializer(TestCase): + def test_create_method(self): + class TestSerializer(serializers.ModelSerializer): + non_model_field = serializers.CharField() + + class Meta: + model = OneFieldModel + fields = ('char_field', 'non_model_field') + + serializer = TestSerializer(data={ + 'char_field': 'foo', + 'non_model_field': 'bar', + }) + serializer.is_valid() + with self.assertRaises(TypeError) as excinfo: + serializer.save() + msginitial = 'Got a `TypeError` when calling `OneFieldModel.objects.create()`.' + assert str(excinfo.exception).startswith(msginitial) + + class TestRegularFieldMappings(TestCase): def test_regular_fields(self): """ diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 3f2672df..88eccef3 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from rest_framework.compat import StringIO from django import forms from django.core.files.uploadhandler import MemoryFileUploadHandler from django.test import TestCase from django.utils import unittest from rest_framework.compat import etree +from rest_framework.compat import StringIO +from rest_framework.exceptions import ParseError from rest_framework.parsers import FormParser, FileUploadParser from rest_framework.parsers import XMLParser import datetime @@ -104,13 +105,40 @@ class TestFileUploadParser(TestCase): self.parser_context = {'request': request, 'kwargs': {}} def test_parse(self): - """ Make sure the `QueryDict` works OK """ + """ + Parse raw file upload. + """ parser = FileUploadParser() self.stream.seek(0) data_and_files = parser.parse(self.stream, None, self.parser_context) file_obj = data_and_files.files['file'] self.assertEqual(file_obj._size, 14) + def test_parse_missing_filename(self): + """ + Parse raw file upload when filename is missing. + """ + parser = FileUploadParser() + self.stream.seek(0) + self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = '' + with self.assertRaises(ParseError): + parser.parse(self.stream, None, self.parser_context) + + def test_parse_missing_filename_multiple_upload_handlers(self): + """ + Parse raw file upload with multiple handlers when filename is missing. + Regression test for #2109. + """ + parser = FileUploadParser() + self.stream.seek(0) + self.parser_context['request'].upload_handlers = ( + MemoryFileUploadHandler(), + MemoryFileUploadHandler() + ) + self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = '' + with self.assertRaises(ParseError): + parser.parse(self.stream, None, self.parser_context) + def test_get_filename(self): parser = FileUploadParser() filename = parser.get_filename(self.stream, None, self.parser_context) |
