From 91f8e36ae6a4a17237f2bb064e48feb602278eb8 Mon Sep 17 00:00:00 2001 From: Andreas Vester Date: Fri, 23 Jan 2026 20:04:34 +0100 Subject: [PATCH] feat: Add send_automatic_confirmation_email setting Add configuration option to control automatic confirmation emails when applications are approved, separate from rejection emails. - Add `send_automatic_confirmation_email` setting (default: False) - Expand test coverage with 8 parametrized scenarios Closes #16 --- .env.example | 5 +- CHANGELOG.md | 7 ++- src/projectvote/backend/config.py | 3 +- src/projectvote/backend/main.py | 7 ++- tests/test_main.py | 81 ++++++++++++++++++++++++++----- 5 files changed, 84 insertions(+), 19 deletions(-) diff --git a/.env.example b/.env.example index ce0b307..d7d9ca9 100644 --- a/.env.example +++ b/.env.example @@ -26,8 +26,9 @@ DB_ECHO=False # ----------------------------------------------------------------------------- # Email Configuration (for fastapi-mail) # ----------------------------------------------------------------------------- -# Send automatic rejection emails to applicants -SEND_AUTOMATIC_REJECTION_EMAIL=true +# Send automatic confirmation and rejection emails to applicants +SEND_AUTOMATIC_CONFIRMATION_EMAIL=true +SEND_AUTOMATIC_REJECTION_EMAIL=false # Example for a real SMTP server (e.g., SendGrid, Mailgun) MAIL_DRIVER=smtp diff --git a/CHANGELOG.md b/CHANGELOG.md index 5be54d7..4aaf637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Added + +- ``send_automatic_confirmation_email`` setting ([GH#16](https://github.com/andreas-vester/ProjectVote/issues/16)). + ## [0.4.0] - 2026-01-23 ### Added -- Time stamps recording the time of application creation and conclusion. -- Time stamps recording the exact time of individual voting casts. +- Time stamps recording the time of application creation and conclusion and time of individual voting casts ([GH#12](https://github.com/andreas-vester/ProjectVote/issues/12)). ### Changed diff --git a/src/projectvote/backend/config.py b/src/projectvote/backend/config.py index 99ec595..cf8143c 100644 --- a/src/projectvote/backend/config.py +++ b/src/projectvote/backend/config.py @@ -34,7 +34,8 @@ def board_members_must_be_set(cls, v: str | None) -> str: # Application settings frontend_url: str = "http://localhost:5173" backend_url: str = "http://localhost:8008" - send_automatic_rejection_email: bool = True + send_automatic_confirmation_email: bool = False + send_automatic_rejection_email: bool = False # Database settings db_echo: bool = True diff --git a/src/projectvote/backend/main.py b/src/projectvote/backend/main.py index 02ff5c8..fbd22e5 100644 --- a/src/projectvote/backend/main.py +++ b/src/projectvote/backend/main.py @@ -271,9 +271,12 @@ async def send_final_decision_emails( } # --- Email to Applicant --- - if not ( + if ( + application.status == ApplicationStatus.APPROVED + and settings.send_automatic_confirmation_email + ) or ( application.status == ApplicationStatus.REJECTED - and not settings.send_automatic_rejection_email + and settings.send_automatic_rejection_email ): await send_email( recipients=[application.applicant_email], diff --git a/tests/test_main.py b/tests/test_main.py index 47f9838..34d8b98 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -738,6 +738,11 @@ async def test_voting_conclusion( else: assert updated_app.concluded_at is not None + @pytest.mark.settings_override( + { + "send_automatic_confirmation_email": True, + } + ) @pytest.mark.asyncio async def test_all_votes_approve( self, client: AsyncClient, session: AsyncSession, mocker: MockerFixture @@ -1274,7 +1279,12 @@ async def test_archive_shows_multiple_applications( class TestEmailFunctionality: """Tests for email sending and related utilities.""" - @pytest.mark.settings_override({"send_automatic_rejection_email": True}) + @pytest.mark.settings_override( + { + "send_automatic_confirmation_email": True, + "send_automatic_rejection_email": True, + } + ) @pytest.mark.parametrize( argnames=("scenario", "votes", "expected_status"), argvalues=[ @@ -1408,6 +1418,7 @@ async def test_final_decision_email_content( @pytest.mark.asyncio @pytest.mark.parametrize( argnames=( + "send_automatic_confirmation_email", "send_automatic_rejection_email", "vote_decision", "expected_status", @@ -1415,33 +1426,71 @@ async def test_final_decision_email_content( "test_id", ), argvalues=[ + # Approval scenarios + ( + True, + True, + VoteOption.APPROVE, + ApplicationStatus.APPROVED, + True, + "approval_email_enabled", + ), + ( + False, + True, + VoteOption.APPROVE, + ApplicationStatus.APPROVED, + False, + "approval_email_disabled", + ), + ( + True, + False, + VoteOption.APPROVE, + ApplicationStatus.APPROVED, + True, + "approval_email_enabled_rejection_disabled", + ), ( + False, + False, + VoteOption.APPROVE, + ApplicationStatus.APPROVED, + False, + "approval_email_disabled_rejection_disabled", + ), + # Rejection scenarios + ( + True, True, VoteOption.REJECT, ApplicationStatus.REJECTED, - 1, + True, "rejection_email_enabled", ), ( + True, False, VoteOption.REJECT, ApplicationStatus.REJECTED, - 0, + False, "rejection_email_disabled", ), ( + False, True, - VoteOption.APPROVE, - ApplicationStatus.APPROVED, - 1, - "approval_email_rejection_enabled", + VoteOption.REJECT, + ApplicationStatus.REJECTED, + True, + "rejection_email_enabled_confirmation_disabled", ), ( False, - VoteOption.APPROVE, - ApplicationStatus.APPROVED, - 1, - "approval_email_rejection_disabled", + False, + VoteOption.REJECT, + ApplicationStatus.REJECTED, + False, + "rejection_email_disabled_confirmation_disabled", ), ], ) @@ -1451,13 +1500,20 @@ async def test_final_decision_email_sending( client: AsyncClient, session: AsyncSession, mocker: MockerFixture, + send_automatic_confirmation_email: bool, send_automatic_rejection_email: bool, vote_decision: VoteOption, expected_status: ApplicationStatus, expected_applicant_emails: int, test_id: str, ) -> None: - """Test sending final decision emails based on settings and outcome.""" + """Test sending final decision emails based on settings and outcome. + + This test verifies that: + 1. Approval emails are only sent when send_automatic_confirmation_email is True + 2. Rejection emails are only sent when send_automatic_rejection_email is True + 3. Board members always receive final decision emails regardless of settings + """ send_email_mock = mocker.patch( "projectvote.backend.main.send_email", new_callable=mocker.AsyncMock ) @@ -1465,6 +1521,7 @@ async def test_final_decision_email_sending( # Override settings for this test app.dependency_overrides[get_app_settings] = lambda: Settings( board_members=",".join(TEST_BOARD_MEMBERS), + send_automatic_confirmation_email=send_automatic_confirmation_email, send_automatic_rejection_email=send_automatic_rejection_email, )