diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index 21fb7fe..4236a75 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/forms.cpython-313.pyc b/__pycache__/forms.cpython-313.pyc index ebfddfa..beb9363 100644 Binary files a/__pycache__/forms.cpython-313.pyc and b/__pycache__/forms.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 0cb9d95..cc98e34 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ 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 33a378d..7bea8e6 100644 Binary files a/routes/__pycache__/contacts.cpython-313.pyc and b/routes/__pycache__/contacts.cpython-313.pyc differ diff --git a/routes/__pycache__/conversations.cpython-313.pyc b/routes/__pycache__/conversations.cpython-313.pyc index 8e064bc..736d4e4 100644 Binary files a/routes/__pycache__/conversations.cpython-313.pyc and b/routes/__pycache__/conversations.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index c7affbd..fae39a5 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/__pycache__/rooms.cpython-313.pyc b/routes/__pycache__/rooms.cpython-313.pyc index 998c3b6..41f64a9 100644 Binary files a/routes/__pycache__/rooms.cpython-313.pyc and b/routes/__pycache__/rooms.cpython-313.pyc differ 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 %}