aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephan Groß2012-11-05 11:45:49 +0100
committerStephan Groß2012-11-05 11:45:49 +0100
commit44449fa1f5f2d68740c9fb6a13c8e0a0eb41434c (patch)
treec4b6d8ed8af239dd5651d9f6a18ddbfe2079bc07
parentff7725f05e8ca624e54d707f7c655e3d5c8b8888 (diff)
parent5b397e50ddb999f85949a7359d0a26c3531c78a9 (diff)
downloaddjango-rest-framework-44449fa1f5f2d68740c9fb6a13c8e0a0eb41434c.tar.bz2
Merge remote-tracking branch 'upstream/master'
-rw-r--r--.gitignore2
-rw-r--r--README.md20
-rw-r--r--docs/api-guide/authentication.md6
-rw-r--r--docs/api-guide/fields.md42
-rw-r--r--docs/api-guide/throttling.md6
-rw-r--r--docs/index.md2
-rw-r--r--docs/template.html2
-rw-r--r--docs/topics/credits.md10
-rw-r--r--docs/topics/release-notes.md22
-rw-r--r--docs/tutorial/3-class-based-views.md4
-rw-r--r--docs/tutorial/quickstart.md9
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/fields.py164
-rw-r--r--rest_framework/mixins.py9
-rw-r--r--rest_framework/renderers.py33
-rw-r--r--rest_framework/response.py10
-rwxr-xr-xrest_framework/runtests/runcoverage.py4
-rw-r--r--rest_framework/runtests/settings.py6
-rw-r--r--rest_framework/static/rest_framework/css/default.css7
-rw-r--r--rest_framework/templates/rest_framework/base.html12
-rw-r--r--rest_framework/tests/generics.py6
-rw-r--r--rest_framework/tests/models.py7
-rw-r--r--rest_framework/tests/pk_relations.py187
-rw-r--r--rest_framework/tests/renderers.py89
-rw-r--r--rest_framework/tests/serializer.py25
25 files changed, 612 insertions, 74 deletions
diff --git a/.gitignore b/.gitignore
index 96186b46..25113d09 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,7 @@ html/
coverage/
build/
dist/
-rest_framework.egg-info/
+*.egg-info/
MANIFEST
!.gitignore
diff --git a/README.md b/README.md
index 4b2245cc..3d8dce94 100644
--- a/README.md
+++ b/README.md
@@ -57,8 +57,28 @@ To run the tests.
# Changelog
+## Master
+
+* Minor field improvements (don't stringify dicts, more robust many-pk fields)
+
+## 2.0.2
+
+**Date**: 2nd Nov 2012
+
+* Fix issues with pk related fields in the browsable API.
+
+## 2.0.1
+
+**Date**: 1st Nov 2012
+
+* Add support for relational fields in the browsable API.
+* Added SlugRelatedField and ManySlugRelatedField.
+* If PUT creates an instance return '201 Created', instead of '200 OK'.
+
## 2.0.0
+**Date**: 30th Oct 2012
+
* Redesign of core components.
* Fix **all of the things**.
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 889d16c0..3137b9d4 100644
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -30,7 +30,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
- 'rest_framework.authentication.UserBasicAuthentication',
+ 'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}
@@ -38,7 +38,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
You can also set the authentication policy on a per-view basis, using the `APIView` class based views.
class ExampleView(APIView):
- authentication_classes = (SessionAuthentication, UserBasicAuthentication)
+ authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
@@ -51,7 +51,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi
Or, if you're using the `@api_view` decorator with function based views.
@api_view(['GET'])
- @authentication_classes((SessionAuthentication, UserBasicAuthentication))
+ @authentication_classes((SessionAuthentication, BasicAuthentication))
@permissions_classes((IsAuthenticated,))
def example_view(request, format=None):
content = {
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 8c3df067..86460b4b 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -235,44 +235,48 @@ Then an example output format for a Bookmark instance would be:
'url': u'https://www.djangoproject.com/'
}
-## PrimaryKeyRelatedField
+## PrimaryKeyRelatedField / ManyPrimaryKeyRelatedField
-This field can be applied to any "to-one" relationship, such as a `ForeignKey` field.
+`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key.
-`PrimaryKeyRelatedField` will represent the target of the field using it's primary key.
+Be default these fields read-write, although you can change this behaviour using the `read_only` flag.
-Be default, `PrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
+**Arguments**:
-## ManyPrimaryKeyRelatedField
+* `queryset` - All relational fields must either set a queryset, or set `read_only=True`
-This field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship.
+## SlugRelatedField / ManySlugRelatedField
-`PrimaryKeyRelatedField` will represent the targets of the field using their primary key.
+`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug.
-Be default, `ManyPrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
+Be default these fields read-write, although you can change this behaviour using the `read_only` flag.
-## HyperlinkedRelatedField
+**Arguments**:
-This field can be applied to any "to-one" relationship, such as a `ForeignKey` field.
+* `slug_field` - The field on the target that should used as the representation. This should be a field that uniquely identifies any given instance. For example, `username`.
+* `queryset` - All relational fields must either set a queryset, or set `read_only=True`
-`HyperlinkedRelatedField` will represent the target of the field using a hyperlink. You must include a named URL pattern in your URL conf, with a name like `'{model-name}-detail'` that corresponds to the target of the hyperlink.
+## HyperlinkedRelatedField / ManyHyperlinkedRelatedField
-Be default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
-
-## ManyHyperlinkedRelatedField
+`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
-This field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship.
+Be default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
-`ManyHyperlinkedRelatedField` will represent the targets of the field using hyperlinks. You must include a named URL pattern in your URL conf, with a name like `'{model-name}-detail'` that corresponds to the target of the hyperlink.
+**Arguments**:
-Be default, `ManyHyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
+* `view_name` - The view name that should be used as the target of the relationship. **required**.
+* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
+* `queryset` - All relational fields must either set a queryset, or set `read_only=True`
## HyperLinkedIdentityField
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer.
-You must include a named URL pattern in your URL conf, with a name like `'{model-name}-detail'` that corresponds to the model.
-
This field is always read-only.
+**Arguments**:
+
+* `view_name` - The view name that should be used as the target of the relationship. **required**.
+* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
+
[cite]: http://www.python.org/dev/peps/pep-0020/
diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md
index bfda7079..b03bc9e0 100644
--- a/docs/api-guide/throttling.md
+++ b/docs/api-guide/throttling.md
@@ -31,8 +31,8 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': (
- 'rest_framework.throttles.AnonThrottle',
- 'rest_framework.throttles.UserThrottle'
+ 'rest_framework.throttling.AnonRateThrottle',
+ 'rest_framework.throttling.UserRateThrottle'
),
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
@@ -136,7 +136,7 @@ For example, given the following views...
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': (
- 'rest_framework.throttles.ScopedRateThrottle'
+ 'rest_framework.throttling.ScopedRateThrottle'
),
'DEFAULT_THROTTLE_RATES': {
'contacts': '1000/day',
diff --git a/docs/index.md b/docs/index.md
index 75a1cf6e..5e086872 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -66,11 +66,9 @@ 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
diff --git a/docs/template.html b/docs/template.html
index 94fc269f..c428dff3 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/credits.md b/docs/topics/credits.md
index 69d57802..ee00fc8b 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -51,6 +51,10 @@ The following people have helped make REST framework great.
* Daniel Vaca Araujo - [diviei]
* Madis Väin - [madisvain]
* Stephan Groß - [minddust]
+* Pavel Savchenko - [asfaltboy]
+* Otto Yiu - [ottoyiu]
+* Jacob Magnusson - [jmagnusson]
+* Osiloke Harold Emoekpere - [osiloke]
Many thanks to everyone who's contributed to the project.
@@ -136,4 +140,8 @@ 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
+[minddust]: https://github.com/minddust
+[asfaltboy]: https://github.com/asfaltboy
+[ottoyiu]: https://github.com/OttoYiu
+[jmagnusson]: https://github.com/jmagnusson
+[osiloke]: https://github.com/osiloke
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index a466f4b1..14d910bd 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -4,8 +4,30 @@
>
> &mdash; Eric S. Raymond, [The Cathedral and the Bazaar][cite].
+## Master
+
+* Support Django's cache framework.
+* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.)
+* Bugfixes (Support choice field in Browseable API)
+
+## 2.0.2
+
+**Date**: 2nd Nov 2012
+
+* Fix issues with pk related fields in the browsable API.
+
+## 2.0.1
+
+**Date**: 1st Nov 2012
+
+* Add support for relational fields in the browsable API.
+* Added SlugRelatedField and ManySlugRelatedField.
+* If PUT creates an instance return '201 Created', instead of '200 OK'.
+
## 2.0.0
+**Date**: 30th Oct 2012
+
* **Fix all of the things.** (Well, almost.)
* For more information please see the [2.0 migration guide][migration].
diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md
index a31dccb2..91ef4038 100644
--- a/docs/tutorial/3-class-based-views.md
+++ b/docs/tutorial/3-class-based-views.md
@@ -92,7 +92,7 @@ Let's take a look at how we can compose our views by using the mixin classes.
class SnippetList(mixins.ListModelMixin,
mixins.CreateModelMixin,
- generics.MultipleObjectBaseView):
+ generics.MultipleObjectAPIView):
model = Snippet
serializer_class = SnippetSerializer
@@ -102,7 +102,7 @@ Let's take a look at how we can compose our views by using the mixin classes.
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
-We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectBaseView`, and adding in `ListModelMixin` and `CreateModelMixin`.
+We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far.
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 6bde725b..93da1a59 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -19,12 +19,19 @@ First up we're going to define some serializers in `quickstart/serializers.py` t
class GroupSerializer(serializers.HyperlinkedModelSerializer):
+ permissions = serializers.ManySlugRelatedField(
+ slug_field='codename',
+ queryset=Permission.objects.all()
+ )
+
class Meta:
model = Group
fields = ('url', 'name', 'permissions')
Notice that we're using hyperlinked relations in this case, with `HyperlinkedModelSerializer`. You can also use primary key and various other relationships, but hyperlinking is good RESTful design.
+We've also overridden the `permission` field on the `GroupSerializer`. In this case we don't want to use a hyperlinked representation, but instead use the list of permission codenames associated with the group, so we've used a `ManySlugRelatedField`, using the `codename` field for the representation.
+
## Views
Right, we'd better write some views then. Open `quickstart/views.py` and get typing.
@@ -152,7 +159,7 @@ We can now access our API, both from the command-line, using tools like `curl`..
},
{
"email": "tom@example.com",
- "groups": [],
+ "groups": [ ],
"url": "http://127.0.0.1:8000/users/2/",
"username": "tom"
}
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 557f5943..56b9a35c 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '2.0.0'
+__version__ = '2.0.2'
VERSION = __version__ # synonym
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 63e15923..2185cf34 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -8,6 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix
from django.conf import settings
from django.forms import widgets
+from django.forms.models import ModelChoiceIterator
from django.utils.encoding import is_protected_type, smart_unicode
from django.utils.translation import ugettext_lazy as _
from rest_framework.reverse import reverse
@@ -89,6 +90,8 @@ class Field(object):
return value
elif hasattr(value, '__iter__') and not isinstance(value, (dict, basestring)):
return [self.to_native(item) for item in value]
+ elif isinstance(value, dict):
+ return dict(map(self.to_native, (k, v)) for k, v in value.items())
return smart_unicode(value)
def attributes(self):
@@ -211,9 +214,9 @@ class ModelField(WritableField):
def from_native(self, value):
try:
rel = self.model_field.rel
+ return rel.to._meta.get_field(rel.field_name).to_python(value)
except:
return self.model_field.to_python(value)
- return rel.to._meta.get_field(rel.field_name).to_python(value)
def field_to_native(self, obj, field_name):
value = self.model_field._get_val_from_obj(obj)
@@ -229,13 +232,77 @@ class ModelField(WritableField):
##### Relational fields #####
+# Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField):
"""
Base class for related model fields.
+
+ If not overridden, this represents a to-one relatinship, using the unicode
+ representation of the target.
"""
+ widget = widgets.Select
+ cache_choices = False
+ empty_label = None
+ default_read_only = True # TODO: Remove this
+
def __init__(self, *args, **kwargs):
self.queryset = kwargs.pop('queryset', None)
super(RelatedField, self).__init__(*args, **kwargs)
+ self.read_only = self.default_read_only
+
+ ### We need this stuff to make form choices work...
+
+ # def __deepcopy__(self, memo):
+ # result = super(RelatedField, self).__deepcopy__(memo)
+ # result.queryset = result.queryset
+ # return result
+
+ def prepare_value(self, obj):
+ return self.to_native(obj)
+
+ def label_from_instance(self, obj):
+ """
+ Return a readable representation for use with eg. select widgets.
+ """
+ desc = smart_unicode(obj)
+ ident = smart_unicode(self.to_native(obj))
+ if desc == ident:
+ return desc
+ return "%s - %s" % (desc, ident)
+
+ def _get_queryset(self):
+ return self._queryset
+
+ def _set_queryset(self, queryset):
+ self._queryset = queryset
+ self.widget.choices = self.choices
+
+ queryset = property(_get_queryset, _set_queryset)
+
+ def _get_choices(self):
+ # If self._choices is set, then somebody must have manually set
+ # the property self.choices. In this case, just return self._choices.
+ if hasattr(self, '_choices'):
+ return self._choices
+
+ # Otherwise, execute the QuerySet in self.queryset to determine the
+ # choices dynamically. Return a fresh ModelChoiceIterator that has not been
+ # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
+ # time _get_choices() is called (and, thus, each time self.choices is
+ # accessed) so that we can ensure the QuerySet has not been consumed. This
+ # construct might look complicated but it allows for lazy evaluation of
+ # the queryset.
+ return ModelChoiceIterator(self)
+
+ def _set_choices(self, value):
+ # Setting choices also sets the choices on the widget.
+ # choices can be any iterable, but we call list() on it because
+ # it will be consumed more than once.
+ self._choices = self.widget.choices = list(value)
+
+ choices = property(_get_choices, _set_choices)
+
+ ### Regular serializier stuff...
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
@@ -253,6 +320,8 @@ class ManyRelatedMixin(object):
"""
Mixin to convert a related field to a many related field.
"""
+ widget = widgets.SelectMultiple
+
def field_to_native(self, obj, field_name):
value = getattr(obj, self.source or field_name)
return [self.to_native(item) for item in value.all()]
@@ -276,6 +345,9 @@ class ManyRelatedMixin(object):
class ManyRelatedField(ManyRelatedMixin, RelatedField):
"""
Base class for related model managers.
+
+ If not overridden, this represents a to-many relationship, using the unicode
+ representations of the target, and is read-only.
"""
pass
@@ -284,9 +356,25 @@ class ManyRelatedField(ManyRelatedMixin, RelatedField):
class PrimaryKeyRelatedField(RelatedField):
"""
- Serializes a related field or related object to a pk value.
+ Represents a to-one relationship as a pk value.
"""
+ default_read_only = False
+
+ # TODO: Remove these field hacks...
+ def prepare_value(self, obj):
+ return self.to_native(obj.pk)
+ def label_from_instance(self, obj):
+ """
+ Return a readable representation for use with eg. select widgets.
+ """
+ desc = smart_unicode(obj)
+ ident = smart_unicode(self.to_native(obj.pk))
+ if desc == ident:
+ return desc
+ return "%s - %s" % (desc, ident)
+
+ # TODO: Possibly change this to just take `obj`, through prob less performant
def to_native(self, pk):
return pk
@@ -297,7 +385,8 @@ class PrimaryKeyRelatedField(RelatedField):
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
- raise ValidationError('Invalid hyperlink - object does not exist.')
+ msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
+ raise ValidationError(msg)
def field_to_native(self, obj, field_name):
try:
@@ -313,8 +402,23 @@ class PrimaryKeyRelatedField(RelatedField):
class ManyPrimaryKeyRelatedField(ManyRelatedField):
"""
- Serializes a to-many related field or related manager to a pk value.
+ Represents a to-many relationship as a pk value.
"""
+ default_read_only = False
+
+ def prepare_value(self, obj):
+ return self.to_native(obj.pk)
+
+ def label_from_instance(self, obj):
+ """
+ Return a readable representation for use with eg. select widgets.
+ """
+ desc = smart_unicode(obj)
+ ident = smart_unicode(self.to_native(obj.pk))
+ if desc == ident:
+ return desc
+ return "%s - %s" % (desc, ident)
+
def to_native(self, pk):
return pk
@@ -329,13 +433,55 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
# Forward relationship
return [self.to_native(item.pk) for item in queryset.all()]
+ 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:
+ msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
+ raise ValidationError(msg)
+
+### Slug relationships
+
+
+class SlugRelatedField(RelatedField):
+ default_read_only = False
+
+ def __init__(self, *args, **kwargs):
+ self.slug_field = kwargs.pop('slug_field', None)
+ assert self.slug_field, 'slug_field is required'
+ super(SlugRelatedField, self).__init__(*args, **kwargs)
+
+ def to_native(self, obj):
+ return getattr(obj, self.slug_field)
+
+ 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(**{self.slug_field: data})
+ except ObjectDoesNotExist:
+ raise ValidationError('Object with %s=%s does not exist.' %
+ (self.slug_field, unicode(data)))
+
+
+class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
+ pass
+
### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
+ """
+ Represents a to-one relationship, using hyperlinking.
+ """
pk_url_kwarg = 'pk'
slug_url_kwarg = 'slug'
slug_field = 'slug'
+ default_read_only = False
def __init__(self, *args, **kwargs):
try:
@@ -419,16 +565,20 @@ class HyperlinkedRelatedField(RelatedField):
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
+ """
+ Represents a to-many relationship, using hyperlinking.
+ """
pass
class HyperlinkedIdentityField(Field):
"""
- A field that represents the model's identity using a hyperlink.
+ Represents the instance, or a property on the instance, using hyperlinking.
"""
+
def __init__(self, *args, **kwargs):
- # TODO: Make this mandatory, and have the HyperlinkedModelSerializer
- # set it on-the-fly
+ # TODO: Make view_name 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)
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 8873e4ae..47e4edf7 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -3,9 +3,6 @@ Basic building blocks for generic class based views.
We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways.
-
-Eg. Use mixins to build a Resource class, and have a Router class
- perform the binding of http methods to actions for us.
"""
from django.http import Http404
from rest_framework import status
@@ -32,7 +29,7 @@ class CreateModelMixin(object):
class ListModelMixin(object):
"""
List a queryset.
- Should be mixed in with `MultipleObjectBaseView`.
+ Should be mixed in with `MultipleObjectAPIView`.
"""
empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
@@ -78,15 +75,17 @@ class UpdateModelMixin(object):
def update(self, request, *args, **kwargs):
try:
self.object = self.get_object()
+ success_status = status.HTTP_200_OK
except Http404:
self.object = None
+ success_status = status.HTTP_201_CREATED
serializer = self.get_serializer(data=request.DATA, instance=self.object)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save()
- return Response(serializer.data)
+ return Response(serializer.data, status=success_status)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 8dff0c77..0a659bd1 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -100,7 +100,7 @@ class JSONPRenderer(JSONRenderer):
callback = self.get_callback(renderer_context)
json = super(JSONPRenderer, self).render(data, accepted_media_type,
renderer_context)
- return "%s(%s);" % (callback, json)
+ return u"%s(%s);" % (callback, json)
class XMLRenderer(BaseRenderer):
@@ -281,11 +281,14 @@ class BrowsableAPIRenderer(BaseRenderer):
serializers.DateField: forms.DateField,
serializers.EmailField: forms.EmailField,
serializers.CharField: forms.CharField,
+ serializers.ChoiceField: forms.ChoiceField,
serializers.BooleanField: forms.BooleanField,
- serializers.PrimaryKeyRelatedField: forms.ModelChoiceField,
- serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField,
- serializers.HyperlinkedRelatedField: forms.ModelChoiceField,
- serializers.ManyHyperlinkedRelatedField: forms.ModelMultipleChoiceField
+ serializers.PrimaryKeyRelatedField: forms.ChoiceField,
+ serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
+ serializers.SlugRelatedField: forms.ChoiceField,
+ serializers.ManySlugRelatedField: forms.MultipleChoiceField,
+ serializers.HyperlinkedRelatedField: forms.ChoiceField,
+ serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField
}
fields = {}
@@ -296,19 +299,14 @@ class BrowsableAPIRenderer(BaseRenderer):
kwargs = {}
kwargs['required'] = v.required
- if getattr(v, 'queryset', None):
- kwargs['queryset'] = v.queryset
+ #if getattr(v, 'queryset', None):
+ # kwargs['queryset'] = v.queryset
+
+ if getattr(v, 'choices', None) is not None:
+ kwargs['choices'] = v.choices
if getattr(v, 'widget', None):
widget = copy.deepcopy(v.widget)
- # If choices have friendly readable names,
- # then add in the identities too
- if getattr(widget, 'choices', None):
- choices = widget.choices
- if any([ident != desc for (ident, desc) in choices]):
- choices = [(ident, "%s (%s)" % (desc, ident))
- for (ident, desc) in choices]
- widget.choices = choices
kwargs['widget'] = widget
if getattr(v, 'default', None) is not None:
@@ -319,7 +317,10 @@ class BrowsableAPIRenderer(BaseRenderer):
try:
fields[k] = field_mapping[v.__class__](**kwargs)
except KeyError:
- fields[k] = forms.CharField(**kwargs)
+ if getattr(v, 'choices', None) is not None:
+ fields[k] = forms.ChoiceField(**kwargs)
+ else:
+ fields[k] = forms.CharField(**kwargs)
return fields
def get_form(self, view, method, request):
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 7a459c8f..006d7eeb 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -45,3 +45,13 @@ class Response(SimpleTemplateResponse):
# TODO: Deprecate and use a template tag instead
# TODO: Status code text for RFC 6585 status codes
return STATUS_CODE_TEXT.get(self.status_code, '')
+
+ def __getstate__(self):
+ """
+ Remove attributes from the response that shouldn't be cached
+ """
+ state = super(Response, self).__getstate__()
+ for key in ('accepted_renderer', 'renderer_context', 'data'):
+ if key in state:
+ del state[key]
+ return state
diff --git a/rest_framework/runtests/runcoverage.py b/rest_framework/runtests/runcoverage.py
index ea2e3d45..0ce379eb 100755
--- a/rest_framework/runtests/runcoverage.py
+++ b/rest_framework/runtests/runcoverage.py
@@ -32,10 +32,10 @@ def main():
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
DeprecationWarning
)
- failures = TestRunner(['rest_framework'])
+ failures = TestRunner(['tests'])
else:
test_runner = TestRunner()
- failures = test_runner.run_tests(['rest_framework'])
+ failures = test_runner.run_tests(['tests'])
cov.stop()
# Discover the list of all modules that we should test coverage for
diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py
index 951b1e72..b48f85e4 100644
--- a/rest_framework/runtests/settings.py
+++ b/rest_framework/runtests/settings.py
@@ -21,6 +21,12 @@ DATABASES = {
}
}
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ }
+}
+
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css
index e29da395..fdf45659 100644
--- a/rest_framework/static/rest_framework/css/default.css
+++ b/rest_framework/static/rest_framework/css/default.css
@@ -36,6 +36,13 @@ ul.breadcrumb {
margin: 58px 0 0 0;
}
+form select, form input {
+ width: 90%;
+}
+
+form select[multiple] {
+ height: 150px;
+}
/* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield {
position: absolute;
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index e0f79481..fb0e19f0 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -131,12 +131,12 @@
{% csrf_token %}
{{ post_form.non_field_errors }}
{% for field in post_form %}
- <div class="control-group {% if field.errors %}error{% endif %}">
+ <div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
- {{ field|add_class:"input-xlarge" }}
+ {{ field }}
<span class="help-inline">{{ field.help_text }}</span>
- {{ field.errors|add_class:"help-block" }}
+ <!--{{ field.errors|add_class:"help-block" }}-->
</div>
</div>
{% endfor %}
@@ -156,12 +156,12 @@
{% csrf_token %}
{{ put_form.non_field_errors }}
{% for field in put_form %}
- <div class="control-group {% if field.errors %}error{% endif %}">
+ <div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
- {{ field|add_class:"input-xlarge" }}
+ {{ field }}
<span class='help-inline'>{{ field.help_text }}</span>
- {{ field.errors|add_class:"help-block" }}
+ <!--{{ field.errors|add_class:"help-block" }}-->
</div>
</div>
{% endfor %}
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index d45ea976..a8279ef2 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -236,7 +236,7 @@ class TestInstanceView(TestCase):
request = factory.put('/1', json.dumps(content),
content_type='application/json')
response = self.view(request, pk=1).render()
- self.assertEquals(response.status_code, status.HTTP_200_OK)
+ self.assertEquals(response.status_code, status.HTTP_201_CREATED)
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
updated = self.objects.get(id=1)
self.assertEquals(updated.text, 'foobar')
@@ -251,7 +251,7 @@ class TestInstanceView(TestCase):
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)
+ self.assertEquals(response.status_code, status.HTTP_201_CREATED)
new_obj = self.objects.get(pk=5)
self.assertEquals(new_obj.text, 'foobar')
@@ -264,7 +264,7 @@ class TestInstanceView(TestCase):
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.status_code, status.HTTP_201_CREATED)
self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'})
new_obj = SlugBasedModel.objects.get(slug='test_slug')
self.assertEquals(new_obj.text, 'foobar')
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index 415e4d06..1a0078e8 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -122,6 +122,13 @@ 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,
+ }
+
# Model for issue #324
class BlankFieldModel(RESTFrameworkModel):
diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/pk_relations.py
new file mode 100644
index 00000000..9095dcd8
--- /dev/null
+++ b/rest_framework/tests/pk_relations.py
@@ -0,0 +1,187 @@
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+
+
+# ManyToMany
+
+class ManyToManyTarget(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManySource(models.Model):
+ name = models.CharField(max_length=100)
+ targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
+
+
+class ManyToManyTargetSerializer(serializers.ModelSerializer):
+ sources = serializers.ManyPrimaryKeyRelatedField(queryset=ManyToManySource.objects.all())
+
+ class Meta:
+ model = ManyToManyTarget
+
+
+class ManyToManySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ManyToManySource
+
+
+# ForeignKey
+
+class ForeignKeyTarget(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ForeignKeySource(models.Model):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
+
+
+class ForeignKeyTargetSerializer(serializers.ModelSerializer):
+ sources = serializers.ManyPrimaryKeyRelatedField(read_only=True)
+
+ class Meta:
+ model = ForeignKeyTarget
+
+
+class ForeignKeySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeySource
+
+
+# TODO: Add test that .data cannot be accessed prior to .is_valid
+
+class PrimaryKeyManyToManyTests(TestCase):
+ def setUp(self):
+ for idx in range(1, 4):
+ target = ManyToManyTarget(name='target-%d' % idx)
+ target.save()
+ source = ManyToManySource(name='source-%d' % idx)
+ source.save()
+ for target in ManyToManyTarget.objects.all():
+ source.targets.add(target)
+
+ def test_many_to_many_retrieve(self):
+ queryset = ManyToManySource.objects.all()
+ serializer = ManyToManySourceSerializer(instance=queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'targets': [1]},
+ {'id': 2, 'name': u'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_many_to_many_retrieve(self):
+ queryset = ManyToManyTarget.objects.all()
+ serializer = ManyToManyTargetSerializer(instance=queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
+ {'id': 2, 'name': u'target-2', 'sources': [2, 3]},
+ {'id': 3, 'name': u'target-3', 'sources': [3]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_many_to_many_update(self):
+ data = {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]}
+ instance = ManyToManySource.objects.get(pk=1)
+ serializer = ManyToManySourceSerializer(data, instance=instance)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = ManyToManySource.objects.all()
+ serializer = ManyToManySourceSerializer(instance=queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]},
+ {'id': 2, 'name': u'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_many_to_many_update(self):
+ data = {'id': 1, 'name': u'target-1', 'sources': [1]}
+ instance = ManyToManyTarget.objects.get(pk=1)
+ serializer = ManyToManyTargetSerializer(data, instance=instance)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # Ensure target 1 is updated, and everything else is as expected
+ queryset = ManyToManyTarget.objects.all()
+ serializer = ManyToManyTargetSerializer(instance=queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [1]},
+ {'id': 2, 'name': u'target-2', 'sources': [2, 3]},
+ {'id': 3, 'name': u'target-3', 'sources': [3]}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+
+class PrimaryKeyForeignKeyTests(TestCase):
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ new_target = ForeignKeyTarget(name='target-2')
+ new_target.save()
+ for idx in range(1, 4):
+ source = ForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_retrieve(self):
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(instance=queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 1},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_reverse_foreign_key_retrieve(self):
+ queryset = ForeignKeyTarget.objects.all()
+ serializer = ForeignKeyTargetSerializer(instance=queryset)
+ expected = [
+ {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
+ {'id': 2, 'name': u'target-2', 'sources': []},
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update(self):
+ data = {'id': 1, 'name': u'source-1', 'target': 2}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(data, instance=instance)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # # Ensure source 1 is updated, and everything else is as expected
+ queryset = ForeignKeySource.objects.all()
+ serializer = ForeignKeySourceSerializer(instance=queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 2},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ # reverse foreign keys MUST be read_only
+ # In the general case they do not provide .remove() or .clear()
+ # and cannot be arbitrarily set.
+
+ # def test_reverse_foreign_key_update(self):
+ # data = {'id': 1, 'name': u'target-1', 'sources': [1]}
+ # instance = ForeignKeyTarget.objects.get(pk=1)
+ # serializer = ForeignKeyTargetSerializer(data, instance=instance)
+ # self.assertTrue(serializer.is_valid())
+ # self.assertEquals(serializer.data, data)
+ # serializer.save()
+
+ # # Ensure target 1 is updated, and everything else is as expected
+ # queryset = ForeignKeyTarget.objects.all()
+ # serializer = ForeignKeyTargetSerializer(instance=queryset)
+ # expected = [
+ # {'id': 1, 'name': u'target-1', 'sources': [1]},
+ # {'id': 2, 'name': u'target-2', 'sources': []},
+ # ]
+ # self.assertEquals(serializer.data, expected)
diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py
index 48d8d9bd..9be4b114 100644
--- a/rest_framework/tests/renderers.py
+++ b/rest_framework/tests/renderers.py
@@ -1,6 +1,8 @@
+import pickle
import re
from django.conf.urls.defaults import patterns, url, include
+from django.core.cache import cache
from django.test import TestCase
from django.test.client import RequestFactory
@@ -83,6 +85,7 @@ class HTMLView1(APIView):
urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
+ url(r'^cache$', MockGETView.as_view()),
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
url(r'^html$', HTMLView.as_view()),
@@ -416,3 +419,89 @@ class XMLRendererTestCase(TestCase):
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
self.assertTrue(xml.endswith('</root>'))
self.assertTrue(string in xml, '%r not in %r' % (string, xml))
+
+
+# Tests for caching issue, #346
+class CacheRenderTest(TestCase):
+ """
+ Tests specific to caching responses
+ """
+
+ urls = 'rest_framework.tests.renderers'
+
+ cache_key = 'just_a_cache_key'
+
+ @classmethod
+ def _get_pickling_errors(cls, obj, seen=None):
+ """ Return any errors that would be raised if `obj' is pickled
+ Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897
+ """
+ if seen == None:
+ seen = []
+ try:
+ state = obj.__getstate__()
+ except AttributeError:
+ return
+ if state == None:
+ return
+ if isinstance(state,tuple):
+ if not isinstance(state[0],dict):
+ state=state[1]
+ else:
+ state=state[0].update(state[1])
+ result = {}
+ for i in state:
+ try:
+ pickle.dumps(state[i],protocol=2)
+ except pickle.PicklingError:
+ if not state[i] in seen:
+ seen.append(state[i])
+ result[i] = cls._get_pickling_errors(state[i],seen)
+ return result
+
+ def http_resp(self, http_method, url):
+ """
+ Simple wrapper for Client http requests
+ Removes the `client' and `request' attributes from as they are
+ added by django.test.client.Client and not part of caching
+ responses outside of tests.
+ """
+ method = getattr(self.client, http_method)
+ resp = method(url)
+ del resp.client, resp.request
+ return resp
+
+ def test_obj_pickling(self):
+ """
+ Test that responses are properly pickled
+ """
+ resp = self.http_resp('get', '/cache')
+
+ # Make sure that no pickling errors occurred
+ self.assertEqual(self._get_pickling_errors(resp), {})
+
+ # Unfortunately LocMem backend doesn't raise PickleErrors but returns
+ # None instead.
+ cache.set(self.cache_key, resp)
+ self.assertTrue(cache.get(self.cache_key) is not None)
+
+ def test_head_caching(self):
+ """
+ Test caching of HEAD requests
+ """
+ resp = self.http_resp('head', '/cache')
+ cache.set(self.cache_key, resp)
+
+ cached_resp = cache.get(self.cache_key)
+ self.assertIsInstance(cached_resp, Response)
+
+ def test_get_caching(self):
+ """
+ Test caching of GET requests
+ """
+ resp = self.http_resp('get', '/cache')
+ cache.set(self.cache_key, resp)
+
+ cached_resp = cache.get(self.cache_key)
+ self.assertIsInstance(cached_resp, Response)
+ self.assertEqual(cached_resp.content, resp.content)
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index d4b43862..3fd49064 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1,7 +1,9 @@
import datetime
from django.test import TestCase
from rest_framework import serializers
-from rest_framework.tests.models import *
+from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
+ BlankFieldModel, BlogPost, CallableDefaultValueModel, DefaultValueModel,
+ ManyToManyModel, Person, ReadOnlyManyToManyModel)
class SubComment(object):
@@ -44,8 +46,11 @@ class ActionItemSerializer(serializers.ModelSerializer):
class PersonSerializer(serializers.ModelSerializer):
+ info = serializers.Field(source='info')
+
class Meta:
model = Person
+ fields = ('name', 'age', 'info')
class BasicTests(TestCase):
@@ -67,6 +72,9 @@ class BasicTests(TestCase):
'created': datetime.datetime(2012, 1, 1),
'sub_comment': 'And Merry Christmas!'
}
+ self.person_data = {'name': 'dwight', 'age': 35}
+ self.person = Person(**self.person_data)
+ self.person.save()
def test_empty(self):
serializer = CommentSerializer()
@@ -97,6 +105,21 @@ class BasicTests(TestCase):
self.assertEquals(serializer.object, expected)
self.assertTrue(serializer.object is expected)
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!')
+
+ def test_model_fields_as_expected(self):
+ """ Make sure that the fields returned are the same as defined
+ in the Meta data
+ """
+ serializer = PersonSerializer(instance=self.person)
+ self.assertEquals(set(serializer.data.keys()),
+ set(['name', 'age', 'info']))
+
+ def test_field_with_dictionary(self):
+ """ Make sure that dictionaries from fields are left intact
+ """
+ serializer = PersonSerializer(instance=self.person)
+ expected = self.person_data
+ self.assertEquals(serializer.data['info'], expected)
class ValidationTests(TestCase):