From 2a1485e00943b8280245d19e1e1f8514b1ef18ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Dec 2014 21:32:43 +0000 Subject: Final bits of docs for ModelSerializer fields API --- rest_framework/utils/model_meta.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index c98725c6..dfc387ca 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -24,7 +24,7 @@ FieldInfo = namedtuple('FieldResult', [ RelationInfo = namedtuple('RelationInfo', [ 'model_field', - 'related', + 'related_model', 'to_many', 'has_through_model' ]) @@ -77,7 +77,7 @@ def get_field_info(model): for field in [field for field in opts.fields if field.serialize and field.rel]: forward_relations[field.name] = RelationInfo( model_field=field, - related=_resolve_model(field.rel.to), + related_model=_resolve_model(field.rel.to), to_many=False, has_through_model=False ) @@ -86,7 +86,7 @@ def get_field_info(model): for field in [field for field in opts.many_to_many if field.serialize]: forward_relations[field.name] = RelationInfo( model_field=field, - related=_resolve_model(field.rel.to), + related_model=_resolve_model(field.rel.to), to_many=True, has_through_model=( not field.rel.through._meta.auto_created @@ -99,7 +99,7 @@ def get_field_info(model): accessor_name = relation.get_accessor_name() reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related_model=relation.model, to_many=relation.field.rel.multiple, has_through_model=False ) @@ -109,7 +109,7 @@ def get_field_info(model): accessor_name = relation.get_accessor_name() reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related_model=relation.model, to_many=True, has_through_model=( (getattr(relation.field.rel, 'through', None) is not None) -- cgit v1.2.3 From 6c5ff712783ae7e6edebb52508f1d43249f1aa00 Mon Sep 17 00:00:00 2001 From: Remi Paulmier Date: Mon, 22 Dec 2014 18:05:07 +0100 Subject: fix the way to use textarea rather than input with models.TextField --- rest_framework/utils/field_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index fca97b4b..b16e9df0 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -80,7 +80,7 @@ def get_field_kwargs(field_name, model_field): kwargs['decimal_places'] = decimal_places if isinstance(model_field, models.TextField): - kwargs['style'] = {'type': 'textarea'} + kwargs['style'] = {'base_template': 'textarea.html'} if isinstance(model_field, models.AutoField) or not model_field.editable: # If this field is read-only, then return early. -- cgit v1.2.3 From ef2eff2abac64ffbed621bb9a72a2229841a1db1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Dec 2014 11:07:38 +0000 Subject: Only pass max_length for CharField. Closes #2317. --- rest_framework/utils/field_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index b16e9df0..b2f4dd80 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -106,7 +106,7 @@ def get_field_kwargs(field_name, model_field): # Ensure that max_length is passed explicitly as a keyword arg, # rather than as a validator. max_length = getattr(model_field, 'max_length', None) - if max_length is not None: + if max_length is not None and isinstance(model_field, models.CharField): kwargs['max_length'] = max_length validator_kwarg = [ validator for validator in validator_kwarg -- cgit v1.2.3 From a7479721c844926f377085d8c336a2f60b7b2a38 Mon Sep 17 00:00:00 2001 From: Kyle Valade Date: Mon, 29 Dec 2014 00:35:00 -0800 Subject: First pass at refactoring get_field_info in utils.model_meta --- rest_framework/utils/model_meta.py | 57 ++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 14 deletions(-) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index c98725c6..375d2e8c 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -35,7 +35,7 @@ def _resolve_model(obj): Resolve supplied `obj` to a Django model class. `obj` must be a Django model class itself, or a string - representation of one. Useful in situtations like GH #1225 where + representation of one. Useful in situations like GH #1225 where Django may not have resolved a string-based reference to a model in another model's foreign key definition. @@ -56,23 +56,44 @@ def _resolve_model(obj): def get_field_info(model): """ - Given a model class, returns a `FieldInfo` instance containing metadata - about the various field types on the model. + Given a model class, returns a `FieldInfo` instance, which is a + `namedtuple`, containing metadata about the various field types on the model + including information about their relationships. """ opts = model._meta.concrete_model._meta - # Deal with the primary key. + pk = _get_pk(opts) + fields = _get_fields(opts) + forward_relations = _get_forward_relationships(opts) + reverse_relations = _get_reverse_relationships(opts) + fields_and_pk = _merge_fields_and_pk(pk, fields) + relationships = _merge_relationships(forward_relations, reverse_relations) + + return FieldInfo(pk, fields, forward_relations, reverse_relations, + fields_and_pk, relationships) + + +def _get_pk(opts): pk = opts.pk while pk.rel and pk.rel.parent_link: - # If model is a child via multitable inheritance, use parent's pk. + # If model is a child via multi-table inheritance, use parent's pk. pk = pk.rel.to._meta.pk - # Deal with regular fields. + return pk + + +def _get_fields(opts): fields = OrderedDict() for field in [field for field in opts.fields if field.serialize and not field.rel]: fields[field.name] = field - # Deal with forward relationships. + return fields + + +def _get_forward_relationships(opts): + """ + Returns an `OrderedDict` of field names to `RelationInfo`. + """ forward_relations = OrderedDict() for field in [field for field in opts.fields if field.serialize and field.rel]: forward_relations[field.name] = RelationInfo( @@ -93,7 +114,13 @@ def get_field_info(model): ) ) - # Deal with reverse relationships. + return forward_relations + + +def _get_reverse_relationships(opts): + """ + Returns an `OrderedDict` of field names to `RelationInfo`. + """ reverse_relations = OrderedDict() for relation in opts.get_all_related_objects(): accessor_name = relation.get_accessor_name() @@ -117,18 +144,20 @@ def get_field_info(model): ) ) - # Shortcut that merges both regular fields and the pk, - # for simplifying regular field lookup. + return reverse_relations + + +def _merge_fields_and_pk(pk, fields): fields_and_pk = OrderedDict() fields_and_pk['pk'] = pk fields_and_pk[pk.name] = pk fields_and_pk.update(fields) - # Shortcut that merges both forward and reverse relationships + return fields_and_pk - relations = OrderedDict( + +def _merge_relationships(forward_relations, reverse_relations): + return OrderedDict( list(forward_relations.items()) + list(reverse_relations.items()) ) - - return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations) -- cgit v1.2.3 From 8cf37449715c32c4a692667814466c7f32e8734f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 5 Jan 2015 10:52:18 +0000 Subject: Ensure no invalid min_length/min_value/max_value arguments. Closes #2369. --- rest_framework/utils/field_mapping.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index b2f4dd80..cba40d31 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -10,6 +10,11 @@ from rest_framework.validators import UniqueValidator import inspect +NUMERIC_FIELD_TYPES = ( + models.IntegerField, models.FloatField, models.DecimalField +) + + class ClassLookupDict(object): """ Takes a dictionary with classes as keys. @@ -119,7 +124,7 @@ def get_field_kwargs(field_name, model_field): validator.limit_value for validator in validator_kwarg if isinstance(validator, validators.MinLengthValidator) ), None) - if min_length is not None: + if min_length is not None and isinstance(model_field, models.CharField): kwargs['min_length'] = min_length validator_kwarg = [ validator for validator in validator_kwarg @@ -132,7 +137,7 @@ def get_field_kwargs(field_name, model_field): validator.limit_value for validator in validator_kwarg if isinstance(validator, validators.MaxValueValidator) ), None) - if max_value is not None: + if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES): kwargs['max_value'] = max_value validator_kwarg = [ validator for validator in validator_kwarg @@ -145,7 +150,7 @@ def get_field_kwargs(field_name, model_field): validator.limit_value for validator in validator_kwarg if isinstance(validator, validators.MinValueValidator) ), None) - if min_value is not None: + if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES): kwargs['min_value'] = min_value validator_kwarg = [ validator for validator in validator_kwarg -- cgit v1.2.3 From 4d287c7aef7b12086930eeb7a05cadb7e8b2cc48 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 14 Jan 2015 13:19:56 +0000 Subject: Include paragraph around view description in browable API --- rest_framework/utils/formatting.py | 1 + 1 file changed, 1 insertion(+) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 470af51b..173848df 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -59,4 +59,5 @@ def markup_description(description): description = apply_markdown(description) else: description = escape(description).replace('\n', '
') + description = '

' + description + '

' return mark_safe(description) -- cgit v1.2.3 From f13fcba9a9f41f7e00e0ea8956fcc65ca168c76c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 14 Jan 2015 13:20:02 +0000 Subject: Include paragraph around view description in browable API --- rest_framework/utils/formatting.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 173848df..8b6f005e 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -2,12 +2,10 @@ Utility functions to return a formatted name and description for a given view. """ from __future__ import unicode_literals -import re - from django.utils.html import escape from django.utils.safestring import mark_safe - from rest_framework.compat import apply_markdown, force_text +import re def remove_trailing_string(content, trailing): -- cgit v1.2.3 From 86d2774cf30351fd4174e97501532056ed0d8f95 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Jan 2015 20:30:46 +0000 Subject: Fix compat issues --- rest_framework/utils/urls.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 rest_framework/utils/urls.py (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/urls.py b/rest_framework/utils/urls.py new file mode 100644 index 00000000..880ef9ed --- /dev/null +++ b/rest_framework/utils/urls.py @@ -0,0 +1,25 @@ +from django.utils.six.moves.urllib import parse as urlparse + + +def replace_query_param(url, key, val): + """ + Given a URL and a key/val pair, set or replace an item in the query + parameters of the URL, and return the new URL. + """ + (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url) + query_dict = urlparse.parse_qs(query) + query_dict[key] = [val] + query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) + return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) + + +def remove_query_param(url, key): + """ + Given a URL and a key/val pair, remove an item in the query + parameters of the URL, and return the new URL. + """ + (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url) + query_dict = urlparse.parse_qs(query) + query_dict.pop(key, None) + query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) + return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) -- cgit v1.2.3 From 5484d570cb8214c776273b45320e7d1c73b85a34 Mon Sep 17 00:00:00 2001 From: Fabien Bochu Date: Mon, 19 Jan 2015 10:57:26 +0100 Subject: Fix timedelta JSON serialization on Python 2.6. --- rest_framework/utils/encoders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 73cbe5d8..104343a4 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -6,7 +6,7 @@ from django.db.models.query import QuerySet from django.utils import six, timezone from django.utils.encoding import force_text from django.utils.functional import Promise -from rest_framework.compat import OrderedDict +from rest_framework.compat import OrderedDict, total_seconds from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList import datetime import decimal @@ -41,7 +41,7 @@ class JSONEncoder(json.JSONEncoder): representation = representation[:12] return representation elif isinstance(obj, datetime.timedelta): - return six.text_type(obj.total_seconds()) + return six.text_type(total_seconds(obj)) elif isinstance(obj, decimal.Decimal): # Serializers will coerce decimals to strings by default. return float(obj) -- cgit v1.2.3 From da1db34a36f6f0cb8722acbbf7c3182e32995ae3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Jan 2015 14:18:02 +0000 Subject: Handle UUID objects in JSONEncoder. Closes #2433. --- rest_framework/utils/encoders.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 104343a4..bf753271 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -12,6 +12,7 @@ import datetime import decimal import types import json +import uuid class JSONEncoder(json.JSONEncoder): @@ -45,6 +46,8 @@ class JSONEncoder(json.JSONEncoder): elif isinstance(obj, decimal.Decimal): # Serializers will coerce decimals to strings by default. return float(obj) + elif isinstance(obj, uuid.UUID): + return six.text_type(obj) elif isinstance(obj, QuerySet): return tuple(obj) elif hasattr(obj, 'tolist'): -- cgit v1.2.3 From 0dffc46cb71ec868cdbb37187ba03dea7bac4e7a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Jan 2015 14:21:09 +0000 Subject: ReturnDict and ReturnList repr as standard dict/list. Closes #2421. --- rest_framework/utils/serializer_helpers.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 65a04d06..f9960603 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -16,6 +16,9 @@ class ReturnDict(OrderedDict): def copy(self): return ReturnDict(self, serializer=self.serializer) + def __repr__(self): + return dict.__repr__(self) + class ReturnList(list): """ @@ -27,6 +30,9 @@ class ReturnList(list): self.serializer = kwargs.pop('serializer') super(ReturnList, self).__init__(*args, **kwargs) + def __repr__(self): + return list.__repr__(self) + class BoundField(object): """ -- cgit v1.2.3 From da6ef3d0b0f3a8e688524bbd446d4350a74fd05a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Jan 2015 13:03:37 +0000 Subject: Allow missing fields option for inherited serializers. Closes #2388. --- rest_framework/utils/serializer_helpers.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index f9960603..ab057862 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -105,3 +105,6 @@ class BindingDict(collections.MutableMapping): def __len__(self): return len(self.fields) + + def __repr__(self): + return dict.__repr__(self.fields) -- cgit v1.2.3 From e59b3d1718de549d0e165d03aeea1488ddfe20ee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Jan 2015 14:18:13 +0000 Subject: Make ReturnDict cachable. Closes #2360. --- rest_framework/utils/serializer_helpers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index ab057862..87bb3ac0 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -19,6 +19,11 @@ class ReturnDict(OrderedDict): def __repr__(self): return dict.__repr__(self) + def __reduce__(self): + # Pickling these objects will drop the .serializer backlink, + # but preserve the raw data. + return (dict, (dict(self),)) + class ReturnList(list): """ @@ -33,6 +38,11 @@ class ReturnList(list): def __repr__(self): return list.__repr__(self) + def __reduce__(self): + # Pickling these objects will drop the .serializer backlink, + # but preserve the raw data. + return (list, (list(self),)) + class BoundField(object): """ -- cgit v1.2.3 From 25a703b42c030f712734ed56b8f1996f8d13ac0c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 11:15:11 +0000 Subject: Work around meta API differences --- rest_framework/utils/model_meta.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index 375d2e8c..6a5835f5 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -121,12 +121,17 @@ def _get_reverse_relationships(opts): """ Returns an `OrderedDict` of field names to `RelationInfo`. """ + # Note that we have a hack here to handle internal API differences for + # this internal API across Django 1.7 -> Django 1.8. + # See: https://code.djangoproject.com/ticket/24208 + reverse_relations = OrderedDict() for relation in opts.get_all_related_objects(): accessor_name = relation.get_accessor_name() + related = getattr(relation, 'related_model', relation.model) reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related=related, to_many=relation.field.rel.multiple, has_through_model=False ) @@ -134,9 +139,10 @@ def _get_reverse_relationships(opts): # Deal with reverse many-to-many relationships. for relation in opts.get_all_related_many_to_many_objects(): accessor_name = relation.get_accessor_name() + related = getattr(relation, 'related_model', relation.model) reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related=related, to_many=True, has_through_model=( (getattr(relation.field.rel, 'through', None) is not None) -- cgit v1.2.3 From 889a07f5563a0f970639a0958c0dcbc26e82919f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 15:32:21 +0000 Subject: Support assignment in ClassLookupDict --- rest_framework/utils/field_mapping.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'rest_framework/utils') diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index cba40d31..c97ec5d0 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -38,6 +38,9 @@ class ClassLookupDict(object): return self.mapping[cls] raise KeyError('Class %s not found in lookup.', cls.__name__) + def __setitem__(self, key, value): + self.mapping[key] = value + def needs_label(model_field, field_name): """ -- cgit v1.2.3