Context-dependant completions

There’s a common usecase: “Please display the items depending on the user context”.

In a multi-site context, you have to define a preemptive filter to gather informations based on the current context. And you also have to validate form data against this context. A malevolent user could forge a POST request with a data field that would be off-context.

On the Autocomplete class side

That’s the reason why the django-agnocomplete views carry the request user into the Autocomplete class. This instance variable is self.user. If you’re using a custom authentication profile, it’ll be an instance of your AUTH_USER_MODE class, hence it’ll have access to its properties and methods.

class AutocompletePersonQueryset(AgnocompleteModel):
    fields = ['first_name', 'last_name']
    requires_authentication = True
    model = Person

    def get_queryset(self):
        email = self.user.email
        _, domain = email.split('@')
        return Person.objects.filter(email__endswith=domain)

class AutocompletePersonSameSite(AgnocompleteModel):
    fields = ['first_name', 'last_name']
    requires_authentication = True
    model = Person

    def get_queryset(self):
        return Person.objects.filter(site=self.user.site)

Important

You may have noticed that these two classes have a model property and a requires_authentication property set to True. Because we’re using a user-based context, the requires_authentication will allow the general “out-of-context code” (form class creation) to instanciate the Agnocomplete class without the context, but will disallow it to return results based on the query. This way, you can filter out unauthorized uses of the autocomplete, as you could do it with the @login_required decorator in your views.

On the Form side

Nothing special to do. Just declare your fields exactly the same way.

from django import forms
from agnocomplete.forms import UserContextFormMixin

class SearchContextForm(UserContextForm, forms.Form):
    search_person = fields.AgnocompleteModelField(
        'AutocompletePersonSameSite')

On the view side

Views that take care of the context to handle form display and form process need to be aware that they’ll have to carry the user context to the form and that, when the POST request has to be processed using this context.

We’re providing a Mixin named UserContextFormViewMixin:

from agnocomplete.views import UserContextFormViewMixin

class SearchContextFormView(UserContextFormViewMixin, FormView):
    form_class = SearchContextForm

That’s it. The view will pass the context to the fields, and this context will be used by the Agnocomplete field to validate against the queryset.

If the POST data pushed to your form don’t comply with the context (e.g., choosing a Category that doesn’t belong to the user context), then the form.is_valid() method will return False.

No need to redeclare the queryset, no need to override the clean_<field>() method.

Context-dependant multiple selects

In the Model Multiple Selection section, we’ve seen how to create multiple-select inputs, with or without enabling creation mode. It may happen that we want to create new model instances using the current context. Typically, let’s imagine that we’re on a multiple-client website, each logged user belongs to their own “domain”. Now we want to tag items, but each tag catalog has to be isolated from the others. The tags of the client A are not the tags of the client B.

Here are our models:

class ContextTag(models.Model):
    name = models.CharField(max_length=50)
    domain = models.CharField(max_length=100)

class ArticleContextTag(models.Model):
    article = models.ForeignKey(Article)
    tags = models.ManyToManyField(ContextTag)

Here’s the corresponding ModelForm

from agnocomplete.forms import UserContextFormMixin

class ArticleContextTagModelForm(UserContextFormMixin,
                                 forms.ModelForm):
    article = fields.AgnocompleteModelField(AutocompleteArticle)
    tags = ModelMultipleDomainField(
        AutocompleteContextTag,
        create_field="name",
        required=False
    )

    class Meta:
        model = ArticleContextTag
        fields = '__all__'

You may have noticed that we have a dedicated ModelMultipleDomainField. This specific field class uses the context passed through the form, and then the fields, to build extra arguments when creating the model instance.

Here’s the ModelMultipleDomainField:

class ModelMultipleDomainField(fields.AgnocompleteModelMultipleField):
    def extra_create_kwargs(self):
        """
        Inject the domain of the current user in the new model instances.
        """
        user = self.get_agnocomplete_context()
        if user:
            _, domain = user.email.split('@')
            return {
                'domain': domain
            }
        return {}

When the field will want to create new records in the ContextTag table, here’s what’s going to happen:

  • the “name” will take the value of the string inserted into the input,
  • the “domain” will take the value of the current user email domain name.

As a consequence, each tag creation could be written like this:

ContextTag.objects.create(**{
    'name': input_value,
    'domain': user_domain_name,
})

Of course, you’re free to extract whichever information out of the context (or not) and feed the extra_kwargs dictionary (the date and time, the weather, whatever).

Important

Views that will use the ArticleContextTagModelForm must inherit from the UserContextFormViewMixin, exactly as above, otherwise, the context is not transmitted to the different elements of the view.

Also, you have to decorate your form_valid method using the @method_decorator(allow_create) trick.