Add notif page
This commit is contained in:
Binary file not shown.
Binary file not shown.
11
create_notifs_table.py
Normal file
11
create_notifs_table.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from app import app, db
|
||||
from models import Notif
|
||||
|
||||
def create_notifs_table():
|
||||
with app.app_context():
|
||||
# Create the table
|
||||
Notif.__table__.create(db.engine)
|
||||
print("Notifications table created successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_notifs_table()
|
||||
Binary file not shown.
61
migrations/add_notifs_table.py
Normal file
61
migrations/add_notifs_table.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the parent directory to Python path so we can import from root
|
||||
sys.path.append(str(Path(__file__).parent.parent))
|
||||
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from extensions import db
|
||||
from sqlalchemy import text
|
||||
|
||||
def upgrade():
|
||||
# Create notifs table
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text('''
|
||||
CREATE TABLE IF NOT EXISTS notifs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
notif_type VARCHAR(50) NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES "user" (id),
|
||||
sender_id INTEGER REFERENCES "user" (id),
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
details JSONB
|
||||
);
|
||||
|
||||
-- Create indexes for faster queries
|
||||
CREATE INDEX IF NOT EXISTS idx_notifs_notif_type ON notifs(notif_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifs_timestamp ON notifs(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifs_user_id ON notifs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifs_sender_id ON notifs(sender_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifs_read ON notifs(read);
|
||||
'''))
|
||||
conn.commit()
|
||||
|
||||
def downgrade():
|
||||
# Drop notifs table and its indexes
|
||||
with db.engine.connect() as conn:
|
||||
conn.execute(text('''
|
||||
DROP INDEX IF EXISTS idx_notifs_notif_type;
|
||||
DROP INDEX IF EXISTS idx_notifs_timestamp;
|
||||
DROP INDEX IF EXISTS idx_notifs_user_id;
|
||||
DROP INDEX IF EXISTS idx_notifs_sender_id;
|
||||
DROP INDEX IF EXISTS idx_notifs_read;
|
||||
DROP TABLE IF EXISTS notifs;
|
||||
'''))
|
||||
conn.commit()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = Flask(__name__)
|
||||
|
||||
# Use the same database configuration as in app.py
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'postgresql://postgres:1253@localhost:5432/docupulse')
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
print("Connecting to database...")
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
with app.app_context():
|
||||
upgrade()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
35
models.py
35
models.py
@@ -262,4 +262,37 @@ class Event(db.Model):
|
||||
user = db.relationship('User', backref='events')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Event {self.event_type} by User {self.user_id} at {self.timestamp}>'
|
||||
return f'<Event {self.event_type} by User {self.user_id} at {self.timestamp}>'
|
||||
|
||||
class NotifType(Enum):
|
||||
# User notifications
|
||||
ACCOUNT_CREATED = 'account_created'
|
||||
PASSWORD_RESET = 'password_reset'
|
||||
ACCOUNT_DELETED = 'account_deleted'
|
||||
ACCOUNT_UPDATED = 'account_updated'
|
||||
|
||||
# Room notifications
|
||||
ROOM_INVITE = 'room_invite'
|
||||
ROOM_INVITE_REMOVED = 'room_invite_removed'
|
||||
|
||||
# Conversation notifications
|
||||
CONVERSATION_INVITE = 'conversation_invite'
|
||||
CONVERSATION_INVITE_REMOVED = 'conversation_invite_removed'
|
||||
CONVERSATION_MESSAGE = 'conversation_message'
|
||||
|
||||
class Notif(db.Model):
|
||||
__tablename__ = 'notifs'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
notif_type = db.Column(db.String(50), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
read = db.Column(db.Boolean, default=False, nullable=False)
|
||||
details = db.Column(db.JSON) # Store additional notification-specific data
|
||||
|
||||
# Relationships
|
||||
user = db.relationship('User', foreign_keys=[user_id], backref='notifications')
|
||||
sender = db.relationship('User', foreign_keys=[sender_id], backref='sent_notifications')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Notif {self.notif_type} for User {self.user_id} at {self.timestamp}>'
|
||||
Binary file not shown.
141
routes/main.py
141
routes/main.py
@@ -1,6 +1,6 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session
|
||||
from flask_login import current_user, login_required
|
||||
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment
|
||||
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif
|
||||
from routes.auth import require_password_change
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
@@ -441,7 +441,144 @@ def init_routes(main_bp):
|
||||
@login_required
|
||||
@require_password_change
|
||||
def notifications():
|
||||
return render_template('notifications/notifications.html')
|
||||
# Get filter parameters
|
||||
notif_type = request.args.get('notif_type', '')
|
||||
date_range = request.args.get('date_range', '7d')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 10
|
||||
|
||||
# Calculate date range
|
||||
end_date = datetime.utcnow()
|
||||
if date_range == '24h':
|
||||
start_date = end_date - timedelta(days=1)
|
||||
elif date_range == '7d':
|
||||
start_date = end_date - timedelta(days=7)
|
||||
elif date_range == '30d':
|
||||
start_date = end_date - timedelta(days=30)
|
||||
else:
|
||||
start_date = None
|
||||
|
||||
# Build query
|
||||
query = Notif.query.filter_by(user_id=current_user.id)
|
||||
|
||||
if notif_type:
|
||||
query = query.filter_by(notif_type=notif_type)
|
||||
if start_date:
|
||||
query = query.filter(Notif.timestamp >= start_date)
|
||||
|
||||
# Get total count for pagination
|
||||
total_notifs = query.count()
|
||||
total_pages = (total_notifs + per_page - 1) // per_page
|
||||
|
||||
# Get paginated notifications
|
||||
notifications = query.order_by(Notif.timestamp.desc()).paginate(page=page, per_page=per_page)
|
||||
|
||||
return render_template('notifications/notifications.html',
|
||||
notifications=notifications.items,
|
||||
total_pages=total_pages,
|
||||
current_page=page)
|
||||
|
||||
@main_bp.route('/api/notifications')
|
||||
@login_required
|
||||
def get_notifications():
|
||||
# Get filter parameters
|
||||
notif_type = request.args.get('notif_type', '')
|
||||
date_range = request.args.get('date_range', '7d')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 10
|
||||
|
||||
# Calculate date range
|
||||
end_date = datetime.utcnow()
|
||||
if date_range == '24h':
|
||||
start_date = end_date - timedelta(days=1)
|
||||
elif date_range == '7d':
|
||||
start_date = end_date - timedelta(days=7)
|
||||
elif date_range == '30d':
|
||||
start_date = end_date - timedelta(days=30)
|
||||
else:
|
||||
start_date = None
|
||||
|
||||
# Build query
|
||||
query = Notif.query.filter_by(user_id=current_user.id)
|
||||
|
||||
if notif_type:
|
||||
query = query.filter_by(notif_type=notif_type)
|
||||
if start_date:
|
||||
query = query.filter(Notif.timestamp >= start_date)
|
||||
|
||||
# Get total count for pagination
|
||||
total_notifs = query.count()
|
||||
total_pages = (total_notifs + per_page - 1) // per_page
|
||||
|
||||
# Get paginated notifications
|
||||
notifications = query.order_by(Notif.timestamp.desc()).paginate(page=page, per_page=per_page)
|
||||
|
||||
return jsonify({
|
||||
'notifications': [{
|
||||
'id': notif.id,
|
||||
'notif_type': notif.notif_type,
|
||||
'timestamp': notif.timestamp.isoformat(),
|
||||
'read': notif.read,
|
||||
'details': notif.details,
|
||||
'sender': {
|
||||
'id': notif.sender.id,
|
||||
'username': notif.sender.username
|
||||
} if notif.sender else None
|
||||
} for notif in notifications.items],
|
||||
'total_pages': total_pages,
|
||||
'current_page': page
|
||||
})
|
||||
|
||||
@main_bp.route('/api/notifications/<int:notif_id>')
|
||||
@login_required
|
||||
def get_notification_details(notif_id):
|
||||
notif = Notif.query.get_or_404(notif_id)
|
||||
if notif.user_id != current_user.id:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
return jsonify({
|
||||
'id': notif.id,
|
||||
'notif_type': notif.notif_type,
|
||||
'timestamp': notif.timestamp.isoformat(),
|
||||
'read': notif.read,
|
||||
'details': notif.details,
|
||||
'sender': {
|
||||
'id': notif.sender.id,
|
||||
'username': notif.sender.username
|
||||
} if notif.sender else None
|
||||
})
|
||||
|
||||
@main_bp.route('/api/notifications/<int:notif_id>/read', methods=['POST'])
|
||||
@login_required
|
||||
def mark_notification_read(notif_id):
|
||||
notif = Notif.query.get_or_404(notif_id)
|
||||
if notif.user_id != current_user.id:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
notif.read = True
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
@main_bp.route('/api/notifications/mark-all-read', methods=['POST'])
|
||||
@login_required
|
||||
def mark_all_notifications_read():
|
||||
result = Notif.query.filter_by(user_id=current_user.id, read=False).update({'read': True})
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'count': result})
|
||||
|
||||
@main_bp.route('/api/notifications/<int:notif_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_notification(notif_id):
|
||||
notif = Notif.query.get_or_404(notif_id)
|
||||
if notif.user_id != current_user.id:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
db.session.delete(notif)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
@main_bp.route('/settings')
|
||||
@login_required
|
||||
|
||||
288
static/js/notifications.js
Normal file
288
static/js/notifications.js
Normal file
@@ -0,0 +1,288 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize variables
|
||||
let currentPage = 1;
|
||||
let totalPages = parseInt(document.getElementById('totalPages').textContent) || 1;
|
||||
let isFetching = false;
|
||||
|
||||
// Get filter elements
|
||||
const notifTypeFilter = document.getElementById('notifTypeFilter');
|
||||
const dateRangeFilter = document.getElementById('dateRangeFilter');
|
||||
const clearFiltersBtn = document.getElementById('clearFilters');
|
||||
const markAllReadBtn = document.getElementById('markAllRead');
|
||||
|
||||
// Get pagination elements
|
||||
const prevPageBtn = document.getElementById('prevPage');
|
||||
const nextPageBtn = document.getElementById('nextPage');
|
||||
const currentPageSpan = document.getElementById('currentPage');
|
||||
const totalPagesSpan = document.getElementById('totalPages');
|
||||
const notifsTableBody = document.getElementById('notifsTableBody');
|
||||
|
||||
// Notification details modal
|
||||
const notifDetailsModal = document.getElementById('notifDetailsModal');
|
||||
const notifDetailsContent = document.getElementById('notifDetailsContent');
|
||||
|
||||
// Function to update URL with current filters
|
||||
function updateURL() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('notif_type', notifTypeFilter.value);
|
||||
params.set('date_range', dateRangeFilter.value);
|
||||
params.set('page', currentPage);
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`);
|
||||
}
|
||||
|
||||
// Function to fetch notifications
|
||||
function fetchNotifications() {
|
||||
if (isFetching) return;
|
||||
isFetching = true;
|
||||
|
||||
// Show loading state
|
||||
notifsTableBody.innerHTML = '<tr><td colspan="6" class="text-center">Loading...</td></tr>';
|
||||
|
||||
const params = new URLSearchParams({
|
||||
notif_type: notifTypeFilter.value,
|
||||
date_range: dateRangeFilter.value,
|
||||
page: currentPage,
|
||||
ajax: 'true'
|
||||
});
|
||||
|
||||
fetch(`${window.location.pathname}?${params.toString()}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.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) {
|
||||
notifsTableBody.innerHTML = newTableBody.innerHTML;
|
||||
|
||||
// Update pagination
|
||||
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);
|
||||
notifsTableBody.innerHTML = '<tr><td colspan="6" class="text-center">Error loading notifications</td></tr>';
|
||||
})
|
||||
.finally(() => {
|
||||
isFetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to get notification type badge
|
||||
function getNotifTypeBadge(type) {
|
||||
const badges = {
|
||||
'account_created': '<span class="badge bg-success">Account Created</span>',
|
||||
'password_reset': '<span class="badge bg-warning">Password Reset</span>',
|
||||
'account_deleted': '<span class="badge bg-danger">Account Deleted</span>',
|
||||
'account_updated': '<span class="badge bg-info">Account Updated</span>',
|
||||
'room_invite': '<span class="badge bg-primary">Room Invite</span>',
|
||||
'room_invite_removed': '<span class="badge bg-secondary">Room Invite Removed</span>',
|
||||
'conversation_invite': '<span class="badge bg-primary">Conversation Invite</span>',
|
||||
'conversation_invite_removed': '<span class="badge bg-secondary">Conversation Invite Removed</span>',
|
||||
'conversation_message': '<span class="badge bg-info">Conversation Message</span>'
|
||||
};
|
||||
return badges[type] || `<span class="badge bg-secondary">${type}</span>`;
|
||||
}
|
||||
|
||||
// Function to load notification details
|
||||
function loadNotifDetails(notifId) {
|
||||
fetch(`/api/notifications/${notifId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const detailsContent = document.getElementById('notifDetailsContent');
|
||||
if (data.details) {
|
||||
detailsContent.textContent = JSON.stringify(data.details, null, 2);
|
||||
} else {
|
||||
detailsContent.textContent = 'No additional details available';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading notification details:', error);
|
||||
document.getElementById('notifDetailsContent').textContent = 'Error loading notification details';
|
||||
});
|
||||
}
|
||||
|
||||
// Function to mark notification as read
|
||||
function markAsRead(notifId) {
|
||||
fetch(`/api/notifications/${notifId}/read`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
fetchNotifications();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error marking notification as read:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to delete notification
|
||||
function deleteNotification(notifId) {
|
||||
if (!confirm('Are you sure you want to delete this notification?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/notifications/${notifId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
fetchNotifications();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error deleting notification:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to mark all notifications as read
|
||||
function markAllAsRead() {
|
||||
fetch('/api/notifications/mark-all-read', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
fetchNotifications();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error marking all notifications as read:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to attach event listeners
|
||||
function attachEventListeners() {
|
||||
// Mark as read buttons
|
||||
document.querySelectorAll('.mark-read').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const notifId = e.target.closest('.mark-read').dataset.notifId;
|
||||
markAsRead(notifId);
|
||||
});
|
||||
});
|
||||
|
||||
// Delete buttons
|
||||
document.querySelectorAll('.delete-notif').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const notifId = e.target.closest('.delete-notif').dataset.notifId;
|
||||
deleteNotification(notifId);
|
||||
});
|
||||
});
|
||||
|
||||
// View details buttons
|
||||
document.querySelectorAll('[data-bs-target="#notifDetailsModal"]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const notifId = e.target.closest('[data-notif-id]').dataset.notifId;
|
||||
loadNotifDetails(notifId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listeners for filters with debounce
|
||||
let filterTimeout;
|
||||
function debouncedFetch() {
|
||||
clearTimeout(filterTimeout);
|
||||
filterTimeout = setTimeout(() => {
|
||||
currentPage = 1; // Reset to first page when filters change
|
||||
fetchNotifications();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
notifTypeFilter.addEventListener('change', debouncedFetch);
|
||||
dateRangeFilter.addEventListener('change', debouncedFetch);
|
||||
|
||||
// Add event listener for clear filters
|
||||
clearFiltersBtn.addEventListener('click', () => {
|
||||
notifTypeFilter.value = '';
|
||||
dateRangeFilter.value = '7d';
|
||||
currentPage = 1;
|
||||
fetchNotifications();
|
||||
});
|
||||
|
||||
// Add event listener for mark all as read
|
||||
markAllReadBtn.addEventListener('click', markAllAsRead);
|
||||
|
||||
// Add event listeners for pagination
|
||||
prevPageBtn.addEventListener('click', () => {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
fetchNotifications();
|
||||
}
|
||||
});
|
||||
|
||||
nextPageBtn.addEventListener('click', () => {
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
fetchNotifications();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize filters from URL parameters
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
notifTypeFilter.value = params.get('notif_type') || '';
|
||||
dateRangeFilter.value = params.get('date_range') || '7d';
|
||||
currentPage = parseInt(params.get('page')) || 1;
|
||||
|
||||
// Initial fetch if filters are set
|
||||
if (notifTypeFilter.value || dateRangeFilter.value !== '7d') {
|
||||
fetchNotifications();
|
||||
}
|
||||
|
||||
// Attach initial event listeners
|
||||
attachEventListeners();
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "common/base.html" %}
|
||||
{% from "components/header.html" import header %}
|
||||
|
||||
{% block title %}Notifications - {{ super() }}{% endblock %}
|
||||
{% block title %}Notifications - DocuPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ header(
|
||||
@@ -11,10 +11,154 @@
|
||||
) }}
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="card">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="text-muted">No notifications at this time.</p>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="card-title mb-0"></h5>
|
||||
<div class="d-flex gap-2">
|
||||
<select id="notifTypeFilter" class="form-select form-select-sm">
|
||||
<option value="">All Notification Types</option>
|
||||
<option value="account_created">Account Created</option>
|
||||
<option value="password_reset">Password Reset</option>
|
||||
<option value="account_deleted">Account Deleted</option>
|
||||
<option value="account_updated">Account Updated</option>
|
||||
<option value="room_invite">Room Invite</option>
|
||||
<option value="room_invite_removed">Room Invite Removed</option>
|
||||
<option value="conversation_invite">Conversation Invite</option>
|
||||
<option value="conversation_invite_removed">Conversation Invite Removed</option>
|
||||
<option value="conversation_message">Conversation Message</option>
|
||||
</select>
|
||||
<select id="dateRangeFilter" class="form-select form-select-sm">
|
||||
<option value="24h">Last 24 Hours</option>
|
||||
<option value="7d" selected>Last 7 Days</option>
|
||||
<option value="30d">Last 30 Days</option>
|
||||
<option value="all">All Time</option>
|
||||
</select>
|
||||
<button id="clearFilters" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-times me-1"></i>Clear Filters
|
||||
</button>
|
||||
<button id="markAllRead" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-check-double"></i> Mark All as Read
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Type</th>
|
||||
<th>From</th>
|
||||
<th>Details</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="notifsTableBody">
|
||||
{% if notifications %}
|
||||
{% for notif in notifications %}
|
||||
<tr class="{% if not notif.read %}table-warning{% endif %}">
|
||||
<td>{{ notif.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||
<td>
|
||||
{% if notif.notif_type == 'account_created' %}
|
||||
<span class="badge bg-success">Account Created</span>
|
||||
{% elif notif.notif_type == 'password_reset' %}
|
||||
<span class="badge bg-warning">Password Reset</span>
|
||||
{% elif notif.notif_type == 'account_deleted' %}
|
||||
<span class="badge bg-danger">Account Deleted</span>
|
||||
{% elif notif.notif_type == 'account_updated' %}
|
||||
<span class="badge bg-info">Account Updated</span>
|
||||
{% elif notif.notif_type == 'room_invite' %}
|
||||
<span class="badge bg-primary">Room Invite</span>
|
||||
{% elif notif.notif_type == 'room_invite_removed' %}
|
||||
<span class="badge bg-secondary">Room Invite Removed</span>
|
||||
{% elif notif.notif_type == 'conversation_invite' %}
|
||||
<span class="badge bg-primary">Conversation Invite</span>
|
||||
{% elif notif.notif_type == 'conversation_invite_removed' %}
|
||||
<span class="badge bg-secondary">Conversation Invite Removed</span>
|
||||
{% elif notif.notif_type == 'conversation_message' %}
|
||||
<span class="badge bg-info">Conversation Message</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ notif.notif_type }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ notif.sender.username if notif.sender else 'System' }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#notifDetailsModal"
|
||||
data-notif-id="{{ notif.id }}">
|
||||
<i class="fas fa-info-circle"></i> View Details
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{% if notif.read %}
|
||||
<span class="badge bg-success">Read</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Unread</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not notif.read %}
|
||||
<button class="btn btn-sm btn-primary 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>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">No notifications found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<div>
|
||||
<button id="prevPage" class="btn btn-outline-primary btn-sm"
|
||||
style="border-color:var(--primary-color); color:var(--primary-color);"
|
||||
onmouseover="this.style.backgroundColor='var(--primary-color)'; this.style.color='white'"
|
||||
onmouseout="this.style.backgroundColor='transparent'; this.style.color='var(--primary-color)'"
|
||||
{% if current_page == 1 %}disabled{% endif %}>
|
||||
<i class="fas fa-chevron-left me-1"></i>Previous
|
||||
</button>
|
||||
<span class="mx-2">Page <span id="currentPage">{{ current_page }}</span> of <span id="totalPages">{{ total_pages }}</span></span>
|
||||
<button id="nextPage" class="btn btn-outline-primary btn-sm"
|
||||
style="border-color:var(--primary-color); color:var(--primary-color);"
|
||||
onmouseover="this.style.backgroundColor='var(--primary-color)'; this.style.color='white'"
|
||||
onmouseout="this.style.backgroundColor='transparent'; this.style.color='var(--primary-color)'"
|
||||
{% if current_page == total_pages %}disabled{% endif %}>
|
||||
Next<i class="fas fa-chevron-right ms-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Details Modal -->
|
||||
<div class="modal fade" id="notifDetailsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Notification Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="notifDetailsContent" class="bg-light p-3 rounded"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{{ url_for('static', filename='js/notifications.js', v=config.CSS_VERSION) }}"></script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,7 @@
|
||||
# Utils package initialization
|
||||
from .permissions import user_has_permission, get_user_permissions
|
||||
from .event_logger import log_event, get_user_events, get_room_events, get_recent_events, get_events_by_type, get_events_by_date_range
|
||||
from .notification import create_notification, get_user_notifications, mark_notification_read, mark_all_notifications_read, get_unread_count, delete_notification, delete_old_notifications
|
||||
from .path_utils import clean_path, secure_file_path
|
||||
from .time_utils import timeago, format_datetime, parse_datetime
|
||||
|
||||
@@ -13,6 +14,13 @@ __all__ = [
|
||||
'get_recent_events',
|
||||
'get_events_by_type',
|
||||
'get_events_by_date_range',
|
||||
'create_notification',
|
||||
'get_user_notifications',
|
||||
'mark_notification_read',
|
||||
'mark_all_notifications_read',
|
||||
'get_unread_count',
|
||||
'delete_notification',
|
||||
'delete_old_notifications',
|
||||
'clean_path',
|
||||
'secure_file_path',
|
||||
'timeago',
|
||||
|
||||
Binary file not shown.
BIN
utils/__pycache__/notification.cpython-313.pyc
Normal file
BIN
utils/__pycache__/notification.cpython-313.pyc
Normal file
Binary file not shown.
91
utils/notification.py
Normal file
91
utils/notification.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from flask import request
|
||||
from models import Notif, NotifType, db
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import desc
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def create_notification(
|
||||
notif_type: str,
|
||||
user_id: int,
|
||||
sender_id: Optional[int] = None,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
) -> Notif:
|
||||
"""
|
||||
Create a notification in the database.
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
The created Notif object
|
||||
"""
|
||||
logger.debug(f"Creating notification of type: {notif_type}")
|
||||
logger.debug(f"Notification details: {details}")
|
||||
|
||||
try:
|
||||
notif = Notif(
|
||||
notif_type=notif_type,
|
||||
user_id=user_id,
|
||||
sender_id=sender_id,
|
||||
timestamp=datetime.utcnow(),
|
||||
details=details or {},
|
||||
read=False
|
||||
)
|
||||
|
||||
logger.debug(f"Created notification object: {notif}")
|
||||
db.session.add(notif)
|
||||
# Don't commit here - let the caller handle the transaction
|
||||
logger.debug("Notification object added to session")
|
||||
return notif
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating notification: {str(e)}")
|
||||
raise
|
||||
|
||||
def get_user_notifications(user_id: int, limit: int = 50, unread_only: bool = False) -> List[Notif]:
|
||||
"""Get recent notifications for a specific user"""
|
||||
query = Notif.query.filter_by(user_id=user_id)
|
||||
if unread_only:
|
||||
query = query.filter_by(read=False)
|
||||
return query.order_by(desc(Notif.timestamp)).limit(limit).all()
|
||||
|
||||
def mark_notification_read(notif_id: int) -> bool:
|
||||
"""Mark a notification as read"""
|
||||
notif = Notif.query.get(notif_id)
|
||||
if notif:
|
||||
notif.read = True
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def mark_all_notifications_read(user_id: int) -> int:
|
||||
"""Mark all notifications as read for a user"""
|
||||
result = Notif.query.filter_by(user_id=user_id, read=False).update({'read': True})
|
||||
db.session.commit()
|
||||
return result
|
||||
|
||||
def get_unread_count(user_id: int) -> int:
|
||||
"""Get count of unread notifications for a user"""
|
||||
return Notif.query.filter_by(user_id=user_id, read=False).count()
|
||||
|
||||
def delete_notification(notif_id: int) -> bool:
|
||||
"""Delete a notification"""
|
||||
notif = Notif.query.get(notif_id)
|
||||
if notif:
|
||||
db.session.delete(notif)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def delete_old_notifications(days: int = 30) -> int:
|
||||
"""Delete notifications older than specified days"""
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
result = Notif.query.filter(Notif.timestamp < cutoff_date).delete()
|
||||
db.session.commit()
|
||||
return result
|
||||
Reference in New Issue
Block a user