Django: Custom "add only" inline - django

I want an inline form to only show its fields contents, and not let users to edit or remove entries, only add them. That means that the values would be similar when using the readonly_fields option, and the "Add another ..." link at the bottom would make a form appear, letting users add more entries.
The can_delete option it's useful here, but the readonly_fields lock both add and change possibilities. I imagine that building a new inline template would do. In that case, how would I just show the field values for each entry and then put a form at the bottom?
Edit: what I got until now:
# models.py
class AbstractModel(models.Model):
user = models.ForeignKey(User, editable = False)
... some more fields ...
class Meta:
abstract = True
class ParentModel(AbstractModel):
... fields ...
class ChildModel(AbstractModel):
parent = models.ForeignKey(ParentModel, ... options ...)
... fields ...
# admin.py
class ChildModelInline(admin.TabularInline):
model = ChildModel
form = ChildModelForm
can_delete = False
class ParentModelAdmin(admin.ModelAdmin):
... options ...
inlines = (ChildModelInline,)
# forms.py
class ChildModelForm(models.ModelForm):
user = forms.CharField(required = False)
... some more fields and stuff needed ...
def __init__(self, *args, **kwargs):
super(ChildModelForm, self).__init__(*args, **kwargs)
try: user = User.objects.get(id = self.instance.user_id)
except: return None
self.fields['user'].initial = user.first_name
self.fields['user'].widget.attrs['readonly'] = 'readonly'
In this example I'm doing like I wanted the user field as readonly.
In the last line, If I change the widget attribute to ['disabled'] = True, it works fine, but I need a text entry, not a disabled form field. I'm also aware that I'll need to override the save_model() and save_formsets() for this to work properly.

I would use extra=1 to get that last working form.
Then loop through all the forms except the last one in your view and change, like this, every field: In a Django form, how do I make a field readonly (or disabled) so that it cannot be edited?
You don't have to do it in the __init__, you can access those attributes after the entire formset is created of course.

Related

Display a Foreign Key Dropdown in Django Form as Text

Is there a way to show a foreign key field in a Django form as an read only text box?
forms.py
class NewModelForm(forms.ModelForm):
class Meta:
model = Model
fields = ['fk_field', 'other_field']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['fk_field'].widget.attrs['readonly'] = True #the dropdown is still active when this is set
#self.fields['fk_field'] = forms.CharField(widget=forms.TextInput()) ##when I turn this on, I get an error that I am assigning to the wrong instance.
You can override any of your ModelForm's fields, even those that come from the model, by just setting it as class attribute like you would for a normal Form field:
class NewModelForm(ModelForm):
fk_field = forms.CharField(required=False, disabled=True)
class Meta:
model = MyModel
fields = ['fk_field', 'other_field']
The disabled option on a field sets the input to disabled.
Note that you should not trust the data submitted to contain the correct initial value for fk_field. Anyone can still submit a different fk_field if they know how to use curl or Postman, even if the <input> is disabled. So if just ignore whatever value is submitted and set it to the correct value in your view.

Django modelform field how to make disabled and prevent tampering?

There is this disabled attribute. But i am not able to apply it to the modelform fields. I am not sure how to. I can add it to forms.Form easily. But since I am using widgets I just dont know where to insert it.
https://docs.djangoproject.com/en/2.0/ref/forms/fields/#disabled
class TestForm(forms.ModelForm):
class Meta:
model = Test
fields = ['date']
widgets = {'date': forms.TextInput(attrs={'readonly': 'readonly'})}
I was facing a situation when I wanted to disable some fields when creating . And some fields disabled when editing.
My Env: Python 3, Django 2.1
My Form:
class AddInvoiceForm(forms.ModelForm):
disabled_fields = ['inv_type', 'report', 'subsidiary']
class Meta:
model = models.Invoice
fields = ('inv_type', 'report', 'subsidiary', 'rate_card', 'reviewed')
def __init__(self, *args, **kwargs):
super(AddInvoiceForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.pk:
for field in self.disabled_fields:
self.fields[field].disabled = True
else:
self.fields['reviewed'].disabled = True
Try something like this, assuming that your date field is forms.DateField and that you want to use TextInput widget:
class TestForm(forms.ModelForm):
date = forms.DateField(widget=forms.TextInput, disabled=True)
class Meta:
model = Test
fields = ['date']
This will override the default field definition which is created from your Test model definition.
The disabled boolean argument, when set to True, disables a form field using the disabled HTML attribute so that it won’t be editable by users. Even if a user tampers with the field’s value submitted to the server, it will be ignored in favor of the value from the form’s initial data.
Read about readonly vs disabled HTML input attributes.
The key note to take out from the above SO post is:
A readonly element is just not editable, but gets sent when the according form submits. a disabled element isn't editable and isn't sent on submit.
From above quote, setting disabled=True is enough, so you dont need to set readonly attribute on your widget.
class TestForm(forms.ModelForm):
date = forms.CharField(disabled=True)
class Meta:
model = Test
fields = ['date']
widgets = {
'date': forms.TextInput(attrs={'readonly': 'readonly'}),
}

Using an instance's fields to filter the choices of a manytomany selection in a Django admin view

I have a Django model with a ManyToManyField.
1) When adding a new instance of this model via admin view, I would like to not see the M2M field at all.
2) When editing an existing instance I would like to be able to select multiple options for the M2M field, but display only a subset of the M2M options, depending on another field in the model. Because of the dependence on another field's actual value, I can't just use formfield_for_manytomany
I can do both of the things using a custom ModelForm, but I can't reliably tell whether that form is being used to edit an existing model instance, or if it's being used to create a new instance. Even MyModel.objects.filter(pk=self.instance.pk).exists() in the custom ModelForm doesn't cut it. How can I accomplish this, or just tell whether the form is being displayed in an "add" or an "edit" context?
EDIT: my relevant code is as follows:
models.py
class LimitedClassForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(LimitedClassForm, self).__init__(*args, **kwargs)
if not self.instance._adding:
# Edit form
clas = self.instance
sheets_in_course = Sheet.objects.filter(course__pk=clas.course.pk)
self.Meta.exclude = ['course']
widget = self.fields['active_sheets'].widget
sheet_choices = []
for sheet in sheets_in_course:
sheet_choices.append((sheet.id, sheet.name))
widget.choices = sheet_choices
else:
# Add form
self.Meta.exclude = ['active_sheets']
class Meta:
exclude = []
admin.py
class ClassAdmin(admin.ModelAdmin):
formfield_overrides = {models.ManyToManyField: {
'widget': CheckboxSelectMultiple}, }
form = LimitedClassForm
admin.site.register(Class, ClassAdmin)
models.py
class Course(models.Model):
name = models.CharField(max_length=255)
class Sheet(models.Model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
file = models.FileField(upload_to=getSheetLocation)
class Class(models.model):
name = models.CharField(max_length=255)
course = models.ForeignKey(Course)
active_sheets = models.ManyToManyField(Sheet)
You can see that both Sheets and Classes have course fields. You shouldn't be able to put a sheet into active_sheets if the sheet's course doesn't match the class's course.

Django admin: make field editable in add but not edit

I've got a model similar to this:
class Product(models.Model):
third_party_id = models.CharField(max_length=64, blank=False, unique=True)
that uses the Django default primary key. I want users to be able to add products by setting the third_party_id on the add page, but I don't want that field editable in the edit page to avoid corrupting the third_party_id. In the Django docs, the same settings appear to be used for add and edit. Is this possible?
Do not set self.readonly_fields to avoid thread issues. Instead override get_readonly_fields method:
def get_readonly_fields(self, request, obj=None):
if obj: # obj is not None, so this is an edit
return ['third_party_id',] # Return a list or tuple of readonly fields' names
else: # This is an addition
return []
The above is helpful (shanyu's answer using get_readonly_fields), however it does not work properly if used in "StackedInline". The result is two copies of whatever field is marked readonly, and it is not editable in the "add" instance. See this bug: https://code.djangoproject.com/ticket/15602
Hope this saves someone some searching!
I am not sure if this is the best way, but you could define your own form for the admin. And custom validate your third_party_id, rejecting if it is already set:
Admin.py
class ProductAdminForm(forms.ModelForm):
class Meta:
model = Product
def clean_third_party_id(self):
cleaned_data = self.cleaned_data
third_party_id = cleaned_data['third_party_id']
id = cleaned_data['id']
obj = Product.objects.get(id=id)
if obj.third_party_id != third_party_id:
raise ValidationError("You cannot edit third_party_id, it must stay as %s" % obj.third_party_id)
return third_party_id
class ProductAdmin(admin.Admin):
form = [ProductAdminForm,]

field choices() as queryset?

I need to make a form, which have 1 select and 1 text input. Select must be taken from database.
model looks like this:
class Province(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(max_length=30)
def __unicode__(self):
return self.name
It's rows to this are added only by admin, but all users can see it in forms.
I want to make a ModelForm from that. I made something like this:
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=CHOICES),
}
but it doesn't work. The select tag is not displayed in html. What did I wrong?
UPDATE:
This solution works as I wanto it to work:
class ProvinceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProvinceForm, self).__init__(*args, **kwargs)
user_provinces = UserProvince.objects.select_related().filter(user__exact=self.instance.id).values_list('province')
self.fields['name'].queryset = Province.objects.exclude(id__in=user_provinces).only('id', 'name')
name = forms.ModelChoiceField(queryset=None, empty_label=None)
class Meta:
model = Province
fields = ('name',)
Read Maersu's answer for the method that just "works".
If you want to customize, know that choices takes a list of tuples, ie (('val','display_val'), (...), ...)
Choices doc:
An iterable (e.g., a list or tuple) of
2-tuples to use as choices for this
field.
from django.forms.widgets import Select
class ProvinceForm(ModelForm):
class Meta:
CHOICES = Province.objects.all()
model = Province
fields = ('name',)
widgets = {
'name': Select(choices=( (x.id, x.name) for x in CHOICES )),
}
ModelForm covers all your needs (Also check the Conversion List)
Model:
class UserProvince(models.Model):
user = models.ForeignKey(User)
province = models.ForeignKey(Province)
Form:
class ProvinceForm(ModelForm):
class Meta:
model = UserProvince
fields = ('province',)
View:
if request.POST:
form = ProvinceForm(request.POST)
if form.is_valid():
obj = form.save(commit=True)
obj.user = request.user
obj.save()
else:
form = ProvinceForm()
If you need to use a query for your choices then you'll need to overwrite the __init__ method of your form.
Your first guess would probably be to save it as a variable before your list of fields but you shouldn't do that since you want your queries to be updated every time the form is accessed. You see, once you run the server the choices are generated and won't change until your next server restart. This means your query will be executed only once and forever hold your peace.
# Don't do this
class MyForm(forms.Form):
# Making the query
MYQUERY = User.objects.values_list('id', 'last_name')
myfield = forms.ChoiceField(choices=(*MYQUERY,))
class Meta:
fields = ('myfield',)
The solution here is to make use of the __init__ method which is called on every form load. This way the result of your query will always be updated.
# Do this instead
class MyForm(forms.Form):
class Meta:
fields = ('myfield',)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the query here
MYQUERY = User.objects.values_list('id', 'last_name')
self.fields['myfield'] = forms.ChoiceField(choices=(*MYQUERY,))
Querying your database can be heavy if you have a lot of users so in the future I suggest some caching might be useful.
the two solutions given by maersu and Yuji 'Tomita' Tomita perfectly works, but there are cases when one cannot use ModelForm (django3 link), ie the form needs sources from several models / is a subclass of a ModelForm class and one want to add an extra field with choices from another model, etc.
ChoiceField is to my point of view a more generic way to answer the need.
The example below provides two choice fields from two models and a blank choice for each :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=([['','-'*10]]+[[x.id, x.__str__()] for x in Speakers.objects.all()]))
event = forms.ChoiceField(choices=( [['','-'*10]]+[[x.id, x.__str__()] for x in Events.objects.all()]))
If one does not need a blank field, or one does not need to use a function for the choice label but the model fields or a property it can be a bit more elegant, as eugene suggested :
class MixedForm(forms.Form):
speaker = forms.ChoiceField(choices=((x.id, x.__str__()) for x in Speakers.objects.all()))
event = forms.ChoiceField(choices=(Events.objects.values_list('id', 'name')))
using values_list() and a blank field :
event = forms.ChoiceField(choices=([['','-------------']] + list(Events.objects.values_list('id', 'name'))))
as a subclass of a ModelForm, using the one of the robos85 question :
class MixedForm(ProvinceForm):
speaker = ...