blob: 5d77410f0478894e671455064b9efa43a002f9de [file] [log] [blame]
showardf46ad4c2010-02-03 20:28:59 +00001import cgi, datetime, re, time, urllib
showardf828c772010-01-25 21:49:42 +00002from django import http
showardf46ad4c2010-02-03 20:28:59 +00003import django.core.exceptions
4from django.core import urlresolvers
jamesren3981f442010-02-16 19:27:59 +00005from django.utils import datastructures
6import simplejson
showardf828c772010-01-25 21:49:42 +00007from autotest_lib.frontend.shared import exceptions, query_lib
8from autotest_lib.frontend.afe import model_logic
9
10
11_JSON_CONTENT_TYPE = 'application/json'
12
13
14def _resolve_class_path(class_path):
15 module_path, class_name = class_path.rsplit('.', 1)
16 module = __import__(module_path, {}, {}, [''])
17 return getattr(module, class_name)
18
19
20_NO_VALUE_SPECIFIED = object()
21
22class _InputDict(dict):
23 def get(self, key, default=_NO_VALUE_SPECIFIED):
24 return super(_InputDict, self).get(key, default)
25
26
27 @classmethod
28 def remove_unspecified_fields(cls, field_dict):
29 return dict((key, value) for key, value in field_dict.iteritems()
30 if value is not _NO_VALUE_SPECIFIED)
31
32
33class Resource(object):
34 _permitted_methods = None # subclasses must override this
35
36
showardf46ad4c2010-02-03 20:28:59 +000037 def __init__(self, request):
showardf828c772010-01-25 21:49:42 +000038 assert self._permitted_methods
jamesren3981f442010-02-16 19:27:59 +000039 # this request should be used for global environment info, like
40 # constructing absolute URIs. it should not be used for query
41 # parameters, because the request may not have been for this particular
42 # resource.
showardf46ad4c2010-02-03 20:28:59 +000043 self._request = request
jamesren3981f442010-02-16 19:27:59 +000044 # this dict will contain the applicable query parameters
45 self._query_params = datastructures.MultiValueDict()
showardf828c772010-01-25 21:49:42 +000046
47
48 @classmethod
49 def dispatch_request(cls, request, *args, **kwargs):
50 # handle a request directly
51 try:
jamesren3981f442010-02-16 19:27:59 +000052 try:
53 instance = cls.from_uri_args(request, **kwargs)
54 except django.core.exceptions.ObjectDoesNotExist, exc:
55 raise http.Http404(exc)
56
57 instance.read_query_parameters(request.GET)
58 return instance.handle_request()
59 except exceptions.RequestError, exc:
60 return exc.response
showardf828c772010-01-25 21:49:42 +000061
62
showardf46ad4c2010-02-03 20:28:59 +000063 def handle_request(self):
64 if self._request.method.upper() not in self._permitted_methods:
showardf828c772010-01-25 21:49:42 +000065 return http.HttpResponseNotAllowed(self._permitted_methods)
66
showardf46ad4c2010-02-03 20:28:59 +000067 handler = getattr(self, self._request.method.lower())
jamesren3981f442010-02-16 19:27:59 +000068 return handler()
showardf828c772010-01-25 21:49:42 +000069
70
71 # the handler methods below only need to be overridden if the resource
72 # supports the method
73
showardf46ad4c2010-02-03 20:28:59 +000074 def get(self):
showardf828c772010-01-25 21:49:42 +000075 """Handle a GET request.
76
77 @returns an HttpResponse
78 """
79 raise NotImplementedError
80
81
showardf46ad4c2010-02-03 20:28:59 +000082 def post(self):
showardf828c772010-01-25 21:49:42 +000083 """Handle a POST request.
84
85 @returns an HttpResponse
86 """
87 raise NotImplementedError
88
89
showardf46ad4c2010-02-03 20:28:59 +000090 def put(self):
showardf828c772010-01-25 21:49:42 +000091 """Handle a PUT request.
92
93 @returns an HttpResponse
94 """
95 raise NotImplementedError
96
97
showardf46ad4c2010-02-03 20:28:59 +000098 def delete(self):
showardf828c772010-01-25 21:49:42 +000099 """Handle a DELETE request.
100
101 @returns an HttpResponse
102 """
103 raise NotImplementedError
104
105
106 @classmethod
jamesren3981f442010-02-16 19:27:59 +0000107 def from_uri_args(cls, request, **kwargs):
showardf828c772010-01-25 21:49:42 +0000108 """Construct an instance from URI args.
109
110 Default implementation for resources with no URI args.
111 """
showardf46ad4c2010-02-03 20:28:59 +0000112 return cls(request)
showardf828c772010-01-25 21:49:42 +0000113
114
115 def _uri_args(self):
jamesren3981f442010-02-16 19:27:59 +0000116 """Return kwargs for a URI reference to this resource.
showardf828c772010-01-25 21:49:42 +0000117
118 Default implementation for resources with no URI args.
119 """
jamesren3981f442010-02-16 19:27:59 +0000120 return {}
showardf828c772010-01-25 21:49:42 +0000121
122
jamesren3981f442010-02-16 19:27:59 +0000123 def _query_parameters_accepted(self):
showardf828c772010-01-25 21:49:42 +0000124 """Return sequence of tuples (name, description) for query parameters.
125
126 Documents the available query parameters for GETting this resource.
127 Default implementation for resources with no parameters.
128 """
129 return ()
130
131
jamesren3981f442010-02-16 19:27:59 +0000132 def read_query_parameters(self, parameters):
133 """Read relevant query parameters from a Django MultiValueDict."""
134 for param_name, _ in self._query_parameters_accepted():
135 if param_name in parameters:
136 self._query_params.setlist(param_name,
137 parameters.getlist(param_name))
138
139
140 def set_query_parameters(self, **parameters):
141 """Set query parameters programmatically."""
142 self._query_params.update(parameters)
143
144
145 def href(self, query_params=None):
showardf828c772010-01-25 21:49:42 +0000146 """Return URI to this resource."""
jamesren3981f442010-02-16 19:27:59 +0000147 kwargs = self._uri_args()
148 path = urlresolvers.reverse(self.dispatch_request, kwargs=kwargs)
149 full_query_params = datastructures.MultiValueDict(self._query_params)
150 if query_params:
151 full_query_params.update(query_params)
152 if full_query_params:
153 path += '?' + urllib.urlencode(full_query_params.lists(),
154 doseq=True)
showardf46ad4c2010-02-03 20:28:59 +0000155 return self._request.build_absolute_uri(path)
showardf828c772010-01-25 21:49:42 +0000156
157
showardf46ad4c2010-02-03 20:28:59 +0000158 def resolve_uri(self, uri):
159 # check for absolute URIs
160 match = re.match(r'(?P<root>https?://[^/]+)(?P<path>/.*)', uri)
161 if match:
162 # is this URI for a different host?
163 my_root = self._request.build_absolute_uri('/')
164 request_root = match.group('root') + '/'
165 if my_root != request_root:
166 # might support this in the future, but not now
167 raise exceptions.BadRequest('Unable to resolve remote URI %s'
168 % uri)
169 uri = match.group('path')
170
jamesren3981f442010-02-16 19:27:59 +0000171 try:
172 view_method, args, kwargs = urlresolvers.resolve(uri)
173 except http.Http404:
174 raise exceptions.BadRequest('Unable to resolve URI %s' % uri)
showardf828c772010-01-25 21:49:42 +0000175 resource_class = view_method.im_self # class owning this classmethod
jamesren3981f442010-02-16 19:27:59 +0000176 return resource_class.from_uri_args(self._request, **kwargs)
showardf828c772010-01-25 21:49:42 +0000177
178
showardf46ad4c2010-02-03 20:28:59 +0000179 def resolve_link(self, link):
showardf828c772010-01-25 21:49:42 +0000180 if isinstance(link, dict):
181 uri = link['href']
182 elif isinstance(link, basestring):
183 uri = link
184 else:
185 raise exceptions.BadRequest('Unable to understand link %s' % link)
showardf46ad4c2010-02-03 20:28:59 +0000186 return self.resolve_uri(uri)
showardf828c772010-01-25 21:49:42 +0000187
188
jamesren3981f442010-02-16 19:27:59 +0000189 def link(self, query_params=None):
190 return {'href': self.href(query_params=query_params)}
showardf828c772010-01-25 21:49:42 +0000191
192
193 def _query_parameters_response(self):
194 return dict((name, description)
jamesren3981f442010-02-16 19:27:59 +0000195 for name, description in self._query_parameters_accepted())
showardf828c772010-01-25 21:49:42 +0000196
197
198 def _basic_response(self, content):
199 """Construct and return a simple 200 response."""
200 assert isinstance(content, dict)
201 query_parameters = self._query_parameters_response()
202 if query_parameters:
203 content['query_parameters'] = query_parameters
204 encoded_content = simplejson.dumps(content)
205 return http.HttpResponse(encoded_content,
206 content_type=_JSON_CONTENT_TYPE)
207
208
showardf46ad4c2010-02-03 20:28:59 +0000209 def _decoded_input(self):
210 content_type = self._request.META.get('CONTENT_TYPE',
211 _JSON_CONTENT_TYPE)
212 raw_data = self._request.raw_post_data
showardf828c772010-01-25 21:49:42 +0000213 if content_type == _JSON_CONTENT_TYPE:
214 try:
215 raw_dict = simplejson.loads(raw_data)
216 except ValueError, exc:
217 raise exceptions.BadRequest('Error decoding request body: '
218 '%s\n%r' % (exc, raw_data))
219 elif content_type == 'application/x-www-form-urlencoded':
220 cgi_dict = cgi.parse_qs(raw_data) # django won't do this for PUT
221 raw_dict = {}
222 for key, values in cgi_dict.items():
223 value = values[-1] # take last value if multiple were given
224 try:
225 # attempt to parse numbers, booleans and nulls
226 raw_dict[key] = simplejson.loads(value)
227 except ValueError:
228 # otherwise, leave it as a string
229 raw_dict[key] = value
230 else:
231 raise exceptions.RequestError(415, 'Unsupported media type: %s'
232 % content_type)
233
234 return _InputDict(raw_dict)
235
236
237 def _format_datetime(self, date_time):
238 """Return ISO 8601 string for the given datetime"""
239 if date_time is None:
240 return None
241 timezone_hrs = time.timezone / 60 / 60 # convert seconds to hours
242 if timezone_hrs >= 0:
243 timezone_join = '+'
244 else:
245 timezone_join = '' # minus sign comes from number itself
246 timezone_spec = '%s%s:00' % (timezone_join, timezone_hrs)
247 return date_time.strftime('%Y-%m-%dT%H:%M:%S') + timezone_spec
248
249
250 @classmethod
251 def _check_for_required_fields(cls, input_dict, fields):
252 assert isinstance(fields, (list, tuple)), fields
253 missing_fields = ', '.join(field for field in fields
254 if field not in input_dict)
255 if missing_fields:
256 raise exceptions.BadRequest('Missing input: ' + missing_fields)
257
258
259class Entry(Resource):
showardf828c772010-01-25 21:49:42 +0000260 @classmethod
jamesren3981f442010-02-16 19:27:59 +0000261 def add_query_selectors(cls, query_processor):
262 """Sbuclasses may override this to support querying."""
263 pass
showardf828c772010-01-25 21:49:42 +0000264
265
266 def short_representation(self):
267 return self.link()
268
269
270 def full_representation(self):
271 return self.short_representation()
272
273
showardf46ad4c2010-02-03 20:28:59 +0000274 def get(self):
showardf828c772010-01-25 21:49:42 +0000275 return self._basic_response(self.full_representation())
276
277
showardf46ad4c2010-02-03 20:28:59 +0000278 def put(self):
showardf828c772010-01-25 21:49:42 +0000279 try:
showardf46ad4c2010-02-03 20:28:59 +0000280 self.update(self._decoded_input())
showardf828c772010-01-25 21:49:42 +0000281 except model_logic.ValidationError, exc:
282 raise exceptions.BadRequest('Invalid input: %s' % exc)
283 return self._basic_response(self.full_representation())
284
285
jamesren3981f442010-02-16 19:27:59 +0000286 def _delete_entry(self):
287 raise NotImplementedError
288
289
showardf46ad4c2010-02-03 20:28:59 +0000290 def delete(self):
jamesren3981f442010-02-16 19:27:59 +0000291 self._delete_entry()
showardf828c772010-01-25 21:49:42 +0000292 return http.HttpResponse(status=204) # No content
293
294
295 def create_instance(self, input_dict, containing_collection):
296 raise NotImplementedError
297
298
299 def update(self, input_dict):
300 raise NotImplementedError
301
302
jamesren3981f442010-02-16 19:27:59 +0000303class InstanceEntry(Entry):
304 class NullEntry(object):
305 def link(self):
306 return None
307
308
309 def short_representation(self):
310 return None
311
312
313 _null_entry = NullEntry()
314 _permitted_methods = ('GET', 'PUT', 'DELETE')
315 model = None # subclasses must override this with a Django model class
316
317
318 def __init__(self, request, instance):
319 assert self.model is not None
320 super(Entry, self).__init__(request)
321 self.instance = instance
322
323
324 @classmethod
325 def from_optional_instance(cls, request, instance):
326 if instance is None:
327 return cls._null_entry
328 return cls(request, instance)
329
330
331 def _delete_entry(self):
332 self.instance.delete()
333
334
showardf828c772010-01-25 21:49:42 +0000335class Collection(Resource):
336 _DEFAULT_ITEMS_PER_PAGE = 50
337
338 _permitted_methods=('GET', 'POST')
339
340 # subclasses must override these
341 queryset = None # or override _fresh_queryset() directly
342 entry_class = None
343
344
showardf46ad4c2010-02-03 20:28:59 +0000345 def __init__(self, request):
346 super(Collection, self).__init__(request)
showardf828c772010-01-25 21:49:42 +0000347 assert self.entry_class is not None
348 if isinstance(self.entry_class, basestring):
349 type(self).entry_class = _resolve_class_path(self.entry_class)
350
jamesren3981f442010-02-16 19:27:59 +0000351 self._query_processor = query_lib.QueryProcessor()
352 self.entry_class.add_query_selectors(self._query_processor)
353
354
355 def _query_parameters_accepted(self):
356 params = [('start_index', 'Index of first member to include'),
357 ('items_per_page', 'Number of members to include')]
358 for selector in self._query_processor.selectors():
359 params.append((selector.name, selector.doc))
360 return params
showardf828c772010-01-25 21:49:42 +0000361
362
363 def _fresh_queryset(self):
364 assert self.queryset is not None
365 # always copy the queryset before using it to avoid caching
366 return self.queryset.all()
367
368
jamesren3981f442010-02-16 19:27:59 +0000369 def _entry_from_instance(self, instance):
370 return self.entry_class(self._request, instance)
371
372
showardf828c772010-01-25 21:49:42 +0000373 def _representation(self, entry_instances):
374 members = []
375 for instance in entry_instances:
jamesren3981f442010-02-16 19:27:59 +0000376 entry = self._entry_from_instance(instance)
showardf828c772010-01-25 21:49:42 +0000377 members.append(entry.short_representation())
378
379 rep = self.link()
380 rep.update({'members': members})
381 return rep
382
383
showardf46ad4c2010-02-03 20:28:59 +0000384 def _read_int_parameter(self, name, default):
jamesren3981f442010-02-16 19:27:59 +0000385 if name not in self._query_params:
showardf828c772010-01-25 21:49:42 +0000386 return default
jamesren3981f442010-02-16 19:27:59 +0000387 input_value = self._query_params[name]
showardf828c772010-01-25 21:49:42 +0000388 try:
389 return int(input_value)
390 except ValueError:
391 raise exceptions.BadRequest('Invalid non-numeric value for %s: %r'
392 % (name, input_value))
393
394
showardf46ad4c2010-02-03 20:28:59 +0000395 def _apply_form_query(self, queryset):
showardf828c772010-01-25 21:49:42 +0000396 """Apply any query selectors passed as form variables."""
jamesren3981f442010-02-16 19:27:59 +0000397 for parameter, values in self._query_params.lists():
showardf828c772010-01-25 21:49:42 +0000398 if not self._query_processor.has_selector(parameter):
399 continue
400 for value in values: # forms keys can have multiple values
401 queryset = self._query_processor.apply_selector(queryset,
402 parameter,
403 value)
404 return queryset
405
406
showardf46ad4c2010-02-03 20:28:59 +0000407 def _filtered_queryset(self):
408 return self._apply_form_query(self._fresh_queryset())
showardf828c772010-01-25 21:49:42 +0000409
410
showardf46ad4c2010-02-03 20:28:59 +0000411 def get(self):
412 queryset = self._filtered_queryset()
showardf828c772010-01-25 21:49:42 +0000413
showardf46ad4c2010-02-03 20:28:59 +0000414 items_per_page = self._read_int_parameter('items_per_page',
showardf828c772010-01-25 21:49:42 +0000415 self._DEFAULT_ITEMS_PER_PAGE)
showardf46ad4c2010-02-03 20:28:59 +0000416 start_index = self._read_int_parameter('start_index', 0)
showardf828c772010-01-25 21:49:42 +0000417 page = queryset[start_index:(start_index + items_per_page)]
418
419 rep = self._representation(page)
showardf828c772010-01-25 21:49:42 +0000420 rep.update({'total_results': len(queryset),
421 'start_index': start_index,
jamesren3981f442010-02-16 19:27:59 +0000422 'items_per_page': items_per_page})
showardf828c772010-01-25 21:49:42 +0000423 return self._basic_response(rep)
424
425
426 def full_representation(self):
427 # careful, this rep can be huge for large collections
428 return self._representation(self._fresh_queryset())
429
430
showardf46ad4c2010-02-03 20:28:59 +0000431 def post(self):
432 input_dict = self._decoded_input()
showardf828c772010-01-25 21:49:42 +0000433 try:
434 instance = self.entry_class.create_instance(input_dict, self)
jamesren3981f442010-02-16 19:27:59 +0000435 entry = self._entry_from_instance(instance)
showardf828c772010-01-25 21:49:42 +0000436 entry.update(input_dict)
437 except model_logic.ValidationError, exc:
438 raise exceptions.BadRequest('Invalid input: %s' % exc)
439 # RFC 2616 specifies that we provide the new URI in both the Location
440 # header and the body
441 response = http.HttpResponse(status=201, # Created
442 content=entry.href())
443 response['Location'] = entry.href()
444 return response
445
446
jamesren3981f442010-02-16 19:27:59 +0000447class Relationship(Entry):
448 _permitted_methods = ('GET', 'DELETE')
showardf828c772010-01-25 21:49:42 +0000449
jamesren3981f442010-02-16 19:27:59 +0000450 # subclasses must override this with a dict mapping name to entry class
451 related_classes = None
showardf828c772010-01-25 21:49:42 +0000452
453
jamesren3981f442010-02-16 19:27:59 +0000454 def __init__(self, **kwargs):
455 assert len(self.related_classes) == 2
456 self.entries = dict((name, kwargs[name])
457 for name in self.related_classes)
458 for name in self.related_classes: # sanity check
459 assert isinstance(self.entries[name], self.related_classes[name])
showardf828c772010-01-25 21:49:42 +0000460
jamesren3981f442010-02-16 19:27:59 +0000461 # just grab the request from one of the entries
462 some_entry = self.entries.itervalues().next()
463 super(Relationship, self).__init__(some_entry._request)
showardf828c772010-01-25 21:49:42 +0000464
465
466 @classmethod
jamesren3981f442010-02-16 19:27:59 +0000467 def from_uri_args(cls, request, **kwargs):
468 # kwargs contains URI args for each entry
469 entries = {}
470 for name, entry_class in cls.related_classes.iteritems():
471 entries[name] = entry_class.from_uri_args(request, **kwargs)
472 return cls(**entries)
showardf828c772010-01-25 21:49:42 +0000473
474
475 def _uri_args(self):
jamesren3981f442010-02-16 19:27:59 +0000476 kwargs = {}
477 for name, entry in self.entries.iteritems():
478 kwargs.update(entry._uri_args())
479 return kwargs
480
481
482 def short_representation(self):
483 rep = self.link()
484 for name, entry in self.entries.iteritems():
485 rep[name] = entry.short_representation()
486 return rep
showardf828c772010-01-25 21:49:42 +0000487
488
489 @classmethod
jamesren3981f442010-02-16 19:27:59 +0000490 def _get_related_manager(cls, instance):
491 """Get the related objects manager for the given instance.
showardf828c772010-01-25 21:49:42 +0000492
jamesren3981f442010-02-16 19:27:59 +0000493 The instance must be one of the related classes. This method will
494 return the related manager from that instance to instances of the other
495 related class.
showardf828c772010-01-25 21:49:42 +0000496 """
jamesren3981f442010-02-16 19:27:59 +0000497 this_model = type(instance)
498 models = [entry_class.model for entry_class
499 in cls.related_classes.values()]
500 if isinstance(instance, models[0]):
501 this_model, other_model = models
502 else:
503 other_model, this_model = models
showardf828c772010-01-25 21:49:42 +0000504
jamesren3981f442010-02-16 19:27:59 +0000505 _, field = this_model.objects.determine_relationship(other_model)
506 this_models_fields = (this_model._meta.fields
507 + this_model._meta.many_to_many)
508 if field in this_models_fields:
509 manager_name = field.attname
510 else:
511 # related manager is on other_model, get name of reverse related
512 # manager on this_model
513 manager_name = field.related.get_accessor_name()
514
515 return getattr(instance, manager_name)
showardf828c772010-01-25 21:49:42 +0000516
517
jamesren3981f442010-02-16 19:27:59 +0000518 def _delete_entry(self):
519 # choose order arbitrarily
520 entry, other_entry = self.entries.itervalues()
521 related_manager = self._get_related_manager(entry.instance)
522 related_manager.remove(other_entry.instance)
showardf828c772010-01-25 21:49:42 +0000523
524
jamesren3981f442010-02-16 19:27:59 +0000525 @classmethod
526 def create_instance(cls, input_dict, containing_collection):
527 other_name = containing_collection.unfixed_name
528 cls._check_for_required_fields(input_dict, (other_name,))
529 entry = containing_collection.fixed_entry
530 other_entry = containing_collection.resolve_link(input_dict[other_name])
531 related_manager = cls._get_related_manager(entry.instance)
532 related_manager.add(other_entry.instance)
533 return other_entry.instance
showardf828c772010-01-25 21:49:42 +0000534
535
jamesren3981f442010-02-16 19:27:59 +0000536 def update(self, input_dict):
537 pass
538
539
540class RelationshipCollection(Collection):
541 def __init__(self, request=None, fixed_entry=None):
542 if request is None:
543 request = fixed_entry._request
544 super(RelationshipCollection, self).__init__(request)
545
546 assert issubclass(self.entry_class, Relationship)
547 self.related_classes = self.entry_class.related_classes
548 self.fixed_name = None
549 self.fixed_entry = None
550 self.unfixed_name = None
551 self.related_manager = None
552
553 if fixed_entry is not None:
554 self._set_fixed_entry(fixed_entry)
555 entry_uri_arg = self.fixed_entry._uri_args().values()[0]
556 self._query_params[self.fixed_name] = entry_uri_arg
557
558
559 def _set_fixed_entry(self, entry):
560 """Set the fixed entry for this collection.
561
562 The entry must be an instance of one of the related entry classes. This
563 method must be called before a relationship is used. It gets called
564 either from the constructor (when collections are instantiated from
565 other resource handling code) or from read_query_parameters() (when a
566 request is made directly for the collection.
567 """
568 names = self.related_classes.keys()
569 if isinstance(entry, self.related_classes[names[0]]):
570 self.fixed_name, self.unfixed_name = names
571 else:
572 assert isinstance(entry, self.related_classes[names[1]])
573 self.unfixed_name, self.fixed_name = names
574 self.fixed_entry = entry
575 self.unfixed_class = self.related_classes[self.unfixed_name]
576 self.related_manager = self.entry_class._get_related_manager(
577 entry.instance)
578
579
580 def _query_parameters_accepted(self):
581 return [(name, 'Show relationships for this %s' % entry_class.__name__)
582 for name, entry_class
583 in self.related_classes.iteritems()]
584
585
586 def _resolve_query_param(self, name, uri_arg):
587 entry_class = self.related_classes[name]
588 return entry_class.from_uri_args(self._request, uri_arg)
589
590
591 def read_query_parameters(self, query_params):
592 super(RelationshipCollection, self).read_query_parameters(query_params)
593 if not self._query_params:
594 raise exceptions.BadRequest(
595 'You must specify one of the parameters %s and %s'
596 % tuple(self.related_classes.keys()))
597 query_items = self._query_params.items()
598 fixed_entry = self._resolve_query_param(*query_items[0])
599 self._set_fixed_entry(fixed_entry)
600
601 if len(query_items) > 1:
602 other_fixed_entry = self._resolve_query_param(*query_items[1])
603 self.related_manager = self.related_manager.filter(
604 pk=other_fixed_entry.instance.id)
605
606
607 def _entry_from_instance(self, instance):
608 unfixed_entry = self.unfixed_class(self._request, instance)
609 entries = {self.fixed_name: self.fixed_entry,
610 self.unfixed_name: unfixed_entry}
611 return self.entry_class(**entries)
612
613
614 def _fresh_queryset(self):
615 return self.related_manager.all()