diff options
25 files changed, 422 insertions, 101 deletions
@@ -6,11 +6,23 @@ [![build-status-image]][travis] +--- + +**Full documentation for REST framework is available on [http://django-rest-framework.org][docs].** + +Note that this is the 2.0 version of REST framework. If you are looking for earlier versions please see the [0.4.x branch][0.4] on GitHub. + +--- + # Overview -This branch is the redesign of Django REST framework. It is a work in progress. +Django REST framework is a lightweight library that makes it easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views. + +Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box. -For more information, check out [the documentation][docs], in particular, the tutorial is recommended as the best place to get an overview of the redesign. +If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcment][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities. + +There is also a sandbox API you can use for testing purposes, [available here][sandbox]. # Requirements @@ -24,8 +36,6 @@ For more information, check out [the documentation][docs], in particular, the tu # Installation -**Leaving these instructions in for the moment, they'll be valid once this becomes the master version** - Install using `pip`... pip install djangorestframework @@ -35,10 +45,6 @@ Install using `pip`... git clone git@github.com:tomchristie/django-rest-framework.git pip install -r requirements.txt -# Quickstart - -**TODO** - # Development To build the docs. @@ -84,6 +90,10 @@ 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=restframework2 [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=restframework2 [twitter]: https://twitter.com/_tomchristie +[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X +[sandbox]: http://restframework.herokuapp.com/ +[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md + [docs]: http://tomchristie.github.com/django-rest-framework/ [urlobject]: https://github.com/zacharyvoase/urlobject [markdown]: http://pypi.python.org/pypi/Markdown/ diff --git a/docs/index.md b/docs/index.md index 4126fc77..a96d0925 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,12 +5,24 @@ **A toolkit for building well-connected, self-describing Web APIs.** -**WARNING: This documentation is for the 2.0 redesign of REST framework. It is a work in progress.** +--- + +**Note**: This documentation is for the 2.0 version of REST framework. If you are looking for earlier versions please see the [0.4.x branch][0.4] on GitHub. + +--- Django REST framework is a lightweight library that makes it easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views. Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box. +If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcment][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities. + +There is also a sandbox API you can use for testing purposes, [available here][sandbox]. + +**Below**: *Screenshot from the browseable API* + +![Screenshot][image] + ## Requirements REST framework requires the following: @@ -25,8 +37,6 @@ The following packages are optional: ## Installation -**WARNING: These instructions will only become valid once this becomes the master version** - Install using `pip`, including any optional packages you want... pip install djangorestframework @@ -56,9 +66,11 @@ If you're intending to use the browseable API you'll want to add REST framework' Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace. +<!-- ## Quickstart Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running with REST framework. +--> ## Tutorial @@ -152,6 +164,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [urlobject]: https://github.com/zacharyvoase/urlobject [markdown]: http://pypi.python.org/pypi/Markdown/ [yaml]: http://pypi.python.org/pypi/PyYAML +[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X +[image]: img/quickstart.png +[sandbox]: http://restframework.herokuapp.com/ [quickstart]: tutorial/quickstart.md [tut-1]: tutorial/1-serialization.md diff --git a/docs/template.html b/docs/template.html index fcceede5..08387968 100644 --- a/docs/template.html +++ b/docs/template.html @@ -53,7 +53,7 @@ <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a> <ul class="dropdown-menu"> - <li><a href="{{ base_url }}/tutorial/quickstart{{ suffix }}">Quickstart</a></li> + <!--<li><a href="{{ base_url }}/tutorial/quickstart{{ suffix }}">Quickstart</a></li>--> <li><a href="{{ base_url }}/tutorial/1-serialization{{ suffix }}">1 - Serialization</a></li> <li><a href="{{ base_url }}/tutorial/2-requests-and-responses{{ suffix }}">2 - Requests and responses</a></li> <li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li> diff --git a/docs/topics/browser-enhancements.md b/docs/topics/browser-enhancements.md index d4e128ae..6a11f0fa 100644 --- a/docs/topics/browser-enhancements.md +++ b/docs/topics/browser-enhancements.md @@ -2,42 +2,63 @@ > "There are two noncontroversial uses for overloaded POST. The first is to *simulate* HTTP's uniform interface for clients like web browsers that don't support PUT or DELETE" > -> — [RESTful Web Services](1), Leonard Richardson & Sam Ruby. +> — [RESTful Web Services][cite], Leonard Richardson & Sam Ruby. ## Browser based PUT, DELETE, etc... -**TODO: Preamble.** Note that this is the same strategy as is used in [Ruby on Rails](2). +REST framework supports browser-based `PUT`, `DELETE` and other methods, by +overloading `POST` requests using a hidden form field. + +Note that this is the same strategy as is used in [Ruby on Rails][rails]. For example, given the following form: <form action="/news-items/5" method="POST"> - <input type="hidden" name="_method" value="DELETE"> - </form> + <input type="hidden" name="_method" value="DELETE"> + </form> `request.method` would return `"DELETE"`. ## Browser based submission of non-form content -Browser-based submission of content types other than form are supported by using form fields named `_content` and `_content_type`: +Browser-based submission of content types other than form are supported by +using form fields named `_content` and `_content_type`: For example, given the following form: <form action="/news-items/5" method="PUT"> - <input type="hidden" name="_content_type" value="application/json"> - <input name="_content" value="{'count': 1}"> - </form> + <input type="hidden" name="_content_type" value="application/json"> + <input name="_content" value="{'count': 1}"> + </form> -`request.content_type` would return `"application/json"`, and `request.stream` would return `"{'count': 1}"` +`request.content_type` would return `"application/json"`, and +`request.stream` would return `"{'count': 1}"` ## URL based accept headers +REST framework can take `?accept=application/json` style URL parameters, +which allow the `Accept` header to be overridden. + +This can be useful for testing the API from a web browser, where you don't +have any control over what is sent in the `Accept` header. + ## URL based format suffixes +REST framework can take `?format=json` style URL parameters, which can be a +useful shortcut for determing which content type should be returned from +the view. + +This is a more concise than using the `accept` override, but it also gives +you less control. (For example you can't specify any media type parameters) + ## Doesn't HTML5 support PUT and DELETE forms? -Nope. It was at one point intended to support `PUT` and `DELETE` forms, but was later [dropped from the spec](3). There remains [ongoing discussion](4) about adding support for `PUT` and `DELETE`, as well as how to support content types other than form-encoded data. +Nope. It was at one point intended to support `PUT` and `DELETE` forms, but +was later [dropped from the spec][html5]. There remains +[ongoing discussion][put_delete] about adding support for `PUT` and `DELETE`, +as well as how to support content types other than form-encoded data. -[1]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260 -[2]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work -[3]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24 -[4]: http://amundsen.com/examples/put-delete-forms/ +[cite]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260 +[rails]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work +[html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24 +[put_delete]: http://amundsen.com/examples/put-delete-forms/ diff --git a/docs/topics/credits.md b/docs/topics/credits.md index a317afde..69d57802 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -50,6 +50,7 @@ The following people have helped make REST framework great. * Rob Dobson - [rdobson] * Daniel Vaca Araujo - [diviei] * Madis Väin - [madisvain] +* Stephan Groß - [minddust] Many thanks to everyone who's contributed to the project. @@ -61,6 +62,8 @@ Project hosting is with [GitHub]. Continuous integration testing is managed with [Travis CI][travis-ci]. +The [live sandbox][sandbox] is hosted on [Heroku]. + Various inspiration taken from the [Piston], [Tastypie] and [Dagny] projects. Development of REST framework 2.0 was sponsored by [DabApps]. @@ -82,6 +85,8 @@ To contact the author directly: [tastypie]: https://github.com/toastdriven/django-tastypie [dagny]: https://github.com/zacharyvoase/dagny [dabapps]: http://lab.dabapps.com +[sandbox]: http://restframework.herokuapp.com/ +[heroku]: http://www.heroku.com/ [tomchristie]: https://github.com/tomchristie [markotibold]: https://github.com/markotibold @@ -131,3 +136,4 @@ To contact the author directly: [rdobson]: https://github.com/rdobson [diviei]: https://github.com/diviei [madisvain]: https://github.com/madisvain +[minddust]: https://github.com/minddust
\ No newline at end of file diff --git a/docs/topics/rest-framework-2-announcement.md b/docs/topics/rest-framework-2-announcement.md index 82e8fe36..885d1918 100644 --- a/docs/topics/rest-framework-2-announcement.md +++ b/docs/topics/rest-framework-2-announcement.md @@ -1,12 +1,18 @@ # Django REST framework 2 -What it is, and why you should care +What it is, and why you should care. > Most people just make the mistake that it should be simple to design simple things. In reality, the effort required to design something is inversely proportional to the simplicity of the result. > > — [Roy Fielding][cite] -REST framework 2 is an almost complete reworking of the original framework, which comprehensivly addresses some of the original design issues. +--- + +**Announcement:** REST framework 2 released - Tue 30th Oct 2012 + +--- + +REST framework 2 is an almost complete reworking of the original framework, which comprehensively addresses some of the original design issues. Because the latest version should be considered a re-release, rather than an incremental improvement, we've skipped a version, and called this release Django REST framework 2.0. @@ -74,15 +80,21 @@ There are also some functionality improvments - actions such as as `POST` and `D As you can see the documentation for REST framework has been radically improved. It gets a completely new style, using markdown for the documentation source, and a bootstrap-based theme for the styling. -We're really pleased with how the docs style looks - it's simple and clean, is easy to navigate around, and we think it reads great. We'll miss being able to use the wonderful [Read the Docs][readthedocs] service, but we think it's a trade-off worth making. +We're really pleased with how the docs style looks - it's simple and clean, is easy to navigate around, and we think it reads great. ## Summary In short, we've engineered the hell outta this thing, and we're incredibly proud of the result. +If you're interested please take a browse around the documentation. [The tutorial][tut] is a great place to get started. + +There's also a [live sandbox version of the tutorial API][sandbox] available for testing. + [cite]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-724 [quote1]: https://twitter.com/kobutsu/status/261689665952833536 [quote2]: https://groups.google.com/d/msg/django-rest-framework/heRGHzG6BWQ/ooVURgpwVC0J [quote3]: https://groups.google.com/d/msg/django-rest-framework/flsXbvYqRoY/9lSyntOf5cUJ [image]: ../img/quickstart.png [readthedocs]: https://readthedocs.org/ +[tut]: ../tutorial/1-serialization.md +[sandbox]: http://restframework.herokuapp.com/ diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 0b84a779..5cf16a67 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -4,7 +4,13 @@ This tutorial will cover creating a simple pastebin code highlighting Web API. Along the way it will introduce the various components that make up REST framework, and give you a comprehensive understanding of how everything fits together. -The tutorial is fairly in-depth, so you should probably get a cookie and a cup of your favorite brew before getting started. If you just want a quick overview, you should head over to the [quickstart] documentation instead. +The tutorial is fairly in-depth, so you should probably get a cookie and a cup of your favorite brew before getting started.<!-- If you just want a quick overview, you should head over to the [quickstart] documentation instead. --> + +--- + +**Note**: The final code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. There is also a sandbox version for testing, [available here][sandbox]. + +--- ## Setting up a new environment @@ -303,5 +309,7 @@ Our API views don't do anything particularly special at the moment, beyond serve We'll see how we can start to improve things in [part 2 of the tutorial][tut-2]. [quickstart]: quickstart.md +[repo]: https://github.com/tomchristie/rest-framework-tutorial +[sandbox]: http://restframework.herokuapp.com/ [virtualenv]: http://www.virtualenv.org/en/latest/index.html [tut-2]: 2-requests-and-responses.md diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 38e32157..1f663745 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -74,7 +74,7 @@ We can easily re-write our existing serializers to use hyperlinking. class SnippetSerializer(serializers.HyperlinkedModelSerializer): owner = serializers.Field(source='owner.username') - highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight') + highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html') class Meta: model = models.Snippet @@ -91,6 +91,8 @@ We can easily re-write our existing serializers to use hyperlinking. Notice that we've also added a new `'highlight'` field. This field is of the same type as the `url` field, except that it points to the `'snippet-highlight'` url pattern, instead of the `'snippet-detail'` url pattern. +Because we've included format suffixed URLs such as `'.json'`, we also need to indicate on the `highlight` field that any format suffixed hyperlinks it returns should use the `'.html'` suffix. + ## Making sure our URL patterns are named If we're going to have a hyperlinked API, we need to make sure we name our URL patterns. Let's take a look at which URL patterns we need to name. @@ -128,6 +130,20 @@ After adding all those names into our URLconf, our final `'urls.py'` file should namespace='rest_framework')) ) +## Adding pagination + +The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages. + +We can change the default list style to use pagination, by modifying our `settings.py` file slightly. Add the following setting: + + REST_FRAMEWORK = { + 'PAGINATE_BY': 10 + } + +Note that settings in REST framework are all namespaced into a single dictionary setting, named 'REST_FRAMEWORK', which helps keep them well seperated from your other project settings. + +We could also customize the pagination style if we needed too, but in this case we'll just stick with the default. + ## Reviewing our work If we open a browser and navigate to the browseable API, you'll find that you can now work your way around the API simply by following links. @@ -151,7 +167,7 @@ We've reached the end of our tutorial. If you want to get more involved in the **Now go build some awesome things.** [repo]: https://github.com/tomchristie/rest-framework-tutorial -[sandbox]: http://sultry-coast-6726.herokuapp.com/ +[sandbox]: http://restframework.herokuapp.com/ [github]: https://github.com/tomchristie/django-rest-framework [group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [twitter]: https://twitter.com/_tomchristie
\ No newline at end of file diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 948973ae..a231f191 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -10,8 +10,18 @@ def api_view(http_method_names): def decorator(func): - class WrappedAPIView(APIView): - pass + WrappedAPIView = type( + 'WrappedAPIView', + (APIView,), + {'__doc__': func.__doc__} + ) + + # Note, the above allows us to set the docstring. + # It is the equivelent of: + # + # class WrappedAPIView(APIView): + # pass + # WrappedAPIView.__doc__ = func.doc <--- Not possible to do this allowed_methods = set(http_method_names) | set(('options',)) WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods] diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 572425b9..89479deb 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -31,14 +31,6 @@ class PermissionDenied(APIException): self.detail = detail or self.default_detail -class InvalidFormat(APIException): - status_code = status.HTTP_404_NOT_FOUND - default_detail = "Format suffix '.%s' not found." - - def __init__(self, format, detail=None): - self.detail = (detail or self.default_detail) % format - - class MethodNotAllowed(APIException): status_code = status.HTTP_405_METHOD_NOT_ALLOWED default_detail = "Method '%s' not allowed." diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c0e527e5..1d6d760e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -5,7 +5,7 @@ import warnings from django.core import validators from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.core.urlresolvers import resolve +from django.core.urlresolvers import resolve, get_script_prefix from django.conf import settings from django.forms import widgets from django.utils.encoding import is_protected_type, smart_unicode @@ -13,6 +13,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.reverse import reverse from rest_framework.compat import parse_date, parse_datetime from rest_framework.compat import timezone +from urlparse import urlparse def is_simple_callable(obj): @@ -43,7 +44,7 @@ class Field(object): Called to set up a field prior to field_to_native or field_from_native. parent - The parent serializer. - model_field - The model field this field corrosponds to, if one exists. + model_field - The model field this field corresponds to, if one exists. """ self.parent = parent self.root = parent.root or parent @@ -113,7 +114,7 @@ class WritableField(Field): def __init__(self, source=None, read_only=False, required=None, validators=[], error_messages=None, widget=None, - default=None): + default=None, blank=None): super(WritableField, self).__init__(source=source) @@ -132,6 +133,7 @@ class WritableField(Field): self.validators = self.default_validators + validators self.default = default or self.default + self.blank = blank # Widgets are ony used for HTML forms. widget = widget or self.widget @@ -197,7 +199,7 @@ class WritableField(Field): class ModelField(WritableField): """ - A generic field that can be used against an arbirtrary model field. + A generic field that can be used against an arbitrary model field. """ def __init__(self, *args, **kwargs): try: @@ -244,7 +246,7 @@ class RelatedField(WritableField): return value = data.get(field_name) - into[(self.source or field_name) + '_id'] = self.from_native(value) + into[(self.source or field_name)] = self.from_native(value) class ManyRelatedMixin(object): @@ -288,6 +290,15 @@ class PrimaryKeyRelatedField(RelatedField): def to_native(self, pk): return pk + def from_native(self, data): + if self.queryset is None: + raise Exception('Writable related fields must include a `queryset` argument') + + try: + return self.queryset.get(pk=data) + except ObjectDoesNotExist: + raise ValidationError('Invalid hyperlink - object does not exist.') + def field_to_native(self, obj, field_name): try: # Prefer obj.serializable_value for performance reasons @@ -331,14 +342,16 @@ class HyperlinkedRelatedField(RelatedField): self.view_name = kwargs.pop('view_name') except: raise ValueError("Hyperlinked field requires 'view_name' kwarg") + self.format = kwargs.pop('format', None) super(HyperlinkedRelatedField, self).__init__(*args, **kwargs) def to_native(self, obj): view_name = self.view_name request = self.context.get('request', None) + format = self.format or self.context.get('format', None) kwargs = {self.pk_url_kwarg: obj.pk} try: - return reverse(view_name, kwargs=kwargs, request=request) + return reverse(view_name, kwargs=kwargs, request=request, format=format) except: pass @@ -349,13 +362,13 @@ class HyperlinkedRelatedField(RelatedField): kwargs = {self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request) + return reverse(self.view_name, kwargs=kwargs, request=request, format=format) except: pass kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request) + return reverse(self.view_name, kwargs=kwargs, request=request, format=format) except: pass @@ -364,6 +377,16 @@ class HyperlinkedRelatedField(RelatedField): def from_native(self, value): # Convert URL -> model instance pk # TODO: Use values_list + if self.queryset is None: + raise Exception('Writable related fields must include a `queryset` argument') + + if value.startswith('http:') or value.startswith('https:'): + # If needed convert absolute URLs to relative path + value = urlparse(value).path + prefix = get_script_prefix() + if value.startswith(prefix): + value = '/' + value[len(prefix):] + try: match = resolve(value) except: @@ -377,7 +400,7 @@ class HyperlinkedRelatedField(RelatedField): # Try explicit primary key. if pk is not None: - return pk + queryset = self.queryset.filter(pk=pk) # Next, try looking up by slug. elif slug is not None: slug_field = self.get_slug_field() @@ -390,7 +413,7 @@ class HyperlinkedRelatedField(RelatedField): obj = queryset.get() except ObjectDoesNotExist: raise ValidationError('Invalid hyperlink - object does not exist.') - return obj.pk + return obj class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField): @@ -405,13 +428,15 @@ class HyperlinkedIdentityField(Field): # TODO: Make this mandatory, and have the HyperlinkedModelSerializer # set it on-the-fly self.view_name = kwargs.pop('view_name', None) + self.format = kwargs.pop('format', None) super(HyperlinkedIdentityField, self).__init__(*args, **kwargs) def field_to_native(self, obj, field_name): request = self.context.get('request', None) + format = self.format or self.context.get('format', None) view_name = self.view_name or self.parent.opts.view_name view_kwargs = {'pk': obj.pk} - return reverse(view_name, kwargs=view_kwargs, request=request) + return reverse(view_name, kwargs=view_kwargs, request=request, format=format) ##### Typed Fields ##### @@ -448,6 +473,16 @@ class CharField(WritableField): if max_length is not None: self.validators.append(validators.MaxLengthValidator(max_length)) + def validate(self, value): + """ + Validates that the value is supplied (if required). + """ + # if empty string and allow blank + if self.blank and not value: + return + else: + super(CharField, self).validate(value) + def from_native(self, value): if isinstance(value, basestring) or value is None: return value @@ -509,7 +544,10 @@ class EmailField(CharField): default_validators = [validators.validate_email] def from_native(self, value): - return super(EmailField, self).from_native(value).strip() + ret = super(EmailField, self).from_native(value) + if ret is None: + return None + return ret.strip() def __deepcopy__(self, memo): result = copy.copy(self) @@ -531,8 +569,9 @@ class DateField(WritableField): empty = None def from_native(self, value): - if value is None: - return value + if value in validators.EMPTY_VALUES: + return None + if isinstance(value, datetime.datetime): if timezone and settings.USE_TZ and timezone.is_aware(value): # Convert aware datetimes to the default time zone @@ -570,8 +609,9 @@ class DateTimeField(WritableField): empty = None def from_native(self, value): - if value is None: - return value + if value in validators.EMPTY_VALUES: + return None + if isinstance(value, datetime.datetime): return value if isinstance(value, datetime.date): @@ -629,6 +669,7 @@ class IntegerField(WritableField): def from_native(self, value): if value in validators.EMPTY_VALUES: return None + try: value = int(str(value)) except (ValueError, TypeError): @@ -644,8 +685,9 @@ class FloatField(WritableField): } def from_native(self, value): - if value is None: - return value + if value in validators.EMPTY_VALUES: + return None + try: return float(value) except (TypeError, ValueError): diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 190a5f79..27540a57 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -1,5 +1,5 @@ """ -Generic views that provide commmonly needed behaviour. +Generic views that provide commonly needed behaviour. """ from rest_framework import views, mixins diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 444f8056..dae38477 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -1,3 +1,4 @@ +from django.http import Http404 from rest_framework import exceptions from rest_framework.settings import api_settings from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches @@ -66,7 +67,7 @@ class DefaultContentNegotiation(BaseContentNegotiation): renderers = [renderer for renderer in renderers if renderer.format == format] if not renderers: - raise exceptions.InvalidFormat(format) + raise Http404 return renderers def get_accept_list(self, request): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 938e8664..8dff0c77 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -283,7 +283,9 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.CharField: forms.CharField, serializers.BooleanField: forms.BooleanField, serializers.PrimaryKeyRelatedField: forms.ModelChoiceField, - serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField + serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField, + serializers.HyperlinkedRelatedField: forms.ModelChoiceField, + serializers.ManyHyperlinkedRelatedField: forms.ModelMultipleChoiceField } fields = {} diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index ba663f98..c9db02f0 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -5,13 +5,15 @@ from django.core.urlresolvers import reverse as django_reverse from django.utils.functional import lazy -def reverse(viewname, *args, **kwargs): +def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra): """ Same as `django.core.urlresolvers.reverse`, but optionally takes a request and returns a fully qualified URL, using the request to get the base URL. """ - request = kwargs.pop('request', None) - url = django_reverse(viewname, *args, **kwargs) + if format is not None: + kwargs = kwargs or {} + kwargs['format'] = format + url = django_reverse(viewname, args=args, kwargs=kwargs, **extra) if request: return request.build_absolute_uri(url) return url diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ce04b3e2..3d134a74 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -279,7 +279,7 @@ class BaseSerializer(Field): def errors(self): """ Run deserialization and return error data, - setting self.object if no errors occured. + setting self.object if no errors occurred. """ if self._errors is None: obj = self.from_native(self.init_data) @@ -393,6 +393,12 @@ class ModelSerializer(Serializer): Creates a default instance of a basic non-relational field. """ kwargs = {} + + kwargs['blank'] = model_field.blank + + if model_field.null: + kwargs['required'] = False + if model_field.has_default(): kwargs['required'] = False kwargs['default'] = model_field.get_default() diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index 739b9300..e29da395 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -32,6 +32,10 @@ h2, h3 { margin-right: 1em; } +ul.breadcrumb { + margin: 58px 0 0 0; +} + /* To allow tooltips to work on disabled elements */ .disabled-tooltip-shield { position: absolute; @@ -55,6 +59,7 @@ pre { .page-header { border-bottom: none; padding-bottom: 0px; + margin-bottom: 20px; } @@ -65,7 +70,7 @@ html{ background: none; } -body, .navbar .navbar-inner .container-fluid{ +body, .navbar .navbar-inner .container-fluid { max-width: 1150px; margin: 0 auto; } @@ -76,13 +81,14 @@ body{ } #content{ - margin: 40px 0 0 0; + margin: 0; } /* custom navigation styles */ .wrapper .navbar{ - width:100%; + width: 100%; position: absolute; - left:0; + left: 0; + top: 0; } .navbar .navbar-inner{ diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 5ac6ef67..e0f79481 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -109,7 +109,7 @@ <div class="content-main"> <div class="page-header"><h1>{{ name }}</h1></div> - <p class="resource-description">{{ description }}</p> + {{ description }} <div class="request-info"> <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre> diff --git a/rest_framework/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html index 65af512e..c1271399 100644 --- a/rest_framework/templates/rest_framework/login.html +++ b/rest_framework/templates/rest_framework/login.html @@ -3,42 +3,50 @@ <html> <head> - <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/style.css'/> + <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/> + <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/> + <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/> </head> - <body class="login"> + <body class="container"> - <div id="container"> - - <div id="header"> - <div id="branding"> - <h1 id="site-name">Django REST framework</h1> +<div class="container-fluid" style="margin-top: 30px"> + <div class="row-fluid"> + + <div class="well" style="width: 320px; margin-left: auto; margin-right: auto"> + <div class="row-fluid"> + <div> + <h3 style="margin: 0 0 20px;">Django REST framework</h3> </div> - </div> + </div><!-- /row fluid --> - <div id="content" class="colM"> - <div id="content-main"> - <form method="post" action="{% url 'rest_framework:login' %}" id="login-form"> + <div class="row-fluid"> + <div> + <form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post"> {% csrf_token %} - <div class="form-row"> - <label for="id_username">Username:</label> {{ form.username }} + <div id="div_id_username" class="clearfix control-group"> + <div class="controls" style="height: 30px"> + <Label class="span4" style="margin-top: 3px">Username:</label> + <input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username"> + </div> </div> - <div class="form-row"> - <label for="id_password">Password:</label> {{ form.password }} - <input type="hidden" name="next" value="{{ next }}" /> + <div id="div_id_password" class="clearfix control-group"> + <div class="controls" style="height: 30px"> + <Label class="span4" style="margin-top: 3px">Password:</label> + <input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password"> + </div> </div> - <div class="form-row"> - <label> </label><input type="submit" value="Log in"> + <input type="hidden" name="next" value="{{ next }}" /> + <div class="form-actions-no-box"> + <input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit"> </div> </form> - <script type="text/javascript"> - document.getElementById('id_username').focus() - </script> </div> - <br class="clear"> - </div> + </div><!-- /row fluid --> + </div><!--/span--> - <div id="footer"></div> + </div><!-- /.row-fluid --> + </div> </div> </body> diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index f4263478..d45ea976 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json from rest_framework import generics, serializers, status -from rest_framework.tests.models import BasicModel, Comment +from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel factory = RequestFactory() @@ -22,6 +22,22 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): model = BasicModel +class SlugSerializer(serializers.ModelSerializer): + slug = serializers.Field() # read only + + class Meta: + model = SlugBasedModel + exclude = ('id',) + + +class SlugBasedInstanceView(InstanceView): + """ + A model with a slug-field. + """ + model = SlugBasedModel + serializer_class = SlugSerializer + + class TestRootView(TestCase): def setUp(self): """ @@ -129,6 +145,7 @@ class TestInstanceView(TestCase): for obj in self.objects.all() ] self.view = InstanceView.as_view() + self.slug_based_view = SlugBasedInstanceView.as_view() def test_get_instance_view(self): """ @@ -198,7 +215,7 @@ class TestInstanceView(TestCase): def test_put_cannot_set_id(self): """ - POST requests to create a new object should not be able to set the id. + PUT requests to create a new object should not be able to set the id. """ content = {'id': 999, 'text': 'foobar'} request = factory.put('/1', json.dumps(content), @@ -224,6 +241,34 @@ class TestInstanceView(TestCase): updated = self.objects.get(id=1) self.assertEquals(updated.text, 'foobar') + def test_put_as_create_on_id_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if it doesn't exist. + """ + content = {'text': 'foobar'} + # pk fields can not be created on demand, only the database can set th pk for a new object + request = factory.put('/5', json.dumps(content), + content_type='application/json') + response = self.view(request, pk=5).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + new_obj = self.objects.get(pk=5) + self.assertEquals(new_obj.text, 'foobar') + + def test_put_as_create_on_slug_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if possible, else return HTTP_403_FORBIDDEN error-response. + """ + content = {'text': 'foobar'} + request = factory.put('/test_slug', json.dumps(content), + content_type='application/json') + response = self.slug_based_view(request, slug='test_slug').render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) + new_obj = SlugBasedModel.objects.get(slug='test_slug') + self.assertEquals(new_obj.text, 'foobar') + # Regression test for #285 diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index 5532a8ee..92c3691e 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -2,11 +2,19 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test.client import RequestFactory from rest_framework import generics, status, serializers -from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel +from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment factory = RequestFactory() +class BlogPostCommentSerializer(serializers.Serializer): + text = serializers.CharField() + blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail', queryset=BlogPost.objects.all()) + + def restore_object(self, attrs, instance=None): + return BlogPostComment(**attrs) + + class BasicList(generics.ListCreateAPIView): model = BasicModel model_serializer_class = serializers.HyperlinkedModelSerializer @@ -32,12 +40,22 @@ class ManyToManyDetail(generics.RetrieveAPIView): model_serializer_class = serializers.HyperlinkedModelSerializer +class BlogPostCommentListCreate(generics.ListCreateAPIView): + model = BlogPostComment + model_serializer_class = BlogPostCommentSerializer + + +class BlogPostDetail(generics.RetrieveAPIView): + model = BlogPost + urlpatterns = patterns('', url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'), url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'), url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'), url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'), url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'), + url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'), + url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list') ) @@ -124,3 +142,27 @@ class TestManyToManyHyperlinkedView(TestCase): response = self.detail_view(request, pk=1).render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data[0]) + + +class TestCreateWithForeignKeys(TestCase): + urls = 'rest_framework.tests.hyperlinkedserializers' + + def setUp(self): + """ + Create a blog post + """ + self.post = BlogPost.objects.create(title="Test post") + self.create_view = BlogPostCommentListCreate.as_view() + + def test_create_comment(self): + + data = { + 'text': 'A test comment', + 'blog_post_url': 'http://testserver/posts/1/' + } + + request = factory.post('/comments/', data=data) + response = self.create_view(request).render() + self.assertEqual(response.status_code, 201) + self.assertEqual(self.post.blogpostcomment_set.count(), 1) + self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index d4ea729b..415e4d06 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -52,6 +52,11 @@ class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100) +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) @@ -111,3 +116,13 @@ class BlogPost(RESTFrameworkModel): class BlogPostComment(RESTFrameworkModel): text = models.TextField() blog_post = models.ForeignKey(BlogPost) + + +class Person(RESTFrameworkModel): + name = models.CharField(max_length=10) + age = models.IntegerField(null=True, blank=True) + + +# Model for issue #324 +class BlankFieldModel(RESTFrameworkModel): + title = models.CharField(max_length=100, blank=True) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 5df3bd7e..d4b43862 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -43,6 +43,11 @@ class ActionItemSerializer(serializers.ModelSerializer): model = ActionItem +class PersonSerializer(serializers.ModelSerializer): + class Meta: + model = Person + + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -188,6 +193,14 @@ class ValidationTests(TestCase): self.assertFalse(serializer.is_valid()) self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']}) + def test_null_is_true_fields(self): + """ + Omitting a value for null-field should validate. + """ + serializer = PersonSerializer({'name': 'marko'}) + self.assertEquals(serializer.is_valid(), True) + self.assertEquals(serializer.errors, {}) + class MetadataTests(TestCase): def test_empty(self): @@ -436,3 +449,52 @@ class ManyRelatedTests(TestCase): } self.assertEqual(serializer.data, expected) + + +# Test for issue #324 +class BlankFieldTests(TestCase): + def setUp(self): + + class BlankFieldModelSerializer(serializers.ModelSerializer): + class Meta: + model = BlankFieldModel + + class BlankFieldSerializer(serializers.Serializer): + title = serializers.CharField(blank=True) + + class NotBlankFieldModelSerializer(serializers.ModelSerializer): + class Meta: + model = BasicModel + + class NotBlankFieldSerializer(serializers.Serializer): + title = serializers.CharField() + + self.model_serializer_class = BlankFieldModelSerializer + self.serializer_class = BlankFieldSerializer + self.not_blank_model_serializer_class = NotBlankFieldModelSerializer + self.not_blank_serializer_class = NotBlankFieldSerializer + self.data = {'title': ''} + + def test_create_blank_field(self): + serializer = self.serializer_class(self.data) + self.assertEquals(serializer.is_valid(), True) + + def test_create_model_blank_field(self): + serializer = self.model_serializer_class(self.data) + self.assertEquals(serializer.is_valid(), True) + + def test_create_not_blank_field(self): + """ + Test to ensure blank data in a field not marked as blank=True + is considered invalid in a non-model serializer + """ + serializer = self.not_blank_serializer_class(self.data) + self.assertEquals(serializer.is_valid(), False) + + def test_create_model_not_blank_field(self): + """ + Test to ensure blank data in a field not marked as blank=True + is considered invalid in a model serializer + """ + serializer = self.not_blank_model_serializer_class(self.data) + self.assertEquals(serializer.is_valid(), False) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 6860e6b9..8fe64248 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -16,7 +16,7 @@ class BaseThrottle(object): def wait(self): """ - Optionally, return a recommeded number of seconds to wait before + Optionally, return a recommended number of seconds to wait before the next request. """ return None diff --git a/rest_framework/views.py b/rest_framework/views.py index c721be3c..71e1fe6c 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -218,7 +218,7 @@ class APIView(View): def get_throttles(self): """ - Instantiates and returns the list of thottles that this view uses. + Instantiates and returns the list of throttles that this view uses. """ return [throttle() for throttle in self.throttle_classes] |
