184 lines
5.4 KiB
Python
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,
|
|
})
|