odex25_standard/odex25_base/tour_genius/models/tour_progress.py

184 lines
5.4 KiB
Python

# -*- coding: utf-8 -*-
from odoo import models, fields, api
class GeniusProgress(models.Model):
"""
Tracks user progress on topics.
One record per user per topic.
"""
_name = 'genius.progress'
_description = 'User Progress'
_order = 'date_started desc'
_sql_constraints = [
('user_topic_uniq', 'unique(user_id, topic_id)', 'Progress record already exists for this tour!')
]
user_id = fields.Many2one(
'res.users',
string='User',
required=True,
ondelete='cascade',
index=True
)
topic_id = fields.Many2one(
'genius.topic',
string='Topic',
required=True,
ondelete='restrict',
index=True
)
plan_id = fields.Many2one(
'genius.plan',
string='Plan',
ondelete='cascade',
help='Training plan (for faster queries)'
)
# Progress State
state = fields.Selection(
selection=[
('pending', 'Pending'),
('in_progress', 'In Progress'),
('skipped', 'Skipped'),
('done', 'Completed'),
('verified', 'Certified'), # Certified = Completed + Passed Quiz
],
string='Status',
default='pending',
index=True
)
# Timestamps
date_started = fields.Datetime(
string='Started At'
)
date_completed = fields.Datetime(
string='Completed At'
)
date_verified = fields.Datetime(
string='Verified At'
)
date_skipped = fields.Datetime(
string='Last Skipped At'
)
# Completion Tracking
completion_count = fields.Integer(
string='Times Completed',
default=0,
help='Number of times user has completed this tour'
)
# Quiz Score (if applicable)
quiz_score = fields.Float(
string='Quiz Score (%)'
)
quiz_attempt_id = fields.Many2one(
'genius.quiz.attempt',
string='Quiz Attempt'
)
# Time spent
time_spent_seconds = fields.Float(
string='Time (Seconds)',
compute='_compute_time_spent',
store=True,
digits=(10, 2)
)
time_spent_minutes = fields.Float(
string='Time (Minutes)',
compute='_compute_time_spent',
store=True,
digits=(10, 2)
)
duration_display = fields.Char(
string='Duration',
compute='_compute_time_spent',
store=True
)
# Gamification (Stored for Performance)
points_earned = fields.Integer(string='Points', compute='_compute_gamification', store=True)
is_quiz_passed = fields.Boolean(string='Quiz Passed', compute='_compute_gamification', store=True)
is_quiz_perfect = fields.Boolean(string='Quiz Perfect', compute='_compute_gamification', store=True)
@api.depends('state', 'quiz_attempt_id', 'quiz_attempt_id.is_passed', 'quiz_score')
def _compute_gamification(self):
for r in self:
points = 0
is_passed = False
is_perfect = False
# Topic Completion
if r.state in ['done', 'verified']:
points += 10
# Quiz Completion
if r.quiz_attempt_id and r.quiz_attempt_id.is_passed:
is_passed = True
points += 25
# Perfect Score (Bonus)
# Check robust float comparison or just use integer points?
# attempt.score is float. attempt.points_earned == points_possible is better.
if r.quiz_attempt_id.points_possible > 0 and r.quiz_attempt_id.points_earned == r.quiz_attempt_id.points_possible:
is_perfect = True
points += 50
r.points_earned = points
r.is_quiz_passed = is_passed
r.is_quiz_perfect = is_perfect
# Notes
notes = fields.Text(
string='Notes',
help='Trainer notes about this progress'
)
@api.depends('date_started', 'date_completed')
def _compute_time_spent(self):
for progress in self:
if progress.date_started and progress.date_completed:
delta = progress.date_completed - progress.date_started
seconds = delta.total_seconds()
progress.time_spent_seconds = seconds
progress.time_spent_minutes = seconds / 60.0
# Format duration (e.g. "2m 15s")
m, s = divmod(int(seconds), 60)
h, m = divmod(m, 60)
if h > 0:
progress.duration_display = f"{h}h {m}m {s}s"
elif m > 0:
progress.duration_display = f"{m}m {s}s"
else:
progress.duration_display = f"{s}s"
else:
progress.time_spent_seconds = 0.0
progress.time_spent_minutes = 0.0
progress.duration_display = "0s"
def action_verify(self):
"""Mark progress as verified by trainer"""
return self.write({
'state': 'verified',
'date_verified': fields.Datetime.now(),
})
def action_reset(self):
"""Reset progress to pending"""
return self.write({
'state': 'pending',
'date_started': False,
'date_completed': False,
'date_verified': False,
'quiz_score': 0,
'quiz_attempt_id': False,
})