662 lines
22 KiB
Python
662 lines
22 KiB
Python
"""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)
|