python - español - Campos sucios en django
django windows (10)
En mi aplicación, necesito guardar los valores cambiados (viejos y nuevos) cuando el modelo se guarda. ¿Algún ejemplo o código de trabajo?
Necesito esto para la premoderación del contenido. Por ejemplo, si el usuario cambia algo en el modelo, el administrador puede ver todos los cambios en una tabla por separado y luego decidir aplicarlos o no.
Actualmente, Django está enviando todas las columnas a la base de datos, incluso si acaba de cambiar una. Para cambiar esto, algunos cambios en el sistema de la base de datos serían necesarios. Esto podría implementarse fácilmente en el código existente al agregar un conjunto de campos sucios al modelo y agregarle nombres de columna, cada vez que __set__
un valor de columna.
Si necesita esa característica, le sugiero que mire el Django ORM, impleméntelo y coloque un parche en el tracto Django. Debería ser muy fácil agregar eso y ayudaría a otros usuarios también. Cuando lo haga, agregue un gancho que se llama cada vez que se establece una columna.
Si no quieres hackear Django, puedes copiar el dict en la creación del objeto y modificarlo.
Tal vez con un mixin como este:
class DiffingMixin(object):
def __init__(self, *args, **kwargs):
super(DiffingMixin, self).__init__(*args, **kwargs)
self._original_state = dict(self.__dict__)
def get_changed_columns(self):
missing = object()
result = {}
for key, value in self._original_state.iteritems():
if key != self.__dict__.get(key, missing):
result[key] = value
return result
class MyModel(DiffingMixin, models.Model):
pass
Este código no ha sido probado pero debería funcionar. Cuando llamas a model.get_changed_columns()
obtienes un dict de todos los valores modificados. Por supuesto, esto no funcionará para objetos mutables en columnas porque el estado original es una copia plana del dict.
Agregar una segunda respuesta porque muchas cosas han cambiado desde el momento en que se publicaron originalmente estas preguntas .
Hay una serie de aplicaciones en el mundo de Django que resuelven este problema ahora. Puede encontrar una lista completa de las aplicaciones modelo de auditoría e historial en el sitio Django Packages.
Escribí una publicación de blog que comparaba algunas de estas aplicaciones. Esta publicación ahora tiene 4 años y está un poco pasada de moda. Sin embargo, los diferentes enfoques para resolver este problema parecen ser los mismos.
Los enfoques:
- Almacenar todos los cambios históricos en un formato serializado (¿JSON?) En una sola tabla
- Almacene todos los cambios históricos en una tabla que refleje el original para cada modelo
- Almacene todos los cambios históricos en la misma tabla que el modelo original (no lo recomiendo)
El paquete django-reversion todavía parece ser la solución más popular para este problema. Se necesita el primer enfoque: serializar los cambios en lugar de las tablas de duplicación.
django-simple-history hace unos años. Toma el segundo enfoque: duplicar cada mesa.
Así que recomendaría usar una aplicación para resolver este problema . Hay un par de populares que funcionan bastante bien en este punto.
Ah, y si solo buscas una verificación de campo sucia y no almacena todos los cambios históricos, echa un vistazo a FieldTracker desde django-model-utils .
Extendí la solución de Trey Hunner para apoyar las relaciones m2m. Con suerte, esto ayudará a otros a buscar una solución similar.
from django.db.models.signals import post_save
DirtyFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
post_save.connect(self._reset_state, sender=self.__class__,
dispatch_uid=''%s._reset_state'' % self.__class__.__name__)
self._reset_state()
def _as_dict(self):
fields = dict([
(f.attname, getattr(self, f.attname))
for f in self._meta.local_fields
])
m2m_fields = dict([
(f.attname, set([
obj.id for obj in getattr(self, f.attname).all()
]))
for f in self._meta.local_many_to_many
])
return fields, m2m_fields
def _reset_state(self, *args, **kwargs):
self._original_state, self._original_m2m_state = self._as_dict()
def get_dirty_fields(self):
new_state, new_m2m_state = self._as_dict()
changed_fields = dict([
(key, value)
for key, value in self._original_state.iteritems()
if value != new_state[key]
])
changed_m2m_fields = dict([
(key, value)
for key, value in self._original_m2m_state.iteritems()
if sorted(value) != sorted(new_m2m_state[key])
])
return changed_fields, changed_m2m_fields
También es posible que desee combinar las dos listas de campos. Para eso, reemplace la última línea
return changed_fields, changed_m2m_fields
con
changed_fields.update(changed_m2m_fields)
return changed_fields
Extendí las soluciones de muhuk y smn para incluir la verificación de diferencias en las claves principales para clave externa y campos de uno a uno:
from django.db.models.signals import post_save
class DirtyFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
post_save.connect(self._reset_state, sender=self.__class__,
dispatch_uid=''%s-DirtyFieldsMixin-sweeper'' % self.__class__.__name__)
self._reset_state()
def _reset_state(self, *args, **kwargs):
self._original_state = self._as_dict()
def _as_dict(self):
return dict([(f.attname, getattr(self, f.attname)) for f in self._meta.local_fields])
def get_dirty_fields(self):
new_state = self._as_dict()
return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
La única diferencia está en _as_dict
Cambié la última línea de
return dict([
(f.name, getattr(self, f.name)) for f in self._meta.local_fields
if not f.rel
])
a
return dict([
(f.attname, getattr(self, f.attname)) for f in self._meta.local_fields
])
Este mixin, como los anteriores, se puede usar así:
class MyModel(DirtyFieldsMixin, models.Model):
....
He encontrado la idea de Armin muy útil. Aquí está mi variación;
class DirtyFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
self._original_state = self._as_dict()
def _as_dict(self):
return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])
def get_dirty_fields(self):
new_state = self._as_dict()
return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
Editar: He probado esto por cierto.
Perdón por las largas colas La diferencia es (aparte de los nombres) solo almacena en caché los campos locales sin relación. En otras palabras, no almacena en caché los campos de un modelo padre si están presentes.
Y hay una cosa más; necesita restablecer _original_state dict después de guardar. Pero no quería sobrescribir el método save () ya que la mayoría de las veces desechamos las instancias del modelo después de guardarlas.
def save(self, *args, **kwargs):
super(Klass, self).save(*args, **kwargs)
self._original_state = self._as_dict()
No ha dicho mucho sobre su caso de uso específico o sus necesidades. En particular, sería útil saber qué necesita hacer con la información de cambio (¿cuánto tiempo necesita almacenarla?). Si solo necesita almacenarlo para fines transitorios, la solución de sesión de @ S.Lott puede ser la mejor. Si desea un seguimiento de auditoría completo de todos los cambios en sus objetos almacenados en el DB, pruebe esta solución AuditTrail .
ACTUALIZACIÓN : El código de AuditTrail al que he vinculado anteriormente es lo más cercano que he visto a una solución completa que funcionaría para su caso, aunque tiene algunas limitaciones (no funciona en absoluto para los campos ManyToMany). Almacenará todas las versiones anteriores de sus objetos en la base de datos, por lo que el administrador podría retroceder a cualquier versión anterior. Tendría que trabajar un poco si quiere que el cambio no surta efecto hasta que se apruebe.
También podría crear una solución personalizada basada en algo como DiffingMixin de @Armin Ronacher. Deberías guardar el diccionario de diferencias (¿en escabeche?) En una tabla para que el administrador lo revise más tarde y aplicarlo si lo deseas (necesitarías escribir el código para tomar el diccionario de diferencias y aplicarlo a una instancia).
Si está utilizando sus propias transacciones (no la aplicación de administración predeterminada), puede guardar las versiones anterior y posterior de su objeto. Puede guardar la versión anterior en la sesión, o puede colocarla en campos "ocultos" en el formulario. Los campos ocultos son una pesadilla de seguridad. Por lo tanto, use la sesión para conservar el historial de lo que está sucediendo con este usuario.
Además, por supuesto, debe buscar el objeto anterior para poder realizar cambios en él. Entonces, tienes varias formas de controlar las diferencias.
def updateSomething( request, object_id ):
object= Model.objects.get( id=object_id )
if request.method == "GET":
request.session[''before'']= object
form= SomethingForm( instance=object )
else request.method == "POST"
form= SomethingForm( request.POST )
if form.is_valid():
# You have before in the session
# You have the old object
# You have after in the form.cleaned_data
# Log the changes
# Apply the changes to the object
object.save()
Siguiendo con la sugerencia de Muhuk y agregando las señales de Django y un único dispatch_uid, puede reiniciar el estado al guardar sin anular el guardado ():
from django.db.models.signals import post_save
class DirtyFieldsMixin(object):
def __init__(self, *args, **kwargs):
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
post_save.connect(self._reset_state, sender=self.__class__,
dispatch_uid=''%s-DirtyFieldsMixin-sweeper'' % self.__class__.__name__)
self._reset_state()
def _reset_state(self, *args, **kwargs):
self._original_state = self._as_dict()
def _as_dict(self):
return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])
def get_dirty_fields(self):
new_state = self._as_dict()
return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
Lo cual limpiaría el estado original una vez guardado sin tener que anular el guardado (). El código funciona pero no está seguro de cuál es la penalización de rendimiento de las señales de conexión en __init__
Una solución actualizada con soporte m2m (utilizando dirtyfields actualizados y la nueva API _meta y algunas correcciones de errores), basada en @Trey y @ Tony anteriores. Esto ha pasado algunas pruebas de luz básicas para mí.
from dirtyfields import DirtyFieldsMixin
class M2MDirtyFieldsMixin(DirtyFieldsMixin):
def __init__(self, *args, **kwargs):
super(M2MDirtyFieldsMixin, self).__init__(*args, **kwargs)
post_save.connect(
reset_state, sender=self.__class__,
dispatch_uid=''{name}-DirtyFieldsMixin-sweeper''.format(
name=self.__class__.__name__))
reset_state(sender=self.__class__, instance=self)
def _as_dict_m2m(self):
if self.pk:
m2m_fields = dict([
(f.attname, set([
obj.id for obj in getattr(self, f.attname).all()
]))
for f,model in self._meta.get_m2m_with_model()
])
return m2m_fields
return {}
def get_dirty_fields(self, check_relationship=False):
changed_fields = super(M2MDirtyFieldsMixin, self).get_dirty_fields(check_relationship)
new_m2m_state = self._as_dict_m2m()
changed_m2m_fields = dict([
(key, value)
for key, value in self._original_m2m_state.iteritems()
if sorted(value) != sorted(new_m2m_state[key])
])
changed_fields.update(changed_m2m_fields)
return changed_fields
def reset_state(sender, instance, **kwargs):
# original state should hold all possible dirty fields to avoid
# getting a `KeyError` when checking if a field is dirty or not
instance._original_state = instance._as_dict(check_relationship=True)
instance._original_m2m_state = instance._as_dict_m2m()
para la información de todos, la solución de muhuk falla bajo python2.6 ya que plantea una excepción que indica que ''object .__ init __ ()'' no acepta ningún argumento ...
editar: ho! aparentemente podría haber sido yo quien usó mal el mixin ... No presté atención y lo declare como el último padre y debido a eso la llamada a init terminó en el objeto primario en lugar de en el siguiente padre, ya que no sería lo mismo con el diamante diagrama de herencia! así que por favor ignora mi comentario :)