I have a model that has a user field that needs to be auto-populated from the currently logged in user. I can get it working as specified here if the user field is in a standard ModalAdmin, but if the model I'm working with is in an InlineModelAdmin and being saved from the record of another model inside the Admin, it won't take.
Here's what I think is the best solution. Took me a while to find it... this answer gave me the clues: https://stackoverflow.com/a/24462173/2453104
On your admin.py:
class YourInline(admin.TabularInline):
model = YourInlineModel
formset = YourInlineFormset
def get_formset(self, request, obj=None, **kwargs):
formset = super(YourInline, self).get_formset(request, obj, **kwargs)
formset.request = request
return formset
On your forms.py:
class YourInlineFormset(forms.models.BaseInlineFormSet):
def save_new(self, form, commit=True):
obj = super(YourInlineFormset, self).save_new(form, commit=False)
# here you can add anything you need from the request
obj.user = self.request.user
if commit:
obj.save()
return obj
I know I'm late to the party, but here's my situation and what I came up with, which might be useful to someone else in the future.
I have 4 inline models that need the currently logged in user.
2 as a created_by type field. (set once on creation)
and the 2 others as a closed_by type field. (only set on condition)
I used the answer provided by rafadev and made it into a simple mixin which enables me to specify the user field name elsewhere.
The generic formset in forms.py
from django.forms.models import BaseInlineFormSet
class SetCurrentUserFormset(forms.models.BaseInlineFormSet):
"""
This assume you're setting the 'request' and 'user_field' properties
before using this formset.
"""
def save_new(self, form, commit=True):
"""
This is called when a new instance is being created.
"""
obj = super(SetCurrentUserFormset, self).save_new(form, commit=False)
setattr(obj, self.user_field, self.request.user)
if commit:
obj.save()
return obj
def save_existing(self, form, instance, commit=True):
"""
This is called when updating an instance.
"""
obj = super(SetCurrentUserFormset, self).save_existing(form, instance, commit=False)
setattr(obj, self.user_field, self.request.user)
if commit:
obj.save()
return obj
Mixin class in your admin.py
class SetCurrentUserFormsetMixin(object):
"""
Use a generic formset which populates the 'user_field' model field
with the currently logged in user.
"""
formset = SetCurrentUserFormset
user_field = "user" # default user field name, override this to fit your model
def get_formset(self, request, obj=None, **kwargs):
formset = super(SetCurrentUserFormsetMixin, self).get_formset(request, obj, **kwargs)
formset.request = request
formset.user_field = self.user_field
return formset
How to use it
class YourModelInline(SetCurrentUserFormsetMixin, admin.TabularInline):
model = YourModel
fields = ['description', 'closing_user', 'closing_date']
readonly_fields = ('closing_user', 'closing_date')
user_field = 'closing_user' # overriding only if necessary
Be careful...
...as this mixin code will set the currently logged in user everytime for every user. If you need the field to be populated only on creation or on specific update, you need to deal with this in your model save method. Here are some examples:
class UserOnlyOnCreationExampleModel(models.Model):
# your fields
created_by = # user field...
comment = ...
def save(self, *args, **kwargs):
if not self.id:
# on creation, let the user field populate
self.date = datetime.today().date()
super(UserOnlyOnCreationExampleModel, self).save(*args, **kwargs)
else:
# on update, remove the user field from the list
super(UserOnlyOnCreationExampleModel, self).save(update_fields=['comment',], *args, **kwargs)
Or if you only need the user if a particular field is set (like boolean field closed) :
def save(self, *args, **kwargs):
if self.closed and self.closing_date is None:
self.closing_date = datetime.today().date()
# let the closing_user field set
elif not self.closed :
self.closing_date = None
self.closing_user = None # unset it otherwise
super(YourOtherModel, self).save(*args, **kwargs) # Call the "real" save() method.
This code could probably be made way more generic as I'm fairly new to python but that's what will be in my project for now.
Only the save_model for the model you're editing is executed, instead you will need to use the post_save signal to update inlined data.
(Not really a duplicate, but essentially the same question is being answered in Do inline model forms emmit post_save signals? (django))
I had a similar issue with a user field I was trying to populate in an inline model. In my case, the parent model also had the user field defined so I overrode save on the child model as follows:
class inline_model:
parent = models.ForeignKey(parent_model)
modified_by = models.ForeignKey(User,editable=False)
def save(self,*args,**kwargs):
self.modified_by = self.parent.modified_by
super(inline_model,self).save(*args,**kwargs)
The user field was originally auto-populated on the parent model by overriding save_model in the ModelAdmin for the parent model and assigning
obj.modified_by = request.user
Keep in mind that if you also have a stand-alone admin page for the child model you will need some other mechanism to keep the parent and child modified_by fields in sync (e.g. you could override save_model on the child ModelAdmin and update/save the modified_by field on the parent before calling save on the child).
I haven't found a good way to handle this if the user is not in the parent model. I don't know how to retrieve request.user using signals (e.g. post_save), but maybe someone else can give more detail on this.
Does the other model save the user? In that case you could use the post_save signal to add that information to the set of the inlined model.
Have you tried implementing custom validation in the admin as it is described in the documentation? Overriding the clean_user() function on the model form might do the trick for you.
Another, more involved option comes to mind. You could override the admin template that renders the change form. Overriding the change form would allow you to build a custom template tag that passes the logged in user to a ModelForm. You could then write a custom init function on the model form that sets the User automatically. This answer provides a good example on how to do that, as does the link on b-list you reference in the question.
Related
I'm using CBV CreateView to display a couple of pages with formsets to the user.
When the model behind a given formset/CreateView is a common one (it will became clearer later), everything works fine using the following logic on the view:
class Create(CreateView):
...
def form_valid(self, formset):
instances = formset.save(commit=False)
for instance in instances:
instance.user = self.request.user
instance.save()
return super(Create, self).form_valid(formset)
However, on one of the models, I had to add extra actions to the model save() method. Namely, I need to create child objects when the parents are saved. Something like:
class Parent(models.Model):
...
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.child_set.create(...., *args, **kwargs)
In this particular case, the child object is being created twice and I believe that the formset.save(commit=False) is the culprit.
I tried replacing the child_set.create() for
child = Child(...parameters, parent=self)
child.save(*args, **kwargs)
But it yields the same result. How can I prevent that?
The .form_valid(…) method [Django-doc] of a CreateView [Django-doc], will call .save() on the form, and this will thus invoke a new round of saving all objects.
You can set the .user of the instances, and then let the CreateView save these instances. This thus means that you implement this as:
class Create(CreateView):
# …
def form_valid(self, formset):
instances = formset.save(commit=False)
for instance in instances:
instance.user = self.request.user
# no instance.save()
# ↓ this will save the instances
return super().form_valid(formset)
That being said, it might be better to work with a .get_or_create(…) [Django-doc] over a .create(…) [Django-doc], since now you will create a Child object each time you save the Parent object, which is likely not the intended effect.
In django forms, for saving other data I usually use form_valid() but as I can also use save() method of formclass.
Today I overrided save() instead of form_valid() and I got problem with my manytomanyfield.
When using , the values of manytomanyfield are not saved but when I use form_valid() they start saving. Can anybody tell me the reason and what are the differences between both, which is the most convenient method and in what situation?
Here is my overriding of save() method:
class ProductCreateForm(forms.ModelForm):
sizes = make_ajax_field(ProductCreateModel,'sizes','sizes')
colours = make_ajax_field(ProductCreateModel,'colours','colours')
class Meta:
model = ProductCreateModel
fields = ('title','category',
'regions',)
def __init__(self,*args,**kwargs):
self.request = kwargs.pop("request")
super(ProductCreateForm, self).__init__(*args, **kwargs)
def save(self):
product = super(ProductCreateForm, self).save(commit=False)
user = self.request.user
product.location = user.user_location
product.save()
return product
When I override form_valid() method:
def get_form_kwargs(self):
kwargs = super(ProductCreateView,self).get_form_kwargs()
kwargs.update({'request':self.request})
return kwargs
def form_valid(self, form):
product = form.save(commit=False)
user = self.request.user
form.instance.user = user
form.instance.location = user.user_location
form.save()
return super(ProductCreateView, self).form_valid(form)
sizes,colours and regions are m2m fields, as I mentioned when I overrides save() values of m2m not get saved but when I overrides form_valid they start saving.
If you save a form with commit=False, you must call the form's save_m2m method to save the many-to-many data. See the docs for more info.
If you decide to use the form_valid method, I would change the following things:
update the instance returned by form.save() and save it, instead of calling form.save() again.
explicitly call form.save_m2m()
return a redirect response instead of calling super().form_valid() (which will save the form again)
Putting that together, you get:
def form_valid(self, form):
product = form.save(commit=False)
product.user = self.request.user
product.location.location = user.user_location
product.save()
form.save_m2m()
return redirect('/success-url/')
About your problem with manytomany i guess is the order they do things... Form > Admin > Models, when you use form_valid is the first thing they do before check other things in chain, while using save is the last, maybe can be because or other things too...
The best way is always use form_valid instead of raw save
form_valid first check the Clean function if there is any native validations errors or custom validations and only then save your models
save just save it without validate then with your form with your validations
Example
from django import forms
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject:
# Only do something if both fields are valid so far.
if "help" not in subject:
raise forms.ValidationError(
"Did not send for 'help' in the subject despite "
"CC'ing yourself."
)
Source: https://docs.djangoproject.com/en/2.0/ref/forms/validation/
My blog post model has a many-to-many field for tags:
tags = models.ManyToManyField(PostTag)
But it was uncomfortable to edit it and I modified my model like this:
def _get_tagging(self): # Returns comma separated list of tags
tagging = []
for tag in self.tags.all():
tagging.append(tag.name)
return ", ".join(tagging)
def _set_tagging (self, tagging): # Saves tags from comma separated list
tagging = tagging.split(", ")
self.tags.clear()
for tag in tagging:
if len(tag) < 1:
continue
try:
self.tags.add(PostTag.objects.get(name=tag))
except ObjectDoesNotExist:
self.tags.create(name=tag)
tagging = property(_get_tagging, _set_tagging)
Then I modified my admin.py:
class BlogAdminForm (forms.ModelForm):
tagging = forms.CharField(required=False, label="Tags", max_length=200,
widget=forms.TextInput(attrs={'class':'vTextField'}))
class Meta:
model = BlogPost
def __init__(self, *args, **kwargs):
super(BlogAdminForm, self).__init__(*args, **kwargs)
if kwargs.has_key('instance'):
instance = kwargs['instance']
self.initial['tagging'] = instance.tagging
def save(self, commit=True):
model = super(BlogAdminForm, self).save(commit=False)
model.tagging = self.cleaned_data["tagging"]
if commit:
model.save()
return model
And this worked fine, but only for editing objects. I got an error when I tried to create a new object. Why? Because a many-to-many relationship can be used with an object that is not yet in the database and doesn't have a primary key ('BlogPost' instance needs to have a primary key value before a many-to-many relationship can be used). I tried to solve it by editing the save method in this way:
def save(self, commit=True):
model = super(BlogAdminForm, self).save(commit=False)
try:
model.tagging = self.cleaned_data["tagging"]
except ValueError:
model.save()
model.tagging = self.cleaned_data["tagging"]
if commit:
model.save()
This resolved the original problem. But now model.save() doesn't call the save_model method of my admin model:
class BlogAdmin (admin.ModelAdmin):
# ...
form = BlogAdminForm
def save_model(self, request, obj, form, change):
obj.author = request.user
obj.save()
In consequence of this I get a new error: null value in column "author_id" violates not-null constraint. What am I doing wrong? Can I call this method manually?
You're going to have to save the tags after you save the instance, and that means doing it in your save_model function. This is nothing to do with your tag-manipulation code: if you look at the documentation for the Form.save method it says:
Another side effect of using commit=False is seen when your model has a many-to-many relation with another model. If your model has a many-to-many relation and you specify commit=False when you save a form, Django cannot immediately save the form data for the many-to-many relation. This is because it isn't possible to save many-to-many data for an instance until the instance exists in the database.
To work around this problem, every time you save a form using commit=False, Django adds a save_m2m() method to your ModelForm subclass. After you've manually saved the instance produced by the form, you can invoke save_m2m() to save the many-to-many form data.
There are a couple of ways to solve your problem. You could write a widget that converts back and forth between lists of tag ids and comma-separated tag names, and then call form.save_m2m() in your save_model method. But this approach has the disadvantage that you'll have to create new tags when decoding the value from the widget, even if the form is not saved (perhaps because of a validation error elsewhere in the form).
So I think a better approach in this case is to add your own save_tags method to the form:
class BlogAdminForm(forms.ModelForm):
tagging = forms.CharField(required=False, label="Tags", max_length=200,
widget=forms.TextInput(attrs={'class':'vTextField'}))
class Meta:
model = Post
def __init__(self, *args, **kwargs):
super(BlogAdminForm, self).__init__(*args, **kwargs)
if 'instance' in kwargs:
tags = (t.name for t in kwargs['instance'].tags.all())
self.initial['tagging'] = ', '.join(tags)
def save_tags(self, obj):
obj.tags = (Tag.objects.get_or_create(name = tag.strip())[0]
for tag in self.cleaned_data['tagging'].split(','))
class BlogPostAdmin(admin.ModelAdmin):
form = BlogAdminForm
def save_model(self, request, obj, form, change):
obj.author = request.user
obj.save()
form.save_tags(obj)
Note that I moved the tag-manipulation code to the form: I think it belongs here, and not in the model, because it's all about user input. I also made a couple of stylistic improvements:
'instance' in kwargs is simpler than kwargs.has_key('instance').
The generator expression (t.name for t in kwargs['instance'].tags.all()) is simpler than building a list in a for loop.
The get_or_create method is a handy shortcut that avoid the need to try: ... except ObjectDoesNotExist: ...
You can assign directly to a ManyToMany field instead of calling clear and then add (also, it's more efficient when the tags don't change).
I have a task list, with ability to assign users. So I have foreignkey to User model in the database. However, the default display is username in the dropdown menu, I would like to display full name (first last) instead of the username. If the foreignkey is pointing to one of my own classes, I can just change the str function in the model, but User is a django authentication model, so I can't easily change it directly right?
Anyone have any idea how to accomplish this?
Thanks a lot!
You can create a new ModelForm for your Task model, which will display the list of users however you like (code here assumes a model named Task with a 'user' attribute):
def get_user_full_name_choices:
return [(user, user.get_full_name()) for user in User.objects.all()]
class TaskAdminForm(forms.ModelForm):
class Meta:
model = Task
user = forms.ChoiceField(choices=get_user_full_name_choices)
Then, tell your ModelAdmin class to use the new form:
class TaskAdmin(admin.ModelAdmin):
form = TaskAdminForm
There is another choice:
USERS = [(user.id, user.get_full_name()) for user in User.objects.all()]
USERS.insert(0, ('', '----'))
class TaskAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(TaskAdminForm, self).__init__(*args, **kwargs)
self.fields['user'].choices=USERS
A more generic, verbose solution, using a ModelChoiceField as base. Avoids fiddling with querysets, just focusing on modifying the visible value in the dropdown.
create your own formfield:
class CustomLabelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return "Howdy {}".format(obj.name)
use it either with formfield_for_dbfield (as in the link), or, more verbose, in your admin form:
class TaskAdminForm(forms.ModelForm):
user = CustomLabelChoiceField(queryset=User.objects.all()
class Meta:
model = Task
# obviously, link it with your admin
class TaskAdmin(admin.ModelAdmin):
form = TaskAdminForm
By overriding formfield_for_foreignkey(), you can display the combination of "first_name" and "last_name" to Django Admin without creating a custom "forms.ModelChoiceField" and a custom "forms.ModelForm" as shown below:
#admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == "user":
formfield.label_from_instance = lambda obj: f'{obj.first_name} ({obj.last_name})'
return formfield
I have a two way foreign relation similar to the following
class Parent(models.Model):
name = models.CharField(max_length=255)
favoritechild = models.ForeignKey("Child", blank=True, null=True)
class Child(models.Model):
name = models.CharField(max_length=255)
myparent = models.ForeignKey(Parent)
How do I restrict the choices for Parent.favoritechild to only children whose parent is itself? I tried
class Parent(models.Model):
name = models.CharField(max_length=255)
favoritechild = models.ForeignKey("Child", blank=True, null=True, limit_choices_to = {"myparent": "self"})
but that causes the admin interface to not list any children.
I just came across ForeignKey.limit_choices_to in the Django docs.
Not sure yet how it works, but it might be the right thing here.
Update: ForeignKey.limit_choices_to allows one to specify either a constant, a callable or a Q object to restrict the allowable choices for the key. A constant obviously is of no use here, since it knows nothing about the objects involved.
Using a callable (function or class method or any callable object) seems more promising. However, the problem of how to access the necessary information from the HttpRequest object remains. Using thread local storage may be a solution.
2. Update: Here is what has worked for me:
I created a middleware as described in the link above. It extracts one or more arguments from the request's GET part, such as "product=1", and stores this information in the thread locals.
Next there is a class method in the model that reads the thread local variable and returns a list of ids to limit the choice of a foreign key field.
#classmethod
def _product_list(cls):
"""
return a list containing the one product_id contained in the request URL,
or a query containing all valid product_ids if not id present in URL
used to limit the choice of foreign key object to those related to the current product
"""
id = threadlocals.get_current_product()
if id is not None:
return [id]
else:
return Product.objects.all().values('pk').query
It is important to return a query containing all possible ids if none was selected so that the normal admin pages work ok.
The foreign key field is then declared as:
product = models.ForeignKey(
Product,
limit_choices_to={
id__in=BaseModel._product_list,
},
)
The catch is that you have to provide the information to restrict the choices via the request. I don't see a way to access "self" here.
The 'right' way to do it is to use a custom form. From there, you can access self.instance, which is the current object. Example --
from django import forms
from django.contrib import admin
from models import *
class SupplierAdminForm(forms.ModelForm):
class Meta:
model = Supplier
fields = "__all__" # for Django 1.8+
def __init__(self, *args, **kwargs):
super(SupplierAdminForm, self).__init__(*args, **kwargs)
if self.instance:
self.fields['cat'].queryset = Cat.objects.filter(supplier=self.instance)
class SupplierAdmin(admin.ModelAdmin):
form = SupplierAdminForm
The new "right" way of doing this, at least since Django 1.1 is by overriding the AdminModel.formfield_for_foreignkey(self, db_field, request, **kwargs).
See http://docs.djangoproject.com/en/1.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey
For those who don't want to follow the link below is an example function that is close for the above questions models.
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "favoritechild":
kwargs["queryset"] = Child.objects.filter(myparent=request.object_id)
return super(MyModelAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
I'm only not sure about how to get the current object that is being edited. I expect it is actually on the self somewhere but I'm not sure.
This isn't how django works. You would only create the relation going one way.
class Parent(models.Model):
name = models.CharField(max_length=255)
class Child(models.Model):
name = models.CharField(max_length=255)
myparent = models.ForeignKey(Parent)
And if you were trying to access the children from the parent you would do
parent_object.child_set.all(). If you set a related_name in the myparent field, then that is what you would refer to it as. Ex: related_name='children', then you would do parent_object.children.all()
Read the docs http://docs.djangoproject.com/en/dev/topics/db/models/#many-to-one-relationships for more.
If you only need the limitations in the Django admin interface, this might work. I based it on this answer from another forum - although it's for ManyToMany relationships, you should be able to replace formfield_for_foreignkey for it to work. In admin.py:
class ParentAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
self.instance = obj
return super(ParentAdmin, self).get_form(request, obj=obj, **kwargs)
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
if db_field.name == 'favoritechild' and self.instance:
kwargs['queryset'] = Child.objects.filter(myparent=self.instance.pk)
return super(ChildAdmin, self).formfield_for_foreignkey(db_field, request=request, **kwargs)
#Ber: I have added validation to the model similar to this
class Parent(models.Model):
name = models.CharField(max_length=255)
favoritechild = models.ForeignKey("Child", blank=True, null=True)
def save(self, force_insert=False, force_update=False):
if self.favoritechild is not None and self.favoritechild.myparent.id != self.id:
raise Exception("You must select one of your own children as your favorite")
super(Parent, self).save(force_insert, force_update)
which works exactly how I want, but it would be really nice if this validation could restrict choices in the dropdown in the admin interface rather than validating after the choice.
I'm trying to do something similar. It seems like everyone saying 'you should only have a foreign key one way' has maybe misunderstood what you're trying do.
It's a shame the limit_choices_to={"myparent": "self"} you wanted to do doesn't work... that would have been clean and simple. Unfortunately the 'self' doesn't get evaluated and goes through as a plain string.
I thought maybe I could do:
class MyModel(models.Model):
def _get_self_pk(self):
return self.pk
favourite = models.ForeignKey(limit_choices_to={'myparent__pk':_get_self_pk})
But alas that gives an error because the function doesn't get passed a self arg :(
It seems like the only way is to put the logic into all the forms that use this model (ie pass a queryset in to the choices for your formfield). Which is easily done, but it'd be more DRY to have this at the model level. Your overriding the save method of the model seems a good way to prevent invalid choices getting through.
Update
See my later answer for another way https://stackoverflow.com/a/3753916/202168
Do you want to restrict the choices available in the admin interface when creating/editing a model instance?
One way to do this is validation of the model. This lets you raise an error in the admin interface if the foreign field is not the right choice.
Of course, Eric's answer is correct: You only really need one foreign key, from child to parent here.
An alternative approach would be not to have 'favouritechild' fk as a field on the Parent model.
Instead you could have an is_favourite boolean field on the Child.
This may help:
https://github.com/anentropic/django-exclusivebooleanfield
That way you'd sidestep the whole problem of ensuring Children could only be made the favourite of the Parent they belong to.
The view code would be slightly different but the filtering logic would be straightforward.
In the admin you could even have an inline for Child models that exposed the is_favourite checkbox (if you only have a few children per parent) otherwise the admin would have to be done from the Child's side.
A much simpler variation of #s29's answer:
Instead of customising the form,
You can simply restrict the choices available in form field from your view:
what worked for me was:
in forms.py:
class AddIncomingPaymentForm(forms.ModelForm):
class Meta:
model = IncomingPayment
fields = ('description', 'amount', 'income_source', 'income_category', 'bank_account')
in views.py:
def addIncomingPayment(request):
form = AddIncomingPaymentForm()
form.fields['bank_account'].queryset = BankAccount.objects.filter(profile=request.user.profile)
from django.contrib import admin
from sopin.menus.models import Restaurant, DishType
class ObjInline(admin.TabularInline):
def __init__(self, parent_model, admin_site, obj=None):
self.obj = obj
super(ObjInline, self).__init__(parent_model, admin_site)
class ObjAdmin(admin.ModelAdmin):
def get_inline_instances(self, request, obj=None):
inline_instances = []
for inline_class in self.inlines:
inline = inline_class(self.model, self.admin_site, obj)
if request:
if not (inline.has_add_permission(request) or
inline.has_change_permission(request, obj) or
inline.has_delete_permission(request, obj)):
continue
if not inline.has_add_permission(request):
inline.max_num = 0
inline_instances.append(inline)
return inline_instances
class DishTypeInline(ObjInline):
model = DishType
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
field = super(DishTypeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == 'dishtype':
if self.obj is not None:
field.queryset = field.queryset.filter(restaurant__exact = self.obj)
else:
field.queryset = field.queryset.none()
return field
class RestaurantAdmin(ObjAdmin):
inlines = [
DishTypeInline
]