Hello, in SQLAlchemy there is possible to do like that:
'''python
class MyModel(Base):
...
def init(self, name=name, value=value, kwargs):
self.name = name
self.value = some_func(value)
# call constructor
super(MyModel, self).init**()
'''
That's very nice if you need to to calculate some fields on create.
But, using Mongoengine, i was surprised that i not able to do like that.
Even if i create init function like above, i'll got error:
''''
fields.py", line 94, in set
instance._data[self.name] = value
AttributeError: 'MyModel' object has no attribute '_data'
'''
If i pass empty constructor
super(MyModel, self).init()
i got error also:
'''
document.py", line 586, in _from_son
obj = cls(__auto_convert=False, **data)
TypeError: __init__() takes at least 3 arguments (4 given)
''''
I checked code of base/document.py and looks like there is no way to do something like above. Is it bug or feature?
The task is simple, i want to calculate password hash with salt, and i want to do it on model level, since there is also method to check if passwords match. I saw this code https://github.com/hmarr/mongoengine/blob/master/mongoengine/django/auth.py with same problem, but author solve it by define new method called set_password(password).
I currently dont have plans to support this - there are hooks in initialisation that you'd have to mimic.
Obviously, if you are overriding core methods you'd need to mimic the signatures but doing pre initialisation logic isn't going to work as the _data dictionary is created in init.
Finally, why would you want to calculate a password salt on initialisation of a class from a database? Surely, its salted in the database?! I think adding a create_password method would be better suited.
Don't want to store password like it entered by user to database, i want only to store password hash with salt and have a function (in same model) to check is there this user and if password match to hash saved in model. I think that is ORM/MVC technologies about to separate data methods and storage algorithms from everything else.
I solve this tasks by overriding save method + it's possible to do it using signals (pre_save).
@istinspring thats my point you should never ever store the password in plaintext.
You could hash it via a pre save signal or override save - but the down side is you have to know if the Document you are saving already exists or not. The good thing about a generate_password_hash method is it can be used explicitly to create the users password at any point: User creation, user change password and user forgotten password.
I have another use case: Say I have an Article model with a title attribute and a slug one. On create, I set only the title, and I would like the slug to default to slugify(self.title). How to accomplish that ?
Besides that, I need other computations to be "triggered" after article creation, such as fetching contents and images from internet, etc. These computations should really be started automatically.
For now, I will fake this with a default callable on a self.inited field. The callable will return False, then will trigger a celery task with some delay (to be sure the object exists in DB) that will run all the heavy computations in the background and set self.inited = True in the end. But I would really have liked to have a cleaner machanism to do that, eg. a def init(), or better, a def post_init().
regards,
Have you tried signals?
Not yet, but it's indeed a very good suggestion ! Thanks :-)
If it works like expected, this will totally void my needs for any kind of init().
Signals works. Also you can override save() method:
def save(self, *args, **kwargs):
if not self.slug:
self.slug = make_slug(self.title)
return super(Article, self).save(*args, **kwargs)
But anyway would be cool to do this kind of tasks w/o using ugly signals and overriding methods. Why not use classical approach like sqlalchemy? because mongoengine looks very similar to sqlalchemy for end programmer and this is great because if you used one you can quickly switch to another.
Here is my signal-based approach, which may be incomplete in terms of checked subclasses, but can be easily extended. [update 2013-07-03: fixed the connect_mongoengine_signals() helper]
in models.py:
class MyModel(Document):
name = StringField()
url = URLField()
@classmethod
def signal_post_init_handler(cls, sender, document, **kwargs):
LOGGER.warning('POST INIT for %s', document)
… #whatever code …
connect_mongoengine_signals(globals())
And in my utils.py:
def connect_mongoengine_signals(module_globals):
""" Automatically iterate classes of a given module and connect handlers
to signals, given they follow the name pattern
``signal_<signal_name>_handler()``.
See https://mongoengine-odm.readthedocs.org/en/latest/guide/signals.html#overview # NOQA
for a list of valid signal names.
"""
if blinker is None:
LOGGER.error('blinker module is not installed, '
'cannot connect mongoengine signals!')
return
for key, potential_class in module_globals.items():
# TODO: use ReferenceDocument and other Mongo classes if appropriate?
try:
look_for_handlers = issubclass(potential_class, Document)
except:
# potential_class is definitely not a class/document ;-)
continue
if look_for_handlers:
for signal_name in ('pre_init', 'post_init', 'pre_save',
'pre_save_post_validation', 'post_save',
'pre_delete', 'post_delete',
'pre_bulk_insert', 'post_bulk_insert'):
handler_name = 'signal_{0}_handler'.format(signal_name)
if hasattr(potential_class, handler_name):
getattr(signals, signal_name).connect(
getattr(potential_class, handler_name),
sender=potential_class)
LOGGER.info('Connected %s.%s to signal %s of sender %s.',
potential_class.__name__,
handler_name, signal_name,
potential_class.__name__)
The calling of this function could be easily moved to some startup parts of django, to avoid the need of calling it in each models.py. This would also avoid passing the globals() argument, which can be thought of _strange_ by newcomers.
I had the same problem, getting AttributeError: 'User' object has no attribute '_data' while trying to overload the init method on a model that inherits from Document.
My solution is to call the Document class initializer first, then override/auto-generate fields, as needed:
def __init__(self, *args, **kwargs):
Document.__init__(self, *args, **kwargs)
if not self.some_field:
self.some_field = function_to_create()
if not self.created_at:
self.created_at = datetime.datetime.now()
What if you won't inherit from Document directly?
Try super(YourClassName, self).__init__(self, *args, **kwargs) instead
Issue Backlog Cleanup: If your question or issue has not been resolved please reopen the issue. Thanks!
@seglberg @rozza
def __init__(self, *args, **kwargs):
super(Article, self).__init__(*args, **kwargs)
self.whatever = ...
def save(self, *args, **kwargs):
if not self.slug:
self.slug = make_slug(self.title)
return super(Article, self).save(*args, **kwargs)
is the overriding of model methods like __init__ and save an accepted practice assuming super is called "appropriately"?
it seems as though for the __init__ method to work, super is called before modifying self while for save, super is called after modifying self, which makes sense as there is preinitialization logic that needs to be run and post save logic that needs to be run
is this a supported way of keeping models more pythonic or will signals be the only supported method?
Most helpful comment
@seglberg @rozza
is the overriding of model methods like
__init__andsavean accepted practice assumingsuperis called "appropriately"?it seems as though for the
__init__method to work,superis called before modifyingselfwhile forsave,superis called after modifying self, which makes sense as there is preinitialization logic that needs to be run and post save logic that needs to be runis this a supported way of keeping models more pythonic or will signals be the only supported method?