Skip to content
This repository has been archived by the owner on May 13, 2024. It is now read-only.

Commit

Permalink
feat(filters): allow filtering of tasks and reports in statistics
Browse files Browse the repository at this point in the history
When filtering statistics, we face the problem that we want to
have a list of all tasks, and count the reported hours. Due to the
way Django works, the Sum of reported hours would trigger an (unfiltered)
subquery or Join, so our filters don't apply.

Therefore, we now have a "split" pseudo-queryset, which contains separate
querysets for tasks and reports. It then applies the filters to EITHER
of the querysets as needed (depending on prefix), then combines them
at the end of the filtering phase, using a subquery.
  • Loading branch information
winged authored and trowik committed Oct 5, 2022
1 parent 21d3677 commit b5b9c8d
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 71 deletions.
10 changes: 9 additions & 1 deletion timed/reports/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
from timed.projects.models import Task


class TaskStatisticFilterSet(FilterSet):
class MultiQSFilterMixin():
def filter_queryset(self, queryset):
qs = super().filter_queryset(queryset)
return qs._finalize()



class TaskStatisticFilterSet(MultiQSFilterMixin, FilterSet):
"""Filter set for the customer, project and task statistic endpoint."""

id = BaseInFilter()
Expand Down Expand Up @@ -141,6 +148,7 @@ def filter_cost_center(self, queryset, name, value):
| Q(project__cost_center=value) & Q(cost_center__isnull=True)
)


class Meta:
"""Meta information for the task statistic filter set."""

Expand Down
19 changes: 10 additions & 9 deletions timed/reports/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from django.contrib.auth import get_user_model
from rest_framework_json_api import relations
from rest_framework_json_api.serializers import DurationField, IntegerField, Serializer
from rest_framework_json_api.serializers import (
CharField,
DurationField,
IntegerField,
Serializer,
)

from timed.projects.models import Customer, Project, Task
from timed.serializers import TotalTimeRootMetaMixin
Expand All @@ -25,9 +30,7 @@ class Meta:

class CustomerStatisticSerializer(TotalTimeRootMetaMixin, Serializer):
duration = DurationField()
customer = relations.ResourceRelatedField(
source="project__customer", model=Customer, read_only=True
)
name = CharField(read_only=True)

included_serializers = {"customer": "timed.projects.serializers.CustomerSerializer"}

Expand All @@ -37,17 +40,15 @@ class Meta:

class ProjectStatisticSerializer(TotalTimeRootMetaMixin, Serializer):
duration = DurationField()
project = relations.ResourceRelatedField(
model=Project, read_only=True
)

included_serializers = {"project": "timed.projects.serializers.ProjectSerializer"}
name = CharField()

class Meta:
resource_name = "project-statistics"


class TaskStatisticSerializer(TotalTimeRootMetaMixin, Serializer):
name = CharField(read_only=True)
most_recent_remaining_effort = DurationField(read_only=True)
duration = DurationField(read_only=True)
project = relations.ResourceRelatedField(model=Project, read_only=True)

Expand Down
26 changes: 12 additions & 14 deletions timed/reports/tests/test_customer_statistic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@

from timed.conftest import setup_customer_and_employment_status
from timed.employment.factories import EmploymentFactory
from timed.tracking.factories import ReportFactory
from timed.projects.models import Customer
from timed.tracking.factories import ReportFactory


@pytest.mark.parametrize(
"is_employed, is_customer_assignee, is_customer, expected, status_code",
[
(False, True, False, 1, status.HTTP_403_FORBIDDEN),
(False, True, True, 1, status.HTTP_403_FORBIDDEN),
(True, False, False, 4, status.HTTP_200_OK),
(True, True, False, 4, status.HTTP_200_OK),
(True, True, True, 4, status.HTTP_200_OK),
(True, False, False, 3, status.HTTP_200_OK),
(True, True, False, 3, status.HTTP_200_OK),
(True, True, True, 3, status.HTTP_200_OK),
],
)
def test_customer_statistic_list(
Expand Down Expand Up @@ -55,17 +55,17 @@ def test_customer_statistic_list(
{
"type": "customer-statistics",
"id": str(report.task.project.customer.id),
"attributes": {"duration": "03:00:00"},
"relationships": {
"customer": {"data": {"id": str(report.task.project.customer.id), "type": "customers"}}
"attributes": {
"duration": "03:00:00",
"name": report.task.project.customer.name,
},
},
{
"type": "customer-statistics",
"id": str(report2.task.project.customer.id),
"attributes": {"duration": "04:00:00"},
"relationships": {
"customer": {"data": {"id": str(report2.task.project.customer.id), "type": "customers"}}
"attributes": {
"duration": "04:00:00",
"name": report2.task.project.customer.name,
},
},
]
Expand All @@ -76,7 +76,7 @@ def test_customer_statistic_list(
@pytest.mark.parametrize(
"is_employed, expected, status_code",
[
(True, 5, status.HTTP_200_OK),
(True, 4, status.HTTP_200_OK),
(False, 1, status.HTTP_403_FORBIDDEN),
],
)
Expand All @@ -89,9 +89,7 @@ def test_customer_statistic_detail(

url = reverse("customer-statistic-detail", args=[report.task.project.customer.id])
with django_assert_num_queries(expected):
result = auth_client.get(
url, data={"ordering": "duration"}
)
result = auth_client.get(url, data={"ordering": "duration"})
assert result.status_code == status_code
if status_code == status.HTTP_200_OK:
json = result.json()
Expand Down
36 changes: 16 additions & 20 deletions timed/reports/tests/test_project_statistic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework import status

from timed.conftest import setup_customer_and_employment_status
from timed.projects.factories import TaskFactory
from timed.tracking.factories import ReportFactory


Expand All @@ -13,9 +14,9 @@
[
(False, True, False, 1, status.HTTP_403_FORBIDDEN),
(False, True, True, 1, status.HTTP_403_FORBIDDEN),
(True, False, False, 4, status.HTTP_200_OK),
(True, True, False, 4, status.HTTP_200_OK),
(True, True, True, 4, status.HTTP_200_OK),
(True, False, False, 3, status.HTTP_200_OK),
(True, True, False, 3, status.HTTP_200_OK),
(True, True, True, 3, status.HTTP_200_OK),
],
)
def test_project_statistic_list(
Expand All @@ -38,38 +39,33 @@ def test_project_statistic_list(
report = ReportFactory.create(duration=timedelta(hours=1))
ReportFactory.create(duration=timedelta(hours=2), task=report.task)
report2 = ReportFactory.create(duration=timedelta(hours=4))
task = TaskFactory(project=report.task.project)
ReportFactory.create(duration=timedelta(hours=2), task=task)

url = reverse("project-statistic-list")
with django_assert_num_queries(expected):
result = auth_client.get(
url, data={"ordering": "duration", "include": "project"}
)
result = auth_client.get(url, data={"ordering": "duration"})
assert result.status_code == status_code

if status_code == status.HTTP_200_OK:
json = result.json()
expected_json = [
{
"type": "project-statistics",
"id": str(report.task.project.id),
"attributes": {"duration": "03:00:00"},
"relationships": {
"project": {
"data": {"id": str(report.task.project.id), "type": "projects"}
}
"id": str(report2.task.project.id),
"attributes": {
"duration": "04:00:00",
"name": report2.task.project.name,
},
},
{
"type": "project-statistics",
"id": str(report2.task.project.id),
"attributes": {"duration": "04:00:00"},
"relationships": {
"project": {
"data": {"id": str(report2.task.project.id), "type": "projects"}
}
"id": str(report.task.project.id),
"attributes": {
"duration": "05:00:00",
"name": report.task.project.name,
},
},
]
assert json["data"] == expected_json
assert len(json["included"]) == 2
assert json["meta"]["total-time"] == "07:00:00"
assert json["meta"]["total-time"] == "09:00:00"
67 changes: 61 additions & 6 deletions timed/reports/tests/test_task_statistic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import timedelta
from datetime import date, timedelta

import pytest
from django.urls import reverse
Expand Down Expand Up @@ -59,20 +59,75 @@ def test_task_statistic_list(
{
"type": "task-statistics",
"id": str(task_test.id),
"attributes": {"duration": "03:00:00"},
"attributes": {
"duration": "03:00:00",
"name": str(task_test.name),
"most-recent-remaining-effort": None,
},
"relationships": {
"project": {"data": {"id": str(task_test.project.id), "type": "projects"}}
"project": {
"data": {"id": str(task_test.project.id), "type": "projects"}
}
},
},
{
"type": "task-statistics",
"id": str(task_z.id),
"attributes": {"duration": "02:00:00"},
"attributes": {
"duration": "02:00:00",
"name": str(task_z.name),
"most-recent-remaining-effort": None,
},
"relationships": {
"project": {"data": {"id": str(task_z.project.id), "type": "projects"}}
"project": {
"data": {"id": str(task_z.project.id), "type": "projects"}
}
},
},
]
assert json["data"] == expected_json
assert len(json["included"]) == 4
assert json["meta"]["total-time"] == "05:00:00"


@pytest.mark.parametrize(
"filter, expected_result",
[("from_date", 5), ("customer", 3)],
)
def test_task_statistic_filtered(
auth_client,
filter,
expected_result,
):

user = auth_client.user
setup_customer_and_employment_status(
user=user,
is_assignee=True,
is_customer=True,
is_employed=True,
is_external=False,
)

task_z = TaskFactory.create(name="Z")
task_test = TaskFactory.create(name="Test")

ReportFactory.create(duration=timedelta(hours=1), date="2022-08-05", task=task_test)
ReportFactory.create(duration=timedelta(hours=2), date="2022-08-30", task=task_test)
ReportFactory.create(duration=timedelta(hours=3), date="2022-09-01", task=task_z)

filter_values = {
"from_date": "2022-08-20", # last two reports
"customer": str(task_test.project.customer.pk), # first two
}
the_filter = {filter: filter_values[filter]}

url = reverse("task-statistic-list")
result = auth_client.get(
url,
data={"ordering": "name", "include": "project,project.customer", **the_filter},
)
assert result.status_code == status.HTTP_200_OK

json = result.json()

assert json["meta"]["total-time"] == f"{expected_result:02}:00:00"
Loading

0 comments on commit b5b9c8d

Please sign in to comment.