1

At the moment, in my admin, i am ordering YelpCompanys by annual_revenue. Some of the annual revenues contain characters that are not numbers. I have a function on the model that converts annual_revenues to integers. How do I use this function in my ordering function in the admin? Any help is appreciated -- thanks in advance. Here is my code:

models.py

class YelpCompany(models.Model):
    title = models.CharField(max_length=255)
    url = models.URLField(max_length=255, unique=True)
    messaged = models.BooleanField(default=False)
    date_created = models.DateTimeField(auto_now_add=True)
    city = models.CharField(max_length=255, blank=True, null=True)
    company_type = models.CharField(max_length=255,blank=True,null=True)
    not_available = models.BooleanField(default=False)
    annual_revenue = models.CharField(max_length=255, blank=True,null=True)

    def __str__(self):
        return self.title

    def revenue_to_int(self):
        try:
            return int(self.annual_revenue)
        except Exception as e:
            if 'less than' in self.revenue:
                return self.revenue.split('less than ')[1].replace('$','').strip()
            elif 'million' in self.revenue:
                return self.revenue.split('to')[0].replace('$','').strip()
            else:
                return 0

admin.py

@admin.register(YelpCompany)
class YelpCompanyAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.filter(messaged=False,not_available=False)
    list_display = ('title','url','messaged','city','annual_revenue','not_available')
    ordering = ('annual_revenue',)
Michael
  • 763
  • 1
  • 10
  • 25
  • The ***`ordering`*** process is handled by the Database itself. So, writing a function on the model level won't help you. Your best bet is, use [**QuerySet annotation**](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#annotate) to change the `annual_revenue` value integer in the DB level – JPG Feb 03 '20 at 03:42

1 Answers1

0

@Arakkl_Abu answer is correct. I played around a bit and tried rebuilding your python method with database functions. It yould be something similar to this:

#admin.py

from django.db.models import Case, Value, When, IntegerField, FloatField, F
from django.db.models.functions import Substr, StrIndex, Cast, Replace, Length

class YelpCompanyAdmin(admin.ModelAdmin):

    list_display = ('title','url','messaged','city','annual_revenue','not_available', 'show_annual_revenue1')

    def get_queryset(self, request):
        qs = YelpCompany.objects.filter(messaged=False,not_available=False).annotate(
            field_case=Case(
                When(annual_revenue__contains="less than", then=Value(1)),
                When(annual_revenue__contains="million", then=Value(2)),
                When(annual_revenue__iregex=r'^[a-zA-Z]+/$', then=Value(3)),
                When(annual_revenue=None, then=Value(3)),
                When(annual_revenue=r'^[/w]+/$', then=Value(3)),
                default=Value(4),
                output_field=IntegerField()

            )
        ).annotate(
            index_nr=Case(
                When(field_case=1, then=StrIndex('annual_revenue', Value('less than')) + Length(Value('less than'))),
                When(field_case=2, then=StrIndex('annual_revenue', Value('up to')) + Length(Value('up to'))),
                When(field_case=3, then=Value(0)),
                default=Value(-1),
                output_field=IntegerField()
            )
        ).annotate(
            annual_revenue1=Case(
                When(index_nr__gt=0, then=Cast(Replace(
                    Substr(F('annual_revenue'), F("index_nr")), Value('$'), Value('')), 
                    output_field=FloatField())),
                When(index_nr=0, then=Value(0)),
                default=Cast(F('annual_revenue'), output_field=FloatField())
            )
        )

        return qs

    def show_annual_revenue1(self, inst):
        return inst.annual_revenue1

    show_annual_revenue1.admin_order_field = 'annual_revenue1'

The annotations create a new annotation annual_revenue1 containing the numeric value of annual_revenue only. This cn be used for ordering. The ModelAdmin above has a new column in the list display called show_annual_revenue1, which is used for default ordering.

A post dealing with using annotations for ordering is here.

A few words of explanation:

The first 'Case' annotation sorts the 'annual_revenue' entries into groups: 1. The field contains 'less than', 2. The field contains 'million', 3. The field contains letters, is None or empty, 4. If none of the above apply it is assumed the field contains a numeric value. You might need to adapt this to your special use-case if other cases apply.

In the second 'Case' annotation we find the index for exracting the substring, similar to your split() command. Fields which do not contain a valid numeric value as defined in the first Case annotation or which are numeric values are marked with index_nr '0' or '-1'.

In the third annotation block we extract the numeric substring or just return 0 (if the field does not hold a valid numeric value) or return the value of the field if it can be used as is. The returned values are cast to numeric values so we get the correct sorting.

Approach 2

The drawback of the approach above is that it is not very flexible and rather "longish". Another approach could be removing all the strings at the beginning:

#admin.py

from django.db.models import Case, Value, When, F, FloatField
from django.db.models.functions import Cast, Replace

#admin.py

from django.db.models import Case, Value, When, IntegerField, FloatField, F
from django.db.models.functions import Substr, StrIndex, Cast, Replace, Length

class YelpCompanyAdmin(admin.ModelAdmin):

    list_display = ('title','url','messaged','city','annual_revenue','not_available', 'show_annual_revenue1')

    def get_queryset(self, request):
        replace_strings = ['million', 'milion', 'up to', '$', 'less than', 'False']

        qs = YelpCompany.objects.filter(messaged=False,not_available=False)

        qs = qs.annotate(
            annual_revenue1 = F('annual_revenue')
        )

        for s in replace_strings:
            qs = qs.annotate(
                annual_revenue1 = Replace(F('annual_revenue1'), Value(s), Value(''))
            )

        qs = qs.annotate(
            annual_revenue1 = Case(
                When(annual_revenue1=None, then=Value(0.0)),
                default=Cast(Trim(F('annual_revenue1')), FloatField())
            )
        )

        return qs.order_by('annual_revenue1')

    def show_annual_revenue1(self, inst):
        return inst.annual_revenue1
    show_annual_revenue1.admin_order_field = 'annual_revenue1'

This give the fleixbility to add whatever strings can appear to the list. You could event create your list of replace strings dynamically from you initial queryset if you like:

replace_strings2 = []

for q in qs.values_list('annual_revenue', flat=True):
    if not q is None:
        s = ''.join([x for x in q if not q.isdigit()])
        replace_strings2.extend(s.split())

replace_strings = []
for s in replace_strings2:
    try:
        float(s)
    except ValueError:
        replace_strings.append(s)
Chris
  • 2,162
  • 1
  • 6
  • 17
  • : (admin.E108) The value of 'list_display[6]' refers to 'show_annual_revenue1', which is not a callable, an attribute of 'YelpCompanyAdmin', or an attribute or method on 'engine.YelpCompany'. – Michael Feb 03 '20 at 19:50
  • Are you sure you put in all the code in you admin, including the ```def show_annual_revenue1(self, inst)``` at the bottom? – Chris Feb 03 '20 at 19:55
  • oops. now I'm getting: invalid input syntax for type double precision: "False" – Michael Feb 03 '20 at 20:03
  • BTW: Which Django version are you using? – Chris Feb 03 '20 at 20:26
  • Django version: 3.0.2 – Michael Feb 03 '20 at 20:28
  • Looks like the annotation functions are getting a ```False``` somewhere where a float number is expected. Could you post some samples of how your annual revenue really looks like? It might be that the functions are not fully adequate for your actual values. I tested my proposed approach and it worked with sample data I entered. – Chris Feb 03 '20 at 20:47
  • Oh, sometimes `annual_revenue` is False. I'd want to convert that to 0 – Michael Feb 03 '20 at 21:29
  • I can just change those to zeros in the shell and update my code to save 0s instead of False. Thanks – Michael Feb 04 '20 at 01:06
  • invalid input syntax for type double precision: " to 5 million" (looks like I didn't account for every case – Michael Feb 04 '20 at 01:10
  • @MikeJohnson I added a fall back case returning 0 if none of the defined cases apply and a few word of explanation to help you adapt this to your needs. – Chris Feb 04 '20 at 07:50
  • copied new code, same error. invalid input syntax for type double precision: " to 5 million" – Michael Feb 04 '20 at 07:54
  • @MikeJohnson: Added a simpler approach, which is much more flexible when it comes to catching all cases – Chris Feb 04 '20 at 09:26