I'm working with instances of database models where I need to construct the object from data in-memory (using Python-style o = Object() as opposed to ModelClass.objects.create(). Whether or not the data will be saved in the database is to be decided later, when a call to o.save() would be used.
These models have a ManyToManyField and own a number of child objects. Problem is, I can't add() to the ManyToManyField until the child objects are actually saved. How can I construct these objects in such a way that save() can be called later? Every potential solution I've found to this problem does not actually do what I want.
Here's some example code showing what I'm trying to do:
class Author:
# ...
#classmethod
def create(cls, data):
# ...
pass
class Book(models.Model):
title = models.CharField(max_length=128)
pages = models.PositiveIntegerField()
authors = models.ManyToManyField(Author)
#classmethod
#transaction.atomic
def create(cls, data):
try:
with transaction.atomic():
b = cls(title=data["title"],
pages=data["pages"])
# This works, but has an unwanted side effect: authors are saved to the database
# as they're created here while the Book is not saved.
b.authors = Author.objects.bulk_create([Author.create(a) for a in data["authors"]])
return b
except Exception:
# ...
raise
################### Later on...
# This data is NOT static - it's formed from JSON which comes from an API. Just is here as an example.
data = {
"title": 1,
"pages": 934,
"authors": [
{
"name": "John Smith",
# ...
}
]
}
# We're going to use this now, but we're unsure if we want to actually save
# the object to the database.
b = Book.create(data)
# Save the data to the database if we want to.
b.save()
The only solution I can think off is queuing the operations and perform them when you call save().
class PostponedOpMixin(models.Model):
def __init__(self, *args, **kwargs):
self._postponed_ops = []
super(PostponedOpMixin, self).__init__(*args, **kwargs)
def _getattr(self, attr):
result = self
for part in attr.split('.'):
result = getattr(result, part)
return result
def postpone(self, op, *args, **kwargs):
if self.pk: # execute now if self already has a pk
return self._getattr(op)(*args, **kwargs)
self._postponed_ops.append((op, *args, **kwargs))
def save(self, *args, *kwargs):
super(PostponedOpMixin, self).save(*args, **kwargs)
while self._postponed_ops:
op, args, kwargs = self._postponed_ops.pop(0):
self._getattr(op)(*args, **kwargs)
def Meta:
abstract = True
This way you can do:
class Book(PostponedOpMixin):
...
authors = models.ManyToManyField(Author)
...
instance = Book()
instance.title = "Romeo and Juliet"
instance.postpone('authors.add', shakespeare)
...
# and some time later:
instance.save()
This code is untested and intended as a start point. Any bug is left as an exercise for the reader.
Maybe you could postpone the actual book's authors adding to the moment you are sure you want to save that book into the database.
In the meantime, you could store a list of processed (but not yet saved) author objects for each book object.
class Book(models.Model):
title = models.CharField(max_length=128)
pages = models.PositiveIntegerField()
authors = models.ManyToManyField(Author)
#classmethod
#transaction.atomic
def create(cls, data):
try:
b = cls(title=data["title"],
pages=data["pages"])
b.author_list = list()
for a in data["authors"]:
b.authors_list.append(Author.create(a))
return b
except Exception:
# ...
raise
Then, when you are sure you want to save the object, you have to save all the authors in that list, and then add them to the 'authors' field in the corresponding book object.
Related
I have an object that need to be instantiated ONLY ONCE. Tried using redis for caching the instance failed with error cache.set("some_key", singles, timeout=60*60*24*30) but got serialization error, due the other thread operations:
TypeError: can't pickle _thread.lock objects
But, I can comfortably cache others instances as need.
Thus I am looking for a way to create a Singleton object, I also tried:
class SingletonModel(models.Model):
class Meta:
abstract = True
def save(self, *args, **kwargs):
# self.pk = 1
super(SingletonModel, self).save(*args, **kwargs)
# if self.can_cache:
# self.set_cache()
def delete(self, *args, **kwargs):
pass
class Singleton(SingletonModel):
singles = []
#classmethod
def setSingles(cls, singles):
cls.singles = singles
#classmethod
def loadSingles(cls):
sins = cls.singles
log.warning("*****Found: {} singles".format(len(sins)))
if len(sins) == 0:
sins = cls.doSomeLongOperation()
cls.setSingles(sins)
return sins
In the view.py I call on Singleton.loadSingles() but I notice that I get
Found: 0 singles
after 2-3 requests. Please what is the best way to create Singleton on Djnago without using third party library that might try serialising and persisting the object (which is NOT possible in my case)
I found it easier to use a unique index to accomplish this
class SingletonModel(models.Model):
_singleton = models.BooleanField(default=True, editable=False, unique=True)
class Meta:
abstract = True
This is my Singleton Abstract Model.
class SingletonModel(models.Model):
"""Singleton Django Model"""
class Meta:
abstract = True
def save(self, *args, **kwargs):
"""
Save object to the database. Removes all other entries if there
are any.
"""
self.__class__.objects.exclude(id=self.id).delete()
super(SingletonModel, self).save(*args, **kwargs)
#classmethod
def load(cls):
"""
Load object from the database. Failing that, create a new empty
(default) instance of the object and return it (without saving it
to the database).
"""
try:
return cls.objects.get()
except cls.DoesNotExist:
return cls()
The code below simply prevents the creation of a new instance of the Revenue model if one exists. I believe this should point you in the right direction.
Best of luck !!!
class RevenueWallet(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta:
verbose_name = "Revenue"
def save(self, *args, **kwargs):
"""
:param args:
:param kwargs:
:return:
"""
# Checking if pk exists so that updates can be saved
if not RevenueWallet.objects.filter(pk=self.pk).exists() and RevenueWallet.objects.exists():
raise ValidationError('There can be only one instance of this model')
return super(RevenueWallet, self).save(*args, **kwargs)
I feel like this must be really simple, but after a couple of days of trying I'm officially clueless.
I have a dictionary where the keys are objects and the values are lists of objects. Here's how I want to use that info to construct a form:
for object in dictionary:
name_of_field = object.slug
name_of_field = forms.ModelMultipleChoiceField(widgets=forms.CheckboxSelectMultiple, queryset=dictionary[object])
Of course, just putting name_of_field in there twice doesn't work to generate dynamically named fields. What this actually does is create a single field called "name_of_field" using the final object it iterates over. I wish it would create a field for every key in the dictionary, named using the key object's slug and with a choice set of that key's values.
Is there a way to loop through this dictionary and create the form fields I want? I feel like the answer lies in superclassing __init__, but I still can't wrap my head around how to get multiple fields with different names.
You don't say where you are using this code. You should be putting it into the form's __init__ method, from where you can reference self.fields:
class DynamicForm(forms.Form):
def __init__(self, *args, **kwargs):
dynamic_fields = kwargs.pop('dynamic_fields')
super(DynamicForm, self).__init__(*args, **kwargs)
for key, value in dynamic_fields:
self.fields[key.slug] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=value)
I feel like I want to contribute to this question although it is really old, because I could not solve my question with the answer alone.
For a given model with the form:
class Product(models.Model):
data = models.JSONField()
store = models.ForeignKey(Store, on_delete = models.CASCADE)
number = models.PositiveIntegerField()
a dynamically created form can be created (careful, this is a simplified case, where all products of the same store share the same keys in the JSONField):
class ProductForm(forms.Form):
def __init__(self, first_product, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["number"] = forms.IntegerField(required = True)
for key in first_product.data.keys():
self.fields[key] = forms.CharField(required = False)
class Meta:
model = Product
fields = ["number", "data"]
The form must be called by handing it the first_product variable in the view:
class SomeView(TemplateView):
template_name = "appName/sometemplatename.html"
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(**kwargs)
store = Store.objects.get(user = self.request.user)
tmp = Product.objects.filter(store = store).first()
context["testform"] = ProductForm(first_product = tmp)
return context
This is a vastly general approach to start from.
Struggling to figure out how to Over-Ride the __init__() method in my Django Form to include additional values from the database. I have a group of photographers that I am trying to list as a form option for the user. Afterwards, the user's photographer selection will be added (along with other information) to the database as an instantiation of a new model.
This is a continuation, or elaboration, of my other Current Question. #Rob Osborne has given me some great advice helping me understand how to extend BaseForm, but I still cannot get my code to execute. The linked question lists my models, form, and views, if you are interested. While I understand that using ModelForm is easier and more documented, I must use BaseForm in this instance.
Here is what I have:
class AForm(BaseForm):
def __init__(self, data=None, files=None, instance=None, auto_id='id_%s',
prefix=None, initial=None, error_class=ErrorList,
label_suffix=':', empty_permitted=False):
self.instance = instance
object_data = self.instance.fields_dict()
self.declared_fields = SortedDict()
self.base_fields = fields_for_a(self.instance)
BaseForm.__init__(self, data, files, auto_id, prefix, object_data,
error_class, label_suffix, empty_permitted)
self.fields['photographer'].queryset = Photographer.objects.all()
def save(self, commit=True):
if not commit:
raise NotImplementedError("AForm.save must commit it's changes.")
if self.errors:
raise ValueError(_(u"The Form could not be updated because the data didn't validate."))
cleaned_data = self.cleaned_data
# save fieldvalues for self.instance
fields = field_list(self.instance)
for field in fields:
if field.enable_wysiwyg:
value = unicode(strip(cleaned_data[field.name]))
else:
value = unicode(cleaned_data[field.name])
Using the above code results in a KeyError at 'photographer'.
I appreciate any ideas / comments on how to resolve this KeyError so that I can get the photographer values into my form. Thank you!
EDIT:
Trying to use super, as recommended by #supervacuo, but still getting a KeyError at photographer as before:
class AForm(BaseForm):
def __init__(self, data=None, files=None, instance=None, auto_id='id_%s',
prefix=None, initial=None, error_class=ErrorList,
label_suffix=':', empty_permitted=False):
super(AForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted)
self.fields['photographer'].queryset = Photographer.objects.all()
What could I be missing that is generating the KeyError? Thanks for any advice.
EDIT 2: adding fields_dict()
from models.py
class A(models.Model):
category = models.ForeignKey(Category)
user = models.ForeignKey(User)
def fields_dict(self):
fields_dict = {}
fields_dict['title'] = self.title
for key, value in self.fields():
fields_dict[key.name] = value.value
return fields_dict
Thanks for any advice.
EDIT 3: (edited class AForm above in the initial question as well, to include more information)
def fields_for_a(instance):
fields_dict = SortedDict()
fields = field_list(instance)
for field in fields:
if field.field_type == Field.BOOLEAN_FIELD:
fields_dict[field.name] = forms.BooleanField(label=field.label, required=False, help_text=field.help_text)
elif field.field_type == Field.CHAR_FIELD:
widget = forms.TextInput
fields_dict[field.name] = forms.CharField(label=field.label, required=field.required, max_length=field.max_length, help_text=field.help_text, widget=widget)
fields_dict[field.name] = field_type(label=field.label,
required=field.required,
help_text=field.help_text,
max_length=field.max_length,
widget=widget)
return fields_dict
EDIT 4: def fields(self). from models.py:
def fields(self):
fields_list = []
fields = list(self.category.field_set.all())
fields += list(Field.objects.filter(category=None))
for field in fields:
try:
fields_list.append((field, field.fieldvalue_set.get(ad=self),))
except FieldValue.DoesNotExist:
pass # If no value is associated with that field, skip it.
return fields_list
def field(self, name):
if name == 'title':
return self.title
else:
return FieldValue.objects.get(field__name=name, ad=self).value
That GitHub link should've been the first thing in your question.
The django-classifieds application has an entire system of dynamic fields (based on the Field and FieldValue models) which is why you're having trouble. If you don't fully understand this aspect of django-classifieds, I recommend you base your project on something else instead.
Looking down the list of FIELD_CHOICES in django-classified's models.py, you can't use this database-driven field system to define relationsips — so there's no dynamic per-category ForeignKey field!
The alternative would be to add a photographer field on your A model (any particular reason you've renamed it from Ad?), as it seems you have done based on your other question. To go the rest of the distance, however, you'd need to edit the fields_dict() method like so:
def fields_dict(self):
fields_dict = {}
fields_dict['title'] = self.title
fields_dict['photographer'] = self.photographer
for key, value in self.fields():
fields_dict[key.name] = value.value
return fields_dict
Your call to BaseForm.__init__ seems wrong; you should be using super(), like so
class AForm(BaseForm):
def __init__(self, *args, **kwargs):
super(AForm, self).__init__(*args, **kwargs)
self.fields['photographer'].queryset = Photographer.objects.all()
(as actually recommended in Rob Osbourne's accepted answer to your other question).
Beyond that, I am suspicious of your fields_dict() method, which isn't part of Django and you haven't provided the definition for. Confirm with print self.fields.keys() that, for whatever mysterious reason, photographer is not there, then post the code for fields_dict().
My form field looks something like the following:
class FooForm(ModelForm):
somefield = models.CharField(
widget=forms.TextInput(attrs={'readonly':'readonly'})
)
class Meta:
model = Foo
Geting an error like the following with the code above: init() got an unexpected keyword argument 'widget'
I thought this is a legitimate use of a form widget?
You should use a form field and not a model field:
somefield = models.CharField(
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
replaced with
somefield = forms.CharField(
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
Should fix it.
Note that the readonly attribute does not keep Django from processing any value sent by the client. If it is important to you that the value doesn't change, no matter how creative your users are with FireBug, you need to use a more involved method, e.g. a ReadOnlyField/ReadOnlyWidget like demonstrated in a blog entry by Alex Gaynor.
I was going into the same problem so I created a Mixin that seems to work for my use cases.
class ReadOnlyFieldsMixin(object):
readonly_fields =()
def __init__(self, *args, **kwargs):
super(ReadOnlyFieldsMixin, self).__init__(*args, **kwargs)
for field in (field for name, field in self.fields.iteritems() if name in self.readonly_fields):
field.widget.attrs['disabled'] = 'true'
field.required = False
def clean(self):
cleaned_data = super(ReadOnlyFieldsMixin,self).clean()
for field in self.readonly_fields:
cleaned_data[field] = getattr(self.instance, field)
return cleaned_data
Usage, just define which ones must be read only:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
As Benjamin (https://stackoverflow.com/a/2359167/565525) nicely explained, additionally to rendering correctly, you need to process field on backend properly.
There is an SO question and answers that has many good solutions. But anyway:
1) first approach - removing field in save() method, e.g. (not tested ;) ):
def save(self, *args, **kwargs):
for fname in self.readonly_fields:
if fname in self.cleaned_data:
del self.cleaned_data[fname]
return super(<form-name>, self).save(*args,**kwargs)
2) second approach - reset field to initial value in clean method:
def clean_<fieldname>(self):
return self.initial[<fieldname>] # or getattr(self.instance, <fieldname>)
Based on second approach I generalized it like this:
from functools import partial
class <Form-name>(...):
def __init__(self, ...):
...
super(<Form-name>, self).__init__(*args, **kwargs)
...
for i, (fname, field) in enumerate(self.fields.iteritems()):
if fname in self.readonly_fields:
field.widget.attrs['readonly'] = "readonly"
field.required = False
# set clean method to reset value back
clean_method_name = "clean_%s" % fname
assert clean_method_name not in dir(self)
setattr(self, clean_method_name, partial(self._clean_for_readonly_field, fname=fname))
def _clean_for_readonly_field(self, fname):
""" will reset value to initial - nothing will be changed
needs to be added dynamically - partial, see init_fields
"""
return self.initial[fname] # or getattr(self.instance, fname)
How do I have actions occur when a field gets changed in one of my models? In this particular case, I have this model:
class Game(models.Model):
STATE_CHOICES = (
('S', 'Setup'),
('A', 'Active'),
('P', 'Paused'),
('F', 'Finished')
)
name = models.CharField(max_length=100)
owner = models.ForeignKey(User)
created = models.DateTimeField(auto_now_add=True)
started = models.DateTimeField(null=True)
state = models.CharField(max_length=1, choices=STATE_CHOICES, default='S')
and I would like to have Units created, and the 'started' field populated with the current datetime (among other things), when the state goes from Setup to Active.
I suspect that a model instance method is needed, but the docs don't seem to have much to say about using them in this manner.
Update: I've added the following to my Game class:
def __init__(self, *args, **kwargs):
super(Game, self).__init__(*args, **kwargs)
self.old_state = self.state
def save(self, force_insert=False, force_update=False):
if self.old_state == 'S' and self.state == 'A':
self.started = datetime.datetime.now()
super(Game, self).save(force_insert, force_update)
self.old_state = self.state
It has been answered, but here's an example of using signals, post_init and post_save.
from django.db.models.signals import post_save, post_init
class MyModel(models.Model):
state = models.IntegerField()
previous_state = None
#staticmethod
def post_save(sender, instance, created, **kwargs):
if instance.previous_state != instance.state or created:
do_something_with_state_change()
#staticmethod
def remember_state(sender, instance, **kwargs):
instance.previous_state = instance.state
post_save.connect(MyModel.post_save, sender=MyModel)
post_init.connect(MyModel.remember_state, sender=MyModel)
Basically, you need to override the save method, check if the state field was changed, set started if needed and then let the model base class finish persisting to the database.
The tricky part is figuring out if the field was changed. Check out the mixins and other solutions in this question to help you out with this:
Dirty fields in django
Django has a nifty feature called signals, which are effectively triggers that are set off at specific times:
Before/after a model's save method is called
Before/after a model's delete method is called
Before/after an HTTP request is made
Read the docs for full info, but all you need to do is create a receiver function and register it as a signal. This is usually done in models.py.
from django.core.signals import request_finished
def my_callback(sender, **kwargs):
print "Request finished!"
request_finished.connect(my_callback)
Simple, eh?
One way is to add a setter for the state. It's just a normal method, nothing special.
class Game(models.Model):
# ... other code
def set_state(self, newstate):
if self.state != newstate:
oldstate = self.state
self.state = newstate
if oldstate == 'S' and newstate == 'A':
self.started = datetime.now()
# create units, etc.
Update: If you want this to be triggered whenever a change is made to a model instance, you can (instead of set_state above) use a __setattr__ method in Game which is something like this:
def __setattr__(self, name, value):
if name != "state":
object.__setattr__(self, name, value)
else:
if self.state != value:
oldstate = self.state
object.__setattr__(self, name, value) # use base class setter
if oldstate == 'S' and value == 'A':
self.started = datetime.now()
# create units, etc.
Note that you wouldn't especially find this in the Django docs, as it (__setattr__) is a standard Python feature, documented here, and is not Django-specific.
note: Don't know about versions of django older than 1.2, but this code using __setattr__ won't work, it'll fail just after the second if, when trying to access self.state.
I tried something similar, and I tried to fix this problem by forcing the initialization of state (first in __init__ then ) in __new__ but this will lead to nasty unexpected behaviour.
I'm editing instead of commenting for obvious reasons, also: I'm not deleting this piece of code since maybe it could work with older (or future?) versions of django, and there may be another workaround to the self.state problem that i'm unaware of
#dcramer came up with a more elegant solution (in my opinion) for this issue.
https://gist.github.com/730765
from django.db.models.signals import post_init
def track_data(*fields):
"""
Tracks property changes on a model instance.
The changed list of properties is refreshed on model initialization
and save.
>>> #track_data('name')
>>> class Post(models.Model):
>>> name = models.CharField(...)
>>>
>>> #classmethod
>>> def post_save(cls, sender, instance, created, **kwargs):
>>> if instance.has_changed('name'):
>>> print "Hooray!"
"""
UNSAVED = dict()
def _store(self):
"Updates a local copy of attributes values"
if self.id:
self.__data = dict((f, getattr(self, f)) for f in fields)
else:
self.__data = UNSAVED
def inner(cls):
# contains a local copy of the previous values of attributes
cls.__data = {}
def has_changed(self, field):
"Returns ``True`` if ``field`` has changed since initialization."
if self.__data is UNSAVED:
return False
return self.__data.get(field) != getattr(self, field)
cls.has_changed = has_changed
def old_value(self, field):
"Returns the previous value of ``field``"
return self.__data.get(field)
cls.old_value = old_value
def whats_changed(self):
"Returns a list of changed attributes."
changed = {}
if self.__data is UNSAVED:
return changed
for k, v in self.__data.iteritems():
if v != getattr(self, k):
changed[k] = v
return changed
cls.whats_changed = whats_changed
# Ensure we are updating local attributes on model init
def _post_init(sender, instance, **kwargs):
_store(instance)
post_init.connect(_post_init, sender=cls, weak=False)
# Ensure we are updating local attributes on model save
def save(self, *args, **kwargs):
save._original(self, *args, **kwargs)
_store(self)
save._original = cls.save
cls.save = save
return cls
return inner
My solution is to put the following code to app's __init__.py:
from django.db.models import signals
from django.dispatch import receiver
#receiver(signals.pre_save)
def models_pre_save(sender, instance, **_):
if not sender.__module__.startswith('myproj.myapp.models'):
# ignore models of other apps
return
if instance.pk:
old = sender.objects.get(pk=instance.pk)
fields = sender._meta.local_fields
for field in fields:
try:
func = getattr(sender, field.name + '_changed', None) # class function or static function
if func and callable(func) and getattr(old, field.name, None) != getattr(instance, field.name, None):
# field has changed
func(old, instance)
except:
pass
and add <field_name>_changed static method to my model class:
class Product(models.Model):
sold = models.BooleanField(default=False, verbose_name=_('Product|sold'))
sold_dt = models.DateTimeField(null=True, blank=True, verbose_name=_('Product|sold datetime'))
#staticmethod
def sold_changed(old_obj, new_obj):
if new_obj.sold is True:
new_obj.sold_dt = timezone.now()
else:
new_obj.sold_dt = None
then the sold_dt field will change when sold field changes.
Any changes of any field defined in the model will trigger the <field_name>_changed method, with old and new object as parameters.
Using Dirty to detect changes and over-writing save method
dirty field
My prev ans: Actions triggered by field change in Django
class Game(DirtyFieldsMixin, models.Model):
STATE_CHOICES = (
('S', 'Setup'),
('A', 'Active'),
('P', 'Paused'),
('F', 'Finished')
)
state = models.CharField(max_length=1, choices=STATE_CHOICES, default='S')
def save(self, *args, **kwargs):
if self.is_dirty():
dirty_fields = self.get_dirty_fields()
if 'state' in dirty_fields:
Do_some_action()
super().save(*args, **kwargs)
If you use PostgreSQL you can create a trigger:
https://www.postgresql.org/docs/current/sql-createtrigger.html
Example:
CREATE TRIGGER check_update
BEFORE UPDATE ON accounts
FOR EACH ROW
WHEN (OLD.balance IS DISTINCT FROM NEW.balance)
EXECUTE FUNCTION check_account_update();