Write a mixin that does things on save?

393 views
Skip to first unread message

Andy Robinson

unread,
Jun 26, 2015, 5:52:28 PM6/26/15
to mongoeng...@googlegroups.com
I'd like to create a Mixin class for many different Document classes, which will
(a) add some fields such as date/time updated
(b) react and do some things - calculate some of these fields - when saved.  I'd like some method of the mixin to be called
(c) if possible, as an enhancement over (b),  when there have been genuine changes saved - there seems to be a _delta() method in the code.

Can anyone point me at some sample code or suggest an approach, please?   

Adding fields works fine; MyClass below gets the fields and saves them to the database with initial values.  I can't work out a clean way to  have HistoryMixin notified when MyClass gets saved.

class HistoryMixin(object):
    date_created = DateTimeField(default=datetime.datetime.now)
    created_by = StringField(max_length=50, default="system")
    date_modified = DateTimeField(default=datetime.datetime.now)
    modified_by = StringField(max_length=50, default="system")
    version = IntField(default=1)

class MyClass(Document, HistoryMixin):
    event = StringField()


Thanks,

Andy

qMax

unread,
Jun 27, 2015, 6:41:43 AM6/27/15
to mongoeng...@googlegroups.com
1. you may just override method save, like this:
```
def save(self, **kwargs):
self.mtime = now000()
if not self.ctime:
self.ctime = self.mtime
return super().save(**kwargs)
```

2. mongoengine sends signals:
pre_save
pre_save_post_validation

You may catch them add modify those fields.

3. you have no way to catch modifcation that happen when using queryset.update
> --
> You received this message because you are subscribed to the Google Groups
> "MongoEngine Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to mongoengine-us...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.



--
qMax,
ever

Andy Robinson

unread,
Jun 27, 2015, 11:30:00 AM6/27/15
to mongoeng...@googlegroups.com
Thanks for this.   I managed to get it a bit cleaner in three ways and have pasted by current script below:

(1) I found that, like Django's ORM, you can set "abstract=True" on a Document subclass, so it won't create a collection, then inherit other docs from that.  This is undocumented but was in the source.
(2) Using the pre-save-post-validate signal
(3) using the builtin delta-tracking to find out of it has really changed.  

There's still one annoying bit: I need to wire a signal to each of my concrete Document subclasses.  If I had ten different kinds of document/model in my project, they would each need a signal.   I can't wire it once to my "MyBase" class and make things "just work" yet by inheriting from it.

I suspect others might want to do this, and I wonder if there are any "hooks" I have missed - e.g. methods intended to be overridden?

Working script below....


"""Trying to create an abstract base class which causes stuff to happen
on saving, when the document has changed

Just need to eliminate the nasty signal

"""

import json
from pprint import pprint
from mongoengine import signals
from datetime import datetime
from mongoengine import *




    
class MyBase(Document):
    "I want lots of classes to inherit from this"
    modified = DateTimeField()
    revision = IntField(default=0)
    meta = {'abstract': True}

    def _bump_revision_if_changed(self):
        ch_set, ch_unset = self._delta()
        if ch_set or ch_unset:
            self.modified = datetime.utcnow()
            self.revision += 1
            print "saved revision %d, delta was: %s" % (self.revision, (ch_set, ch_unset))
        else:
            print "saved revision %d, no change" % self.revision


    @classmethod
    def pre_save_post_validate(cls, sender, document, **kwargs):
        print "pre_save_post_validate fired"
        cls._bump_revision_if_changed(document)


class Pet(EmbeddedDocument):
    name = StringField()
    kind = StringField()

class Person(MyBase):
    gender = StringField(max_length=1,required=True, default="M")
    first_name = StringField(max_length=30)
    last_name = StringField(max_length=30)
    pets = ListField(EmbeddedDocumentField(Pet))


signals.pre_save_post_validation.connect(MyBase.pre_save_post_validate, sender=Person)




if __name__=='__main__':
    connect('mongorecalc')
    print "connected"
    Person.drop_collection()


    p = Person(gender="M", first_name="Fred", last_name="Flintstone", pets=[
        Pet(name="dino", kind="saurus")
        ])

    print "creating"
    p.save()

    p.gender = "F"
    p.first_name = "Wilma"
    print "made changes to", p._get_changed_fields(), "..."
    p.save()

    p.pets[0].kind = "lizard"
    print
    print "made changes to embedded doc"
    p.save()
    print
    print "save after no more changes"
    p.save()


    print
    print "convert to json"
    pprint(p.to_json())
 

qMax

unread,
Jun 27, 2015, 11:52:40 AM6/27/15
to mongoeng...@googlegroups.com
You do not need to deal with that "abstract".
It seems quite ok to use mixins.

Here is my solution:
```
class Timeable(object):
""" model mixin for time tracking
updates ctime and mtime on save
"""
ctime = DateTimeField(verbose_name="время создания")
mtime = DateTimeField(verbose_name="время обновления")

def save(self, **kwargs):
self.mtime = now000()
if not self.ctime:
self.ctime = self.mtime
return super().save(**kwargs)

def update_times(self):
self.update({ 'mtime': now000() })

class Foo(Timeable, Document):
"bablabla"
```
Important point is that mixin should go before Document in mro, to
allow it using super()

Andy Robinson

unread,
Jun 27, 2015, 3:46:36 PM6/27/15
to mongoeng...@googlegroups.com


On Saturday, June 27, 2015 at 4:52:40 PM UTC+1, qMax wrote:
You do not need to deal with that "abstract".
It seems quite ok to use mixins.

Many thanks.   You're correct, I had tried it with the wrong MRO the first time, so turned to mixins.  Your second example is much simpler.

I'm very impressed with MongoEngine.  I need to build audit trails and record when records changed.  There seem to be very sophisticated checks built into MongoEngine already - just calling self._delta() before save describes exactly what changed...

- Andy
Reply all
Reply to author
Forward
0 new messages