سؤال

أحتاج في تطبيقي إلى حفظ القيم المتغيرة (القديمة والجديدة) عند حفظ النموذج.أي أمثلة أو رمز العمل؟

أحتاج إلى هذا من أجل الاعتدال المسبق للمحتوى.على سبيل المثال، إذا قام المستخدم بتغيير شيء ما في النموذج، فيمكن للمسؤول رؤية جميع التغييرات في جدول منفصل ثم يقرر تطبيقها أم لا.

هل كانت مفيدة؟

المحلول

لم تقل الكثير عن حالة الاستخدام أو الاحتياجات المحددة الخاصة بك.على وجه الخصوص، قد يكون من المفيد معرفة ما عليك فعله بمعلومات التغيير (ما هي المدة التي تحتاج إلى تخزينها؟).إذا كنت تحتاج فقط إلى تخزينها لأغراض عابرة، فقد يكون حل جلسة @S.Lott هو الأفضل.إذا كنت تريد مسار تدقيق كامل لجميع التغييرات التي تم إجراؤها على العناصر المخزنة في قاعدة البيانات، فجرب ذلك حل AuditTrail.

تحديث:يعد كود AuditTrail الذي قمت بربطه أعلاه هو الأقرب الذي رأيته إلى حل كامل يناسب حالتك، على الرغم من أنه يحتوي على بعض القيود (لا يعمل على الإطلاق مع حقول ManyToMany).سيقوم بتخزين كافة الإصدارات السابقة من الكائنات الخاصة بك في قاعدة البيانات، بحيث يمكن للمسؤول العودة إلى أي إصدار سابق.سيتعين عليك العمل معه قليلاً إذا كنت تريد ألا يسري التغيير حتى تتم الموافقة عليه.

يمكنك أيضًا إنشاء حل مخصص يعتمد على شيء مثل @Armin Ronacher's DiffingMixin.ستقوم بتخزين القاموس المختلف (ربما مخللًا؟) في جدول ليقوم المشرف بمراجعته لاحقًا وتطبيقه إذا رغبت في ذلك (ستحتاج إلى كتابة الكود لأخذ القاموس المختلف وتطبيقه على مثيل).

نصائح أخرى

لقد وجدت فكرة أرمين مفيدة جدًا.هنا هو الاختلاف الخاص بي.

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

يحرر:لقد اختبرت هذا راجع للشغل.

آسف على الخطوط الطويلة.الفرق هو (بصرف النظر عن الأسماء) أنه يقوم فقط بتخزين الحقول المحلية غير المرتبطة.بمعنى آخر، لا يقوم بتخزين حقول النموذج الأصلي في حالة وجودها.

وهناك شيء آخر؛تحتاج إلى إعادة تعيين _original_state إملاء بعد الحفظ.لكنني لم أرغب في الكتابة فوق save() الطريقة نظرًا لأننا في معظم الأوقات نتجاهل مثيلات النموذج بعد الحفظ.

def save(self, *args, **kwargs):
    super(Klass, self).save(*args, **kwargs)
    self._original_state = self._as_dict()

يقوم Django حاليًا بإرسال جميع الأعمدة إلى قاعدة البيانات، حتى لو قمت بتغيير أحدها للتو.لتغيير هذا، سيكون من الضروري إجراء بعض التغييرات في نظام قاعدة البيانات.يمكن تنفيذ ذلك بسهولة على الكود الموجود عن طريق إضافة مجموعة من الحقول القذرة إلى النموذج وإضافة أسماء الأعمدة إليه، في كل مرة تقوم فيها __set__ قيمة العمود.

إذا كنت بحاجة إلى هذه الميزة، أقترح عليك إلقاء نظرة على Django ORM وتنفيذه ووضع تصحيح في Django trac.يجب أن يكون من السهل جدًا إضافة ذلك وسيساعد المستخدمين الآخرين أيضًا.عند القيام بذلك، قم بإضافة خطاف يتم استدعاؤه في كل مرة يتم فيها تعيين عمود.

إذا كنت لا ترغب في اختراق Django نفسه، فيمكنك نسخ الأمر الخاص بإنشاء الكائن وتغييره.

ربما مع مزيج مثل هذا:

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

لم يتم اختبار هذا الرمز ولكن يجب أن يعمل.عندما تتصل model.get_changed_columns() تحصل على إملاء لجميع القيم المتغيرة.هذا بالطبع لن يعمل مع الكائنات القابلة للتغيير في الأعمدة لأن الحالة الأصلية هي نسخة مسطحة من الإملاء.

لقد قمت بتوسيع حل Trey Hunner لدعم العلاقات بين الأشخاص.نأمل أن يساعد هذا الآخرين الذين يبحثون عن حل مماثل.

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

قد يرغب المرء أيضًا في دمج قائمتي الحقول.لذلك، استبدل السطر الأخير

return changed_fields, changed_m2m_fields

مع

changed_fields.update(changed_m2m_fields)
return changed_fields

إضافة إجابة ثانية لأن لقد تغير الكثير منذ وقت نشر هذه الأسئلة في الأصل.

هناك عدد من التطبيقات في عالم Django التي تحل هذه المشكلة الآن.يمكنك العثور على كامل قائمة تطبيقات التدقيق والتاريخ النموذجية على موقع حزم جانغو.

كتبت مشاركة مدونة مقارنة عدد قليل من هذه التطبيقات.يبلغ عمر هذا المنشور الآن 4 سنوات وهو قديم قليلاً.يبدو أن الأساليب المختلفة لحل هذه المشكلة هي نفسها.

النهج:

  1. قم بتخزين جميع التغييرات التاريخية بتنسيق متسلسل (JSON؟) في جدول واحد
  2. قم بتخزين جميع التغييرات التاريخية في جدول يعكس النسخة الأصلية لكل طراز
  3. قم بتخزين جميع التغييرات التاريخية في نفس الجدول مثل النموذج الأصلي (لا أوصي بهذا)

ال عودة جانغو لا تزال الحزمة هي الحل الأكثر شيوعًا لهذه المشكلة.ويأخذ النهج الأول:إجراء تسلسل للتغييرات بدلاً من عكس الجداول.

لقد أحيت تاريخ جانغو البسيط قبل بضع سنوات.ويأخذ النهج الثاني:مرآة كل طاولة.

لذلك أود أن أوصي باستخدام التطبيق لحل هذه المشكلة.هناك نوعان من البرامج الشائعة التي تعمل بشكل جيد في هذه المرحلة.

أوه، وإذا كنت تبحث فقط عن فحص الحقول القذرة وعدم تخزين جميع التغييرات التاريخية، فقم بالخروج FieldTracker من Django-model-utils.

بالاستمرار في اقتراح Muhuk وإضافة إشارات Django وdisput_uid الفريد، يمكنك إعادة تعيين الحالة عند الحفظ دون تجاوز save():

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

والذي من شأنه تنظيف الحالة الأصلية بمجرد حفظها دون الحاجة إلى تجاوز save().يعمل الرمز ولكن لست متأكدًا من عقوبة الأداء لتوصيل الإشارات عند __init__

لقد قمت بتوسيع حلول muhuk وsmn لتشمل التحقق من الاختلافات في المفاتيح الأساسية للمفتاح الخارجي والحقول الفردية:

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

الفرق الوحيد هو في _as_dict لقد غيرت السطر الأخير من

return dict([
    (f.name, getattr(self, f.name)) for f in self._meta.local_fields
    if not f.rel
])

ل

return dict([
    (f.attname, getattr(self, f.attname)) for f in self._meta.local_fields
])

يمكن استخدام هذا المزيج، مثل تلك المذكورة أعلاه، على النحو التالي:

class MyModel(DirtyFieldsMixin, models.Model):
    ....

إذا كنت تستخدم معاملاتك الخاصة (وليس تطبيق الإدارة الافتراضي)، فيمكنك حفظ الإصدارات السابقة واللاحقة للكائن الخاص بك.يمكنك حفظ الإصدار السابق في الجلسة، أو يمكنك وضعه في الحقول "المخفية" في النموذج.الحقول المخفية هي كابوس أمني.لذلك، استخدم الجلسة للاحتفاظ بسجل ما يحدث مع هذا المستخدم.

بالإضافة إلى ذلك، بالطبع، يتعين عليك جلب الكائن السابق حتى تتمكن من إجراء تغييرات عليه.لذلك لديك عدة طرق لمراقبة الاختلافات.

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

حل محدث مع دعم m2m (باستخدام Update Dirtfields و الجديد _meta API وبعض إصلاحات الأخطاء)، بناءً على @Trey و@Tony's أعلاه.لقد اجتاز هذا بعض اختبارات الضوء الأساسية بالنسبة لي.

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

لمعلومات الجميع، فشل حل Muhuk ضمن python2.6 لأنه يثير استثناء ينص على أن "object.__ init __()" لا يقبل أي وسيطة...

يحرر:هو!على ما يبدو أنه من الممكن أن أكون أنا من أساء استخدام المزيج...لم أنتبه وأعلنت أنه الوالد الأخير ولهذا السبب اتصلت به فيه انتهى الأمر في الأصل الكائن بدلاً من الوالد التالي كما هو الحال عادةً مع وراثة المخطط الماسي!لذا يرجى تجاهل تعليقي :)

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top