From 8d602b12e22dc39550edb82ccd8b3e2ac2a78fd9 Mon Sep 17 00:00:00 2001
From: Todd Dembrey <todd.dembrey@torchbox.com>
Date: Wed, 6 Nov 2019 15:30:28 +0000
Subject: [PATCH] Feature/gh 1651 all reports table (#1663)

* Add table to filter by submitted reports
* Add report table to the project overview page
---
 opentech/apply/projects/filters.py            | 40 +++++++++++-
 opentech/apply/projects/models.py             | 64 ++++++++++++++++++-
 opentech/apply/projects/tables.py             | 28 +++++++-
 .../widgets/date_range_input_widget.html      |  5 ++
 .../application_projects/overview.html        | 13 ++++
 .../application_projects/report_list.html     | 37 +++++++++++
 opentech/apply/projects/urls.py               |  2 +
 opentech/apply/projects/views/project.py      | 16 ++++-
 opentech/apply/projects/views/report.py       | 14 +++-
 9 files changed, 212 insertions(+), 7 deletions(-)
 create mode 100644 opentech/apply/projects/templates/application_projects/filters/widgets/date_range_input_widget.html
 create mode 100644 opentech/apply/projects/templates/application_projects/report_list.html

diff --git a/opentech/apply/projects/filters.py b/opentech/apply/projects/filters.py
index 260cc0ec0..8f793c80c 100644
--- a/opentech/apply/projects/filters.py
+++ b/opentech/apply/projects/filters.py
@@ -12,7 +12,8 @@ from .models import (
     PROJECT_STATUS_CHOICES,
     REQUEST_STATUS_CHOICES,
     PaymentRequest,
-    Project
+    Project,
+    Report,
 )
 
 User = get_user_model()
@@ -41,3 +42,40 @@ class ProjectListFilter(filters.FilterSet):
     class Meta:
         fields = ['status', 'lead', 'fund']
         model = Project
+
+
+class DateRangeInputWidget(filters.widgets.SuffixedMultiWidget):
+    template_name = 'application_projects/filters/widgets/date_range_input_widget.html'
+    suffixes = ['after', 'before']
+
+    def __init__(self, attrs=None):
+        widgets = (forms.DateInput, forms.DateInput)
+        super().__init__(widgets, attrs)
+
+    def decompress(self, value):
+        if value:
+            return [value.start, value.stop]
+        return [None, None]
+
+
+class ReportListFilter(filters.FilterSet):
+    reporting_period = filters.DateFromToRangeFilter(
+        label="Reporting Period",
+        method="filter_reporting_period",
+        widget=DateRangeInputWidget,
+    )
+    submitted = filters.DateFromToRangeFilter(widget=DateRangeInputWidget)
+
+    class Meta:
+        model = Report
+        fields = ['submitted']
+
+    def filter_reporting_period(self, queryset, name, value):
+        after, before = value.start, value.stop
+        q = {}
+        if after:
+            q['start__gte'] = after
+        if before:
+            q['end_date__lte'] = before
+
+        return queryset.filter(**q)
diff --git a/opentech/apply/projects/models.py b/opentech/apply/projects/models.py
index f047e1da7..4d0b0034b 100644
--- a/opentech/apply/projects/models.py
+++ b/opentech/apply/projects/models.py
@@ -1,4 +1,5 @@
 import collections
+import datetime
 import decimal
 import json
 import logging
@@ -12,8 +13,19 @@ from django.contrib.postgres.fields import JSONField
 from django.core.exceptions import ValidationError
 from django.core.validators import MinValueValidator
 from django.db import models
-from django.db.models import F, Max, Q, Sum, Value as V
-from django.db.models.functions import Coalesce
+from django.db.models import (
+    Case,
+    F,
+    ExpressionWrapper,
+    Max,
+    OuterRef,
+    Q,
+    Subquery,
+    Sum,
+    Value as V,
+    When,
+)
+from django.db.models.functions import Cast, Coalesce
 from django.db.models.signals import post_delete
 from django.dispatch.dispatcher import receiver
 from django.urls import reverse
@@ -301,6 +313,20 @@ class ProjectQuerySet(models.QuerySet):
             last_payment_request=Max('payment_requests__requested_at'),
         )
 
+    def with_start_date(self):
+        return self.annotate(
+            start=Cast(
+                Subquery(
+                    Contract.objects.filter(
+                        project=OuterRef('pk'),
+                    ).approved().order_by(
+                        'approved_at'
+                    ).values('approved_at')[:1]
+                ),
+                models.DateField(),
+            )
+        )
+
     def for_table(self):
         return self.with_amount_paid().with_last_payment()
 
@@ -651,6 +677,40 @@ class ReportQueryset(models.QuerySet):
             Q(current__isnull=False) | Q(skipped=True),
         )
 
+    def submitted(self):
+        return self.filter(current__isnull=False)
+
+    def for_table(self):
+        return self.annotate(
+            last_end_date=Subquery(
+                Report.objects.filter(
+                    project=OuterRef('project_id'),
+                    end_date__lt=OuterRef('end_date')
+                ).values('end_date')[:1]
+            ),
+            project_start_date=Subquery(
+                Project.objects.filter(
+                    pk=OuterRef('project_id'),
+                ).with_start_date().values('start')[:1]
+            ),
+            start=Case(
+                When(
+                    last_end_date__isnull=False,
+                    # Expression Wrapper doesn't cast the calculated object
+                    # Use cast to get an actual date object
+                    then=Cast(
+                        ExpressionWrapper(
+                            F('last_end_date') + datetime.timedelta(days=1),
+                            output_field=models.DateTimeField(),
+                        ),
+                        models.DateField(),
+                    ),
+                ),
+                default=F('project_start_date'),
+                output_field=models.DateField(),
+            )
+        )
+
 
 class Report(models.Model):
     skipped = models.BooleanField(default=False)
diff --git a/opentech/apply/projects/tables.py b/opentech/apply/projects/tables.py
index 1aaa9d67b..4064f5dae 100644
--- a/opentech/apply/projects/tables.py
+++ b/opentech/apply/projects/tables.py
@@ -4,7 +4,7 @@ import django_tables2 as tables
 from django.db.models import F, Sum
 from django.contrib.humanize.templatetags.humanize import intcomma
 
-from .models import PaymentRequest, Project
+from .models import PaymentRequest, Project, Report
 
 
 class BasePaymentRequestsTable(tables.Table):
@@ -119,3 +119,29 @@ class ProjectsListTable(BaseProjectsTable):
 
     def order_end_date(self, qs, desc):
         return qs.by_end_date(desc), True
+
+
+class ReportListTable(tables.Table):
+    project = tables.LinkColumn(
+        'funds:projects:reports:detail',
+        text=lambda r: textwrap.shorten(r.project.title, width=30, placeholder="..."),
+        args=[tables.utils.A('pk')],
+    )
+    report_period = tables.Column(accessor='pk')
+    submitted = tables.DateColumn()
+    lead = tables.Column(accessor='project.lead')
+
+    class Meta:
+        fields = [
+            'project',
+            'submitted',
+        ]
+        sequence = [
+            'project',
+            'report_period',
+            '...'
+        ]
+        model = Report
+
+    def render_report_period(self, record):
+        return f"{record.start} to {record.end_date}"
diff --git a/opentech/apply/projects/templates/application_projects/filters/widgets/date_range_input_widget.html b/opentech/apply/projects/templates/application_projects/filters/widgets/date_range_input_widget.html
new file mode 100644
index 000000000..fe469bc85
--- /dev/null
+++ b/opentech/apply/projects/templates/application_projects/filters/widgets/date_range_input_widget.html
@@ -0,0 +1,5 @@
+from:
+{% include widget.subwidgets.0.template_name %}
+to:
+{% include widget.subwidgets.1.template_name %}
+
diff --git a/opentech/apply/projects/templates/application_projects/overview.html b/opentech/apply/projects/templates/application_projects/overview.html
index 6adc7c31c..e3159e849 100644
--- a/opentech/apply/projects/templates/application_projects/overview.html
+++ b/opentech/apply/projects/templates/application_projects/overview.html
@@ -54,6 +54,19 @@
     </div>
     {% endif %}
 
+    {% if reports.table.data %}
+    <div class="wrapper wrapper--bottom-space">
+
+        {% include "funds/includes/table_filter_and_search.html" with filter=reports.filterset filter_action=reports.url heading="Reports" %}
+
+        {% render_table reports.table %}
+
+        <div class="all-submissions-table__more">
+            <a href="{{ reports.url }}">Show all</a>
+        </div>
+
+    </div>
+    {% endif %}
 </div>
 
 {% endblock %}
diff --git a/opentech/apply/projects/templates/application_projects/report_list.html b/opentech/apply/projects/templates/application_projects/report_list.html
new file mode 100644
index 000000000..8a3579808
--- /dev/null
+++ b/opentech/apply/projects/templates/application_projects/report_list.html
@@ -0,0 +1,37 @@
+{% extends "base-apply.html" %}
+
+{% load render_table from django_tables2 %}
+{% load static %}
+
+{% block title %}Reports{% endblock %}
+
+{% block content %}
+<div class="admin-bar">
+    <div class="admin-bar__inner wrapper--search">
+        {% block page_header %}
+            <div>
+                <h1 class="gamma heading heading--no-margin heading--bold">Submitted Reports</h1>
+            </div>
+        {% endblock %}
+    </div>
+</div>
+
+<div class="wrapper wrapper--large wrapper--inner-space-medium">
+    {% if table %}
+    {% include "funds/includes/table_filter_and_search.html" with filter_form=filter_form use_search=False filter_action=filter_action %}
+    {% render_table table %}
+    {% else %}
+    <p>No Reports Available</p>
+    {% endif %}
+</div>
+
+{% endblock content %}
+
+{% block extra_css %}
+    {{ filter.form.media.css }}
+{% endblock %}
+
+{% block extra_js %}
+    {{ filter.form.media.js }}
+    <script src="{% static 'js/apply/submission-filters.js' %}"></script>
+{% endblock %}
diff --git a/opentech/apply/projects/urls.py b/opentech/apply/projects/urls.py
index d5f379881..41d284975 100644
--- a/opentech/apply/projects/urls.py
+++ b/opentech/apply/projects/urls.py
@@ -15,6 +15,7 @@ from .views import (
     ProjectOverviewView,
     ProjectPrivateMediaView,
     ReportDetailView,
+    ReportListView,
     ReportPrivateMedia,
     ReportSkipView,
     ReportUpdateView,
@@ -44,6 +45,7 @@ urlpatterns = [
         ])),
     ], 'payments'))),
     path('reports/', include(([
+        path('', ReportListView.as_view(), name='all'),
         path('<int:pk>/', include([
             path('', ReportDetailView.as_view(), name='detail'),
             path('skip/', ReportSkipView.as_view(), name='skip'),
diff --git a/opentech/apply/projects/views/project.py b/opentech/apply/projects/views/project.py
index 0f823130b..76e4e727d 100644
--- a/opentech/apply/projects/views/project.py
+++ b/opentech/apply/projects/views/project.py
@@ -38,6 +38,7 @@ from ..files import get_files
 from ..filters import (
     PaymentRequestListFilter,
     ProjectListFilter,
+    ReportListFilter,
 )
 from ..forms import (
     ApproveContractForm,
@@ -61,11 +62,13 @@ from ..models import (
     Contract,
     PacketFile,
     PaymentRequest,
-    Project
+    Project,
+    Report,
 )
 from ..tables import (
     PaymentRequestsListTable,
-    ProjectsListTable
+    ProjectsListTable,
+    ReportListTable,
 )
 
 from .report import ReportingMixin, ReportFrequencyUpdate
@@ -608,9 +611,18 @@ class ProjectOverviewView(TemplateView):
         context = super().get_context_data(**kwargs)
         context['projects'] = self.get_projects(self.request)
         context['payment_requests'] = self.get_payment_requests(self.request)
+        context['reports'] = self.get_reports(self.request)
         context['status_counts'] = self.get_status_counts()
         return context
 
+    def get_reports(self, request):
+        reports = Report.objects.for_table().submitted()[:10]
+        return {
+            'filterset': ReportListFilter(request.GET or None, request=request, queryset=reports),
+            'table': ReportListTable(reports, order_by=()),
+            'url': reverse('apply:projects:reports:all'),
+        }
+
     def get_payment_requests(self, request):
         payment_requests = PaymentRequest.objects.order_by('date_to')[:10]
 
diff --git a/opentech/apply/projects/views/report.py b/opentech/apply/projects/views/report.py
index 2e804f7fe..b76779ef6 100644
--- a/opentech/apply/projects/views/report.py
+++ b/opentech/apply/projects/views/report.py
@@ -10,17 +10,21 @@ from django.views.generic import (
     UpdateView,
 )
 from django.views.generic.detail import SingleObjectMixin
+from django_filters.views import FilterView
+from django_tables2 import SingleTableMixin
 
 from opentech.apply.activity.messaging import MESSAGES, messenger
 from opentech.apply.utils.storage import PrivateMediaView
 from opentech.apply.utils.views import DelegatedViewMixin
 from opentech.apply.users.decorators import staff_required
 
-from ..models import Report, ReportConfig, ReportPrivateFiles
+from ..filters import ReportListFilter
 from ..forms import (
     ReportEditForm,
     ReportFrequencyForm,
 )
+from ..models import Report, ReportConfig, ReportPrivateFiles
+from ..tables import ReportListTable
 
 
 class ReportingMixin:
@@ -197,3 +201,11 @@ class ReportFrequencyUpdate(DelegatedViewMixin, UpdateView):
         )
 
         return response
+
+
+@method_decorator(staff_required, name='dispatch')
+class ReportListView(SingleTableMixin, FilterView):
+    queryset = Report.objects.submitted().for_table()
+    filterset_class = ReportListFilter
+    table_class = ReportListTable
+    template_name = 'application_projects/report_list.html'
-- 
GitLab