SAVE and add another given a restriction

I like the idea in Django Admin of having a SAVE and add another button in the create view. The issue I’m facing is quota on the number of instances a user is allowed to create for a number of classes.

For example, a user is allowed to add 8 tags by default. In views.py TagListView I have the following code to keep track of the number of tags a user created:

class TagListView(PermissionRequiredMixin, ListView):
    ...

    def get_queryset(self):
        queryset = models.Tag.objects.filter(node_id=self.request.user.node_id)
        self.counter = len(queryset)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        ...
        quota = list_view_quota['tag']
        if self.counter >= quota:
            context['add_btn'] = False
            messages.info(self.request, f"The maximum number of tags in your account is {quota}, the actual number is {self.counter}")
        else:
            context['add_btn'] = True
        return context

In the template:

{% if add_btn %}
    <input type="submit" class="btn btn-primary btn-sm" name="_addanother" value="SAVE and add another" role="button"></input>
{% endif %}

And in views.py

class TagAddView(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
    ...

    def get_success_url(self):
        if '_addanother' in self.request.POST:
            return reverse('node-tag-add')
        else:
            return super().get_success_url()

The first question is: if the user comes from the TagListView, do I need to check if the actual number of tags is less than the allowed number of tags, I guess it’s possible to reload the TagAddView, bypassing the TagListView.

Then, if the user clicks SAVE and add another I need to add one to the counter I set in TagListView. How do I get the counter from the TagListView to the TagAddView

Kind regards,

Johanna

My initial reaction to this would be to look at this from a slightly different angle.

What I would likely try to do is determine whether the person who will be viewing the current page is going to be able to add an additional tag if the current submission is successful - and not render that button if they can’t.

In other words, if a person has 7 tags and you are rendering the page where they would be entering their 8th tag, the button to “Save and add another” wouldn’t appear on that page.

Side note: Using the count() method could be more efficient than getting the length of a queryset. In other words, your get_queryset could be rewritten as:

queryset = models.Tag.objects.filter(node_id=self.request.user.node_id)
self.counter = queryset.count()

Now, in practical terms, since your upper limit of the length of this list is supposed to be 8, you probably couldn’t measure the performance difference between the two - and I don’t suggest you change your function. I’m only pointing this out as an FYI-type item.

That’s what I did also.

The user will be viewing a ListView or a CreateView. I realised that the add_btn in the ListView, is different from the addanother_btn in the CreateView, both should be handled differently. The relevant parts of the ListView:

class TagListView(PermissionRequiredMixin, ListView):
    ...

    def get_queryset(self):
        queryset = models.Tag.objects.filter(node_id=self.request.user.node_id)
        self.request.session['counter'] = self.counter = queryset.count()
        return queryset

    def get_context_data(self, **kwargs):
        ...

        quota = list_view_quota['node_tag']
        if self.counter < quota:
            context['add_btn'] = True
        else:
            context['add_btn'] = False
            messages.info(self.request, f"The maximum number of tags in your account is {quota}, the actual number is {self.counter}")
        return context

Then in the relevant parts in the CreateView

class TagAddView(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
    ...

    def get_success_url(self):
        if '_addanother' in self.request.POST:
            return reverse('tag-add')
        else:
            return super().get_success_url()

    def form_valid(self, form):
        form.instance.node_id = self.request.user.node_id
        self.request.session['counter'] += 1
        return super().form_valid(form)

    def get_context_data(self, **kwargs):
        ...

        quota = list_view_quota['node_tag']
        if self.request.session['counter'] < quota - 1 :
            context['addanother_btn'] = True
        else:
            context['addanother_btn'] = False
        return context

I’m not sure I coded this well, but the code does what I want it to do.

While working on this I discovered two things that worry me.

First, the entries in Tag have a unique constraint:

class Meta:
    constraints = [
        models.UniqueConstraint(fields=['node', 'tag'], name='unique_tag')
    ]

When I enter the same tag I get an IntegrityError: UNIQUE constraint failed. I expected Django to fail silently and return the TagForm with an error message or return the TagListView with an error message, not the Django traceback. How do I solve this issue.

Second, when I use the browser’s back button to go back to the form and submit it again. I can enter more tags than I’m allowed to enter. Is there a way to prevent the user fro resubmitting the same form?

Changed to using the count() method. I very much appreciate the FYI-type items in your replies. I add them to the logs I’m keeping on developing my application.

The Model constraints are “applied” at the time the model is attempting to be saved. If you want to ensure that this error isn’t thrown, you could either create a custom clean method on your form to verify validity of the submission, or perform the necessary tests in your view. (I tend to recommend the former where possible, the latter when necessary.)

Technically, no. You can do things to prevent it from happening accidentally, but you can’t prevent it from happening by someone wanting to do it.

Thank you! I appreciate hearing that.

Hi,

I think I solved the issue of users adding more tags than their quota. In views.py TagAddView() I added the following code:

def get_form_kwargs(self):
    kwargs = super().get_form_kwargs()
    kwargs.update({
        'counter': self.request.session['counter'],
    })
    return kwargs

And in forms.py TagForm(ModelForm):

def __init__(self, *args, **kwargs):
    self.counter = kwargs.pop('counter')
    super().__init__(*args, **kwargs)
    ....

def clean(self):
    super().clean()
    quotum = list_view_quota['tag']
    if self.counter >= quotum:
        raise ValidationError(message='The maximum number of tags in your account is %(quotum)s, the actual number is %(counter)s', code = 'invalid', params={'quotum': quotum, 'counter': self.counter})

This works, however, I’m not sure about the way I implemented the clean() method.

You mean add a second if statement to the clean() method which queries the database for the node and tag combination in the form and if the query returns a record, raise a ValidationError?

First, I’m personally not a fan of trying to use sessions for validations like this. The biggest issue is that they aren’t coordinated between two sessions of the same user. (In other words, you could apply more than your specified number of tags by opening up several separate private tabs to the site.)

I see no reason not to update kwargs[‘counter’] with a live database count.
e.g.

kwargs.update({
    'counter': Tag.objects.filter(node_id=self.request.user.node_id).count()
})

and completely forgetting about trying to track this in the session.

That’s one way - using the exists method, if you want to throw an error in this case. (And it’s probably the easiest way to do it.)

Another option would be to override the save method of the form to perform the same check and simply ignore the duplicate entry from being saved if you don’t think it’s worth throwing an error.

Thanks, I followed your advice.

At the moment I store the quota in a dictionary in a module in the config app.

list_view_quota = {'tag': 8, 'article': 4, 'landing_page': 1, 'job_post': 1,}

These are sort of a default setting. If I want to allow for custom quota, store them in a database and only update the default quota in case there is a custom quota.

Am I right to do this on log in, make a copy of the dictionary containing the defaults and then replace the defaults with custom quota? Or is there a better way to implement this?

Kind regards,

Johanna

I don’t know about “better”, but it’s a design decision that I wouldn’t make. (It doesn’t mean this is “wrong” - it’s really an issue of a choice being made.)

In general, I don’t like “application layer” configuration being stored in code. I much prefer that type of data be stored in a table somewhere. It allows for customization at the “installation” level without changing code. (e.g. “Customer A” wants “Default X” while “Customer B” wants “Default Y”.) If I were making this decision, those defaults would be in a table.
The management of that data beyond that point then depends upon when and how those defaults might change, how the user-specific settings may change, and what the interaction is of changes in both areas.

In the clean() method I have the following code:

if models.Space.objects.filter(node_id=self.user.node_id, nav_link_id=nav_link_id).exists():
    raise ValidationError(message='Space already exists. Please enter a unique space', code='invalid')

In a CreateView this works, however, in an UpdateView it raises the exception.

I thought adding `id’ to the filter would solve the issue:

if models.Space.objects.filter(~Q(id=id) & Q(node_id=self.user.node_id) & Q(nav_link_id=nav_link_id)).exists():
    raise ValidationError(message='Space already exists. Please enter a unique space', code='invalid')

But in my form I don’t have the id:

class Meta:
    model = models.Space
    fields = (
        'nav_link',
        'price_currency',
        'default_price',
        'price_unit',
        )

and in case of a CreateView there is no id. Is there a way to solve this issue?

Kind regards,

Johanna

I’m sorry, I’m kinda lost here. We’ve been bouncing around different topics, and I’m not with you regarding what this is or how this fits in with what we’ve been discussing. I can guess from the general discussion we’re now back to talking about a UniqueConstraint that is throwing an error on an update view of a form being saved.

However, for me to be able to offer any constructive suggestions, I’m going to need to see the current view, form, and model (including the constraint) for whatever it is that is throwing this error. (What you’ve posted previously refers to a unique constraint on the Tag model - I don’t see a tie-in to a Space model.)

I apologize messing up this topic. I created a new topic containing just my question related to the UniqueConstraint.

1 Like