Django admin add custom filter
Asked Answered
E

3

19

i'm using django 1.10 and I need to display data and create a filter based on a value from a different model(which has a foreign key referencing my model that is used on the admin template) These are my 2 models: This one is used to generate the template:

class Job(models.Model):
    company = models.ForeignKey(Company)
    title = models.CharField(max_length=100, blank=False)
    description = models.TextField(blank=False, default='')
    store = models.CharField(max_length=100, blank=True, default='')
    phone_number = models.CharField(max_length=60, null=True, blank=True)

This is the other one that holds a foreign key reference to my first one:

class JobAdDuration(models.Model):
    job = models.ForeignKey(Job)
    ad_activated = models.DateTimeField(auto_now_add=True)
    ad_finished = models.DateTimeField(blank=True, null=True)

Inside my template, I have been able to display the(latest)start and end times

def start_date(self,obj):
    if JobAdDuration.objects.filter(job=obj.id).exists():
        tempad = JobAdDuration.objects.filter(job=obj).order_by("-id")[0]
        return tempad.ad_activated

And then I just call this inside the list_display and that is working fine. However, i have trouble setting a filter field using these criteria.

If I just add it to my list_filter then I get an error that there is no such field inside my model which is true (since that one is in another table that has reference to my job table). So I was wondering what is the right approach to solve this? Do I need to create another function for the filter itself but even then I'm not sure how should I call it inside the list_filter.

Here is a snippet of my Django admin page.

class JobAdmin(admin.OSMGeoAdmin, ImportExportModelAdmin):
    inlines = [
    ]

    readonly_fields = ( 'id', "start_date", )

    raw_id_fields = ("company",)

    list_filter = (('JobAdDuration__ad_activated', DateRangeFilter), 'recruitment', 'active', 'deleted', 'position', ('created', DateRangeFilter), 'town')
    search_fields = ('title', 'description', 'company__name', 'id', 'phone_number', 'town')
    list_display = ('title', 'id', 'description', 'active', 'transaction_number', 'company', 'get_position', 'town','created', 'expires', 'views', 'recruitment', 'recruits', 'paid', 'deleted', "start_date", "end_Date", "ad_consultant")


    def start_date(self,obj):
        if JobAdDuration.objects.filter(job=obj.id).exists():
            tempad = JobAdDuration.objects.filter(job=obj).order_by("-id")[0]
            return tempad.ad_activated

EDIT: In the meantime, I tried to solve it with a simple list filter, but I am unable to get it to work. I would like to place 2 input fields with a calendar(like the default DateRangeFilter) that would represent the start and end time, and then return data based on those values. This is my "prototype" functionality for the simple filter, it works but it returns hard-coded data.

class StartTimeFilter(SimpleListFilter):
    title = ('Start date')
    parameter_name = 'ad_finished'

    def lookups(self, request, model_admin):
       #return JobAdDuration.objects.values_list("ad_finished")
       return (
       ('startDate', 'stest1'),
       ('startDate1', 'test2')
       )

    def queryset(self, request, queryset):
        if not self.value():
            return queryset

 
        assigned = JobAdDuration.objects.filter(ad_finished__range=(datetime.now() - timedelta(minutes=45000), datetime.now()))
        allJobs = Job.objects.filter(pk__in=[current.job.id for current in assigned])
        return allJobs

 
Eleonoreeleoptene answered 5/2, 2018 at 10:14 Comment(4)
You'll need a custom filter class, see docs and a sample tutorialPhonograph
yeah i did that but im not able to add a date picker to the filter from where i would get the infromation from the user. I would like to have 2 fields where i would input a start date and a end date, take those values and filter my data based on it. I forgot to update my question with that, will do it nowEleonoreeleoptene
Just to be clear, are you expecting a ManyToOne relation between JobAdDuration and Job, or a OneToOne?Phonograph
it is many to one.Eleonoreeleoptene
O
9

I would go with customized FieldListFilter as it allows to bind filter to different model fields based on your requirements.

What is we actually do to implement such filter is next:

  • build lookup_kwargs gte and lte and specify them as expected_parameters
  • define choices to return empty list otherwise NotImplementedError
  • create form to care fields validation
  • create custom template which just outputs form, e.g. {{spec.form}}
  • if form is valid take it's cleaned data, filter out Nones and filter queryset otherwise do something with errors (in code below errors are silenced)

Filter code:

class StartTimeFilter(admin.filters.FieldListFilter):
    # custom template which just outputs form, e.g. {{spec.form}}
    template = 'start_time_filter.html'

    def __init__(self, *args, **kwargs):
        field_path = kwargs['field_path']
        self.lookup_kwarg_since = '%s__gte' % field_path
        self.lookup_kwarg_upto = '%s__lte' % field_path
        super(StartTimeFilter, self).__init__(*args, **kwargs)
        self.form = StartTimeForm(data=self.used_parameters, field_name=field_path)

    def expected_parameters(self):
        return [self.lookup_kwarg_since, self.lookup_kwarg_upto]

    # no predefined choices
    def choices(self, cl):
        return []

    def queryset(self, request, queryset):
        if self.form.is_valid():
            filter_params = {
                p: self.form.cleaned_data.get(p) for p in self.expected_parameters()
                if self.form.cleaned_data.get(p) is not None
            }
            return queryset.filter(**filter_params)
        else:
            return queryset

Form can be as simple as follows:

class StartTimeForm(forms.Form):

    def __init__(self, *args, **kwargs):
        self.field_name = kwargs.pop('field_name')
        super(StartTimeForm, self).__init__(*args, **kwargs)
        self.fields['%s__gte' % self.field_name] = forms.DateField()
        self.fields['%s__lte' % self.field_name] = forms.DateField()
Obedient answered 8/2, 2018 at 19:44 Comment(3)
Thanks, im still busy with other stuff but i will try your solution during the weekend and report back then. Thanks for the helpEleonoreeleoptene
Eh i still do not have the time to test it but it seems it could work... I will ping you for helper later on then :PEleonoreeleoptene
sorry i forgot to give you the bountyEleonoreeleoptene
P
6

This isn't exactly what you've asked for, but you could instead have the filter on the JobAdDuration modelAdmin. This way, you can get the corresponding jobs filtered according to the ad_activated and ad_finished fields. And I've added a link to the job field, so you can directly click it for easier navigation.

To make it a date html5 filter, I've used django-admin-rangefilter library.

from django.urls import reverse
from django.contrib import admin
from .models import Job, JobAdDuration
from django.utils.html import format_html
from rangefilter.filter import DateRangeFilter


@admin.register(JobAdDuration)
class JobAdDurationAdmin(admin.ModelAdmin):

    list_filter = (('ad_activated', DateRangeFilter), ('ad_finished', DateRangeFilter))
    list_display = ('id', 'job_link', 'ad_activated', 'ad_finished')

    def job_link(self, obj):
        return format_html('<a href="{}">{}</a>', reverse('admin:job_job_change', args=[obj.job.id]), obj.job.title)
    job_link.short_description = 'Job'

If you indeed want to go the existing route (filter inside JobAdmin), then things will get quite complicated.

Phonograph answered 8/2, 2018 at 22:41 Comment(1)
I have to go with the filter inside JobAdmin in this case.Eleonoreeleoptene
B
3

I have recently faced similar problem where I needed to filter data based on value from another model. This can be done using SimpleListFilter. You just need a little tweak in the lookup and queryset function. I will suggest you to install django debug toolbar so that you may know what sql queries are being executed internally by django.

#import your corresponding models first

class StartTimeFilter(SimpleListFilter):
title = ('Start date')
parameter_name = 'ad_finished'

  def lookups(self, request, model_admin):

   data = []
   qs = JobAdDuration.objects.filter()   # Note : if you do not have distinct values of ad_activated apply distinct filter here to only get distinct values
   print qs
   for c in qs:
       data.append([c.ad_activated, c.ad_activated])  # The first c.activated is the queryset condition your filter will execute on your Job model to filter data ... and second c.ad_activated is the data that will be displayed in dropdown in StartTimeFilter
   return data

  def queryset(self, request, queryset):
     if self.value():
       assigned = JobAdDuration.objects.filter(ad_activated__exact = self.value())  # add your custom filter function based on your requirement
       return Job.objects.filter(pk__in=[current.job.id for current in assigned])
     else:
       return queryset

and in list_filter

list_filter = (StartTimeFilter) # no quotes else it will search for a field in the model 'job'.
Blameworthy answered 12/2, 2018 at 6:10 Comment(2)
Will that not give me just a list with all possible dates? I would like to have two "input boxes" where i would put my dates and filter according to that.Eleonoreeleoptene
In that case @bellum's solution is good enough. Customised FieldListFilter, to create a datetimerangeform, will solve your problem.Blameworthy

© 2022 - 2024 — McMap. All rights reserved.