progression 50% pour evolution des joueurs
CI - Security, Lint & Tests / validate (push) Failing after 1m13s
CI - Security, Lint & Tests / validate (push) Failing after 1m13s
This commit is contained in:
@@ -0,0 +1,661 @@
|
||||
"""Player Evolution — Objective management routes.
|
||||
|
||||
Allows coaches and staff to create, update, delete, and list
|
||||
KPI-driven objectives for individual players or teams.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||
from flask_babel import gettext as _
|
||||
from flask_login import current_user, login_required
|
||||
from marshmallow import ValidationError
|
||||
|
||||
from app.api import json_endpoint
|
||||
from app.extensions import db
|
||||
from app.forms import flash_validation_errors, form_payload
|
||||
from app.models import (
|
||||
DEFAULT_OBJECTIVE_TEMPLATES,
|
||||
OBJECTIVE_CHECK_FREQUENCIES,
|
||||
OBJECTIVE_PRIORITIES,
|
||||
OBJECTIVE_STATUSES,
|
||||
OBJECTIVE_TYPES,
|
||||
Admin,
|
||||
Coach,
|
||||
Manager,
|
||||
Objective,
|
||||
ObjectiveComment,
|
||||
ObjectiveEntry,
|
||||
OrgTeam,
|
||||
PersonalNote,
|
||||
TeamNote,
|
||||
User,
|
||||
)
|
||||
from app.pagination import paginate
|
||||
from app.permissions import (
|
||||
can_manage_objective,
|
||||
can_manage_objectives,
|
||||
coach_org_team_ids,
|
||||
coach_player_ids,
|
||||
)
|
||||
from app.validators import ObjectiveSchema
|
||||
|
||||
objectives_bp = Blueprint('objectives', __name__, url_prefix='/objectives')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _resolve_objective(obj_id):
|
||||
"""Load an objective or 404."""
|
||||
return db.get_or_404(Objective, obj_id)
|
||||
|
||||
|
||||
def _can_edit(user, objective):
|
||||
"""Short-hand permission check with flash fallback."""
|
||||
if can_manage_objective(user, objective):
|
||||
return True
|
||||
flash(_('You do not have permission to manage this objective.'), 'danger')
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# List / Dashboard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@objectives_bp.route('')
|
||||
@login_required
|
||||
def list_objectives():
|
||||
"""List all objectives the current user may see."""
|
||||
user = current_user
|
||||
can_edit = can_manage_objectives(user)
|
||||
|
||||
# ── Query base ──────────────────────────────────────────────────────
|
||||
base = Objective.query
|
||||
|
||||
# Scope: players only see their own, coaches/admins see theirs, etc.
|
||||
if isinstance(user, Manager):
|
||||
base = base.filter(Objective.created_by == user.id)
|
||||
elif isinstance(user, Coach):
|
||||
team_ids = coach_org_team_ids(user)
|
||||
player_ids = coach_player_ids(user)
|
||||
conditions = [Objective.created_by == user.id]
|
||||
if team_ids:
|
||||
conditions.append(Objective.team_id.in_(team_ids))
|
||||
if player_ids:
|
||||
conditions.append(Objective.player_id.in_(player_ids))
|
||||
base = base.filter(db.or_(*conditions))
|
||||
elif isinstance(user, Admin):
|
||||
pass # all
|
||||
elif hasattr(user, 'role') and user.role == 'player':
|
||||
base = base.filter(Objective.player_id == user.id)
|
||||
else:
|
||||
base = base.filter(Objective.created_by == user.id)
|
||||
# ── Tab / filter ────────────────────────────────────────────────────
|
||||
tab = request.args.get('tab', 'all')
|
||||
if tab == 'kpi':
|
||||
base = base.filter(Objective.kpi_metric.isnot(None))
|
||||
elif tab in ('modeles', 'templates'):
|
||||
base = base.filter(Objective.is_template.is_(True))
|
||||
|
||||
search = request.args.get('search', '').strip()
|
||||
if search:
|
||||
base = base.filter(Objective.title.ilike(f'%{search}%'))
|
||||
|
||||
f_status = request.args.get('status', '').strip()
|
||||
f_priority = request.args.get('priority', '').strip()
|
||||
f_type = request.args.get('type', '').strip()
|
||||
f_team = request.args.get('team', type=int)
|
||||
|
||||
if f_status:
|
||||
base = base.filter(Objective.status == f_status)
|
||||
if f_priority:
|
||||
base = base.filter(Objective.priority == f_priority)
|
||||
if f_type:
|
||||
base = base.filter(Objective.type == f_type)
|
||||
if f_team:
|
||||
base = base.filter(Objective.team_id == f_team)
|
||||
|
||||
sort_column = request.args.get('sort', 'due_date')
|
||||
sort_order = request.args.get('order', 'asc')
|
||||
if sort_order not in ('asc', 'desc'):
|
||||
sort_order = 'asc'
|
||||
|
||||
sort_map = {
|
||||
'title': Objective.title,
|
||||
'priority': Objective.priority,
|
||||
'status': Objective.status,
|
||||
'progress': Objective.progress,
|
||||
'due_date': Objective.due_date,
|
||||
'created_at': Objective.created_at,
|
||||
}
|
||||
sort_expr = sort_map.get(sort_column, Objective.due_date)
|
||||
if sort_order == 'desc':
|
||||
sort_expr = sort_expr.desc()
|
||||
else:
|
||||
sort_expr = sort_expr.asc()
|
||||
|
||||
objectives_page = paginate(base.order_by(sort_expr, Objective.id))
|
||||
|
||||
all_q = Objective.query
|
||||
count_total = all_q.count()
|
||||
count_kpi = all_q.filter(Objective.kpi_metric.isnot(None)).count()
|
||||
count_templates = all_q.filter(Objective.is_template.is_(True)).count()
|
||||
success_rate = 0.0
|
||||
total_with_status = all_q.count()
|
||||
if total_with_status > 0:
|
||||
completed = all_q.filter(Objective.status == 'completed').count()
|
||||
success_rate = round((completed / total_with_status) * 100, 1)
|
||||
|
||||
teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
||||
players = User.query.filter(User.role == 'player').order_by(User.username).all()
|
||||
templates = Objective.query.filter_by(is_template=True).order_by(Objective.title).all()
|
||||
|
||||
return render_template(
|
||||
'pages/objectives.html',
|
||||
objectives=objectives_page,
|
||||
pagination=objectives_page,
|
||||
can_edit=can_edit,
|
||||
tab=tab,
|
||||
search=search,
|
||||
f_status=f_status,
|
||||
f_priority=f_priority,
|
||||
f_type=f_type,
|
||||
f_team=f_team,
|
||||
sort_column=sort_column,
|
||||
sort_order=sort_order,
|
||||
count_total=count_total,
|
||||
count_kpi=count_kpi,
|
||||
count_templates=count_templates,
|
||||
success_rate=success_rate,
|
||||
objective_types=OBJECTIVE_TYPES,
|
||||
objective_statuses=OBJECTIVE_STATUSES,
|
||||
objective_priorities=OBJECTIVE_PRIORITIES,
|
||||
objective_frequencies=OBJECTIVE_CHECK_FREQUENCIES,
|
||||
teams=teams,
|
||||
players=players,
|
||||
templates=templates,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Create
|
||||
# ---------------------------------------------------------------------------
|
||||
# New / Edit form page
|
||||
# ---------------------------------------------------------------------------
|
||||
# ---------------------------------------------------------------------------
|
||||
# View (player-facing detail page)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@objectives_bp.route('/<int:objective_id>')
|
||||
@login_required
|
||||
def view_objective(objective_id):
|
||||
"""Player-facing detail page for a specific objective."""
|
||||
from app.permissions import can_view_objective as _can_view
|
||||
|
||||
objective = _resolve_objective(objective_id)
|
||||
if not _can_view(current_user, objective):
|
||||
flash(_('You do not have permission to view this objective.'), 'danger')
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
can_edit = can_manage_objectives(current_user)
|
||||
entries = (
|
||||
ObjectiveEntry.query.filter_by(objective_id=objective.id)
|
||||
.order_by(ObjectiveEntry.recorded_at)
|
||||
.all()
|
||||
)
|
||||
comments = (
|
||||
ObjectiveComment.query.filter_by(objective_id=objective.id)
|
||||
.order_by(ObjectiveComment.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
personal_notes = []
|
||||
if objective.player_id:
|
||||
personal_notes = (
|
||||
PersonalNote.query.filter_by(player_id=objective.player_id)
|
||||
.order_by(PersonalNote.created_at.desc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
team_notes = []
|
||||
if objective.team_id:
|
||||
team_notes = (
|
||||
TeamNote.query.filter_by(org_team_id=objective.team_id)
|
||||
.order_by(TeamNote.created_at.desc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'pages/objective_view.html',
|
||||
objective=objective,
|
||||
entries=entries,
|
||||
comments=comments,
|
||||
personal_notes=personal_notes,
|
||||
team_notes=team_notes,
|
||||
can_edit=can_edit,
|
||||
objective_types=OBJECTIVE_TYPES,
|
||||
objective_statuses=OBJECTIVE_STATUSES,
|
||||
objective_priorities=OBJECTIVE_PRIORITIES,
|
||||
objective_frequencies=OBJECTIVE_CHECK_FREQUENCIES,
|
||||
)
|
||||
|
||||
|
||||
def _form_context():
|
||||
"""Shared context for the create/edit form page."""
|
||||
teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
||||
players = User.query.filter(User.role == 'player').order_by(User.username).all()
|
||||
templates = Objective.query.filter_by(is_template=True).order_by(Objective.title).all()
|
||||
return {
|
||||
'objective_types': OBJECTIVE_TYPES,
|
||||
'objective_statuses': OBJECTIVE_STATUSES,
|
||||
'objective_priorities': OBJECTIVE_PRIORITIES,
|
||||
'objective_frequencies': OBJECTIVE_CHECK_FREQUENCIES,
|
||||
'teams': teams,
|
||||
'players': players,
|
||||
'templates': templates,
|
||||
}
|
||||
|
||||
|
||||
@objectives_bp.route('/new')
|
||||
@login_required
|
||||
def new_objective():
|
||||
"""Render the create-objective form page with preview."""
|
||||
if not can_manage_objectives(current_user):
|
||||
flash(_('You do not have permission to create objectives.'), 'danger')
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
ctx = _form_context()
|
||||
ctx['objective'] = None
|
||||
ctx['edit_mode'] = False
|
||||
ctx['entries'] = []
|
||||
ctx['comments'] = []
|
||||
ctx['personal_notes'] = []
|
||||
ctx['team_notes'] = []
|
||||
return render_template('pages/objective_form.html', **ctx)
|
||||
|
||||
|
||||
@objectives_bp.route('/<int:objective_id>/edit')
|
||||
@login_required
|
||||
def edit_objective(objective_id):
|
||||
"""Render the edit-objective form page with preview."""
|
||||
objective = _resolve_objective(objective_id)
|
||||
if not _can_edit(current_user, objective):
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
ctx = _form_context()
|
||||
ctx['objective'] = objective
|
||||
ctx['edit_mode'] = True
|
||||
ctx['entries'] = (
|
||||
ObjectiveEntry.query.filter_by(objective_id=objective.id)
|
||||
.order_by(ObjectiveEntry.recorded_at)
|
||||
.all()
|
||||
)
|
||||
ctx['comments'] = (
|
||||
ObjectiveComment.query.filter_by(objective_id=objective.id)
|
||||
.order_by(ObjectiveComment.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
ctx['personal_notes'] = []
|
||||
if objective.player_id:
|
||||
ctx['personal_notes'] = (
|
||||
PersonalNote.query.filter_by(player_id=objective.player_id)
|
||||
.order_by(PersonalNote.created_at.desc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
ctx['team_notes'] = []
|
||||
if objective.team_id:
|
||||
ctx['team_notes'] = (
|
||||
TeamNote.query.filter_by(org_team_id=objective.team_id)
|
||||
.order_by(TeamNote.created_at.desc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
return render_template('pages/objective_form.html', **ctx)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@objectives_bp.route('/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_objective():
|
||||
"""Create a new objective."""
|
||||
if not can_manage_objectives(current_user):
|
||||
flash(_('You do not have permission to create objectives.'), 'danger')
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
try:
|
||||
data = ObjectiveSchema().load(
|
||||
form_payload(list_fields=(), optional_blank=('password', 'check_frequency'))
|
||||
)
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
objective = Objective(
|
||||
title=data['title'],
|
||||
description=data.get('description'),
|
||||
type=data['type'],
|
||||
status=data['status'],
|
||||
priority=data['priority'],
|
||||
category=data.get('category'),
|
||||
is_template=data.get('is_template', False),
|
||||
player_id=data.get('player_id'),
|
||||
team_id=data.get('team_id'),
|
||||
kpi_metric=data.get('kpi_metric'),
|
||||
kpi_target=data.get('kpi_target'),
|
||||
kpi_unit=data.get('kpi_unit'),
|
||||
kpi_precision=data.get('kpi_precision'),
|
||||
start_date=data.get('start_date'),
|
||||
due_date=data['due_date'],
|
||||
check_frequency=data.get('check_frequency') or None,
|
||||
progress=data.get('progress', 0),
|
||||
tags=data.get('tags'),
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.session.add(objective)
|
||||
db.session.commit()
|
||||
flash(_('Objective created successfully.'), 'success')
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Update
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@objectives_bp.route('/<int:objective_id>/update', methods=['POST'])
|
||||
@login_required
|
||||
def update_objective(objective_id):
|
||||
"""Update an existing objective."""
|
||||
objective = _resolve_objective(objective_id)
|
||||
if not _can_edit(current_user, objective):
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
try:
|
||||
data = ObjectiveSchema().load(
|
||||
form_payload(list_fields=(), optional_blank=('password', 'check_frequency')),
|
||||
partial=('due_date',),
|
||||
)
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
for field in (
|
||||
'title',
|
||||
'description',
|
||||
'type',
|
||||
'status',
|
||||
'priority',
|
||||
'category',
|
||||
'is_template',
|
||||
'player_id',
|
||||
'team_id',
|
||||
'kpi_metric',
|
||||
'kpi_unit',
|
||||
'kpi_precision',
|
||||
'check_frequency',
|
||||
'progress',
|
||||
'tags',
|
||||
):
|
||||
val = data.get(field)
|
||||
setattr(objective, field, val)
|
||||
if 'kpi_target' in data:
|
||||
objective.kpi_target = data['kpi_target']
|
||||
if 'start_date' in data:
|
||||
objective.start_date = data['start_date']
|
||||
if 'due_date' in data:
|
||||
objective.due_date = data['due_date']
|
||||
|
||||
db.session.commit()
|
||||
flash(_('Objective updated successfully.'), 'success')
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Delete
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@objectives_bp.route('/<int:objective_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_objective(objective_id):
|
||||
"""Delete an objective."""
|
||||
objective = _resolve_objective(objective_id)
|
||||
if not _can_edit(current_user, objective):
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
db.session.delete(objective)
|
||||
db.session.commit()
|
||||
flash(_('Objective deleted.'), 'success')
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON data endpoint for the edit modal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@objectives_bp.route('/<int:objective_id>/data')
|
||||
@json_endpoint
|
||||
@login_required
|
||||
def objective_data(objective_id):
|
||||
"""Return objective details as JSON for the edit modal."""
|
||||
from flask import jsonify
|
||||
|
||||
objective = _resolve_objective(objective_id)
|
||||
return jsonify(
|
||||
{
|
||||
'id': objective.id,
|
||||
'title': objective.title,
|
||||
'description': objective.description,
|
||||
'type': objective.type,
|
||||
'status': objective.status,
|
||||
'priority': objective.priority,
|
||||
'category': objective.category,
|
||||
'is_template': objective.is_template,
|
||||
'player_id': objective.player_id,
|
||||
'team_id': objective.team_id,
|
||||
'kpi_metric': objective.kpi_metric,
|
||||
'kpi_target': str(objective.kpi_target) if objective.kpi_target is not None else None,
|
||||
'kpi_unit': objective.kpi_unit,
|
||||
'kpi_precision': objective.kpi_precision,
|
||||
'start_date': objective.start_date.isoformat() if objective.start_date else None,
|
||||
'due_date': objective.due_date.isoformat() if objective.due_date else None,
|
||||
'check_frequency': objective.check_frequency,
|
||||
'progress': objective.progress,
|
||||
'tags': objective.tags,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Seed default templates (idempotent)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def seed_default_templates():
|
||||
"""Create the default Objective templates if they don't exist yet.
|
||||
|
||||
Idempotent — safe to call on every startup.
|
||||
Skips if no admin user exists yet (first run seeding may not have run).
|
||||
"""
|
||||
admin = User.query.filter(User.role == 'admin').first()
|
||||
if not admin:
|
||||
return
|
||||
|
||||
existing = {t.title for t in Objective.query.filter_by(is_template=True).all()}
|
||||
for (
|
||||
priority,
|
||||
title,
|
||||
description,
|
||||
kpi_metric,
|
||||
kpi_target,
|
||||
kpi_unit,
|
||||
typ,
|
||||
category,
|
||||
) in DEFAULT_OBJECTIVE_TEMPLATES:
|
||||
if title in existing:
|
||||
continue
|
||||
obj = Objective(
|
||||
title=title,
|
||||
description=description,
|
||||
type=typ,
|
||||
status='active',
|
||||
priority=priority,
|
||||
category=category,
|
||||
is_template=True,
|
||||
kpi_metric=kpi_metric,
|
||||
kpi_target=kpi_target,
|
||||
kpi_unit=kpi_unit,
|
||||
progress=0,
|
||||
due_date=datetime.date(2099, 12, 31),
|
||||
created_by=admin.id,
|
||||
)
|
||||
db.session.add(obj)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# KPI Entries — measurements for the quantitative graph
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@objectives_bp.route('/<int:objective_id>/entries', methods=['GET'])
|
||||
@login_required
|
||||
@json_endpoint
|
||||
def list_entries(objective_id):
|
||||
"""Return KPI entries for an objective as JSON."""
|
||||
from flask import jsonify
|
||||
|
||||
objective = _resolve_objective(objective_id)
|
||||
entries = (
|
||||
ObjectiveEntry.query.filter_by(objective_id=objective.id)
|
||||
.order_by(ObjectiveEntry.recorded_at)
|
||||
.all()
|
||||
)
|
||||
return jsonify(
|
||||
[
|
||||
{
|
||||
'id': e.id,
|
||||
'value': str(e.value),
|
||||
'recorded_at': e.recorded_at.isoformat(),
|
||||
'note': e.note,
|
||||
}
|
||||
for e in entries
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@objectives_bp.route('/<int:objective_id>/entries', methods=['POST'])
|
||||
@login_required
|
||||
def add_entry(objective_id):
|
||||
"""Record a KPI measurement for an objective.
|
||||
|
||||
Accepts an optional numeric value (for quantitative KPI tracking)
|
||||
and an optional note. If neither is provided the entry is rejected.
|
||||
The assigned player may record entries on their own objectives.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from app.permissions import can_add_objective_entry
|
||||
|
||||
objective = _resolve_objective(objective_id)
|
||||
if not can_add_objective_entry(current_user, objective):
|
||||
flash(_('You do not have permission to record entries for this objective.'), 'danger')
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
raw_value = request.form.get('value', '').strip()
|
||||
note = request.form.get('note', '').strip()
|
||||
value = None
|
||||
if raw_value:
|
||||
try:
|
||||
value = float(raw_value)
|
||||
except (ValueError, TypeError):
|
||||
flash(_('Invalid measurement value.'), 'danger')
|
||||
return redirect(url_for('objectives.view_objective', objective_id=objective_id))
|
||||
|
||||
if value is None and not note:
|
||||
flash(_('Provide a value, a note, or both.'), 'warning')
|
||||
return redirect(url_for('objectives.view_objective', objective_id=objective_id))
|
||||
|
||||
entry = ObjectiveEntry(
|
||||
objective_id=objective.id,
|
||||
value=value,
|
||||
recorded_at=datetime.now(),
|
||||
note=note or None,
|
||||
recorded_by=current_user.id,
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.commit()
|
||||
flash(_('Measurement recorded.'), 'success')
|
||||
return redirect(url_for('objectives.view_objective', objective_id=objective_id))
|
||||
flash(_('Measurement recorded.'), 'success')
|
||||
return redirect(url_for('objectives.edit_objective', objective_id=objective_id))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Comments — coach feedback on an objective
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@objectives_bp.route('/<int:objective_id>/comments', methods=['POST'])
|
||||
@login_required
|
||||
def add_comment(objective_id):
|
||||
"""Add a coach comment on an objective."""
|
||||
objective = _resolve_objective(objective_id)
|
||||
if not can_manage_objectives(current_user):
|
||||
flash(_('You do not have permission to comment.'), 'danger')
|
||||
return redirect(url_for('objectives.list_objectives'))
|
||||
|
||||
from app.validators import NoteContentSchema
|
||||
|
||||
try:
|
||||
data = NoteContentSchema().load(form_payload(list_fields=()))
|
||||
except ValidationError as err:
|
||||
flash_validation_errors(err)
|
||||
return redirect(url_for('objectives.edit_objective', objective_id=objective_id))
|
||||
|
||||
comment = ObjectiveComment(
|
||||
objective_id=objective.id,
|
||||
coach_id=current_user.id,
|
||||
content=data['content'],
|
||||
)
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
flash(_('Comment added.'), 'success')
|
||||
return redirect(url_for('objectives.edit_objective', objective_id=objective_id))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Trend / prediction data for the chart
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@objectives_bp.route('/<int:objective_id>/trend')
|
||||
@login_required
|
||||
@json_endpoint
|
||||
def trend_data(objective_id):
|
||||
"""Return the trend + prediction data for an objective's chart."""
|
||||
from flask import jsonify
|
||||
|
||||
from app.objectives_analytics import compute_trend
|
||||
|
||||
objective = _resolve_objective(objective_id)
|
||||
entries = (
|
||||
ObjectiveEntry.query.filter_by(objective_id=objective.id)
|
||||
.order_by(ObjectiveEntry.recorded_at)
|
||||
.all()
|
||||
)
|
||||
timestamps = [e.recorded_at for e in entries]
|
||||
values = [float(e.value) for e in entries]
|
||||
target = float(objective.kpi_target) if objective.kpi_target is not None else None
|
||||
|
||||
trend = compute_trend(timestamps, values, target)
|
||||
return jsonify(trend)
|
||||
Reference in New Issue
Block a user