4

I have this Django App where I'm showing some data using Highchart (just an example here).

I have two separate views, one where you perform the Filter on the Product table (among other things), and the other view builds the Json from History Table to "pass" to the AJAX function for the plot (The real data is quite heavy).

The data is based on a History table where I have product_id, year, quantity (I want to show the Quantity over Time).

In my model I also have a table for Products with products, category (each product has a category, multiple products can share the same category).

The two tables have a one-to-many relationship with the field product.

In my template I'd like the user to be able to filter the products by the category field (ie: filter products with category 'A'), and this filter should also update the Chart (ie: I want to see the history for just the products in category 'A').

Below my code, I've tried many attempts but none has worked so far.

Please let me know if the code has all the info you need, I've tried to take just the essential.

models.py

class Products(models.Model):
    product = models.TextField(primary_key=True)
    category = models.TextField(blank=True,null=True)
    
    # ...
    
class HistoryProducts(models.Model):
    product_id = models.ForeignKey(Products)
    year = models.TextField(blank=True, null=True)
    quantity = models.DecimalFeld(..)
    
    # ...

filters.py

import django_filters
class ProductsFilter(django_filters.FilterSet):
    category_contains = CharFilter(field_name='category', lookup_expr='icontains') 
    class Meta:
         model = Products
         fields = ['category']

views.py

def index(request):
    products = Products.objects.all()
    myFilter = ProductsFilter(request.GET, queryset=products)
    products = myFilter.qs
    
    # ...
    
    return render(request, 'index.html', context={..})
    
def chart_data(request):
    
    # maybe here we should filter History by myFilter, but can't find how
    # ...
    
    # calculate total quantity
    history = HistoryProducts.objects.values('year').order_by('year').annotate(Total=Sum('quantity'))
    
    
    chart = {
        'chart': {'type': 'column'},
        'title': {'text': 'Quantity by Year'},
        'series': [{
            'name': 'Quantity',
            'data': list(map(lambda row: {'name': round(row['year']),'y': round(row['Total'])}, history))
        }]
    }
    
    return JsonResponse(chart)

index.html

<!-- Filter -->
<form method="GET">
   {{myFilter.form}}
   <button type="submit"> Filter</button>
</form>

<!-- Chart -->
<div>
    <div id="container" data-url="{% url 'chart_data' %}"></div>
</div>

<!-- Scripts -->
<script src="https://code.highcharts.com/highcharts.src.js"></script>
<script>
  // highchart function
  $.ajax({
    url: $("#container").attr("data-url"),
    dataType: 'json',
    success: function (data) {
      Highcharts.chart("container", data);
    }
  });
</script>

I guess my question is: is it possibile to "connect" the same Form Filter built with django-filters to multiple models?

Or more in general how would someone takle this kind of problem? I'm open to any suggestion. Thanks

EDIT ---

I have found a solution that is not pretty, and also makes the page much slower.

views.py

def index(request):
    products = Products.objects.all()
    myFilter = ProductsFilter(request.GET, queryset=products)
    products = myFilter.qs

    # get the id filtered
    ids = []
    qs = products.values_list('products',flat=True)
    for i in qs:
        ids.append(i)

    # use ids to filter History
    history = History.objects.filter(product_id_in=ids).values('year').order_by('year').annotate(Total=Sum('quantity'))

    # make the json here
    chart = {
        'chart': {'type': 'column'},
        'title': {'text': 'Quantity by Year'},
        'series': [{
            'name': 'Quantity',
            'data': list(map(lambda row: {'name': round(row['year']),'y': round(row['Total'])}, history))
        }]
    }
    dump = json.dumps(chart)

    # return the json to the template

    return render(request, 'index.html', context={..})

Now I only need this part in the template:

<script>
  Highcharts.chart('container', {{ chart|safe }});
</script>

Basically I moved the Json inside the same view where I filter the data, but this is much much slower.

RLave
  • 8,144
  • 3
  • 21
  • 37
  • I started a bounty because I haven't found anything related to this and it seems like a typical scenario (others may find this helpful). If the question is unclear or is missing some key info let me know so I can improve it. – RLave Jul 02 '20 at 12:40

1 Answers1

5

That's more of a jQuery/Javascript question but it should be possible to capture the form submission assuming you're assigning an id attribute to its HTML element and then use $.getJSON to GET the data from the chart_data view.

https://api.jquery.com/submit/

e.g.

// Assuming you assigned id="filter-form" to your form
$('#filter-form').submit(function(event){
  event.preventDefault();
  var url = $("#container").attr("data-url");
  var formData = $('#filter-form').serialize()
  $.getJSON(url, formData, function(data){
      Highcharts.chart("container", data);
  })
});

You'll then be able to use ProductsFilter from request.GET in your chart_data just like you did in index

def chart_data(request):
    
    products = Products.objects.all()
    myFilter = ProductsFilter(request.GET, queryset=products)
    products = myFilter.qs
    HistoryProducts.objects.filter(
        product_id__in=products
    ).values('year').order_by('year').annotate(
        Total=Sum('quantity')
    )
    
    
    chart = {
        'chart': {'type': 'column'},
        'title': {'text': 'Quantity by Year'},
        'series': [{
            'name': 'Quantity',
            'data': list(map(lambda row: {'name': round(row['year']),'y': round(row['Total'])}, history))
        }]
    }
    
    return JsonResponse(chart)
Simon Charette
  • 5,009
  • 1
  • 25
  • 33
  • Thank you for the answer,now it almost works. But when the page is first loaded the plot is not showing, only after I make a request from the Form. Can this be fixed so that the plot shows even if no Filter is made? – RLave Jul 02 '20 at 14:21
  • Also inside `index()` I have a count for the `Products` that are avaiable after you make a Filter Request, and now it's not working. It seems that `ProductsFilter` inside `index` is broken, any chace you know the problem? – RLave Jul 02 '20 at 14:23
  • To be honest I didn't tag this jQuery/JS because at first I wanted to see if there was a "Django" solution..the main problem for me is to use a Filter from a Table to filter other Tables related, but not in the same view. (Sorry for the triple-comment) – RLave Jul 02 '20 at 14:37
  • 1
    > Can this be fixed so that the plot shows even if no Filter is made? Sure it should still load if you keep your initial ` – Simon Charette Jul 02 '20 at 18:19
  • I don't understand why we're filtering products and not HistoryProducts. Naming conventions and lack of indexes aside, you can just `HistoryProducts.objects.filter(product_id__category__icontains=value)`. –  Jul 02 '20 at 19:41
  • @Melvyn I do need to filter both Models, but with the proposed solution the Product Model does not filter anymore (History does)..If you care to elaborate your suggestion I'd be happy to see it, thanks to both – RLave Jul 03 '20 at 06:31
  • @SimonCharette this part `event.preventDefault();` is preventing the data to be filtered correctly. As I said it filters `History` but not `Products`. – RLave Jul 03 '20 at 09:45