Squashed commit of the following:

commit 1eeea9f83ad9230a5c1f7a75662770eaab0df837
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 21:15:41 2025 +0200

    Disable resuming of old time entries.

commit 3e3ec2f01cb7943622b819a19179388078ae1315
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 20:59:19 2025 +0200

    Refactor db migrations.

commit 15a51a569da36c6b7c9e01ab17b6fdbdee6ad994
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 19:58:04 2025 +0200

    Apply new style for Time Tracking view.

commit 77e5278b303e060d2b03853b06277f8aa567ae68
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 18:06:04 2025 +0200

    Allow direct registrations as a Company.

commit 188a8772757cbef374243d3a5f29e4440ddecabe
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 18:04:45 2025 +0200

    Add email invitation feature.

commit d9ebaa02aa01b518960a20dccdd5a327d82f30c6
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 17:12:32 2025 +0200

    Apply common style for Company, User, Team management pages.

commit 81149caf4d8fc6317e2ab1b4f022b32fc5aa6d22
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 16:44:32 2025 +0200

    Move export functions to own module.

commit 1a26e19338e73f8849c671471dd15cc3c1b1fe82
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 15:51:15 2025 +0200

    Split up models.py.

commit 61f1ccd10f721b0ff4dc1eccf30c7a1ee13f204d
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 12:05:28 2025 +0200

    Move utility function into own modules.

commit 84b341ed35e2c5387819a8b9f9d41eca900ae79f
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 11:44:24 2025 +0200

    Refactor auth functions use.

commit 923e311e3da5b26d85845c2832b73b7b17c48adb
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 11:35:52 2025 +0200

    Refactor route nameing and fix bugs along the way.

commit f0a5c4419c340e62a2615c60b2a9de28204d2995
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 10:34:33 2025 +0200

    Fix URL endpoints in announcement template.

commit b74d74542a1c8dc350749e4788a9464d067a88b5
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 09:25:53 2025 +0200

    Move announcements to own module.

commit 9563a28021ac46c82c04fe4649b394dbf96f92c7
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 09:16:30 2025 +0200

    Combine Company view and edit templates.

commit 6687c373e681d54e4deab6b2582fed5cea9aadf6
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 08:17:42 2025 +0200

    Move Users, Company and System Administration to own modules.

commit 8b7894a2e3eb84bb059f546648b6b9536fea724e
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 07:40:57 2025 +0200

    Move Teams and Projects to own modules.

commit d11bf059d99839ecf1f5d7020b8c8c8a2454c00b
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 07:09:33 2025 +0200

    Move Tasks and Sprints to own modules.
This commit is contained in:
2025-07-07 21:16:36 +02:00
parent 4214e88d18
commit 9a79778ad6
116 changed files with 21063 additions and 5653 deletions

224
routes/sprints_api.py Normal file
View File

@@ -0,0 +1,224 @@
"""
Sprint Management API Routes
Handles all sprint-related API endpoints
"""
from flask import Blueprint, jsonify, request, g
from datetime import datetime
from models import db, Role, Project, Sprint, SprintStatus, Task
from routes.auth import login_required, role_required, company_required
import logging
logger = logging.getLogger(__name__)
sprints_api_bp = Blueprint('sprints_api', __name__, url_prefix='/api')
@sprints_api_bp.route('/sprints')
@role_required(Role.TEAM_MEMBER)
@company_required
def get_sprints():
"""Get all sprints for the user's company"""
try:
# Base query for sprints in user's company
query = Sprint.query.filter(Sprint.company_id == g.user.company_id)
# Apply access restrictions based on user role and team
if g.user.role not in [Role.ADMIN, Role.SUPERVISOR]:
# Regular users can only see sprints they have access to
accessible_sprint_ids = []
sprints = query.all()
for sprint in sprints:
if sprint.can_user_access(g.user):
accessible_sprint_ids.append(sprint.id)
if accessible_sprint_ids:
query = query.filter(Sprint.id.in_(accessible_sprint_ids))
else:
# No accessible sprints, return empty list
return jsonify({'success': True, 'sprints': []})
sprints = query.order_by(Sprint.created_at.desc()).all()
sprint_list = []
for sprint in sprints:
task_summary = sprint.get_task_summary()
sprint_data = {
'id': sprint.id,
'name': sprint.name,
'description': sprint.description,
'status': sprint.status.name,
'company_id': sprint.company_id,
'project_id': sprint.project_id,
'project_name': sprint.project.name if sprint.project else None,
'project_code': sprint.project.code if sprint.project else None,
'start_date': sprint.start_date.isoformat(),
'end_date': sprint.end_date.isoformat(),
'goal': sprint.goal,
'capacity_hours': sprint.capacity_hours,
'created_by_id': sprint.created_by_id,
'created_by_name': sprint.created_by.username if sprint.created_by else None,
'created_at': sprint.created_at.isoformat(),
'is_current': sprint.is_current,
'duration_days': sprint.duration_days,
'days_remaining': sprint.days_remaining,
'progress_percentage': sprint.progress_percentage,
'task_summary': task_summary
}
sprint_list.append(sprint_data)
return jsonify({'success': True, 'sprints': sprint_list})
except Exception as e:
logger.error(f"Error in get_sprints: {str(e)}")
return jsonify({'success': False, 'message': str(e)})
@sprints_api_bp.route('/sprints', methods=['POST'])
@role_required(Role.TEAM_LEADER) # Team leaders and above can create sprints
@company_required
def create_sprint():
"""Create a new sprint"""
try:
data = request.get_json()
# Validate required fields
name = data.get('name')
start_date = data.get('start_date')
end_date = data.get('end_date')
if not name:
return jsonify({'success': False, 'message': 'Sprint name is required'})
if not start_date:
return jsonify({'success': False, 'message': 'Start date is required'})
if not end_date:
return jsonify({'success': False, 'message': 'End date is required'})
# Parse dates
try:
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
except ValueError:
return jsonify({'success': False, 'message': 'Invalid date format'})
if start_date >= end_date:
return jsonify({'success': False, 'message': 'End date must be after start date'})
# Verify project access if project is specified
project_id = data.get('project_id')
if project_id:
project = Project.query.filter_by(id=project_id, company_id=g.user.company_id).first()
if not project or not project.is_user_allowed(g.user):
return jsonify({'success': False, 'message': 'Project not found or access denied'})
# Create sprint
sprint = Sprint(
name=name,
description=data.get('description', ''),
status=SprintStatus[data.get('status', 'PLANNING')],
company_id=g.user.company_id,
project_id=int(project_id) if project_id else None,
start_date=start_date,
end_date=end_date,
goal=data.get('goal'),
capacity_hours=int(data.get('capacity_hours')) if data.get('capacity_hours') else None,
created_by_id=g.user.id
)
db.session.add(sprint)
db.session.commit()
return jsonify({'success': True, 'message': 'Sprint created successfully'})
except Exception as e:
db.session.rollback()
logger.error(f"Error creating sprint: {str(e)}")
return jsonify({'success': False, 'message': str(e)})
@sprints_api_bp.route('/sprints/<int:sprint_id>', methods=['PUT'])
@role_required(Role.TEAM_LEADER)
@company_required
def update_sprint(sprint_id):
"""Update an existing sprint"""
try:
sprint = Sprint.query.filter_by(id=sprint_id, company_id=g.user.company_id).first()
if not sprint or not sprint.can_user_access(g.user):
return jsonify({'success': False, 'message': 'Sprint not found or access denied'})
data = request.get_json()
# Update sprint fields
if 'name' in data:
sprint.name = data['name']
if 'description' in data:
sprint.description = data['description']
if 'status' in data:
sprint.status = SprintStatus[data['status']]
if 'goal' in data:
sprint.goal = data['goal']
if 'capacity_hours' in data:
sprint.capacity_hours = int(data['capacity_hours']) if data['capacity_hours'] else None
if 'project_id' in data:
project_id = data['project_id']
if project_id:
project = Project.query.filter_by(id=project_id, company_id=g.user.company_id).first()
if not project or not project.is_user_allowed(g.user):
return jsonify({'success': False, 'message': 'Project not found or access denied'})
sprint.project_id = int(project_id)
else:
sprint.project_id = None
# Update dates if provided
if 'start_date' in data:
try:
sprint.start_date = datetime.strptime(data['start_date'], '%Y-%m-%d').date()
except ValueError:
return jsonify({'success': False, 'message': 'Invalid start date format'})
if 'end_date' in data:
try:
sprint.end_date = datetime.strptime(data['end_date'], '%Y-%m-%d').date()
except ValueError:
return jsonify({'success': False, 'message': 'Invalid end date format'})
# Validate date order
if sprint.start_date >= sprint.end_date:
return jsonify({'success': False, 'message': 'End date must be after start date'})
db.session.commit()
return jsonify({'success': True, 'message': 'Sprint updated successfully'})
except Exception as e:
db.session.rollback()
logger.error(f"Error updating sprint: {str(e)}")
return jsonify({'success': False, 'message': str(e)})
@sprints_api_bp.route('/sprints/<int:sprint_id>', methods=['DELETE'])
@role_required(Role.TEAM_LEADER)
@company_required
def delete_sprint(sprint_id):
"""Delete a sprint and remove it from all associated tasks"""
try:
sprint = Sprint.query.filter_by(id=sprint_id, company_id=g.user.company_id).first()
if not sprint or not sprint.can_user_access(g.user):
return jsonify({'success': False, 'message': 'Sprint not found or access denied'})
# Remove sprint assignment from all tasks
Task.query.filter_by(sprint_id=sprint_id).update({'sprint_id': None})
# Delete the sprint
db.session.delete(sprint)
db.session.commit()
return jsonify({'success': True, 'message': 'Sprint deleted successfully'})
except Exception as e:
db.session.rollback()
logger.error(f"Error deleting sprint: {str(e)}")
return jsonify({'success': False, 'message': str(e)})