mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 12:50:11 -05:00
b4486a627f
- Webhook models: remove duplicate index definitions so db.create_all() no longer raises 'index already exists' (columns already have index=True) - ImportService: fix circular import by late-importing ClientService, ProjectService, TimeTrackingService in __init__ - reports: fix F823 by renaming unpack variable _ to _entry_count to avoid shadowing gettext _ in export_task_excel() - Code quality: add .flake8 with extend-ignore so flake8 CI passes; simplify pyproject.toml isort config (drop unsupported options) - Format: run black and isort on app/ - tests: restore minimal app fixture in test_import_export_models
200 lines
6.9 KiB
Python
200 lines
6.9 KiB
Python
"""
|
|
REFERENCE ONLY — This module is not registered as an active blueprint.
|
|
|
|
Example refactored projects route using service layer and fixing N+1 queries.
|
|
It demonstrates the intended architecture pattern (service layer, eager loading).
|
|
The active routes live in app/routes/projects.py. Do not register this blueprint;
|
|
use it as reference when refactoring or when adding new project routes.
|
|
"""
|
|
|
|
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
|
|
from flask_babel import gettext as _
|
|
from flask_login import current_user, login_required
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
from app import db
|
|
from app.models import Client, Project, TimeEntry, UserFavoriteProject
|
|
from app.repositories import ClientRepository, ProjectRepository, TimeEntryRepository
|
|
from app.services import ProjectService
|
|
from app.utils.permissions import admin_or_permission_required
|
|
|
|
projects_bp = Blueprint("projects", __name__)
|
|
|
|
|
|
@projects_bp.route("/projects")
|
|
@login_required
|
|
def list_projects():
|
|
"""
|
|
List all projects - REFACTORED VERSION
|
|
|
|
This version fixes N+1 queries by using joinedload to eagerly load
|
|
related data (clients) in a single query.
|
|
"""
|
|
from app import track_page_view
|
|
|
|
track_page_view("projects_list")
|
|
|
|
page = request.args.get("page", 1, type=int)
|
|
status = request.args.get("status", "active")
|
|
client_name = request.args.get("client", "").strip()
|
|
search = request.args.get("search", "").strip()
|
|
favorites_only = request.args.get("favorites", "").lower() == "true"
|
|
|
|
# Use repository with eager loading to fix N+1 queries
|
|
project_repo = ProjectRepository()
|
|
query = project_repo.query().options(joinedload(Project.client_obj)) # Eagerly load client to avoid N+1
|
|
|
|
# Filter by favorites if requested
|
|
if favorites_only:
|
|
query = query.join(
|
|
UserFavoriteProject,
|
|
db.and_(UserFavoriteProject.project_id == Project.id, UserFavoriteProject.user_id == current_user.id),
|
|
)
|
|
|
|
# Filter by status
|
|
if status == "active":
|
|
query = query.filter(Project.status == "active")
|
|
elif status == "archived":
|
|
query = query.filter(Project.status == "archived")
|
|
elif status == "inactive":
|
|
query = query.filter(Project.status == "inactive")
|
|
|
|
# Filter by client
|
|
if client_name:
|
|
query = query.join(Client).filter(Client.name == client_name)
|
|
|
|
# Search filter
|
|
if search:
|
|
like = f"%{search}%"
|
|
query = query.filter(db.or_(Project.name.ilike(like), Project.description.ilike(like)))
|
|
|
|
# Paginate with eager loading
|
|
projects_pagination = query.order_by(Project.name).paginate(page=page, per_page=20, error_out=False)
|
|
|
|
# Get user's favorite project IDs (single query)
|
|
favorite_project_ids = {
|
|
fav.project_id for fav in UserFavoriteProject.query.filter_by(user_id=current_user.id).all()
|
|
}
|
|
|
|
# Get clients for filter dropdown (single query)
|
|
client_repo = ClientRepository()
|
|
clients = client_repo.get_active_clients()
|
|
client_list = [c.name for c in clients]
|
|
|
|
return render_template(
|
|
"projects/list.html",
|
|
projects=projects_pagination.items,
|
|
status=status,
|
|
clients=client_list,
|
|
favorite_project_ids=favorite_project_ids,
|
|
favorites_only=favorites_only,
|
|
pagination=projects_pagination,
|
|
)
|
|
|
|
|
|
@projects_bp.route("/projects/<int:project_id>")
|
|
@login_required
|
|
def view_project(project_id):
|
|
"""
|
|
View project details - REFACTORED VERSION
|
|
|
|
This version uses the service layer and fixes N+1 queries.
|
|
"""
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
from app.models import Comment, KanbanColumn, ProjectCost, Task
|
|
from app.repositories import TimeEntryRepository
|
|
|
|
# Use repository to get project with relations
|
|
project_repo = ProjectRepository()
|
|
project = project_repo.get_with_stats(project_id)
|
|
|
|
if not project:
|
|
flash(_("Project not found"), "error")
|
|
return redirect(url_for("projects.list_projects"))
|
|
|
|
# Get time entries with eager loading (fixes N+1)
|
|
time_entry_repo = TimeEntryRepository()
|
|
page = request.args.get("page", 1, type=int)
|
|
|
|
entries_query = (
|
|
time_entry_repo.query()
|
|
.filter(TimeEntry.project_id == project_id, TimeEntry.end_time.isnot(None))
|
|
.options(joinedload(TimeEntry.user), joinedload(TimeEntry.task)) # Eagerly load user # Eagerly load task
|
|
.order_by(TimeEntry.start_time.desc())
|
|
)
|
|
|
|
entries_pagination = entries_query.paginate(page=page, per_page=50, error_out=False)
|
|
|
|
# Get tasks with eager loading
|
|
tasks = (
|
|
Task.query.filter_by(project_id=project_id)
|
|
.options(joinedload(Task.assignee)) # If Task has assignee relationship
|
|
.order_by(Task.priority.desc(), Task.due_date.asc(), Task.created_at.asc())
|
|
.all()
|
|
)
|
|
|
|
# Get user totals (this might need optimization too)
|
|
user_totals = project.get_user_totals()
|
|
|
|
# Get comments with eager loading
|
|
comments = (
|
|
Comment.query.filter_by(project_id=project_id)
|
|
.options(joinedload(Comment.author)) # Eagerly load author
|
|
.order_by(Comment.created_at.desc())
|
|
.all()
|
|
)
|
|
|
|
# Get recent project costs
|
|
recent_costs = (
|
|
ProjectCost.query.filter_by(project_id=project_id).order_by(ProjectCost.cost_date.desc()).limit(5).all()
|
|
)
|
|
|
|
# Get kanban columns
|
|
kanban_columns = KanbanColumn.get_active_columns(project_id=project_id) if KanbanColumn else []
|
|
|
|
return render_template(
|
|
"projects/view.html",
|
|
project=project,
|
|
entries=entries_pagination.items,
|
|
entries_pagination=entries_pagination,
|
|
tasks=tasks,
|
|
user_totals=user_totals,
|
|
comments=comments,
|
|
recent_costs=recent_costs,
|
|
kanban_columns=kanban_columns,
|
|
)
|
|
|
|
|
|
@projects_bp.route("/projects/create", methods=["GET", "POST"])
|
|
@login_required
|
|
@admin_or_permission_required("create_projects")
|
|
def create_project():
|
|
"""
|
|
Create a new project - REFACTORED VERSION using service layer
|
|
"""
|
|
if request.method == "POST":
|
|
# Use service layer for business logic
|
|
project_service = ProjectService()
|
|
|
|
result = project_service.create_project(
|
|
name=request.form.get("name", "").strip(),
|
|
client_id=request.form.get("client_id", type=int),
|
|
description=request.form.get("description", "").strip() or None,
|
|
billable=request.form.get("billable") == "on",
|
|
hourly_rate=request.form.get("hourly_rate", type=float),
|
|
created_by=current_user.id,
|
|
)
|
|
|
|
if result["success"]:
|
|
flash(_("Project created successfully"), "success")
|
|
return redirect(url_for("projects.view_project", project_id=result["project"].id))
|
|
else:
|
|
flash(_(result["message"]), "error")
|
|
|
|
# GET request - show form
|
|
client_repo = ClientRepository()
|
|
clients = client_repo.get_active_clients()
|
|
|
|
return render_template("projects/create.html", clients=clients)
|