# Query Selectors

Django is a great framework but the code tends to get messy as time passes by and the project
grows. [HackSoft](https://github.com/HackSoftware/Django-Styleguide#selectors) has invented a pattern called "Selectors"
which takes care of calling ORM methods everywhere and thinking in (and encapsulating) use-cases instead of using the
ORM directly and not DRY.

In addition to this the company [WirBauenDigital](https://www.wirbauen.digital/) created a coding style which enforces
all Custom QuerySet (CQS) methods to be as atomic as possible. These CQS methods will be (re-)used in any place
throughout your system.

Combining these approaches with the [Ambient Digital](https://ambient.digital) style to separate CQS and managers in a
very strict way:

* CQS methods will always return a QuerySet
* They NEVER alter the data, just fetch it

## Definition

The result of this blend is a pattern we'd like to introduce as "Query Selectors". These selectors follow some rules:

A query selector...

* is a method or function which represents a single specific use-case ("fetch all active users who like beer")
* has a well-defined in- and output and no "magic" (for example, some functionality provided by its class)
* can be registered in the model to have a django-esque way of calling
  it (`MyModel.selectors.active_users_liking_beer()`)

## Example

Here you'll see how a selector class could look like. Note, that `is_active()` and `likes_beverages()` are CQS methods
defined within `UserQuerySet` which is registered in the `UserManager`.

```python
# my_app/selectors/user.py

from ambient_toolbox.selectors.base import Selector


class UserSelector(Selector):
    def active_users_liking_beer(self):
        """
        Fetches a list of active users who like to drink beer.
        """
        return self.model.objects.is_active().likes_beverages(beverage_list=["beer"])
```

Here is an example on how to register a selector within a model.

```python
# my_app/models.py

from django.db import models


class User(models.Model):
    ...

    objects = UserManager()
    selectors = UserSelector()
```

To complete the example, we'll add the CQS and manager as well:

```python
# my_app/managers/user.py

from django.db.models import manager


class UserQuerySet(manager.QuerySet):
    """
    Custom queryset for the "User" clas
    """

    def is_active(self):
        """
        Get all users who have the active flag set to "True"
        """
        return self.filter(is_active=True)

    def likes_beverages(self, beverage_list: list[str]):
        """
        Gets all users who have set at least one of the given list as their favourite beverage
        """
        return self.filter(beverages__name__in=beverage_list)


class UserManager(manager.Manager):
    def active_users_liking_beer(self):
        """
        Fetches a list of active users who like to drink beer.
        """
        return self.model.objects.is_active().likes_bevarages(beverage_list=["beer"])
```

## Remarks

* Take care that the current selector class inherits from the Django manager. This is a workaround to inject the current
  model class into the selector to avoid circular dependency errors and access the model manager/CQS via `self.model.*`.
  This means that Django will think you already have a custom manager, and you have to register a real custom manager.
  If you follow the given pattern, you need one anyway so that shouldn't be an issue. Nevertheless, if you leave one
  out, you'll get a weird error because Django will not create a default one.

## Permissions & Visibility

Similar to the manager section in this documentation, you can use a neat pattern for handling basic object visibility
with a mixin `AbstractUserSpecificSelectorMixin`. This mixin provides three methods: `visible_for()`,
`editable_for()` and `deletable_for()`. Each method needs to be implemented per selector class like this:

```python
# my_app/selector/mymodel.py

from ambient_toolbox.selectors.base import Selector
from ambient_toolbox.selectors.permission import AbstractUserSpecificSelectorMixin


class MyModelSelector(AbstractUserSpecificSelectorMixin, Selector):

    def visible_for(self, user): ...

    def editable_for(self, user): ...

    def deletable_for(self, user): ...
```
