diff options
| author | Tom Christie | 2012-11-01 23:04:13 +0000 | 
|---|---|---|
| committer | Tom Christie | 2012-11-01 23:04:13 +0000 | 
| commit | d327c5f531b341ad980d20454211b02b87f34d0e (patch) | |
| tree | a12f9fd53c73f7e9b4dd06785e3d357259e0a678 | |
| parent | 027c9079f62322fe933bdfd4438f23cf4848e3cc (diff) | |
| download | django-rest-framework-d327c5f531b341ad980d20454211b02b87f34d0e.tar.bz2 | |
Relational field support in browseable API.
Add slug relational fields.
Add quickstart.
| -rw-r--r-- | djangorestframework.egg-info/PKG-INFO | 19 | ||||
| -rw-r--r-- | djangorestframework.egg-info/SOURCES.txt | 86 | ||||
| -rw-r--r-- | djangorestframework.egg-info/dependency_links.txt | 1 | ||||
| -rw-r--r-- | djangorestframework.egg-info/top_level.txt | 7 | ||||
| -rw-r--r-- | docs/index.md | 2 | ||||
| -rw-r--r-- | docs/template.html | 2 | ||||
| -rw-r--r-- | docs/tutorial/quickstart.md | 9 | ||||
| -rw-r--r-- | rest_framework/fields.py | 107 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 25 | ||||
| -rw-r--r-- | rest_framework/static/rest_framework/css/default.css | 7 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 4 | 
11 files changed, 244 insertions, 25 deletions
diff --git a/djangorestframework.egg-info/PKG-INFO b/djangorestframework.egg-info/PKG-INFO new file mode 100644 index 00000000..04eafbde --- /dev/null +++ b/djangorestframework.egg-info/PKG-INFO @@ -0,0 +1,19 @@ +Metadata-Version: 1.0 +Name: djangorestframework +Version: 2.0.0 +Summary: A lightweight REST framework for Django. +Home-page: http://django-rest-framework.org +Author: Tom Christie +Author-email: tom@tomchristie.com +License: BSD +Download-URL: http://pypi.python.org/pypi/rest_framework/ +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Web Environment +Classifier: Framework :: Django +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP diff --git a/djangorestframework.egg-info/SOURCES.txt b/djangorestframework.egg-info/SOURCES.txt new file mode 100644 index 00000000..298d1c33 --- /dev/null +++ b/djangorestframework.egg-info/SOURCES.txt @@ -0,0 +1,86 @@ +MANIFEST.in +setup.py +djangorestframework.egg-info/PKG-INFO +djangorestframework.egg-info/SOURCES.txt +djangorestframework.egg-info/dependency_links.txt +djangorestframework.egg-info/top_level.txt +rest_framework/__init__.py +rest_framework/authentication.py +rest_framework/compat.py +rest_framework/decorators.py +rest_framework/exceptions.py +rest_framework/fields.py +rest_framework/generics.py +rest_framework/mixins.py +rest_framework/models.py +rest_framework/negotiation.py +rest_framework/pagination.py +rest_framework/parsers.py +rest_framework/permissions.py +rest_framework/renderers.py +rest_framework/request.py +rest_framework/response.py +rest_framework/reverse.py +rest_framework/serializers.py +rest_framework/settings.py +rest_framework/status.py +rest_framework/throttling.py +rest_framework/urlpatterns.py +rest_framework/urls.py +rest_framework/views.py +rest_framework/authtoken/__init__.py +rest_framework/authtoken/models.py +rest_framework/authtoken/views.py +rest_framework/authtoken/migrations/0001_initial.py +rest_framework/authtoken/migrations/__init__.py +rest_framework/runtests/__init__.py +rest_framework/runtests/runcoverage.py +rest_framework/runtests/runtests.py +rest_framework/runtests/settings.py +rest_framework/runtests/urls.py +rest_framework/static/rest_framework/css/bootstrap-tweaks.css +rest_framework/static/rest_framework/css/bootstrap.min.css +rest_framework/static/rest_framework/css/default.css +rest_framework/static/rest_framework/css/prettify.css +rest_framework/static/rest_framework/img/glyphicons-halflings-white.png +rest_framework/static/rest_framework/img/glyphicons-halflings.png +rest_framework/static/rest_framework/img/grid.png +rest_framework/static/rest_framework/js/bootstrap.min.js +rest_framework/static/rest_framework/js/default.js +rest_framework/static/rest_framework/js/jquery-1.8.1-min.js +rest_framework/static/rest_framework/js/prettify-min.js +rest_framework/templates/rest_framework/api.html +rest_framework/templates/rest_framework/base.html +rest_framework/templates/rest_framework/login.html +rest_framework/templatetags/__init__.py +rest_framework/templatetags/rest_framework.py +rest_framework/tests/__init__.py +rest_framework/tests/authentication.py +rest_framework/tests/breadcrumbs.py +rest_framework/tests/decorators.py +rest_framework/tests/description.py +rest_framework/tests/files.py +rest_framework/tests/genericrelations.py +rest_framework/tests/generics.py +rest_framework/tests/htmlrenderer.py +rest_framework/tests/hyperlinkedserializers.py +rest_framework/tests/models.py +rest_framework/tests/modelviews.py +rest_framework/tests/negotiation.py +rest_framework/tests/pagination.py +rest_framework/tests/parsers.py +rest_framework/tests/renderers.py +rest_framework/tests/request.py +rest_framework/tests/response.py +rest_framework/tests/reverse.py +rest_framework/tests/serializer.py +rest_framework/tests/status.py +rest_framework/tests/testcases.py +rest_framework/tests/tests.py +rest_framework/tests/throttling.py +rest_framework/tests/validators.py +rest_framework/tests/views.py +rest_framework/utils/__init__.py +rest_framework/utils/breadcrumbs.py +rest_framework/utils/encoders.py +rest_framework/utils/mediatypes.py
\ No newline at end of file diff --git a/djangorestframework.egg-info/dependency_links.txt b/djangorestframework.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/djangorestframework.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/djangorestframework.egg-info/top_level.txt b/djangorestframework.egg-info/top_level.txt new file mode 100644 index 00000000..7e18534d --- /dev/null +++ b/djangorestframework.egg-info/top_level.txt @@ -0,0 +1,7 @@ +rest_framework/authtoken +rest_framework/utils +rest_framework/tests +rest_framework/runtests +rest_framework/templatetags +rest_framework +rest_framework/authtoken/migrations 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/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/fields.py b/rest_framework/fields.py index 1d6d760e..6d858087 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 @@ -232,11 +233,72 @@ class ModelField(WritableField):  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 +      def __init__(self, *args, **kwargs):          self.queryset = kwargs.pop('queryset', None)          super(RelatedField, self).__init__(*args, **kwargs) +    ### 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 = 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)          return self.to_native(value) @@ -253,6 +315,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 +340,9 @@ class ManyRelatedMixin(object):  class ManyRelatedField(ManyRelatedMixin, RelatedField):      """      Base class for related model managers. + +    If not overridden, this represents a to-many relatinship, using the unicode +    representations of the target, and is read-only.      """      pass @@ -284,7 +351,7 @@ 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.      """      def to_native(self, pk): @@ -313,7 +380,7 @@ 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.      """      def to_native(self, pk):          return pk @@ -329,10 +396,36 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):          # Forward relationship          return [self.to_native(item.pk) for item in queryset.all()] +### Slug relationships + + +class SlugRelatedField(RelatedField): +    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): +        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' @@ -417,16 +510,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/renderers.py b/rest_framework/renderers.py index 8dff0c77..63578b14 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -282,10 +282,12 @@ class BrowsableAPIRenderer(BaseRenderer):              serializers.EmailField: forms.EmailField,              serializers.CharField: forms.CharField,              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 +298,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: 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..9b0a0dca 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -134,7 +134,7 @@                                  <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" }}                                      </div> @@ -159,7 +159,7 @@                                  <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" }}                                      </div>  | 
