django save_model() error creating a new object with m2m fields - django

I have a model models.py:
class MyModelClass(models.Model):
name = models.CharField(max_length=255)
m2m_1 = models.ManyToManyField(A, blank=True, null=True)
m2m_2 = models.ManyToManyField(B, blank=True, null=True)
fk = models.ForeignKey('C')
int = models.IntegerField()
and an admin.py class:
class MyModelClassAdmin(admin.ModelAdmin):
list_display = ('name',)
#Get all fields selected in MyModelClass m2m_2
def get_selected_in_m2m_2(self, obj):
sel = obj.m2m_2.all() #This line is the one for i get an error. The error is described below.
return sel
def save_model(self, request, obj, form, change):
"""When creating a new object, set the creator field.
"""
m2m_2_selected = self.get_selected_in_m2m_2(obj)
print m2m_2_selected
print request.user
if not change:
obj.creator = request.user
obj.save()
The problem:
Everything works if i click "Save" button on already existing MyModelClass's object in my admin-page.
But if i try to create a new model object in admin-page and click the "Save" button(to save a new object, not to update existing one), i'll get an error:'MyModelClass' instance needs to have a primary key value before a many-to-many relationship can be used.
Sorry for my English.
Any help is appreciated.
Thanks.

def save_model(self, request, obj, form, change):
"""When creating a new object, set the creator field.
"""
if not change:
obj.creator = request.user
super(MyModelClassAdmin, self).save_model(request, obj, form, change)
m2m_2_selected = self.get_selected_in_m2m_2(obj)
print m2m_2_selected
print request.user

Very common problem. You haven't got object, so you can't set m2m relationship. Try to use super function (I'm sorry, can't remember proper usage of it, but you'll easly find it in docs) to create this object (well, process pure function before chages) and then modify it and save.

Related

Store who updated Django Model from admin

I have a use case where data is only inserted and updated from django admin. Now, I have multiple users who have access to django admin page. I want to be able to store who exactly updated or created a record in django admin page.
Ideally, I want to add a separate column to an existing model.
models.py
class Links(models.Model):
link = models.URLField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
created_by = model.ForeignKey(UserModel)
updated_at = models.DateTimeField(auto_now=True)
updated_by = model.ForeignKey(UserModel)
You can override the .save_model(…) method [Django-doc] to set the .updated_by and .created_by fields, depending on whether change is True or False:
from django.contrib import admin
class LinkAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
if not change:
obj.created_by = request.user
obj.updated_by = request.user
return super().save_model(request, obj, form, change)
admin.site.register(Links, LinkAdmin)
If you need this for a large number of models, you can make a mixin, and then use that for all sorts of models:
class CreateUpdateUserAdminMixin:
def save_model(self, request, obj, form, change):
if not change:
obj.created_by = request.user
obj.updated_by = request.user
return super().save_model(request, obj, form, change)
and then use the mixin with:
class LinkAdmin(CreateUpdateUserAdminMixin, admin.ModelAdmin):
pass
class OtherModelAdmin(CreateUpdateUserAdminMixin, admin.ModelAdmin):
pass
admin.site.register(Links, LinkAdmin)
admin.site.register(OtherModel, OtherModelAdmin)
Note: normally a Django model is given a singular name, so Link instead of Links.

Django Admin - Detect changes in inline fields

I am pretty new to python. I have Models named Project and ProjectTopic.
class Project(models.Model):
name = models.CharField(max_length=250)
description = models.TextField()
class ProjectTopic(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
title = models.CharField(max_length=250)
In the admin.py, I have added the ProjectTopic as an inline field. The admin can add as many topics that he likes.
Now the requirement is I need to send a notification if there is any change in the project that includes the ProjectTopic section. I have added
def save_model(self, request, obj, form, change):
if 'description' in form.changed_data:
send_notification()
super().save_model(request, obj, form, change)
But don't know how to track the changes in the inline ProjectTopic section.
I have been looking into getting a proper solution to this as well. In case you are not strictly looking to access the changed inline fields from within the Project admin's save_model() method, I can suggest three solutions. Even though it might not be the best solution, you can get your job done.
Method 1
You can use the save_related() method as follows
def save_related(self, request, form, formsets, change):
if form.changed_data:
# Call function to notify change in Project fields
send_project_notification()
for form_set in formsets:
if form_set.has_changed():
# Call function to notify change in ProjectTopic fields
send_project_topic_notification()
super().save_related(request, form, formsets, change)
Method 2
It would require you to use the Field Tracker using which you can track changes in model fields. Add the project_topic_tracker to your model and use a post_save signal to track the changes as follows.
models.py
class ProjectTopic(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
title = models.CharField(max_length=250)
project_topic_tracker = FieldTracker()
signals.py
#receiver(post_save, sender=ProjectTopic)
def notify_project_topic_change(sender, **kwargs):
if 'instance' in kwargs:
instance = kwargs['instance']
if(
hasattr(instance, 'project_topic_tracker') and
instance.project_topic_tracker.changed()
):
changed_list = list(instance.project_topic_tracker.changed().keys())
if changed_list:
# Call your notification function
send_project_topic_notification()
Method 3
You can use a form ProjectTopicForm from whose clean() method you can access the changed data.
forms.py
class ProjectTopicForm(forms.ModelForm):
class Meta:
model = ProjectTopic
fields = '__all__'
def clean(self):
if self.changed_data:
send_project_topic_notification()
I have a similar issue here for which I have been looking for work arounds

Django - Setting default owner as the logged in user

This is a bump post. I have tried various ways and went through all examples here:
https://stackoverflow.com/a/4672123/6274043
https://stackoverflow.com/a/5529770/6274043
How do you specify a default for a Django ForeignKey Model or AdminModel field?
and all doesn't seem to work for me.
# models.py in main/ app
from django.contrib.auth.models import User
class Mileage(models.Model):
owner = models.ForeignKey(User)
date = models.DateField()
... #other parameters
#admin.py
class MileageAdmin(admin.ModelAdmin):
list_display = ['date', ...]
def save_model(self, request, instance, form, change):
user = request.user
instance = form.save(commit=False)
instance.owner= user
instance.save()
form.save_m2m()
return instance
#views.py
def home(request):
form = MileageForm(request.POST or None)
context = {"form": form}
if form.is_valid():
instance = form.save()
form = MileageForm()
context = {"form": form}
return render(request, 'record_trip.html', context)
I am trying to set the default owner as the logged in user. I tried doing it in ModelAdmin like how other posts do but doesn't seem to work. Can any kind soul point out where am I doing it wrong?
It keeps throwing IntegrityError owner_id cannot be null.
Please pardon me if I made elementary mistakes above. I started coding half a year ago for leisure.
Not sure if I understood the question correctly. You are trying to set the logged-in user as the default owner of a newly created model?
Currently, you are only setting created_by and modified_by to default to the logged-in user.
#admin.py
class MileageAdmin(admin.ModelAdmin):
...
def save_model(self, request, instance, form, change):
user = request.user
instance = form.save(commit=False)
if not change or not instance.created_by:
instance.created_by = user
instance.modified_by = user
instance.save()
form.save_m2m()
return instance
But those two fields do not even exist in you example model
class Mileage(models.Model):
owner = models.ForeignKey(User)
date = models.DateField()
... #other parameters
However, you are never actually setting instance.owner to be the request.user. So instance.owner remains to be None, that's why Django complains.
You need to set it before save()ing your instance.
class MileageAdmin(admin.ModelAdmin):
...
def save_model(self, request, instance, form, change):
instance.owner = request.user
if not change or not instance.created_by:
instance.created_by = request.user
instance.modified_by = request.user
instance.save()
form.save_m2m()
return instance
It sounds like the model's save method is getting called somewhere before you've had a chance to set it to the request.user.
You could either find out where that's happening (and perhaps other answers might have a better idea of where that's happening?) or if you wanted a shortcut, you could set the owner attribute to not be a required field, trusting that you'll set it later.
owner = models.ForeignKey(User, blank=True, null=True)
That way, creating the model without the owner wouldn't throw up an integrity error, but you could trust that the owner attribute would eventually be set.

Automatically fill a field with the name of the user that creates the object?

I have a model, Package:
class Package(models.Model):
VIP = models.BooleanField()
name = models.CharField(max_length=200)
contents = models.CharField(max_length=200)
owner = # username that created this object
Whenever a user adds a new Package (through admin), I want that the owner to contain the name of this user. How can I do this?
If you want to hide the owner field in the admin, then exclude the owner field from the model admin, and set the owner in the save_model method.
class PackageAdmin(admin.ModelAdmin):
exclude = ('owner',)
def save_model(self, request, obj, form, change):
if not change:
# only set owner when object is first created
obj.owner = request.user
obj.save()
If you want to keep the form's owner field, then override get_changeform_initial_data, and add the owner as an initial value.
class PackageAdmin(admin.ModelAdmin):
def get_changeform_initial_data(self, request):
return {'owner': request.user}
The code above assumes that owner is a foreign key to the user model, which is the approach I recommend. If you really want to store the username as a string, then you need to change the lines above to:
obj.owner = request.user.username
return {'owner': request.user.username}

Django, adding excluded properties to the submitted modelform

I've a modelform and I excluded two fields, the create_date and the created_by fields. Now I get the "Not Null" error when using the save() method because the created_by is empty.
I've tried to add the user id to the form before the save() method like this: form.cleaned_data['created_by'] = 1 and form.cleaned_data['created_by_id'] = 1. But none of this works.
Can someone explain to me how I can 'add' additional stuff to the submitted modelform so that it will save?
class Location(models.Model):
name = models.CharField(max_length = 100)
created_by = models.ForeignKey(User)
create_date = models.DateTimeField(auto_now=True)
class LocationForm(forms.ModelForm):
class Meta:
model = Location
exclude = ('created_by', 'create_date', )
Since you have excluded the fields created_by and create_date in your form, trying to assign them through form.cleaned_data does not make any sense.
Here is what you can do:
If you have a view, you can simply use form.save(commit=False) and then set the value of created_by
def my_view(request):
if request.method == "POST":
form = LocationForm(request.POST)
if form.is_valid():
obj = form.save(commit=False)
obj.created_by = request.user
obj.save()
...
...
`
If you are using the Admin, you can override the save_model() method to get the desired result.
class LocationAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.created_by = request.user
obj.save()
Pass a user as a parameter to form constructor, then use it to set created_by field of a model instance:
def add_location(request):
...
form = LocationForm(user=request.user)
...
class LocationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(forms.ModelForm, self).__init__(*args, **kwargs)
self.instance.created_by = user
The correct solution is to pass an instance of the object with pre-filled fields to the model form's constructor. That way the fields will be populated at validation time. Assigning values after form.save() may result in validation errors if fields are required.
LocationForm(request.POST or None, instance=Location(
created_by=request.user,
create_date=datetime.now(),
))
Notice that instance is an unsaved object, so the id will not be assigned until form saves it.
One way to do this is by using form.save(commit=False) (doc)
That will return an object instance of the model class without committing it to the database.
So, your processing might look something like this:
form = some_form(request.POST)
location = form.save(commit=False)
user = User(pk=1)
location.created_by = user
location.create_date = datetime.now()
location.save()