Django modelformset with modelform that has excluded fields with unique together but it is not working

I have a form where a user selects a player and a position and submits it, however there is a unique together on the model that says a player, user and draft session is unique. This does not work as the fields are not in the form as they are set manually later, this is not for the user to select. I tried looking into custom form validation but was unable to find a solution there, I also checked into full clean in the instances but I am not able to add to the form errors and return to the form. Any advise on how to overcome this?

I have the following models:

class DuringSeasonPlayerDraft(models.Model):
    start_datetime = models.DateTimeField()
    end_datetime = models.DateTimeField()
    owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    open = models.BooleanField(default=False)

    class Meta:
        ordering = ["open", "start_datetime"]

class DuringSeasonPlayerPick(models.Model):
    during_season_draft = models.ForeignKey(DuringSeasonPlayerDraft, on_delete=models.CASCADE)
    player = models.ForeignKey(Player, on_delete=models.CASCADE)
    position = models.ForeignKey(PlayerPosition, on_delete=models.SET_NULL, null=True, blank=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
    timestamp = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["player"]
        unique_together = [["during_season_draft", "player", "user"]]

    def unique_error_message(self, model_class, unique_check):
        if model_class == type(self) and unique_check == ("during_season_draft", "player", "user"):
            return "You cannot have duplicate players"
        else:
            return super(DuringSeasonPlayerPick, self).unique_error_message(model_class, unique_check)

Forms:

class DuringSeasonPlayerForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(DuringSeasonPlayerForm, self).__init__(*args, **kwargs)
        self.fields["player"] = forms.ModelChoiceField(queryset=Player.objects.filter(active=True))

    class Meta:
        model = DuringSeasonPlayerPick
        fields = ["player", "position"]

DuringSeasonPlayerPickFormset = modelformset_factory(DuringSeasonPlayerPick, form=DuringSeasonPlayerForm, extra=1, max_num=1, can_delete=True)

View:

def during_season_player_pick_list(request, pk):
    template_name = "fantasy_football/during_season_player_pick.html"
    
    draft = DuringSeasonPlayerDraft.objects.get(id=pk)
    picks = DuringSeasonPlayerPick.objects.filter(during_season_draft=draft, user=request.user)
    
    context = {}

    if request.method == "POST":
        player_picks_formset = DuringSeasonPlayerPickFormset(request.POST, request.FILES, queryset=picks)

        for form in player_picks_formset:
            if timezone.now() >= draft.end_datetime:
                form.add_error(None, "Cannot make changes after draft end datetime.")

        if player_picks_formset.is_valid():
            instances = player_picks_formset.save(commit=False)
            for i in instances:
                i.during_season_draft = draft
                i.user = request.user
                i.save()
            
            for i in player_picks_formset.deleted_objects:
                i.delete()
            
            return redirect("during_season_player_pick", pk=pk)

    elif request.method == "GET":
        player_picks_formset = DuringSeasonPlayerPickFormset(queryset=picks)
        
    context["player_picks_formset"] = player_picks_formset

    return render(request, template_name, context)

Template:

{% extends "base.html" %}

{% load widget_tweaks %}
{% load crispy_forms_tags %}

{% block content %}

<h6>During Season Player Picks</h6>
<form id="form-container" action="" method="post">
  {% csrf_token %}
  {{player_picks_formset|crispy }}
  <button id="add-form" type="button" class="btn btn-sm btn-secondary">Add Another Player</button>
  <button type="submit" class="btn btn-sm btn-primary">Save</button>
</form>
{% endblock %}

{% block javascript %}
<script>
  let playerForm = document.querySelectorAll(".multiField");
  let container = document.querySelector("#form-container");
  let addButton = document.querySelector("#add-form");
  let totalForms = document.querySelector("#id_form-TOTAL_FORMS");

  let formNum = playerForm.length-1;

  addButton.addEventListener('click', addForm);

  function addForm(e) {
    e.preventDefault()

    let newForm = playerForm[0].cloneNode(true);
    let formRegex = RegExp("form-(\\d){1}-","g");

    formNum++;
    newForm.innerHTML = newForm.innerHTML.replace(formRegex, `form-${formNum}-`);
    
    container.insertBefore(newForm, addButton);

    const player_select = document.querySelector(`#id_form-${formNum}-player`);
    const position_select = document.querySelector(`#id_form-${formNum}-position`);

    player_select.value = "";
    position_select.value = "";

    totalForms.setAttribute("value", `${formNum+1}`);
};
</script>
{% endblock %}

Welcome @darync2 !

When posting code, templates, or error messages here, please enclose the code (etc) 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’ve taken the liberty of editing your original post for this.)

1 Like

Hi Ken, do you have any advise on this scenario?

Well, you’re right. If those fields aren’t part of the form, then no form validation is going to occur using those fields.

I can think of a couple different possibilities - none that really jump out at me as being all that great - but seem to be workable.

  • Add those fields to the form, as disabled and hidden. (That at least gets those fields into the form to be used during validation.)

  • Store those values as extra attributes on the form, so that they would also be available during validation.

  • Catch the error in the section of the view where you’re trying to save the data, and return the rendered view at that point.

There might be others options, but if there are, they’re not coming to my mind.

Hi Ken,

I’d like to investigate this option further, after the formset save (commit=False) is called, how do i add errors back to the forms/formset to render a view again with the errors? I was unable to figure this out.

Appreciate any assistance on this.

Thanks

It’s this block of code that I think would need to be modified:

First, I’d assume you’d want to wrap this up in a transaction, such that when an entry failed, they’d all fail. This way, all entries would be rejected if there’s an error. (Or, you could try to save them all and just keep track of the forms with errors. A little more work, but possibly more useful.)

For you to have reached this point, I think formset.errors would need to be empty. That means you can create a new list to be assigned to formset.errors. You could even build it incrementally as you’re iterating over the individual forms.

Then, once you’re done, you may be able to call is_valid again on the formset. If it’s false, you return the same value at this point in the code as if the original is_valid test failed instead of returning the redirect.

Hi Ken,

You are correct, this is where I think the code should be added for the form errors. I would like to add the error on the object to the respective form so that in the form the errors shows with the problem form. However since I am iterating over the instances which are model objects and not the form itself where I would set the error, I am having trouble wrapping me head around the correct way to iterate the form and the instances together so that I can line them up so to speak

Thanks

You don’t need to specifically iterate over the forms.
What I’m thinking of should be able to be achieved something like this (pseudo code here, not actual code):

formset.errors = []
for i in instances:
   ... stuff ...
   is this an error?
        formset.errors.append({'field name': 'Error message'})
   else:
        formset.errors.append({})
if formset.is_valid():
    return redirect(...)
return render(...)