Question

I have a boolean field on my model that represents whether someone has canceled their membership or not. I am trying to create a custom SimpleListFilter that allows this field to be filtered on.

However, I really want to show only those who are not canceled by default. Is there someway to select the "No" option by default? This is my filter so far:

class CanceledFilter(SimpleListFilter):

    title = 'Canceled'

    # Parameter for the filter that will be used in the URL query.
    parameter_name = 'canceled'

    def lookups(self, request, model_admin):
        return (
            (True, 'Yes'),
            (False, 'No'),
        )

    def queryset(self, request, queryset):

        if self.value() is True or self.value() is None:
            return queryset.filter(canceled=True)
        if self.value() is False:
            return queryset.filter(canceled=False)

EDIT: I should have been a bit clearer. I am specifically trying to do this in the Admin interface. When I add the above filter as a list_filter in admin. I get a filter on the side of the admin page with 3 choices: All, Yes and No.

I would like the "No" choice or none of the choices to be set by default. Instead the "All" choice is always set by default. Is there some none hacky way to set the default filter choice or something like that.

Basiclly in Admin when they view the Members, I only want to show the active (not canceled) by default. If they click "All" or "Yes" then I want to show the canceled ones.

Update: Note this is the same as question Default filter in Django admin, but I that question is now 6 years old. The accepted answer is marked as requiring Django 1.4. I am not sure if that answer will still work with newer Django versions or is still the best answer.

Given the age of the answers on the other question, I am not sure how we should proceed. I don't think there is any way to merge the two.

Was it helpful?

Solution

Had to do the same and stumbled upon your question. This is how I fixed it in my code (adapted to your example):

class CanceledFilter(SimpleListFilter):

    title = 'Canceled'

    # Parameter for the filter that will be used in the URL query.
    parameter_name = 'canceled'

    def lookups(self, request, model_admin):
        return (
            (2, 'All'),
            (1, 'Yes'),
            (0, 'No'),
        )

    def queryset(self, request, queryset):

        if self.value() is None:
            self.used_parameters[self.parameter_name] = 0
        else:
            self.used_parameters[self.parameter_name] = int(self.value())
        if self.value() == 2:
            return queryset
        return queryset.filter(cancelled=self.value())

Some explanation is required. The querystring is just part of the URL, and exactly what the name implies: a query string. Your values come in as strings, not as booleans or integers. So when you call self.value(), it returns a string.

If you examine the URL you get when you click on the Yes/No, when not using a custom list filter, you'll see it encodes it as 1/0, not True/False. I went with the same scheme.

For completeness and our future readers, I also added 2 for All. Without verifying, I assume that was None before. But None is also used when nothing is selected, which defaults to All. Except, in our case it needs to default to False, so I had to pick a different value. If you don't need the All option, just remove the final if-block in the queryset method, and the first tuple in the lookups method.

With that out of the way, how does it work? The trick is in realising that self.value() just returns:

self.used_parameters.get(self.parameter_name, None)

which is either a string, or None, depending on whether the key is found in the dictionary or not. So that's the central idea: we make sure it contains integers and not strings, so that self.value() can be used in the call to queryset.filter(). Special treatment for the value for All, which is 2: in this case, just return queryset rather than a filtered queryset. Another special value is None, which means there is no key parameter_name in the dictionary. In that case, we create one with value 0, so that False becomes the default value.

Note: your logic was incorrect there; you want the non-cancelled by default, but you treat None the same as True. My version corrects this.

ps: yes, you could check for 'True' and 'False' rather than True and False in your querystring method, but then you'd notice the correct selection would not be highlighted because the first elements in your tuple don't match up (you're comparing strings to booleans then). I tried making the first elements in the tuples strings too, but then I'd have to do string comparison or eval to match up 'True' to True, which is kind of ugly/unsafe. So best stick to integers, like in my example.

OTHER TIPS

If anyone is still interested in a solution for this, I used a different and IMHO much cleaner approach. As I'm fine with a default choice and the handling of it, I decided I just want to rename the default display label. This is IMHO much cleaner and you don't need any "hacks" to handle the default value.

class CompleteFilter(admin.SimpleListFilter):
    '''
    Model admin filter to filter orders for their completion state.
    '''
    title          = _('completion')
    parameter_name = 'complete'

    def choices(self, changelist):
        '''
        Return the available choices, while setting a new default.

        :return: Available choices
        :rtype: list
        '''
        choices = list(super().choices(changelist))
        choices[0]['display'] = _('Only complete')
        return choices

    def lookups(self, request, model_admin):
        '''
        Return the optionally available lookup items.

        :param django.http.HttpRequest request: The Django request instance
        :param django.contrib.admin.ModelAdmin model_admin: The model admin instance

        :return: Optional lookup states
        :rtype: tuple
        '''
        return (
            ('incomplete', _('Only incomplete')),
            ('all', _('All')),
        )

    def queryset(self, request, queryset):
        '''
        Filter the retreived queryset.

        :param django.http.HttpRequest request: The Django request instance
        :param django.db.models.query.QuerySet: The Django database query set

        :return: The filtered queryset
        :rtype: django.db.models.query.QuerySet
        '''
        value = self.value()

        if value is None:
            return queryset.filter(state__complete=True)
        elif value == 'incomplete':
            return queryset.filter(state__complete=False)

        return queryset

In the choices() method, I just rename the display label from All to Only complete. Thus, the new default (which has a value of None is now renamed).

Then I've added all additional lookups as usual in the lookups() method. Because I still want an All choice, I add it again. However, you can also skip that part if you don't need it.

That's basically it! However, if you want to display the All choice on top again, you might want to reorder the choices list in the choices() method before returning it. For example:

        # Reorder choices so that our custom "All" choice is on top again.
        return [choices[2], choices[0], choices[1]]

Look at the section called "Adding Extra Manager Methods" in the link below:

http://www.djangobook.com/en/2.0/chapter10.html

You can add an additional models.Manager to your model to only return people that have not cancelled their membership. A rough implementation of the additional models.Manager would look like this:

class MemberManager(models.Manager):
  def get_query_set(self):
    return super(MemberManager, self).get_query_set().filter(membership=True)

class Customer(models.Model):
  # fields in your model
  membership = BooleanField() # here you can set to default=True or default=False for when they sign up inside the brackets

  objects = models.Manager  # default Manager
  members = MemberManager() # Manager to filter for members only

Anytime you need to get a list of you current members only, you would then just call:

Customer.members.all()
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top