Django modelform field how to make disabled and prevent tampering? - django

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'}),
}

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 REST Framework: default fields in browseable API form

I have a model:
class XCall(models.Model):
created_on = models.DateTimeField(auto_now_add=True)
send_on = models.DateTimeField(default=datetime.now)
recipient = models.ForeignKey(User)
text = models.CharField(max_length=4096)
backup_calls = models.IntegerField(blank=True, null=True)
And a serializer for that model:
class CallSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='call-detail',
)
# some validation and custom field definitions
...
class Meta:
model = XCall
fields = ('url', 'id', 'text', 'recipient', 'send_on', 'backup_calls', 'status')
lookup_field= 'pk'
And here's the list view:
class CallList(generics.ListCreateAPIView):
serializer_class = CallSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrSuperuser,)
def pre_save(self, obj):
auth_user = self.request.user
obj.auth_user = auth_user
def get_queryset(self):
"""
This view should return a list of all the calls
for the currently authenticated user.
"""
auth = self.request.user
if isinstance(auth, AnonymousUser):
return []
elif auth.is_superuser:
return XCall.objects.all()
else:
return XCall.objects.filter(auth_user=auth)
In CallList's browseable API, I see the following in the POST form at the bottom:
My question is: why is there no default value set for send_on, and there is one for backup_calls? I assumed that the form would follow the XCall model specification and use datetime.now() for defaulting the former, and leave backup_calls blank (since it's nullable). How can I get the form to follow the model specifications?
You actually want to set an initial value, not a default value. See the docs. Your code should be:
from django.utils import timezone
class CallSerializer(serializers.HyperlinkedModelSerializer):
send_on = serializers.DateTimeField(initial=timezone.now())
...
A default value is the value provided to an attribute if no value is set for the field. The distinction between initial and default arguments mirrors the difference between Django's initial argument for form fields and Django's default argument for model fields.
There is a distinction between model defaults and initial values in forms. This is especially the case for default values which are actually functions because they are only called when the instance is saved. For example, which now do you want - this time at which the blank form is displayed, or the time at which the user presses "POST"? Django applies the default when saving the model if the field value is missing. To achieve what you want you need to manually set the default in the serialize field, for example:
class CallSerializer(serializers.HyperlinkedModelSerializer):
send_on = serializers.DateTimeField(default=datetime.now)
...

Django admin BaseInlineFormSet validation after POST

Looking for info how django formsets validation works, though it is more complicated than it sounds. I have a formset with values, part of these values can be inserted there by javascript (it means they do not exist in database yet).
class RequireOneFormSet(BaseInlineFormSet):
def clean(self):
if any(self.errors):
return
form_count = len([f for f in self.forms if f.cleaned_data])
if form_count < 1:
raise ValidationError(_('At least one %(object)s is required.') %
{'object':
_(self.model._meta.object_name.lower())})
class VariantInline(admin.StackedInline):
model = Variant
extra = 1
formset = RequireOneFormSet
class ProductAdmin(admin.ModelAdmin):
class Meta:
model = Product
class Media:
js = (os.path.join(STATIC_URL, 'js', 'admin_utils.js'), )
exclude = ('slug',)
filter_horizontal = ('category',)
inlines = [ImageInline, DetailInline, VariantInline]
manufacturer = ModelChoiceField(Manufacturer.objects.all())
list_filter = ('name', 'manufacturer', 'category')
list_display = ('name', 'manufacturer')
search_fields = ('name',)
save_as = True
Next, basing on those entries I`d like to create objects during formset validation. Django complains that there is no such object in DB when 'Save' button is clicked.
I have tried to override clean method of model, clean of ModelAdmin, save_formset of formset but with no luck as these values created by javascript are filtered out earlier in process. I am looking for info which method takes care of that, and can it be overriden?
EDIT:
Added some code, used view is a generic one from Django.
I`ve managed to resolve it. Key was to create my own field and override clean() method there. As you can see in file django/forms/models.py in class ModelMultipleChoiceField clean() is responsible for checking send values.
class DetailsField(ModelMultipleChoiceField):
def clean(self, value):
(code here)
class VariantForm(ModelForm):
details = DetailsField(queryset=Detail.objects.all(),
widget=FilteredSelectMultiple('details', False))
class VariantInline(admin.StackedInline):
model = Variant
extra = 1
formset = RequireOneFormSet
form = VariantForm

Django: Custom "add only" inline

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.

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 = ...