I'm new to django and trying to do something with an issue.I have this model:
class MyModel(models.Model):
value = models.CharField(max_length=50, choises=...)
custom_value = models.CharField(max_length=50, blank=True)
And I need custom_value field to be enabled to edit only if value of value field is "CUSTOM".
I'm assuming when you say 'enabled to edit' you are referring to a form field.
I'm also assuming that the data will already be saved in the model when the form loads.
In that case, this should be done on the corresponding Form for that model. You'll need to initialize that form field to be disabled, and then enable it on the form load.
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
custom_value = self.fields.get('custom_value')
if custom_value and custom_value.widget.attrs['value'] == 'CUSTOM':
self.fields['value'].widget.attrs['disabled'] = 'false'
I know your code is just an example, but using value as class attribute name is confusing.
If you were hoping to do this dynamically on one form, as in having a user select a choice and then see another option become enabled without a browser refresh, that must be done in Javascript. This is how you would do it with just Django forms.
Let's say I have the following model:
class Folder(models.Model):
name = models.CharField(default='untitled', max_length=255)
parent = models.ForeignKey('self', null=True, blank=True)
root = models.ForeignKey('self', null=True, blank=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
In my app, this class is used to represents two types of folders: a normal folder-object and a so called root_folder-object, which does not have a parent nor a root-FK set.
I created a custom ModelForm with custom clean(), which is working like a charm (according to unittests and manual testing):
class FolderForm(ModelForm):
def __init__(self, *args, **kwargs):
try:
data = kwargs.get('data', None).copy()
except AttributeError:
data = None
self.prefix = kwargs.get('prefix')
user = kwargs.pop('user', None)
if data is not None:
if user is not None:
data[self.add_prefix('user')] = user.id
kwargs['data'] = data
super(FolderForm, self).__init__(*args, **kwargs)
def clean(self):
# just working fine, so I won't include it here
pass
class Meta:
model = Folder
fields = '__all__'
So, because my root-folder is just a normal Folder-object with blank FKs, I don't want to even show these fields to the user while creation. I created another form for this:
class AddRootFolderForm(FolderForm):
class Meta:
model = Folder
exclude = ['parent', 'root', 'user']
As you can see, I exclude user aswell, this value will be set in the view. Currently, this is my view code:
#login_required
def create_rootfolder(request):
if request.method == 'POST':
form = FolderForm(data = request.POST,
user = request.user)
else:
form = AddRootFolderForm()
if form.is_valid():
new = form.save()
return redirect('show_rootfolder', root_id = new.id)
return render(request, 'create_rootfolder.html',
{ 'form': form })
This whole setup is working, but seems awful hackerish. Is there any better approach to hide certain fields from the user (meaning: Don't even show them as hidden fields), but include them in validation? My main problem is, that I can't use the same form for displaying and validating, because the excluded fields will not be validated, if I use AddRootFolderForm as single form instance.
I am aware that I can exclude the fields dynamically in the constructor, I even tried this, but it got my constructor bloated to 50 LOC, which seemed unclean.
So what would be the best approach to validate the model with all fields, even if they were not included in the form shown to the user?
Why validate fields, not used in Form?
The cleaning process is the way to check the data posted by a user. The rest of the data, required for Model operations must be added after the form validation
if form.is_valid():
new = form.save(commit=False)
new.user = request.user
...
new.save()
Example:
class MyUser(models.Model):
blocked_users = models.ManyToManyField("self", blank=True, null=True)
user = MyUser.object.get(pk=1)
user.blocked_users.add(user)
user.blocked_users.all()[0] == user # (!!!)
Can It be prevented on model/db level? Or we need just do check somewhere in app.
Looking at the Django docs for ManyToManyField arguments, it does not seem possible.
The closest argument to what you want is the limit_choices_to However, that only limits choices on ModelForms and admin (you can still save it like you did in your example), and there is currently no easy way to use it to limit based on another value (pk) in the current model.
If you want to prevent it from happening altogether, you'll have to resort to overriding the save method on the through model--something like:
class MyUser(models.Model):
blocked_users = models.ManyToManyField(..., through="BlockedUser")
class BlockedUser(models.Model):
user = models.ForeignKey(MyUser)
blocked = models.ForeignKey(MyUser)
def save(self, *args, **kwargs):
# Only allow this relationship to be created if
if self.user != self.blocked:
super(BlockedUser, self).save(*args, **kwargs)
You could of course also do this with signals.
I need set field value, not passed to Django Form constructor.
I have model and form like this:
class Message(models.Model):
created = models.DateTimeField()
text = models.CharField(max_length=200, blank=True, null=True)
active = models.BooleanField(default=False)
class MessageForm(forms.ModelForm):
class Meta:
model = Message
exclude = ('created', 'active')
def clean(self):
# check if user is blocked
if user.is_admin():
self.cleaned_data['active'] = True
return self.cleaned_data
Expected: if current user is admin - I need automatically set message as active. User should not pass this parameter by form.
Actual: I see that saved message always have flag "False" (I can delete condition and in this case I also see that message is not active).
Please help me understand, how can I do set this "active" flag in clean() method.
The previous answer would work, but I like encapsulating all the form's internal operations like what to show and what not, within the form. I know you mentioned you don't want to send a field value to the constructor, but if you don't mind sending the user, your solution would work.
i.e., your constructor:
def __init__(self, user):
self.user = user
super(BaseForm, self).__init__()
then in your clean, you just change the user to self.user.
There is another added benefit to this. Say tomorrow you want to assign more fields based on your user, you don't need to add anything to the views, you can simply add it to the form.
EDIT:
When you add a field to exclude, it is not available in the cleaned data. Instead, set its widget as hidden.
active = forms.BooleanField(widget=forms.HiddenInput)
EDIT 2: If you really don't want the field in the form
In this case, instead of overriding the clean, why don't you override the save?
def save (self):
super(BaseForm, self).save()
if user.is_admin():
self.instance.active=True
super(BaseForm, self).save()
Don't do this in the form's clean() method, do this in the view.
def your_view(request):
if request.method == 'POST':
form = MessageForm(data=request.POST)
if form.is_valid():
new_message = form.save(commit=False)
if user.is_admin():
new_message.active = True
However, if you also want to handle the case where your user is not admin using the same form, you can look at incorporating similar logic in the form's init() instead of the view, probably by passing info about the user from the view to the form's init()
Use this:
def message_form_factory(user):
class MessageForm(forms.ModelForm):
def clean(self):
# check if user is blocked
if user.is_admin():
self.cleaned_data['active'] = True
return self.cleaned_data
return MessageForm
And in your view use:
form = message_form_factory(request.user)()
form = message_form_factory(request.user)(request.POST)
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
]