Add Team Roles feature.
This commit is contained in:
257
app.py
257
app.py
@@ -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,7 +146,30 @@ def login():
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if user and user.check_password(password): # Use the check_password method
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
# 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')
|
||||
@@ -127,7 +182,7 @@ def login():
|
||||
|
||||
flash('Login successful!', 'success')
|
||||
return redirect(url_for('home'))
|
||||
else:
|
||||
|
||||
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)
|
||||
89
migrate_roles_teams.py
Normal file
89
migrate_roles_teams.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from app import app, db
|
||||
from models import User, Team, Role, SystemSettings
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def migrate_roles_teams():
|
||||
with app.app_context():
|
||||
logger.info("Starting migration for roles and teams...")
|
||||
|
||||
# Check if the team table exists
|
||||
try:
|
||||
# Create the team table if it doesn't exist
|
||||
db.engine.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS team (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
description VARCHAR(255),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
logger.info("Team table created or already exists")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating team table: {e}")
|
||||
return
|
||||
|
||||
# Check if the user table has the role and team_id columns
|
||||
try:
|
||||
# Check if role column exists
|
||||
result = db.engine.execute(text("PRAGMA table_info(user)"))
|
||||
columns = [row[1] for row in result]
|
||||
|
||||
if 'role' not in columns:
|
||||
# Use the enum name instead of the value
|
||||
db.engine.execute(text("ALTER TABLE user ADD COLUMN role VARCHAR(20) DEFAULT 'TEAM_MEMBER'"))
|
||||
logger.info("Added role column to user table")
|
||||
|
||||
if 'team_id' not in columns:
|
||||
db.engine.execute(text("ALTER TABLE user ADD COLUMN team_id INTEGER REFERENCES team(id)"))
|
||||
logger.info("Added team_id column to user table")
|
||||
|
||||
# Create a default team for existing users
|
||||
default_team = Team.query.filter_by(name="Default Team").first()
|
||||
if not default_team:
|
||||
default_team = Team(name="Default Team", description="Default team for existing users")
|
||||
db.session.add(default_team)
|
||||
db.session.commit()
|
||||
logger.info("Created default team")
|
||||
|
||||
# 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,
|
||||
'ADMIN': Role.ADMIN
|
||||
}
|
||||
|
||||
# Assign all existing users to the default team and set role based on admin status
|
||||
users = User.query.all()
|
||||
for user in users:
|
||||
if user.team_id is None:
|
||||
user.team_id = default_team.id
|
||||
|
||||
# Handle role conversion properly
|
||||
if isinstance(user.role, str):
|
||||
# Try to map the string to an enum value
|
||||
user.role = role_mapping.get(user.role, Role.TEAM_MEMBER)
|
||||
elif user.role is None:
|
||||
# Set default role based on admin status
|
||||
user.role = Role.ADMIN if user.is_admin else Role.TEAM_MEMBER
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"Assigned {len(users)} existing users to default team and updated roles")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating user table: {e}")
|
||||
return
|
||||
|
||||
logger.info("Migration completed successfully")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate_roles_teams()
|
||||
32
models.py
32
models.py
@@ -2,9 +2,31 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
import enum
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
# Define Role as an Enum for better type safety
|
||||
class Role(enum.Enum):
|
||||
TEAM_MEMBER = "Team Member"
|
||||
TEAM_LEADER = "Team Leader"
|
||||
SUPERVISOR = "Supervisor"
|
||||
ADMIN = "Administrator" # Keep existing admin role
|
||||
|
||||
# Create Team model
|
||||
class Team(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False, unique=True)
|
||||
description = db.Column(db.String(255))
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
# Relationship with users (one team has many users)
|
||||
users = db.relationship('User', backref='team', lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Team {self.name}>'
|
||||
|
||||
# Update User model to include role and team relationship
|
||||
class User(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
@@ -18,13 +40,17 @@ class User(db.Model):
|
||||
verification_token = db.Column(db.String(100), unique=True, nullable=True)
|
||||
token_expiry = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# New field for blocking users
|
||||
is_blocked = db.Column(db.Boolean, default=False)
|
||||
|
||||
# New fields for role and team
|
||||
role = db.Column(db.Enum(Role), default=Role.TEAM_MEMBER)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('team.id'), nullable=True)
|
||||
|
||||
# Relationships
|
||||
time_entries = db.relationship('TimeEntry', backref='user', lazy=True)
|
||||
work_config = db.relationship('WorkConfig', backref='user', lazy=True, uselist=False)
|
||||
|
||||
# New field for blocking users
|
||||
is_blocked = db.Column(db.Boolean, default=False)
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
|
||||
47
repair_roles.py
Normal file
47
repair_roles.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from app import app, db
|
||||
from models import User, Role
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def repair_user_roles():
|
||||
with app.app_context():
|
||||
logger.info("Starting user role repair...")
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
users = User.query.all()
|
||||
fixed_count = 0
|
||||
|
||||
for user in users:
|
||||
original_role = user.role
|
||||
|
||||
# Fix role if it's a string or None
|
||||
if isinstance(user.role, str):
|
||||
user.role = role_mapping.get(user.role, Role.TEAM_MEMBER)
|
||||
fixed_count += 1
|
||||
elif user.role is None:
|
||||
user.role = Role.ADMIN if user.is_admin else Role.TEAM_MEMBER
|
||||
fixed_count += 1
|
||||
|
||||
if fixed_count > 0:
|
||||
db.session.commit()
|
||||
logger.info(f"Fixed roles for {fixed_count} users")
|
||||
else:
|
||||
logger.info("No role fixes needed")
|
||||
|
||||
logger.info("Role repair completed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
repair_user_roles()
|
||||
@@ -154,7 +154,7 @@ button {
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@@ -162,6 +162,27 @@ button {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #45a049;
|
||||
color: white;
|
||||
margin-bottom: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
display: inline-block;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
@@ -352,20 +373,6 @@ footer {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0069d9;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
@@ -474,6 +481,14 @@ input[type="time"]::-webkit-datetime-edit {
|
||||
}
|
||||
|
||||
/* Admin Dashboard Styles */
|
||||
.admin-container {
|
||||
padding: 1.5rem;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.admin-panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -597,3 +612,25 @@ input[type="time"]::-webkit-datetime-edit {
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* General table styling */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.data-table th, .data-table td {
|
||||
padding: 0.8rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background-color: #f2f2f2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
@@ -11,6 +11,12 @@
|
||||
<a href="{{ url_for('admin_users') }}" class="btn btn-primary">Manage Users</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<h2>Team Management</h2>
|
||||
<p>Configure teams and their members.</p>
|
||||
<a href="{{ url_for('admin_teams') }}" class="btn btn-primary">Configure</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<h2>System Settings</h2>
|
||||
<p>Configure application-wide settings like registration and more.</p>
|
||||
|
||||
44
templates/admin_teams.html
Normal file
44
templates/admin_teams.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends 'layout.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Team Management</h1>
|
||||
|
||||
<div class="mb-3">
|
||||
<a href="{{ url_for('create_team') }}" class="btn btn-primary">Create New Team</a>
|
||||
</div>
|
||||
|
||||
{% if teams %}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Members</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for team in teams %}
|
||||
<tr>
|
||||
<td>{{ team.name }}</td>
|
||||
<td>{{ team.description }}</td>
|
||||
<td>{{ team.users|length }}</td>
|
||||
<td>{{ team.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('manage_team', team_id=team.id) }}" class="button btn btn-sm btn-info">Manage</a>
|
||||
<form method="POST" action="{{ url_for('delete_team', team_id=team.id) }}" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this team?');">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<p>No teams found. Create a team to get started.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
|
||||
<div class="user-list">
|
||||
<table class="table">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
|
||||
24
templates/create_team.html
Normal file
24
templates/create_team.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="admin-container">
|
||||
<h1>Create New Team</h1>
|
||||
|
||||
<form method="POST" action="{{ url_for('create_team') }}" class="team-form">
|
||||
<div class="form-group">
|
||||
<label for="name">Team Name</label>
|
||||
<input type="text" id="name" name="name" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Create Team</button>
|
||||
<a href="{{ url_for('admin_teams') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -20,6 +20,29 @@
|
||||
<input type="password" id="password" name="password" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="role">Role</label>
|
||||
<select id="role" name="role" class="form-control">
|
||||
{% for role in roles %}
|
||||
<option value="{{ role.name }}" {% if user.role == role %}selected{% endif %}>
|
||||
{{ role.name.replace('_', ' ').title() }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="team_id">Team</label>
|
||||
<select id="team_id" name="team_id" class="form-control">
|
||||
<option value="">-- No Team --</option>
|
||||
{% for team in teams %}
|
||||
<option value="{{ team.id }}" {% if user.team_id == team.id %}selected{% endif %}>
|
||||
{{ team.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" name="is_admin" {% if user.is_admin %}checked{% endif %}> Administrator privileges
|
||||
|
||||
96
templates/manage_team.html
Normal file
96
templates/manage_team.html
Normal file
@@ -0,0 +1,96 @@
|
||||
{% extends 'layout.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Manage Team: {{ team.name }}</h1>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2>Team Details</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('manage_team', team_id=team.id) }}">
|
||||
<input type="hidden" name="action" value="update_team">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Team Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="{{ team.name }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3">{{ team.description }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Update Team</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2>Team Members</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if team_members %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in team_members %}
|
||||
<tr>
|
||||
<td>{{ member.username }}</td>
|
||||
<td>{{ member.email }}</td>
|
||||
<td>{{ member.role.value }}</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('manage_team', team_id=team.id) }}" class="d-inline">
|
||||
<input type="hidden" name="action" value="remove_member">
|
||||
<input type="hidden" name="user_id" value="{{ member.id }}">
|
||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this user from the team?')">
|
||||
Remove
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No members in this team yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Add Team Member</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if available_users %}
|
||||
<form method="POST" action="{{ url_for('manage_team', team_id=team.id) }}">
|
||||
<input type="hidden" name="action" value="add_member">
|
||||
<div class="mb-3">
|
||||
<label for="user_id" class="form-label">Select User</label>
|
||||
<select class="form-select" id="user_id" name="user_id" required>
|
||||
<option value="">-- Select User --</option>
|
||||
{% for user in available_users %}
|
||||
<option value="{{ user.id }}">{{ user.username }} ({{ user.email }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Add to Team</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>No available users to add to this team.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="{{ url_for('admin_teams') }}" class="btn btn-secondary">Back to Teams</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user