From 33f6e0386bcf159d4c0c88b8e029cdac8b7b6fdf Mon Sep 17 00:00:00 2001 From: Kobe Date: Thu, 5 Jun 2025 14:43:06 +0200 Subject: [PATCH] added manager user type --- __pycache__/app.cpython-313.pyc | Bin 7960 -> 8214 bytes __pycache__/forms.cpython-313.pyc | Bin 6574 -> 6981 bytes __pycache__/models.cpython-313.pyc | Bin 33189 -> 34167 bytes forms.py | 6 ++ .../versions/72ab6c4c6a5f_merge_heads.py | 24 +++++ .../versions/add_docupulse_settings_table.py | 3 +- migrations/versions/add_manager_role.py | 38 ++++++++ models.py | 35 +++++++- routes/__pycache__/contacts.cpython-313.pyc | Bin 24668 -> 25169 bytes .../__pycache__/conversations.cpython-313.pyc | Bin 29378 -> 29627 bytes routes/__pycache__/main.cpython-313.pyc | Bin 75991 -> 77952 bytes routes/__pycache__/rooms.cpython-313.pyc | Bin 21266 -> 20970 bytes routes/contacts.py | 21 +++-- routes/conversations.py | 12 +-- routes/main.py | 35 ++++++-- routes/rooms.py | 83 +++++++----------- static/js/rooms/viewManager.js | 52 ++++------- templates/contacts/form.html | 16 ++-- templates/contacts/list.html | 7 +- templates/conversations/conversation.html | 4 +- templates/conversations/conversations.html | 4 +- templates/dashboard/dashboard.html | 2 +- templates/rooms/room.html | 8 +- templates/rooms/rooms.html | 4 +- 24 files changed, 226 insertions(+), 128 deletions(-) create mode 100644 migrations/versions/72ab6c4c6a5f_merge_heads.py create mode 100644 migrations/versions/add_manager_role.py diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 21fb7fe8337e07909721424c415bd22d68abc5d6..4236a75183d4d9a540649f040ed80cf092434032 100644 GIT binary patch delta 1030 zcmYjPUu;ul6z{pWw`+TM+hVtN-TLQlSlku1c64mv#&czZ_n;@GMTiR4rTQx^lVli&W&Xc{Gy)Fr0jw48=+T$U%H6aSGffQIK9hoBFCZ;UuPNmKy*#npr{B@YQaro>3_nbHN1 zP*bAK}07R;b%uejN_u+x{4pUiJ?N@Dl#qTz)peQ@LKA znWYN3BX6)IS5I(wfkzrEW;IvNS1Jo7Huaz@wv#JT1R7XUerj%}Sm8HAr9z(uQg8%+ z2z*3iDFw%%3$F!L7{}j&*WnOeY6-l^=g#;IVS70Ya(IrzIEP6NuW>LqY-~;@OTGUL zFpYf6U0`OuYUb!r*t8qT?_R#z3b+zMj*IC#3u6qB-4K=egbUbq`b7FDL9$DA6 zUk+Ui)l(ziK6XRP-i_3~@iqIl+Lxj4AhF_uQB_=dD3;kHtn6`*Mx<5AGuk4p`X%C5 zTO`gzL~g6@(FA_c_G5pMDzh?&*9k1gD`vH@FvWKBhBn8-dAZC=G-)$eo++^Rh&hg5 zhm$m>Tj2>PVkYtyOyKp%{4Tzmjg7MfZgDA_Bd|m^Uu6F~Rn9s0&XuO-XU%==G*+YI Rj#&Wj?Za4)4%_+B{|Aa00U!VX delta 890 zcmYjOOHUI~6z-WaZJBn4R(X_X>!Zc;5D*9u6>G!?O^o75L0Mp>BNS+9Zc9L70*TQT zadQko6PFs%1SPlQ0nhVaiYd=&~G0~~1 z3l6R9TGAFJxVXG3%>u6HOoECR^M^!yUju&w!DpeZ5i1$rS0OPje!cjKbPw|SSUHJc$Oy_h_IEOqvhEgiiSTdghj zvh8;{PFasb1-`Q00}qC6=fI1PY@hLE?ngL)vx*yh%MXKDfzs+)ul=H{rroM!H)3>(8h3#p^HIh=w%pUIM1-rt(`3I_6Z66o+WBQ`x-L>Jb;@iv5OWr|U#s(p`Iiq+w8>LL65-N~0N41bs zUbD-@uQ_F6)(XsQyOmHSJ}p~s8zDInXGjoaErUjSU?M77ILl`9%-fSVS=n#j3>l@yJOCFBt8}$A2r&= V912g^G7OOSy%*nmI&yfye*pxC(=`A9 diff --git a/__pycache__/forms.cpython-313.pyc b/__pycache__/forms.cpython-313.pyc index ebfddfa6866ce6bd351805af8b5d9d1babf7f6e8..beb9363e5bb74bdcbb392f1401ddf47650a5b097 100644 GIT binary patch delta 1051 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 delta 3895 zcmaJ^dr*|u72o@@EH4*fSzuw2hbXu~kf4F6*paF@MnRd?1Q8|sVp(1b_gi2HC^jUT z=%C_>Nz;hL*d``6Rr5`nR#O|3J~DkwYnzVgrk&0-|HNrJaYj4Qj+ylS?gz4(rH=dK z+5L_l6=E#PBe&rSKDgZ{Pp9>HfB6u92;x&$2*p5m9dG8UD9ey zrx{&*tb#%*Wi%3RA*!gL-?}VxwIYXtaT!wnOMDJp4y4f$R{}Yg=1^BcHZ>&Z`nhfk z%c9OL2CyeZIk&>a;l1oYv(rJja=yZQ%2q83J>inF8~iHlD9lCJr-JLR!)p zRz@vzPO%3m#$3V*Xo-10y>I?KbJLOJm*{p;vQ{_O9K)8;4$E`QL)R_GSPktIpXs`8a)}-xCKdi8IlwvEb*a_V(0kwP~#0u;wrZ;ie!^HM?M=_p<`y1c1?^BBfQlzWOa(;vyqPX6s8+fS)X?8dTNhRsCxrpI z(lY4fg;}hhE-Wma$_y(qgGDe*!BjAhOb4ngs*0Ke6EFm{)4`(evk9%d_^TK;NM$9t zYn!2)57-aL0Tcr6-7U{UAMzn=4sn;-Cfp(eBvQpgM@ov!2;=Jt_rlIYACye6F>)=o znq(RuqM^mB*a72c#dKAq<^BUBELGA6*V@!F?Kuoo1-X#M@Hm3b;=^Knq(*B1wPwhT$`lynr@?F+I zA5@mylUuhd=PkO7DWj!~1_fYD{M2Dl9I~~>st%@T`l5e-_yA+4scpql_KtRZg_p6b z#HyEOU%iGpGOxl+ z5#UJRe}x+~?$v{9J&b)yU#v@ydq?Q%Ju!48$*SemnprA58&1$E z;j~|*RqHEFa`(4raD6SiroFd5OR;<@>>SBl2I97mh2g(8{(T|0(uWyX6}DOe=3#pwyd1Um#V0PC1gT72_f( zPfx>P&WF&H_v0c+F2EUj#d(s=qqSS7Z*bdmSr`G+aGWP7(z!b{JKHqOBa6ADoQ$O; z?_!Kka)m-JPg|Fm;{Ol^Cl0_y)!&JM`wWz^?T^fSRLy$pQM+PF6N*4S6*OipkkLiC z4;VxZ;00J;mUcOdO&E%McD!c|O*UrFMD@>&cD9(lXq@JWcYu7UbRcHo#HpyX1iat4 zcCo-XAX>yFX)6|;HJsQx2jtV6+keB-sLPi{<4q^n60Lm49mX29(w%8a)N3ynLjg-@ zSIY$@m(h39ZCb_SwTh!j^oC8oet1?-g=-aJlT-N+kp*kGveVfKr1~i@$8uJ ztkCmY-pTXw+*nDbuZ-n=R6SJgEib(r*4eo>V;Vpcjd}keUP@g)4{N6@zVlOcGgPHio)05K40hiCLL~n znYpOEBgg0ww(@wlcFbkZ2>O{c)`25_zQeA&z%(jMIH&6!R?q$Pq1k{uF>;^F%YA6K z0%Z8_`bAtLzHadfo83QqFbMF3=I=BzWse@wr@Kxl>>w@ZDae!Q{bQt?OllPR$TLme zp5o~;Hr}&+Cc%xpne0W%>Ai>32%awCIJM8+?(>B7r?}A_^bSVNc+Cr+@clnsj=U*uOGKTA#uEduU7lS@y%D z17(o`sXB(~4gpY$_+d!T1MUkCO0OU(PC|=I+*&j_P#HD5#xWLfT)Qz~XUe$l{6&MY z3Oh?>Lq&6TVjcZ+qM05ax_fR$hhAly=z-x{c9D9Af5|RU)$XbLL4VWg4?uZz%8bsq} TwYL7L0+#q6Lke3azyAIQMmC!! diff --git a/forms.py b/forms.py index 5e05847..77bcf0b 100644 --- a/forms.py +++ b/forms.py @@ -14,6 +14,7 @@ class UserForm(FlaskForm): position = StringField('Position (Optional)', validators=[Optional(), Length(max=100)]) notes = TextAreaField('Notes (Optional)', validators=[Optional()]) is_admin = BooleanField('Admin Role', default=False) + is_manager = BooleanField('Manager Role', default=False) new_password = PasswordField('New Password (Optional)') confirm_password = PasswordField('Confirm Password (Optional)') profile_picture = FileField('Profile Picture (Optional)', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')]) @@ -30,6 +31,11 @@ class UserForm(FlaskForm): if total_admins <= 1: raise ValidationError('There must be at least one admin user in the system.') + def validate_is_manager(self, field): + # Prevent setting both admin and manager roles + if field.data and self.is_admin.data: + raise ValidationError('A user cannot be both an admin and a manager.') + def validate(self, extra_validators=None): rv = super().validate(extra_validators=extra_validators) if not rv: diff --git a/migrations/versions/72ab6c4c6a5f_merge_heads.py b/migrations/versions/72ab6c4c6a5f_merge_heads.py new file mode 100644 index 0000000..dd9691f --- /dev/null +++ b/migrations/versions/72ab6c4c6a5f_merge_heads.py @@ -0,0 +1,24 @@ +"""merge heads + +Revision ID: 72ab6c4c6a5f +Revises: 0a8006bd1732, add_docupulse_settings, add_manager_role, make_events_user_id_nullable +Create Date: 2025-06-05 14:21:46.046125 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '72ab6c4c6a5f' +down_revision = ('0a8006bd1732', 'add_docupulse_settings', 'add_manager_role', 'make_events_user_id_nullable') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/migrations/versions/add_docupulse_settings_table.py b/migrations/versions/add_docupulse_settings_table.py index dadf320..cbc5720 100644 --- a/migrations/versions/add_docupulse_settings_table.py +++ b/migrations/versions/add_docupulse_settings_table.py @@ -8,6 +8,7 @@ Create Date: 2024-03-19 10:00:00.000000 from alembic import op import sqlalchemy as sa from datetime import datetime +from sqlalchemy import text # revision identifiers, used by Alembic. revision = 'add_docupulse_settings' @@ -28,7 +29,7 @@ def upgrade(): server_default='10737418240') # Check if we need to insert default data - result = conn.execute("SELECT COUNT(*) FROM docupulse_settings").scalar() + result = conn.execute(text("SELECT COUNT(*) FROM docupulse_settings")).scalar() if result == 0: conn.execute(""" INSERT INTO docupulse_settings (id, max_rooms, max_conversations, max_storage, updated_at) diff --git a/migrations/versions/add_manager_role.py b/migrations/versions/add_manager_role.py new file mode 100644 index 0000000..9faf47a --- /dev/null +++ b/migrations/versions/add_manager_role.py @@ -0,0 +1,38 @@ +"""Add manager role + +Revision ID: add_manager_role +Revises: 25da158dd705 +Create Date: 2024-03-20 10:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect + + +# revision identifiers, used by Alembic. +revision = 'add_manager_role' +down_revision = '25da158dd705' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + conn = op.get_bind() + inspector = inspect(conn) + columns = [col['name'] for col in inspector.get_columns('user')] + + with op.batch_alter_table('user', schema=None) as batch_op: + if 'is_manager' not in columns: + batch_op.add_column(sa.Column('is_manager', sa.Boolean(), nullable=True, server_default='false')) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_column('is_manager') + + # ### end Alembic commands ### \ No newline at end of file diff --git a/models.py b/models.py index 012a1b4..9d154b4 100644 --- a/models.py +++ b/models.py @@ -26,6 +26,7 @@ class User(UserMixin, db.Model): email = db.Column(db.String(150), unique=True, nullable=False) password_hash = db.Column(db.String(256)) is_admin = db.Column(db.Boolean, default=False) + is_manager = db.Column(db.Boolean, default=False) # New field for manager role created_at = db.Column(db.DateTime, default=datetime.utcnow) phone = db.Column(db.String(20)) company = db.Column(db.String(100)) @@ -444,4 +445,36 @@ class PasswordSetupToken(db.Model): return not self.used and datetime.utcnow() < self.expires_at def __repr__(self): - return f'' \ No newline at end of file + return f'' + +def user_has_permission(room, perm_name): + """ + Check if the current user has a specific permission in a room. + + Args: + room: Room object + perm_name: Name of the permission to check (e.g., 'can_view', 'can_upload') + + Returns: + bool: True if user has permission, False otherwise + """ + # Admin and manager users have all permissions + if current_user.is_admin or current_user.is_manager: + return True + + # Check if user is a member of the room + if current_user not in room.members: + return False + + # Get user's permissions for this room + permission = RoomMemberPermission.query.filter_by( + room_id=room.id, + user_id=current_user.id + ).first() + + # If no specific permissions are set, user only has view access + if not permission: + return perm_name == 'can_view' + + # Check the specific permission + return getattr(permission, perm_name, False) \ No newline at end of file diff --git a/routes/__pycache__/contacts.cpython-313.pyc b/routes/__pycache__/contacts.cpython-313.pyc index 33a378db36a8f2b1eb3f639be869a5dd21a132cc..7bea8e67051de3fbb5d499ab66080ed5c08f4a46 100644 GIT binary patch delta 3929 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 XCX+F6YkqNzkQBL-GZ*gDy2<|lc)1yy diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index 8e064bcf064311c9025271e864104bac4c532219..736d4e409236a385a0279fa88525e0aad04ad87f 100644 GIT binary patch delta 2553 zcmb7`3v5$W7=U~Fe(bj0Ztoslw{~3_D;w;^P;e`5Kmc(VHb%&b8Qpq;nzma`J8(n- z@`#ua5PyV76amp?7+FS#1VsTu5F#2h7_<^mA~9+r5Ee8j==o3g%7P{~w_ncr|9k%P zzs_ktzCk|tiJ0#Qf>FUft9R6`Y7O?A`vr0YP79@$YK6j&3jS$S!xw^+=-{@HWikv) z{U-QU%p(F+m?fy3FG8nY?=8|+}R2^ILxk}}qI&7$M8bm7c$aLtp+MzZ>G(9$~X)>Xek?qlqF-EjiDsI1jnFzYvOi~Lc zeEHxMO)Q0(@VID&jYbjf`vm9|(}toR3bKertsB|J++?X%re;h&zOXZ)qE<|WZ!?iP zD6zSmGoFfuyIp}`BoqzB;xrKNpfOh<8gxYh(ZEVM@wDw4$ts{}%qZ<(NE$=2)|8GW z=4N`xCtjg>9;{mt^-P}64bhj zN9w%AZG+Y2xoUnmDszj#X)~zQ{sQ>L?V0i}I=CLOfnjakf0q!-3LhtZ&{^0<^Y6|ioJ1CEp$V6A78BQ5(ObzO9+ zspYcX563;*bd}N^ZQ5Elbk56ybuL|E)r8w>a1>5UOL=<&ckD|%Gbu{c18fYnnRd8a zlGE}!D#jIwb%KJ15OKt4jk9rj2=Nxe#*oy=kw7TyRnx5~;#J#@qh1J==93GFt);zM z>nW7*w51$@x~hBRbEuiRMLNw?-kh;{VFK>nt*MTMqn-1AoTBH^=#Wd?8}3&xv{V<6 zuM*Ey&rb7v%3h%t5t|WP5T7BoAvPh-z>jrjS><}`r;q{ox_(mYYbbt|A*ov_$HRDR z_u)(xljC$FDlLcz;xdAJ{jUiepL__+@;JGxxCcFqxQgJ#*oC8BI5g`O@^fOwY<4C0 z!J39j(hr9lme&uU7Ax}5%Ds&WiokP9k0IVej43@lxvQaTm;Q5U> zx-==oWacVu4Xh5t1C$3ll4lIXTRASzrS{S6@?IG&>wY*FEY{w{ZEwL~uy8$Zx&t`p z&B=or$uZ@P@{n^*Wn3}ptQy_MN+m6ww1Zvya%*R(J>ErSUJR;1J=Vr+9W6`J5|LrI zY&IEDi;>XZpiiDCVNH0uv!8`{oN7_6gJ0!R;z?w$*sf&_*AuSS4&ZA&?B1Mc;tha* zw^&pK5x2z4@BruBY!8m^z|u%5DM@UJ>?PzTxH{I%$6w6UCG{bWE+D=_{EQexU{6pF eqJ*JWLEBKeoFS>NiF5?J!m^J}Pki5TUHK1bnVD4p delta 2253 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®@x0kFT|;dS{Ev)_PxEqm#DK>#;AC^6P`gQov|VFYK6N+6(>WW4Dmi zbu1?9)E7FwF`9zn!hW;&?pe2jVl`k5;5NYRfLg#E0EoLkt$KOQO19!aa&nIF?fK{y zo1-;{+a)_%n?@51dqcUcqD}_BTv4#l60%9 zUfioM3sMlmp%r(Z)J(@T)%0?+6r?Fqt7ZwZ0(^T}@ymtt_}$=z0y=Bql5C?ge?<#z zb~ZU$HN%EBhfIY|LJ2)x3`wwnj<6SBUPT7f?_XYOb%FHQ%AR8eTN!unKrhWNrpXN&$fqAuJJYgEZ!_=(V|Ban}^0JX{3%ip^OWJaieFWNoI|rG>Lb6p8%_^gG7$T70!@v20Rw>TfDZtJfMkF_ z(_{w^xQ6Hdp&=VOoKSuhLH!$e3CFsz8W-L$>S241rcLrUU16MPx+BbYv^K1iPeoF- zp6%?;R_(_sB6wAL>8}rvUo)$Be)1M>>S-i`Ri_!7C7C*0TBzLSYPR$1yPZAittLC! z3GZIAg*`lABfHd32WFFSSZUSMZOlBFXnX?RTFy3f$FshH1a|je3faT93@%FC0jf~C zo&Td9fTsaZ0iI!}2iKAgR^clk-?Cr$_K+>=wI^Ei)GokGfZRmitb@w zom_0Z#G`mmL?x`lX`rTGsRwrwKZ|7`{{avJjR}YQ zvsO}Py|1hC=CUf2gx zr^D>bxx9ia8uTjXVGY&7d9+()OnFscDhBHww)*@XVXJrvk=7CA4~b-kI($AhZ2nJQ zNOAc@xF?!|Ur}c2NLh?1`^M_~B-!3Vb;|uZQfqtxf}@p_I+A{A&{Epd$z-@`ZB zp5(g4pz|(_JOp?iAObOrl~7qY${^Y8>uChv{BhD>p6dRnybHm{IcP>_o9t?n>0huT z96|X?isr0RQj}r-HA*&iUuPgUUN&AS4@Q!#Ie~T-W9Ji7a~(I$)y%sPA09pczd;T> zmqo&WEj_M$7)er$7#ygiT#O{S(E&moJ))GDNKTUovPj$pyd;LLF|w267W@2s&7iF5zB9WlMgQjti0Zk!*=<$D+)^YwC znMHL_1<|9hQ4002x>fSMfICKe@&!C#I$u8G#Y@i<~ z@tGvky$_0g3HT@A4B#68ezKs`02V+h;5tAypc+sISPzf^8v&02UIG*Vx&VCu58xy~ z=zAKgGlZuPJ+1U*l4A0mav_sEN){@ctYnIDp&>N#oAU0RR+1h{yvm^*vWh&cOv)u0 zStzV@%%NILW3x?KU$;?i%&C`Xqxb@*J)p5Gyw((3KZZGOz<1a!$EpZ`pJuf1(iUe! VTeE!ujZogsC2GWd@g!25{}(nxHg5m` diff --git a/routes/__pycache__/rooms.cpython-313.pyc b/routes/__pycache__/rooms.cpython-313.pyc index 998c3b6f282c134cced1a74f902347a6a94096e4..41f64a936f9a577eb82fdab1268a8be2e11bc59a 100644 GIT binary patch delta 3830 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 delta 4510 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 a>xbZMuiIWq%5z~gt*f!So`T=?mj4f=Uc7|> diff --git a/routes/contacts.py b/routes/contacts.py index 8a30b2c..c08824f 100644 --- a/routes/contacts.py +++ b/routes/contacts.py @@ -29,8 +29,8 @@ def inject_unread_notifications(): def admin_required(): if not current_user.is_authenticated: return redirect(url_for('auth.login')) - if not current_user.is_admin: - flash('You must be an admin to access this page.', 'error') + if not (current_user.is_admin or current_user.is_manager): + flash('You must be an admin or manager to access this page.', 'error') return redirect(url_for('main.dashboard')) @contacts_bp.route('/') @@ -72,8 +72,10 @@ def contacts_list(): # Apply role filter if role == 'admin': query = query.filter(User.is_admin == True) + elif role == 'manager': + query = query.filter(User.is_manager == True) elif role == 'user': - query = query.filter(User.is_admin == False) + query = query.filter(User.is_admin == False, User.is_manager == False) # Order by creation date query = query.order_by(User.created_at.desc()) @@ -97,8 +99,13 @@ def new_contact(): total_admins = User.query.filter_by(is_admin=True).count() if request.method == 'GET': form.is_admin.data = False # Ensure admin role is unchecked by default - elif request.method == 'POST' and 'is_admin' not in request.form: - form.is_admin.data = False # Explicitly set to False if not present in POST + form.is_manager.data = False # Ensure manager role is unchecked by default + elif request.method == 'POST': + if 'is_admin' not in request.form: + form.is_admin.data = False + if 'is_manager' not in request.form: + form.is_manager.data = False + if form.validate_on_submit(): # Check if a user with this email already exists existing_user = User.query.filter_by(email=form.email.data).first() @@ -130,9 +137,10 @@ def new_contact(): notes=form.notes.data, is_active=True, # Set default value is_admin=form.is_admin.data, + is_manager=form.is_manager.data, profile_picture=profile_picture ) - user.set_password(random_password) # Set random password + user.set_password(random_password) db.session.add(user) db.session.commit() @@ -171,6 +179,7 @@ def new_contact(): 'user_name': f"{user.username} {user.last_name}", 'email': user.email, 'is_admin': user.is_admin, + 'is_manager': user.is_manager, 'method': 'admin_creation' } ) diff --git a/routes/conversations.py b/routes/conversations.py index adec0ee..f2bb20c 100644 --- a/routes/conversations.py +++ b/routes/conversations.py @@ -61,8 +61,8 @@ def conversations(): @login_required @require_password_change def create_conversation(): - if not current_user.is_admin: - flash('Only administrators can create conversations.', 'error') + if not (current_user.is_admin or current_user.is_manager): + flash('Only administrators and managers can create conversations.', 'error') return redirect(url_for('conversations.conversations')) form = ConversationForm() @@ -148,8 +148,8 @@ def conversation(conversation_id): # 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 + # Get all users for member selection (needed for admin and manager) + all_users = User.query.all() if (current_user.is_admin or current_user.is_manager) else None unread_count = get_unread_count(current_user.id) return render_template('conversations/conversation.html', @@ -167,8 +167,8 @@ def conversation_members(conversation_id): 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') + if not (current_user.is_admin or current_user.is_manager): + flash('Only administrators and managers 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() diff --git a/routes/main.py b/routes/main.py index cb7d5d1..8d3390b 100644 --- a/routes/main.py +++ b/routes/main.py @@ -273,11 +273,36 @@ def init_routes(main_bp): ).group_by('extension').all() # Get conversation stats - conversation_count = Conversation.query.count() - message_count = Message.query.count() - attachment_count = MessageAttachment.query.count() - conversation_total_size = db.session.query(func.sum(MessageAttachment.size)).scalar() or 0 - recent_conversations = Conversation.query.order_by(Conversation.created_at.desc()).limit(5).all() + if current_user.is_admin: + conversation_count = Conversation.query.count() + message_count = Message.query.count() + attachment_count = MessageAttachment.query.count() + conversation_total_size = db.session.query(func.sum(MessageAttachment.size)).scalar() or 0 + recent_conversations = Conversation.query.order_by(Conversation.created_at.desc()).limit(5).all() + else: + # Get conversations where user is a member + user_conversations = Conversation.query.filter(Conversation.members.any(id=current_user.id)).all() + conversation_count = len(user_conversations) + + # Get message count for user's conversations + conversation_ids = [conv.id for conv in user_conversations] + message_count = Message.query.filter(Message.conversation_id.in_(conversation_ids)).count() + + # Get attachment count and size for user's conversations + attachment_stats = db.session.query( + func.count(MessageAttachment.id).label('count'), + func.sum(MessageAttachment.size).label('total_size') + ).filter(MessageAttachment.message_id.in_( + db.session.query(Message.id).filter(Message.conversation_id.in_(conversation_ids)) + )).first() + + attachment_count = attachment_stats.count or 0 + conversation_total_size = attachment_stats.total_size or 0 + + # Get recent conversations for the user + recent_conversations = Conversation.query.filter( + Conversation.members.any(id=current_user.id) + ).order_by(Conversation.created_at.desc()).limit(5).all() return render_template('dashboard/dashboard.html', room_count=room_count, diff --git a/routes/rooms.py b/routes/rooms.py index 80a4a04..a9a9db2 100644 --- a/routes/rooms.py +++ b/routes/rooms.py @@ -85,7 +85,7 @@ def create_room(): @require_password_change def room(room_id): room = Room.query.get_or_404(room_id) - # Admins always have access + # Admins always have access, managers need to be members if not current_user.is_admin: is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None if not is_member: @@ -116,14 +116,15 @@ def room(room_id): @require_password_change def room_members(room_id): room = Room.query.get_or_404(room_id) - # Admins always have access + # Check if user is a member if not current_user.is_admin: is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None if not is_member: flash('You do not have access to this room.', 'error') return redirect(url_for('rooms.rooms')) - if not current_user.is_admin: - flash('Only administrators can manage room members.', 'error') + # Only admins and managers who are members can manage room members + if not (current_user.is_admin or (current_user.is_manager and is_member)): + flash('Only administrators and managers can manage room members.', 'error') return redirect(url_for('rooms.room', room_id=room_id)) member_permissions = {p.user_id: p for p in room.member_permissions} available_users = User.query.filter(~User.id.in_(member_permissions.keys())).all() @@ -139,8 +140,9 @@ def add_member(room_id): if not is_member: flash('You do not have access to this room.', 'error') return redirect(url_for('rooms.rooms')) - if not current_user.is_admin: - flash('Only administrators can manage room members.', 'error') + # Only admins and managers who are members can manage room members + if not (current_user.is_admin or (current_user.is_manager and is_member)): + flash('Only administrators and managers can manage room members.', 'error') return redirect(url_for('rooms.room', room_id=room_id)) user_id = request.form.get('user_id') if not user_id: @@ -211,59 +213,30 @@ def remove_member(room_id, user_id): if not is_member: flash('You do not have access to this room.', 'error') return redirect(url_for('rooms.rooms')) - if not current_user.is_admin: - flash('Only administrators can manage room members.', 'error') + # Only admins and managers who are members can manage room members + if not (current_user.is_admin or (current_user.is_manager and is_member)): + flash('Only administrators and managers can manage room members.', 'error') return redirect(url_for('rooms.room', room_id=room_id)) if user_id == room.created_by: flash('Cannot remove the room creator.', 'error') else: perm = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first() - if not perm: - flash('User is not a member of this room.', 'error') + if perm: + db.session.delete(perm) + db.session.commit() + flash('Member has been removed from the room.', 'success') else: - user = User.query.get(user_id) - try: - # Create notification for the removed user - create_notification( - notif_type='room_invite_removed', - user_id=user_id, - sender_id=current_user.id, - details={ - 'message': f'You have been removed from room "{room.name}"', - 'room_id': room_id, - 'room_name': room.name, - 'removed_by': f"{current_user.username} {current_user.last_name}", - 'timestamp': datetime.utcnow().isoformat() - } - ) - - log_event( - event_type='room_member_remove', - details={ - 'room_id': room_id, - 'room_name': room.name, - 'removed_user': f"{user.username} {user.last_name}", - 'removed_by': f"{current_user.username} {current_user.last_name}" - }, - user_id=current_user.id - ) - - db.session.delete(perm) - db.session.commit() - flash('User has been removed from the room.', 'success') - except Exception as e: - db.session.rollback() - flash('An error occurred while removing the member.', 'error') - print(f"Error removing member: {str(e)}") - + flash('Member not found.', 'error') return redirect(url_for('rooms.room_members', room_id=room_id)) @rooms_bp.route('//members//permissions', methods=['POST']) @login_required def update_member_permissions(room_id, user_id): room = Room.query.get_or_404(room_id) - if not current_user.is_admin: - flash('Only administrators can update permissions.', 'error') + # Check if user is a member + is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None + if not (current_user.is_admin or (current_user.is_manager and is_member)): + flash('Only administrators and managers can update permissions.', 'error') return redirect(url_for('rooms.room_members', room_id=room_id)) perm = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first() if not perm: @@ -312,11 +285,13 @@ def update_member_permissions(room_id, user_id): @rooms_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit_room(room_id): - if not current_user.is_admin: - flash('Only administrators can edit rooms.', 'error') + room = Room.query.get_or_404(room_id) + # Check if user is a member + is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None + if not (current_user.is_admin or (current_user.is_manager and is_member)): + flash('Only administrators and managers can edit rooms.', 'error') return redirect(url_for('rooms.rooms')) - room = Room.query.get_or_404(room_id) form = RoomForm() if form.validate_on_submit(): @@ -354,11 +329,13 @@ def edit_room(room_id): @rooms_bp.route('//delete', methods=['POST']) @login_required def delete_room(room_id): - if not current_user.is_admin: - flash('Only administrators can delete rooms.', 'error') + room = Room.query.get_or_404(room_id) + # Check if user is a member + is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None + if not (current_user.is_admin or (current_user.is_manager and is_member)): + flash('Only administrators and managers can delete rooms.', 'error') return redirect(url_for('rooms.rooms')) - room = Room.query.get_or_404(room_id) room_name = room.name try: diff --git a/static/js/rooms/viewManager.js b/static/js/rooms/viewManager.js index c71f67c..c56df66 100644 --- a/static/js/rooms/viewManager.js +++ b/static/js/rooms/viewManager.js @@ -346,49 +346,27 @@ export class ViewManager { * @returns {string} HTML string for the action buttons */ renderFileActions(file, index) { - console.log('[ViewManager] Rendering file actions:', { file, index }); const actions = []; - - if (file.type === 'folder') { + + // Add details button + actions.push(` + + `); + + // Add download button if user has permission + if (this.roomManager.canDownload) { actions.push(` - `); - } else { - // Check if file type is supported for preview - const extension = file.name.split('.').pop().toLowerCase(); - const supportedTypes = [ - // Images - 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'tiff', - // Documents - 'pdf', 'txt', 'md', 'csv', 'py', 'js', 'html', 'css', 'json', 'xml', 'sql', 'sh', 'bat', - 'docx', 'doc', 'xlsx', 'xls', 'pptx', 'ppt', 'odt', 'odp', 'ods', - // Media - 'mp4', 'webm', 'avi', 'mov', 'wmv', 'flv', 'mkv', - 'mp3', 'wav', 'ogg', 'm4a', 'flac' - ]; - - if (supportedTypes.includes(extension)) { - actions.push(` - - `); - } - - if (this.roomManager.canDownload) { - actions.push(` - - `); - } } + // Add rename button if user has permission if (this.roomManager.canRename) { actions.push(` @@ -133,7 +133,7 @@ -{% if current_user.is_admin %} +{% if current_user.is_admin or current_user.is_manager %}