Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/rest_framework/relations.py: 55%
278 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import sys
2from collections import OrderedDict
3from urllib import parse
5from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
6from django.db.models import Manager
7from django.db.models.query import QuerySet
8from django.urls import NoReverseMatch, Resolver404, get_script_prefix, resolve
9from django.utils.encoding import smart_str, uri_to_iri
10from django.utils.translation import gettext_lazy as _
12from rest_framework.fields import (
13 Field, empty, get_attribute, is_simple_callable, iter_options
14)
15from rest_framework.reverse import reverse
16from rest_framework.settings import api_settings
17from rest_framework.utils import html
20def method_overridden(method_name, klass, instance):
21 """
22 Determine if a method has been overridden.
23 """
24 method = getattr(klass, method_name)
25 default_method = getattr(method, '__func__', method) # Python 3 compat
26 return default_method is not getattr(instance, method_name).__func__
29class ObjectValueError(ValueError):
30 """
31 Raised when `queryset.get()` failed due to an underlying `ValueError`.
32 Wrapping prevents calling code conflating this with unrelated errors.
33 """
36class ObjectTypeError(TypeError):
37 """
38 Raised when `queryset.get()` failed due to an underlying `TypeError`.
39 Wrapping prevents calling code conflating this with unrelated errors.
40 """
43class Hyperlink(str):
44 """
45 A string like object that additionally has an associated name.
46 We use this for hyperlinked URLs that may render as a named link
47 in some contexts, or render as a plain URL in others.
48 """
49 def __new__(cls, url, obj):
50 ret = super().__new__(cls, url)
51 ret.obj = obj
52 return ret
54 def __getnewargs__(self):
55 return (str(self), self.name)
57 @property
58 def name(self):
59 # This ensures that we only called `__str__` lazily,
60 # as in some cases calling __str__ on a model instances *might*
61 # involve a database lookup.
62 return str(self.obj)
64 is_hyperlink = True
67class PKOnlyObject:
68 """
69 This is a mock object, used for when we only need the pk of the object
70 instance, but still want to return an object with a .pk attribute,
71 in order to keep the same interface as a regular model instance.
72 """
73 def __init__(self, pk):
74 self.pk = pk
76 def __str__(self):
77 return "%s" % self.pk
80# We assume that 'validators' are intended for the child serializer,
81# rather than the parent serializer.
82MANY_RELATION_KWARGS = (
83 'read_only', 'write_only', 'required', 'default', 'initial', 'source',
84 'label', 'help_text', 'style', 'error_messages', 'allow_empty',
85 'html_cutoff', 'html_cutoff_text'
86)
89class RelatedField(Field):
90 queryset = None
91 html_cutoff = None
92 html_cutoff_text = None
94 def __init__(self, **kwargs):
95 self.queryset = kwargs.pop('queryset', self.queryset)
97 cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF
98 if cutoff_from_settings is not None: 98 ↛ 100line 98 didn't jump to line 100, because the condition on line 98 was never false
99 cutoff_from_settings = int(cutoff_from_settings)
100 self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings)
102 self.html_cutoff_text = kwargs.pop(
103 'html_cutoff_text',
104 self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
105 )
106 if not method_overridden('get_queryset', RelatedField, self): 106 ↛ 111line 106 didn't jump to line 111, because the condition on line 106 was never false
107 assert self.queryset is not None or kwargs.get('read_only'), (
108 'Relational field must provide a `queryset` argument, '
109 'override `get_queryset`, or set read_only=`True`.'
110 )
111 assert not (self.queryset is not None and kwargs.get('read_only')), (
112 'Relational fields should not provide a `queryset` argument, '
113 'when setting read_only=`True`.'
114 )
115 kwargs.pop('many', None)
116 kwargs.pop('allow_empty', None)
117 super().__init__(**kwargs)
119 def __new__(cls, *args, **kwargs):
120 # We override this method in order to automagically create
121 # `ManyRelatedField` classes instead when `many=True` is set.
122 if kwargs.pop('many', False):
123 return cls.many_init(*args, **kwargs)
124 return super().__new__(cls, *args, **kwargs)
126 @classmethod
127 def many_init(cls, *args, **kwargs):
128 """
129 This method handles creating a parent `ManyRelatedField` instance
130 when the `many=True` keyword argument is passed.
132 Typically you won't need to override this method.
134 Note that we're over-cautious in passing most arguments to both parent
135 and child classes in order to try to cover the general case. If you're
136 overriding this method you'll probably want something much simpler, eg:
138 @classmethod
139 def many_init(cls, *args, **kwargs):
140 kwargs['child'] = cls()
141 return CustomManyRelatedField(*args, **kwargs)
142 """
143 list_kwargs = {'child_relation': cls(*args, **kwargs)}
144 for key in kwargs:
145 if key in MANY_RELATION_KWARGS:
146 list_kwargs[key] = kwargs[key]
147 return ManyRelatedField(**list_kwargs)
149 def run_validation(self, data=empty):
150 # We force empty strings to None values for relational fields.
151 if data == '': 151 ↛ 152line 151 didn't jump to line 152, because the condition on line 151 was never true
152 data = None
153 return super().run_validation(data)
155 def get_queryset(self):
156 queryset = self.queryset
157 if isinstance(queryset, (QuerySet, Manager)): 157 ↛ 165line 157 didn't jump to line 165, because the condition on line 157 was never false
158 # Ensure queryset is re-evaluated whenever used.
159 # Note that actually a `Manager` class may also be used as the
160 # queryset argument. This occurs on ModelSerializer fields,
161 # as it allows us to generate a more expressive 'repr' output
162 # for the field.
163 # Eg: 'MyRelationship(queryset=ExampleModel.objects.all())'
164 queryset = queryset.all()
165 return queryset
167 def use_pk_only_optimization(self):
168 return False
170 def get_attribute(self, instance):
171 if self.use_pk_only_optimization() and self.source_attrs: 171 ↛ 190line 171 didn't jump to line 190, because the condition on line 171 was never false
172 # Optimized case, return a mock object only containing the pk attribute.
173 try:
174 attribute_instance = get_attribute(instance, self.source_attrs[:-1])
175 value = attribute_instance.serializable_value(self.source_attrs[-1])
176 if is_simple_callable(value): 176 ↛ 179line 176 didn't jump to line 179, because the condition on line 176 was never true
177 # Handle edge case where the relationship `source` argument
178 # points to a `get_relationship()` method on the model.
179 value = value()
181 # Handle edge case where relationship `source` argument points
182 # to an instance instead of a pk (e.g., a `@property`).
183 value = getattr(value, 'pk', value)
185 return PKOnlyObject(pk=value)
186 except AttributeError:
187 pass
189 # Standard case, return the object instance.
190 return super().get_attribute(instance)
192 def get_choices(self, cutoff=None):
193 queryset = self.get_queryset()
194 if queryset is None:
195 # Ensure that field.choices returns something sensible
196 # even when accessed with a read-only field.
197 return {}
199 if cutoff is not None:
200 queryset = queryset[:cutoff]
202 return OrderedDict([
203 (
204 self.to_representation(item),
205 self.display_value(item)
206 )
207 for item in queryset
208 ])
210 @property
211 def choices(self):
212 return self.get_choices()
214 @property
215 def grouped_choices(self):
216 return self.choices
218 def iter_options(self):
219 return iter_options(
220 self.get_choices(cutoff=self.html_cutoff),
221 cutoff=self.html_cutoff,
222 cutoff_text=self.html_cutoff_text
223 )
225 def display_value(self, instance):
226 return str(instance)
229class StringRelatedField(RelatedField):
230 """
231 A read only field that represents its targets using their
232 plain string representation.
233 """
235 def __init__(self, **kwargs):
236 kwargs['read_only'] = True
237 super().__init__(**kwargs)
239 def to_representation(self, value):
240 return str(value)
243class PrimaryKeyRelatedField(RelatedField):
244 default_error_messages = {
245 'required': _('This field is required.'),
246 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
247 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
248 }
250 def __init__(self, **kwargs):
251 self.pk_field = kwargs.pop('pk_field', None)
252 super().__init__(**kwargs)
254 def use_pk_only_optimization(self):
255 return True
257 def to_internal_value(self, data):
258 if self.pk_field is not None: 258 ↛ 259line 258 didn't jump to line 259, because the condition on line 258 was never true
259 data = self.pk_field.to_internal_value(data)
260 queryset = self.get_queryset()
261 try:
262 if isinstance(data, bool): 262 ↛ 263line 262 didn't jump to line 263, because the condition on line 262 was never true
263 raise TypeError
264 return queryset.get(pk=data)
265 except ObjectDoesNotExist:
266 self.fail('does_not_exist', pk_value=data)
267 except (TypeError, ValueError):
268 self.fail('incorrect_type', data_type=type(data).__name__)
270 def to_representation(self, value):
271 if self.pk_field is not None: 271 ↛ 272line 271 didn't jump to line 272, because the condition on line 271 was never true
272 return self.pk_field.to_representation(value.pk)
273 return value.pk
276class HyperlinkedRelatedField(RelatedField):
277 lookup_field = 'pk'
278 view_name = None
280 default_error_messages = {
281 'required': _('This field is required.'),
282 'no_match': _('Invalid hyperlink - No URL match.'),
283 'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'),
284 'does_not_exist': _('Invalid hyperlink - Object does not exist.'),
285 'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'),
286 }
288 def __init__(self, view_name=None, **kwargs):
289 if view_name is not None:
290 self.view_name = view_name
291 assert self.view_name is not None, 'The `view_name` argument is required.'
292 self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
293 self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
294 self.format = kwargs.pop('format', None)
296 # We include this simply for dependency injection in tests.
297 # We can't add it as a class attributes or it would expect an
298 # implicit `self` argument to be passed.
299 self.reverse = reverse
301 super().__init__(**kwargs)
303 def use_pk_only_optimization(self):
304 return self.lookup_field == 'pk'
306 def get_object(self, view_name, view_args, view_kwargs):
307 """
308 Return the object corresponding to a matched URL.
310 Takes the matched URL conf arguments, and should return an
311 object instance, or raise an `ObjectDoesNotExist` exception.
312 """
313 lookup_value = view_kwargs[self.lookup_url_kwarg]
314 lookup_kwargs = {self.lookup_field: lookup_value}
315 queryset = self.get_queryset()
317 try:
318 return queryset.get(**lookup_kwargs)
319 except ValueError:
320 exc = ObjectValueError(str(sys.exc_info()[1]))
321 raise exc.with_traceback(sys.exc_info()[2])
322 except TypeError:
323 exc = ObjectTypeError(str(sys.exc_info()[1]))
324 raise exc.with_traceback(sys.exc_info()[2])
326 def get_url(self, obj, view_name, request, format):
327 """
328 Given an object, return the URL that hyperlinks to the object.
330 May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
331 attributes are not configured to correctly match the URL conf.
332 """
333 # Unsaved objects will not yet have a valid URL.
334 if hasattr(obj, 'pk') and obj.pk in (None, ''):
335 return None
337 lookup_value = getattr(obj, self.lookup_field)
338 kwargs = {self.lookup_url_kwarg: lookup_value}
339 return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
341 def to_internal_value(self, data):
342 request = self.context.get('request')
343 try:
344 http_prefix = data.startswith(('http:', 'https:'))
345 except AttributeError:
346 self.fail('incorrect_type', data_type=type(data).__name__)
348 if http_prefix:
349 # If needed convert absolute URLs to relative path
350 data = parse.urlparse(data).path
351 prefix = get_script_prefix()
352 if data.startswith(prefix):
353 data = '/' + data[len(prefix):]
355 data = uri_to_iri(parse.unquote(data))
357 try:
358 match = resolve(data)
359 except Resolver404:
360 self.fail('no_match')
362 try:
363 expected_viewname = request.versioning_scheme.get_versioned_viewname(
364 self.view_name, request
365 )
366 except AttributeError:
367 expected_viewname = self.view_name
369 if match.view_name != expected_viewname:
370 self.fail('incorrect_match')
372 try:
373 return self.get_object(match.view_name, match.args, match.kwargs)
374 except (ObjectDoesNotExist, ObjectValueError, ObjectTypeError):
375 self.fail('does_not_exist')
377 def to_representation(self, value):
378 assert 'request' in self.context, (
379 "`%s` requires the request in the serializer"
380 " context. Add `context={'request': request}` when instantiating "
381 "the serializer." % self.__class__.__name__
382 )
384 request = self.context['request']
385 format = self.context.get('format')
387 # By default use whatever format is given for the current context
388 # unless the target is a different type to the source.
389 #
390 # Eg. Consider a HyperlinkedIdentityField pointing from a json
391 # representation to an html property of that representation...
392 #
393 # '/snippets/1/' should link to '/snippets/1/highlight/'
394 # ...but...
395 # '/snippets/1/.json' should link to '/snippets/1/highlight/.html'
396 if format and self.format and self.format != format:
397 format = self.format
399 # Return the hyperlink, or error if incorrectly configured.
400 try:
401 url = self.get_url(value, self.view_name, request, format)
402 except NoReverseMatch:
403 msg = (
404 'Could not resolve URL for hyperlinked relationship using '
405 'view name "%s". You may have failed to include the related '
406 'model in your API, or incorrectly configured the '
407 '`lookup_field` attribute on this field.'
408 )
409 if value in ('', None):
410 value_string = {'': 'the empty string', None: 'None'}[value]
411 msg += (
412 " WARNING: The value of the field on the model instance "
413 "was %s, which may be why it didn't match any "
414 "entries in your URL conf." % value_string
415 )
416 raise ImproperlyConfigured(msg % self.view_name)
418 if url is None:
419 return None
421 return Hyperlink(url, value)
424class HyperlinkedIdentityField(HyperlinkedRelatedField):
425 """
426 A read-only field that represents the identity URL for an object, itself.
428 This is in contrast to `HyperlinkedRelatedField` which represents the
429 URL of relationships to other objects.
430 """
432 def __init__(self, view_name=None, **kwargs):
433 assert view_name is not None, 'The `view_name` argument is required.'
434 kwargs['read_only'] = True
435 kwargs['source'] = '*'
436 super().__init__(view_name, **kwargs)
438 def use_pk_only_optimization(self):
439 # We have the complete object instance already. We don't need
440 # to run the 'only get the pk for this relationship' code.
441 return False
444class SlugRelatedField(RelatedField):
445 """
446 A read-write field that represents the target of the relationship
447 by a unique 'slug' attribute.
448 """
449 default_error_messages = {
450 'does_not_exist': _('Object with {slug_name}={value} does not exist.'),
451 'invalid': _('Invalid value.'),
452 }
454 def __init__(self, slug_field=None, **kwargs):
455 assert slug_field is not None, 'The `slug_field` argument is required.'
456 self.slug_field = slug_field
457 super().__init__(**kwargs)
459 def to_internal_value(self, data):
460 queryset = self.get_queryset()
461 try:
462 return queryset.get(**{self.slug_field: data})
463 except ObjectDoesNotExist:
464 self.fail('does_not_exist', slug_name=self.slug_field, value=smart_str(data))
465 except (TypeError, ValueError):
466 self.fail('invalid')
468 def to_representation(self, obj):
469 return getattr(obj, self.slug_field)
472class ManyRelatedField(Field):
473 """
474 Relationships with `many=True` transparently get coerced into instead being
475 a ManyRelatedField with a child relationship.
477 The `ManyRelatedField` class is responsible for handling iterating through
478 the values and passing each one to the child relationship.
480 This class is treated as private API.
481 You shouldn't generally need to be using this class directly yourself,
482 and should instead simply set 'many=True' on the relationship.
483 """
484 initial = []
485 default_empty_html = []
486 default_error_messages = {
487 'not_a_list': _('Expected a list of items but got type "{input_type}".'),
488 'empty': _('This list may not be empty.')
489 }
490 html_cutoff = None
491 html_cutoff_text = None
493 def __init__(self, child_relation=None, *args, **kwargs):
494 self.child_relation = child_relation
495 self.allow_empty = kwargs.pop('allow_empty', True)
497 cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF
498 if cutoff_from_settings is not None: 498 ↛ 500line 498 didn't jump to line 500, because the condition on line 498 was never false
499 cutoff_from_settings = int(cutoff_from_settings)
500 self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings)
502 self.html_cutoff_text = kwargs.pop(
503 'html_cutoff_text',
504 self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT)
505 )
506 assert child_relation is not None, '`child_relation` is a required argument.'
507 super().__init__(*args, **kwargs)
508 self.child_relation.bind(field_name='', parent=self)
510 def get_value(self, dictionary):
511 # We override the default field access in order to support
512 # lists in HTML forms.
513 if html.is_html_input(dictionary): 513 ↛ 515line 513 didn't jump to line 515, because the condition on line 513 was never true
514 # Don't return [] if the update is partial
515 if self.field_name not in dictionary:
516 if getattr(self.root, 'partial', False):
517 return empty
518 return dictionary.getlist(self.field_name)
520 return dictionary.get(self.field_name, empty)
522 def to_internal_value(self, data):
523 if isinstance(data, str) or not hasattr(data, '__iter__'): 523 ↛ 524line 523 didn't jump to line 524, because the condition on line 523 was never true
524 self.fail('not_a_list', input_type=type(data).__name__)
525 if not self.allow_empty and len(data) == 0: 525 ↛ 526line 525 didn't jump to line 526, because the condition on line 525 was never true
526 self.fail('empty')
528 return [
529 self.child_relation.to_internal_value(item)
530 for item in data
531 ]
533 def get_attribute(self, instance):
534 # Can't have any relationships if not created
535 if hasattr(instance, 'pk') and instance.pk is None: 535 ↛ 536line 535 didn't jump to line 536, because the condition on line 535 was never true
536 return []
538 relationship = get_attribute(instance, self.source_attrs)
539 return relationship.all() if hasattr(relationship, 'all') else relationship
541 def to_representation(self, iterable):
542 return [
543 self.child_relation.to_representation(value)
544 for value in iterable
545 ]
547 def get_choices(self, cutoff=None):
548 return self.child_relation.get_choices(cutoff)
550 @property
551 def choices(self):
552 return self.get_choices()
554 @property
555 def grouped_choices(self):
556 return self.choices
558 def iter_options(self):
559 return iter_options(
560 self.get_choices(cutoff=self.html_cutoff),
561 cutoff=self.html_cutoff,
562 cutoff_text=self.html_cutoff_text
563 )