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();
Related
I've written a custom editable table with columns subclassing from django_tables2.Column, but keep struggling with rendering a select tag in my custom column. Considering the model:
myapp/models.py
from django.db import models
from myapp.utils.enums import MyModelChoices
class MyModel(models.Model):
bound_model = models.ForeignKey(
SomeOtherModel,
related_name='bound_model'
)
used_as = models.CharField(
max_length=50,
blank=True,
null=True,
choices=MyModelChoices.choices()
)
and my enum in myapp/utils/enums.py:
class MyModelChoices:
__metaclass__ = EnumMeta # Logic irrelevant
First = 'First',
Second = 'Second',
Third = 'Third'
I end up with custom column like this:
import django_tables2 as tables
from django.forms import ChoiceField
class ChoicesColumn(tables.Column):
def __init__(self, choices, attrs=None, **extra):
self.choices = choices
kwargs = {'orderable': False, 'attrs': attrs}
kwargs.update(extra)
super(ChoicesColumn, self).__init__(**kwargs)
def render(self, value, bound_column):
select = ChoiceField(choices=self.choices)
return select.widget.render(
bound_column.name,
self.label_to_value(value)
)
def label_to_value(self, label):
for (v, l) in self.choices:
if l == label:
return v
which is later called in my table class like this:
import django_tables2 as tables
from myapp.models import MyModel
from myapp.tables.utils import ChoicesColumn
class MyTable(tables.Table):
name = tables.Column()
used_as = ChoicesColumn(
choices=lambda record: record.used_as.choices()
)
def render_name(self, record):
return record.bound_model.name
class Meta:
model = MyModel
fields = ('name', 'used_as',)
but still there's rendered just a plain <td></td> with text instead of select field. What am I doing wrong in this situation? I'm using Python 2.7, Django 1.8 and django-tables2 1.16.0. Thanks in advance for your advice!
UPDATE
I changed my custom column class like this:
class ChoicesColumn(tables.Column):
def __init__(self, attrs=None, **extra):
kwargs = {'orderable': False, 'attrs': attrs}
kwargs.update(extra)
super(ChoicesColumn, self).__init__(**kwargs)
def render(self, value, bound_column):
options = [self.render_option(c) for c in value]
html_template = '''
<select name={}>{}</select>
'''.format(bound_column.name, options)
return mark_safe(html_template)
def render_option(self, choice):
return '<option value={0}>{0}</option>'.format(choice)
and added a render_options method according to this paragraph in documentation:
class MyTable(tables.Table):
name = tables.Column(accessor='pk')
# With or without accessor it doesn't work neither way
used_as = ChoicesColumn(accessor='used_as')
def render_name(self, record):
return record.bound_model.name
def render_used_as(self, record):
return record.used_as.choices()
class Meta:
model = MyModel,
fields = ('name', 'options',)
but this method isn't even executed on render, what I've spotted while debugging, though the method before it executes when I reload the page and renders data correctly. Is that because name column uses the library class, and options column uses custom class inherited from it? If so, what is my subclass missing?
ANOTHER UPDATE
I figured out what was the previous problem with choices, though it didn't solve the problem :( The thing was that I was passing model instance's field used_as, which was set to None, thus it would never populate the ChoiceField. So, I rolled back my custom column class to the initial variant, and in my table class instead of
used_as = ChoicesColumn(
choices=lambda record: record.used_as.choices()
)
I imported MyModelChoices enum and used it instead of model instance
used_as = ChoicesColumn(choices=MyModelChoices.choices())
and now I see the options passing to constructor, though the render method isn't still called for some mysterious reason =/
LAST UPDATE AS FOR NOW
As for the current moment my custom column and table look like this:
class ChoicesColumn(tables.Column):
def __init__(self, choices, attrs=None, **extra)
self.choices = choices
self.choices.insert(0, ('', '------'))
kwargs = {'orderable': False, 'attrs': attrs}
kwargs.update(extra)
super(ChoicesColumn, self).__init__(**kwargs)
def render(self, value, bound_column):
select = forms.ChoiceField(choices=self.choices)
return select.widget.render(bound_column.name, value)
class MyTable(tables.Table):
name = tables.Column(accessor='pk')
used_as = ChoiceColumn(UsedAs.choices(), accessor='used_as')
def render_name(self, record):
return record.bound_model.name
def render_used_as(self, record):
if record.used_as is None:
return ''
return record.used_as
class Meta:
model = MyModel
fields = ('name', 'used_as')
The ChoiceColumn render method and the corresponding method in table class are never called on rendering stage (unlike the other columns), and I completely give up. Please, be merciful enough either to shoot me or tell me where exactly I'm an idiot :)
So, as I accidentally found out, the problem was in accessor attribute – when changed from
used_as = ChoiceColumn(UsedAs.choices(), accessor='used_as')
to
used_as = ChoiceColumn(UsedAs.choices(), accessor='pk')
it finally rendered. I don't understand why that happened and would be very grateful if someone explained that to me.
There is an easier way:
If you have a Enum column (say used_as), you can change the renderer so that it displays the value (instead of the name). Place this in the Table definition (in class MyTable(tables.Table) ).
def render_used_as(self,value):
v = value.split(".")[1]
members = MyModelChoices.__members__
return (members[v].value)
Note that I was using a bit of a different syntax for the Enum
from enum import Enum
Class MyModelChoices(Enum):
First = 'First'
Second = 'Second'
Third = 'Third'
Note: render_used_as is render_%s with %s = variable name
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)
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().
How can I add a composed default value to a charfield?
Example
class Myclass(xxx):
type = models.ForeignKey(somewhere)
code = models.CharField(default=("current id of MyClass wich is autoincremented + type value"))
Is it possible?
To do so, you override the save method on your model.
class MyClass(models.Model):
...
def save(self):
super(Myclass,self).save()
if not self.code:
self.code = str(self.id) + str(self.type_id)
self.save()
There is stuff you need to take care, like making the code a blank field, but you get the idea.
You should override the save method as Lakshman suggest, however, since this is the default and not blank=False, the code should be a little different:
Class MyClass(models.Model):
...
def save(self):
if not self.id:
self.code = str(self.id) + str(self.type_id)
return super(Myclass,self).save())
You could also use the post_save signal
from django.db.models import signals
class MyClass(models.Model):
type = models.ForeignKey(somewhere)
code = models.CharField(blank=True)
def set_code_post(instance, created, **kwargs):
instance.code = str(instance.id) + str(instance.type_id)
instance.save()
signals.post_save.connect(set_code_post, sender=MyClass)
Or, for that matter, you could use a combination of pre_save and post_save signals to avoid running save() twice...
from django.db.models import signals
class MyClass(models.Model):
type = models.ForeignKey(somewhere)
code = models.CharField(blank=True)
def set_code_pre(instance, **kwargs):
if hasattr(instance, 'id'):
instance.code = str(instance.id) + str(instance.type_id)
def set_code_post(instance, created, **kwargs):
if created:
instance.code = str(instance.id) + str(instance.type_id)
instance.save()
signals.pre_save.connect(set_code_pre, sender=MyClass)
signals.post_save.connect(set_code_post, sender=MyClass)
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)