diff --git a/setup/website_slides_attendees_completed_time/odoo/addons/website_slides_attendees_completed_time b/setup/website_slides_attendees_completed_time/odoo/addons/website_slides_attendees_completed_time new file mode 120000 index 0000000..3e4ee7e --- /dev/null +++ b/setup/website_slides_attendees_completed_time/odoo/addons/website_slides_attendees_completed_time @@ -0,0 +1 @@ +../../../../website_slides_attendees_completed_time \ No newline at end of file diff --git a/setup/website_slides_attendees_completed_time/setup.py b/setup/website_slides_attendees_completed_time/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/website_slides_attendees_completed_time/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/website_slides_attendees_completed_time/README.rst b/website_slides_attendees_completed_time/README.rst new file mode 100644 index 0000000..f9e0b19 --- /dev/null +++ b/website_slides_attendees_completed_time/README.rst @@ -0,0 +1,132 @@ +======================================= +Website Slides Attendees Completed Time +======================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e15e26b7cb5d42f02ef1a21ffd984021c19230eeef3ca8c78b7f158110d918a3 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fe--learning-lightgray.png?logo=github + :target: https://github.com/OCA/e-learning/tree/16.0/website_slides_attendees_completed_time + :alt: OCA/e-learning +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/e-learning-16-0/e-learning-16-0-website_slides_attendees_completed_time + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/e-learning&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +🕒 Completed Time: Time-Based Completion Tracking per Attendee (in Hours) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This addon enhances Odoo eLearning by introducing a new metric: +**Completed Time**, computed **per attendee**, which represents the +**sum of the completion times (in hours)** of the slides they have +completed. + +🔧 What's Built-In vs. What's Added +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Built-in (Odoo):** + +- **Progress** + ``Progress = Number of Completed Slides / Total Number of Slides`` → + A simple ratio, based only on slide count. + +**Added by this addon:** + +- **Completed Time (in hours)** + ``Completed Time = Σ(completion_time of each completed slide)`` → A + cumulative total of time-based effort for each attendee. + +💡 Why This Matters +^^^^^^^^^^^^^^^^^^^ + +- ⏱️ **Duration-Aware Insight** Accurately reflects how much content a + learner has completed, by time spent rather than steps clicked. + +- 👤 **Per-Attendee Computation** Each attendee has a personalized + Completed Time value, making metrics more meaningful for trainers and + reports. + +- 📊 **Actionable Training Analytics** Trainers can better identify + high-effort learners and tailor support accordingly. + +🧪 Example +^^^^^^^^^^ + +For a course with 10 slides (total 20 hours), if an attendee completes: + +- 1 slide of 15 hours → **Progress**: 10% → **Completed Time**: 15h + +-------------- + +This addon is ideal for serious training environments, compliance +programs, and professional development, where **time commitment** is a +better measure than simple slide count. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +After module installation, you will see course's completed time within +attendee views. + +.. image:: https://raw.githubusercontent.com/OCA/e-learning/16.0/website_slides_attendees_completed_time/static/description/attendees_tree_view.png + :alt: Attendees Tree View + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Binhex + +Contributors +------------ + +- Rolando Pérez rrebollo@binhex.cloud https://binhex.cloud + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/e-learning `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_slides_attendees_completed_time/__init__.py b/website_slides_attendees_completed_time/__init__.py new file mode 100644 index 0000000..271337f --- /dev/null +++ b/website_slides_attendees_completed_time/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .post_install import channel_partner_recompute_completion diff --git a/website_slides_attendees_completed_time/__manifest__.py b/website_slides_attendees_completed_time/__manifest__.py new file mode 100644 index 0000000..9ee4c83 --- /dev/null +++ b/website_slides_attendees_completed_time/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Website Slides Attendees Completed Time", + "summary": """Show course completed time in attendee views""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Binhex,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/e-learning", + "depends": ["website_slides"], + "post_init_hook": "channel_partner_recompute_completion", + "data": [ + "views/slide_channel_partner_views.xml", + ], + "demo": [], +} diff --git a/website_slides_attendees_completed_time/models/__init__.py b/website_slides_attendees_completed_time/models/__init__.py new file mode 100644 index 0000000..23e6148 --- /dev/null +++ b/website_slides_attendees_completed_time/models/__init__.py @@ -0,0 +1 @@ +from . import slide_channel_partner diff --git a/website_slides_attendees_completed_time/models/slide_channel_partner.py b/website_slides_attendees_completed_time/models/slide_channel_partner.py new file mode 100644 index 0000000..cfc09bd --- /dev/null +++ b/website_slides_attendees_completed_time/models/slide_channel_partner.py @@ -0,0 +1,43 @@ +# Copyright 2025 Binhex +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SlideChannelPartner(models.Model): + _inherit = "slide.channel.partner" + + completed_time = fields.Float("Completed Time (hours)") + + def _recompute_completion(self): + res = super()._recompute_completion() + read_group_res = ( + self.env["slide.slide.partner"] + .sudo() + ._read_group( + [ + "&", + "&", + ("channel_id", "in", self.mapped("channel_id").ids), + ("partner_id", "in", self.mapped("partner_id").ids), + ("completed", "=", True), + ("slide_id.is_published", "=", True), + ("slide_id.active", "=", True), + ], + ["channel_id", "partner_id", "slide_id:array_agg"], + groupby=["channel_id", "partner_id"], + lazy=False, + ) + ) + mapped_data = dict() + Slide = self.env["slide.slide"] + for item in read_group_res: + mapped_data.setdefault(item["channel_id"][0], dict()) + mapped_data[item["channel_id"][0]][item["partner_id"][0]] = sum( + Slide.browse(set(item["slide_id"])).mapped("completion_time"), 0.0 + ) + for record in self: + record.completed_time = mapped_data.get(record.channel_id.id, dict()).get( + record.partner_id.id, 0.0 + ) + return res diff --git a/website_slides_attendees_completed_time/post_install.py b/website_slides_attendees_completed_time/post_install.py new file mode 100644 index 0000000..fa932ce --- /dev/null +++ b/website_slides_attendees_completed_time/post_install.py @@ -0,0 +1,11 @@ +# © 2025 Binhex - Rolando Pérez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from odoo import SUPERUSER_ID, api + + +def channel_partner_recompute_completion(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + env["slide.channel.partner"].search([])._recompute_completion() + return diff --git a/website_slides_attendees_completed_time/readme/CONTRIBUTORS.md b/website_slides_attendees_completed_time/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..4a3b9e4 --- /dev/null +++ b/website_slides_attendees_completed_time/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Rolando Pérez https://binhex.cloud diff --git a/website_slides_attendees_completed_time/readme/DESCRIPTION.md b/website_slides_attendees_completed_time/readme/DESCRIPTION.md new file mode 100644 index 0000000..74e76e6 --- /dev/null +++ b/website_slides_attendees_completed_time/readme/DESCRIPTION.md @@ -0,0 +1,38 @@ +### 🕒 Completed Time: Time-Based Completion Tracking per Attendee (in Hours) + +This addon enhances Odoo eLearning by introducing a new metric: **Completed Time**, computed **per attendee**, which represents the **sum of the completion times (in hours)** of the slides they have completed. + +#### 🔧 What's Built-In vs. What's Added + +**Built-in (Odoo):** +- **Progress** + `Progress = Number of Completed Slides / Total Number of Slides` + → A simple ratio, based only on slide count. + +**Added by this addon:** +- **Completed Time (in hours)** + `Completed Time = Σ(completion_time of each completed slide)` + → A cumulative total of time-based effort for each attendee. + +#### 💡 Why This Matters + +- ⏱️ **Duration-Aware Insight** + Accurately reflects how much content a learner has completed, by time spent rather than steps clicked. + +- 👤 **Per-Attendee Computation** + Each attendee has a personalized Completed Time value, making metrics more meaningful for trainers and reports. + +- 📊 **Actionable Training Analytics** + Trainers can better identify high-effort learners and tailor support accordingly. + +#### 🧪 Example + +For a course with 10 slides (total 20 hours), if an attendee completes: + +- 1 slide of 15 hours +→ **Progress**: 10% +→ **Completed Time**: 15h + +--- + +This addon is ideal for serious training environments, compliance programs, and professional development, where **time commitment** is a better measure than simple slide count. diff --git a/website_slides_attendees_completed_time/readme/USAGE.md b/website_slides_attendees_completed_time/readme/USAGE.md new file mode 100644 index 0000000..22a5d56 --- /dev/null +++ b/website_slides_attendees_completed_time/readme/USAGE.md @@ -0,0 +1,3 @@ +After module installation, you will see course's completed time within attendee views. + +![Attendees Tree View](../static/description/attendees_tree_view.png) diff --git a/website_slides_attendees_completed_time/static/description/attendees_tree_view.png b/website_slides_attendees_completed_time/static/description/attendees_tree_view.png new file mode 100644 index 0000000..d8b7edc Binary files /dev/null and b/website_slides_attendees_completed_time/static/description/attendees_tree_view.png differ diff --git a/website_slides_attendees_completed_time/static/description/icon.png b/website_slides_attendees_completed_time/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/website_slides_attendees_completed_time/static/description/icon.png differ diff --git a/website_slides_attendees_completed_time/static/description/index.html b/website_slides_attendees_completed_time/static/description/index.html new file mode 100644 index 0000000..ce20778 --- /dev/null +++ b/website_slides_attendees_completed_time/static/description/index.html @@ -0,0 +1,474 @@ + + + + + +Website Slides Attendees Completed Time + + + +
+

Website Slides Attendees Completed Time

+ + +

Beta License: AGPL-3 OCA/e-learning Translate me on Weblate Try me on Runboat

+
+

🕒 Completed Time: Time-Based Completion Tracking per Attendee (in Hours)

+

This addon enhances Odoo eLearning by introducing a new metric: +Completed Time, computed per attendee, which represents the +sum of the completion times (in hours) of the slides they have +completed.

+
+

🔧 What’s Built-In vs. What’s Added

+

Built-in (Odoo):

+
    +
  • Progress +Progress = Number of Completed Slides / Total Number of Slides → +A simple ratio, based only on slide count.
  • +
+

Added by this addon:

+
    +
  • Completed Time (in hours) +Completed Time = Σ(completion_time of each completed slide) → A +cumulative total of time-based effort for each attendee.
  • +
+
+
+

💡 Why This Matters

+
    +
  • ⏱️ Duration-Aware Insight Accurately reflects how much content a +learner has completed, by time spent rather than steps clicked.
  • +
  • 👤 Per-Attendee Computation Each attendee has a personalized +Completed Time value, making metrics more meaningful for trainers and +reports.
  • +
  • 📊 Actionable Training Analytics Trainers can better identify +high-effort learners and tailor support accordingly.
  • +
+
+
+

🧪 Example

+

For a course with 10 slides (total 20 hours), if an attendee completes:

+
    +
  • 1 slide of 15 hours → Progress: 10% → Completed Time: 15h
  • +
+
+

This addon is ideal for serious training environments, compliance +programs, and professional development, where time commitment is a +better measure than simple slide count.

+

Table of contents

+ +
+

Usage

+

After module installation, you will see course’s completed time within +attendee views.

+Attendees Tree View +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Binhex
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/e-learning project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+
+ + diff --git a/website_slides_attendees_completed_time/tests/__init__.py b/website_slides_attendees_completed_time/tests/__init__.py new file mode 100644 index 0000000..396a1e2 --- /dev/null +++ b/website_slides_attendees_completed_time/tests/__init__.py @@ -0,0 +1 @@ +from . import test_attendees_completed_time diff --git a/website_slides_attendees_completed_time/tests/test_attendees_completed_time.py b/website_slides_attendees_completed_time/tests/test_attendees_completed_time.py new file mode 100644 index 0000000..e3ddd0c --- /dev/null +++ b/website_slides_attendees_completed_time/tests/test_attendees_completed_time.py @@ -0,0 +1,97 @@ +from odoo.tests import tagged +from odoo.tools import mute_logger + +from odoo.addons.website_slides.tests import common + + +@tagged("-at_install", "post_install") +class TestAttendeesCompletedTime(common.SlidesCase): + @mute_logger("odoo.models") + def test_channel_attendees_completed_time(self): + channel_publisher = self.channel.with_user(self.user_officer) + channel_publisher.write( + { + "enroll": "invite", + } + ) + channel_publisher._action_add_members(self.user_emp.partner_id) + self.channel.with_user(self.user_emp) + + members = self.env["slide.channel.partner"].search( + [("channel_id", "=", self.channel.id)] + ) + member_emp = members.filtered( + lambda m: m.partner_id == self.user_emp.partner_id + ) + member_publisher = members.filtered( + lambda m: m.partner_id == self.user_officer.partner_id + ) + + slides_emp = (self.slide | self.slide_2).with_user(self.user_emp) + slides_emp.action_set_viewed() + self.assertEqual(member_emp.completed_time, 0) + + slides_emp.action_mark_completed() + + slides_emp_completed_time = sum(slides_emp.mapped("completion_time")) + self.assertEqual(member_emp.completed_time, slides_emp_completed_time) + self.slide_3.with_user(self.user_emp)._action_mark_completed() + slides_emp_completed_time += self.slide_3.completion_time + self.assertEqual(member_emp.completed_time, slides_emp_completed_time) + + # The following tests should update the completed_time even for users + # that have already completed the course + + self.slide_3.is_published = False + slides_emp_completed_time -= self.slide_3.completion_time + self.assertEqual(member_emp.completed_time, slides_emp_completed_time) + self.slide_3.is_published = True + self.slide_3.active = False + self.assertEqual(member_emp.completed_time, slides_emp_completed_time) + + # Should update completed_time when slide is marked as completed + + self.assertEqual(member_publisher.completed_time, 0) + self.slide.with_user(self.user_officer).action_mark_completed() + slides_publisher_completed_time = self.slide.completion_time + self.assertEqual( + member_publisher.completed_time, slides_publisher_completed_time + ) + + # Should update completed_time when slide is (un)archived + self.slide_3.active = True + slides_emp_completed_time += self.slide_3.completion_time + self.assertEqual(member_emp.completed_time, slides_emp_completed_time) + self.assertEqual( + member_publisher.completed_time, slides_publisher_completed_time + ) + + # Shouln't update completed_time when a new published slide is created + self.slide_4 = self.slide_3.copy({"is_published": True}) + self.assertEqual(member_emp.completed_time, slides_emp_completed_time) + self.assertEqual( + member_publisher.completed_time, slides_publisher_completed_time + ) + + # Shouldn't update completed_time when slide is (un)published + self.slide_4.is_published = False + self.assertEqual(member_emp.completed_time, slides_emp_completed_time) + self.assertEqual( + member_publisher.completed_time, slides_publisher_completed_time + ) + + # Should update completed_time when slide is marked as uncompleted + self.slide.with_user(self.user_emp).action_mark_uncompleted() + slides_emp_completed_time -= self.slide.completion_time + self.assertEqual(member_emp.completed_time, slides_emp_completed_time) + self.assertEqual( + member_publisher.completed_time, slides_publisher_completed_time + ) + + # Should update completed_time when a slide is unlinked + slides_publisher_completed_time -= self.slide.completion_time + self.slide.with_user(self.user_manager).unlink() + self.assertEqual(member_emp.completed_time, slides_emp_completed_time) + self.assertEqual( + member_publisher.completed_time, slides_publisher_completed_time + ) diff --git a/website_slides_attendees_completed_time/views/slide_channel_partner_views.xml b/website_slides_attendees_completed_time/views/slide_channel_partner_views.xml new file mode 100644 index 0000000..cae95f0 --- /dev/null +++ b/website_slides_attendees_completed_time/views/slide_channel_partner_views.xml @@ -0,0 +1,16 @@ + + + + + + slide.channel.partner + + + + + + + + +