Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/projectvote/backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/projectvote/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
81 changes: 69 additions & 12 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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=[
Expand Down Expand Up @@ -1408,40 +1418,79 @@ 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",
"expected_applicant_emails",
"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",
),
],
)
Expand All @@ -1451,20 +1500,28 @@ 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
)

# 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,
)

Expand Down