From 224d4d400e8912c94c84577c6f8bd0165c61f2eb Mon Sep 17 00:00:00 2001 From: Kobe Date: Sat, 31 May 2025 18:28:53 +0200 Subject: [PATCH] Add notif page --- __pycache__/app.cpython-313.pyc | Bin 5477 -> 5477 bytes __pycache__/models.cpython-313.pyc | Bin 20720 -> 22802 bytes create_notifs_table.py | 11 + migrations/__pycache__/env.cpython-313.pyc | Bin 4535 -> 4535 bytes migrations/add_notifs_table.py | 61 ++++ ...achment_fields_to_message_.cpython-313.pyc | Bin 2273 -> 2273 bytes ...user_authentication_fields.cpython-313.pyc | Bin 1568 -> 1568 bytes ..._is_admin_to_contact_model.cpython-313.pyc | Bin 1320 -> 1320 bytes ...m_member_permissions_table.cpython-313.pyc | Bin 1879 -> 1879 bytes ...b78_add_room_members_table.cpython-313.pyc | Bin 2002 -> 2002 bytes ...5b8d8e53cd_add_rooms_table.cpython-313.pyc | Bin 1778 -> 1778 bytes ...ntact_fields_to_user_model.cpython-313.pyc | Bin 2193 -> 2193 bytes ...ile_table_for_file_folder_.cpython-313.pyc | Bin 2924 -> 2924 bytes ..._column_to_room_file_table.cpython-313.pyc | Bin 2735 -> 2735 bytes ...b70efe7_fix_existing_users.cpython-313.pyc | Bin 1044 -> 1044 bytes .../76da0573e84b_merge_heads.cpython-313.pyc | Bin 781 -> 781 bytes ...any_information_fields_to_.cpython-313.pyc | Bin 3346 -> 3346 bytes ...t_name_field_in_user_model.cpython-313.pyc | Bin 1432 -> 1432 bytes ...36_add_site_settings_table.cpython-313.pyc | Bin 2356 -> 2356 bytes .../add_conversations_tables.cpython-313.pyc | Bin 3146 -> 3146 bytes ...dd_deleted_by_to_room_file.cpython-313.pyc | Bin 1838 -> 1838 bytes ...eleted_column_to_room_file.cpython-313.pyc | Bin 1404 -> 1404 bytes .../add_trashed_file_table.cpython-313.pyc | Bin 2563 -> 2563 bytes ...add_preferred_view_to_user.cpython-313.pyc | Bin 2043 -> 2043 bytes .../bd04430cda95_merge_heads.cpython-313.pyc | Bin 739 -> 739 bytes ...d_granular_permissions_to_.cpython-313.pyc | Bin 1963 -> 1963 bytes ...dd_profile_picture_to_user.cpython-313.pyc | Bin 3711 -> 3711 bytes ...dd_last_name_to_user_model.cpython-313.pyc | Bin 1330 -> 1330 bytes ...ad_add_colorsettings_table.cpython-313.pyc | Bin 2901 -> 2901 bytes ...te_user_starred_file_table.cpython-313.pyc | Bin 2450 -> 2450 bytes ...assword_hash_column_length.cpython-313.pyc | Bin 1527 -> 1527 bytes ...5d2d3ed0_add_contact_model.cpython-313.pyc | Bin 3308 -> 3308 bytes ...age_attachments_table_and_.cpython-313.pyc | Bin 3511 -> 3511 bytes ...888_add_company_logo_field.cpython-313.pyc | Bin 1332 -> 1332 bytes models.py | 35 ++- routes/__pycache__/main.cpython-313.pyc | Bin 52214 -> 58918 bytes routes/main.py | 141 ++++++++- static/js/notifications.js | 288 ++++++++++++++++++ templates/notifications/notifications.html | 150 ++++++++- utils/__init__.py | 8 + utils/__pycache__/__init__.cpython-313.pyc | Bin 696 -> 999 bytes .../__pycache__/notification.cpython-313.pyc | Bin 0 -> 5155 bytes utils/notification.py | 91 ++++++ 43 files changed, 779 insertions(+), 6 deletions(-) create mode 100644 create_notifs_table.py create mode 100644 migrations/add_notifs_table.py create mode 100644 static/js/notifications.js create mode 100644 utils/__pycache__/notification.cpython-313.pyc create mode 100644 utils/notification.py diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 5158dca5d65ca75b9b2fe71f8de00a875d7f5e57..b8a910aa5c25b5b65f21987c38c6000e79cce9d7 100644 GIT binary patch delta 20 acmaE=^;C=dGcPX}0}vdWWVw+$N)!M_Uj_XD delta 20 ZcmaE=^;C=dGcPX}0|LMgq4l|pGs?}u-9k*Iud}FU;j@1|0 zY1V-PSfpR^F4^uekozazB!lO?Xr0WNYqQ^+`x~KqWU0%dki@@ab6s|XHc3Tyo8}(a z0d9i*mSjI;Qj)j27pVvAt#)G(+9Csv4wWSS9UM(WrE-#=9Dkw$;e96+TR?7+UwhWk zR(8qxn_RI2%nrHQ{{z~Q>Rj~f7NLKL&wB?olM3HS)IwT)euV{5orO&Mem251N^M7j z#?pBB0$Fo*PK}jY)di(LnTlTuy)z-iRWx7FM558;l|+hPfk*cHkwRK4hUF`q~giMq0|WM z6XcPl>R57I5XH!tfW!28{8pAkdUaD8htnAd`$N1l=yrtMt^zyg;JD#H(8UMcoI6xF z61d!6Hwcc1@Ssa_sDgojkN5b8JRx^M`&d9L_yR-JqICxRL+&8w2zdhjL!}YO(@(HR zma1bGpPS$^5XOKS>9*1tDDk+ysjbb&r36a2MR`OPQRQtwB_VA zG%%i_zxg_QPHxH@pV^e_aXa?23^0dn)Fwa{lFSbxcn#ot{=c}%p7EC$4b=i<{ed}HIX1dp^ z)?z;ibcEky#y|8WqX0TF0N%tNTFdvWMejrN#JOHo< z+zT2x{iDTjE5vO8rI2ofjN-seayEme4^jYdp5hAv`;~CFpWB1*v zjS7F-L$-ZJL8YGUoaww4{Pe=yg=O7$x($^*?aH$iv+d%g^xhLz&J@bFS~z4fSZc_mLbGpGOCc2HWsk>&a{WJC+sL zt#l7cx0!TtNq3cW5lJ21M>+#1%SNb)ei(5ndol1HvhFb~8b*6&7PViKL(slw)}Zs~ QYR_!{2mOy2m`lt32dF{Q`v3p{ delta 354 zcmbQViSffiM!wIyyj%=GP{(4GQDiWYPlD+c^G1zV%q;0zn%bMgSYD_wYHqgIY+__w zwfT_N0#3$VlWkNa*wzBI6s_IdXwt{XxOTIE*&Am5vmhZ5u^5PpPfS*DmYS>~oIQDh zl@H_D$rlwQH-EEw&&YUj@-rJ1Hn8HWn+0s`85u844zSbbzX;L}A~u1v?*(fwkmsDd z(QYwN4@i5Sy)+Z!jmce(rrcn~cR@PTCwp@{Pv&)c#&~n`d|z3%8z8|Olf|9SG2Ynx z%=r!r+Y^w;lgTGM_Ax#Io3p_44HIM4o3 z#PritoE#o@NDAyzuyetdg3SjTsXW;>T$alMs1%48iaR!^gtszse`Vlf3}-Ad0tx{D Dwe)XO 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 7812ee9b83203b558ddd0dbcefaf44108c8560d0..003d0651f4ec2d71efeffc90ec3d1b02c614f782 100644 GIT binary patch delta 20 acmdn4yj_|5GcPX}0}x!$u-M4GMi2lz^aX$b delta 20 acmdn4yj_|5GcPX}0}#9yGuX(zMi2l!IR%9P 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 e1e2056d9b1855a4eb8029966738548b4ae7a280..c78a72967aee4abee4c5be84b8e23ec2db712be4 100644 GIT binary patch delta 22 ccmaDT_)w7dGcPX}0}xDbwa8ewk@pG*08PXOxc~qF delta 22 ccmaDT_)w7dGcPX}0}vbyH_52n$a{qY08dQ@)Bpeg 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 53e57f750fa570a4b52e8ab64e7e14b5507ece47..c3ba359c7508852a70449a8fe7af9feae5e3851d 100644 GIT binary patch delta 22 ccmZ3$vw(;9GcPX}0}xDbwaB=-kyn@v0723Po&W#< delta 22 ccmZ3$vw(;9GcPX}0}$v)7-VeR$SceS06Y2w;Q#;t 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 5e7a28ef184a011b810af350c3a001394b8ec8bb..650458aa4bba77442e4c5af88f8b60ace95934ef 100644 GIT binary patch delta 22 ccmZ3%wStTHGcPX}0}xDbwaD<;$SchX06-uGBme*a delta 22 ccmZ3%wStTHGcPX}0}#AzG|14{$SchX07PvCl>h($ 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 7299cfb2c2795ce2e25bdca9bb97557f4ba5055c..7149de97cad1322f68081b25d62d4d2db3fc2a08 100644 GIT binary patch delta 22 ccmcc4cb$*-GcPX}0}xDbwaD18k=KhI07~8lBLDyZ delta 22 ccmcc4cb$*-GcPX}0}xnUFv#fI$m_)p082UsDgXcg 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 a8d4debe32806fb0c49eb8f8b2082da0e372c211..4e341c1e2b76652586313c5e6b13022338039d08 100644 GIT binary patch delta 22 ccmcb_e~F*>GcPX}0}xDbwaD1Bk@qM&083^Dq5uE@ delta 22 ccmcb_e~F*>GcPX}0}zOwG02#*k@qM&07^>+egFUf 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 a2a161141aa4f52440b07694b7bcdf3f036f17d8..cffd41e5eaecdb22e16ceeb0597c98cbe5c0b225 100644 GIT binary patch delta 22 ccmeyw`-zwLGcPX}0}xDbwa75s$orHH08QQoasU7T delta 22 ccmeyw`-zwLGcPX}0}w1fZIHpUk@qPZ08#7);s5{u 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 c05f61e056f07dec00718ccfda8f05c9fe04ab48..a627b59d892681713dd0ba2ef3d06d34f8f26993 100644 GIT binary patch delta 22 ccmbOzI8l)IGcPX}0}xDbwaBp9$lJmJ06q%^WB>pF delta 22 ccmbOzI8l)IGcPX}0}wn>GRP3w$lJmJ06ymhWdHyG 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 6bec1d57618b75834d8c274876d82bdb8d8ed6a1..0b51f761c8c0d7cb9f0e9fb41a73917ea7fd768b 100644 GIT binary patch delta 22 ccmaDO_C}2NGcPX}0}yn(T4b!*$Q#cM08f1eZ2$lO delta 22 ccmaDO_C}2NGcPX}0}!ay8D`XO1+M@A delta 23 dcmZ24x?Ys;GcPX}0}$kWFwD5ezL9Ss7XVPL2J`>` 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 d442bc6cc1ece60f1217e7155aeea3cf9b6c8fb7..6601fda84bbf8bef921a280d9a6b5871032691aa 100644 GIT binary patch delta 22 ccmbQjF@=NoGcPX}0}xDbwa8eqk(ZqX06r@PKL7v# delta 22 ccmbQjF@=NoGcPX}0}%XHGRSD!$jit<8 diff --git a/migrations/versions/__pycache__/76da0573e84b_merge_heads.cpython-313.pyc b/migrations/versions/__pycache__/76da0573e84b_merge_heads.cpython-313.pyc index b282136b7b33b7bb7cab4a38e3ce40fcf96d88ad..c3f2a5840f77502f909f553e18de424a7247973f 100644 GIT binary patch delta 21 bcmeBW>t*Bp%*)Hg00a|UEi$ezt*Bp%*)Hg00i658D;EW$jb}>Jd*|5 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 ec6ef61493fa2230e7c90159863506c20014be8f..10a989125e63d135751f593a708e058b47cddb5a 100644 GIT binary patch delta 22 ccmbOvHA#y1GcPX}0}xDbwa94L$jizL06+KzLI3~& delta 22 ccmbOvHA#y1GcPX}0}y0tnq>HGA28#dy 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 c51223a5b1e6062c62388698c6d72a1a52475c04..a05bed2c1880546a996a0a8a594552181b426198 100644 GIT binary patch delta 22 ccmdlYv_**bGcPX}0}xDbwa7TWkyo7)07dTw+W-In delta 22 ccmdlYv_**bGcPX}0}xElG0vE~kyo7)07h{I+5i9m diff --git a/migrations/versions/__pycache__/add_conversations_tables.cpython-313.pyc b/migrations/versions/__pycache__/add_conversations_tables.cpython-313.pyc index c046bbc61c2d866adb9c8108d5c2978f1102dee9..31a94fe58039eab067a6210ff7f1f754c7c91d43 100644 GIT binary patch delta 20 acmX>laY};wGcPX}0}yn(T5ROD=K%mbCIt2X delta 20 acmX>laY};wGcPX}0}xy=Fxkj$&jSEGcLgE< 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 0bcd5203865f58a7d4de564ca336e2ba263a676e..65b7a36b394df1b0c1c1c4c9a574b35d153f0ae8 100644 GIT binary patch delta 20 acmZ3-w~mkdGcPX}0}yn(T5RN2W(NQ^Sp' \ 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 c453e41245e670f74e7cdca8c2d95f3d59d5d782..1bbc838396a07005398b42922b2aa564b745744d 100644 GIT binary patch delta 8457 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 delta 4197 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 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 6b2462b19513b197ecff7d64a4cf0b7434c1c9de..15aee4c06b84ead6402a6d091d57d88e54fb3733 100644 GIT binary patch delta 492 zcmdnN`kbBbGcPX}0}!0iw$5;5naC%>*fLSw+0Bc$h}Vm+h|i0^h~G<~NWe?5NKkWoy0kC0tAcgVC`K5U!w}ewtb5cQesv=t|0Ts>9NkKJcvL2%^qsZhMMn|BMiUARg#&T3Kd1NXtE(wpoap`07f7#HUknLm>C%vpEEdpXJTMElAD~%B+2ZjDKvQvlZ!4_L1IZp zd}&E$PH_=8NEHu|xW!eHnVSj~Ia diff --git a/utils/__pycache__/notification.cpython-313.pyc b/utils/__pycache__/notification.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1025b40bab70b57b763fcb5268cd301deb05b2a1 GIT binary patch literal 5155 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 E0D+t*(f|Me literal 0 HcmV?d00001 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