Email notification
This commit is contained in:
@@ -8,6 +8,8 @@ from app.crud.crud_journal import journal as crud_journal
|
||||
from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate, Journal
|
||||
from app.models.ppr import User
|
||||
from app.core.utils import get_client_ip
|
||||
from app.core.email import email_service
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -78,6 +80,23 @@ async def create_public_ppr(
|
||||
}
|
||||
})
|
||||
|
||||
# Send email if email provided
|
||||
if ppr_in.email:
|
||||
await email_service.send_email(
|
||||
to_email=ppr_in.email,
|
||||
subject="PPR Submitted Successfully",
|
||||
template_name="ppr_submitted.html",
|
||||
template_vars={
|
||||
"name": ppr_in.captain,
|
||||
"aircraft": ppr_in.ac_reg,
|
||||
"arrival_time": ppr_in.eta.strftime("%Y-%m-%d %H:%M"),
|
||||
"departure_time": ppr_in.etd.strftime("%Y-%m-%d %H:%M") if ppr_in.etd else "N/A",
|
||||
"purpose": ppr_in.notes or "N/A",
|
||||
"public_token": ppr.public_token,
|
||||
"base_url": settings.base_url
|
||||
}
|
||||
)
|
||||
|
||||
return ppr
|
||||
|
||||
|
||||
@@ -199,6 +218,20 @@ async def update_ppr_status(
|
||||
}
|
||||
})
|
||||
|
||||
# Send email if cancelled and email provided
|
||||
if status_update.status == PPRStatus.CANCELED and ppr.email:
|
||||
await email_service.send_email(
|
||||
to_email=ppr.email,
|
||||
subject="PPR Cancelled",
|
||||
template_name="ppr_cancelled.html",
|
||||
template_vars={
|
||||
"name": ppr.captain,
|
||||
"aircraft": ppr.ac_reg,
|
||||
"arrival_time": ppr.eta.strftime("%Y-%m-%d %H:%M"),
|
||||
"departure_time": ppr.etd.strftime("%Y-%m-%d %H:%M") if ppr.etd else "N/A"
|
||||
}
|
||||
)
|
||||
|
||||
return ppr
|
||||
|
||||
|
||||
@@ -231,6 +264,100 @@ async def delete_ppr(
|
||||
return ppr
|
||||
|
||||
|
||||
@router.get("/public/edit/{token}", response_model=PPR)
|
||||
async def get_ppr_for_edit(
|
||||
token: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get PPR details for public editing using token"""
|
||||
ppr = crud_ppr.get_by_public_token(db, token)
|
||||
if not ppr:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Invalid or expired token"
|
||||
)
|
||||
# Only allow editing if not already processed
|
||||
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="PPR cannot be edited at this stage"
|
||||
)
|
||||
return ppr
|
||||
|
||||
|
||||
@router.patch("/public/edit/{token}", response_model=PPR)
|
||||
async def update_ppr_public(
|
||||
token: str,
|
||||
ppr_in: PPRUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update PPR publicly using token"""
|
||||
ppr = crud_ppr.get_by_public_token(db, token)
|
||||
if not ppr:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Invalid or expired token"
|
||||
)
|
||||
# Only allow editing if not already processed
|
||||
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="PPR cannot be edited at this stage"
|
||||
)
|
||||
|
||||
client_ip = get_client_ip(request)
|
||||
updated_ppr = crud_ppr.update(db, db_obj=ppr, obj_in=ppr_in, user="public", user_ip=client_ip)
|
||||
return updated_ppr
|
||||
|
||||
|
||||
@router.delete("/public/cancel/{token}", response_model=PPR)
|
||||
async def cancel_ppr_public(
|
||||
token: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Cancel PPR publicly using token"""
|
||||
ppr = crud_ppr.get_by_public_token(db, token)
|
||||
if not ppr:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Invalid or expired token"
|
||||
)
|
||||
# Only allow canceling if not already processed
|
||||
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="PPR cannot be cancelled at this stage"
|
||||
)
|
||||
|
||||
client_ip = get_client_ip(request)
|
||||
# Cancel by setting status to CANCELED
|
||||
cancelled_ppr = crud_ppr.update_status(
|
||||
db,
|
||||
ppr_id=ppr.id,
|
||||
status=PPRStatus.CANCELED,
|
||||
user="public",
|
||||
user_ip=client_ip
|
||||
)
|
||||
|
||||
# Send cancellation email if email provided
|
||||
if cancelled_ppr.email:
|
||||
await email_service.send_email(
|
||||
to_email=cancelled_ppr.email,
|
||||
subject="PPR Cancelled",
|
||||
template_name="ppr_cancelled.html",
|
||||
template_vars={
|
||||
"name": cancelled_ppr.captain,
|
||||
"aircraft": cancelled_ppr.ac_reg,
|
||||
"arrival_time": cancelled_ppr.eta.strftime("%Y-%m-%d %H:%M"),
|
||||
"departure_time": cancelled_ppr.etd.strftime("%Y-%m-%d %H:%M") if cancelled_ppr.etd else "N/A"
|
||||
}
|
||||
)
|
||||
|
||||
return cancelled_ppr
|
||||
|
||||
|
||||
@router.get("/{ppr_id}/journal", response_model=List[Journal])
|
||||
async def get_ppr_journal(
|
||||
ppr_id: int,
|
||||
|
||||
48
backend/app/core/email.py
Normal file
48
backend/app/core/email.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import aiosmtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
import os
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class EmailService:
|
||||
def __init__(self):
|
||||
self.smtp_host = settings.mail_host
|
||||
self.smtp_port = settings.mail_port
|
||||
self.smtp_user = settings.mail_username
|
||||
self.smtp_password = settings.mail_password
|
||||
self.from_email = settings.mail_from
|
||||
self.from_name = settings.mail_from_name
|
||||
|
||||
# Set up Jinja2 environment for templates
|
||||
template_dir = os.path.join(os.path.dirname(__file__), '..', 'templates')
|
||||
self.jinja_env = Environment(loader=FileSystemLoader(template_dir))
|
||||
|
||||
async def send_email(self, to_email: str, subject: str, template_name: str, template_vars: dict):
|
||||
# Render the template
|
||||
template = self.jinja_env.get_template(template_name)
|
||||
html_content = template.render(**template_vars)
|
||||
|
||||
# Create message
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = f"{self.from_name} <{self.from_email}>"
|
||||
msg['To'] = to_email
|
||||
|
||||
# Attach HTML content
|
||||
html_part = MIMEText(html_content, 'html')
|
||||
msg.attach(html_part)
|
||||
|
||||
# Send email
|
||||
try:
|
||||
async with aiosmtplib.SMTP(hostname=self.smtp_host, port=self.smtp_port, use_tls=True) as smtp:
|
||||
await smtp.login(self.smtp_user, self.smtp_password)
|
||||
await smtp.send_message(msg)
|
||||
except Exception as e:
|
||||
# Log error, but for now, print
|
||||
print(f"Failed to send email: {e}")
|
||||
# In production, use logging
|
||||
|
||||
|
||||
email_service = EmailService()
|
||||
@@ -2,6 +2,7 @@ from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func, desc
|
||||
from datetime import date, datetime
|
||||
import secrets
|
||||
from app.models.ppr import PPRRecord, PPRStatus
|
||||
from app.schemas.ppr import PPRCreate, PPRUpdate
|
||||
from app.crud.crud_journal import journal as crud_journal
|
||||
@@ -11,6 +12,9 @@ class CRUDPPR:
|
||||
def get(self, db: Session, ppr_id: int) -> Optional[PPRRecord]:
|
||||
return db.query(PPRRecord).filter(PPRRecord.id == ppr_id).first()
|
||||
|
||||
def get_by_public_token(self, db: Session, token: str) -> Optional[PPRRecord]:
|
||||
return db.query(PPRRecord).filter(PPRRecord.public_token == token).first()
|
||||
|
||||
def get_multi(
|
||||
self,
|
||||
db: Session,
|
||||
@@ -67,7 +71,8 @@ class CRUDPPR:
|
||||
db_obj = PPRRecord(
|
||||
**obj_in.dict(),
|
||||
created_by=created_by,
|
||||
status=PPRStatus.NEW
|
||||
status=PPRStatus.NEW,
|
||||
public_token=secrets.token_urlsafe(64)
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
|
||||
@@ -42,6 +42,7 @@ class PPRRecord(Base):
|
||||
departed_dt = Column(DateTime, nullable=True)
|
||||
created_by = Column(String(16), nullable=True)
|
||||
submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
public_token = Column(String(128), nullable=True, unique=True)
|
||||
|
||||
|
||||
class User(Base):
|
||||
|
||||
19
backend/app/templates/ppr_cancelled.html
Normal file
19
backend/app/templates/ppr_cancelled.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>PPR Cancelled</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PPR Cancelled</h1>
|
||||
<p>Dear {{ name }},</p>
|
||||
<p>Your Prior Permission Request (PPR) has been cancelled.</p>
|
||||
<p><strong>PPR Details:</strong></p>
|
||||
<ul>
|
||||
<li>Aircraft: {{ aircraft }}</li>
|
||||
<li>Original Arrival: {{ arrival_time }}</li>
|
||||
<li>Original Departure: {{ departure_time }}</li>
|
||||
</ul>
|
||||
<p>If this was not intended, please contact us.</p>
|
||||
<p>Best regards,<br>Swansea Airport Team</p>
|
||||
</body>
|
||||
</html>
|
||||
21
backend/app/templates/ppr_submitted.html
Normal file
21
backend/app/templates/ppr_submitted.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>PPR Submitted</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PPR Submitted Successfully</h1>
|
||||
<p>Dear {{ name }},</p>
|
||||
<p>Your Prior Permission Request (PPR) has been submitted.</p>
|
||||
<p><strong>PPR Details:</strong></p>
|
||||
<ul>
|
||||
<li>Aircraft: {{ aircraft }}</li>
|
||||
<li>Arrival: {{ arrival_time }}</li>
|
||||
<li>Departure: {{ departure_time }}</li>
|
||||
<li>Purpose: {{ purpose }}</li>
|
||||
</ul>
|
||||
<p>You can <a href="{{ base_url }}/edit?token={{ public_token }}">edit or cancel</a> your PPR using this secure link.</p>
|
||||
<p>You will receive further updates via email.</p>
|
||||
<p>Best regards,<br>Swansea Airport Team</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -14,4 +14,6 @@ pydantic-settings==2.0.3
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
httpx==0.25.2
|
||||
redis==5.0.1
|
||||
redis==5.0.1
|
||||
aiosmtplib==3.0.1
|
||||
jinja2==3.1.2
|
||||
Reference in New Issue
Block a user