add mail to table on notif

This commit is contained in:
2025-06-02 09:17:21 +02:00
parent 17e0781b14
commit 7d08a57c85
8 changed files with 233 additions and 5 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,41 @@
"""add_mails_table
Revision ID: 20519a2437c2
Revises: 444d76da74ba
Create Date: 2025-06-02 09:04:39.972021
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '20519a2437c2'
down_revision = '444d76da74ba'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('mails',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('recipient', sa.String(length=150), nullable=False),
sa.Column('subject', sa.String(length=200), nullable=False),
sa.Column('body', sa.Text(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('sent_at', sa.DateTime(), nullable=True),
sa.Column('template_id', sa.Integer(), nullable=True),
sa.Column('notif_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['notif_id'], ['notifs.id'], ),
sa.ForeignKeyConstraint(['template_id'], ['email_templates.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('mails')
# ### end Alembic commands ###

View File

@@ -313,3 +313,22 @@ class EmailTemplate(db.Model):
def __repr__(self):
return f'<EmailTemplate {self.name}>'
class Mail(db.Model):
__tablename__ = 'mails'
id = db.Column(db.Integer, primary_key=True)
recipient = db.Column(db.String(150), nullable=False)
subject = db.Column(db.String(200), nullable=False)
body = db.Column(db.Text, nullable=False)
status = db.Column(db.String(20), default='pending', nullable=False) # e.g., pending, sent, failed
created_at = db.Column(db.DateTime, default=datetime.utcnow)
sent_at = db.Column(db.DateTime, nullable=True)
template_id = db.Column(db.Integer, db.ForeignKey('email_templates.id'), nullable=True)
notif_id = db.Column(db.Integer, db.ForeignKey('notifs.id'), nullable=True)
# Relationships
template = db.relationship('EmailTemplate', backref='mails')
notif = db.relationship('Notif', backref='mails')
def __repr__(self):
return f'<Mail to {self.recipient} status={self.status}>'

View File

@@ -11,13 +11,27 @@
{% for template in templates %}
<option value="{{ template.id }}"
data-subject="{{ template.subject }}"
data-body="{{ template.body }}">
data-body="{{ template.body }}"
data-type="{{ template.name }}">
{{ template.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Available Variables -->
<div class="card mb-4" id="availableVariables" style="display: none;">
<div class="card-header bg-light">
<h6 class="mb-0">Available Variables</h6>
</div>
<div class="card-body">
<p class="text-muted mb-2">Use these variables in your template by wrapping them in double curly braces, e.g., <code>{{ '{{ user.username }}' }}</code></p>
<div id="variableList" class="mb-0">
<!-- Variables will be populated here -->
</div>
</div>
</div>
<!-- Template Editor -->
<div class="card">
<div class="card-header bg-light">
@@ -52,6 +66,83 @@
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.js"></script>
<script>
// Template variables mapping
const templateVariables = {
'Account Created': {
'user.username': 'The username of the created account',
'user.email': 'The email address of the created account'
},
'Password Reset': {
'user.username': 'The username of the account',
'reset_link': 'The password reset link'
},
'Account Deleted': {
'user.username': 'The username of the deleted account',
'deleted_at': 'The date and time of deletion'
},
'Account Updated': {
'user.username': 'The username of the updated account',
'updated_fields': 'The fields that were updated'
},
'Room Invite': {
'user.username': 'The username of the invited user',
'room.name': 'The name of the room',
'inviter.username': 'The username of the user who sent the invite'
},
'Room Invite Removed': {
'user.username': 'The username of the user',
'room.name': 'The name of the room',
'remover.username': 'The username of the user who removed the invite'
},
'Conversation Invite': {
'user.username': 'The username of the invited user',
'conversation.name': 'The name of the conversation',
'inviter.username': 'The username of the user who sent the invite'
},
'Conversation Invite Removed': {
'user.username': 'The username of the user',
'conversation.name': 'The name of the conversation',
'remover.username': 'The username of the user who removed the invite'
},
'Conversation Message': {
'user.username': 'The username of the recipient',
'sender.username': 'The username of the message sender',
'message.content': 'The content of the message',
'conversation.name': 'The name of the conversation',
'message_link': 'The link to view the message'
}
};
// Function to update available variables display
function updateAvailableVariables(templateType) {
const variables = templateVariables[templateType] || {};
const variableList = document.getElementById('variableList');
const availableVariables = document.getElementById('availableVariables');
if (Object.keys(variables).length === 0) {
availableVariables.style.display = 'none';
return;
}
let html = '<div class="row">';
for (const [variable, description] of Object.entries(variables)) {
html += `
<div class="col-md-6 mb-2">
<div class="card h-100">
<div class="card-body p-2">
<code class="text-primary">{{ '{{ ' }}${variable}{{ ' }}' }}</code>
<p class="text-muted small mb-0">${description}</p>
</div>
</div>
</div>
`;
}
html += '</div>';
variableList.innerHTML = html;
availableVariables.style.display = 'block';
}
// Wait for document and jQuery to be ready
document.addEventListener('DOMContentLoaded', function() {
// Initialize Summernote
@@ -86,11 +177,21 @@ document.addEventListener('DOMContentLoaded', function() {
const selectedOption = this.options[this.selectedIndex];
const subject = selectedOption.dataset.subject || '';
const body = selectedOption.dataset.body || '';
const templateType = selectedOption.dataset.type || '';
$('#templateSubject').val(subject);
$('#templateBody').summernote('code', body);
updateAvailableVariables(templateType);
});
// Check for initially selected template
const templateSelect = document.getElementById('templateSelect');
if (templateSelect.value) {
const selectedOption = templateSelect.options[templateSelect.selectedIndex];
const templateType = selectedOption.dataset.type || '';
updateAvailableVariables(templateType);
}
// Handle template save
$('#saveTemplate').on('click', function() {
const templateId = $('#templateSelect').val();
@@ -187,5 +288,20 @@ document.addEventListener('DOMContentLoaded', function() {
.note-placeholder {
color: #6c757d;
}
/* Variable card styles */
#variableList .card {
border: 1px solid #dee2e6;
transition: all 0.2s ease-in-out;
}
#variableList .card:hover {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.1);
}
#variableList code {
font-size: 0.9em;
padding: 0.2em 0.4em;
background-color: #f8f9fa;
border-radius: 0.25rem;
}
</style>
{% endmacro %}

View File

@@ -1,5 +1,5 @@
from flask import request
from models import Notif, NotifType, db
from models import Notif, NotifType, db, EmailTemplate, Mail
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from flask_login import current_user
@@ -8,20 +8,65 @@ import logging
logger = logging.getLogger(__name__)
def generate_mail_from_notification(notif: Notif) -> Optional[Mail]:
"""
Generate a mail record from a notification using the appropriate email template.
Args:
notif: The notification object to generate mail from
Returns:
The created Mail object or None if no template was found
"""
logger.debug(f"Generating mail for notification: {notif}")
# Find the corresponding email template based on notif_type
template = EmailTemplate.query.filter_by(name=notif.notif_type).first()
if not template:
logger.warning(f"No email template found for notification type: {notif.notif_type}")
return None
try:
# Fill in the template with notification details
filled_body = template.body
if notif.user:
filled_body = filled_body.replace('{{ user.username }}', notif.user.username)
if notif.details:
for key, value in notif.details.items():
filled_body = filled_body.replace(f'{{{{ {key} }}}}', str(value))
# Create a new Mail record
mail = Mail(
recipient=notif.user.email,
subject=template.subject,
body=filled_body,
status='pending',
template_id=template.id,
notif_id=notif.id
)
db.session.add(mail)
logger.debug(f"Created mail record: {mail}")
return mail
except Exception as e:
logger.error(f"Error generating mail from notification: {str(e)}")
return None
def create_notification(
notif_type: str,
user_id: int,
sender_id: Optional[int] = None,
details: Optional[Dict[str, Any]] = None
details: Optional[Dict[str, Any]] = None,
generate_mail: bool = True
) -> Notif:
"""
Create a notification in the database.
Create a notification in the database and optionally generate a mail record.
Args:
notif_type: The type of notification (must match NotifType enum)
user_id: The ID of the user to notify
sender_id: Optional ID of the user who triggered the notification
details: Optional dictionary containing notification details
generate_mail: Whether to generate a mail record for this notification
Returns:
The created Notif object
@@ -41,6 +86,13 @@ def create_notification(
logger.debug(f"Created notification object: {notif}")
db.session.add(notif)
# Generate mail if requested
if generate_mail:
mail = generate_mail_from_notification(notif)
if mail:
logger.debug(f"Generated mail record for notification: {mail}")
# Don't commit here - let the caller handle the transaction
logger.debug("Notification object added to session")
return notif