added events to event system

This commit is contained in:
2025-05-31 19:07:29 +02:00
parent 224d4d400e
commit 2c9b302a69
12 changed files with 309 additions and 112 deletions

View File

@@ -3,7 +3,7 @@ from flask_login import login_user, logout_user, login_required, current_user
from models import db, User from models import db, User
from functools import wraps from functools import wraps
from datetime import datetime from datetime import datetime
from utils import log_event from utils import log_event, create_notification
def require_password_change(f): def require_password_change(f):
@wraps(f) @wraps(f)
@@ -94,6 +94,18 @@ def init_routes(auth_bp):
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
# Create notification for the new user
create_notification(
notif_type='account_created',
user_id=new_user.id,
details={
'message': 'Welcome to DocuPulse! Your account has been created successfully.',
'username': new_user.username,
'email': new_user.email,
'timestamp': datetime.utcnow().isoformat()
}
)
# Log successful registration # Log successful registration
log_event( log_event(
event_type='user_create', event_type='user_create',
@@ -169,6 +181,16 @@ def init_routes(auth_bp):
current_user.set_password(new_password) current_user.set_password(new_password)
# Create password change notification
create_notification(
notif_type='password_changed',
user_id=current_user.id,
details={
'message': 'Your password has been changed successfully.',
'timestamp': datetime.utcnow().isoformat()
}
)
# Log successful password change # Log successful password change
log_event( log_event(
event_type='user_update', event_type='user_update',

View File

@@ -5,10 +5,11 @@ from forms import UserForm
from flask import abort from flask import abort
from sqlalchemy import or_ from sqlalchemy import or_
from routes.auth import require_password_change from routes.auth import require_password_change
from utils import log_event from utils import log_event, create_notification
import json import json
import os import os
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from datetime import datetime
contacts_bp = Blueprint('contacts', __name__, url_prefix='/contacts') contacts_bp = Blueprint('contacts', __name__, url_prefix='/contacts')
@@ -122,6 +123,20 @@ def new_contact():
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
# Create notification for the new user
create_notification(
notif_type='account_created',
user_id=user.id,
sender_id=current_user.id, # Admin who created the account
details={
'message': 'Your DocuPulse account has been created by an administrator.',
'username': user.username,
'email': user.email,
'created_by': f"{current_user.username} {current_user.last_name}",
'timestamp': datetime.utcnow().isoformat()
}
)
# Log user creation event # Log user creation event
log_event( log_event(
event_type='user_create', event_type='user_create',
@@ -304,6 +319,26 @@ def edit_contact(id):
db.session.commit() db.session.commit()
# Create notification for the user being updated
create_notification(
notif_type='account_updated',
user_id=user.id,
sender_id=current_user.id,
details={
'message': 'Your account has been updated by an administrator.',
'updated_by': f"{current_user.username} {current_user.last_name}",
'changes': {
'name': f"{user.username} {user.last_name}",
'email': user.email,
'phone': user.phone,
'company': user.company,
'position': user.position,
'password_changed': password_changed
},
'timestamp': datetime.utcnow().isoformat()
}
)
# Log user update event # Log user update event
log_event( log_event(
event_type='user_update', event_type='user_update',
@@ -342,6 +377,18 @@ def delete_contact(id):
flash('You cannot delete your own account.', 'error') flash('You cannot delete your own account.', 'error')
return redirect(url_for('contacts.contacts_list')) return redirect(url_for('contacts.contacts_list'))
# Create notification for the user being deleted
create_notification(
notif_type='account_deleted',
user_id=user.id,
sender_id=current_user.id,
details={
'message': 'Your account has been deleted by an administrator.',
'deleted_by': f"{current_user.username} {current_user.last_name}",
'timestamp': datetime.utcnow().isoformat()
}
)
# Log user deletion event # Log user deletion event
log_event( log_event(
event_type='user_delete', event_type='user_delete',

View File

@@ -3,7 +3,7 @@ from flask_login import login_required, current_user
from models import db, Conversation, User, Message, MessageAttachment from models import db, Conversation, User, Message, MessageAttachment
from forms import ConversationForm from forms import ConversationForm
from routes.auth import require_password_change from routes.auth import require_password_change
from utils import log_event from utils import log_event, create_notification
import os import os
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from datetime import datetime from datetime import datetime
@@ -177,6 +177,20 @@ def add_member(conversation_id):
conversation.members.append(user) conversation.members.append(user)
db.session.commit() db.session.commit()
# Create notification for the invited user
create_notification(
notif_type='conversation_invite',
user_id=user.id,
sender_id=current_user.id,
details={
'message': f'You have been invited to join conversation "{conversation.name}"',
'conversation_id': conversation.id,
'conversation_name': conversation.name,
'invited_by': f"{current_user.username} {current_user.last_name}",
'timestamp': datetime.utcnow().isoformat()
}
)
# Log member addition # Log member addition
log_event( log_event(
event_type='conversation_member_add', event_type='conversation_member_add',
@@ -186,7 +200,7 @@ def add_member(conversation_id):
'added_by': current_user.id, 'added_by': current_user.id,
'added_by_name': f"{current_user.username} {current_user.last_name}", 'added_by_name': f"{current_user.username} {current_user.last_name}",
'added_user_id': user.id, 'added_user_id': user.id,
'added_by_name': f"{current_user.username} {current_user.last_name}", 'added_user_name': f"{user.username} {user.last_name}",
'added_user_email': user.email 'added_user_email': user.email
} }
) )
@@ -215,6 +229,20 @@ def remove_member(conversation_id, user_id):
conversation.members.remove(user) conversation.members.remove(user)
db.session.commit() db.session.commit()
# Create notification for the removed user
create_notification(
notif_type='conversation_invite_removed',
user_id=user.id,
sender_id=current_user.id,
details={
'message': f'You have been removed from conversation "{conversation.name}"',
'conversation_id': conversation.id,
'conversation_name': conversation.name,
'removed_by': f"{current_user.username} {current_user.last_name}",
'timestamp': datetime.utcnow().isoformat()
}
)
# Log member removal # Log member removal
log_event( log_event(
event_type='conversation_member_remove', event_type='conversation_member_remove',
@@ -437,6 +465,25 @@ def send_message(conversation_id):
db.session.commit() db.session.commit()
# Create notifications for all conversation members except the sender
for member in conversation.members:
if member.id != current_user.id:
create_notification(
notif_type='conversation_message',
user_id=member.id,
sender_id=current_user.id,
details={
'message': f'New message in conversation "{conversation.name}"',
'conversation_id': conversation.id,
'conversation_name': conversation.name,
'sender': f"{current_user.username} {current_user.last_name}",
'message_preview': message_content[:100] + ('...' if len(message_content) > 100 else ''),
'has_attachments': len(attachments) > 0,
'attachment_count': len(attachments),
'timestamp': datetime.utcnow().isoformat()
}
)
# Log message creation # Log message creation
log_event( log_event(
event_type='message_create', event_type='message_create',

View File

@@ -10,7 +10,7 @@ import logging
import sys import sys
import time import time
from forms import CompanySettingsForm from forms import CompanySettingsForm
from utils import log_event from utils import log_event, create_notification
# Set up logging to show in console # Set up logging to show in console
logging.basicConfig( logging.basicConfig(
@@ -55,7 +55,7 @@ def init_routes(main_bp):
Event.user_id == current_user.id, # User's own actions Event.user_id == current_user.id, # User's own actions
db.and_( db.and_(
Event.event_type.in_(['conversation_create', 'message_create']), # Conversation-related events Event.event_type.in_(['conversation_create', 'message_create']), # Conversation-related events
Event.details['conversation_id'].astext.in_( Event.details['conversation_id'].cast(db.Integer).in_(
db.session.query(Conversation.id) db.session.query(Conversation.id)
.join(Conversation.members) .join(Conversation.members)
.filter(User.id == current_user.id) .filter(User.id == current_user.id)
@@ -370,6 +370,17 @@ def init_routes(main_bp):
flash('Passwords do not match.', 'error') flash('Passwords do not match.', 'error')
return render_template('profile/profile.html') return render_template('profile/profile.html')
current_user.set_password(new_password) current_user.set_password(new_password)
# Create password change notification
create_notification(
notif_type='password_changed',
user_id=current_user.id,
details={
'message': 'Your password has been changed successfully.',
'timestamp': datetime.utcnow().isoformat()
}
)
flash('Password updated successfully.', 'success') flash('Password updated successfully.', 'success')
elif confirm_password: elif confirm_password:
flash('Please enter a new password.', 'error') flash('Please enter a new password.', 'error')
@@ -522,7 +533,8 @@ def init_routes(main_bp):
'details': notif.details, 'details': notif.details,
'sender': { 'sender': {
'id': notif.sender.id, 'id': notif.sender.id,
'username': notif.sender.username 'username': notif.sender.username,
'last_name': notif.sender.last_name
} if notif.sender else None } if notif.sender else None
} for notif in notifications.items], } for notif in notifications.items],
'total_pages': total_pages, 'total_pages': total_pages,

View File

@@ -1,7 +1,8 @@
from flask import Blueprint, jsonify, request, abort from flask import Blueprint, jsonify, request, abort
from flask_login import login_required, current_user from flask_login import login_required, current_user
from models import db, Room, User, RoomMemberPermission from models import db, Room, User, RoomMemberPermission
from utils import user_has_permission, log_event from utils import user_has_permission, log_event, create_notification
from datetime import datetime
room_members_bp = Blueprint('room_members', __name__) room_members_bp = Blueprint('room_members', __name__)
@@ -70,6 +71,21 @@ def add_room_member(room_id):
db.session.commit() db.session.commit()
# Create notification for the invited user
create_notification(
notif_type='room_invite',
user_id=user_id,
sender_id=current_user.id,
details={
'message': f'You have been invited to join room "{room.name}"',
'room_id': room_id,
'room_name': room.name,
'invited_by': f"{current_user.username} {current_user.last_name}",
'permissions': permissions,
'timestamp': datetime.utcnow().isoformat()
}
)
log_event( log_event(
event_type='room_member_add', event_type='room_member_add',
details={ details={
@@ -77,8 +93,7 @@ def add_room_member(room_id):
'room_name': room.name, 'room_name': room.name,
'added_user_id': user_id, 'added_user_id': user_id,
'added_user_name': f"{user.username} {user.last_name}", 'added_user_name': f"{user.username} {user.last_name}",
'added_by': f"{current_user.username} {current_user.last_name}", 'added_by': f"{current_user.username} {current_user.last_name}"
'permissions': permissions
}, },
user_id=current_user.id user_id=current_user.id
) )
@@ -105,6 +120,20 @@ def remove_room_member(room_id, user_id):
db.session.commit() db.session.commit()
# Create notification for the removed user
create_notification(
notif_type='room_invite_removed',
user_id=user_id,
sender_id=current_user.id,
details={
'message': f'You have been removed from room "{room.name}"',
'room_id': room_id,
'room_name': room.name,
'removed_by': f"{current_user.username} {current_user.last_name}",
'timestamp': datetime.utcnow().isoformat()
}
)
log_event( log_event(
event_type='room_member_remove', event_type='room_member_remove',
details={ details={

View File

@@ -21,6 +21,9 @@ document.addEventListener('DOMContentLoaded', function() {
const notifDetailsModal = document.getElementById('notifDetailsModal'); const notifDetailsModal = document.getElementById('notifDetailsModal');
const notifDetailsContent = document.getElementById('notifDetailsContent'); const notifDetailsContent = document.getElementById('notifDetailsContent');
// Get CSRF token from meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
// Function to update URL with current filters // Function to update URL with current filters
function updateURL() { function updateURL() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
@@ -31,7 +34,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Function to fetch notifications // Function to fetch notifications
function fetchNotifications() { async function fetchNotifications() {
if (isFetching) return; if (isFetching) return;
isFetching = true; isFetching = true;
@@ -45,54 +48,26 @@ document.addEventListener('DOMContentLoaded', function() {
ajax: 'true' ajax: 'true'
}); });
fetch(`${window.location.pathname}?${params.toString()}`, { try {
headers: { const response = await fetch(`${window.location.pathname}?${params.toString()}`, {
'X-Requested-With': 'XMLHttpRequest' headers: {
} 'X-Requested-With': 'XMLHttpRequest'
}) }
.then(response => { });
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok'); throw new Error('Network response was not ok');
} }
return response.text();
})
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newTableBody = doc.getElementById('notifsTableBody');
if (newTableBody) { const data = await response.json();
notifsTableBody.innerHTML = newTableBody.innerHTML; updateNotificationsTable(data.notifications);
updatePagination(data.total_pages, data.current_page);
// Update pagination } catch (error) {
const newCurrentPage = parseInt(doc.getElementById('currentPage').textContent);
const newTotalPages = parseInt(doc.getElementById('totalPages').textContent);
currentPage = newCurrentPage;
totalPages = newTotalPages;
currentPageSpan.textContent = currentPage;
totalPagesSpan.textContent = totalPages;
// Update pagination buttons
prevPageBtn.disabled = currentPage === 1;
nextPageBtn.disabled = currentPage === totalPages;
// Update URL
updateURL();
// Reattach event listeners
attachEventListeners();
} else {
console.error('Could not find notifications table in response');
notifsTableBody.innerHTML = '<tr><td colspan="6" class="text-center">Error loading notifications</td></tr>';
}
})
.catch(error => {
console.error('Error fetching notifications:', error); console.error('Error fetching notifications:', error);
notifsTableBody.innerHTML = '<tr><td colspan="6" class="text-center">Error loading notifications</td></tr>'; notifsTableBody.innerHTML = '<tr><td colspan="6" class="text-center">Error loading notifications</td></tr>';
}) } finally {
.finally(() => {
isFetching = false; isFetching = false;
}); }
} }
// Function to get notification type badge // Function to get notification type badge
@@ -130,80 +105,123 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Function to mark notification as read // Function to mark notification as read
function markAsRead(notifId) { async function markAsRead(notifId) {
fetch(`/api/notifications/${notifId}/read`, { try {
method: 'POST', const response = await fetch(`/api/notifications/${notifId}/read`, {
headers: { method: 'POST',
'X-Requested-With': 'XMLHttpRequest', headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
} 'X-CSRFToken': csrfToken,
}) 'X-Requested-With': 'XMLHttpRequest'
.then(response => { }
});
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok'); throw new Error('Network response was not ok');
} }
return response.json();
}) const data = await response.json();
.then(data => {
if (data.success) { if (data.success) {
fetchNotifications(); // Update the UI to show the notification as read
const notifRow = document.querySelector(`tr[data-notif-id="${notifId}"]`);
if (notifRow) {
notifRow.classList.remove('table-warning');
const statusCell = notifRow.querySelector('td:nth-last-child(2)');
if (statusCell) {
statusCell.innerHTML = '<span class="badge bg-success">Read</span>';
}
const actionsCell = notifRow.querySelector('td:last-child');
if (actionsCell) {
const markReadBtn = actionsCell.querySelector('.mark-read');
if (markReadBtn) {
markReadBtn.remove();
}
}
}
} }
}) } catch (error) {
.catch(error => {
console.error('Error marking notification as read:', error); console.error('Error marking notification as read:', error);
}); }
} }
// Function to delete notification // Function to delete notification
function deleteNotification(notifId) { async function deleteNotification(notifId) {
if (!confirm('Are you sure you want to delete this notification?')) { try {
return; const response = await fetch(`/api/notifications/${notifId}`, {
} method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
}
});
fetch(`/api/notifications/${notifId}`, {
method: 'DELETE',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok'); throw new Error('Network response was not ok');
} }
return response.json();
}) const data = await response.json();
.then(data => {
if (data.success) { if (data.success) {
fetchNotifications(); // Remove the notification row from the table
const notifRow = document.querySelector(`tr[data-notif-id="${notifId}"]`);
if (notifRow) {
notifRow.remove();
}
} }
}) } catch (error) {
.catch(error => {
console.error('Error deleting notification:', error); console.error('Error deleting notification:', error);
}); }
} }
// Function to mark all notifications as read // Function to mark all notifications as read
function markAllAsRead() { async function markAllAsRead() {
fetch('/api/notifications/mark-all-read', { try {
method: 'POST', const response = await fetch('/api/notifications/mark-all-read', {
headers: { method: 'POST',
'X-Requested-With': 'XMLHttpRequest' headers: {
} 'Content-Type': 'application/json',
}) 'X-CSRFToken': csrfToken,
.then(response => { 'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) { if (!response.ok) {
throw new Error('Network response was not ok'); throw new Error('Network response was not ok');
} }
return response.json();
}) const data = await response.json();
.then(data => {
if (data.success) { if (data.success) {
fetchNotifications(); // Update all notifications to show as read
document.querySelectorAll('tr[data-notif-id]').forEach(row => {
row.classList.remove('table-warning');
const statusCell = row.querySelector('td:nth-last-child(2)');
if (statusCell) {
statusCell.innerHTML = '<span class="badge bg-success">Read</span>';
}
const actionsCell = row.querySelector('td:last-child');
if (actionsCell) {
const markReadBtn = actionsCell.querySelector('.mark-read');
if (markReadBtn) {
markReadBtn.remove();
}
}
});
} }
}) } catch (error) {
.catch(error => {
console.error('Error marking all notifications as read:', error); console.error('Error marking all notifications as read:', error);
}); }
}
// Function to handle notification action clicks
function handleNotificationAction(notifId, actionType) {
// Mark the notification as read when an action is taken
markAsRead(notifId);
// Additional handling for specific action types can be added here
if (actionType === 'view_room' || actionType === 'view_conversation') {
// The link will handle the navigation automatically
return true;
}
} }
// Function to attach event listeners // Function to attach event listeners
@@ -231,6 +249,15 @@ document.addEventListener('DOMContentLoaded', function() {
loadNotifDetails(notifId); loadNotifDetails(notifId);
}); });
}); });
// Action buttons (View Room, View Conversation)
document.querySelectorAll('.btn-group a').forEach(btn => {
btn.addEventListener('click', (e) => {
const notifId = e.target.closest('tr').dataset.notifId;
const actionType = btn.classList.contains('fa-door-open') ? 'view_room' : 'view_conversation';
handleNotificationAction(notifId, actionType);
});
});
} }
// Add event listeners for filters with debounce // Add event listeners for filters with debounce

View File

@@ -58,7 +58,7 @@
<tbody id="notifsTableBody"> <tbody id="notifsTableBody">
{% if notifications %} {% if notifications %}
{% for notif in notifications %} {% for notif in notifications %}
<tr class="{% if not notif.read %}table-warning{% endif %}"> <tr class="{% if not notif.read %}table-warning{% endif %}" data-notif-id="{{ notif.id }}">
<td>{{ notif.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td> <td>{{ notif.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td> <td>
{% if notif.notif_type == 'account_created' %} {% if notif.notif_type == 'account_created' %}
@@ -83,7 +83,7 @@
<span class="badge bg-secondary">{{ notif.notif_type }}</span> <span class="badge bg-secondary">{{ notif.notif_type }}</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ notif.sender.username if notif.sender else 'System' }}</td> <td>{{ notif.sender.username + ' ' + notif.sender.last_name if notif.sender else 'System' }}</td>
<td> <td>
<button class="btn btn-sm btn-secondary" <button class="btn btn-sm btn-secondary"
data-bs-toggle="modal" data-bs-toggle="modal"
@@ -100,14 +100,27 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if not notif.read %} <div class="btn-group">
<button class="btn btn-sm btn-primary mark-read" data-notif-id="{{ notif.id }}"> {% if notif.notif_type in ['room_invite', 'room_invite_removed'] and notif.details and notif.details.room_id %}
<i class="fas fa-check"></i> Mark as Read <a href="{{ url_for('rooms.room', room_id=notif.details.room_id) }}"
</button> class="btn btn-sm btn-primary">
{% endif %} <i class="fas fa-door-open"></i> View Room
<button class="btn btn-sm btn-danger delete-notif" data-notif-id="{{ notif.id }}"> </a>
<i class="fas fa-trash"></i> {% elif notif.notif_type in ['conversation_invite', 'conversation_invite_removed', 'conversation_message'] and notif.details and notif.details.conversation_id %}
</button> <a href="{{ url_for('conversations.conversation', conversation_id=notif.details.conversation_id) }}"
class="btn btn-sm btn-primary">
<i class="fas fa-comments"></i> View Conversation
</a>
{% endif %}
{% if not notif.read %}
<button class="btn btn-sm btn-success mark-read" data-notif-id="{{ notif.id }}">
<i class="fas fa-check"></i> Mark as Read
</button>
{% endif %}
<button class="btn btn-sm btn-danger delete-notif" data-notif-id="{{ notif.id }}">
<i class="fas fa-trash"></i>
</button>
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}