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
3 changes: 2 additions & 1 deletion ai_docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This document outlines the core functional requirements for the web application
* **Purpose:** Enable board members to securely review and vote on submitted applications.
* **Features:**
* Upon application submission, unique, secure voting links are generated for each board member.
* Board members receive email notifications with their individual voting links.
* Board members receive email notifications with their individual voting links. The email also contains all relevant information about the application.
* Access to application details via the unique voting link.
* Ability to cast a vote (approve, reject, or abstain) through a dedicated form.
* Prevention of multiple votes from the same token.
Expand All @@ -29,6 +29,7 @@ This document outlines the core functional requirements for the web application
* The application status is automatically updated as soon as a definitive decision has been reached, even if not all board members have voted.
* Decision logic: A simple majority of cast votes determines the outcome. The voting closes as soon as an irreversible majority is reached. In case of a tie, the application is rejected.
* Email notifications are sent to all board members once a final decision is reached.
* The email contains all relevant information about the application.
* An email notification is sent to the applicant. For rejected applications, this can be disabled via a configuration setting to allow for a personalized response.

## 4. Application Archive
Expand Down
9 changes: 6 additions & 3 deletions ai_docs/task_4_2.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ Integrate the email service to automatically send notifications to all board mem
### Phase 1: Email Template
* [x] Design a clear and informative email template.
* [x] The template should include:
* The project title.
* The applicant's name.
* The estimated costs.
* Applicant's Full Name
* Applicant's Email
* Department
* Project Title
* Project Description
* Estimated Costs
* A direct, unique link to the voting page (e.g., `http://localhost:5173/vote/{token}`).
* [x] Consider using a simple HTML template for better formatting.

Expand Down
25 changes: 20 additions & 5 deletions ai_docs/task_4_3.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,30 @@ Automate the sending of final decision emails to both the applicant and all boar
### Phase 2: Applicant Email Template
* [x] Design an email template for the applicant.
* [x] The template should:
* Clearly state the final decision (approved or rejected).
* Include the project title.
* Thank the applicant for their submission.
* [x] Clearly state the final decision (approved or rejected).
* [x] It should contain:
* [x] Applicant's Full Name
* [x] Applicant's Email
* [x] Department
* [x] Project Title
* [x] Project Description
* [x] Estimated Costs
* [x] A link to the archive of the web app.
* [x] Thank the applicant for their submission.

### Phase 3: Board Member Email Template
* [x] Design an email template for the board members.
* [x] The template should:
* Announce that voting is complete for a specific application.
* State the final outcome.
* [x] Announce that voting is complete for a specific application.
* [x] State the final outcome.
* [x] It should contain:
* [x] Applicant's Full Name
* [x] Applicant's Email
* [x] Department
* [x] Project Title
* [x] Project Description
* [x] Estimated Costs
* [x] A link to the archive of the web app.

### Phase 4: Verification
* [x] Use a local email testing tool to verify that both the applicant and all board members receive the correct notification.
Expand Down
40 changes: 23 additions & 17 deletions src/projectvote/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ async def send_confirmation_email(application: Application, settings: Settings)
"""Send a confirmation email to the applicant."""
await send_email(
recipients=[application.applicant_email], # type: ignore[list-item]
subject=f"Bestätigung Ihres Antrags: {application.project_title}",
subject=f"Bestätigung Deines Antrags: {application.project_title}",
template_body={
"first_name": application.first_name,
"last_name": application.last_name,
Expand Down Expand Up @@ -199,7 +199,10 @@ async def send_voting_links(
template_body={
"first_name": application.first_name,
"last_name": application.last_name,
"applicant_email": application.applicant_email,
"department": application.department,
"project_title": application.project_title,
"project_description": application.project_description,
"costs": application.costs,
"vote_url": vote_url,
"token": vote_record.token,
Expand Down Expand Up @@ -238,35 +241,38 @@ async def send_final_decision_emails(
# Get the German translation, defaulting to the original status if not found
german_status = status_translations.get(status_str, status_str.upper())

if (
# Prepare a common template body for both emails
template_body = {
"first_name": application.first_name,
"last_name": application.last_name,
"applicant_email": application.applicant_email,
"department": application.department,
"project_title": application.project_title,
"project_description": application.project_description,
"costs": application.costs,
"status": german_status,
"frontend_url": settings.frontend_url,
}

# --- Email to Applicant ---
if not (
application.status == ApplicationStatus.REJECTED
and not settings.send_automatic_rejection_email
):
# If automatic rejection emails are disabled, skip sending to applicant
pass # pragma: no cover
else:
await send_email(
recipients=[application.applicant_email], # type: ignore[arg-type]
subject=f"Entscheidung über Ihren Antrag: {application.project_title}",
template_body={
"first_name": application.first_name,
"last_name": application.last_name,
"project_title": application.project_title,
"status": german_status,
},
subject=f"Entscheidung über Deinen Antrag: {application.project_title}",
template_body=template_body,
template_name="final_decision_applicant.html",
settings=settings,
)

# Notification to Board Members
# --- Email to Board Members ---
for member_email in board_members:
await send_email(
recipients=[member_email],
subject=f"Abstimmung abgeschlossen für: {application.project_title}",
template_body={
"project_title": application.project_title,
"status": german_status,
},
template_body=template_body,
template_name="final_decision_board.html",
settings=settings,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
<!DOCTYPE html>
<html>
<body>
<p>Hallo {{ first_name }} {{ last_name }},</p>
<p>Vielen Dank für Ihren Förderantrag. Wir haben ihn erfolgreich erhalten.</p>
<p>Diese E-Mail dient als Bestätigung Ihrer Einreichung. Nachfolgend finden Sie eine Zusammenfassung Ihrer übermittelten Details:</p>
<p>Hallo {{ first_name }},</p>
<p>Vielen Dank für Deinen Förderantrag. Wir haben ihn erfolgreich erhalten.</p>
<p>Diese E-Mail dient als Bestätigung Deiner Einreichung. Nachfolgend findest Du eine Zusammenfassung Deiner übermittelten Details:</p>

<h3>Zusammenfassung des Antrags</h3>
<ul>
<li><strong>Projekttitel:</strong> {{ project_title }}</li>
<li><strong>Name des Antragstellers:</strong> {{ first_name }} {{ last_name }}</li>
<li><strong>E-Mail des Antragstellers:</strong> {{ applicant_email }}</li>
<li><strong>Abteilung/Fachschaft:</strong> {{ department }}</li>
<li><strong>Geschätzte Kosten:</strong> €{{ costs }}</li>
<li><strong>Geschätzte Kosten:</strong> €{{ "%.2f"|format(costs) }}</li>
<li><strong>Projektbeschreibung:</strong><br>{{ project_description }}</li>
</ul>

{% if attachment_filename %}
<p><strong>Anhang:</strong> Ihre Datei '{{ attachment_filename }}' wurde erfolgreich hochgeladen.</p>
<p><strong>Anhang:</strong> Deine Datei '{{ attachment_filename }}' wurde erfolgreich hochgeladen.</p>
{% endif %}

<p>Wir werden Ihren Antrag prüfen und uns nach Abschluss des Abstimmungsprozesses bei Ihnen melden.</p>
<p>Sie können den Status aller Anträge im <a href="{{ frontend_url }}/archive">Antragsarchiv</a> einsehen.</p>
<p>Mit freundlichen Grüßen,</p>
<p>Wir werden Deinen Antrag prüfen und uns nach Abschluss des Abstimmungsprozesses bei Dir melden.</p>
<p>Du kannst den Status aller Anträge im <a href="{{ frontend_url }}/archive">Antragsarchiv</a> einsehen.</p>
<p>Viele Grüße</p>
<p>LvD Förderverein</p>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@
<html>
<body>
<p>Hallo {{ first_name }},</p>
<p>Es wurde eine Entscheidung über Deinen Antrag "{{ project_title }}" getroffen.</p>
<p>Das Projekt wurde {{ status }}.</p>
<p>Viele Grüße.</p>
<p>ProjectVote</p>
<p>vielen Dank für deine Geduld. Es wurde eine Entscheidung über deinen Förderantrag getroffen.</p>
<h3><strong>Der Antrag wurde {{ status }}.</strong></h3>

<h3>Zusammenfassung des Antrags</h3>
<ul>
<li><strong>Projekttitel:</strong> {{ project_title }}</li>
<li><strong>Name des Antragstellers:</strong> {{ first_name }} {{ last_name }}</li>
<li><strong>E-Mail des Antragstellers:</strong> {{ applicant_email }}</li>
<li><strong>Abteilung/Fachschaft:</strong> {{ department }}</li>
<li><strong>Geschätzte Kosten:</strong> €{{ "%.2f"|format(costs) }}</li>
<li><strong>Projektbeschreibung:</strong><br>{{ project_description }}</li>
</ul>

<p>Du kannst den Status aller Anträge jederzeit im <a href="{{ frontend_url }}/archive">Antragsarchiv</a> einsehen.</p>
<p>Wir danken dir für deine Einreichung.</p>
<p>Viele Grüße</p>
<p>LvD Förderverein</p>
</body>
</html>
18 changes: 16 additions & 2 deletions src/projectvote/backend/templates/email/final_decision_board.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@
<html>
<body>
<p>Hallo,</p>
<p>Die Abstimmung für den Antrag "{{ project_title }}" ist abgeschlossen.</p>
<p>Der Antrag wurde {{ status }}.</p>
<p>die Abstimmung für den Antrag "{{ project_title }}" ist abgeschlossen.</p>
<h3><strong>Der Antrag wurde {{ status }}.</strong></h3>

<h3>Zusammenfassung des Antrags</h3>
<ul>
<li><strong>Projekttitel:</strong> {{ project_title }}</li>
<li><strong>Name des Antragstellers:</strong> {{ first_name }} {{ last_name }}</li>
<li><strong>E-Mail des Antragstellers:</strong> {{ applicant_email }}</li>
<li><strong>Abteilung/Fachschaft:</strong> {{ department }}</li>
<li><strong>Geschätzte Kosten:</strong> €{{ "%.2f"|format(costs) }}</li>
<li><strong>Projektbeschreibung:</strong><br>{{ project_description }}</li>
</ul>

<p>Das konkrete Abstimmungsergebnis ist im <a href="{{ frontend_url }}/archive">Antragsarchiv</a> einsehbar.</p>
<p>Viele Grüße</p>
<p>Project-Vote</p>
</body>
</html>
19 changes: 16 additions & 3 deletions src/projectvote/backend/templates/email/new_application.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@
<html>
<body>
<p>Hallo,</p>
<p>Ein neuer Förderantrag wurde von {{ first_name }} {{ last_name }} für das Projekt "{{ project_title }}" eingereicht.</p>
<p>Die geschätzten Kosten belaufen sich auf: {{ costs }}€.</p>
<p>Ein neuer Förderantrag wurde eingereicht und wartet auf Deine Stimme.</p>

<h3>Zusammenfassung des Antrags</h3>
<ul>
<li><strong>Projekttitel:</strong> {{ project_title }}</li>
<li><strong>Name des Antragstellers:</strong> {{ first_name }} {{ last_name }}</li>
<li><strong>E-Mail des Antragstellers:</strong> {{ applicant_email }}</li>
<li><strong>Abteilung/Fachschaft:</strong> {{ department }}</li>
<li><strong>Geschätzte Kosten:</strong> €{{ "%.2f"|format(costs) }}</li>
<li><strong>Projektbeschreibung:</strong><br>{{ project_description }}</li>
</ul>

{% if attachments %}
<p><strong>Anhänge:</strong></p>
<ul>
Expand All @@ -12,6 +22,9 @@
{% endfor %}
</ul>
{% endif %}
<p>Bitte geben Sie hier Ihre Stimme ab: <a href="{{ vote_url }}">Link zur Abstimmung</a></p>

<p>Bitte gib hier Deine Stimme ab: <a href="{{ vote_url }}">Link zur Abstimmung</a></p>
<p>Viele Grüße</p>
<p>Project-Vote</p>
</body>
</html>
47 changes: 30 additions & 17 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ async def test_create_application(
assert applicant_email_call is not None
assert (
applicant_email_call.kwargs["subject"]
== f"Bestätigung Ihres Antrags: {application_data['project_title']}"
== f"Bestätigung Deines Antrags: {application_data['project_title']}"
)
assert (
applicant_email_call.kwargs["template_name"] == "application_confirmation.html"
Expand All @@ -162,6 +162,12 @@ async def test_create_application(
board_template_body = first_board_call_args.kwargs["template_body"]
assert board_template_body["first_name"] == application_data["first_name"]
assert board_template_body["last_name"] == application_data["last_name"]
assert board_template_body["applicant_email"] == application_data["applicant_email"]
assert board_template_body["department"] == application_data["department"]
assert (
board_template_body["project_description"]
== application_data["project_description"]
)
assert board_template_body["project_title"] == application_data["project_title"]
assert board_template_body["costs"] == application_data["costs"]
assert "vote_url" in board_template_body
Expand Down Expand Up @@ -545,26 +551,24 @@ async def test_final_decision_email_content(
)
assert (
applicant_email_call.kwargs["subject"]
== f"Entscheidung über Ihren Antrag: {app_data['project_title']}"
== f"Entscheidung über Deinen Antrag: {app_data['project_title']}"
)
assert (
applicant_email_call.kwargs["template_name"] == "final_decision_applicant.html"
)
applicant_template_body = applicant_email_call.kwargs["template_body"]
assert applicant_template_body["first_name"] == app_data["first_name"]
assert applicant_template_body["last_name"] == app_data["last_name"]
assert applicant_template_body["applicant_email"] == app_data["applicant_email"]
assert applicant_template_body["department"] == app_data["department"]
assert applicant_template_body["project_title"] == app_data["project_title"]
assert (
applicant_email_call.kwargs["template_body"]["first_name"]
== app_data["first_name"]
)
assert (
applicant_email_call.kwargs["template_body"]["last_name"]
== app_data["last_name"]
)
assert (
applicant_email_call.kwargs["template_body"]["project_title"]
== app_data["project_title"]
)
assert (
applicant_email_call.kwargs["template_body"]["status"] == expected_german_status
applicant_template_body["project_description"]
== app_data["project_description"]
)
assert applicant_template_body["costs"] == app_data["costs"]
assert applicant_template_body["status"] == expected_german_status
assert "frontend_url" in applicant_template_body

# Check board member emails
board_member_email_calls = [
Expand All @@ -579,10 +583,19 @@ async def test_final_decision_email_content(
== f"Abstimmung abgeschlossen für: {app_data['project_title']}"
)
assert call.kwargs["template_name"] == "final_decision_board.html"
board_template_body = call.kwargs["template_body"]
assert board_template_body["first_name"] == app_data["first_name"]
assert board_template_body["last_name"] == app_data["last_name"]
assert board_template_body["applicant_email"] == app_data["applicant_email"]
assert board_template_body["department"] == app_data["department"]
assert board_template_body["project_title"] == app_data["project_title"]
assert (
call.kwargs["template_body"]["project_title"] == app_data["project_title"]
board_template_body["project_description"]
== app_data["project_description"]
)
assert call.kwargs["template_body"]["status"] == expected_german_status
assert board_template_body["costs"] == app_data["costs"]
assert board_template_body["status"] == expected_german_status
assert "frontend_url" in board_template_body


@pytest.mark.asyncio
Expand Down