Search code examples
djangodjango-formsdjango-templatesinline-formset

Customizing inlineformset choice in Django template


I've got some forms I'm trying to customize.

I render the fields manually - and it all works fine until get to a particular field (which is an InlineFormset itself). I'm trying to customize those options but can't seem to figure out how to do so.

my forms.py looks like this:

class SummativeScoreForm(forms.ModelForm):
    subdomain_proficiency_level = forms.ModelChoiceField(
        empty_label="Undecided",
        queryset=SubdomainProficiencyLevel.objects.none(),
        widget=forms.RadioSelect,
        required=False,
    )

    def __init__(self, request, *args, **kwargs):
        super(SummativeScoreForm, self).__init__(*args, **kwargs)
        if self.instance:
            if request.user == self.instance.summative.employee:
                self.fields["subdomain_proficiency_level"].disabled = True
        self.fields[
            "subdomain_proficiency_level"
        ].queryset = SubdomainProficiencyLevel.objects.filter(
            subdomain=self.instance.subdomain
        )
        self.fields[
            "subdomain_proficiency_level"
        ].label = f"""
        {self.instance.subdomain.character_code}:
        {self.instance.subdomain.short_description}
        """

    class Meta:
        model = SummativeScore
        fields = "__all__"


SummativeScoreInlineFormset = inlineformset_factory(
    Summative,
    SummativeScore,
    fields=("subdomain_proficiency_level",),
    can_delete=False,
    extra=0,
    form=SummativeScoreForm,
)

My template for summative_score_form looks like this:

            <form method="post" novalidate>
                {% csrf_token %}
                    {% include "myapp/includes/summative_score_response_formset_snippet.html" with formset=form %}
                <button type="submit" class="btn btn-primary"><i class="fal fa-clipboard-check"></i> Submit Updated Scores</button>
            </form>

The summative_score_response_formset_snippet looks like this:

{{ formset.management_form }}
{% for formset_form in formset.forms %}
    {% if formset_form.non_field_errors %}
        <ul>
            {% for error in formset_form.non_field_errors %}
                <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    {% for hidden_field in formset_form.hidden_fields %}
        {% if hidden_field.errors %}
            <ul>
                {% for error in hidden_field.errors %}
                    <li>
                        (Hidden field {{ hidden_field.name }}) {{ error }}
                    </li>
                {% endfor %}
            </ul>
        {% endif %}

        {{ hidden_field }}

    {% endfor %}
    {% for field in formset_form.visible_fields %}
        {% if field.name == 'subdomain_proficiency_level' %}
            <label class="form-check-label" for="{{ field.id_for_label }}">
                {{ field.label }}
            </label>
            <ul id="{{ field.auto_id }}" class="form-check mt-2">
                {% for choice in formset_form.subdomain_proficiency_level %}
                    <div class="form-check">
                        <!--
                        THIS IS THE PART I WOULD LIKE TO CUSTOMIZE:
                        Unsatisfactory (name) Lorum Ipsum (description)
                        Satisfactory (name) Lorum Ipsum (description)
                        Excellent (name) Lorum Ipsum (description)
                        CURRENTLY IT ONLY SHOWS THE NAME
                         -->
                        {{ choice }}
                    </div>
                {% endfor %}
            </ul>
            {% if field.help_text %}
                <p class="help">{{ field.help_text|safe }}</p>
            {% endif %}
        {% else %}
            {{ field }}
        {% endif %}
    {% endfor %}
{% endfor %}

My models look like this:

class SubdomainProficiencyLevel(CreateUpdateMixin):
    "THIS IS THE 'UNSATISFACTORY' (name) 'LORUM IPSUM' (description)"
    name = models.CharField(max_length=75)
    description = models.TextField()
    sequence = models.IntegerField()

    class Meta:
        ordering = ["sequence"]
        verbose_name = "Subdomain Rank"
        verbose_name_plural = "Subdomain Ranks"

    def __str__(self):
        """
        THIS IS WHAT IS 'CHOICE' IN THE FORM
        I'm trying to edit this to add styles to the self.description on the form
        """
        return f"{self.name}"

class SummativeScore(CreateUpdateMixin, CreateUpdateUserMixin):
    summative = models.ForeignKey(Summative, on_delete=models.PROTECT)
    subdomain = models.ForeignKey(Subdomain, on_delete=models.PROTECT)
    subdomain_proficiency_level = models.ForeignKey(
        SubdomainProficiencyLevel,
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )

    class Meta:
        ordering = ["subdomain__character_code"]
        verbose_name = "SummativeScore"
        verbose_name_plural = "SummativeScores"

    def __str__(self):
        """Unicode representation of SummativeScore."""
        return f"{self.subdomain_proficiency_level}"

The view is a Class Based FormView

class SummativeScoreFormView(
    LoginRequiredMixin,
    UserIsObserverOrObserveeMixin,
    SingleObjectMixin,
    FormView,
):

    model = Summative
    template_name = "myapp/summative_score_form.html"
    pk_url_kwarg = "summative_id"

    def get(self, request, *args, **kwargs):
        summative_id = kwargs.pop("summative_id")
        self.object = self.get_object(
            queryset=Summative.objects.filter(id=summative_id)
        )
        return super().get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        summative_id = kwargs.pop("summative_id")
        self.object = self.get_object(
            queryset=Summative.objects.filter(id=summative_id)
        )
        return super().post(request, *args, **kwargs)

    def get_form(self, form_class=None):
        formset = SummativeScoreInlineFormset(
            **self.get_form_kwargs(), instance=self.object
        )
        return formset

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs["form_kwargs"] = {"request": self.request}
        return kwargs

    def form_valid(self, form):
        form.save()
        messages.success(self.request, "Changes were saved!")
        return super().form_valid(form)

    def form_invalid(self, form):
        return super().form_invalid(form)

    def get_success_url(self):
        user_id = self.kwargs["user_id"]
        summative_id = self.kwargs["summative_id"]
        return reverse(
            "myapp:summative_detail",
            kwargs={
                "user_id": user_id,
                "summative_id": summative_id,
            },
        )

As you can see in the template - I render the SubdomainProficiencyLevel objects with the template variable {{ choice }}

I have tried doing {{ choice.description }} or {{ choice.name }} <span class="bold">{{ choice.description }}</span> but then nothing displays.

I have also tried adjusting the __str__ method on the model - which changes there work, but do not render as HTML (just as a string as expected).

What is the best way to customize that in the HTML?


Solution

  • I ended up creating a custom radio button class (similar to the documentation)

    class CustomRadioSelect(forms.RadioSelect):
        def create_option(
            self, name, value, label, selected, index, subindex=None, attrs=None
        ):
            option = super().create_option(
                name, value, label, selected, index, subindex, attrs
            )
            if value:
                option["attrs"]["description"] = value.instance.description
            return option
    

    Using that in the form:

        subdomain_proficiency_level = forms.ModelChoiceField(
            empty_label="Undecided",
            queryset=SubdomainProficiencyLevel.objects.none(),
            widget=CustomRadioSelect(),
            required=False,
        )
    

    Then I could access it like this in the template:

    {{ choice.data.attrs.description }}