Django smart field sort

Feature(s) impacted

Django smart field sort

Observed behavior

No documentation is present in order to explain how to setup smart field with sorting and put 'isSortable': True does not work

Expected behavior

Documentation present that explain how to make it works on Django

Failure Logs

Context

  • Project name: Keltio Intent
  • Team name: Keltio
  • Environment name: Production
  • Agent (forest package) name & version: django-forestadmin==1.4.11
  • Database type: Mysql
  • Recent changes made on your end if any: NA

Hi @Kevin_Didelot, and welcome to our community :wave:

You’re right, there is no explicit example in python to make a smart field sortable because this feature is not available on this agent.
If this is important to you, we have a work around, consisting in re-implementing the collection list view for your collection, but sorting on multiple fields is not possible at all.

Assuming we are using the customer collection from the documentation, and the fullname smart field :

In your app/forest/customer.py, update the smartfield definition with the is_sortable attribute :

def load_fields(self):
    return [
        {
            'field': 'fullname',
            'type': 'String',
            'get': self.get_fullname,
            'search': self.search_fullname
            'is_sortable': True,
        },
    ]

Now on the frontend your field is sortable, but the backend can’t handle it ; for that you have to create a new view app/views.py :

from django_forest.resources.views.list import ListView

class CustomerView(ListView):

def dispatch(self, request, *args, **kwargs):
    return super().dispatch(request, "customer", *args, **kwargs)

def get(self, request, *args, **kwargs):
    # the redefinition of the search param
    request.GET._mutable = True. # by default we can't modify the GET request
    # to sort fullname by firstname
    request.GET['sort'] = request.GET['sort'].replace("fullname", "firstname")
    request.GET._mutable = False
    return super().get(request)

And in your app/urls.py :

from django.urls import path
from django.views.decorators.csrf import csrf_exempt

urlpatterns = [
# …
path(“/customer”, csrf_exempt(views.CustomerView.as_view()), name=“customer”),
]

For this to work, we assume you have a line importing your app/urls.py under the path ‘forest’ in your project/urls.py like described in the action part of the documentation :

from django.urls import path
from django.conf.urls import include

urlpatterns = [
path(‘forest’, include(‘library.urls’)),
path(‘forest’, include(‘django_forest.urls’)),
]

If question about this solution, do not hesitate to answer this thread.

Have a good day.

3 Likes

Thanks Julien for the quick answer

Indeed i have a question : Is it possible to make a sort on a @property field in django (let me explain above)

Here is my implementation :

I have a model in the core app

class CompanyWebsite(models.Model):
    website = models.CharField(max_length=250, null=True)
    http_status = models.IntegerField(default=500, null=True)
    domain = models.CharField(max_length=250, null=True)
    meta_description = models.CharField(max_length=500, null=True)
    content = models.CharField(max_length=5000, null=True)

    company = models.ForeignKey(
        Company,
        related_name='possible_websites',
        on_delete=models.CASCADE,
        null=True
    )

    @property
    def score(self):
        return self.health_score + self.website_score + self.content_score

    @property
    def health_score(self) -> Literal[100, 70, 0]:
        if self.http_status < 300:
            return 100
        elif self.http_status < 500:
            return 70
        else:
            return 0

    @property
    def content_score(self) -> Literal[-100, 50, 0]:
        if not self.content:
            return 0
       ...
        return 50

The view

from django_forest.resources.views.list import ListView


class CompanyWebsiteView(ListView):
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, "core_companywebsite", *args, **kwargs)

    def get(self, request, *args, **kwargs):
        # the redefinition of the search param
        request.GET._mutable = True  # by default we can't modify the GET request
        # to sort fullname by firstname
        # request.GET['sort'] = request.GET['sort'].replace("Score", "firstname")
        request.GET._mutable = False
        return super().get(request)

The fields

from django_forest.utils.collection import Collection
from core.models import CompanyWebsite


class CompanyWebsiteForest(Collection):
    def load(self):
        self.fields = [
            {
                'field': 'Score',
                'type': 'Number',
                'is_sortable': True,
                'get': self.get_score
            },
            {
                'field': 'Health score',
                'type': 'Number',
                'is_sortable': True,
                'get': self.get_health_score
            },
            {
                'field': 'Website score',
                'type': 'Number',
                'is_sortable': True,
                'get': self.get_website_score
            },
            {
                'field': 'Content score',
                'type': 'Number',
                'is_sortable': True,
                'get': self.get_content_score
            },
        ]

    def get_score(self, company_website):
        return company_website.score

    def get_health_score(self, company_website):
        return company_website.health_score

    def get_website_score(self, company_website):
        return company_website.website_score

    def get_content_score(self, company_website):
        return company_website.content_score


Collection.register(CompanyWebsiteForest, CompanyWebsite)

The urls file :

from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from core.views.company import ConsolidateCompany, QualifyCompanies, FindCTO
from core.views.company_website import CompanyWebsiteView

app_name = 'core'
urlpatterns = [
    path('/core_companywebsite', csrf_exempt(CompanyWebsiteView.as_view()), name='core_companywebsite')
]

The error i got :

Internal Server Error: /forest/core_company/2276/relationships/possible_websites
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 56, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 55, in wrapped_view
    return view_func(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/base.py", line 103, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django_forest/resources/utils/resource.py", line 13, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/base.py", line 142, in dispatch
    return handler(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django_forest/resources/associations/views/list.py", line 27, in get
    queryset = self.enhance_queryset(queryset, RelatedModel, params, request)
  File "/usr/local/lib/python3.9/site-packages/django_forest/resources/utils/queryset/__init__.py", line 34, in enhance_queryset
    queryset = queryset.order_by(params['sort'].replace('.', '__'))
  File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 1645, in order_by
    obj.query.add_ordering(*field_names)
  File "/usr/local/lib/python3.9/site-packages/django/db/models/sql/query.py", line 2202, in add_ordering
    self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)
  File "/usr/local/lib/python3.9/site-packages/django/db/models/sql/query.py", line 1709, in names_to_path
    raise FieldError(
django.core.exceptions.FieldError: Cannot resolve keyword 'Score' into field. Choices are: company, company_id, content, domain, http_status, id, meta_description, website

In the error i dont see my @property fields and i want to filter on it

What should i do, to make it work ?

From what i understand from the lib and django doc it willl be complicated to do this kind os sorting with pagination and everything : python - Ordering Django queryset by a @property - Stack Overflow

This function seems to be the one to change if we want to be able to do it :

    def enhance_queryset(self, queryset, Model, params, request, apply_pagination=True):
        # scopes + filter + search
        queryset = self.filter_queryset(queryset, Model, params, request)

        # sort
        if 'sort' in params:
            queryset = queryset.order_by(params['sort'].replace('.', '__'))

        # segment
        if 'segment' in params:
            collection = Collection._registry[Model._meta.db_table]
            segment = next((x for x in collection.segments if x['name'] == params['segment']), None)
            if segment is not None and 'where' in segment:
                queryset = queryset.filter(segment['where']())

        # limit fields
        queryset = self.handle_limit_fields(params, Model, queryset)

        # pagination
        if apply_pagination:
            queryset = self.get_pagination(params, queryset)

        return queryset

To have something working like i want i will have to do something like this :

class Cart(models.Model):
    id = models.AutoField(primary_key=True)
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField(null=False,blank=False)
    total = models.FloatField(null=False,blank=False)

    def save(self, *args, **kwargs):
        self.total =  self.quantity * self.product.price
        super(Cart, self).save(*args, **kwargs)

The use of smart field has less value in this case

Hi @Kevin_Didelot,
Indeed the order_by is done on the databases fields so using property doesn’t work.
But you can override the enhance_queryset method in your CompanyWebsiteForest to add an annotation

from django.db.models import Sum, F
from django_forest.utils.collection import Collection

class CompanyWebsiteForest(ListView):
    def enhance_queryset(self, queryset, Model, params, request, apply_pagination=True):
        # scopes + filter + search
        queryset = self.filter_queryset(queryset, Model, params, request)
        queryset = queryset.annotate(score=Sum(F("health_score") + F("website_score") + F("...")))
        
        # sort
        # ...

    def dispatch(self, request, *args, **kwargs):
        # .....

With a better comprehension of your needs, I think the annotation solution make the re-implementation of the get method obsolete.

Let us know if that helps you.

Best regards.
Julien.

In fact the health_score etc are not database saved attributes but computed, so i don’t think i will work, but the workaround that pre save the data is enough for my need