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
269 lines
11 KiB
Python
269 lines
11 KiB
Python
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 app import db, log_event, track_event
|
|
from app.models import Client, ClientNote, Settings
|
|
from app.utils.db import safe_commit
|
|
from app.utils.module_registry import ModuleRegistry
|
|
|
|
client_notes_bp = Blueprint("client_notes", __name__)
|
|
|
|
|
|
@client_notes_bp.before_request
|
|
def _enforce_clients_module():
|
|
"""Client notes are part of Clients; allow admins only when disabled."""
|
|
if not current_user or not getattr(current_user, "is_authenticated", False):
|
|
return None
|
|
|
|
settings = Settings.get_settings()
|
|
if ModuleRegistry.is_enabled("clients", settings, current_user):
|
|
return None
|
|
|
|
flash(_("Clients module is disabled by the administrator."), "warning")
|
|
return redirect(url_for("main.dashboard"))
|
|
|
|
|
|
@client_notes_bp.route("/clients/<int:client_id>/notes/create", methods=["POST"])
|
|
@login_required
|
|
def create_note(client_id):
|
|
"""Create a new note for a client"""
|
|
# Verify client exists first (before try block to let 404 abort properly)
|
|
client = Client.query.get_or_404(client_id)
|
|
|
|
try:
|
|
content = request.form.get("content", "").strip()
|
|
is_important = request.form.get("is_important", "false").lower() == "true"
|
|
|
|
# Validation
|
|
if not content:
|
|
flash(_("Note content cannot be empty"), "error")
|
|
return redirect(url_for("clients.view_client", client_id=client_id))
|
|
|
|
# Create the note
|
|
note = ClientNote(content=content, user_id=current_user.id, client_id=client_id, is_important=is_important)
|
|
|
|
db.session.add(note)
|
|
if safe_commit("create_client_note", {"client_id": client_id}):
|
|
# Log note creation
|
|
log_event("client_note.created", user_id=current_user.id, client_note_id=note.id, client_id=client_id)
|
|
track_event(current_user.id, "client_note.created", {"note_id": note.id, "client_id": client_id})
|
|
flash(_("Note added successfully"), "success")
|
|
else:
|
|
flash(_("Error adding note"), "error")
|
|
|
|
except ValueError as e:
|
|
flash(_("Error adding note: %(error)s", error=str(e)), "error")
|
|
except Exception as e:
|
|
flash(_("Error adding note: %(error)s", error=str(e)), "error")
|
|
|
|
# Redirect back to the client page
|
|
return redirect(url_for("clients.view_client", client_id=client_id))
|
|
|
|
|
|
@client_notes_bp.route("/clients/<int:client_id>/notes/<int:note_id>/edit", methods=["GET", "POST"])
|
|
@login_required
|
|
def edit_note(client_id, note_id):
|
|
"""Edit an existing client note"""
|
|
note = ClientNote.query.get_or_404(note_id)
|
|
|
|
# Verify note belongs to this client
|
|
if note.client_id != client_id:
|
|
flash(_("Note does not belong to this client"), "error")
|
|
return redirect(url_for("clients.view_client", client_id=client_id))
|
|
|
|
# Check permissions
|
|
if not note.can_edit(current_user):
|
|
flash(_("You do not have permission to edit this note"), "error")
|
|
return redirect(url_for("clients.view_client", client_id=client_id))
|
|
|
|
if request.method == "POST":
|
|
try:
|
|
content = request.form.get("content", "").strip()
|
|
is_important = request.form.get("is_important", "false").lower() == "true"
|
|
|
|
if not content:
|
|
flash(_("Note content cannot be empty"), "error")
|
|
return render_template("client_notes/edit.html", note=note, client_id=client_id)
|
|
|
|
note.edit_content(content, current_user, is_important=is_important)
|
|
|
|
if not safe_commit("edit_client_note", {"note_id": note_id}):
|
|
flash(_("Error updating note"), "error")
|
|
return render_template("client_notes/edit.html", note=note, client_id=client_id)
|
|
|
|
# Log note update
|
|
log_event("client_note.updated", user_id=current_user.id, client_note_id=note.id)
|
|
track_event(current_user.id, "client_note.updated", {"note_id": note.id})
|
|
|
|
flash(_("Note updated successfully"), "success")
|
|
return redirect(url_for("clients.view_client", client_id=client_id))
|
|
|
|
except ValueError as e:
|
|
flash(_("Error updating note: %(error)s", error=str(e)), "error")
|
|
except Exception as e:
|
|
flash(_("Error updating note: %(error)s", error=str(e)), "error")
|
|
|
|
return render_template("client_notes/edit.html", note=note, client_id=client_id)
|
|
|
|
|
|
@client_notes_bp.route("/clients/<int:client_id>/notes/<int:note_id>/delete", methods=["POST"])
|
|
@login_required
|
|
def delete_note(client_id, note_id):
|
|
"""Delete a client note"""
|
|
note = ClientNote.query.get_or_404(note_id)
|
|
|
|
# Verify note belongs to this client
|
|
if note.client_id != client_id:
|
|
flash(_("Note does not belong to this client"), "error")
|
|
return redirect(url_for("clients.view_client", client_id=client_id))
|
|
|
|
# Check permissions
|
|
if not note.can_delete(current_user):
|
|
flash(_("You do not have permission to delete this note"), "error")
|
|
return redirect(url_for("clients.view_client", client_id=client_id))
|
|
|
|
try:
|
|
note_id_for_log = note.id
|
|
|
|
db.session.delete(note)
|
|
|
|
if not safe_commit("delete_client_note", {"note_id": note_id}):
|
|
flash(_("Error deleting note"), "error")
|
|
return redirect(url_for("clients.view_client", client_id=client_id))
|
|
|
|
# Log note deletion
|
|
log_event("client_note.deleted", user_id=current_user.id, client_note_id=note_id_for_log)
|
|
track_event(current_user.id, "client_note.deleted", {"note_id": note_id_for_log})
|
|
|
|
flash(_("Note deleted successfully"), "success")
|
|
|
|
except Exception as e:
|
|
flash(_("Error deleting note: %(error)s", error=str(e)), "error")
|
|
|
|
return redirect(url_for("clients.view_client", client_id=client_id))
|
|
|
|
|
|
@client_notes_bp.route("/clients/<int:client_id>/notes/<int:note_id>/toggle-important", methods=["POST"])
|
|
def toggle_important(client_id, note_id):
|
|
"""Toggle the important flag on a client note"""
|
|
# Explicit auth check to avoid redirect behavior from login_required for JSON flows
|
|
if not getattr(current_user, "is_authenticated", False):
|
|
return jsonify({"error": "Authentication required"}), 401
|
|
note = ClientNote.query.get_or_404(note_id)
|
|
|
|
# Verify note belongs to this client
|
|
if note.client_id != client_id:
|
|
return jsonify({"error": "Note does not belong to this client"}), 400
|
|
|
|
# Check permissions
|
|
if not note.can_edit(current_user):
|
|
return jsonify({"error": "Permission denied"}), 403
|
|
|
|
try:
|
|
note.is_important = not note.is_important
|
|
|
|
if not safe_commit("toggle_important_note", {"note_id": note_id}):
|
|
return jsonify({"error": "Error updating note"}), 500
|
|
|
|
# Log note update
|
|
log_event(
|
|
"client_note.importance_toggled",
|
|
user_id=current_user.id,
|
|
client_note_id=note.id,
|
|
is_important=note.is_important,
|
|
)
|
|
track_event(
|
|
current_user.id, "client_note.importance_toggled", {"note_id": note.id, "is_important": note.is_important}
|
|
)
|
|
|
|
return jsonify({"success": True, "is_important": note.is_important})
|
|
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@client_notes_bp.route("/api/clients/<int:client_id>/notes")
|
|
def list_notes(client_id):
|
|
"""API endpoint to get notes for a client"""
|
|
# Explicit auth check to avoid redirect behavior from login_required for JSON flows
|
|
if not getattr(current_user, "is_authenticated", False):
|
|
return jsonify({"error": "Authentication required"}), 401
|
|
order_by_important = request.args.get("order_by_important", "false").lower() == "true"
|
|
|
|
try:
|
|
# Verify client exists
|
|
client = Client.query.get_or_404(client_id)
|
|
notes = ClientNote.get_client_notes(client_id, order_by_important)
|
|
|
|
return jsonify({"success": True, "notes": [note.to_dict() for note in notes]})
|
|
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@client_notes_bp.route("/api/client-notes/<int:note_id>")
|
|
def get_note(note_id):
|
|
"""API endpoint to get a single client note"""
|
|
# Explicit auth check to avoid redirect behavior from login_required for JSON flows
|
|
if not getattr(current_user, "is_authenticated", False):
|
|
return jsonify({"error": "Authentication required"}), 401
|
|
try:
|
|
note = ClientNote.query.get_or_404(note_id)
|
|
return jsonify({"success": True, "note": note.to_dict()})
|
|
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@client_notes_bp.route("/api/client-notes/important")
|
|
def get_important_notes():
|
|
"""API endpoint to get all important client notes"""
|
|
# Explicit auth check to avoid redirect behavior from login_required for JSON flows
|
|
if not getattr(current_user, "is_authenticated", False):
|
|
return jsonify({"error": "Authentication required"}), 401
|
|
client_id = request.args.get("client_id", type=int)
|
|
|
|
try:
|
|
notes = ClientNote.get_important_notes(client_id)
|
|
return jsonify({"success": True, "notes": [note.to_dict() for note in notes]})
|
|
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@client_notes_bp.route("/api/client-notes/recent")
|
|
def get_recent_notes():
|
|
"""API endpoint to get recent client notes"""
|
|
# Explicit auth check to avoid redirect behavior from login_required for JSON flows
|
|
if not getattr(current_user, "is_authenticated", False):
|
|
return jsonify({"error": "Authentication required"}), 401
|
|
limit = request.args.get("limit", 10, type=int)
|
|
|
|
try:
|
|
notes = ClientNote.get_recent_notes(limit)
|
|
return jsonify({"success": True, "notes": [note.to_dict() for note in notes]})
|
|
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@client_notes_bp.route("/api/client-notes/user/<int:user_id>")
|
|
def get_user_notes(user_id):
|
|
"""API endpoint to get notes by a specific user"""
|
|
# Explicit auth check to avoid redirect behavior from login_required for JSON flows
|
|
if not getattr(current_user, "is_authenticated", False):
|
|
return jsonify({"error": "Authentication required"}), 401
|
|
limit = request.args.get("limit", type=int)
|
|
|
|
# Only allow users to see their own notes unless they're admin
|
|
if not current_user.is_admin and current_user.id != user_id:
|
|
return jsonify({"error": "Permission denied"}), 403
|
|
|
|
try:
|
|
notes = ClientNote.get_user_notes(user_id, limit)
|
|
return jsonify({"success": True, "notes": [note.to_dict() for note in notes]})
|
|
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|