How to update an m2m field programmatically in django signals - python

I'm trying to automatically update an m2m field within my model class. It's an observe-execute if true type, meaning if my model instance has a certain attribute, assign it to this group or that group.
The model class looks like this:
class User(AbstractBaseUser, PermissionsMixin):
is_worker = models.BooleanField(_('Worker'), default=False, help_text='register as a worker')
is_client = models.BooleanField(_('Client'), default=False, help_text='register as a client')
The signal looks like this:
#receiver(post_save, sender=User)
def assign_user_to_group(sender, instance, **kwargs):
if instance.is_client:
instance.groups.add([g.pk for g in Group.objects.filter(name='clients')][0])
elif instance.is_worker:
instance.groups.add([g.pk for g in Group.objects.filter(name='workers')][0])
Usually, I may need to call the save() method on my instance, but the docs states otherwise, plus defying that just gets me a
RecursionError.
Could you please suggest a cleaner approach that best solves this? Thank you.
EDIT
I ended up extending the model's save() method like this:
from django.db import transaction
def save(self, force_insert=False, force_update=False, *args, **kwargs):
instance = super(User, self).save(force_insert, force_update, *args, **kwargs)
transaction.on_commit(self.update_user_group)
return instance
def update_user_group(self):
if self.is_worker:
self.groups.set([g.pk for g in Group.objects.filter(name='workers')])
elif self.is_client:
self.groups.set([g.pk for g in Group.objects.filter(name='clients')])

What is going wrong with your current approach? Using .add() on a many-to-many relation should be sufficient to update the relation without then calling .save()
You're also doing an unnecessary list-comprehension. It is sufficient to do Group.objects.filter(name='workers')[0] or Group.objects.filter(name='workers').first()

Related

How to tell if a model instance is new or not when using UUIDField as a Primary Key

I have a model that requires some post-processing (I generate an MD5 of the body field).
models.py
class MyModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
body = models.TextField()
md5 = models.CharField(max_length=32)
...
def save(self, *args, **kwargs):
if self.pk is None: # Only for create (edit disabled)
self.md5 = get_md5(self.body)
super(MyModel, self).save(*args, **kwargs)
The problem is that the final block won't execute because I don't see a way to check if the instance is new or not: self.pk is never None because a UUID is populated before saving.
I'd like to know what the best practice is for handling this.
Thanks in advance.
Update:
The only solution I can think of is to call the database directly and:
Check if the id exists
Compare the modified and created fields to tell if it's an edit
EDIT
self.pk is never None because a UUID is populated before saving.
Instead of setting a default for id, use a method to set id for the new instance.
class MyModel(...):
id = models.UUIDField(primary_key=True, default=None,...)
def set_pk(self):
self.pk = uuid.uuid4()
def save(self, *args, **kwargs):
if self.pk is None:
self.set_pk()
self.md5 = get_md5(self.body)
super(MyModel, self).save(*args, **kwargs)
Looks like the cleanest approach to this is to make sure that all your models have a created date on them by inheriting from an Abstract model, then you simply check if created has a value:
models.py
class BaseModel(models.Model):
"""
Base model which all other models can inherit from.
"""
id = fields.CustomUUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Meta:
# Abstract models are not created in the DB
abstract = True
class MyModel(BaseModel):
my_field = models.CharField(max_length=50)
def save(self, *args, **kwargs):
if self.created:
# Do stuff
pass
super(MyModel, self).save(*args, **kwargs)
I just got the same issue in my project and found out that you can check the internal state of the model's instance:
def save(self, *args, **kwargs):
if self._state.adding: # Only for create (edit disabled)
self.md5 = get_md5(self.body)
super(MyModel, self).save(*args, **kwargs)
But this solution relies on internal implementation and may stop working after Django is updated
As I've answered here as well, the cleanest solution I've found that doesn't require any additional datetime fields or similar tinkering is to plug into the Django's post_save signal. Add this to your models.py:
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=MyModel)
def mymodel_saved(sender, instance, created, **kwargs):
if created:
# do extra work on your instance
self.md5 = get_md5(self.body)
This callback will block the save method, so you can do things like trigger notifications or update the model further before your response is sent back over the wire, whether you're using forms or the Django REST framework for AJAX calls. Of course, use responsibly and offload heavy tasks to a job queue instead of keeping your users waiting :)

How to set True as default value for BooleanField on Django?

I am using BooleanField in Django.
By default, the checkbox generated by it is unchecked state. I want the state to be checked by default. How do I do that?
If you're just using a vanilla form (not a ModelForm), you can set a Field initial value ( https://docs.djangoproject.com/en/2.2/ref/forms/fields/#django.forms.Field.initial ) like
class MyForm(forms.Form):
my_field = forms.BooleanField(initial=True)
If you're using a ModelForm, you can set a default value on the model field ( https://docs.djangoproject.com/en/2.2/ref/models/fields/#default ), which will apply to the resulting ModelForm, like
class MyModel(models.Model):
my_field = models.BooleanField(default=True)
Finally, if you want to dynamically choose at runtime whether or not your field will be selected by default, you can use the initial parameter to the form when you initialize it:
form = MyForm(initial={'my_field':True})
from django.db import models
class Foo(models.Model):
any_field = models.BooleanField(default=True)
I am using django==1.11. The answer get the most vote is actually wrong. Checking the document from django, it says:
initial -- A value to use in this Field's initial display. This value
is not used as a fallback if data isn't given.
And if you dig into the code of form validation process, you will find that, for each fields, form will call it's widget's value_from_datadict to get actual value, so this is the place where we can inject default value.
To do this for BooleanField, we can inherit from CheckboxInput, override default value_from_datadict and init function.
class CheckboxInput(forms.CheckboxInput):
def __init__(self, default=False, *args, **kwargs):
super(CheckboxInput, self).__init__(*args, **kwargs)
self.default = default
def value_from_datadict(self, data, files, name):
if name not in data:
return self.default
return super(CheckboxInput, self).value_from_datadict(data, files, name)
Then use this widget when creating BooleanField.
class ExampleForm(forms.Form):
bool_field = forms.BooleanField(widget=CheckboxInput(default=True), required=False)
In Django 3.0 the default value of a BooleanField in model.py is set like this:
class model_name(models.Model):
example_name = models.BooleanField(default=False)
I found the cleanest way of doing it is this.
Tested on Django 3.1.5
class MyForm(forms.Form):
my_boolean = forms.BooleanField(required=False, initial=True)
I found the answer here
Another way to check the default state in BooleanField is:
active = forms.BooleanField(
widget=forms.CheckboxInput(
attrs={
'checked': True
}
)
)
Both initial and default properties were not working for me, if that's your case try this:
class MyForm(forms.ModelForm):
validated = forms.BooleanField()
class Meta:
model = MyModel
fields = '__all__'
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['validated'].widget.attrs['checked'] = True
I tried to change inital of BooleanField:
def __init__(self, *args, **kwargs):
super(UserConfirmForm,self).__init__(*args, **kwargs)
self.fields['bool_field'].initial = True
but it didn't work.
My solve:
def __init__(self, *args, **kwargs):
kwargs['initial'] = {'bool_field': True}
super(UserConfirmForm,self).__init__(*args, **kwargs)
It works like:
UserConfirmForm(initial={'bool_field':True})
but we can't call form in Generic editing views.
I think this is a great alternative to a regular call form object.

post_save in django to update instance immediately

I'm trying to immediately update a record after it's saved. This example may seem pointless but imagine we need to use an API after the data is saved to get some extra info and update the record:
def my_handler(sender, instance=False, **kwargs):
t = Test.objects.filter(id=instance.id)
t.blah = 'hello'
t.save()
class Test(models.Model):
title = models.CharField('title', max_length=200)
blah = models.CharField('blah', max_length=200)
post_save.connect(my_handler, sender=Test)
So the 'extra' field is supposed to be set to 'hello' after each save. Correct? But it's not working.
Any ideas?
When you find yourself using a post_save signal to update an object of the sender class, chances are you should be overriding the save method instead. In your case, the model definition would look like:
class Test(models.Model):
title = models.CharField('title', max_length=200)
blah = models.CharField('blah', max_length=200)
def save(self, force_insert=False, force_update=False):
if not self.blah:
self.blah = 'hello'
super(Test, self).save(force_insert, force_update)
Doesn't the post_save handler take the instance? Why are you filtering using it? Why not just do:
def my_handler(sender, instance=False, created, **kwargs):
if created:
instance.blah = 'hello'
instance.save()
Your existing code doesn't work because it loops, and Test.objects.filter(id=instance.id) returns a query set, not an object. To get a single object directly, use Queryset.get(). But you don't need to do that here. The created argument keeps it from looping, as it only sets it the first time.
In general, unless you absolutely need to be using post_save signals, you should be overriding your object's save() method anyway.

Creating modelformset from a modelform

I have a model MyModel which contains a PK - locid, that is an AutoField.
I want to construct a model formset from this, with some caveats:
The queryset for the formset should be a custom one (say, order_by('field')) rather than all()
Since locid for MyModel is an AutoField and thus hidden by default, I want to be able to show it to the user.
I'm not sure how to do this. I've tried multiple approaches,
MyModelFormSet = modelformset_factory(MyModel, fields=('locid', 'name', 'dupof'))
The above gives me the 3 fields, but locid is hidden.
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['locid'].widget.attrs["type"] = 'visible'
locid = forms.IntegerField(min_value = 1, required=True)
class Meta:
model = MyModel
fields = ('locid', 'name', 'dupof')
The above gives me a ManyToMany error.
Has anyone done something like this before?
Edit 2
I can now use a custom query when I instantiate the formset - but I still need to show the locid field to the user, because the id is important for the application's use. How would I do this? Is there a way to override the default behavior of hiding a PK if its an autofield?
It makes no sense to show an autofield to the user, as it's an autoincremented primary key -- the user can not change it and it will not be available before saving the record to the database (where the DBMS selectes the next available id).
This is how you set a custom queryset for a formset:
from django.forms.models import BaseModelFormSet
class OrderedFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
self.queryset = MyModel.objects.order_by("field")
super(OrderedFormSet, self).__init__(*args, **kwargs)
and then you use that formset in the factory function:
MyModelFormSet = modelformset_factory(MyModel, formset=OrderedFormSet)
I ended up using a template side variable to do this, as I mentioned here:
How to show hidden autofield in django formset
If you like cheap workarounds, why not mangle the locid into the __unicode__ method? The user is guaranteed to see it, and no special knowledge of django-admin is required.
But, to be fair, all my answers to django-admin related questions tend along the lines of "don't strain to hard to make django-admin into an all-purpose CRUD interface".

Changing case (upper/lower) on adding data through Django admin site

I'm configuring the admin site of my new project, and I have a little doubt on how should I do for, on hitting 'Save' when adding data through the admin site, everything is converted to upper case...
Edit: Ok I know the .upper property, and I I did a view, I would know how to do it, but I'm wondering if there is any property available for the field configuration on the admin site :P
If your goal is to only have things converted to upper case when saving in the admin section, you'll want to create a form with custom validation to make the case change:
class MyArticleAdminForm(forms.ModelForm):
class Meta:
model = Article
def clean_name(self):
return self.cleaned_data["name"].upper()
If your goal is to always have the value in uppercase, then you should override save in the model field:
class Blog(models.Model):
name = models.CharField(max_length=100)
def save(self, force_insert=False, force_update=False):
self.name = self.name.upper()
super(Blog, self).save(force_insert, force_update)
Updated example from documentation suggests using args, kwargs to pass through as:
Django will, from time to time, extend the capabilities of built-in
model methods, adding new arguments. If you use *args, **kwargs in
your method definitions, you are guaranteed that your code will
automatically support those arguments when they are added.
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
def save(self, *args, **kwargs):
do_something()
super(Blog, self).save( *args, **kwargs) # Call the "real" save() method.
do_something_else()
you have to override save(). An example from the documentation:
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
def save(self, force_insert=False, force_update=False):
do_something()
super(Blog, self).save(force_insert, force_update) # Call the "real" save() method.
do_something_else()

Categories

Resources