0
votes

I'd like to retrieve a model's objects via a search form but add another column for search score. I'm unsure how to achieve this using django-tables2 and django-filter.

In the future, I'd like the user to be able to use django-filter to help filter the search result. I can access the form variables from PeopleSearchListView but perhaps it's a better approach to integrate a django form for form handling?

My thought so far is to handle to the get request in get_queryset() and then modify the queryset before it's sent to PeopleTable, but adding another column to the queryset does not seem like a standard approach.

tables.py

class PeopleTable(tables.Table):
   score = tables.Column()
   class Meta:
     model = People
     template_name = 'app/bootstrap4.html'
     exclude = ('id',)
     sequence = ('score', '...')

views.py

class PeopleFilter(django_filters.FilterSet):
  class Meta:
    model = People
    exclude = ('id',)

class PeopleSearchListView(SingleTableMixin, FilterView):
  table_class = PeopleTable
  model = People
  template_name = 'app/people.html'
  filterset_class = PeopleFilter
  
  def get_queryset(self):
    p = self.request.GET.get('check_this')
    qs = People.objects.all()
    ####
    # Run code to score users against "check_this".
    # The scoring code I'm using is complex, so below is a simpler
    # example.
    # Modify queryset using output of scoring code?
    ####
    for person in qs:
      if person.first_name == 'Phil' and q == 'Hey!':
        score = 1
      else:
        score = 0
    return qs

urls.py

 urlpatterns = [
   ...
   path('search/', PeopleSearchListView.as_view(), name='search_test'),
   ... ]

models.py

 class People(models.model):
   first_name = models.CharField(max_length=200)
   last_name = models.CharField(max_length=200)

Edit: The scoring algorithm is a bit more complex than the above example. It requires a full pass over all of the rows in the People table to generate a score matrix, before finally comparing each scored row with the search query. It's not a one-off score. For example:

 def get_queryset(self):
   all = []
   for person in qs:
     all.append(person.name)
   # Do something complex with all,
   # e.g., measure cosine distance between every person,
   # and finally compare to the get request
   scores = measure_cosine(all, self.request.GET.get('check_this'))
   # We now have the scores for each person.
   
 
 
 
 
2
How do you calculate the score? - markwalker_
@markwalker_ The scoring algorithm is kind of trivial. But I've added an example scoring snippet to help. All that matters is that each row in the model is given a score, and I'd like to output all the scores beside the row in a filterable table. - There

2 Answers

0
votes

So you can add extra columns when you initialise the table.

I've got a couple of tables which do this based on events in the system;

    def __init__(self, *args, **kwargs):
        """
        Override the init method in order to add dynamic columns as
        we need to declare one column per existent event on the system.
        """
        extra_columns = []

        events = Event.objects.filter(
            enabled=True,
        ).values(
            'pk', 'title', 'city'
        )

        for event in events:
            extra_columns.append((
                event['city'],
                MyColumn(event_pk=event['pk'])
            ))

        if extra_columns:
            kwargs.update({
                'extra_columns': extra_columns
            })

        super().__init__(*args, **kwargs)

So you could add your score column similar to this when a score has been provided. Perhaps passing your scores into the table from the view so you can identify they're present and add the column, then use the data when rendering the column.

extra_columns doesn't appear to be in the tables2 docs, but you can find the code here; https://github.com/jieter/django-tables2/blob/master/django_tables2/tables.py#L251

0
votes

When you define a new column for django-tables2 which is not included in table data or queryset, you should provide a render method to calculate it's value.

You don't have to override get_queryset if a complex filtering, preprocess or join required.

In your table class:

class PeopleTable(tables.Table):    
    score = tables.Column(accessor="first_name")
    class Meta:
        model = People

    def render_score(self, record):
        return 1 if record["first_name"] == "Phil" and q == "Hey!" else 0

In your view you can override and provide complex data as well as special filtering or aggregates with get_context_data:

def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["filter"] = self.filter

        aggs = {
            "score": Function("..."),
            "other": Sum("..."),
        }
        _data = (
            People.objects.filter(**params)
            .values(*values)
            .annotate(**aggs)
            .order_by(*values)
            .distinct()
        )

        df = pandas.DataFrame(_data)
        df = df....
        chart_data = df.to_json()
        data = df.to_dict()...
      
        self.table = PeopleTable(data)
        context["table"] = self.table      
        context['chart_data']=chart_data

        return context