djangodjango-admindjango-admin-filters

How to include custom model method in Django admin custom filter?


What I am trying to do:

I am trying to make custom filter in Django admin using SimpleFilter for using in list_filter.

What I tried:

Below is the code that I wrote down in admin.py file.

I used SimpleFilter for creating custom filter called RoomScoreFilter. RoomScoreFilter filters that if average score is 1.00 to 1.99, it will filter as Very poor and so forth.

class RoomScoreFilter(admin.SimpleListFilter):
    title = _("Room Score")
    parameter_name = "score"

    def lookups(self, request, model_admin):
        return [
            ("1", _("Very poor")),
            ("2", _("Poor")),
            ("3", _("Normal")),
            ("4", _("Good")),
            ("5", _("Excellent")),
        ]

    def queryset(self, request, queryset):
        if self.value() == "1":
            return queryset.filter(get_average_rating__gte=1, get_average_rating__lt=2)

@admin.register(Review)
class ReviewAdmin(admin.ModelAdmin):
    empty_value_display = "-----"
    fieldsets = [
        ("Room & Customer", {"fields": ["room", "customer"]}),
        (
            "Evaluation",
            {"fields": ["get_average_rating", "comment"], "classes": "wide"},
        ),
        (
            "Individual Scores",
            {
                "fields": [
                    "cleanliness",
                    "accuracy",
                    "location",
                    "communication",
                    "check_in",
                    "value",
                ]
            },
        ),
    ]
    list_display = (
        "room",
        "customer",
        "cleanliness",
        "accuracy",
        "location",
        "communication",
        "check_in",
        "value",
        "get_average_rating",
        "comment",
    )
    list_display_links = ("room",)
    list_per_page = 20
    list_filter = [RoomScoreFilter]
    search_fields = ("room", "user")
    search_help_text = _("Searchable by room name and user ID.")
    readonly_fields = ("room", "customer", "comment", "get_average_rating")

and below is my model.

class Review(CommonDateTimeModel):

    """Review model Definition"""

    cleanliness = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)],
        verbose_name=_("Cleanliness"),
        help_text=_("How clean a room was."),
    )
    accuracy = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)],
        verbose_name=_("Accuracy"),
        help_text=_("How much did host provide accurate information about room."),
    )
    location = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)],
        verbose_name=_("Location"),
        help_text=_("Was location good or fair enough to reach out?"),
    )
    communication = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)],
        verbose_name=_("Communication"),
        help_text=_("How well did room host communicate with customers?"),
    )
    check_in = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)],
        verbose_name=_("Check In"),
        help_text=_("How easy was it for checking in?"),
    )
    value = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)],
        verbose_name=_("Value"),
        help_text=_("Was it valuable enough compared to the price per night?"),
    )
    room = models.ForeignKey(
        "rooms.Room",
        on_delete=models.DO_NOTHING,
        related_name="reviews",
        verbose_name=_("Room"),
    )
    customer = models.ForeignKey(
        "users.User",
        on_delete=models.DO_NOTHING,
        related_name="reviews",
        verbose_name=_("Customer"),
    )
    comment = models.TextField(verbose_name=_("Comment"), null=True, blank=True)

    def get_average_rating(self):
        total_scores = (
            self.cleanliness
            + self.accuracy
            + self.location
            + self.communication
            + self.check_in
            + self.value
        )
        average_score = round(total_scores / 6, 2)
        return average_score

    get_average_rating.short_description = _("Total Rating")

    def __str__(self):
        return str(f"{self.customer}'s review on {self.room}")

so basically what get_average_rating() method does is simply add up all 6 fields that I wrote down (cleanliness, check_in, etc) and divide by 6 with rounding up to 2 digits.

The error that I got:

However, it spits the error which I possibly expected though:

Cannot resolve keyword 'get_average_rating' into field. Choices are: accuracy, check_in, cleanliness, comment, communication, created_at, customer, customer_id, id, location, room, room_id, updated_at, value

enter image description here

Are there any solutions for solving this problem?


Solution

  • You can not use properties or methods in a queryset: querysets are converted into an SQL query, and the database does not know anything about these properties or methods, it just "understands" columns.

    You can however convert the expression to an SQL expression with:

    from django.db.models import F
    
    # …
    
    
    def queryset(self, request, queryset):
        queryset = queryset.alias(
            avg_rating=(
                F('cleanliness')
                + F('accuracy')
                + F('location')
                + F('communication')
                + F('check_in')
                + F('value')
            )
            / 6
        )
    
        if self.value() == '1':
            return queryset.filter(avg_rating__gte=1, avg_rating__lt=2)

    You can also avoid the division, which only wastes computational effort and can result in rounding errors:

    from django.db.models import F
    
    # …
    
    
    def queryset(self, request, queryset):
        queryset = queryset.alias(
            sum_rating=(
                F('cleanliness')
                + F('accuracy')
                + F('location')
                + F('communication')
                + F('check_in')
                + F('value')
            )
        )
    
        if self.value() == '1':
            return queryset.filter(sum_rating__gte=6, sum_rating__lt=12)
        # …