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

1import sys 

2from collections import OrderedDict 

3from urllib import parse 

4 

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 _ 

11 

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 

18 

19 

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__ 

27 

28 

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 """ 

34 

35 

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 """ 

41 

42 

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 

53 

54 def __getnewargs__(self): 

55 return (str(self), self.name) 

56 

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) 

63 

64 is_hyperlink = True 

65 

66 

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 

75 

76 def __str__(self): 

77 return "%s" % self.pk 

78 

79 

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) 

87 

88 

89class RelatedField(Field): 

90 queryset = None 

91 html_cutoff = None 

92 html_cutoff_text = None 

93 

94 def __init__(self, **kwargs): 

95 self.queryset = kwargs.pop('queryset', self.queryset) 

96 

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) 

101 

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) 

118 

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) 

125 

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. 

131 

132 Typically you won't need to override this method. 

133 

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: 

137 

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) 

148 

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) 

154 

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 

166 

167 def use_pk_only_optimization(self): 

168 return False 

169 

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() 

180 

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) 

184 

185 return PKOnlyObject(pk=value) 

186 except AttributeError: 

187 pass 

188 

189 # Standard case, return the object instance. 

190 return super().get_attribute(instance) 

191 

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 {} 

198 

199 if cutoff is not None: 

200 queryset = queryset[:cutoff] 

201 

202 return OrderedDict([ 

203 ( 

204 self.to_representation(item), 

205 self.display_value(item) 

206 ) 

207 for item in queryset 

208 ]) 

209 

210 @property 

211 def choices(self): 

212 return self.get_choices() 

213 

214 @property 

215 def grouped_choices(self): 

216 return self.choices 

217 

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 ) 

224 

225 def display_value(self, instance): 

226 return str(instance) 

227 

228 

229class StringRelatedField(RelatedField): 

230 """ 

231 A read only field that represents its targets using their 

232 plain string representation. 

233 """ 

234 

235 def __init__(self, **kwargs): 

236 kwargs['read_only'] = True 

237 super().__init__(**kwargs) 

238 

239 def to_representation(self, value): 

240 return str(value) 

241 

242 

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 } 

249 

250 def __init__(self, **kwargs): 

251 self.pk_field = kwargs.pop('pk_field', None) 

252 super().__init__(**kwargs) 

253 

254 def use_pk_only_optimization(self): 

255 return True 

256 

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__) 

269 

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 

274 

275 

276class HyperlinkedRelatedField(RelatedField): 

277 lookup_field = 'pk' 

278 view_name = None 

279 

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 } 

287 

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) 

295 

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 

300 

301 super().__init__(**kwargs) 

302 

303 def use_pk_only_optimization(self): 

304 return self.lookup_field == 'pk' 

305 

306 def get_object(self, view_name, view_args, view_kwargs): 

307 """ 

308 Return the object corresponding to a matched URL. 

309 

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() 

316 

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]) 

325 

326 def get_url(self, obj, view_name, request, format): 

327 """ 

328 Given an object, return the URL that hyperlinks to the object. 

329 

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 

336 

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) 

340 

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__) 

347 

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):] 

354 

355 data = uri_to_iri(parse.unquote(data)) 

356 

357 try: 

358 match = resolve(data) 

359 except Resolver404: 

360 self.fail('no_match') 

361 

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 

368 

369 if match.view_name != expected_viewname: 

370 self.fail('incorrect_match') 

371 

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') 

376 

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 ) 

383 

384 request = self.context['request'] 

385 format = self.context.get('format') 

386 

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 

398 

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) 

417 

418 if url is None: 

419 return None 

420 

421 return Hyperlink(url, value) 

422 

423 

424class HyperlinkedIdentityField(HyperlinkedRelatedField): 

425 """ 

426 A read-only field that represents the identity URL for an object, itself. 

427 

428 This is in contrast to `HyperlinkedRelatedField` which represents the 

429 URL of relationships to other objects. 

430 """ 

431 

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) 

437 

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 

442 

443 

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 } 

453 

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) 

458 

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') 

467 

468 def to_representation(self, obj): 

469 return getattr(obj, self.slug_field) 

470 

471 

472class ManyRelatedField(Field): 

473 """ 

474 Relationships with `many=True` transparently get coerced into instead being 

475 a ManyRelatedField with a child relationship. 

476 

477 The `ManyRelatedField` class is responsible for handling iterating through 

478 the values and passing each one to the child relationship. 

479 

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 

492 

493 def __init__(self, child_relation=None, *args, **kwargs): 

494 self.child_relation = child_relation 

495 self.allow_empty = kwargs.pop('allow_empty', True) 

496 

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) 

501 

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) 

509 

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) 

519 

520 return dictionary.get(self.field_name, empty) 

521 

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') 

527 

528 return [ 

529 self.child_relation.to_internal_value(item) 

530 for item in data 

531 ] 

532 

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 [] 

537 

538 relationship = get_attribute(instance, self.source_attrs) 

539 return relationship.all() if hasattr(relationship, 'all') else relationship 

540 

541 def to_representation(self, iterable): 

542 return [ 

543 self.child_relation.to_representation(value) 

544 for value in iterable 

545 ] 

546 

547 def get_choices(self, cutoff=None): 

548 return self.child_relation.get_choices(cutoff) 

549 

550 @property 

551 def choices(self): 

552 return self.get_choices() 

553 

554 @property 

555 def grouped_choices(self): 

556 return self.choices 

557 

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 )