Skip to content

Commit acbedec

Browse files
committed
feat(forms): excel and zip export to önör dimension filters (closes #572)
1 parent a55ea78 commit acbedec

File tree

7 files changed

+80
-25
lines changed

7 files changed

+80
-25
lines changed

backend/dimensions/filters.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from dataclasses import dataclass, field
2+
from typing import Self, TypeVar
3+
4+
from django.db import models
5+
from django.http import QueryDict
6+
7+
from dimensions.graphql.dimension_filter_input import DimensionFilterInput
8+
9+
T = TypeVar("T", bound=models.Model)
10+
11+
12+
@dataclass
13+
class DimensionFilters:
14+
filters: dict[str, list[str]] = field(default_factory=dict)
15+
16+
@classmethod
17+
def from_query_dict(
18+
cls,
19+
filters: QueryDict | dict[str, list[str]],
20+
) -> Self:
21+
if isinstance(filters, QueryDict):
22+
filters = {k: [str(v) for v in vs] for k, vs in filters.lists()}
23+
24+
filters = {
25+
dimension_slug: [slug for slugs in value_slugs for slug in slugs.split(",")]
26+
for (dimension_slug, value_slugs) in filters.items()
27+
}
28+
29+
return cls(
30+
filters=filters,
31+
)
32+
33+
@classmethod
34+
def from_graphql(
35+
cls,
36+
filters: list[DimensionFilterInput] | None,
37+
):
38+
dimensions = (
39+
{filter.dimension: ["*"] if filter.values is None else filter.values for filter in filters}
40+
if filters
41+
else {}
42+
)
43+
44+
return cls(
45+
filters=dimensions, # type: ignore
46+
)
47+
48+
def filter(
49+
self,
50+
queryset: models.QuerySet[T],
51+
):
52+
for dimension_slug, value_slugs in self.filters.items():
53+
value_slugs = [slug for slugs in value_slugs for slug in slugs.split(",")]
54+
if "*" in value_slugs:
55+
queryset = queryset.filter(dimensions__value__dimension__slug=dimension_slug)
56+
else:
57+
queryset = queryset.filter(
58+
dimensions__value__dimension__slug=dimension_slug,
59+
dimensions__value__slug__in=value_slugs,
60+
)
61+
62+
return queryset.distinct()
Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Self, TypeVar
1+
from typing import TypeVar
22

33
import graphene
44
from django.db import models
@@ -16,19 +16,3 @@ class DimensionFilterInput(graphene.InputObjectType):
1616

1717
dimension = graphene.NonNull(graphene.String)
1818
values = graphene.List(graphene.NonNull(graphene.String))
19-
20-
@classmethod
21-
def filter(cls, queryset: models.QuerySet[T], filters: list[Self] | None) -> models.QuerySet[T]:
22-
if filters is None:
23-
filters = []
24-
25-
for filter in filters:
26-
if filter.values is None:
27-
queryset = queryset.filter(dimensions__value__dimension__slug=filter.dimension)
28-
else:
29-
queryset = queryset.filter(
30-
dimensions__value__dimension__slug=filter.dimension,
31-
dimensions__value__slug__in=filter.values,
32-
)
33-
34-
return queryset

backend/forms/graphql/survey_full.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from access.cbac import graphql_check_instance
77
from core.graphql.event_limited import LimitedEventType
88
from core.utils import normalize_whitespace
9+
from dimensions.filters import DimensionFilters
910
from dimensions.graphql.dimension_filter_input import DimensionFilterInput
1011

1112
from ..models.form import Form
@@ -82,7 +83,7 @@ def resolve_responses(
8283
Authorization required.
8384
"""
8485
graphql_check_instance(survey, info, app=survey.app, field="responses")
85-
return DimensionFilterInput.filter(survey.responses.all(), filters)
86+
return DimensionFilters.from_graphql(filters).filter(survey.responses.all()).order_by("created_at")
8687

8788
responses = graphene.List(
8889
graphene.NonNull(LimitedResponseType),
@@ -130,7 +131,7 @@ def resolve_count_responses(
130131
Authorization required.
131132
"""
132133
graphql_check_instance(survey, info, app=survey.app, field="responses")
133-
return DimensionFilterInput.filter(survey.responses.all(), filters).count()
134+
return DimensionFilters.from_graphql(filters).filter(survey.responses.all()).count()
134135

135136
count_responses = graphene.Field(
136137
graphene.NonNull(graphene.Int),
@@ -151,7 +152,7 @@ def resolve_summary(
151152
not present in the base language is not guaranteed. Authorization required.
152153
"""
153154
graphql_check_instance(survey, info, app=survey.app, field="responses")
154-
responses = DimensionFilterInput.filter(survey.responses.all(), filters)
155+
responses = DimensionFilters.from_graphql(filters).filter(survey.responses.all()).order_by("created_at")
155156
fields = survey.get_combined_fields(lang)
156157
valuesies = [response.get_processed_form_data(fields)[0] for response in responses.only("form_data")]
157158
summary = summarize_responses(fields, valuesies)

backend/forms/models/response.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737

3838
class Response(models.Model):
39+
# TODO UUID7
3940
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
4041
form = models.ForeignKey(Form, on_delete=models.CASCADE, related_name="responses")
4142
form_data = JSONField()

backend/forms/views/forms_survey_excel_export_view.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from access.cbac import graphql_check_instance
66
from core.models import Event
7+
from dimensions.filters import DimensionFilters
78

89
from ..excel_export import write_responses_as_excel
910
from ..models.survey import Survey
@@ -32,10 +33,12 @@ def forms_survey_excel_export_view(
3233
response = HttpResponse(content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
3334
response["Content-Disposition"] = f'attachment; filename="{filename}"'
3435

36+
responses = DimensionFilters.from_query_dict(request.GET).filter(survey.responses.all())
37+
3538
write_responses_as_excel(
3639
survey.dimensions.order_by("order"),
3740
survey.combined_fields,
38-
survey.responses.order_by("created_at").only("form_data"),
41+
responses.only("form_data").order_by("created_at"),
3942
response,
4043
)
4144

backend/forms/views/forms_survey_zip_export_view.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.utils.timezone import now
88

99
from access.cbac import graphql_check_instance
10+
from dimensions.filters import DimensionFilters
1011
from dimensions.models.scope import Scope
1112

1213
from ..excel_export import write_responses_as_excel
@@ -38,6 +39,8 @@ def forms_survey_zip_export_view(
3839
operation="query",
3940
)
4041

42+
responses = DimensionFilters.from_query_dict(request.GET).filter(survey.responses.all())
43+
4144
fields = survey.get_combined_fields()
4245
session = requests.Session()
4346
tempfile = TemporaryFile()
@@ -47,11 +50,11 @@ def forms_survey_zip_export_view(
4750
write_responses_as_excel(
4851
survey.dimensions.order_by("order"),
4952
survey.combined_fields,
50-
survey.responses.order_by("created_at").only("form_data"),
53+
responses.only("form_data").order_by("created_at"),
5154
excel_file, # type: ignore
5255
)
5356

54-
for form_response in survey.responses.all():
57+
for form_response in responses:
5558
for attachment in form_response.get_attachments(fields):
5659
attachment_bytes = attachment.download(session=session)
5760
attachment_filename = truncate_filename(

frontend/src/app/[locale]/[eventSlug]/surveys/[surveySlug]/responses/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,10 @@ export default async function FormResponsesPage({
227227
columns.push(...buildKeyDimensionColumns(dimensions));
228228

229229
const exportBaseUrl = `${kompassiBaseUrl}/events/${eventSlug}/surveys/${surveySlug}/responses`;
230+
const queryString = new URLSearchParams(searchParams).toString();
230231
const exportUrls = {
231-
excel: `${exportBaseUrl}.xlsx`,
232-
zip: `${exportBaseUrl}.zip`,
232+
excel: `${exportBaseUrl}.xlsx?${queryString}`,
233+
zip: `${exportBaseUrl}.zip?${queryString}`,
233234
};
234235
const responses = survey.responses || [];
235236

0 commit comments

Comments
 (0)