From 9b8836183a481bae74396e3f801d13af45da4464 Mon Sep 17 00:00:00 2001 From: Kobe Date: Mon, 26 May 2025 15:11:26 +0200 Subject: [PATCH] Added messaging functions --- __pycache__/app.cpython-313.pyc | Bin 3820 -> 3945 bytes __pycache__/extensions.cpython-313.pyc | Bin 0 -> 493 bytes __pycache__/forms.cpython-313.pyc | Bin 4205 -> 5341 bytes __pycache__/models.cpython-313.pyc | Bin 13538 -> 17774 bytes app.py | 13 +- docker-compose.yml | 25 +- extensions.py | 10 + forms.py | 14 +- ..._add_file_attachment_fields_to_message_.py | 40 + ...achment_fields_to_message_.cpython-313.pyc | Bin 0 -> 2273 bytes .../add_conversations_tables.cpython-313.pyc | Bin 0 -> 3146 bytes .../bd04430cda95_merge_heads.cpython-313.pyc | Bin 0 -> 739 bytes ...age_attachments_table_and_.cpython-313.pyc | Bin 0 -> 3511 bytes .../versions/add_conversations_tables.py | 57 ++ .../versions/bd04430cda95_merge_heads.py | 24 + ...1f7a_add_message_attachments_table_and_.py | 52 ++ models.py | 56 +- requirements.txt | 3 +- routes/__init__.py | 2 + routes/__pycache__/__init__.cpython-313.pyc | Bin 1838 -> 1965 bytes .../__pycache__/conversations.cpython-313.pyc | Bin 0 -> 21692 bytes routes/__pycache__/main.cpython-313.pyc | Bin 33045 -> 33384 bytes routes/conversations.py | 390 ++++++++++ routes/main.py | 5 + templates/common/base.html | 7 +- templates/conversations/conversation.html | 685 ++++++++++++++++++ templates/conversations/conversations.html | 154 ++++ .../conversations/create_conversation.html | 238 ++++++ .../13/20250526_145757_Kobe_Amerijckx.docx | Bin 0 -> 15545 bytes uploads/15/20250526_150145_logo-light.png | Bin 0 -> 2874 bytes uploads/15/20250526_150153_logo-light.png | Bin 0 -> 2874 bytes .../15/20250526_150153_logo-placeholder.png | Bin 0 -> 3809 bytes 32 files changed, 1744 insertions(+), 31 deletions(-) create mode 100644 __pycache__/extensions.cpython-313.pyc create mode 100644 extensions.py create mode 100644 migrations/versions/0f48943140fa_add_file_attachment_fields_to_message_.py create mode 100644 migrations/versions/__pycache__/0f48943140fa_add_file_attachment_fields_to_message_.cpython-313.pyc create mode 100644 migrations/versions/__pycache__/add_conversations_tables.cpython-313.pyc create mode 100644 migrations/versions/__pycache__/bd04430cda95_merge_heads.cpython-313.pyc create mode 100644 migrations/versions/__pycache__/e7e4ff171f7a_add_message_attachments_table_and_.cpython-313.pyc create mode 100644 migrations/versions/add_conversations_tables.py create mode 100644 migrations/versions/bd04430cda95_merge_heads.py create mode 100644 migrations/versions/e7e4ff171f7a_add_message_attachments_table_and_.py create mode 100644 routes/__pycache__/conversations.cpython-313.pyc create mode 100644 routes/conversations.py create mode 100644 templates/conversations/conversation.html create mode 100644 templates/conversations/conversations.html create mode 100644 templates/conversations/create_conversation.html create mode 100644 uploads/13/20250526_145757_Kobe_Amerijckx.docx create mode 100644 uploads/15/20250526_150145_logo-light.png create mode 100644 uploads/15/20250526_150153_logo-light.png create mode 100644 uploads/15/20250526_150153_logo-placeholder.png diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 734a7cd463608aeb6b2f66c3e4434d5cf7218fbd..c34c5369cf26b895fbddb5cb9b95719bde6f1d3c 100644 GIT binary patch delta 1366 zcmY*Z&2Jk;6rahCcWv*k9mnwpPW+WZf|Jy(;zXnfC_&J}Zh}y(03sHaVb0_Q8aB-sc=CmQbRa#VrCpGVI)8Q<~MKN_w38` zpP8u>(+ncM7B}X%|L`O9yVwkt?-ZQO-#EwIp+cWHhm4u|^O#3w}1u1I`#;kD| zx6)8Z=TJqRKntfqakWqiL8g*9;~-nft}fBzp(u^ucqw0*C`aD@9HH6>LY4IDwLxNM z{}ZT^EsvC=SB7$Ei)<14DJ~`SmRsAbZn|BkbK~OzW@OECpn&N^+<3P@*wE$%Gh6Of zvr}!?I<+ka7^!>EV1Cc7?>OD2TR(Gvkf@@V;QHu5t)&mp zdXgW`;FM4yYiM;`Ci9D8m_ujks~O3@RE)e*Hp?MiV}FO_&>ERQYiI%YjqyENy+D`9 z6c#c{zb2y`-6P{hIb0qV8J{Xd6Vn6#|GPX(u@=Rbq_YzY9_aC#bMkavoIWV} z@)-N+Cu$47PJdRDI75F`dsw5(+9b|WPrHZ<^m}cB^N+ND(7eNXDNL z|J{|_EBhA?N&Fcxo{-o9iS5@9Ns31FB>sm^>p${p9q13=ND2qWF1S2Er@Gs$dj%g5 zPQsiJB_ZbNPevvoL($J^f*AP^=wHTVZYyIZd3WA2C->C;Y=g)`8ewYN-E>+WXq;i8 zMyuxSRBv`0MKRaFzYB;(!0mM%uL!Q&u8K%J5H)7{Dc|F7q+jOfdCj)d-rz-*tzbs~ zWUttTnzt=G!+c%t%L6eIFe?oDdfg_EM7AgBe8nH3*K|7`$mWZdpSD>bQEIpAwxL-D@l;+29ievW33Q2q#Io}$1JN*tj#={KR+ u=ySCA7%d*F2pf0IzsU>^AAdoA2+hTfyUVwi_oMeed`$AM6hu;Te*Xhj^gD?F delta 1401 zcmYjRO>7%Q6rQnny=(8T9oMm)IQ~!bTstSK`I z$t>ELQS)g1G$3XYOdv?@q|P`<@1%<_k%yiLiQ-r|yOS%0pTC8WJ%$j|+~gYVLMgI= zv#23AWb!^b` zn6BC*U*Y;hb(AMQ%k1;A@8n<4W6QIFUyO2=k1|@vL)7t<0%cE28?F%Ao*bcbHr_HS zCP+@o$Wqx;Oo|JAAnEIJ$tVS_kg0ZjSycAr&-rC*RLqThoz2^M^0^ejIr6DA$K*Fc z4y0s$6p+|>>1@LY6&J;^@5!-gWqWDFvO|_f_-rn&aNEaU$T^HP^1ZKxr^!`i0;hvJ^6JF?d&bJ!p)EC$#`Yq==u-^sZZ`D0u;>e5f20lcW2Pii{7Y8W)4EYby z)FE0QpoIa-4A2#!$~;5*08JkI5Y|67{*f->(D5yj3$4VIPc}c=d~oq#^Cu~Oy0O;3hQTds^%a{x<@HAU%y0X3hKf5y@A~)>dBlQ9l1-&71{_7%4@2H# z)iTG1>KEs^dAU#vebFq7$>;`0O=ZdSOuP^MX$HoRm(ElU<1jR0W#;ZhwL&1UhGr}# zsuh`2#zJM@SL-OFw$7-bI;Yl{YN zi$bR_?fn+7c0+c6i^{G32gH^-zBNL~2v$e1F@m)btpAu`rl08`F-$MLEDTaZEA#v~ R(1%X;E&Ji*pKQ!1+b`?ghGGB! literal 0 HcmV?d00001 diff --git a/__pycache__/forms.cpython-313.pyc b/__pycache__/forms.cpython-313.pyc index faa792065df9fe5e7b01d412bf8eb379202fe070..62aa08cdad1a82bf71099e701f6768ab4ea39132 100644 GIT binary patch delta 1785 zcma)6O>7fK6rS$UeMj%`8`C~;^JY(OCuA%Z{wk&q&%iNTIrD$=IL*@P{&7v4HV zaVj~ds_mhGmR3E2RH;N9nkrI%ZmrY7doU1jV;Q4hB&O7o6k!}wZ=eE2>rd_>&pgJ|Vx0T|7L~w}U zFhL_h)Y+kI7rV*qb%qqZ1=Bo+Gn*qE18=h1;FEL$pBYjrsvH^CEPE2Z8 zquX@-!<4vAvHyKT@CLbqLq_qOQ3V@?B<3vx@7_AeWt+p;(Y-d-bCST{7bPw4iY@(t}~OlAp; zR}!);O?vvzD3D;iF^PZG|D0gDe|SXaFoX`5Cv>F>2hjvNwJVK{nI*G4HU@MffIBeQ zLk=NVD7Zdz+?A$m13dYhBtBTA8wcAB=FeMZ-mvumP&K*!98B7-cwyT6@~OK1NpcAs zbM+cHM}JmZHa-o#b?37zdeLu}5T4Aj2abm zvwtOVqE=1K)Y>W^_^&7*D)*$Ve=oOHYQrlFaH=MW?Qp)@ib;X~d14fnLIU4wAf`M0oN(Ta}xxUYm@w3?I(BLv<_slRkRZ!oTO7z0lgj3Z#9hbq;kZuW=3 kIe*C7er3h`tmqtzyx9Dh34GV%u*7Hi)tNsTeZ4sU0Pv8JegFUf delta 763 zcmY*W&ubGw6rSlO+w9J6(loI@BH~iPW)VuLL}?XmZ5nhV5fcwRh)Ya1b#ar`x0^~u zsQ-b)1y5e|=D|}=UIan#;8A*6DBgq~1i|Xrd6R=WhmY@l@6CMgy*V(y8|k0At`dH} zeJSy{*mF4v~5 z9OSG# zO*)tmeICBx6>liakG@DBCrW7^#mR>Q2Xw2LB_T61LE#$5XK?sY2xBWps1by^L zekD;ea@AsSi6`JZW+z}LR&2Y?JpQn^CpGMJPIg>wXsDGv$@fGtwiOr;L(x( zc5ephEJA@p%qmXM2KPbe`hE9^UxyQ1e#|z&X@!o_@ckaFbER&k;R@c`K&T*WBH&ms zo`ADB(Qow}NTR4=%sPLche@s&P{-sA1bi1nYhou23>(o}=DH?CjO*rX^dYx=EuMZ9 zcKOEzOFJ#k?X*H4g0Zbw8N;h+2s(#Y?)e?Z3qX7%>*BB5p1a+Um2B;kt>|_Bl=_QEv?#{-0~^YyLI3~& diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index e5b1d991a9528abae1a4728957acd1d2783d1990..1d1dbb43fff2ece0c1ba12e3c7a9eda5862f481e 100644 GIT binary patch literal 17774 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 LTRw-f{EYoCW=+_O literal 13538 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 diff --git a/app.py b/app.py index 4244b0e..335dbc9 100644 --- a/app.py +++ b/app.py @@ -1,10 +1,9 @@ from flask import Flask, send_from_directory from flask_migrate import Migrate -from flask_login import LoginManager from dotenv import load_dotenv import os -from models import db, User -from flask_wtf.csrf import CSRFProtect, generate_csrf +from models import User +from flask_wtf.csrf import generate_csrf from routes.room_files import room_files_bp from routes.user import user_bp from routes.room_members import room_members_bp @@ -12,6 +11,7 @@ from routes.trash import trash_bp from tasks import cleanup_trash import click from utils import timeago +from extensions import db, login_manager, csrf, socketio # Load environment variables load_dotenv() @@ -28,9 +28,10 @@ def create_app(): # Initialize extensions db.init_app(app) migrate = Migrate(app, db) - login_manager = LoginManager(app) + login_manager.init_app(app) login_manager.login_view = 'auth.login' - csrf = CSRFProtect(app) + csrf.init_app(app) + socketio.init_app(app) @app.context_processor def inject_csrf_token(): @@ -67,4 +68,4 @@ def profile_pic(filename): return send_from_directory(os.path.join(os.getcwd(), 'uploads', 'profile_pics'), filename) if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file + socketio.run(app, debug=True) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7d5133a..426cd59 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,36 +4,25 @@ services: web: build: . ports: - - "10334:5000" + - "5000:5000" + volumes: + - ./uploads:/app/uploads environment: - - DATABASE_URL=postgresql://postgres:postgres@db:5432/flask_app - FLASK_APP=app.py - FLASK_ENV=development + - UPLOAD_FOLDER=/app/uploads depends_on: - db - volumes: - - .:/app - - room_data:/data - networks: - - app-network db: - image: postgres:15 + image: postgres:13 environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - - POSTGRES_DB=flask_app - ports: - - "5432:5432" + - POSTGRES_DB=docupulse volumes: - postgres_data:/var/lib/postgresql/data - networks: - - app-network volumes: postgres_data: - room_data: - -networks: - app-network: - driver: bridge \ No newline at end of file + uploads: \ No newline at end of file diff --git a/extensions.py b/extensions.py new file mode 100644 index 0000000..6396315 --- /dev/null +++ b/extensions.py @@ -0,0 +1,10 @@ +from flask_socketio import SocketIO +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_wtf.csrf import CSRFProtect + +# Initialize extensions +db = SQLAlchemy() +login_manager = LoginManager() +csrf = CSRFProtect() +socketio = SocketIO(cors_allowed_origins="*") \ No newline at end of file diff --git a/forms.py b/forms.py index f263117..652057a 100644 --- a/forms.py +++ b/forms.py @@ -1,5 +1,5 @@ from flask_wtf import FlaskForm -from wtforms import StringField, TextAreaField, BooleanField, SubmitField, PasswordField +from wtforms import StringField, TextAreaField, BooleanField, SubmitField, PasswordField, SelectMultipleField from wtforms.validators import DataRequired, Email, Length, Optional, ValidationError from models import User from flask_login import current_user @@ -47,4 +47,14 @@ class UserForm(FlaskForm): class RoomForm(FlaskForm): name = StringField('Room Name', validators=[DataRequired(), Length(min=3, max=100)]) description = TextAreaField('Description', validators=[Optional(), Length(max=500)]) - submit = SubmitField('Create Room') \ No newline at end of file + submit = SubmitField('Create Room') + +class ConversationForm(FlaskForm): + name = StringField('Name', validators=[DataRequired(), Length(min=1, max=100)]) + description = TextAreaField('Description') + members = SelectMultipleField('Members', coerce=int) + submit = SubmitField('Create Conversation') + + def __init__(self, *args, **kwargs): + super(ConversationForm, self).__init__(*args, **kwargs) + self.members.choices = [(u.id, f"{u.username} {u.last_name}") for u in User.query.filter_by(is_active=True).all()] \ No newline at end of file diff --git a/migrations/versions/0f48943140fa_add_file_attachment_fields_to_message_.py b/migrations/versions/0f48943140fa_add_file_attachment_fields_to_message_.py new file mode 100644 index 0000000..eccd880 --- /dev/null +++ b/migrations/versions/0f48943140fa_add_file_attachment_fields_to_message_.py @@ -0,0 +1,40 @@ +"""add file attachment fields to message model + +Revision ID: 0f48943140fa +Revises: bd04430cda95 +Create Date: 2025-05-26 14:00:05.521776 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0f48943140fa' +down_revision = 'bd04430cda95' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('message', schema=None) as batch_op: + batch_op.add_column(sa.Column('has_attachment', sa.Boolean(), nullable=True)) + batch_op.add_column(sa.Column('attachment_name', sa.String(length=255), nullable=True)) + batch_op.add_column(sa.Column('attachment_path', sa.String(length=512), nullable=True)) + batch_op.add_column(sa.Column('attachment_type', sa.String(length=100), nullable=True)) + batch_op.add_column(sa.Column('attachment_size', sa.Integer(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('message', schema=None) as batch_op: + batch_op.drop_column('attachment_size') + batch_op.drop_column('attachment_type') + batch_op.drop_column('attachment_path') + batch_op.drop_column('attachment_name') + batch_op.drop_column('has_attachment') + + # ### end Alembic commands ### 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 new file mode 100644 index 0000000000000000000000000000000000000000..e1e2056d9b1855a4eb8029966738548b4ae7a280 GIT binary patch literal 2273 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 ChB6cY literal 0 HcmV?d00001 diff --git a/migrations/versions/__pycache__/bd04430cda95_merge_heads.cpython-313.pyc b/migrations/versions/__pycache__/bd04430cda95_merge_heads.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64b48ccfe4254b2cc2903607d7c5261845785400 GIT binary patch literal 739 zcmaJ;OK;Oa5Z<-(aNHP*KuXnNxHK1tR1g+7ORKeaNuv72ZcUpa2KP z7{4-v;cD9)o1zj_L^Y_2T40Jgs1?+Xk!S>UKzm}gtxwC8iwSq9oHA+KZ~5&^&hpHC zIr80zdHsIRiy6HbY$IIy?)%R0yf^6eh9J*f${30B?3N2j=a4ALoJI+k_Lbmt&fO7! z?{>ZJ;KUo8bkE#Q$M5ugZ*Y3ny*P)u_BTzWjg^rX*Q&YSvu&v6em8aWuWs>f8RY?+ zLMG^@0oU#RBH=H@5k&Q_Wf$PV#29}!YPj d=JC>6A6_n<_2DxB=gHFkQNy^gu>eXEzW_}HydnSq literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..c8d43d7768227eb253107842c1fecd793d80f313 GIT binary patch literal 3511 zcmb_fU2M}<6ux$x#Bq~0lu}q*ARzIRZIrY$lor@j+QQmGis~|HBa-FDH;GGPXV(r> zd8&9}uj6MRn6y2Xw?0mQgft0lj|r3&d3)OS(l@p;G3{kL*ZFg4pk=C~oLnD&_dCbu zUVq0~4g>@Q$FI02{UMLgZ}g;cu;sz>5g6P+A|gm+M0SiBWeFRiW7n9Aa3V+CqMLX` z7xBWE7d>r=_(U&QA>|75m%fx0C6vX6A*XOiHcdH^&SK3pLZ&>G0gtAH@;OB|aTS!+ z6`T=-34BpCR9y>=4E2X_FYZYuyL!8ly>jUaHu^)6WY3Yloc!!1RZ@ zBi+#hk?4W$!=bKdew@p{ z1F$#H`RcWaN};MAgmy!D&T|#Vgr_p9{Q`n@AfWiK9=lPr>=Iof*XMGe>+`uCh>hU$ zIQZ;svNA)s1Fy*U!8TY|cD^;)gtuU8Sd-0fvjx_bTcd;R&6QgpUz3B+zp`rd%(1es z@-@~&i|&Jr7(B>C%R9}%9;}OJqfE&kEw5Y1xnG;>&e}XC0 zO*Py=s90J4QWlb}wH9m0mS<4U23CQ-GN#Z%#&RI}J>IjIw@rnE6U zGpK8ZNn}+st(LPy&B|nErFYYmY$no@oG~#eM`=)qo|6)G7`RKmgwS&a-IO6KIoKaR zXJBH)$Mh*2AHv23Q_sbR^hEw_K4ai`R!tGv1mQR07XfAuoFXkjLy;3yelCk9?tAKXG&P>gcu8bI~H# zRqa|=qJEp=L^rf4 zkKL}gns|WB{hxR~N4z(S`$k0+91-g_i0X^NMn!CJM66#^|Ay8yux7l*`te?E0FKwP z`mM34w*GHw7O_3$W)X}L+6zf5H67H@H7E2TI5`<+t$>_2byWkIr7yL_M=!Qh(?-oZ z)X;uQwoyY|IpDoX14~#8=>RCxH$qtIxNxj~W2POtT8khi~j!h1q8iv7%@$u8c z12Idm1=f~yvJbE&VI^TY&8O&}YbD+5hI6WY#@}E-_k0cY6r#Pzw}a%2+#V_H?S0Vv zuO z(!~ySfuCN5dfT9@va{9cMI&;1ubjDjrqI0iDYx%s6DZbGDpz#5z8Y#D4!Rx%h2b6E zaD(NQBt=h1lI4{%I6I{#O1)ArKFi1%+LCANb+C9+{s&?OXjP@!=@gMQ=z8#;fHMZH z2hL$lF(h5Hn+>6rzl)Z^qSC7H$KXFrK7&6X$G}JXpg}RuT@1tg!Fm|(uSUdd{TXe2 k&Le-@EdMOHV>a+C*a9Z7X;%2t)67JcJAKT4aulZh2L|b?z5oCK literal 0 HcmV?d00001 diff --git a/migrations/versions/add_conversations_tables.py b/migrations/versions/add_conversations_tables.py new file mode 100644 index 0000000..eb13bf1 --- /dev/null +++ b/migrations/versions/add_conversations_tables.py @@ -0,0 +1,57 @@ +"""Add conversations and messages tables + +Revision ID: add_conversations_tables +Revises: 2c5f57dddb78 +Create Date: 2024-03-19 10:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'add_conversations_tables' +down_revision = '2c5f57dddb78' +branch_labels = None +depends_on = None + + +def upgrade(): + # Create conversation table first + op.create_table('conversation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['created_by'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + # Create conversation_members table + op.create_table('conversation_members', + sa.Column('conversation_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['conversation_id'], ['conversation.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('conversation_id', 'user_id') + ) + + # Create message table + op.create_table('message', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('conversation_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['conversation_id'], ['conversation.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + # Drop tables in reverse order + op.drop_table('message') + op.drop_table('conversation_members') + op.drop_table('conversation') \ No newline at end of file diff --git a/migrations/versions/bd04430cda95_merge_heads.py b/migrations/versions/bd04430cda95_merge_heads.py new file mode 100644 index 0000000..1991835 --- /dev/null +++ b/migrations/versions/bd04430cda95_merge_heads.py @@ -0,0 +1,24 @@ +"""merge heads + +Revision ID: bd04430cda95 +Revises: f18735338888, add_conversations_tables +Create Date: 2025-05-26 11:14:05.629795 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bd04430cda95' +down_revision = ('f18735338888', 'add_conversations_tables') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/migrations/versions/e7e4ff171f7a_add_message_attachments_table_and_.py b/migrations/versions/e7e4ff171f7a_add_message_attachments_table_and_.py new file mode 100644 index 0000000..e3c9b6a --- /dev/null +++ b/migrations/versions/e7e4ff171f7a_add_message_attachments_table_and_.py @@ -0,0 +1,52 @@ +"""add message attachments table and update message model + +Revision ID: e7e4ff171f7a +Revises: 0f48943140fa +Create Date: 2025-05-26 15:00:18.557702 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e7e4ff171f7a' +down_revision = '0f48943140fa' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('message_attachment', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('message_id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('path', sa.String(length=512), nullable=False), + sa.Column('type', sa.String(length=100), nullable=True), + sa.Column('size', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['message_id'], ['message.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('message', schema=None) as batch_op: + batch_op.drop_column('attachment_path') + batch_op.drop_column('attachment_type') + batch_op.drop_column('has_attachment') + batch_op.drop_column('attachment_size') + batch_op.drop_column('attachment_name') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('message', schema=None) as batch_op: + batch_op.add_column(sa.Column('attachment_name', sa.VARCHAR(length=255), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('attachment_size', sa.INTEGER(), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('has_attachment', sa.BOOLEAN(), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('attachment_type', sa.VARCHAR(length=100), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('attachment_path', sa.VARCHAR(length=512), autoincrement=False, nullable=True)) + + op.drop_table('message_attachment') + # ### end Alembic commands ### diff --git a/models.py b/models.py index c711992..0c9dd28 100644 --- a/models.py +++ b/models.py @@ -3,8 +3,7 @@ from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime from sqlalchemy.orm import relationship - -db = SQLAlchemy() +from extensions import db # Association table for room members room_members = db.Table('room_members', @@ -12,6 +11,12 @@ room_members = db.Table('room_members', db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True) ) +# Association table for conversation members +conversation_members = db.Table('conversation_members', + db.Column('conversation_id', db.Integer, db.ForeignKey('conversation.id'), primary_key=True), + db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True) +) + class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(150), unique=True, nullable=False) @@ -151,4 +156,49 @@ class SiteSettings(db.Model): settings = cls() db.session.add(settings) db.session.commit() - return settings \ No newline at end of file + return settings + +class Conversation(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + description = db.Column(db.Text) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + # Relationships + creator = db.relationship('User', backref='created_conversations', foreign_keys=[created_by]) + members = db.relationship('User', secondary=conversation_members, backref=db.backref('conversations', lazy='dynamic')) + messages = db.relationship('Message', back_populates='conversation', cascade='all, delete-orphan') + + def __repr__(self): + return f'' + +class Message(db.Model): + id = db.Column(db.Integer, primary_key=True) + content = db.Column(db.Text, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + # Relationships + conversation = db.relationship('Conversation', back_populates='messages') + user = db.relationship('User', backref='messages') + attachments = db.relationship('MessageAttachment', back_populates='message', cascade='all, delete-orphan') + + def __repr__(self): + return f'' + +class MessageAttachment(db.Model): + id = db.Column(db.Integer, primary_key=True) + message_id = db.Column(db.Integer, db.ForeignKey('message.id'), nullable=False) + name = db.Column(db.String(255), nullable=False) + path = db.Column(db.String(512), nullable=False) + type = db.Column(db.String(100)) + size = db.Column(db.Integer) # Size in bytes + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + # Relationships + message = db.relationship('Message', back_populates='attachments') + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 27f7956..277bbd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ Werkzeug==3.0.1 WTForms==3.1.1 python-dotenv==1.0.1 psycopg2-binary==2.9.9 -gunicorn==21.2.0 \ No newline at end of file +gunicorn==21.2.0 +Flask-SocketIO==5.3.6 \ No newline at end of file diff --git a/routes/__init__.py b/routes/__init__.py index 97d597d..f158c49 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -13,6 +13,7 @@ def init_app(app: Flask): from .auth import init_routes as init_auth_routes from .contacts import contacts_bp as contacts_routes from .rooms import rooms_bp as rooms_routes + from .conversations import conversations_bp as conversations_routes # Initialize routes init_main_routes(main_bp) @@ -29,6 +30,7 @@ def init_app(app: Flask): app.register_blueprint(auth_bp) app.register_blueprint(rooms_routes) app.register_blueprint(contacts_routes) + app.register_blueprint(conversations_routes) @app.route('/rooms//trash') @login_required diff --git a/routes/__pycache__/__init__.cpython-313.pyc b/routes/__pycache__/__init__.cpython-313.pyc index 636a1f35bfc61b568b47b9fc40be43024f20f468..7ab3b58e1181b40804ebfc48b8738569e59cfcef 100644 GIT binary patch delta 460 zcmZ3-x0avpGcPX}0}w1SFv+N3oyd2X@!Z5qlevP~g1Njoy}0r?CMPqhs)~V>pn_m- zDF#!hP#$L}Ly+X;kBq{T4>0mEf?51bri>721qP_BB1C4g8WU7aAP0&XX;d3PykH)f zif9>_Y%njvX1EKuLK##TH2Ef%Gern!GTstM&d)1LEhX>&d5F4i#6WsmI#Vmd{KUB zNosMC6i{=KJdh~n1`-MgP$UWD-{Q#3%PfgcEGQ__19DkG>TXWH!)nQVm&JXuC|er; z4How6EK(O)q%KJ7UtuwrT+OD#C^LBln~e-e!%BuCkn5mk+2rP@l;)(`6&X)rm*-bz OtbB)c^oDS7G-6 delta 357 zcmZ3>zmAXZGcPX}0}ycOnq>TEnaFpTalyn(lP4E4Dys^C#8E*omlT63R49)llp#oT zvLlo5UOARjBhK@rH(TO65rnI-Xw1qDUgKrSmt+40GqY?hN-+0rKeV$)%i to~+JpBLk9L$xsBc0jkd?H$SB`C)KXVU~)aXJij6%{{+>q3_uF32mlNvJ`Df> diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d3995ca297e4c18e14ce2e0f3beea8d4efb8a66 GIT binary patch literal 21692 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 H$-w@91i=FL literal 0 HcmV?d00001 diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index e316fef822ce6bc63b4586886de12b619421a1fc..477f0cbbef11977f8371e4439f1c84c2a2be7c7c 100644 GIT binary patch delta 916 zcmZ9KT}V@57{}k|Y&%ouS>~MDoNf8VHmO-;BxQOLR-}SVOVfp8tSQ);o>};TF{8VJ zE}p#Ws&4v#pw3N!q!wvX=s*ZsV$lZ_Wta=98|!^^L+xDr-uLU#)hYAviK}rTW*lbfi#TS8J1~s z(67zrl>zN?O0s~xx`z%=nj1WZ3ZYfI*3Fp5VqxWpAUVng^2p0eN+KB5xYU@F>Ec!+tTnM2##%M_PH(Ms3iShd8=J$sHbqWB(Q&0B z5=sdrghGy(w5c;zfD?7*(oMM|e7Ufh_eR)Hk!9pw*GKJ@bGe7+wp2w^s<5TuGSsS{ z8g>eqkI8k6gD+^K`8XCe?uQ1|+t{TM*Qz*sGHRHhVj^}2^Ha3RV79p)+STnXD*{Yn zep{b7MQ%pyX|`l+CDDIMeBX8y2GvsUiXJ9aSD+TMNfFZr19Cv|bvZ#9C5IAnu&g`i z$?FE;CSeB0&m2TU&|!#C_=Yf#?%-|xRXTi!fV;gFYHr6j5f)Gv8iXkv4pqVz91B@t z5$8i5c!p)&w|M4tw-2V(13iV{THrQLjq;TbG4RJk-`UVPxi`$88?)OZtJkl5r(zvl zI?CZ;)LJ0FYJYv>a}pEwqV1BP|Z(B<#3wTegS}L z>nj5>p=|1Jf^qe3f4c^Zm_FoiJR-qU!gIn4!UuwxpIo`eXJI3U9Zw9|4OX7Zrj%a5 z;h{gasHt#HS2V9P{SZJ`4RH+sfUa0OoMXphWfx4cV`n;Htq_-7Mq(gAs)+FTBL} z#(s51j4cYxVj#I4Uq^kIN#24Rtek9i!5nTgFTqp1Z64UTz*Qd|93iQsq%E7K%#za{ zG}8&*c?lt-!vsb$cVGd_nK(?bKDr62=7d3}i+IzjS3dEibQGbh7EN') +@login_required +def conversation(conversation_id): + conversation = Conversation.query.get_or_404(conversation_id) + # Check if user is a member + if not current_user.is_admin and current_user not in conversation.members: + flash('You do not have access to this conversation.', 'error') + return redirect(url_for('conversations.conversations')) + + # Query messages directly using the Message model + messages = Message.query.filter_by(conversation_id=conversation_id).order_by(Message.created_at.asc()).all() + + # Get all users for member selection (only needed for admin) + all_users = User.query.all() if current_user.is_admin else None + + return render_template('conversations/conversation.html', + conversation=conversation, + messages=messages, + all_users=all_users) + +@conversations_bp.route('//members') +@login_required +def conversation_members(conversation_id): + conversation = Conversation.query.get_or_404(conversation_id) + if not current_user.is_admin and current_user not in conversation.members: + flash('You do not have access to this conversation.', 'error') + return redirect(url_for('conversations.conversations')) + + if not current_user.is_admin: + flash('Only administrators can manage conversation members.', 'error') + return redirect(url_for('conversations.conversation', conversation_id=conversation_id)) + + available_users = User.query.filter(~User.id.in_([m.id for m in conversation.members])).all() + return render_template('conversations/conversation_members.html', + conversation=conversation, + available_users=available_users) + +@conversations_bp.route('//members/add', methods=['POST']) +@login_required +def add_member(conversation_id): + conversation = Conversation.query.get_or_404(conversation_id) + if not current_user.is_admin: + flash('Only administrators can manage conversation members.', 'error') + return redirect(url_for('conversations.conversation', conversation_id=conversation_id)) + + user_id = request.form.get('user_id') + if not user_id: + flash('Please select a user to add.', 'error') + return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id)) + + user = User.query.get_or_404(user_id) + if user in conversation.members: + flash('User is already a member of this conversation.', 'error') + else: + conversation.members.append(user) + db.session.commit() + flash(f'{user.username} has been added to the conversation.', 'success') + + return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id)) + +@conversations_bp.route('//members//remove', methods=['POST']) +@login_required +def remove_member(conversation_id, user_id): + conversation = Conversation.query.get_or_404(conversation_id) + if not current_user.is_admin: + flash('Only administrators can manage conversation members.', 'error') + return redirect(url_for('conversations.conversation', conversation_id=conversation_id)) + + if user_id == conversation.created_by: + flash('Cannot remove the conversation creator.', 'error') + else: + user = User.query.get_or_404(user_id) + if user not in conversation.members: + flash('User is not a member of this conversation.', 'error') + else: + conversation.members.remove(user) + db.session.commit() + flash('User has been removed from the conversation.', 'success') + + return redirect(url_for('conversations.conversation_members', conversation_id=conversation_id)) + +@conversations_bp.route('//edit', methods=['GET', 'POST']) +@login_required +def edit_conversation(conversation_id): + if not current_user.is_admin: + flash('Only administrators can edit conversations.', 'error') + return redirect(url_for('conversations.conversations')) + conversation = Conversation.query.get_or_404(conversation_id) + form = ConversationForm(obj=conversation) + + if request.method == 'POST': + # Get members from the form data + member_ids = request.form.getlist('members') + + # Update members + current_member_ids = {str(user.id) for user in conversation.members} + new_member_ids = set(member_ids) + + # Remove members that are no longer in the list + for member_id in current_member_ids - new_member_ids: + if int(member_id) != conversation.created_by: # Don't remove the creator + user = User.query.get(member_id) + if user: + conversation.members.remove(user) + + # Add new members + for member_id in new_member_ids - current_member_ids: + user = User.query.get(member_id) + if user and user not in conversation.members: + conversation.members.append(user) + + db.session.commit() + flash('Conversation members updated successfully!', 'success') + + # Check if redirect parameter is provided + redirect_url = request.args.get('redirect') + if redirect_url: + return redirect(redirect_url) + return redirect(url_for('conversations.conversations')) + + # Prepopulate form members with current members + form.members.data = [str(user.id) for user in conversation.members] + return render_template('conversations/create_conversation.html', form=form, edit_mode=True, conversation=conversation) + +@conversations_bp.route('//delete', methods=['POST']) +@login_required +def delete_conversation(conversation_id): + if not current_user.is_admin: + flash('Only administrators can delete conversations.', 'error') + return redirect(url_for('conversations.conversations')) + + conversation = Conversation.query.get_or_404(conversation_id) + + # Delete all messages in the conversation + Message.query.filter_by(conversation_id=conversation_id).delete() + + # Delete the conversation + db.session.delete(conversation) + db.session.commit() + + flash('Conversation has been deleted successfully.', 'success') + return redirect(url_for('conversations.conversations')) + +@socketio.on('join_conversation') +@login_required +def on_join(data): + conversation_id = data.get('conversation_id') + conversation = Conversation.query.get_or_404(conversation_id) + + # Check if user is a member + if not current_user.is_admin and current_user not in conversation.members: + return + + # Join the room + join_room(f'conversation_{conversation_id}') + +@socketio.on('leave_conversation') +@login_required +def on_leave(data): + conversation_id = data.get('conversation_id') + leave_room(f'conversation_{conversation_id}') + +@conversations_bp.route('//send_message', methods=['POST']) +@login_required +def send_message(conversation_id): + conversation = Conversation.query.get_or_404(conversation_id) + + # Check if user is a member + if not current_user.is_admin and current_user not in conversation.members: + return jsonify({'success': False, 'error': 'You do not have access to this conversation.'}), 403 + + message_content = request.form.get('message', '').strip() + file_count = int(request.form.get('file_count', 0)) + + if not message_content and file_count == 0: + return jsonify({'success': False, 'error': 'Message or file is required.'}), 400 + + # Create new message + message = Message( + content=message_content, + conversation_id=conversation_id, + user_id=current_user.id + ) + + # Create conversation-specific directory + conversation_dir = os.path.join(UPLOAD_FOLDER, str(conversation_id)) + os.makedirs(conversation_dir, exist_ok=True) + + # Handle file attachments + attachments = [] + for i in range(file_count): + file = request.files.get(f'file_{i}') + if file and file.filename: + if not allowed_file(file.filename): + return jsonify({'success': False, 'error': f'File type not allowed: {file.filename}'}), 400 + + if file.content_length and file.content_length > MAX_FILE_SIZE: + return jsonify({'success': False, 'error': f'File size exceeds limit: {file.filename}'}), 400 + + # Generate unique filename + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = secure_filename(file.filename) + unique_filename = f"{timestamp}_{filename}" + file_path = os.path.join(conversation_dir, unique_filename) + + # Save file + file.save(file_path) + + # Create attachment record + attachment = MessageAttachment( + name=filename, + path=file_path, + type=get_file_extension(filename), + size=os.path.getsize(file_path) + ) + message.attachments.append(attachment) + attachments.append(attachment) + + db.session.add(message) + db.session.commit() + + # Prepare message data for WebSocket + message_data = { + 'id': message.id, + 'content': message.content, + 'created_at': message.created_at.strftime('%b %d, %Y %H:%M'), + 'sender_id': str(current_user.id), + 'sender_name': f"{current_user.username} {current_user.last_name}", + 'sender_avatar': url_for('profile_pic', filename=current_user.profile_picture) if current_user.profile_picture else url_for('static', filename='default-avatar.png'), + 'attachments': [{ + 'name': attachment.name, + 'size': attachment.size, + 'url': url_for('conversations.download_attachment', message_id=message.id, attachment_index=index) + } for index, attachment in enumerate(attachments)] + } + + # Emit the message to all users in the conversation room + socketio.emit('new_message', message_data, room=f'conversation_{conversation_id}') + + # Return minimal response since the message will be received through WebSocket + return jsonify({'success': True}) + +@conversations_bp.route('/messages//attachment/') +@login_required +def download_attachment(message_id, attachment_index): + message = Message.query.get_or_404(message_id) + conversation = message.conversation + + # Check if user is a member + if not current_user.is_admin and current_user not in conversation.members: + flash('You do not have access to this file.', 'error') + return redirect(url_for('conversations.conversation', conversation_id=conversation.id)) + + try: + attachment = message.attachments[attachment_index] + return send_file( + attachment.path, + as_attachment=True, + download_name=attachment.name + ) + except (IndexError, Exception) as e: + flash('File not found.', 'error') + return redirect(url_for('conversations.conversation', conversation_id=conversation.id)) + +@conversations_bp.route('//messages') +@login_required +def get_messages(conversation_id): + conversation = Conversation.query.get_or_404(conversation_id) + + # Check if user is a member + if not current_user.is_admin and current_user not in conversation.members: + return jsonify({'success': False, 'error': 'You do not have access to this conversation.'}), 403 + + # Get the last message ID from the request + last_message_id = request.args.get('last_message_id', type=int) + + # Query for new messages + query = Message.query.filter_by(conversation_id=conversation_id) + if last_message_id: + query = query.filter(Message.id > last_message_id) + + messages = query.order_by(Message.created_at.asc()).all() + + # Format messages for response + formatted_messages = [] + for message in messages: + formatted_messages.append({ + 'id': message.id, + 'content': message.content, + 'created_at': message.created_at.strftime('%b %d, %Y %H:%M'), + 'sender_id': str(message.user.id), + 'sender_name': f"{message.user.username} {message.user.last_name}", + 'sender_avatar': url_for('profile_pic', filename=message.user.profile_picture) if message.user.profile_picture else url_for('static', filename='default-avatar.png'), + 'attachments': [{ + 'name': attachment.name, + 'size': attachment.size, + 'url': url_for('conversations.download_attachment', message_id=message.id, attachment_index=index) + } for index, attachment in enumerate(message.attachments)] + }) + + return jsonify({'success': True, 'messages': formatted_messages}) \ No newline at end of file diff --git a/routes/main.py b/routes/main.py index 4032f9e..240863e 100644 --- a/routes/main.py +++ b/routes/main.py @@ -324,6 +324,11 @@ def init_routes(main_bp): def starred(): return render_template('starred/starred.html') + @main_bp.route('/conversations') + @login_required + def conversations(): + return redirect(url_for('conversations.conversations')) + @main_bp.route('/trash') @login_required def trash(): diff --git a/templates/common/base.html b/templates/common/base.html index a461988..7f1e917 100644 --- a/templates/common/base.html +++ b/templates/common/base.html @@ -25,7 +25,7 @@ style="height: 40px; margin-right: 10px;"> {% endif %} {% if site_settings.company_name %} - DocuPulse for {% if site_settings.company_website %}{{ site_settings.company_name }}{% else %}{{ site_settings.company_name }}{% endif %} + DocuPulse for:{% if site_settings.company_website %}{{ site_settings.company_name }}{% else %}{{ site_settings.company_name }}{% endif %} {% else %} DocuPulse {% endif %} @@ -85,6 +85,11 @@ Rooms +