Skip to content

Commit

Permalink
Refactor CSV exports
Browse files Browse the repository at this point in the history
- Rewrite ReservationUnit export to use BaseCSVExporter
- Consolidate export logic from exporters to BaseCSVExporter
- Move exporters to services
  • Loading branch information
matti-lamppu committed Nov 25, 2024
1 parent 9f24392 commit 84c7a53
Show file tree
Hide file tree
Showing 22 changed files with 1,189 additions and 1,036 deletions.
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ USER root

# Setup environment variables.
ENV PYTHONUNBUFFERED=1 \
PYTHONIOENCODING=utf-8 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONFAULTHANDLER=1 \
PYTHONHASHSEED=random
Expand Down
24 changes: 17 additions & 7 deletions tests/test_csv_exporters/helpers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from dataclasses import dataclass, field
import dataclasses
from collections.abc import Generator
from contextlib import contextmanager
from typing import Any, NamedTuple
from unittest import mock


@dataclass
@dataclasses.dataclass
class Missing:
deleted: list[str] = field(default_factory=list)
null: list[str] = field(default_factory=list)
empty: list[str] = field(default_factory=list)
deleted: list[str] = dataclasses.field(default_factory=list)
null: list[str] = dataclasses.field(default_factory=list)
empty: list[str] = dataclasses.field(default_factory=list)

def remove_from_data(self, data: dict[str, Any]) -> None:
self._delete(data)
Expand Down Expand Up @@ -49,5 +51,13 @@ class MissingParams(NamedTuple):
column_value_mapping: dict[str, Any]


def get_writes(mock_file: mock.MagicMock) -> list[list[str]]:
return [call[1][0] for call in mock_file.mock_calls if call[0] == "().writerow"]
class CSVWriterMock(mock.MagicMock):
def get_writes(self) -> list[list[str]]:
return [call[1][0] for call in self.mock_calls if call[0] == "().writerow"]


@contextmanager
def mock_csv_writer() -> Generator[CSVWriterMock, None, None]:
path = "tilavarauspalvelu.services.csv_export._base_exporter.csv.writer"
with mock.patch(path, new_callable=CSVWriterMock) as mock_file:
yield mock_file
187 changes: 91 additions & 96 deletions tests/test_csv_exporters/test_application_round_applications_export.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import datetime
from typing import TYPE_CHECKING
from unittest import mock

import pytest
from django.utils import timezone
Expand All @@ -9,13 +8,11 @@
from tests.factories import ApplicationFactory, ApplicationRoundFactory
from tests.factories.application import ApplicationBuilder
from tilavarauspalvelu.enums import ApplicantTypeChoice, Priority, Weekday
from tilavarauspalvelu.utils.exporter.application_round_applications_exporter import (
ApplicationExportRow,
ApplicationRoundApplicationsCSVExporter,
)
from tilavarauspalvelu.services.csv_export import ApplicationRoundApplicationsCSVExporter
from tilavarauspalvelu.services.csv_export.application_round_applications_exporter import ApplicationExportRow
from utils.date_utils import local_date_string, local_timedelta_string

from .helpers import Missing, MissingParams, get_writes
from .helpers import Missing, MissingParams, mock_csv_writer

if TYPE_CHECKING:
from tilavarauspalvelu.models import ApplicationSection
Expand All @@ -25,8 +22,6 @@
pytest.mark.django_db,
]

CSV_WRITER_MOCK_PATH = "tilavarauspalvelu.utils.exporter.application_round_applications_exporter.csv.writer"


def test_application_round_applications_export__multiple_applications(graphql):
application_round = ApplicationRoundFactory.create_in_status_in_allocation()
Expand Down Expand Up @@ -60,100 +55,96 @@ def test_application_round_applications_export__multiple_applications(graphql):
section_2: ApplicationSection = application_2.application_sections.first()

exporter = ApplicationRoundApplicationsCSVExporter(application_round_id=application_round.id)
with mock.patch(CSV_WRITER_MOCK_PATH) as mock_writer:
exporter.export()
with mock_csv_writer() as mock_writer:
exporter.write()

writes = get_writes(mock_writer)
writes = mock_writer.get_writes()

assert exporter.max_options == 1
header_rows = exporter._get_header_rows()
header_rows = [list(row.as_row()) for row in exporter.get_header_rows()]
assert writes[0] == header_rows[0]
assert writes[1] == header_rows[1]
assert writes[2] == header_rows[2]

assert writes[3] == [
*list(
ApplicationExportRow(
application_id=str(application_1.id),
application_status=application_1.status.value,
applicant=application_1.organisation.name,
organisation_id=application_1.organisation.identifier,
contact_person_first_name=application_1.contact_person.first_name,
contact_person_last_name=application_1.contact_person.last_name,
contact_person_email=application_1.contact_person.email,
contact_person_phone=application_1.contact_person.phone_number,
section_id=str(section_1.id),
section_status=section_1.status.value,
section_name=section_1.name,
reservations_begin_date=local_date_string(section_1.reservations_begin_date),
reservations_end_date=local_date_string(section_1.reservations_end_date),
home_city_name=application_1.home_city.name,
purpose_name=section_1.purpose.name,
age_group_str=str(section_1.age_group),
num_persons=str(section_1.num_persons),
applicant_type=application_1.applicant_type,
applied_reservations_per_week=str(section_1.applied_reservations_per_week),
reservation_min_duration=local_timedelta_string(section_1.reservation_min_duration),
reservation_max_duration=local_timedelta_string(section_1.reservation_max_duration),
primary_monday="",
primary_tuesday="12:00-14:00",
primary_wednesday="",
primary_thursday="",
primary_friday="",
primary_saturday="",
primary_sunday="",
secondary_monday="",
secondary_tuesday="",
secondary_wednesday="",
secondary_thursday="",
secondary_friday="",
secondary_saturday="",
secondary_sunday="",
)
),
*ApplicationExportRow(
application_id=str(application_1.id),
application_status=application_1.status.value,
applicant=application_1.organisation.name,
organisation_id=application_1.organisation.identifier,
contact_person_first_name=application_1.contact_person.first_name,
contact_person_last_name=application_1.contact_person.last_name,
contact_person_email=application_1.contact_person.email,
contact_person_phone=application_1.contact_person.phone_number,
section_id=str(section_1.id),
section_status=section_1.status.value,
section_name=section_1.name,
reservations_begin_date=local_date_string(section_1.reservations_begin_date),
reservations_end_date=local_date_string(section_1.reservations_end_date),
home_city_name=application_1.home_city.name,
purpose_name=section_1.purpose.name,
age_group_str=str(section_1.age_group),
num_persons=str(section_1.num_persons),
applicant_type=application_1.applicant_type,
applied_reservations_per_week=str(section_1.applied_reservations_per_week),
reservation_min_duration=local_timedelta_string(section_1.reservation_min_duration),
reservation_max_duration=local_timedelta_string(section_1.reservation_max_duration),
primary_monday="",
primary_tuesday="12:00-14:00",
primary_wednesday="",
primary_thursday="",
primary_friday="",
primary_saturday="",
primary_sunday="",
secondary_monday="",
secondary_tuesday="",
secondary_wednesday="",
secondary_thursday="",
secondary_friday="",
secondary_saturday="",
secondary_sunday="",
).as_row(),
"foo, fizz", # reservation unit option
]

assert writes[4] == [
*list(
ApplicationExportRow(
application_id=str(application_2.id),
application_status=application_2.status.value,
applicant=application_2.organisation.name,
organisation_id=application_2.organisation.identifier,
contact_person_first_name=application_2.contact_person.first_name,
contact_person_last_name=application_2.contact_person.last_name,
contact_person_email=application_2.contact_person.email,
contact_person_phone=application_2.contact_person.phone_number,
section_id=str(section_2.id),
section_status=section_2.status.value,
section_name=section_2.name,
reservations_begin_date=local_date_string(section_2.reservations_begin_date),
reservations_end_date=local_date_string(section_2.reservations_end_date),
home_city_name=application_2.home_city.name,
purpose_name=section_2.purpose.name,
age_group_str=str(section_2.age_group),
num_persons=str(section_2.num_persons),
applicant_type=application_2.applicant_type,
applied_reservations_per_week=str(section_2.applied_reservations_per_week),
reservation_min_duration=local_timedelta_string(section_2.reservation_min_duration),
reservation_max_duration=local_timedelta_string(section_2.reservation_max_duration),
primary_monday="",
primary_tuesday="",
primary_wednesday="",
primary_thursday="",
primary_friday="",
primary_saturday="",
primary_sunday="",
secondary_monday="",
secondary_tuesday="",
secondary_wednesday="",
secondary_thursday="",
secondary_friday="12:00-14:00",
secondary_saturday="",
secondary_sunday="",
)
),
*ApplicationExportRow(
application_id=str(application_2.id),
application_status=application_2.status.value,
applicant=application_2.organisation.name,
organisation_id=application_2.organisation.identifier,
contact_person_first_name=application_2.contact_person.first_name,
contact_person_last_name=application_2.contact_person.last_name,
contact_person_email=application_2.contact_person.email,
contact_person_phone=application_2.contact_person.phone_number,
section_id=str(section_2.id),
section_status=section_2.status.value,
section_name=section_2.name,
reservations_begin_date=local_date_string(section_2.reservations_begin_date),
reservations_end_date=local_date_string(section_2.reservations_end_date),
home_city_name=application_2.home_city.name,
purpose_name=section_2.purpose.name,
age_group_str=str(section_2.age_group),
num_persons=str(section_2.num_persons),
applicant_type=application_2.applicant_type,
applied_reservations_per_week=str(section_2.applied_reservations_per_week),
reservation_min_duration=local_timedelta_string(section_2.reservation_min_duration),
reservation_max_duration=local_timedelta_string(section_2.reservation_max_duration),
primary_monday="",
primary_tuesday="",
primary_wednesday="",
primary_thursday="",
primary_friday="",
primary_saturday="",
primary_sunday="",
secondary_monday="",
secondary_tuesday="",
secondary_wednesday="",
secondary_thursday="",
secondary_friday="12:00-14:00",
secondary_saturday="",
secondary_sunday="",
).as_row(),
"bar, buzz", # reservation unit option
]

Expand Down Expand Up @@ -231,14 +222,16 @@ def test_application_round_applications_export__missing_data(graphql, column_val
missing.remove_from_data(data)
ApplicationFactory.create(**data)

exporter = ApplicationRoundApplicationsCSVExporter(application_round_id=application_round.id)

# when:
# - The exporter is run
with mock.patch(CSV_WRITER_MOCK_PATH) as mock_writer:
ApplicationRoundApplicationsCSVExporter(application_round_id=application_round.id).export()
with mock_csv_writer() as mock_writer:
exporter.write()

# then:
# - The writes to the csv file are correct
writes = get_writes(mock_writer)
writes = mock_writer.get_writes()

header_row = writes[2]
data_row = writes[3]
Expand All @@ -263,14 +256,16 @@ def test_application_round_applications_export__no_reservation_unit_options(grap
)
application_1.application_sections.first()

exporter = ApplicationRoundApplicationsCSVExporter(application_round_id=application_round.id)

# when:
# - The exporter is run
with mock.patch(CSV_WRITER_MOCK_PATH) as mock_writer:
ApplicationRoundApplicationsCSVExporter(application_round_id=application_round.id).export()
with mock_csv_writer() as mock_writer:
exporter.write()

# then:
# - The csv doesn't contain the reservation unit options column
writes = get_writes(mock_writer)
writes = mock_writer.get_writes()

header_row = writes[2]
assert "tilatoive 1" not in header_row
Loading

0 comments on commit 84c7a53

Please sign in to comment.