add mail to table on notif
This commit is contained in:
Binary file not shown.
Binary file not shown.
41
migrations/versions/20519a2437c2_add_mails_table.py
Normal file
41
migrations/versions/20519a2437c2_add_mails_table.py
Normal 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 ###
|
||||||
Binary file not shown.
19
models.py
19
models.py
@@ -313,3 +313,22 @@ class EmailTemplate(db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<EmailTemplate {self.name}>'
|
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}>'
|
||||||
@@ -11,13 +11,27 @@
|
|||||||
{% for template in templates %}
|
{% for template in templates %}
|
||||||
<option value="{{ template.id }}"
|
<option value="{{ template.id }}"
|
||||||
data-subject="{{ template.subject }}"
|
data-subject="{{ template.subject }}"
|
||||||
data-body="{{ template.body }}">
|
data-body="{{ template.body }}"
|
||||||
|
data-type="{{ template.name }}">
|
||||||
{{ template.name }}
|
{{ template.name }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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 -->
|
<!-- Template Editor -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-light">
|
<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 src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.js"></script>
|
||||||
|
|
||||||
<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
|
// Wait for document and jQuery to be ready
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize Summernote
|
// Initialize Summernote
|
||||||
@@ -86,11 +177,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const selectedOption = this.options[this.selectedIndex];
|
const selectedOption = this.options[this.selectedIndex];
|
||||||
const subject = selectedOption.dataset.subject || '';
|
const subject = selectedOption.dataset.subject || '';
|
||||||
const body = selectedOption.dataset.body || '';
|
const body = selectedOption.dataset.body || '';
|
||||||
|
const templateType = selectedOption.dataset.type || '';
|
||||||
|
|
||||||
$('#templateSubject').val(subject);
|
$('#templateSubject').val(subject);
|
||||||
$('#templateBody').summernote('code', body);
|
$('#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
|
// Handle template save
|
||||||
$('#saveTemplate').on('click', function() {
|
$('#saveTemplate').on('click', function() {
|
||||||
const templateId = $('#templateSelect').val();
|
const templateId = $('#templateSelect').val();
|
||||||
@@ -187,5 +288,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.note-placeholder {
|
.note-placeholder {
|
||||||
color: #6c757d;
|
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>
|
</style>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
from flask import request
|
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 typing import Optional, Dict, Any, List
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
@@ -8,20 +8,65 @@ import logging
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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(
|
def create_notification(
|
||||||
notif_type: str,
|
notif_type: str,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
sender_id: Optional[int] = None,
|
sender_id: Optional[int] = None,
|
||||||
details: Optional[Dict[str, Any]] = None
|
details: Optional[Dict[str, Any]] = None,
|
||||||
|
generate_mail: bool = True
|
||||||
) -> Notif:
|
) -> Notif:
|
||||||
"""
|
"""
|
||||||
Create a notification in the database.
|
Create a notification in the database and optionally generate a mail record.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
notif_type: The type of notification (must match NotifType enum)
|
notif_type: The type of notification (must match NotifType enum)
|
||||||
user_id: The ID of the user to notify
|
user_id: The ID of the user to notify
|
||||||
sender_id: Optional ID of the user who triggered the notification
|
sender_id: Optional ID of the user who triggered the notification
|
||||||
details: Optional dictionary containing notification details
|
details: Optional dictionary containing notification details
|
||||||
|
generate_mail: Whether to generate a mail record for this notification
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The created Notif object
|
The created Notif object
|
||||||
@@ -41,6 +86,13 @@ def create_notification(
|
|||||||
|
|
||||||
logger.debug(f"Created notification object: {notif}")
|
logger.debug(f"Created notification object: {notif}")
|
||||||
db.session.add(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
|
# Don't commit here - let the caller handle the transaction
|
||||||
logger.debug("Notification object added to session")
|
logger.debug("Notification object added to session")
|
||||||
return notif
|
return notif
|
||||||
|
|||||||
Reference in New Issue
Block a user