Strange form validation behaviour with AJAX

I have built a marking application which allows a marker to enter both a comment and a mark for a given element of a student’s work. There are 9 mark-comment pairs and I use a generic view and form to allow the marker to enter and save the comment and mark. The mark field in the form is a text field and I have a validation which checks if the value entered is between 0 and 100. This is to assist in the monitoring of the marking process. (Setting a default value of 0 requires the marking monitor to check if every 0 is a real 0 or a consequence of the work not yet being marked.)

I am using CrispyForms 2 and Bootstrap 5 to render the form on the page. The form validation process raises a ValidationError on the mark field if the entered mark is not between 0 and 100. My approach works as expected without AJAX.

To save a marker’s comments and mark to the database, if the marker forgets to press the “enter…” button on the view before navigating to another page, I have tried to use AJAX and “get”. The “get” is called when there is a change to the form. When using AJAX I see the behaviour outlined below.

Behaviour 1
1a - Enter an invalid mark and press the “enter …” button and an error message is generated on the mark field.
1b - Correct the invalid mark and exit the form, but do not use the “enter …” button but click at random on the page. As the form is changed AJAX does its stuff, the error flag is removed and the data are saved to database.
1c - Enter an invalid mark and click at random on the page. As the form is changed AJAX does its stuff and produces the error message seen in 1a.
This is the behaviour I want to see. If however, I do the following steps.

Behaviour 2
2a - Enter an invalid mark and click at random on the page. The mark field boundary is coloured red indicating the field is invalid but no error message text is attached to the invalid field.
2b - Enter an invalid mark and press the “enter …” button and an error message is generated on the mark field.
2c - Enter an invalid mark and click at random on the page. As the form is changed AJAX does its stuff and produces the error message seen in 2b.
I do not seem to be able to generate the error message under “get”, without first pressing the “enter…” button. I have tried appending an HTML tag similar to "<span>error text</span>" to the <div> that contains the mark field, but this results in the error message appearing twice.

Any thoughts would be greatly welcomed.

My view, form and AJAX call are below

class AjaxView(LoginRequiredMixin, GetMenuContextViewMixin, FormView):
    template_name = "marking/ajaxpage.html"
    model = StudentDetail
    form_class = AjaxMarkingForm

    def dispatch(self, request, *args, **kwargs):
        """Dispatch used to stop marker seeing students that are not on their list."""
        s_id = self.kwargs["pk"]
        stud = StudentDetail.objects.get(id=s_id)

        marker = self.request.user.marker
        allowed = stud.checkAllowed(marker)
        context = {"mid": stud.pcode.id}
        if not allowed:
            return render(self.request, "general/403.html", context=context, status=403)
        return super(AjaxView, self).dispatch(request, *args, **kwargs)

    def get(self, request, *args, **kwargs):
        print(self, "from get", self.get_initial())
        form = self.form_class
        if (
            self.request.headers.get("x-requested-with") == "XMLHttpRequest"
            and self.request.method == "GET"
        ):
            form = form(self.request.GET)
            if form.is_valid():
                # get the student id
                s_id = self.kwargs["pk"]
                # get the student object associated with the student id from database
                stud = StudentDetail.objects.get(id=s_id)
                # get the data from the form
                text_in = form.cleaned_data["text"]
                mark_in = form.cleaned_data["mark"]
                text_f = self.kwargs["section"]
                mark_f = text_f.replace("text", "mark")
                setattr(stud, text_f, text_in)
                setattr(stud, mark_f, mark_in)
                stud.save()
                return render(
                    request,
                    self.template_name,
                    {"form": form},
                )
            if not form.is_valid():
                return JsonResponse(form.errors, status=400)

        else:
            return render(
                request,
                self.template_name,
                {"form": form(self.get_initial())},
            )

    def get_initial(self, **kwargs):
        initial = super(AjaxView, self).get_initial(**kwargs)
        # get the student id
        s_id = self.kwargs["pk"]
        # get the student object associated with the student id from database
        stud = StudentDetail.objects.get(id=s_id)
        # get the text field name
        text_f = self.kwargs["section"]
        # make the associated mark field
        mark_f = text_f.replace("text", "mark")
        self.stud = stud
        self.text_f = text_f
        self.mark_f = mark_f
        initial["text"] = getattr(stud, text_f)
        initial["mark"] = getattr(stud, mark_f)
        return initial

    def get_context_data(self, **kwargs):
        context = super(AjaxView, self).get_context_data(**kwargs)
        return context

    def form_valid(self, form):
        stud = self.stud
        stud.markCommSave(form, self)
        return super(AjaxView, self).form_valid(form)

    def get_success_url(self, **kwargs):
        return reverse_lazy(
            "project:ajaxview",
            args=(self.kwargs["pk"], self.kwargs["section"]),
        )
class AjaxMarkingForm(forms.Form):
    def __init__(self, *args, **kwargs):
        super(AjaxMarkingForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper(self)
        self.helper.form_show_labels = False
        self.fields["mark"].label = ""
        self.fields["text"].label = False

    mark = forms.CharField(max_length=3, required=True)
    text = forms.CharField(
        required=True,
        widget=forms.Textarea(
            attrs={
                "rows": 4,
                "cols": 60,
                "placeholder": "Justification for mark",
                "label": "",
            }
        ),
    )

    def clean(self):
        cleaned_data = super().clean()
        markin = cleaned_data.get("mark")
        # check mark is between 0 - 100
        StudentDetail.checkMarkValue(markin, "mark", "form")

        return cleaned_data
$("#rdform").on("change",function () {
    var urlin = window.location.href; 
    var serializedData = $(this).serialize();
    $.ajax({
        type: 'GET',
        url: urlin,
        data: serializedData,
        success: function(response) {
        $("#id_mark").removeClass("is-invalid");
        $("#id_text").removeClass("is-invalid");
        },
        error: function(response)
        {
        $("#id_mark").addClass("is-invalid");
        },
        
    });
})

Welcome @nlangford1 !

Side Note: When posting code here, enclose the code between lines of three
backtick - ` characters. This means you’ll have a line of ```, then your code,
then another line of ```. This forces the forum software to keep your code
properly formatted. (I have taken the liberty of correcting your original posts.
Please remember to do this in the future.)

When you have a single line or portion of a line containing some text that also needs to be fenced, such as html or http links, you can use the single backtick - ` for that string. e.g. `<span>`