In my settings.py I have set
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
now in can add my own templates in:
<project>/templates/django/forms/widgets/
or
<app>/templates/django/forms/widgets/
this works great! However, what I can't find is where do I override the default html (form) label?
class TestForm(forms.Form):
first_name = forms.CharField(label="First name", max_length=50)
last_name = forms.CharField(label="Last name")
nick_name = forms.CharField(required=False)
The above form would render the labels like this:
<label for="id_first_name">First name:</label>
I want to render the label differently. So, I thought it would easy as adding a html label template: templates/django/forms/widgets/label.html
This doesn't work. Was going through the Django docs, but I can't find how to do this for labels. Apparently a label is not a widget.
https://docs.djangoproject.com/en/1.11/ref/forms/widgets/#built-in-widgets
My question, where/how do I change the default label?
Here is a working solution (tested on Django 3.1) :
Create a child class of Django's Form, and set LABEL_TEMPLATE to the template your want.
myapp/base_form.py
from django import forms
from django.forms.utils import flatatt
from django.utils.html import conditional_escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
LABEL_TEMPLATE = '<p{}>{}</p>'
class CustomLabelBoundField(forms.boundfield.BoundField):
def label_tag(self, contents=None, attrs=None, label_suffix=None):
"""
Wrap the given contents in a <label>, if the field has an ID attribute.
contents should be mark_safe'd to avoid HTML escaping. If contents
aren't given, use the field's HTML-escaped label.
If attrs are given, use them as HTML attributes on the <label> tag.
label_suffix overrides the form's label_suffix.
"""
contents = contents or self.label
if label_suffix is None:
label_suffix = (self.field.label_suffix if self.field.label_suffix is not None
else self.form.label_suffix)
# Only add the suffix if the label does not end in punctuation.
# Translators: If found as last label character, these punctuation
# characters will prevent the default label_suffix to be appended to the label
if label_suffix and contents and contents[-1] not in _(':?.!'):
contents = format_html('{}{}', contents, label_suffix)
widget = self.field.widget
id_ = widget.attrs.get('id') or self.auto_id
if id_:
id_for_label = widget.id_for_label(id_)
if id_for_label:
attrs = {**(attrs or {}), 'for': id_for_label}
if self.field.required and hasattr(self.form, 'required_css_class'):
attrs = attrs or {}
if 'class' in attrs:
attrs['class'] += ' ' + self.form.required_css_class
else:
attrs['class'] = self.form.required_css_class
attrs = flatatt(attrs) if attrs else ''
contents = format_html(LABEL_TEMPLATE, attrs, contents)
else:
contents = conditional_escape(contents)
return mark_safe(contents)
def get_bound_field(field, form, field_name):
"""
Return a BoundField instance that will be used when accessing the form
field in a template.
"""
return CustomLabelBoundField(form, field, field_name)
class CustomLabelForm(forms.Form):
def __getitem__(self, name):
"""Return a BoundField with the given name."""
try:
field = self.fields[name]
except KeyError:
raise KeyError(
"Key '%s' not found in '%s'. Choices are: %s." % (
name,
self.__class__.__name__,
', '.join(sorted(self.fields)),
)
)
if name not in self._bound_fields_cache:
self._bound_fields_cache[name] = get_bound_field(field, self, name)
return self._bound_fields_cache[name]
And inherit your form from CustomLabelForm :
myapp/forms.py
from django import forms
from myapp.base_form import CustomLabelForm
class TestForm(CustomLabelForm):
first_name = forms.CharField(label="First name", max_length=50)
last_name = forms.CharField(label="Last name")
nick_name = forms.CharField(required=False)
This produces <p for="id_first_name">First name:</p>
You could override __init__() method of the form:
def __init__(self, *args, **kwargs):
self.fields['field_name'].label = 'Your new label here'
Overriding existing form widgets in Django can actually be done.
First you need to add 'django.forms' to your INSTALLED_APPS to treat it as an app.
INSTALLED_APPS = [
...
"django.forms",
...
]
Then append FORM_RENDERER to your settings.py
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
More info on TemplatesSetting can be found on the Django docs.
After that, copy any widget files you'd like to override from the django/forms/widgets/ folder to your own projectname/templates/django/forms/widgets folder. Customize away!
I hope this helps.
Related
In the Django admin panel, all fields are saved to the database, except for the flags field of the SubscriberPlan model. That is, I can (un)check any flag and try to thus update a record, but the flag statuses won't be saved to the database.
If I run python manage.py shell, import SubscriberPlan, do something like
plan = SubscriberPlan.objects.all()[0],
plan.flags = "a"
plan.save()
then the database will be updated and the Active flag will be displayed in the Admin panel, but, still, it won't be possible to update it from the Admin panel.
So, how is it possible in Django to save this kind of a field to the database from the Admin panel? To be honest, I don't understand why it's not saved by default, while other fields are saved. It seems that the Admin panel, for some reason, doesn't pass the checkmark values in its form.
admin.py
from django.contrib import admin
from django.utils.safestring import mark_safe
class SubscriberPlanFlagsWidget(forms.Widget):
available_flags = (
('a', ('Active')),
('n', ('New')),
('p', ('Popular')),
def render(self, name, value, attrs=None, renderer=None):
html = []
for f in self.available_flags:
html.append('<li><input type="checkbox" id="flag_%(key)s" %(checked)s key="%(key)s"/><label for="flag_%(key)s">%(name)s</label></li>' % {
'key': f[0], 'name': f[1], 'checked': 'checked' if f[0] in value.lower() else ''})
html = '<input type="hidden" name="%s" value="%s"/><ul class="checkbox flags">%s</ul>' % (name, value, ''.join(html))
return mark_safe(html)
class SubscriberPlanAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'flags':
kwargs['widget'] = SubscriberPlanFlagsWidget
return super(SubscriberPlanAdmin, self).formfield_for_dbfield(db_field, **kwargs)
models.py
from django.db import models
class SubscriberPlan(models.Model):
name = models.CharField(max_length=50, verbose_name=("Name"))
price = models.DecimalField(max_digits=15, decimal_places=2,
verbose_name=("Price"))
flags = models.CharField(max_length=30, verbose_name=("Flags"),
default='', blank=True)
def _check_flag(self, name):
return name in self.flags.lower()
def active(self):
return self._check_flag('a')
def new(self):
return self._check_flag('n')
def popular(self):
return self._check_flag('p')
Your widget's render method translates the flags field into checkboxes on the form, but when the form is submitted you need to go the other direction, and translate the checkboxes back into flags. The widget doesn't know how to do that automatically. From looking at the django source code and the docs, you need to override the value_from_datadict method. Give this a try:
class SubscriberPlanFlagsWidget(forms.Widget):
...
def value_from_datadict(self, data, files, name):
value = ''
for f in self.available_flags:
if f[1] in data:
value += f[0]
return value
I am trying to get a file upload form field working in Django and the part I am having problems with is dynamically changing the form field required attribute. I have tried using "self.fields['field_name'].required=True' in the init method of the form but that isn't working for me.
I have looked at Django dynamically changing the required property on forms but I don't want to build several custom models and a custom render function for one form as surely it must be easier than that.
The reason I am trying to do this is because when a django form validates and has errors it doesn't pass any uploaded files back to the browser form for reediting. It will pass text areas and text inputs that didn't validate back to the form for reediting but not file uploads. I thought if I made the file upload fields mandatory for the first time the record is created mandatory and for subsequent times make them optional. That is basically what I am trying to do.
So here is what I have been trying so far:
In forms.py
from django.forms import fields
from .widgets import PDFUploadWidget, PlainTextWidget
class WQPDFField(fields.Field):
widget = PDFUploadWidget
def widget_attrs(self, widget):
attrs = super().widget_attrs(widget)
attrs['label'] = self.label
return attrs
def clean(self, *args, **kwargs):
return super().clean(*args, **kwargs)
In widgets.py
from django.forms import widgets
from django.utils.safestring import mark_safe
# We subclass from HiddenInput because we want to suppress the printing
# of the label and prefer to print it ourselves.
class PDFUploadWidget(widgets.HiddenInput):
template_name = 'webquest_widgets/widgets/pdf_upload.html'
input_type = 'file'
def __init__(self, *args, **kwargs):
style = 'visibility:hidden'
attrs = kwargs.pop('attrs', None)
if attrs:
attrs['style'] = style
else:
attrs = {'style':style}
attrs['accept'] = '.pdf'
print (attrs)
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
return context
#property
def is_hidden(self):
return True
class Media:
css = { 'all': ( 'css/pdfupload.css', ) }
js = ('js/pdfupload.js', )
and finally in forms.py
class WorkWantedForm(forms.Form):
category = forms.ChoiceField(choices=CHOICES)
about = forms.CharField(label="About Yourself", widget=forms.Textarea())
static1 = WQStaticField(text="Enter either phone or email")
phone = forms.CharField(required=False)
email = forms.EmailField(required=False)
cv = WQPDFField(label="Upload CV")
supporting_document = WQPDFField(label="Supporting Document (optional)", required=False)
I am not sure how to pass the "required" attribute to the custom field class after the initialisation of the form but before rendering the form as HTML.
This is my sample code (and see the screenshot):
models.py
class User(models.Model):
# the variable to take the inputs
user_name = models.CharField(max_length=100)
user_avatar = models.FileField(upload_to = 'images/%Y%m%d')
admin.py
class UserAdmin(admin.ModelAdmin):
exclude = ('user_avatar',)
readonly_fields = ('avatar_readonly',)
def avatar_readonly(self, instance):
value_link = "/mediafileupdown/download/" + str(instance.id)
value_desc = str(instance.user_avatar.name)
return format_html('{}', value_link, value_desc)
avatar_readonly.short_description = "User avatar (read only)"
avatar_readonly.allow_tags=True
screenshot
The problem is: i'd like obtain the 'Currently' user_avatar's link (that's a models.FileField field) via the my 'download' views and not as simple url. I can do that if the field is in the 'readonly_fields' list and so I can overide the html format ... in that case, on the change page, the 'avatar_readonly' link is redirected to the my 'download' view.
The question is: how can I get the same result (overide the html format) in a FileField (like my 'user_avatar') when is not in the readonly_fileds list?
Sorry for my english and many Thanks for the help.
You can use formfield_overrides to modify the AdminFileWidget. This AdminFileWidget can display images and mp4 files, but you can use only for images omitting the if file_name[-4:] == '.mp4':
part.
from django.db.models.fields.files import FileField
from django.contrib.admin.widgets import AdminFileWidget
class AdminMediaWidget(AdminFileWidget):
def render(self, name, value, attrs=None):
output = []
if value and getattr(value, "url", None):
image_url = value.url
file_name = str(value)
if file_name[-4:] == '.mp4':
output.append('<video width="320" height="240" controls>'
'<source src="{0}" type="video/mp4">'
'Your browser does not support the video tag.'
'</video>'.format(image_url))
else:
output.append('<a href="{0}" target="_blank">'
'<img height=200 src="{1}" alt="{2}" />'
'</a>'.format(image_url, image_url, file_name))
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(''.join(output))
class UserAdmin(admin.ModelAdmin):
formfield_overrides = {
FileField: {'widget': AdminMediaWidget},
}
exclude = ('user_avatar',)
readonly_fields = ('avatar_readonly',)
def avatar_readonly(self, instance):
value_link = "/mediafileupdown/download/" + str(instance.id)
value_desc = str(instance.user_avatar.name)
return format_html('{}', value_link, value_desc)
avatar_readonly.short_description = "User avatar (read only)"
avatar_readonly.allow_tags=True
Hope it helps!
Is there any chance to add link to change page of a Foreign key object next to 'raw_id_fields' in django admin?
I use raw_id_fields to reduce number of queries to mysql.
I could edit or add a foreign key object when I didn't use raw_id_fields.
How to do it with raw_id_fields?
#AntoinePinsard, I've tried the second solution. The field 'Edit My FK' was created but there is nothing except for a hyphen.
That's my modeladmin.
class FlatAdmin(admin.ModelAdmin):
inlines = [NeedInline]
readonly_fields = ('flat_house_edit_link',)
raw_id_fields = ('flat_house',)
list_display = ('show_date','show_block','show_house','show_rooms','show_price','show_stage')
list_filter = ('flat_house__house_block','flat_rooms')
search_fields = ('flat_house__id',)
field = ['flat_house','flat_house_edit_link','flat_owner','flat_price','flat_rooms','flat_total_sq','flat_life_sq','flat_kitchen_sq','flat_floors']
def flat_house_edit_link(self, instance):
if instance:
fk_id = instance.user_id
else:
fk_id = None
if fk_id:
opts = instance._meta.get_field('flat_house').rel.model._meta
related_url = reverse(
'admin:{}_{}_change/?_to_field=id&_popup=1'.format(
opts.ha,
opts.house,
),
args=[fk_id],
)
return format_html(
'<a target=_blank href="{}">Go!</a>', related_url)
else:
return "No related object"
flat_house_edit_link.short_description = "Change house"
admin.site.register(Flat,FlatAdmin)
Note This behavior is now built-in since Django 1.10.
You can create a custom widget to render this field as you would like to.
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.html import format_html
class ForeignKeyLinkedRawIdWidget(ForeignKeyRawIdWidget):
def render(self, name, value, attrs=None):
output = super().render(name, value, attrs)
try:
related_url = reverse(
'admin:{}_{}_change'.format(
self.rel.model._meta.app_label,
self.rel.model._meta.model_name,
),
args=[value],
)
except NoReverseMatch:
return output
return format_html('{output} edit',
output=output, url=related_url)
And use this widget in your form, rather than using raw_id_fields:
from myapp.forms.widgets import ForeignKeyLinkedRawIdWidget
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = '__all__'
widgets = {
'my_foreignkey': ForeignKeyLinkedRawIdWidget(),
}
class MyModelAdmin(admin.ModelAdmin):
form = MyForm
However, it would be much simpler to add it as a "fake" readonly_field below the actual field:
from django.core.urlresolvers import reverse
from django.utils.html import format_html
class MyModelAdmin(admin.ModelAdmin):
readonly_fields = ['my_foreignkey_link']
raw_id_fields = ['my_foreignkey']
fields = [..., 'my_foreignkey', 'my_foreignkey_link', ...]
def my_foreignkey_link(self, instance):
if instance:
fk_id = instance.my_foreignkey_id
else:
fk_id = None
if fk_id:
opts = instance._meta.get_field('my_foreignkey').rel.model._meta
related_url = reverse(
'admin:{}_{}_change'.format(
opts.app_label,
opts.model_name,
),
args=[fk_id],
)
return format_html(
'<a target=_blank href="{}">Go!</a>', related_url)
else:
return "No related object"
my_foreignkey_link.short_description = "Edit My FK"
Further reading: https://docs.djangoproject.com/en/1.9/ref/contrib/admin/#django.contrib.admin.ModelAdmin.readonly_fields
Here's a snippet page (updated for 6 years) that does it for me:
https://djangosnippets.org/snippets/2217/
Just use ImproveRawIdFieldsForm in place of ModelAdmin and all raw_id fields automatically links the object name displayed next to the id input.
I am using Django everyday now for three month and it is really great. Fast web application development.
I have still one thing that I cannot do exactly how I want to.
It is the SelectField and SelectMultiple Field.
I want to be able to put some args to an option of a Select.
I finally success with the optgroup :
class EquipmentField(forms.ModelChoiceField):
def __init__(self, queryset, **kwargs):
super(forms.ModelChoiceField, self).__init__(**kwargs)
self.queryset = queryset
self.to_field_name=None
group = None
list = []
self.choices = []
for equipment in queryset:
if not group:
group = equipment.type
if group != equipment.type:
self.choices.append((group.name, list))
group = equipment.type
list = []
else:
list.append((equipment.id, equipment.name))
But for another ModelForm, I have to change the background color of every option, using the color property of the model.
Do you know how I can do that ?
Thank you.
What you need to do, is to change the output which is controlled by the widget. Default is the select widget, so you can subclass it. It looks like this:
class Select(Widget):
def __init__(self, attrs=None, choices=()):
super(Select, self).__init__(attrs)
# choices can be any iterable, but we may need to render this widget
# multiple times. Thus, collapse it into a list so it can be consumed
# more than once.
self.choices = list(choices)
def render(self, name, value, attrs=None, choices=()):
if value is None: value = ''
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<select%s>' % flatatt(final_attrs)]
options = self.render_options(choices, [value])
if options:
output.append(options)
output.append('</select>')
return mark_safe(u'\n'.join(output))
def render_options(self, choices, selected_choices):
def render_option(option_value, option_label):
option_value = force_unicode(option_value)
selected_html = (option_value in selected_choices) and u' selected="selected"' or ''
return u'<option value="%s"%s>%s</option>' % (
escape(option_value), selected_html,
conditional_escape(force_unicode(option_label)))
# Normalize to strings.
selected_choices = set([force_unicode(v) for v in selected_choices])
output = []
for option_value, option_label in chain(self.choices, choices):
if isinstance(option_label, (list, tuple)):
output.append(u'<optgroup label="%s">' % escape(force_unicode(option_value)))
for option in option_label:
output.append(render_option(*option))
output.append(u'</optgroup>')
else:
output.append(render_option(option_value, option_label))
return u'\n'.join(output)
It's a lot of code. But what you need to do, is to make your own widget with an altered render method. It's the render method that determines the html that is created. In this case, it's the render_options method you need to change. Here you could include some check to determine when to add a class, which you could style.
Another thing, in your code above it doesn't look like you append the last group choices. Also you might want to add an order_by() to the queryset, as you need it to be ordered by the type. You could do that in the init method, so you don't have to do it all over when you use the form field.
render_option has been removed from Django 1.11 onwards. This is what I did to achieve this. A little bit of digging and this seems straightforward and neat. Works with Django 2.0+
class CustomSelect(forms.Select):
def __init__(self, attrs=None, choices=()):
self.custom_attrs = {}
super().__init__(attrs, choices)
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
index = str(index) if subindex is None else "%s_%s" % (index, subindex)
if attrs is None:
attrs = {}
option_attrs = self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
if selected:
option_attrs.update(self.checked_attribute)
if 'id' in option_attrs:
option_attrs['id'] = self.id_for_label(option_attrs['id'], index)
# setting the attributes here for the option
if len(self.custom_attrs) > 0:
if value in self.custom_attrs:
custom_attr = self.custom_attrs[value]
for k, v in custom_attr.items():
option_attrs.update({k: v})
return {
'name': name,
'value': value,
'label': label,
'selected': selected,
'index': index,
'attrs': option_attrs,
'type': self.input_type,
'template_name': self.option_template_name,
}
class MyModelChoiceField(ModelChoiceField):
# custom method to label the option field
def label_from_instance(self, obj):
# since the object is accessible here you can set the extra attributes
if hasattr(obj, 'type'):
self.widget.custom_attrs.update({obj.pk: {'type': obj.type}})
return obj.get_display_name()
The form:
class BookingForm(forms.ModelForm):
customer = MyModelChoiceField(required=True,
queryset=Customer.objects.filter(is_active=True).order_by('name'),
widget=CustomSelect(attrs={'class': 'chosen-select'}))
The output which I needed is as:
<select name="customer" class="chosen-select" required="" id="id_customer">
<option value="" selected="">---------</option>
<option value="242" type="CNT">AEC Transcolutions Private Limited</option>
<option value="243" type="CNT">BBC FREIGHT CARRIER</option>
<option value="244" type="CNT">Blue Dart Express Limited</option>
I run into this question many times when searching by
'how to customize/populate Django SelectField options'
The answer provided by Dimitris Kougioumtzis is quite easy
Hope it could help somebody like me.
# forms.py
from django.forms import ModelForm, ChoiceField
from .models import MyChoices
class ProjectForm(ModelForm):
choice = ChoiceField(choices=[
(choice.pk, choice) for choice in MyChoices.objects.all()])
# admin.py
class ProjectAdmin(BaseAdmin):
form = ProjectForm
....
You should not mess with form fields for adding some custom attributes to the rendered html tag. But you should subclass and add a these to the Widget.
From the docs: customizing-widget-instances
You can submit attrs dictionary to the form Widgets, that render as attributes on the output form widgets.
class CommentForm(forms.Form):
name = forms.CharField(
widget=forms.TextInput(attrs={'class':'special'}))
url = forms.URLField()
comment = forms.CharField(
widget=forms.TextInput(attrs={'size':'40'}))
Django will then include the extra attributes in the rendered output:
>>> f = CommentForm(auto_id=False)
>>> f.as_table()
<tr><th>Name:</th><td><input type="text" name="name" class="special"/></td></tr>
<tr><th>Url:</th><td><input type="text" name="url"/></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr>
http://code.djangoproject.com/browser/django/trunk/django/newforms/widgets.py?rev=7083
As seen under the class Select(Widget):, there is no way to add the style attribute to an option tag. To this, you will have to subclass this widget and add such functionality.
The class Select(Widget): definition only adds style attribute to the main select tag.