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:
224
routes/sprints_api.py
Normal file
224
routes/sprints_api.py
Normal 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)})
|
||||
Reference in New Issue
Block a user