Where should validation logic to limit users from object creation go?

I’m currently trying to implement something akin to generalized “plans” for my user model which I handled by creating a FK to a new table with the fields denoting the limits. I can add or change “plans” and assign them to a user, which works correctly. The problem is that I’m not sure where should I put the logic that checks for the limit of a user’s “plan”. I’m not sure if it should go in each save method of a model related to the limit or clean, handle it with model validators, on the forms somewhere or put it in the view.

Here’s a similiar example to the structure I have:

class Plan(models.Model):
    name = models.CharField(max_length=255)
    maximum_projects = models.PositiveIntegerField()


class Account(AbstractUser):
    plan = models.ForeignKey(Plan, on_delete=models.CASCADE, null=True)


class Project(models.Model):
    name = models.CharField(max_length=255)
    owner = models.ForeignKey(Account, on_delete=models.CASCADE)

If I have a user associated with a plan with a limit of 10 maximum_projects, how should I go about enforcing this limit?

If this were something I needed to handle, my decision would depend upon where and how the Projects get assigned to the Account. If this assignment is being done in only one location, then I’d be tempted to add this at that location.

In my current project I’m handling the creation of objects via forms, and the assignment of users via the following mixin:

class SetUserMixin(LoginRequiredMixin):
    """
    Automatically add `self.request.user` to a specified `user_field`. `use_field` defaults to "user"
    """

    user_field = "user"

    def form_valid(self, form):
        setattr(form.instance, self.user_field, self.request.user)
        return super().form_valid(form)

Using the Project example, it’d look something like this:

# forms.py
class ProjectForm(forms.ModelForm):

    class Meta:
        model = Project
        fields = ["name"]

# views.py
class ProjectCreateView(SetUserMixin, CreateView):
    form_class = ProjectForm
    user_field = "owner"
    template_name = "some_template.html"
    success_url = reverse_lazy("some-view")

Is this what you mean by location?

Personally, I’d be looking to test this in both the GET and POST processing.

I’d want to check it in the GET, such that the user doesn’t even get the opportunity to see the form to add a project if they’re not allowed to do that.

Then I’d still want to check it in the POST, just in case the user has two browser tabs open with the forms being displayed in both, and then saving both - becoming entries #10 and #11.

In that case, based upon what I’m seeing here, I’d probably implement this as a test_func for use by the UserPassesTestMixin, and add that mixin to my views.

1 Like