Add Team Roles feature.

This commit is contained in:
Jens Luedicke
2025-06-28 22:39:28 +02:00
parent e7593dc840
commit d8ec7d636e
11 changed files with 672 additions and 37 deletions

279
app.py
View File

@@ -1,5 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g
from models import db, TimeEntry, WorkConfig, User, SystemSettings
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role
import logging
from datetime import datetime, time, timedelta
import os
@@ -78,6 +78,38 @@ def admin_required(f):
return f(*args, **kwargs)
return decorated_function
# Add this decorator function after your existing decorators
def role_required(min_role):
"""
Decorator to restrict access based on user role.
min_role should be a Role enum value (e.g., Role.TEAM_LEADER)
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
return redirect(url_for('login', next=request.url))
# Admin always has access
if g.user.is_admin:
return f(*args, **kwargs)
# Check role hierarchy
role_hierarchy = {
Role.TEAM_MEMBER: 1,
Role.TEAM_LEADER: 2,
Role.SUPERVISOR: 3,
Role.ADMIN: 4
}
if role_hierarchy.get(g.user.role, 0) < role_hierarchy.get(min_role, 0):
flash('You do not have sufficient permissions to access this page.', 'error')
return redirect(url_for('home'))
return f(*args, **kwargs)
return decorator
return decorator
@app.before_request
def load_logged_in_user():
user_id = session.get('user_id')
@@ -114,21 +146,44 @@ def login():
user = User.query.filter_by(username=username).first()
if user and user.check_password(password): # Use the check_password method
# Check if user is blocked
if user.is_blocked:
flash('Your account has been disabled. Please contact an administrator.', 'error')
return render_template('login.html')
if user:
# Fix role if it's a string or None
if isinstance(user.role, str) or user.role is None:
# Map string role values to enum values
role_mapping = {
'Team Member': Role.TEAM_MEMBER,
'TEAM_MEMBER': Role.TEAM_MEMBER,
'Team Leader': Role.TEAM_LEADER,
'TEAM_LEADER': Role.TEAM_LEADER,
'Supervisor': Role.SUPERVISOR,
'SUPERVISOR': Role.SUPERVISOR,
'Administrator': Role.ADMIN,
'ADMIN': Role.ADMIN
}
# Continue with normal login process
session['user_id'] = user.id
session['username'] = user.username
session['is_admin'] = user.is_admin
if isinstance(user.role, str):
user.role = role_mapping.get(user.role, Role.TEAM_MEMBER)
else:
user.role = Role.ADMIN if user.is_admin else Role.TEAM_MEMBER
db.session.commit()
flash('Login successful!', 'success')
return redirect(url_for('home'))
else:
flash('Invalid username or password', 'error')
# Now proceed with password check
if user.check_password(password):
# Check if user is blocked
if user.is_blocked:
flash('Your account has been disabled. Please contact an administrator.', 'error')
return render_template('login.html')
# Continue with normal login process
session['user_id'] = user.id
session['username'] = user.username
session['is_admin'] = user.is_admin
flash('Login successful!', 'success')
return redirect(url_for('home'))
flash('Invalid username or password', 'error')
return render_template('login.html', title='Login')
@@ -245,7 +300,11 @@ def create_user():
email = request.form.get('email')
password = request.form.get('password')
is_admin = 'is_admin' in request.form
auto_verify = 'auto_verify' in request.form # New checkbox for auto verification
auto_verify = 'auto_verify' in request.form
# Get role and team
role_name = request.form.get('role')
team_id = request.form.get('team_id')
# Validate input
error = None
@@ -261,7 +320,21 @@ def create_user():
error = 'Email already registered'
if error is None:
new_user = User(username=username, email=email, is_admin=is_admin, is_verified=auto_verify)
# Convert role string to enum
try:
role = Role[role_name] if role_name else Role.TEAM_MEMBER
except KeyError:
role = Role.TEAM_MEMBER
# Create new user with role and team
new_user = User(
username=username,
email=email,
is_admin=is_admin,
is_verified=auto_verify,
role=role,
team_id=team_id if team_id else None
)
new_user.set_password(password)
if not auto_verify:
@@ -293,7 +366,11 @@ The TimeTrack Team
flash(error, 'error')
return render_template('create_user.html', title='Create User')
# Get all teams for the form
teams = Team.query.all()
roles = [role for role in Role]
return render_template('create_user.html', title='Create User', teams=teams, roles=roles)
@app.route('/admin/users/edit/<int:user_id>', methods=['GET', 'POST'])
@admin_required
@@ -306,6 +383,10 @@ def edit_user(user_id):
password = request.form.get('password')
is_admin = 'is_admin' in request.form
# Get role and team
role_name = request.form.get('role')
team_id = request.form.get('team_id')
# Validate input
error = None
if not username:
@@ -322,6 +403,14 @@ def edit_user(user_id):
user.email = email
user.is_admin = is_admin
# Convert role string to enum
try:
user.role = Role[role_name] if role_name else Role.TEAM_MEMBER
except KeyError:
user.role = Role.TEAM_MEMBER
user.team_id = team_id if team_id else None
if password:
user.set_password(password)
@@ -332,7 +421,11 @@ def edit_user(user_id):
flash(error, 'error')
return render_template('edit_user.html', title='Edit User', user=user)
# Get all teams for the form
teams = Team.query.all()
roles = [role for role in Role]
return render_template('edit_user.html', title='Edit User', user=user, teams=teams, roles=roles)
@app.route('/admin/users/delete/<int:user_id>', methods=['POST'])
@admin_required
@@ -728,5 +821,155 @@ def admin_settings():
return render_template('admin_settings.html', title='System Settings', settings=settings)
# Add these routes for team management
@app.route('/admin/teams')
@admin_required
def admin_teams():
teams = Team.query.all()
return render_template('admin_teams.html', title='Team Management', teams=teams)
@app.route('/admin/teams/create', methods=['GET', 'POST'])
@admin_required
def create_team():
if request.method == 'POST':
name = request.form.get('name')
description = request.form.get('description')
# Validate input
error = None
if not name:
error = 'Team name is required'
elif Team.query.filter_by(name=name).first():
error = 'Team name already exists'
if error is None:
new_team = Team(name=name, description=description)
db.session.add(new_team)
db.session.commit()
flash(f'Team "{name}" created successfully!', 'success')
return redirect(url_for('admin_teams'))
flash(error, 'error')
return render_template('create_team.html', title='Create Team')
@app.route('/admin/teams/edit/<int:team_id>', methods=['GET', 'POST'])
@admin_required
def edit_team(team_id):
team = Team.query.get_or_404(team_id)
if request.method == 'POST':
name = request.form.get('name')
description = request.form.get('description')
# Validate input
error = None
if not name:
error = 'Team name is required'
elif name != team.name and Team.query.filter_by(name=name).first():
error = 'Team name already exists'
if error is None:
team.name = name
team.description = description
db.session.commit()
flash(f'Team "{name}" updated successfully!', 'success')
return redirect(url_for('admin_teams'))
flash(error, 'error')
return render_template('edit_team.html', title='Edit Team', team=team)
@app.route('/admin/teams/delete/<int:team_id>', methods=['POST'])
@admin_required
def delete_team(team_id):
team = Team.query.get_or_404(team_id)
# Check if team has members
if team.users:
flash('Cannot delete team with members. Remove all members first.', 'error')
return redirect(url_for('admin_teams'))
team_name = team.name
db.session.delete(team)
db.session.commit()
flash(f'Team "{team_name}" deleted successfully!', 'success')
return redirect(url_for('admin_teams'))
@app.route('/admin/teams/<int:team_id>', methods=['GET', 'POST'])
@admin_required
def manage_team(team_id):
team = Team.query.get_or_404(team_id)
if request.method == 'POST':
action = request.form.get('action')
if action == 'update_team':
# Update team details
name = request.form.get('name')
description = request.form.get('description')
# Validate input
error = None
if not name:
error = 'Team name is required'
elif name != team.name and Team.query.filter_by(name=name).first():
error = 'Team name already exists'
if error is None:
team.name = name
team.description = description
db.session.commit()
flash(f'Team "{name}" updated successfully!', 'success')
else:
flash(error, 'error')
elif action == 'add_member':
# Add user to team
user_id = request.form.get('user_id')
if user_id:
user = User.query.get(user_id)
if user:
user.team_id = team.id
db.session.commit()
flash(f'User {user.username} added to team!', 'success')
else:
flash('User not found', 'error')
else:
flash('No user selected', 'error')
elif action == 'remove_member':
# Remove user from team
user_id = request.form.get('user_id')
if user_id:
user = User.query.get(user_id)
if user and user.team_id == team.id:
user.team_id = None
db.session.commit()
flash(f'User {user.username} removed from team!', 'success')
else:
flash('User not found or not in this team', 'error')
else:
flash('No user selected', 'error')
# Get team members
team_members = User.query.filter_by(team_id=team.id).all()
# Get users not in this team for the add member form
available_users = User.query.filter(
(User.team_id != team.id) | (User.team_id == None)
).all()
return render_template(
'manage_team.html',
title=f'Manage Team: {team.name}',
team=team,
team_members=team_members,
available_users=available_users
)
if __name__ == '__main__':
app.run(debug=True)