2012-06-22 21 views
6

Tengo el siguiente:¿Es posible usar una clave natural para GenericForeignKey en Django?

target_content_type = models.ForeignKey(ContentType, related_name='target_content_type') 
target_object_id = models.PositiveIntegerField() 
target = generic.GenericForeignKey('target_content_type', 'target_object_id') 

me gustaría dumpdata --natural para emitir una clave natural de esta relación. es posible? Si no, ¿existe una estrategia alternativa que no me ate a la clave principal del objetivo?

+0

Tengo curiosidad, ¿se encuentra alguna solución para esto? Hice una búsqueda pero nada útil ha surgido. – shanyu

+0

aún no, pero lo actualizaré con una solución si encuentro uno – Riz

+0

¿Podría elaborar su pregunta? Algún ejemplo. – Rohan

Respuesta

6

TL; DR - En la actualidad no hay manera sensata de hacerlo, sin llegar a la creación de una costumbre Serializer/Deserializer par.

El problema con los modelos que tienen las relaciones genéricas es que Django no ver target como un campo en absoluto, solamente target_content_type y target_object_id, y se intenta serializar y deserializar ellos individualmente.

Las clases responsables de serializar y deserializar modelos de Django se encuentran en los módulos django.core.serializers.base y django.core.serializers.python. Todos los demás (xml, json y yaml) amplían cualquiera de ellos (y python se extiende base). La serialización de campo se hace de la siguiente (líneas irrelevantes oitirán):

for obj in queryset: 
     for field in concrete_model._meta.local_fields: 
       if field.rel is None: 
         self.handle_field(obj, field) 
       else: 
         self.handle_fk_field(obj, field) 

Aquí está la primera complicación: la clave externa a ContentType se maneja bien, con teclas naturales como esperábamos. Pero el PositiveIntegerField es manejado por handle_field, que se implementa como esto:

def handle_field(self, obj, field): 
    value = field._get_val_from_obj(obj) 
    # Protected types (i.e., primitives like None, numbers, dates, 
    # and Decimals) are passed through as is. All other values are 
    # converted to string first. 
    if is_protected_type(value): 
     self._current[field.name] = value 
    else: 
     self._current[field.name] = field.value_to_string(obj) 

es decir, la única posibilidad de personalización aquí (subclasificación de PositiveIntegerField y la definición de un custom value_to_string) no tendrá ningún efecto, ya que el serializador no llamarlo. Cambiar el tipo de datos de target_object_id a algo más que un entero probablemente romperá muchas otras cosas, por lo que no es una opción.

Nos podría definir nuestra costumbre handle_field para emitir claves naturales, en este caso, pero luego viene la segunda complicación: la deserialización se hace así:

for (field_name, field_value) in six.iteritems(d["fields"]): 
     field = Model._meta.get_field(field_name) 
     ... 
      data[field.name] = field.to_python(field_value) 

Incluso si personalizamos el método to_python, se actúa solo en el field_value, fuera del contexto del objeto. No es un problema cuando se usan números enteros, ya que se interpretará como la clave principal del modelo , independientemente del modelo que sea. Pero para deserializar una clave natural, primero necesitamos saber a qué modelo pertenece esa clave, y esa información no está disponible a menos que obtengamos una referencia al objeto (y el campo target_content_type ya se ha deserializado).

Como puede ver, no es una tarea imposible, ya que admite claves naturales en relaciones genéricas, pero para lograr eso, habría que cambiar muchas cosas en el código de serialización y deserialización.Las medidas necesarias, entonces (si alguien se siente a la altura) son:

  • Crea una alfombrilla de Field extendiéndose PositiveIntegerField, con métodos para codificar/decodificar un objeto - pidiendo que los modelos de referencia natural_key y get_by_natural_key;
  • Ignorar el serializador handle_field para llamar al codificador si está presente;
  • Implemente un deserializador personalizado que: 1) impone cierto orden en los campos, asegurando que el tipo de contenido se deserialice antes que la clave natural; 2) llama al decodificador, pasando no solo el field_value sino también una referencia al decodificado ContentType.
0

He escrito un Serializador y Deserializador personalizado que admite GenericFK. Lo revisé brevemente y parece hacer el trabajo.

Esto es lo que ocurrió:

import json 

from django.contrib.contenttypes.generic import GenericForeignKey 
from django.utils import six 
from django.core.serializers.json import Serializer as JSONSerializer 
from django.core.serializers.python import Deserializer as \ 
    PythonDeserializer, _get_model 
from django.core.serializers.base import DeserializationError 
import sys 


class Serializer(JSONSerializer): 

    def get_dump_object(self, obj): 
     dumped_object = super(CustomJSONSerializer, self).get_dump_object(obj) 
     if self.use_natural_keys and hasattr(obj, 'natural_key'): 
      dumped_object['pk'] = obj.natural_key() 
      # Check if there are any generic fk's in this obj 
      # and add a natural key to it which will be deserialized by a matching Deserializer. 
      for virtual_field in obj._meta.virtual_fields: 
       if type(virtual_field) == GenericForeignKey: 
        content_object = getattr(obj, virtual_field.name) 
        dumped_object['fields'][virtual_field.name + '_natural_key'] = content_object.natural_key() 
     return dumped_object 


def Deserializer(stream_or_string, **options): 
    """ 
    Deserialize a stream or string of JSON data. 
    """ 
    if not isinstance(stream_or_string, (bytes, six.string_types)): 
     stream_or_string = stream_or_string.read() 
    if isinstance(stream_or_string, bytes): 
     stream_or_string = stream_or_string.decode('utf-8') 
    try: 
     objects = json.loads(stream_or_string) 
     for obj in objects: 
      Model = _get_model(obj['model']) 
      if isinstance(obj['pk'], (tuple, list)): 
       o = Model.objects.get_by_natural_key(*obj['pk']) 
       obj['pk'] = o.pk 
       # If has generic fk's, find the generic object by natural key, and set it's 
       # pk according to it. 
       for virtual_field in Model._meta.virtual_fields: 
        if type(virtual_field) == GenericForeignKey: 
         natural_key_field_name = virtual_field.name + '_natural_key' 
         if natural_key_field_name in obj['fields']: 
          content_type = getattr(o, virtual_field.ct_field) 
          content_object_by_natural_key = content_type.model_class().\ 
          objects.get_by_natural_key(obj['fields'][natural_key_field_name][0]) 
          obj['fields'][virtual_field.fk_field] = content_object_by_natural_key.pk 
     for obj in PythonDeserializer(objects, **options): 
      yield obj 
    except GeneratorExit: 
     raise 
    except Exception as e: 
     # Map to deserializer error 
     six.reraise(DeserializationError, DeserializationError(e), sys.exc_info()[2]) 
Cuestiones relacionadas