Repository: django-oscar/django-oscar

Issue:
"Product analytics" report on dashboard ignores date range
Found a bug? Please fill out the sections below.

### Issue Summary
"Product analytics" report on dashboard ignores date range

### Steps to Reproduce
Visit the dashboard, click on the "Reports" tab.
Select "Product analytics" from the "Report type" dropdown.
Select a date range and click "generate report"
The results returned will always be for all time, despite the date range selected

### Technical details
Any Python / Django version, Oscar 1.6.2

The ProductReportGenerator ignores the date range, and the stored analytics do not keep track of it. It might be nice to disable the date range controls for those reports that will never store date information.  Though a date breakdown of products purchased would be a nice to have...



Discussion:
You could add a new model <Insert Name Here> that contains the values on ProductRecord and has a Foreign Key to ProductRecord. This model is created/updated for each day. (If no views/additions/purchases are made then the objects would not be created) The model would have a date for that day for filtering purposes.

Just a note that "User analytics" behave in the same way
Date filtering is definitely broken for CSV exports. The [function that performs the filtering](https://github.com/django-oscar/django-oscar/blob/master/src/oscar/apps/dashboard/reports/views.py#L59) is [never called](https://github.com/django-oscar/django-oscar/blob/master/src/oscar/apps/dashboard/reports/views.py#L48) for CSV downloads. It also wouldn't work because the CSV formatter's `generate()` returns a response.

Fix patch:
diff --git a/src/oscar/apps/analytics/reports.py b/src/oscar/apps/analytics/reports.py
--- a/src/oscar/apps/analytics/reports.py
+++ b/src/oscar/apps/analytics/reports.py
@@ -37,6 +37,7 @@ class ProductReportHTMLFormatter(ReportHTMLFormatter):
 class ProductReportGenerator(ReportGenerator):
     code = 'product_analytics'
     description = _('Product analytics')
+    model_class = ProductRecord
 
     formatters = {
         'CSV_formatter': ProductReportCSVFormatter,
@@ -45,10 +46,6 @@ class ProductReportGenerator(ReportGenerator):
     def report_description(self):
         return self.description
 
-    def generate(self):
-        records = ProductRecord._default_manager.all()
-        return self.formatter.generate_response(records)
-
     def is_available_to(self, user):
         return user.is_staff
 
@@ -89,14 +86,11 @@ class UserReportHTMLFormatter(ReportHTMLFormatter):
 class UserReportGenerator(ReportGenerator):
     code = 'user_analytics'
     description = _('User analytics')
+    queryset = UserRecord._default_manager.select_related().all()
 
     formatters = {
         'CSV_formatter': UserReportCSVFormatter,
         'HTML_formatter': UserReportHTMLFormatter}
 
-    def generate(self):
-        users = UserRecord._default_manager.select_related().all()
-        return self.formatter.generate_response(users)
-
     def is_available_to(self, user):
         return user.is_staff
diff --git a/src/oscar/apps/basket/reports.py b/src/oscar/apps/basket/reports.py
--- a/src/oscar/apps/basket/reports.py
+++ b/src/oscar/apps/basket/reports.py
@@ -56,6 +56,7 @@ class OpenBasketReportGenerator(ReportGenerator):
     code = 'open_baskets'
     description = _('Open baskets')
     date_range_field_name = 'date_created'
+    queryset = Basket._default_manager.filter(status=Basket.OPEN)
 
     formatters = {
         'CSV_formatter': OpenBasketReportCSVFormatter,
@@ -65,8 +66,7 @@ def generate(self):
         additional_data = {
             'start_date': self.start_date,
             'end_date': self.end_date}
-        baskets = Basket._default_manager.filter(status=Basket.OPEN)
-        return self.formatter.generate_response(baskets, **additional_data)
+        return self.formatter.generate_response(self.queryset, **additional_data)
 
 
 class SubmittedBasketReportCSVFormatter(ReportCSVFormatter):
@@ -110,6 +110,7 @@ class SubmittedBasketReportGenerator(ReportGenerator):
     code = 'submitted_baskets'
     description = _('Submitted baskets')
     date_range_field_name = 'date_submitted'
+    queryset = Basket._default_manager.filter(status=Basket.SUBMITTED)
 
     formatters = {
         'CSV_formatter': SubmittedBasketReportCSVFormatter,
@@ -119,5 +120,4 @@ def generate(self):
         additional_data = {
             'start_date': self.start_date,
             'end_date': self.end_date}
-        baskets = Basket._default_manager.filter(status=Basket.SUBMITTED)
-        return self.formatter.generate_response(baskets, **additional_data)
+        return self.formatter.generate_response(self.queryset, **additional_data)
diff --git a/src/oscar/apps/dashboard/reports/reports.py b/src/oscar/apps/dashboard/reports/reports.py
--- a/src/oscar/apps/dashboard/reports/reports.py
+++ b/src/oscar/apps/dashboard/reports/reports.py
@@ -19,13 +19,17 @@ class ReportGenerator(object):
     code = ''
     description = '<insert report description>'
     date_range_field_name = None
+    model_class = None
+    queryset = None
 
     def __init__(self, **kwargs):
         self.start_date = kwargs.get('start_date')
         self.end_date = kwargs.get('end_date')
 
-        formatter_name = '%s_formatter' % kwargs['formatter']
+        formatter_name = '%s_formatter' % kwargs.get('formatter', 'HTML')
         self.formatter = self.formatters[formatter_name]()
+        self.queryset = self.get_queryset()
+        self.queryset = self.filter_with_date_range(self.queryset)
 
     def report_description(self):
         return _('%(report_filter)s between %(start_date)s and %(end_date)s') \
@@ -34,8 +38,19 @@ def report_description(self):
                'end_date': date(self.end_date, 'DATE_FORMAT')
                }
 
+    def get_queryset(self):
+        if self.queryset is not None:
+            return self.queryset
+
+        if not self.model_class:
+            raise ValueError(
+                "Please define a model_class property on your report generator class, "
+                "or override the qet_queryset() method."
+            )
+        return self.model_class._default_manager.all()
+
     def generate(self):
-        pass
+        return self.formatter.generate_response(self.queryset)
 
     def filename(self):
         """
diff --git a/src/oscar/apps/dashboard/reports/views.py b/src/oscar/apps/dashboard/reports/views.py
--- a/src/oscar/apps/dashboard/reports/views.py
+++ b/src/oscar/apps/dashboard/reports/views.py
@@ -47,7 +47,8 @@ def get(self, request, *args, **kwargs):
                 if form.cleaned_data['download']:
                     return report
                 else:
-                    self.set_list_view_attrs(generator, report)
+                    self.template_name = generator.filename()
+                    self.object_list = self.queryset = generator.queryset
                     context = self.get_context_data(object_list=self.queryset)
                     context['form'] = form
                     context['description'] = generator.report_description()
@@ -55,8 +56,3 @@ def get(self, request, *args, **kwargs):
         else:
             form = self.report_form_class()
         return TemplateResponse(request, self.template_name, {'form': form})
-
-    def set_list_view_attrs(self, generator, report):
-        self.template_name = generator.filename()
-        queryset = generator.filter_with_date_range(report)
-        self.object_list = self.queryset = queryset
diff --git a/src/oscar/apps/offer/reports.py b/src/oscar/apps/offer/reports.py
--- a/src/oscar/apps/offer/reports.py
+++ b/src/oscar/apps/offer/reports.py
@@ -42,15 +42,17 @@ class OfferReportGenerator(ReportGenerator):
         'HTML_formatter': OfferReportHTMLFormatter,
     }
 
-    def generate(self):
+    def get_queryset(self):
         qs = OrderDiscount._default_manager.all()
         if self.start_date:
             qs = qs.filter(order__date_placed__gte=self.start_date)
         if self.end_date:
             qs = qs.filter(order__date_placed__lt=self.end_date + datetime.timedelta(days=1))
+        return qs
 
+    def generate(self):
         offer_discounts = {}
-        for discount in qs:
+        for discount in self.queryset:
             if discount.offer_id not in offer_discounts:
                 try:
                     all_offers = ConditionalOffer._default_manager
diff --git a/src/oscar/apps/order/reports.py b/src/oscar/apps/order/reports.py
--- a/src/oscar/apps/order/reports.py
+++ b/src/oscar/apps/order/reports.py
@@ -51,21 +51,21 @@ class OrderReportGenerator(ReportGenerator):
         'HTML_formatter': OrderReportHTMLFormatter,
     }
 
-    def generate(self):
+    def get_queryset(self):
         qs = Order._default_manager.all()
-
         if self.start_date:
             qs = qs.filter(date_placed__gte=self.start_date)
         if self.end_date:
             qs = qs.filter(
                 date_placed__lt=self.end_date + datetime.timedelta(days=1))
+        return qs
 
+    def generate(self):
         additional_data = {
             'start_date': self.start_date,
             'end_date': self.end_date
         }
-
-        return self.formatter.generate_response(qs, **additional_data)
+        return self.formatter.generate_response(self.queryset, **additional_data)
 
     def is_available_to(self, user):
         return user.is_staff
diff --git a/src/oscar/apps/voucher/reports.py b/src/oscar/apps/voucher/reports.py
--- a/src/oscar/apps/voucher/reports.py
+++ b/src/oscar/apps/voucher/reports.py
@@ -34,14 +34,10 @@ class VoucherReportHTMLFormatter(ReportHTMLFormatter):
 
 
 class VoucherReportGenerator(ReportGenerator):
-
     code = 'vouchers'
     description = _('Voucher performance')
+    model_class = Voucher
 
     formatters = {
         'CSV_formatter': VoucherReportCSVFormatter,
         'HTML_formatter': VoucherReportHTMLFormatter}
-
-    def generate(self):
-        vouchers = Voucher._default_manager.all()
-        return self.formatter.generate_response(vouchers)


Test patch:
diff --git a/tests/integration/basket/test_report.py b/tests/integration/basket/test_report.py
--- a/tests/integration/basket/test_report.py
+++ b/tests/integration/basket/test_report.py
@@ -1,12 +1,21 @@
 import datetime
 
 from django.test import TestCase
+from django.utils.timezone import utc
+from freezegun import freeze_time
 
 from oscar.apps.basket.reports import (
     OpenBasketReportGenerator, SubmittedBasketReportGenerator)
+from oscar.core.loading import get_model
+from oscar.test.factories import BasketFactory
+
+Basket = get_model('basket', 'Basket')
 
 
 class TestBasketReports(TestCase):
+    def setUp(self) -> None:
+        BasketFactory.create_batch(5, status=Basket.OPEN)
+        BasketFactory.create_batch(6, status=Basket.SUBMITTED)
 
     def test_open_report_doesnt_error(self):
         data = {
@@ -17,6 +26,21 @@ def test_open_report_doesnt_error(self):
         generator = OpenBasketReportGenerator(**data)
         generator.generate()
 
+    def test_open_report_queryset(self):
+        generator = OpenBasketReportGenerator()
+        assert generator.queryset.count() == 5
+
+    @freeze_time('2020-05-02')
+    def test_open_report_filtering_by_date_range(self):
+        BasketFactory.create(status=Basket.OPEN)
+        data = {
+            'start_date': datetime.date(2020, 5, 1),
+            'end_date': datetime.date(2020, 6, 1),
+            'formatter': 'CSV'
+        }
+        generator = OpenBasketReportGenerator(**data)
+        assert generator.queryset.count() == 1
+
     def test_submitted_report_doesnt_error(self):
         data = {
             'start_date': datetime.date(2012, 5, 1),
@@ -25,3 +49,18 @@ def test_submitted_report_doesnt_error(self):
         }
         generator = SubmittedBasketReportGenerator(**data)
         generator.generate()
+
+    def test_submitted_report_queryset(self):
+        generator = SubmittedBasketReportGenerator()
+        assert generator.queryset.count() == 6
+
+    def test_submitted_report_filtering_by_date_range(self):
+        date_submitted = datetime.datetime(2020, 7, 3).replace(tzinfo=utc)
+        BasketFactory.create(status=Basket.SUBMITTED, date_submitted=date_submitted)
+        data = {
+            'start_date': datetime.date(2020, 7, 1),
+            'end_date': datetime.date(2020, 8, 1),
+            'formatter': 'CSV'
+        }
+        generator = SubmittedBasketReportGenerator(**data)
+        assert generator.queryset.count() == 1