added manager user type
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
6
forms.py
6
forms.py
@@ -14,6 +14,7 @@ class UserForm(FlaskForm):
|
|||||||
position = StringField('Position (Optional)', validators=[Optional(), Length(max=100)])
|
position = StringField('Position (Optional)', validators=[Optional(), Length(max=100)])
|
||||||
notes = TextAreaField('Notes (Optional)', validators=[Optional()])
|
notes = TextAreaField('Notes (Optional)', validators=[Optional()])
|
||||||
is_admin = BooleanField('Admin Role', default=False)
|
is_admin = BooleanField('Admin Role', default=False)
|
||||||
|
is_manager = BooleanField('Manager Role', default=False)
|
||||||
new_password = PasswordField('New Password (Optional)')
|
new_password = PasswordField('New Password (Optional)')
|
||||||
confirm_password = PasswordField('Confirm Password (Optional)')
|
confirm_password = PasswordField('Confirm Password (Optional)')
|
||||||
profile_picture = FileField('Profile Picture (Optional)', validators=[FileAllowed(['jpg', 'jpeg', 'png', 'gif'], 'Images only!')])
|
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:
|
if total_admins <= 1:
|
||||||
raise ValidationError('There must be at least one admin user in the system.')
|
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):
|
def validate(self, extra_validators=None):
|
||||||
rv = super().validate(extra_validators=extra_validators)
|
rv = super().validate(extra_validators=extra_validators)
|
||||||
if not rv:
|
if not rv:
|
||||||
|
|||||||
24
migrations/versions/72ab6c4c6a5f_merge_heads.py
Normal file
24
migrations/versions/72ab6c4c6a5f_merge_heads.py
Normal file
@@ -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
|
||||||
@@ -8,6 +8,7 @@ Create Date: 2024-03-19 10:00:00.000000
|
|||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'add_docupulse_settings'
|
revision = 'add_docupulse_settings'
|
||||||
@@ -28,7 +29,7 @@ def upgrade():
|
|||||||
server_default='10737418240')
|
server_default='10737418240')
|
||||||
|
|
||||||
# Check if we need to insert default data
|
# 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:
|
if result == 0:
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
INSERT INTO docupulse_settings (id, max_rooms, max_conversations, max_storage, updated_at)
|
INSERT INTO docupulse_settings (id, max_rooms, max_conversations, max_storage, updated_at)
|
||||||
|
|||||||
38
migrations/versions/add_manager_role.py
Normal file
38
migrations/versions/add_manager_role.py
Normal file
@@ -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 ###
|
||||||
35
models.py
35
models.py
@@ -26,6 +26,7 @@ class User(UserMixin, db.Model):
|
|||||||
email = db.Column(db.String(150), unique=True, nullable=False)
|
email = db.Column(db.String(150), unique=True, nullable=False)
|
||||||
password_hash = db.Column(db.String(256))
|
password_hash = db.Column(db.String(256))
|
||||||
is_admin = db.Column(db.Boolean, default=False)
|
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)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
phone = db.Column(db.String(20))
|
phone = db.Column(db.String(20))
|
||||||
company = db.Column(db.String(100))
|
company = db.Column(db.String(100))
|
||||||
@@ -444,4 +445,36 @@ class PasswordSetupToken(db.Model):
|
|||||||
return not self.used and datetime.utcnow() < self.expires_at
|
return not self.used and datetime.utcnow() < self.expires_at
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<PasswordSetupToken {self.token}>'
|
return f'<PasswordSetupToken {self.token}>'
|
||||||
|
|
||||||
|
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)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -29,8 +29,8 @@ def inject_unread_notifications():
|
|||||||
def admin_required():
|
def admin_required():
|
||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
if not current_user.is_admin:
|
if not (current_user.is_admin or current_user.is_manager):
|
||||||
flash('You must be an admin to access this page.', 'error')
|
flash('You must be an admin or manager to access this page.', 'error')
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
@contacts_bp.route('/')
|
@contacts_bp.route('/')
|
||||||
@@ -72,8 +72,10 @@ def contacts_list():
|
|||||||
# Apply role filter
|
# Apply role filter
|
||||||
if role == 'admin':
|
if role == 'admin':
|
||||||
query = query.filter(User.is_admin == True)
|
query = query.filter(User.is_admin == True)
|
||||||
|
elif role == 'manager':
|
||||||
|
query = query.filter(User.is_manager == True)
|
||||||
elif role == 'user':
|
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
|
# Order by creation date
|
||||||
query = query.order_by(User.created_at.desc())
|
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()
|
total_admins = User.query.filter_by(is_admin=True).count()
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
form.is_admin.data = False # Ensure admin role is unchecked by default
|
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_manager.data = False # Ensure manager role is unchecked by default
|
||||||
form.is_admin.data = False # Explicitly set to False if not present in POST
|
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():
|
if form.validate_on_submit():
|
||||||
# Check if a user with this email already exists
|
# Check if a user with this email already exists
|
||||||
existing_user = User.query.filter_by(email=form.email.data).first()
|
existing_user = User.query.filter_by(email=form.email.data).first()
|
||||||
@@ -130,9 +137,10 @@ def new_contact():
|
|||||||
notes=form.notes.data,
|
notes=form.notes.data,
|
||||||
is_active=True, # Set default value
|
is_active=True, # Set default value
|
||||||
is_admin=form.is_admin.data,
|
is_admin=form.is_admin.data,
|
||||||
|
is_manager=form.is_manager.data,
|
||||||
profile_picture=profile_picture
|
profile_picture=profile_picture
|
||||||
)
|
)
|
||||||
user.set_password(random_password) # Set random password
|
user.set_password(random_password)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -171,6 +179,7 @@ def new_contact():
|
|||||||
'user_name': f"{user.username} {user.last_name}",
|
'user_name': f"{user.username} {user.last_name}",
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'is_admin': user.is_admin,
|
'is_admin': user.is_admin,
|
||||||
|
'is_manager': user.is_manager,
|
||||||
'method': 'admin_creation'
|
'method': 'admin_creation'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ def conversations():
|
|||||||
@login_required
|
@login_required
|
||||||
@require_password_change
|
@require_password_change
|
||||||
def create_conversation():
|
def create_conversation():
|
||||||
if not current_user.is_admin:
|
if not (current_user.is_admin or current_user.is_manager):
|
||||||
flash('Only administrators can create conversations.', 'error')
|
flash('Only administrators and managers can create conversations.', 'error')
|
||||||
return redirect(url_for('conversations.conversations'))
|
return redirect(url_for('conversations.conversations'))
|
||||||
|
|
||||||
form = ConversationForm()
|
form = ConversationForm()
|
||||||
@@ -148,8 +148,8 @@ def conversation(conversation_id):
|
|||||||
# Query messages directly using the Message model
|
# Query messages directly using the Message model
|
||||||
messages = Message.query.filter_by(conversation_id=conversation_id).order_by(Message.created_at.asc()).all()
|
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)
|
# Get all users for member selection (needed for admin and manager)
|
||||||
all_users = User.query.all() if current_user.is_admin else None
|
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)
|
unread_count = get_unread_count(current_user.id)
|
||||||
return render_template('conversations/conversation.html',
|
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')
|
flash('You do not have access to this conversation.', 'error')
|
||||||
return redirect(url_for('conversations.conversations'))
|
return redirect(url_for('conversations.conversations'))
|
||||||
|
|
||||||
if not current_user.is_admin:
|
if not (current_user.is_admin or current_user.is_manager):
|
||||||
flash('Only administrators can manage conversation members.', 'error')
|
flash('Only administrators and managers can manage conversation members.', 'error')
|
||||||
return redirect(url_for('conversations.conversation', conversation_id=conversation_id))
|
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()
|
available_users = User.query.filter(~User.id.in_([m.id for m in conversation.members])).all()
|
||||||
|
|||||||
@@ -273,11 +273,36 @@ def init_routes(main_bp):
|
|||||||
).group_by('extension').all()
|
).group_by('extension').all()
|
||||||
|
|
||||||
# Get conversation stats
|
# Get conversation stats
|
||||||
conversation_count = Conversation.query.count()
|
if current_user.is_admin:
|
||||||
message_count = Message.query.count()
|
conversation_count = Conversation.query.count()
|
||||||
attachment_count = MessageAttachment.query.count()
|
message_count = Message.query.count()
|
||||||
conversation_total_size = db.session.query(func.sum(MessageAttachment.size)).scalar() or 0
|
attachment_count = MessageAttachment.query.count()
|
||||||
recent_conversations = Conversation.query.order_by(Conversation.created_at.desc()).limit(5).all()
|
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',
|
return render_template('dashboard/dashboard.html',
|
||||||
room_count=room_count,
|
room_count=room_count,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ def create_room():
|
|||||||
@require_password_change
|
@require_password_change
|
||||||
def room(room_id):
|
def room(room_id):
|
||||||
room = Room.query.get_or_404(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:
|
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
|
is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None
|
||||||
if not is_member:
|
if not is_member:
|
||||||
@@ -116,14 +116,15 @@ def room(room_id):
|
|||||||
@require_password_change
|
@require_password_change
|
||||||
def room_members(room_id):
|
def room_members(room_id):
|
||||||
room = Room.query.get_or_404(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:
|
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
|
is_member = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=current_user.id).first() is not None
|
||||||
if not is_member:
|
if not is_member:
|
||||||
flash('You do not have access to this room.', 'error')
|
flash('You do not have access to this room.', 'error')
|
||||||
return redirect(url_for('rooms.rooms'))
|
return redirect(url_for('rooms.rooms'))
|
||||||
if not current_user.is_admin:
|
# Only admins and managers who are members can manage room members
|
||||||
flash('Only administrators can manage room members.', 'error')
|
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))
|
return redirect(url_for('rooms.room', room_id=room_id))
|
||||||
member_permissions = {p.user_id: p for p in room.member_permissions}
|
member_permissions = {p.user_id: p for p in room.member_permissions}
|
||||||
available_users = User.query.filter(~User.id.in_(member_permissions.keys())).all()
|
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:
|
if not is_member:
|
||||||
flash('You do not have access to this room.', 'error')
|
flash('You do not have access to this room.', 'error')
|
||||||
return redirect(url_for('rooms.rooms'))
|
return redirect(url_for('rooms.rooms'))
|
||||||
if not current_user.is_admin:
|
# Only admins and managers who are members can manage room members
|
||||||
flash('Only administrators can manage room members.', 'error')
|
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))
|
return redirect(url_for('rooms.room', room_id=room_id))
|
||||||
user_id = request.form.get('user_id')
|
user_id = request.form.get('user_id')
|
||||||
if not user_id:
|
if not user_id:
|
||||||
@@ -211,59 +213,30 @@ def remove_member(room_id, user_id):
|
|||||||
if not is_member:
|
if not is_member:
|
||||||
flash('You do not have access to this room.', 'error')
|
flash('You do not have access to this room.', 'error')
|
||||||
return redirect(url_for('rooms.rooms'))
|
return redirect(url_for('rooms.rooms'))
|
||||||
if not current_user.is_admin:
|
# Only admins and managers who are members can manage room members
|
||||||
flash('Only administrators can manage room members.', 'error')
|
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))
|
return redirect(url_for('rooms.room', room_id=room_id))
|
||||||
if user_id == room.created_by:
|
if user_id == room.created_by:
|
||||||
flash('Cannot remove the room creator.', 'error')
|
flash('Cannot remove the room creator.', 'error')
|
||||||
else:
|
else:
|
||||||
perm = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first()
|
perm = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first()
|
||||||
if not perm:
|
if perm:
|
||||||
flash('User is not a member of this room.', 'error')
|
db.session.delete(perm)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Member has been removed from the room.', 'success')
|
||||||
else:
|
else:
|
||||||
user = User.query.get(user_id)
|
flash('Member not found.', 'error')
|
||||||
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)}")
|
|
||||||
|
|
||||||
return redirect(url_for('rooms.room_members', room_id=room_id))
|
return redirect(url_for('rooms.room_members', room_id=room_id))
|
||||||
|
|
||||||
@rooms_bp.route('/<int:room_id>/members/<int:user_id>/permissions', methods=['POST'])
|
@rooms_bp.route('/<int:room_id>/members/<int:user_id>/permissions', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def update_member_permissions(room_id, user_id):
|
def update_member_permissions(room_id, user_id):
|
||||||
room = Room.query.get_or_404(room_id)
|
room = Room.query.get_or_404(room_id)
|
||||||
if not current_user.is_admin:
|
# Check if user is a member
|
||||||
flash('Only administrators can update permissions.', 'error')
|
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))
|
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()
|
perm = RoomMemberPermission.query.filter_by(room_id=room_id, user_id=user_id).first()
|
||||||
if not perm:
|
if not perm:
|
||||||
@@ -312,11 +285,13 @@ def update_member_permissions(room_id, user_id):
|
|||||||
@rooms_bp.route('/<int:room_id>/edit', methods=['GET', 'POST'])
|
@rooms_bp.route('/<int:room_id>/edit', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def edit_room(room_id):
|
def edit_room(room_id):
|
||||||
if not current_user.is_admin:
|
room = Room.query.get_or_404(room_id)
|
||||||
flash('Only administrators can edit rooms.', '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 edit rooms.', 'error')
|
||||||
return redirect(url_for('rooms.rooms'))
|
return redirect(url_for('rooms.rooms'))
|
||||||
|
|
||||||
room = Room.query.get_or_404(room_id)
|
|
||||||
form = RoomForm()
|
form = RoomForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@@ -354,11 +329,13 @@ def edit_room(room_id):
|
|||||||
@rooms_bp.route('/<int:room_id>/delete', methods=['POST'])
|
@rooms_bp.route('/<int:room_id>/delete', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_room(room_id):
|
def delete_room(room_id):
|
||||||
if not current_user.is_admin:
|
room = Room.query.get_or_404(room_id)
|
||||||
flash('Only administrators can delete rooms.', '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 delete rooms.', 'error')
|
||||||
return redirect(url_for('rooms.rooms'))
|
return redirect(url_for('rooms.rooms'))
|
||||||
|
|
||||||
room = Room.query.get_or_404(room_id)
|
|
||||||
room_name = room.name
|
room_name = room.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -346,49 +346,27 @@ export class ViewManager {
|
|||||||
* @returns {string} HTML string for the action buttons
|
* @returns {string} HTML string for the action buttons
|
||||||
*/
|
*/
|
||||||
renderFileActions(file, index) {
|
renderFileActions(file, index) {
|
||||||
console.log('[ViewManager] Rendering file actions:', { file, index });
|
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
|
||||||
if (file.type === 'folder') {
|
// Add details button
|
||||||
|
actions.push(`
|
||||||
|
<button class="btn btn-sm file-action-btn" title="Details" onclick="window.roomManager.modalManager.showDetailsModal('${file.name}', '${file.path || ''}')"
|
||||||
|
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Add download button if user has permission
|
||||||
|
if (this.roomManager.canDownload) {
|
||||||
actions.push(`
|
actions.push(`
|
||||||
<button class="btn btn-sm file-action-btn" title="Open" onclick="window.roomManager.navigateToFolder('${file.name}')"
|
<button class="btn btn-sm file-action-btn" title="Download" onclick="window.roomManager.fileManager.downloadFile('${file.name}', '${file.path || ''}')"
|
||||||
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
|
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-download"></i>
|
||||||
</button>
|
</button>
|
||||||
`);
|
`);
|
||||||
} 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(`
|
|
||||||
<button class="btn btn-sm file-action-btn" title="Preview" onclick="window.roomManager.viewManager.previewFile(${index})"
|
|
||||||
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</button>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.roomManager.canDownload) {
|
|
||||||
actions.push(`
|
|
||||||
<button class="btn btn-sm file-action-btn" title="Download" onclick="window.roomManager.fileManager.downloadFile('${file.name}', '${file.path || ''}')"
|
|
||||||
style="background-color:var(--primary-opacity-8);color:var(--primary-color);">
|
|
||||||
<i class="fas fa-download"></i>
|
|
||||||
</button>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add rename button if user has permission
|
||||||
if (this.roomManager.canRename) {
|
if (this.roomManager.canRename) {
|
||||||
actions.push(`
|
actions.push(`
|
||||||
<button class="btn btn-sm file-action-btn" title="Rename" onclick="window.roomManager.modalManager.showRenameModal('${file.name}', '${file.path || ''}')"
|
<button class="btn btn-sm file-action-btn" title="Rename" onclick="window.roomManager.modalManager.showRenameModal('${file.name}', '${file.path || ''}')"
|
||||||
@@ -398,6 +376,7 @@ export class ViewManager {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add move button if user has permission
|
||||||
if (this.roomManager.canMove) {
|
if (this.roomManager.canMove) {
|
||||||
actions.push(`
|
actions.push(`
|
||||||
<button class="btn btn-sm file-action-btn" title="Move" onclick="window.roomManager.modalManager.showMoveModal('${file.name}', '${file.path || ''}')"
|
<button class="btn btn-sm file-action-btn" title="Move" onclick="window.roomManager.modalManager.showMoveModal('${file.name}', '${file.path || ''}')"
|
||||||
@@ -416,6 +395,7 @@ export class ViewManager {
|
|||||||
</button>
|
</button>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Add delete button if user has permission
|
||||||
if (this.roomManager.canDelete) {
|
if (this.roomManager.canDelete) {
|
||||||
actions.push(`
|
actions.push(`
|
||||||
<button class="btn btn-sm file-action-btn" title="Delete" onclick="window.roomManager.modalManager.showDeleteModal('${file.name}', '${file.path || ''}')"
|
<button class="btn btn-sm file-action-btn" title="Delete" onclick="window.roomManager.modalManager.showDeleteModal('${file.name}', '${file.path || ''}')"
|
||||||
|
|||||||
@@ -129,12 +129,16 @@
|
|||||||
<div class="absolute left-0 bottom-full mb-2 hidden group-hover:block bg-gray-800 text-white text-xs rounded py-1 px-2 w-48">
|
<div class="absolute left-0 bottom-full mb-2 hidden group-hover:block bg-gray-800 text-white text-xs rounded py-1 px-2 w-48">
|
||||||
Admin users have full access to manage contacts and system settings.
|
Admin users have full access to manage contacts and system settings.
|
||||||
</div>
|
</div>
|
||||||
{% if is_last_admin and form.is_admin.data %}
|
</div>
|
||||||
<div class="ml-2 text-sm text-amber-600">
|
<div class="flex items-center relative group">
|
||||||
<i class="fas fa-exclamation-circle"></i>
|
{{ form.is_manager(
|
||||||
You are the only admin
|
class="h-4 w-4 focus:ring-blue-500 border-gray-300 rounded",
|
||||||
</div>
|
style="accent-color: #16767b;"
|
||||||
{% endif %}
|
) }}
|
||||||
|
{{ form.is_manager.label(class="ml-2 block text-sm text-gray-900") }}
|
||||||
|
<div class="absolute left-0 bottom-full mb-2 hidden group-hover:block bg-gray-800 text-white text-xs rounded py-1 px-2 w-48">
|
||||||
|
Manager users have similar permissions to admins but with some restrictions.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if form.is_admin.errors %}
|
{% if form.is_admin.errors %}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<select name="role" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-200">
|
<select name="role" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-200">
|
||||||
<option value="">All Roles</option>
|
<option value="">All Roles</option>
|
||||||
<option value="admin" {% if request.args.get('role') == 'admin' %}selected{% endif %}>Admin</option>
|
<option value="admin" {% if request.args.get('role') == 'admin' %}selected{% endif %}>Admin</option>
|
||||||
|
<option value="manager" {% if request.args.get('role') == 'manager' %}selected{% endif %}>Manager</option>
|
||||||
<option value="user" {% if request.args.get('role') == 'user' %}selected{% endif %}>User</option>
|
<option value="user" {% if request.args.get('role') == 'user' %}selected{% endif %}>User</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" id="clearFilters" class="px-4 py-2 rounded-lg text-white font-medium transition-colors duration-200"
|
<button type="button" id="clearFilters" class="px-4 py-2 rounded-lg text-white font-medium transition-colors duration-200"
|
||||||
@@ -99,9 +100,9 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="flex flex-row flex-wrap gap-1.5">
|
<div class="flex flex-row flex-wrap gap-1.5">
|
||||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm font-medium"
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm font-medium"
|
||||||
style="background-color: {% if user.is_admin %}rgba(147,51,234,0.1){% else %}rgba(107,114,128,0.1){% endif %}; color: {% if user.is_admin %}#7e22ce{% else %}#374151{% endif %};">
|
style="background-color: {% if user.is_admin %}rgba(147,51,234,0.1){% elif user.is_manager %}rgba(59,130,246,0.1){% else %}rgba(107,114,128,0.1){% endif %}; color: {% if user.is_admin %}#7e22ce{% elif user.is_manager %}#2563eb{% else %}#374151{% endif %};">
|
||||||
<i class="fas fa-{% if user.is_admin %}shield-alt{% else %}user{% endif %}" style="font-size: 0.85em; opacity: 0.7;"></i>
|
<i class="fas fa-{% if user.is_admin %}shield-alt{% elif user.is_manager %}user-tie{% else %}user{% endif %}" style="font-size: 0.85em; opacity: 0.7;"></i>
|
||||||
{{ 'Admin' if user.is_admin else 'User' }}
|
{{ 'Admin' if user.is_admin else 'Manager' if user.is_manager else 'User' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
<i class="fas fa-users me-2"></i>Members
|
<i class="fas fa-users me-2"></i>Members
|
||||||
</h5>
|
</h5>
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin or current_user.is_manager %}
|
||||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#manageMembersModal">
|
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#manageMembersModal">
|
||||||
<i class="fas fa-user-edit me-1"></i>Manage Members
|
<i class="fas fa-user-edit me-1"></i>Manage Members
|
||||||
</button>
|
</button>
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin or current_user.is_manager %}
|
||||||
<!-- Manage Members Modal -->
|
<!-- Manage Members Modal -->
|
||||||
<div class="modal fade" id="manageMembersModal" tabindex="-1" aria-labelledby="manageMembersModalLabel" aria-hidden="true">
|
<div class="modal fade" id="manageMembersModal" tabindex="-1" aria-labelledby="manageMembersModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
{{ header(
|
{{ header(
|
||||||
title="Conversations",
|
title="Conversations",
|
||||||
description="Manage your conversations and messages",
|
description="Manage your conversations and messages",
|
||||||
button_text="New Conversation" if current_user.is_admin else "",
|
button_text="New Conversation" if current_user.is_admin or current_user.is_manager else "",
|
||||||
button_url=url_for('conversations.create_conversation') if current_user.is_admin else "",
|
button_url=url_for('conversations.create_conversation') if current_user.is_admin or current_user.is_manager else "",
|
||||||
icon="fa-comments",
|
icon="fa-comments",
|
||||||
button_icon="fa-plus"
|
button_icon="fa-plus"
|
||||||
) }}
|
) }}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<!-- Storage Section -->
|
<!-- Storage Section -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="section-title">Storage Overview</h2>
|
<h2 class="section-title">Storage Overview</h2>
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin or current_user.is_manager %}
|
||||||
{{ usage_limits(usage_stats) }}
|
{{ usage_limits(usage_stats) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="masonry">
|
<div class="masonry">
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
<h5 class="mb-0">Files</h5>
|
<h5 class="mb-0">Files</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2" id="actionButtonsRow">
|
<div class="d-flex gap-2" id="actionButtonsRow">
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin or (current_user.is_manager and room.member_permissions|selectattr('user_id', 'equalto', current_user.id)|list|length > 0) %}
|
||||||
<a href="{{ url_for('rooms.room_members', room_id=room.id) }}"
|
<a href="{{ url_for('rooms.room_members', room_id=room.id) }}"
|
||||||
class="btn btn-outline-primary d-flex align-items-center gap-2"
|
class="btn btn-outline-primary d-flex align-items-center gap-2"
|
||||||
style="border-color:var(--primary-color); color:var(--primary-color);"
|
style="border-color:var(--primary-color); color:var(--primary-color);"
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<i class="fas fa-users"></i> Manage Members
|
<i class="fas fa-users"></i> Manage Members
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_admin or can_upload %}
|
{% if current_user.is_admin or (current_user.is_manager and room.member_permissions|selectattr('user_id', 'equalto', current_user.id)|list|length > 0) or can_upload %}
|
||||||
<button type="button" id="newFolderBtn" class="btn btn-outline-primary d-flex align-items-center gap-2"
|
<button type="button" id="newFolderBtn" class="btn btn-outline-primary d-flex align-items-center gap-2"
|
||||||
style="border-color:var(--primary-color); color:var(--primary-color);"
|
style="border-color:var(--primary-color); color:var(--primary-color);"
|
||||||
onmouseover="this.style.backgroundColor='var(--primary-color)'; this.style.color='white'"
|
onmouseover="this.style.backgroundColor='var(--primary-color)'; this.style.color='white'"
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_admin or can_download %}
|
{% if current_user.is_admin or (current_user.is_manager and room.member_permissions|selectattr('user_id', 'equalto', current_user.id)|list|length > 0) or can_download %}
|
||||||
<button id="downloadSelectedBtn" class="btn btn-outline-primary btn-sm d-flex align-items-center gap-2"
|
<button id="downloadSelectedBtn" class="btn btn-outline-primary btn-sm d-flex align-items-center gap-2"
|
||||||
style="display:none; border-color:var(--primary-color); color:var(--primary-color);"
|
style="display:none; border-color:var(--primary-color); color:var(--primary-color);"
|
||||||
onmouseover="this.style.backgroundColor='var(--primary-color)'; this.style.color='white'"
|
onmouseover="this.style.backgroundColor='var(--primary-color)'; this.style.color='white'"
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<i class="fas fa-download"></i> Download Selected
|
<i class="fas fa-download"></i> Download Selected
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_admin or can_delete %}
|
{% if current_user.is_admin or (current_user.is_manager and room.member_permissions|selectattr('user_id', 'equalto', current_user.id)|list|length > 0) or can_delete %}
|
||||||
<button id="deleteSelectedBtn" class="btn btn-outline-danger btn-sm d-flex align-items-center gap-2"
|
<button id="deleteSelectedBtn" class="btn btn-outline-danger btn-sm d-flex align-items-center gap-2"
|
||||||
style="display:none; border-color:var(--danger-color); color:var(--danger-color);"
|
style="display:none; border-color:var(--danger-color); color:var(--danger-color);"
|
||||||
onmouseover="this.style.backgroundColor='var(--danger-color)'; this.style.color='white'"
|
onmouseover="this.style.backgroundColor='var(--danger-color)'; this.style.color='white'"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
<a href="{{ url_for('rooms.room', room_id=room.id) }}" class="btn btn-primary flex-grow-1">
|
<a href="{{ url_for('rooms.room', room_id=room.id) }}" class="btn btn-primary flex-grow-1">
|
||||||
<i class="fas fa-door-open me-2"></i>Open Room
|
<i class="fas fa-door-open me-2"></i>Open Room
|
||||||
</a>
|
</a>
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin or (current_user.is_manager and room.member_permissions|selectattr('user_id', 'equalto', current_user.id)|list|length > 0) %}
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="roomActions{{ room.id }}" data-bs-toggle="dropdown" aria-expanded="false">
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="roomActions{{ room.id }}" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin or (current_user.is_manager and room.member_permissions|selectattr('user_id', 'equalto', current_user.id)|list|length > 0) %}
|
||||||
<!-- Delete Room Modal -->
|
<!-- Delete Room Modal -->
|
||||||
<div class="modal fade" id="deleteRoomModal{{ room.id }}" tabindex="-1" aria-labelledby="deleteRoomModalLabel{{ room.id }}" aria-hidden="true">
|
<div class="modal fade" id="deleteRoomModal{{ room.id }}" tabindex="-1" aria-labelledby="deleteRoomModalLabel{{ room.id }}" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
|||||||
Reference in New Issue
Block a user