How can I disable a model field in a django form - python

I have a model like this:
class MyModel(models.Model):
REGULAR = 1
PREMIUM = 2
STATUS_CHOICES = ((REGULAR, "regular"), (PREMIUM, "premium"))
name = models.CharField(max_length=30)
status = models.IntegerField(choices = STATUS_CHOICES, default = REGULAR)
class MyForm(forms.ModelForm):
class Meta:
model = models.MyModel
In a view I initialize one field and try to make it non-editable:
myform = MyForm(initial = {'status': requested_status})
myform.fields['status'].editable = False
But the user can still change that field.
What's the real way to accomplish what I'm after?

Step 1: Disable the frontend widget
Use the HTML readonly attribute:
http://www.w3schools.com/tags/att_input_readonly.asp
Or disabled attribute:
http://www.w3.org/TR/html401/interact/forms.html#adef-disabled
You can inject arbitrary HTML key value pairs via the widget attrs property:
myform.fields['status'].widget.attrs['readonly'] = True # text input
myform.fields['status'].widget.attrs['disabled'] = True # radio / checkbox
Step 2: Ensure the field is effectively disabled on backend
Override your clean method for your field so that regardless of POST input (somebody can fake a POST, edit the raw HTML, etc.) you get the field value that already exists.
def clean_status(self):
# when field is cleaned, we always return the existing model field.
return self.instance.status

From django 1.9:
from django.forms import Textarea
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = '__all__'
widgets = {'my_field_in_my_model': Textarea(attrs={'cols':80,'rows':1}),}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['my_field_in_my_model'].disabled = True

Have you tried using the exclude function?
something like this
class PartialAuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title')
class PartialAuthorForm(ModelForm):
class Meta:
model = Author
exclude = ('birth_date',)
Reference Here

Just customize the widget instance for the status field:
class MyModel(models.Model):
REGULAR = 1
PREMIUM = 2
STATUS_CHOICES = ((REGULAR, "regular"), (PREMIUM, "premium"))
name = models.CharField(max_length=30)
status = models.IntegerField(choices = STATUS_CHOICES, default = REGULAR)
class MyForm(forms.ModelForm):
status = forms.CharField(widget=forms.TextInput(attrs={'readonly':'True'}))
class Meta:
model = models.MyModel
see: Django Documentation

There is a very easy way of doing it:
class GenerateCertificate(models.Model):
field_name = models.CharField(
max_length=15,
editable=False)
def __unicode__(self):
return unicode(self.field_name)
The editable=False will make the field disabled for editing.

Related

Django - Redirect to a subclass admin page

I'm creating a web application with Django.
In my models.py I have a class BaseProduct and a class DetailProduct, which extends BaseProduct.
In my admin.py I have BaseProductAdmin class and DetailProductAdmin class, which extends BaseProductAdmin.
I have another class called System, with a many to many relation with BaseProduct.
In the System admin page, I can visualize a list of the BaseProduct objects related to that system.
When I click on a product, the application redirect me to the BaseProduct admin page.
When a product of the list is a DetailProduct object, I would like to be redirected on the DetailProduct admin page instead.
Any idea on how to do this?
In models.py :
class BaseProduct(models.Model):
id = models.AutoField(primary_key=True, db_column='ID')
_prod_type_id = models.ForeignKey(
ProductTypes, verbose_name="product type", db_column='_prod_type_ID')
systems = models.ManyToManyField(
'database.System', through='database.SystemProduct')
def connected_to_system(self):
return self.systems.exists()
class Meta:
db_table = u'products'
verbose_name = "Product"
ordering = ['id', ]
class System(models.Model):
id = models.AutoField(primary_key=True, db_column='ID')
name = models.CharField(max_length=300)
def has_related_products(self):
""" Returns True if the system is connected with products. """
return self.products_set.exists()
class Meta:
managed = False
db_table = u'systems'
verbose_name = "System"
ordering = ['id', ]
class DetailProduct(BaseProduct):
options_id = models.AutoField(db_column='ID', primary_key=True)
product = models.OneToOneField(BaseProduct, db_column='_product_ID', parent_link=True)
min_height = models.FloatField(help_text="Minimum height in meters.")
max_height = models.FloatField(help_text="Maximum height in meters.")
def __init__(self, *args, **kwargs):
super(DetailProduct, self).__init__(*args, **kwargs)
if not self.pk:
self._prod_type_id = ProductTypes.objects.get(pk=9)
class Meta:
managed = False
db_table = 'detail_product'
verbose_name = "Detail product"
verbose_name_plural = "Detail products"
class SystemProduct(models.Model):
id = models.AutoField(primary_key=True, db_column='ID')
_system_id = models.ForeignKey(System, db_column='_system_ID')
_product_id = models.ForeignKey(BaseProduct, db_column='_Product_ID')
class Meta:
db_table = u'system_product'
unique_together = ('_system_id', '_product_id')
verbose_name = "system/product connection"
In my admin.py page:
class SystemProductInlineGeneric(admin.TabularInline):
model = SystemProduct
extra = 0
show_edit_link = True
show_url = True
class SystemProductForm(forms.ModelForm):
class Meta:
model = SystemProduct
fields = '__all__'
def __init__(self, *args, **kwargs):
""" Remove the blank option for the inlines. If the user wants to remove
the inline should use the proper delete button. In this way we can
safely check for orphan entries. """
super(SystemProductForm, self).__init__(*args, **kwargs)
modelchoicefields = [field for field_name, field in self.fields.iteritems() if
isinstance(field, forms.ModelChoiceField)]
for field in modelchoicefields:
field.empty_label = None
class SystemProductInlineForSystem(SystemProductInlineGeneric):
""" Custom inline, used under the System change page. Prevents all product-system
connections to be deleted from a product. """
form = SystemProductForm
raw_id_fields = ("_product_id",)
class SystemAdmin(admin.ModelAdmin):
inlines = [SystemProductInlineForSystem]
actions = None
list_display = ('id', 'name')
fieldsets = [('System information',
{'fields': (('id', 'name',), ),}),
]
list_display_links = ('id', 'configuration',)
readonly_fields = ('id',)
save_as = True
If I understand correctly, your question is how to change the InlineAdmin (SystemProductInlineForSystem) template so the "change link" redirects to the DetailProduct admin change form (instead of the BaseProduct admin change form) when the product is actually a DetailProduct.
I never had to deal with this use case so I can't provide a full-blown definitive answer, but basically you will have to override the inlineadmin template for SystemProductInlineForSystem and change the part of the code that generates this url.
I can't tell you exactly which change you will have to make (well, I probably could if I had a couple hours to spend on this but that's not the case so...), so you will have to analyze this part of the code and find out by yourself - unless of course someone more knowledgeable chimes in...

how to show a django ModelForm field as uneditable

taking my initial lessons with django ModelForm ,I wanted to give the user ,ability to edit an entry in a blog.The BlogEntry has a date,postedTime, title and content.I want to show the user an editform which shows all these fields,but with only title and content as editable. The date and postedTime should be shown as uneditable.
class BlogEntry(models.Model):
title = models.CharField(unique=True,max_length=50)
description = models.TextField(blank=True)
date = models.DateField(default=datetime.date.today)
postedTime = models.TimeField(null=True)
...
For adding an entry ,I use a ModelForm in the normal way..
class BlogEntryAddForm(ModelForm):
class Meta:
model = BlogEntry
...
But how do I create the edit form?I want it to show the date,postedTime as uneditable (but still show them on the form) and let the user edit the title and description.
if I use,exclude in class Meta for date and postedTime,that will cause them not to appear on the form.So,how can I show them as uneditable?
class BlogEntryEditForm(ModelForm):
class Meta:
model = BlogEntry
...?...
In the form object, declare the attribute of the field as readonly:
form.fields['field'].widget.attrs['readonly'] = True
Is date field represent a date when the entry first created or when it was modified last time? If first then use auto_now_add option else use auto_now. That is:
date = models.DateField(auto_now_add=True)
will set date to now when entry will be created.
auto_now_add makes field uneditable. For other cases use editable option to make any field uneditable. For example
postedDate = models.TimeField(null=True, editable=False)
Also, likely you will add posted boolean field to Entry model, so it is convinient to set auto_now on postedDate. It will set postedDate to now every time you modify a Entry including one when you set posted to True.
I implemented it this way: https://djangosnippets.org/snippets/10514/
this implementation uses the data of model instance for all read-only fields and not the data obtained while processing the form
below the same code but using his example
from __future__ import unicode_literals
from django.utils import six
from django.utils.encoding import force_str
__all__ = (
'ReadOnlyFieldsMixin',
'new_readonly_form_class'
)
class ReadOnlyFieldsMixin(object):
"""Usage:
class MyFormAllFieldsReadOnly(ReadOnlyFieldsMixin, forms.Form):
...
class MyFormSelectedFieldsReadOnly(ReadOnlyFieldsMixin, forms.Form):
readonly_fields = ('field1', 'field2')
...
"""
readonly_fields = ()
def __init__(self, *args, **kwargs):
super(ReadOnlyFieldsMixin, self).__init__(*args, **kwargs)
self.define_readonly_fields(self.fields)
def clean(self):
cleaned_data = super(ReadOnlyFieldsMixin, self).clean()
for field_name, field in six.iteritems(self.fields):
if self._must_be_readonly(field_name):
cleaned_data[field_name] = getattr(self.instance, field_name)
return cleaned_data
def define_readonly_fields(self, field_list):
fields = [field for field_name, field in six.iteritems(field_list)
if self._must_be_readonly(field_name)]
map(lambda field: self._set_readonly(field), fields)
def _all_fields(self):
return not bool(self.readonly_fields)
def _set_readonly(self, field):
field.widget.attrs['disabled'] = 'true'
field.required = False
def _must_be_readonly(self, field_name):
return field_name in self.readonly_fields or self._all_fields()
def new_readonly_form_class(form_class, readonly_fields=()):
name = force_str("ReadOnly{}".format(form_class.__name__))
class_fields = {'readonly_fields': readonly_fields}
return type(name, (ReadOnlyFieldsMixin, form_class), class_fields)
Usage:
class BlogEntry(models.Model):
title = models.CharField(unique=True,max_length=50)
description = models.TextField(blank=True)
date = models.DateField(default=datetime.date.today)
postedTime = models.TimeField(null=True)
# all fields are readonly
class BlogEntryReadOnlyForm(ReadOnlyFieldsMixin, forms.ModelForm):
class Meta:
model = BlogEntry
# selected fields are readonly
class BlogEntryReadOnlyForm2(ReadOnlyFieldsMixin, forms.ModelForm):
readonly_fields = ('date', 'postedTime')
class Meta:
model = BlogEntry
or use the function
class BlogEntryForm(forms.ModelForm):
class Meta:
model = BlogEntry
BlogEntryFormReadOnlyForm = new_readonly_form_class(BlogEntryForm, readonly_fields=('description', ))
This will prevent any user from hacking the request:
self.fields['is_admin'].disabled = True
Custom form example:
class MemberShipInlineForm(forms.ModelForm):
is_admin = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
super(MemberShipInlineForm, self).__init__(*args, **kwargs)
if 'instance' in kwargs and kwargs['instance'].is_group_creator:
self.fields['is_admin'].disabled = True
class Meta:
model = MemberShip
fields = '__all__'
From the documentation,
class BlogEntryEditForm(ModelForm):
class Meta:
model = BlogEntry
readonly_fields = ['date','postedTime']

How do I filter values in a Django form using ModelForm?

I am trying to use the ModelForm to add my data. It is working well, except that the ForeignKey dropdown list is showing all values and I only want it to display the values that a pertinent for the logged in user.
Here is my model for ExcludedDate, the record I want to add:
class ExcludedDate(models.Model):
date = models.DateTimeField()
reason = models.CharField(max_length=50)
user = models.ForeignKey(User)
category = models.ForeignKey(Category)
recurring = models.ForeignKey(RecurringExclusion)
def __unicode__(self):
return self.reason
Here is the model for the category, which is the table containing the relationship that I'd like to limit by user:
class Category(models.Model):
name = models.CharField(max_length=50)
user = models.ForeignKey(User, unique=False)
def __unicode__(self):
return self.name
And finally, the form code:
class ExcludedDateForm(ModelForm):
class Meta:
model = models.ExcludedDate
exclude = ('user', 'recurring',)
How do I get the form to display only the subset of categories where category.user equals the logged in user?
You can customize your form in init
class ExcludedDateForm(ModelForm):
class Meta:
model = models.ExcludedDate
exclude = ('user', 'recurring',)
def __init__(self, user=None, **kwargs):
super(ExcludedDateForm, self).__init__(**kwargs)
if user:
self.fields['category'].queryset = models.Category.objects.filter(user=user)
And in views, when constructing your form, besides the standard form params, you'll specify also the current user:
form = ExcludedDateForm(user=request.user)
Here example:
models.py
class someData(models.Model):
name = models.CharField(max_length=100,verbose_name="some value")
class testKey(models.Model):
name = models.CharField(max_length=100,verbose_name="some value")
tst = models.ForeignKey(someData)
class testForm(forms.ModelForm):
class Meta:
model = testKey
views.py
...
....
....
mform = testForm()
mform.fields["tst"] = models.forms.ModelMultipleChoiceField(queryset=someData.objects.filter(name__icontains="1"))
...
...
Or u can try something like this:
class testForm(forms.ModelForm):
class Meta:
model = testKey
def __init__(self,*args,**kwargs):
super (testForm,self ).__init__(*args,**kwargs)
self.fields['tst'].queryset = someData.objects.filter(name__icontains="1")
I know this is old; but its one of the first Google search results so I thought I would add how I found to do it.
class CustomModelFilter(forms.ModelChoiceField):
def label_from_instance(self, obj):
return "%s %s" % (obj.column1, obj.column2)
class CustomForm(ModelForm):
model_to_filter = CustomModelFilter(queryset=CustomModel.objects.filter(active=1))
class Meta:
model = CustomModel
fields = ['model_to_filter', 'field1', 'field2']
Where 'model_to_filter' is a ForiegnKey of the "CustomModel" model
Why I like this method:
in the "CustomModelFilter" you can also change the default way that the Model object is displayed in the ChoiceField that is created, as I've done above.
is the best answer:
BookDemoForm.base_fields['location'] = forms.ModelChoiceField(widget=forms.Select(attrs={'class': 'form-control select2'}),queryset=Location.objects.filter(location_for__fuel=True))

Django: disallow can_delete on GenericStackedInline

I've built this model which contains a generic foreign key:
class MyModel(models.Model):
content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
object_id = models.PositiveIntegerField(_('object id'))
content_object = generic.GenericForeignKey('content_type', 'object_id')
Next I've made a generic stacked inline to put it in any ModelAmin class:
class MyModelStackedInline(generic.GenericStackedInline):
model = MyModel
formset = generic.generic_inlineformset_factory(MyModel, can_delete=False)
extra = 0
class SomeOhterModelAdmin(admin.ModelAdmin):
inlines = [MyModelStackedInline]
However, despite the can_delete=False arg passed by in generic_inlineformset_factory, I always see a Delete checkbox in my admin change_form.
Here is an example: http://img8.imageshack.us/img8/3323/screenshotbe.png
Do you know how to remove this checkbox ?
Thank you :)
Maybe It is a post '09 feature, but you can specify that without overriding the __init__() method :
class StupidCarOptionsInline(admin.StackedInline):
model = models.StupidOption
form = StupidCarOptionAdminForm
extra = 0
can_delete = False
Update 2016: as per Stan's answer below, modern versions of django let you set can_delete = True on the GenericStackedInline subclass, as it inherits from InlineModelAdmin
I've run into this before - for some reason passing can_delete as an argument doesn't work, but setting it in the formset's init method does. Try this:
class MyInlineFormset(generic.generic_inlineformset_factory(MyModel)):
def __init__(self, *args, **kwargs):
super(MyInlineFormset, self).__init__(*args, **kwargs)
self.can_delete = False
then in your admin inline class:
class MyModelStackedInline(generic.GenericStackedInline):
model = MyModel
formset = MyInlineFormset
extra = 0

How does Django Know the Order to Render Form Fields?

If I have a Django form such as:
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField()
sender = forms.EmailField()
And I call the as_table() method of an instance of this form, Django will render the fields as the same order as specified above.
My question is how does Django know the order that class variables where defined?
(Also how do I override this order, for example when I want to add a field from the classe's init method?)
New to Django 1.9 is Form.field_order and Form.order_fields().
# forms.Form example
class SignupForm(forms.Form):
password = ...
email = ...
username = ...
field_order = ['username', 'email', 'password']
# forms.ModelForm example
class UserAccount(forms.ModelForm):
custom_field = models.CharField(max_length=254)
def Meta:
model = User
fields = ('username', 'email')
field_order = ['username', 'custom_field', 'password']
[NOTE: this answer is now pretty completely outdated - please see the discussion below it, and more recent answers].
If f is a form, its fields are f.fields, which is a django.utils.datastructures.SortedDict (it presents the items in the order they are added). After form construction f.fields has a keyOrder attribute, which is a list containing the field names in the order they should be presented. You can set this to the correct ordering (though you need to exercise care to ensure you don't omit items or add extras).
Here's an example I just created in my current project:
class PrivEdit(ModelForm):
def __init__(self, *args, **kw):
super(ModelForm, self).__init__(*args, **kw)
self.fields.keyOrder = [
'super_user',
'all_districts',
'multi_district',
'all_schools',
'manage_users',
'direct_login',
'student_detail',
'license']
class Meta:
model = Privilege
I went ahead and answered my own question. Here's the answer for future reference:
In Django form.py does some dark magic using the __new__ method to load your class variables ultimately into self.fields in the order defined in the class. self.fields is a Django SortedDict instance (defined in datastructures.py).
So to override this, say in my example you wanted sender to come first but needed to add it in an init method, you would do:
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField()
def __init__(self,*args,**kwargs):
forms.Form.__init__(self,*args,**kwargs)
#first argument, index is the position of the field you want it to come before
self.fields.insert(0,'sender',forms.EmailField(initial=str(time.time())))
Fields are listed in the order they are defined in ModelClass._meta.fields. But if you want to change order in Form, you can do by using keyOrder function.
For example :
class ContestForm(ModelForm):
class Meta:
model = Contest
exclude=('create_date', 'company')
def __init__(self, *args, **kwargs):
super(ContestForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = [
'name',
'description',
'image',
'video_link',
'category']
With Django >= 1.7 your must modify ContactForm.base_fields as below:
from collections import OrderedDict
...
class ContactForm(forms.Form):
...
ContactForm.base_fields = OrderedDict(
(k, ContactForm.base_fields[k])
for k in ['your', 'field', 'in', 'order']
)
This trick is used in Django Admin PasswordChangeForm: Source on Github
Form fields have an attribute for creation order, called creation_counter. .fields attribute is a dictionary, so simple adding to dictionary and changing creation_counter attributes in all fields to reflect new ordering should suffice (never tried this, though).
Use a counter in the Field class. Sort by that counter:
import operator
import itertools
class Field(object):
_counter = itertools.count()
def __init__(self):
self.count = Field._counter.next()
self.name = ''
def __repr__(self):
return "Field(%r)" % self.name
class MyForm(object):
b = Field()
a = Field()
c = Field()
def __init__(self):
self.fields = []
for field_name in dir(self):
field = getattr(self, field_name)
if isinstance(field, Field):
field.name = field_name
self.fields.append(field)
self.fields.sort(key=operator.attrgetter('count'))
m = MyForm()
print m.fields # in defined order
Output:
[Field('b'), Field('a'), Field('c')]
If either fields = '__all__':
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = '__all__'
or exclude are used:
class PartialAuthorForm(ModelForm):
class Meta:
model = Author
exclude = ['title']
Then Django references the order of fields as defined in the model. This just caught me out, so I thought I'd mention it. It's referenced in the ModelForm docs:
If either of these are used, the order the fields appear in the form will be the order the fields are defined in the model, with ManyToManyField instances appearing last.
As of Django 1.7 forms use OrderedDict which does not support the append operator. So you have to rebuild the dictionary from scratch...
class ChecklistForm(forms.ModelForm):
class Meta:
model = Checklist
fields = ['name', 'email', 'website']
def __init__(self, guide, *args, **kwargs):
self.guide = guide
super(ChecklistForm, self).__init__(*args, **kwargs)
new_fields = OrderedDict()
for tier, tasks in guide.tiers().items():
questions = [(t['task'], t['question']) for t in tasks if 'question' in t]
new_fields[tier.lower()] = forms.MultipleChoiceField(
label=tier,
widget=forms.CheckboxSelectMultiple(),
choices=questions,
help_text='desired set of site features'
)
new_fields['name'] = self.fields['name']
new_fields['email'] = self.fields['email']
new_fields['website'] = self.fields['website']
self.fields = new_fields
For future reference: things have changed a bit since newforms. This is one way of reordering fields from base formclasses you have no control over:
def move_field_before(form, field, before_field):
content = form.base_fields[field]
del(form.base_fields[field])
insert_at = list(form.base_fields).index(before_field)
form.base_fields.insert(insert_at, field, content)
return form
Also, there's a little bit of documentation about the SortedDict that base_fields uses here: http://code.djangoproject.com/wiki/SortedDict
The easiest way to order fields in django 1.9 forms is to use field_order in your form Form.field_order
Here is a small example
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField()
sender = forms.EmailField()
field_order = ['sender','message','subject']
This will show everything in the order you specified in field_order dict.
Using fields in inner Meta class is what worked for me on Django==1.6.5:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Example form declaration with custom field order.
"""
from django import forms
from app.models import AppModel
class ExampleModelForm(forms.ModelForm):
"""
An example model form for ``AppModel``.
"""
field1 = forms.CharField()
field2 = forms.CharField()
class Meta:
model = AppModel
fields = ['field2', 'field1']
As simple as that.
I've used this to move fields about:
def move_field_before(frm, field_name, before_name):
fld = frm.fields.pop(field_name)
pos = frm.fields.keys().index(before_name)
frm.fields.insert(pos, field_name, fld)
This works in 1.5 and I'm reasonably sure it still works in more recent versions.
To add something, you can use this (Django 3+):
class ...(forms.ModelForm):
field = ...
class Meta:
model = Xxxxxx
fields = '__all__'
field_order = ['field', '__all__']
__all__ works
It has to do with the meta class that is used in defining the form class. I think it keeps an internal list of the fields and if you insert into the middle of the list it might work. It has been a while since I looked at that code.
None of these answers worked for me, Actually, you do not have to do anything custom, you can just order the fields in the order you want in your Model class. For eg ... the below code
from django.db import models
class Student(models.Model):
class Meta:
verbose_name_plural = "categories"
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=300)
nick_name = models.CharField(max_length=300)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
Your admin interface for model Will display the fields exactly in the same order in which you have declared in this case it will be (id, name, nick_name )
The order of the fields in the form depends on the order of the enumeration in the View , tested in Django 4.0.5.
class Sec_CreateView(CreateView):
model = Sec
template_name = 'forms/sec_create.html'
fields = ['rto', 'ssid', 'lic', 'IPv4', 'vlans']

Categories