Accessing Attributes of a CheckboxSelectMultiple checkbox - django

Short Version
In the Django template language, how do I access the attributes of a given checkbox within a CheckboxSelectMultiple widget?
Long Version
The attributes of a typical Django widget can be accessed easily:
{% for field in form %}
{{ field.widget.attrs.something }}
{% endfor %}
However, this method isn't working for a checkbox within a CheckboxSelectMultiple widget.
I have a customized CheckboxSelectMultiple widget which I'm using to display a ManyToMany ModelForm field. The customized widget adds additional attributes to each checkbox in the create_option method.
The additional attributes display appropriately within the HTML of the input element:
<input type="checkbox" name="questions" value="22" id="id_questions_12" category="Category Name" category_number="3" question="Question Name" question_number="4">
I need to access these additional attributes for purposes of display and organizing the form fields.

I turned back to this after letting it sit for a week or so. After playing around some more and reading into the docs for BoundField (and BoundWidget specifically), I found out how to access the attrs of an individual checkbox in a CheckboxSelectMultiple widget:
{% for field in form %}
{% for check in field.subwidgets %}
{% for a in check.data.attrs %}

I was able to use the same technique given in this answer. It works perfectly for CheckboxSelectMultiple although it is not used in the answer.
I saved this in my project's forms.py:
from django.forms.widgets import CheckboxSelectMultiple, Select
class SelectWithAttrs(Select):
"""
Select With Option Attributes:
Subclass of Django's Select widget that allows attributes in options,
e.g. disabled="disabled", title="help text", class="some classes",
style="background: color;", etc.
Pass a dict instead of a string for its label:
choices = [ ('value_1', 'label_1'),
...
('value_k', {'label': 'label_k', 'foo': 'bar', ...}),
... ]
The option k will be rendered as:
<option value="value_k" foo="bar" ...>label_k</option>
"""
def create_option(self, name, value, label, selected, index,
subindex=None, attrs=None):
if isinstance(label, dict):
opt_attrs = label.copy()
label = opt_attrs.pop('label')
else:
opt_attrs = {}
option_dict = super().create_option(
name, value, label, selected, index,
subindex=subindex, attrs=attrs)
for key, val in opt_attrs.items():
option_dict['attrs'][key] = val
return option_dict
class CheckboxSelectMultipleWithAttrs(
SelectWithAttrs, CheckboxSelectMultiple):
pass
Here is a working snippet from a project of mine that uses this example. The stuff in the beginning isn't really important, but it shows how to build your attributes dict and pass it into your choices.
from django import forms
from django.whatever import other_stuff
from project_folder import forms as project_forms
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['employees']
def __init__(self, *args, **kwargs)
super().__init__(*args, **kwargs)
self.fields['employees'].queryset =\
self.company.employee_set.filter(is_active=True)
existing_crews_employees = []
for crew in existing_job_crews:
crew_employees =\
[employee.__str__() for employee in crew.employees.all()]
existing_crews_employees.append({'crew_name': crew.crewtype.name,
'employees': crew_employees})
employees_choices = []
for (index, choice) in enumerate(self.fields['employees'].choices):
# loop over each choice and set proper paramaters for employees
# that are unassigned/on the current crew/on a different crew
employee_in_crew = False
employee_name = choice[1]
for crew_and_employees in existing_crews_employees:
for employee in crew_and_employees['employees']:
if employee_name == employee.__str__():
crew_name = crew_and_employees['crew_name']
if self.obj and self.obj.crewtype.name == crew_name:
# check the box if employee in current crew
employees_choices.append((choice[0], {
'label': choice[1],
'checked': True,
'id': f'id_employees_{choice[0].instance.id}'
}))
else:
# disable the choice if employee in another crew
employees_choices.append((choice[0], {
'label':
employee_name + f" (on Crew: {crew_name})",
'disabled': True}))
employee_in_crew = True
# for valid entries, ensure that we pass the proper ID
# so that clicking the label will also check the box
if not employee_in_crew:
employees_choices.append((choice[0], {
'label': choice[1],
'id': f'id_employees_{choice[0].instance.id}'}))
self.fields['employees'].widget = project_forms.CheckboxSelectMultipleWithAttrs(
choices=employees_choices)
There are two important things to keep in mind when using this technique:
Ensure that you pass the id into your attrs for your clickable options, otherwise your labels will not check the proper boxes when they are clicked.
This method currently requires initial values to be set using the new attributes dict. Ensure that you pass the 'checked': True key-value pair to any boxes that should be checked.

Related

Django Admin: Add Hyperlink to related model

I would like to add a hyperlink to the related model Training
It would be nice to have declarative solution, since I want to use
this at several places.
The "pencil" icon opens the related model in a popup window. That's not what I want. I want a plain hyperlink to the related model.
BTW, if you use "raw_id_fields", then the result is exactly what I was looking for: There is a hyperlink to the corresponding admin interface of this ForeignKey.
Update Jan 4, 2023
From Django 4.1, this becomes a part of the official build (related PR).
Related widget wrappers now have a link to object’s change form
Result
Previous Answer
The class named RelatedFieldWidgetWrapper is showing the icons on the Django Admin page and thus you need to override the same. So, create a custom class as below,
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
class CustomRelatedFieldWidgetWrapper(RelatedFieldWidgetWrapper):
template_name = 'admin/widgets/custom_related_widget_wrapper.html'
#classmethod
def create_from_root(cls, root_widget: RelatedFieldWidgetWrapper):
# You don't need this method of you are using the MonkeyPatch method
set_attr_fields = [
"widget", "rel", "admin_site", "can_add_related", "can_change_related",
"can_delete_related", "can_view_related"
]
init_args = {field: getattr(root_widget, field) for field in set_attr_fields}
return CustomRelatedFieldWidgetWrapper(**init_args)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
rel_opts = self.rel.model._meta
info = (rel_opts.app_label, rel_opts.model_name)
context['list_related_url'] = self.get_related_url(info, 'changelist')
return context
See, the context variable list_related_url is the relative path that we need here. Now, create an HTML file to render the output,
#File: any_registered_appname/templates/admin/widgets/custom_related_widget_wrapper.html
{% extends "admin/widgets/related_widget_wrapper.html" %}
{% block links %}
{{ block.super }}
- Link To Related Model -
{% endblock %}
How to connect?
Method-1 : Monkey Patch
# admin.py
# other imports
from ..widgets import CustomRelatedFieldWidgetWrapper
from django.contrib.admin import widgets
widgets.RelatedFieldWidgetWrapper = CustomRelatedFieldWidgetWrapper # monket patch
Method-2 : Override ModelAdmin
# admin.py
class AlbumAdmin(admin.ModelAdmin):
hyperlink_fields = ["related_field_1"]
def formfield_for_dbfield(self, db_field, request, **kwargs):
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
if db_field.name in self.hyperlink_fields:
formfield.widget = CustomRelatedFieldWidgetWrapper.create_from_root(
formfield.widget
)
return formfield
Result
There are several ways to go. Here is one.
Add some javascript that changes the existing link behavior. Add the following script at the end of the overridden admin template admin/widgets/related_widget_wrapper.html. It removes the class which triggers the modal and changes the link to the object.
It will only be triggered for id_company field. Change to your needs.
{% block javascript %}
<script>
'use strict';
{
const $ = django.jQuery;
function changeEditButton() {
const edit_btn = document.getElementById('change_id_company');
const value = edit_btn.previousElementSibling.value;
const split_link_template = edit_btn.getAttribute('data-href-template').split('?');
edit_btn.classList.remove('related-widget-wrapper-link');
edit_btn.setAttribute('href', split_link_template[0].replace('__fk__', value));
};
$(document).ready(function() {
changeEditButton();
$('body').on('change', '#id_company', function(e) {
changeEditButton();
});
});
}
</script>
{% endblock %}
This code can also be modified to be triggered for all edit buttons and not only for the company edit button.

Added disabled attribute to rendered html , and now form is not valid

Django 1.9.7
class PlaceName(models.Model):
one = ForeignKey(Place, on_delete=models.CASCADE)
...
class PlaceNameCreate(CreateView):
model = PlaceName
fields = ['one', 'name', 'since', ]
#register.filter
def disable_one(value):
"""
Disable selection of "one" field in case of one-to-many relations.
"""
value = value.replace('name="one"', 'name="one" disabled')
value = mark_safe(value)
return value
<form method="post" action="">
{% csrf_token %}
<table>
{{ form.as_table|disable_one }}
</table>
<input type="submit" value="Сохранить"/>
</form>
This PlaceName is a subordinate reference book, main one is Place. From PlaceView(DetailView) I call PlaceNameCreate, transfer a signed value of Place and would not like to allow users to change it.
Well, this doesn't work. On the page for this ForeignKey there is a message "This field is required".
If I remove the disable_one filter from the template, everything works: saves the model instance successfully.
If I dig to FormMixin and there in the method get_form I can see that
dict: {'instance': None, 'data': <QueryDict: {'since': ['2'], 'csrfmiddlewaretoken': ['6NPIva2Z7XEZzuG2xgWB3sAz0N1SPqZc'], 'name': ['2']}>, 'files': <MultiValueDict: {}>, 'prefix': None, 'initial': {}}
This means that one field is empty. Not surprising that the form is not valid.
Without the filter:
dict: {'instance': None, 'data': <QueryDict: {'one': ['3'], 'since': ['3'], 'csrfmiddlewaretoken': ['6NPIva2Z7XEZzuG2xgWB3sAz0N1SPqZc'], 'name': ['2']}>, 'files': <MultiValueDict: {}>, 'prefix': None, 'initial': {}}
I checked the rendered html. The only difference - as expected - is that attribute "disabled".
Any variant would be acceptable, my way with the filter is not a dogma. I just thought it to be the easiest one.
Could you give me a hint how to achieve my current goal: the value shold be shown to the user, whereas selection of other variants is impossible.
Added later:
I tried readonly as suggested. Not working. This is the rendered html.
<!DOCTYPE html>
...
<select id="id_one" name="one" readonly>
<option value="">---------</option>
<option value="3" selected="selected">664011</option>
</select>
Well, in the browser a user can still select between two values ("---------" and "664011".
Added again
This question was considered as duplicate. But this is not. This is a pure
Django question. What was suggested for me as a solution was like this: disable element, but keep the value in a hidden input. Then use JQuery. This is not what I'd like to do.
Possible solution
I just decided to deprive the user of any other choices. If there will be no better solution, this is acceptable.
def get_form(self, form_class):
form = super(PlaceNameCreate, self).get_form(form_class)
one = self.initial.get('one')
choices = ((one.id, one),)
form.fields['one'].widget = forms.Select(choices=choices)
return form
disabled input values aren't submitted with the form. From MDN:
disabled
This Boolean attribute indicates that the form control is not available for interaction. In particular, the click event will not be dispatched on disabled controls. Also, a disabled control's value isn't submitted with the form.
Instead, you should consider using readonly:
readonly
This Boolean attribute indicates that the user cannot modify the value of the control.
value = value.replace('name="one"', 'name="one" readonly')
def get_form(self, form_class):
"""
For one-to-many relationships.
Deprive user of any choice.
"""
form = super(CreateSubordinate, self).get_form(form_class)
one = self.initial.get('one')
if one:
choices = ((one.id, one),)
form.fields['one'].widget = forms.Select(choices=choices)
return form

django modelform property hidden field

I'm manually displaying my formset as a table, with each form being looped over. At the bottom of each form I include the hidden fields like:
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
But the problem is that I am also including properties in my form like:
class AllocationForm(forms.ModelForm):
name = forms.CharField(widget=forms.TextInput(attrs={'size': '15'}))
def __init__(self, *args, **kwargs):
super(AllocationForm, self).__init__(*args, **kwargs)
if self.instance:
self.fields['total_budgeted'] = self.instance.total_budgeted()
self.fields['total_budgeted_account_percent'] = self.instance.total_budgeted_account_percent()
self.fields['actual_spent'] = self.instance.actual_spent()
self.fields['actual_spent_account_percent'] = self.instance.actual_spent_account_percent()
self.fields['total_budgeted_category_percent'] = self.instance.total_budgeted_category_percent()
self.fields['actual_spent_category_percent'] = self.instance.actual_spent_category_percent()
class Meta:
model = Allocation
exclude = {'created', 'modified', 'source_account'}
And this works in the sense that I definitely see the properties being called, however they display as nothing so that's another issue.
The problem is when I keep the hidden fields in the template I will get errors such as 'int' object has no attribute 'get_bound_field' and so on depending on the return type of the property/method call.
My question is first: is there a check I can do to see if the field is a property in the template and therefore skip over it?
It may have something to do with how I'm using the property since in fact every property is displaying nothing (but I see it callback), so second would be about how to display the properties.
Well I am in the next step of the problem, but i have success in generating true form fields. In place of:
if self.instance:
self.fields['total_budgeted'] = self.instance.total_budgeted()
You can write:
if self.instance:
self.fields['total_budgeted'] = form.CharField(
initial=self.instance.total_budgeted(),
widget=HiddenInput()
)
In this code you I instantiate the form Field as CharField, you can use the FormField you want, and I hide it by choosing the Hidden input widget.

Django - Pass object into custom form field

I am trying to create a rating form field using the jquery code here
So far I have it working fine but what I need to do is pass in a url based on the object I am trying to rate. See line $.post("URL TO GO HERE", {rating: value}, function(db) is code below. The url would be something like /rating/object_id where object_id would be the pk of the object I want to rate. What is the best way to pass in a object id so I can use it. Would I need to pass it into RatingField first and then pass it from there into StarWidget?
class StarWidget(widgets.Select):
"""
widget to show stars which the user can click on to rate
"""
class Media:
css = {
'all': ('css/ui.stars.css',)
}
js = ('js/ui.stars.js',)
def render(self, name, value, attrs=None):
output = super(StarWidget, self).render(name, value, attrs)
jquery = u"""
<div id="stars-rating" class="rating_section">
%s
<span id="caption"></span>
</div>
<script type="text/javascript">
$(function(){
$("#stars-rating").stars({
inputType: "select",
captionEl: $("#caption"),
cancelShow: false,
callback: function(ui, type, value)
{
// Hide Stars while AJAX connection is active
$("#stars-rating").hide();
$("#loader").show();
$.post("URL TO GO HERE", {rating: value}, function(db)
{
$("#loader").hide();
$("#stars-rating").show();
}, "json");
}
});
});
</script>
""" % (output)
return mark_safe(jquery)
class RatingField(forms.ChoiceField):
"""
rating field. changes the widget and sets the choices based on the model
"""
widget = StarWidget
def __init__(self, *args, **kwargs):
super(RatingField, self).__init__(*args, **kwargs)
self.label = "Rating:"
self.initial = 3
self.choices = Rating.RATING_CHOICES
I know the built-in fields are done this way, but it's really not good practice to embed large amounts of HTML or JS into the Python code. Instead, create a separate template fragment which is rendered by the field's render method. You can pass in the object's ID for use in the template {% url %} function, or just pass in the entire URL via call to reverse.

Django: How to check if there are field errors from within custom widget definition?

I'd like to create widgets that add specific classes to element markup when the associated field has errors.
I'm having a hard time finding information on how to check whether a field has errors associated with it, from within widget definition code.
At the moment I have the following stub widget code (the final widget will use more complex markup).
from django import forms
from django.utils.safestring import mark_safe
class CustomTextWidget(forms.Widget):
def render(self, name, value, attrs):
field_has_errors=False # change to dynamically reflect field errors, somehow
if field_has_errors:
error_class_string="error"
else:
error_class_string=""
return mark_safe(
"<input type=\"text\" class=\"%s\" value=\"%s\" id=\"id_%s\" name=\"%s\">" % (error_class_string, value, name, name)
)
Can anyone shed light on a sensible way to populate the field_has_errors Boolean here? (or perhaps suggest a better way to accomplish what I'm trying to do). Thanks in advance.
As Jason says, the widget has no access to the field itself. I think a better solution though is to use the cascading nature of CSS.
{% for field in form %}
<div class="field{% if field.errors %} field_error{% endif %}">
{{ field }}
</div>
{% endfor %}
Now in your CSS you can do:
div.field_error input { color: red }
or whatever you need.
The widget has no knowledge of the field to which it is being applied. It is the field that maintains information about errors. You can check for error_messages in the init method of your form, and inject an error class to your widget accordingly:
class YourForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(YourForm, self).__init__(*args, **kwargs)
attrs = {}
if self.fields['your_field'].error_messages is not None:
attrs['class'] = 'errors'
self.fields['your_field'].widget = YourWidget(attrs=attrs)