diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 5158dca..b8a910a 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 88e91f3..a3f0b46 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/create_notifs_table.py b/create_notifs_table.py new file mode 100644 index 0000000..b708e06 --- /dev/null +++ b/create_notifs_table.py @@ -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() \ No newline at end of file diff --git a/migrations/__pycache__/env.cpython-313.pyc b/migrations/__pycache__/env.cpython-313.pyc index 7812ee9..003d065 100644 Binary files a/migrations/__pycache__/env.cpython-313.pyc and b/migrations/__pycache__/env.cpython-313.pyc differ diff --git a/migrations/add_notifs_table.py b/migrations/add_notifs_table.py new file mode 100644 index 0000000..4153c77 --- /dev/null +++ b/migrations/add_notifs_table.py @@ -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() \ No newline at end of file diff --git a/migrations/versions/__pycache__/0f48943140fa_add_file_attachment_fields_to_message_.cpython-313.pyc b/migrations/versions/__pycache__/0f48943140fa_add_file_attachment_fields_to_message_.cpython-313.pyc index e1e2056..c78a729 100644 Binary files a/migrations/versions/__pycache__/0f48943140fa_add_file_attachment_fields_to_message_.cpython-313.pyc and b/migrations/versions/__pycache__/0f48943140fa_add_file_attachment_fields_to_message_.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/1c297825e3a9_add_user_authentication_fields.cpython-313.pyc b/migrations/versions/__pycache__/1c297825e3a9_add_user_authentication_fields.cpython-313.pyc index 53e57f7..c3ba359 100644 Binary files a/migrations/versions/__pycache__/1c297825e3a9_add_user_authentication_fields.cpython-313.pyc and b/migrations/versions/__pycache__/1c297825e3a9_add_user_authentication_fields.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/25da158dd705_add_is_admin_to_contact_model.cpython-313.pyc b/migrations/versions/__pycache__/25da158dd705_add_is_admin_to_contact_model.cpython-313.pyc index 5e7a28e..650458a 100644 Binary files a/migrations/versions/__pycache__/25da158dd705_add_is_admin_to_contact_model.cpython-313.pyc and b/migrations/versions/__pycache__/25da158dd705_add_is_admin_to_contact_model.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/26b0e5357f52_add_room_member_permissions_table.cpython-313.pyc b/migrations/versions/__pycache__/26b0e5357f52_add_room_member_permissions_table.cpython-313.pyc index 7299cfb..7149de9 100644 Binary files a/migrations/versions/__pycache__/26b0e5357f52_add_room_member_permissions_table.cpython-313.pyc and b/migrations/versions/__pycache__/26b0e5357f52_add_room_member_permissions_table.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/2c5f57dddb78_add_room_members_table.cpython-313.pyc b/migrations/versions/__pycache__/2c5f57dddb78_add_room_members_table.cpython-313.pyc index a8d4deb..4e341c1 100644 Binary files a/migrations/versions/__pycache__/2c5f57dddb78_add_room_members_table.cpython-313.pyc and b/migrations/versions/__pycache__/2c5f57dddb78_add_room_members_table.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/3a5b8d8e53cd_add_rooms_table.cpython-313.pyc b/migrations/versions/__pycache__/3a5b8d8e53cd_add_rooms_table.cpython-313.pyc index a2a1611..cffd41e 100644 Binary files a/migrations/versions/__pycache__/3a5b8d8e53cd_add_rooms_table.cpython-313.pyc and b/migrations/versions/__pycache__/3a5b8d8e53cd_add_rooms_table.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/43dfd2543fad_add_contact_fields_to_user_model.cpython-313.pyc b/migrations/versions/__pycache__/43dfd2543fad_add_contact_fields_to_user_model.cpython-313.pyc index c05f61e..a627b59 100644 Binary files a/migrations/versions/__pycache__/43dfd2543fad_add_contact_fields_to_user_model.cpython-313.pyc and b/migrations/versions/__pycache__/43dfd2543fad_add_contact_fields_to_user_model.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/64b5c28510b0_add_room_file_table_for_file_folder_.cpython-313.pyc b/migrations/versions/__pycache__/64b5c28510b0_add_room_file_table_for_file_folder_.cpython-313.pyc index 6bec1d5..0b51f76 100644 Binary files a/migrations/versions/__pycache__/64b5c28510b0_add_room_file_table_for_file_folder_.cpython-313.pyc and b/migrations/versions/__pycache__/64b5c28510b0_add_room_file_table_for_file_folder_.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/6651332488d9_add_starred_column_to_room_file_table.cpython-313.pyc b/migrations/versions/__pycache__/6651332488d9_add_starred_column_to_room_file_table.cpython-313.pyc index 7152828..dcb79da 100644 Binary files a/migrations/versions/__pycache__/6651332488d9_add_starred_column_to_room_file_table.cpython-313.pyc and b/migrations/versions/__pycache__/6651332488d9_add_starred_column_to_room_file_table.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/7554ab70efe7_fix_existing_users.cpython-313.pyc b/migrations/versions/__pycache__/7554ab70efe7_fix_existing_users.cpython-313.pyc index d442bc6..6601fda 100644 Binary files a/migrations/versions/__pycache__/7554ab70efe7_fix_existing_users.cpython-313.pyc and b/migrations/versions/__pycache__/7554ab70efe7_fix_existing_users.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/76da0573e84b_merge_heads.cpython-313.pyc b/migrations/versions/__pycache__/76da0573e84b_merge_heads.cpython-313.pyc index b282136..c3f2a58 100644 Binary files a/migrations/versions/__pycache__/76da0573e84b_merge_heads.cpython-313.pyc and b/migrations/versions/__pycache__/76da0573e84b_merge_heads.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/787468cfea77_add_company_information_fields_to_.cpython-313.pyc b/migrations/versions/__pycache__/787468cfea77_add_company_information_fields_to_.cpython-313.pyc index ec6ef61..10a9891 100644 Binary files a/migrations/versions/__pycache__/787468cfea77_add_company_information_fields_to_.cpython-313.pyc and b/migrations/versions/__pycache__/787468cfea77_add_company_information_fields_to_.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/7a5747dc773f_update_last_name_field_in_user_model.cpython-313.pyc b/migrations/versions/__pycache__/7a5747dc773f_update_last_name_field_in_user_model.cpython-313.pyc index 6d92984..2ae9513 100644 Binary files a/migrations/versions/__pycache__/7a5747dc773f_update_last_name_field_in_user_model.cpython-313.pyc and b/migrations/versions/__pycache__/7a5747dc773f_update_last_name_field_in_user_model.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/9faab7ef6036_add_site_settings_table.cpython-313.pyc b/migrations/versions/__pycache__/9faab7ef6036_add_site_settings_table.cpython-313.pyc index c51223a..a05bed2 100644 Binary files a/migrations/versions/__pycache__/9faab7ef6036_add_site_settings_table.cpython-313.pyc and b/migrations/versions/__pycache__/9faab7ef6036_add_site_settings_table.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/add_conversations_tables.cpython-313.pyc b/migrations/versions/__pycache__/add_conversations_tables.cpython-313.pyc index c046bbc..31a94fe 100644 Binary files a/migrations/versions/__pycache__/add_conversations_tables.cpython-313.pyc and b/migrations/versions/__pycache__/add_conversations_tables.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/add_deleted_by_to_room_file.cpython-313.pyc b/migrations/versions/__pycache__/add_deleted_by_to_room_file.cpython-313.pyc index 0bcd520..65b7a36 100644 Binary files a/migrations/versions/__pycache__/add_deleted_by_to_room_file.cpython-313.pyc and b/migrations/versions/__pycache__/add_deleted_by_to_room_file.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/add_deleted_column_to_room_file.cpython-313.pyc b/migrations/versions/__pycache__/add_deleted_column_to_room_file.cpython-313.pyc index f186c7d..b63e597 100644 Binary files a/migrations/versions/__pycache__/add_deleted_column_to_room_file.cpython-313.pyc and b/migrations/versions/__pycache__/add_deleted_column_to_room_file.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/add_trashed_file_table.cpython-313.pyc b/migrations/versions/__pycache__/add_trashed_file_table.cpython-313.pyc index acca59b..cf6935c 100644 Binary files a/migrations/versions/__pycache__/add_trashed_file_table.cpython-313.pyc and b/migrations/versions/__pycache__/add_trashed_file_table.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/b978642e7b10_add_preferred_view_to_user.cpython-313.pyc b/migrations/versions/__pycache__/b978642e7b10_add_preferred_view_to_user.cpython-313.pyc index 47e5081..0c95380 100644 Binary files a/migrations/versions/__pycache__/b978642e7b10_add_preferred_view_to_user.cpython-313.pyc and b/migrations/versions/__pycache__/b978642e7b10_add_preferred_view_to_user.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/bd04430cda95_merge_heads.cpython-313.pyc b/migrations/versions/__pycache__/bd04430cda95_merge_heads.cpython-313.pyc index 64b48cc..c583a8e 100644 Binary files a/migrations/versions/__pycache__/bd04430cda95_merge_heads.cpython-313.pyc and b/migrations/versions/__pycache__/bd04430cda95_merge_heads.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/be1f7bdd10e1_add_granular_permissions_to_.cpython-313.pyc b/migrations/versions/__pycache__/be1f7bdd10e1_add_granular_permissions_to_.cpython-313.pyc index 155a394..ced8a3f 100644 Binary files a/migrations/versions/__pycache__/be1f7bdd10e1_add_granular_permissions_to_.cpython-313.pyc and b/migrations/versions/__pycache__/be1f7bdd10e1_add_granular_permissions_to_.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/c21f243b3640_add_profile_picture_to_user.cpython-313.pyc b/migrations/versions/__pycache__/c21f243b3640_add_profile_picture_to_user.cpython-313.pyc index c4ec16f..c897497 100644 Binary files a/migrations/versions/__pycache__/c21f243b3640_add_profile_picture_to_user.cpython-313.pyc and b/migrations/versions/__pycache__/c21f243b3640_add_profile_picture_to_user.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/c243d6a1843d_add_last_name_to_user_model.cpython-313.pyc b/migrations/versions/__pycache__/c243d6a1843d_add_last_name_to_user_model.cpython-313.pyc index 83de312..5cf7f6d 100644 Binary files a/migrations/versions/__pycache__/c243d6a1843d_add_last_name_to_user_model.cpython-313.pyc and b/migrations/versions/__pycache__/c243d6a1843d_add_last_name_to_user_model.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/ca9026520dad_add_colorsettings_table.cpython-313.pyc b/migrations/versions/__pycache__/ca9026520dad_add_colorsettings_table.cpython-313.pyc index 42eefe2..34ac5df 100644 Binary files a/migrations/versions/__pycache__/ca9026520dad_add_colorsettings_table.cpython-313.pyc and b/migrations/versions/__pycache__/ca9026520dad_add_colorsettings_table.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/create_user_starred_file_table.cpython-313.pyc b/migrations/versions/__pycache__/create_user_starred_file_table.cpython-313.pyc index df2b464..cd02d4e 100644 Binary files a/migrations/versions/__pycache__/create_user_starred_file_table.cpython-313.pyc and b/migrations/versions/__pycache__/create_user_starred_file_table.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/d8dcbf9fe881_increase_password_hash_column_length.cpython-313.pyc b/migrations/versions/__pycache__/d8dcbf9fe881_increase_password_hash_column_length.cpython-313.pyc index 8820724..3a95e7a 100644 Binary files a/migrations/versions/__pycache__/d8dcbf9fe881_increase_password_hash_column_length.cpython-313.pyc and b/migrations/versions/__pycache__/d8dcbf9fe881_increase_password_hash_column_length.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/dbcb5d2d3ed0_add_contact_model.cpython-313.pyc b/migrations/versions/__pycache__/dbcb5d2d3ed0_add_contact_model.cpython-313.pyc index f4b3129..c7d9ba0 100644 Binary files a/migrations/versions/__pycache__/dbcb5d2d3ed0_add_contact_model.cpython-313.pyc and b/migrations/versions/__pycache__/dbcb5d2d3ed0_add_contact_model.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/e7e4ff171f7a_add_message_attachments_table_and_.cpython-313.pyc b/migrations/versions/__pycache__/e7e4ff171f7a_add_message_attachments_table_and_.cpython-313.pyc index c8d43d7..3d264a1 100644 Binary files a/migrations/versions/__pycache__/e7e4ff171f7a_add_message_attachments_table_and_.cpython-313.pyc and b/migrations/versions/__pycache__/e7e4ff171f7a_add_message_attachments_table_and_.cpython-313.pyc differ diff --git a/migrations/versions/__pycache__/f18735338888_add_company_logo_field.cpython-313.pyc b/migrations/versions/__pycache__/f18735338888_add_company_logo_field.cpython-313.pyc index d8e655a..500dedb 100644 Binary files a/migrations/versions/__pycache__/f18735338888_add_company_logo_field.cpython-313.pyc and b/migrations/versions/__pycache__/f18735338888_add_company_logo_field.cpython-313.pyc differ diff --git a/models.py b/models.py index e6e3866..6c890a9 100644 --- a/models.py +++ b/models.py @@ -262,4 +262,37 @@ class Event(db.Model): user = db.relationship('User', backref='events') def __repr__(self): - return f'' \ No newline at end of file + return f'' + +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'' \ No newline at end of file diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index c453e41..1bbc838 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/main.py b/routes/main.py index 02fe663..69d6d55 100644 --- a/routes/main.py +++ b/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/') + @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//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/', 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 diff --git a/static/js/notifications.js b/static/js/notifications.js new file mode 100644 index 0000000..1b65785 --- /dev/null +++ b/static/js/notifications.js @@ -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 = 'Loading...'; + + 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 = 'Error loading notifications'; + } + }) + .catch(error => { + console.error('Error fetching notifications:', error); + notifsTableBody.innerHTML = 'Error loading notifications'; + }) + .finally(() => { + isFetching = false; + }); + } + + // Function to get notification type badge + function getNotifTypeBadge(type) { + const badges = { + 'account_created': 'Account Created', + 'password_reset': 'Password Reset', + 'account_deleted': 'Account Deleted', + 'account_updated': 'Account Updated', + 'room_invite': 'Room Invite', + 'room_invite_removed': 'Room Invite Removed', + 'conversation_invite': 'Conversation Invite', + 'conversation_invite_removed': 'Conversation Invite Removed', + 'conversation_message': 'Conversation Message' + }; + return badges[type] || `${type}`; + } + + // 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(); +}); \ No newline at end of file diff --git a/templates/notifications/notifications.html b/templates/notifications/notifications.html index 6849c6b..f2a3018 100644 --- a/templates/notifications/notifications.html +++ b/templates/notifications/notifications.html @@ -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 @@ ) }}
-
+
-

No notifications at this time.

+
+
+
+ + + + +
+
+ +
+ + + + + + + + + + + + + {% if notifications %} + {% for notif in notifications %} + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
TimestampTypeFromDetailsStatusActions
{{ notif.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} + {% if notif.notif_type == 'account_created' %} + Account Created + {% elif notif.notif_type == 'password_reset' %} + Password Reset + {% elif notif.notif_type == 'account_deleted' %} + Account Deleted + {% elif notif.notif_type == 'account_updated' %} + Account Updated + {% elif notif.notif_type == 'room_invite' %} + Room Invite + {% elif notif.notif_type == 'room_invite_removed' %} + Room Invite Removed + {% elif notif.notif_type == 'conversation_invite' %} + Conversation Invite + {% elif notif.notif_type == 'conversation_invite_removed' %} + Conversation Invite Removed + {% elif notif.notif_type == 'conversation_message' %} + Conversation Message + {% else %} + {{ notif.notif_type }} + {% endif %} + {{ notif.sender.username if notif.sender else 'System' }} + + + {% if notif.read %} + Read + {% else %} + Unread + {% endif %} + + {% if not notif.read %} + + {% endif %} + +
No notifications found
+
+ +
+
+ + Page {{ current_page }} of {{ total_pages }} + +
+
+ + + + +{% block extra_js %} + +{% endblock %} {% endblock %} \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py index 93e480a..abae6e6 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -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', diff --git a/utils/__pycache__/__init__.cpython-313.pyc b/utils/__pycache__/__init__.cpython-313.pyc index 6b2462b..15aee4c 100644 Binary files a/utils/__pycache__/__init__.cpython-313.pyc and b/utils/__pycache__/__init__.cpython-313.pyc differ diff --git a/utils/__pycache__/notification.cpython-313.pyc b/utils/__pycache__/notification.cpython-313.pyc new file mode 100644 index 0000000..1025b40 Binary files /dev/null and b/utils/__pycache__/notification.cpython-313.pyc differ diff --git a/utils/notification.py b/utils/notification.py new file mode 100644 index 0000000..811c9d2 --- /dev/null +++ b/utils/notification.py @@ -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 \ No newline at end of file