diff --git a/.gitattributes b/.gitattributes index 21f6efb..91d5d9d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,7 @@ *.sh text eol=lf *.conf text eol=lf +*.pdf binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary diff --git a/addons/mc_education_lms/__init__.py b/addons/mc_education_lms/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/addons/mc_education_lms/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/mc_education_lms/__manifest__.py b/addons/mc_education_lms/__manifest__.py new file mode 100644 index 0000000..d414a81 --- /dev/null +++ b/addons/mc_education_lms/__manifest__.py @@ -0,0 +1,17 @@ +{ + "name": "School ERP - LMS", + "version": "19.0.1.0.0", + "category": "Education", + "summary": "Thin glue linking a batch to a website_slides course channel.", + "author": "Metatroncube Software Solutions LLP", + "license": "Other proprietary", + "depends": ["mc_education_base", "website_slides"], + "data": [ + "views/mc_batch_views.xml", + ], + "demo": [ + "demo/mc_lms_demo.xml", + ], + "installable": True, + "application": False, +} diff --git a/addons/mc_education_lms/demo/mc_lms_demo.xml b/addons/mc_education_lms/demo/mc_lms_demo.xml new file mode 100644 index 0000000..d3e3e0f --- /dev/null +++ b/addons/mc_education_lms/demo/mc_lms_demo.xml @@ -0,0 +1,129 @@ + + + + + Mathematics - Algebra Basics + invite + + + + + + + + Algebra Basics - Video Lesson + + video + https://www.youtube.com/watch?v=dQw4w9WgXcQ + True + 1 + + + + Algebra Basics - Handout + + document + + + True + 2 + + + + Algebra Basics - Quiz + + quiz + True + 3 + + + + + 1 + What is the value of x in x + 5 = 12? + + + + 7 + True + + + + 17 + + + + + 2 + Simplify: 3x + 2x + + + + 5x + True + + + + 6x + + + + + 3 + What is the coefficient of x in 4x + 3? + + + + 4 + True + + + + 3 + + + + + 4 + Solve for y: 2y = 10 + + + + 5 + True + + + + 20 + + + + + 5 + Which of these is a linear equation? + + + + y = 2x + 1 + True + + + + y = x^2 + + diff --git a/addons/mc_education_lms/models/__init__.py b/addons/mc_education_lms/models/__init__.py new file mode 100644 index 0000000..b8133f5 --- /dev/null +++ b/addons/mc_education_lms/models/__init__.py @@ -0,0 +1,2 @@ +from . import mc_batch +from . import mc_enrollment diff --git a/addons/mc_education_lms/models/mc_batch.py b/addons/mc_education_lms/models/mc_batch.py new file mode 100644 index 0000000..84870db --- /dev/null +++ b/addons/mc_education_lms/models/mc_batch.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class McBatch(models.Model): + _inherit = "mc.batch" + + channel_id = fields.Many2one( + "slide.channel", string="Course Channel", ondelete="set null", + help="Enrolling a student in this batch enrolls them in this course channel.", + ) diff --git a/addons/mc_education_lms/models/mc_enrollment.py b/addons/mc_education_lms/models/mc_enrollment.py new file mode 100644 index 0000000..d26168f --- /dev/null +++ b/addons/mc_education_lms/models/mc_enrollment.py @@ -0,0 +1,27 @@ +from odoo import api, models + + +class McEnrollment(models.Model): + _inherit = "mc.enrollment" + + @api.model_create_multi + def create(self, vals_list): + enrollments = super().create(vals_list) + enrollments.filtered(lambda e: e.state == "active")._sync_lms_channel_membership() + return enrollments + + def write(self, vals): + res = super().write(vals) + if "state" in vals or "batch_id" in vals: + self.filtered(lambda e: e.state == "active")._sync_lms_channel_membership() + return res + + def _sync_lms_channel_membership(self): + # Enrolling in the batch is what enrolls the student in its + # course channel - the whole point of this module (O8 spec: + # "link a channel to a mc.batch so enrolled students see their + # courses"). Stock website_slides API, nothing custom. + for enrollment in self: + channel = enrollment.batch_id.channel_id + if channel: + channel._action_add_members(enrollment.student_id.partner_id) diff --git a/addons/mc_education_lms/static/demo/algebra_handout.pdf b/addons/mc_education_lms/static/demo/algebra_handout.pdf new file mode 100644 index 0000000..d0df64d Binary files /dev/null and b/addons/mc_education_lms/static/demo/algebra_handout.pdf differ diff --git a/addons/mc_education_lms/tests/__init__.py b/addons/mc_education_lms/tests/__init__.py new file mode 100644 index 0000000..d0b38d2 --- /dev/null +++ b/addons/mc_education_lms/tests/__init__.py @@ -0,0 +1 @@ +from . import test_lms diff --git a/addons/mc_education_lms/tests/test_lms.py b/addons/mc_education_lms/tests/test_lms.py new file mode 100644 index 0000000..bedbe4e --- /dev/null +++ b/addons/mc_education_lms/tests/test_lms.py @@ -0,0 +1,67 @@ +from odoo.tests.common import TransactionCase + + +class TestLms(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.year = cls.env["mc.academic.year"].create({ + "name": "TEST-LMS-2026-27", + "date_start": "2026-06-01", "date_end": "2027-04-30", + }) + cls.program = cls.env["mc.program"].create({ + "name": "TEST LMS Program", "code": "TEST-LMS-P1", "sequence_no": 1, + "display_label": "Test Grade", + }) + cls.channel = cls.env["slide.channel"].create({"name": "TEST LMS Channel"}) + cls.batch = cls.env["mc.batch"].create({ + "name": "TEST LMS Batch", "program_id": cls.program.id, "year_id": cls.year.id, + "channel_id": cls.channel.id, + }) + partner = cls.env["res.partner"].create({"name": "LMS Test Student"}) + cls.student = cls.env["mc.student"].create({ + "partner_id": partner.id, "name": "LMS Test Student", + }) + + def test_active_enrollment_joins_the_batchs_channel(self): + self.env["mc.enrollment"].create({ + "student_id": self.student.id, "program_id": self.program.id, + "batch_id": self.batch.id, "year_id": self.year.id, "state": "active", + }) + member = self.env["slide.channel.partner"].search([ + ("channel_id", "=", self.channel.id), ("partner_id", "=", self.student.partner_id.id), + ]) + self.assertTrue(member) + self.assertEqual(member.member_status, "joined") + + def test_draft_enrollment_does_not_join_the_channel(self): + self.env["mc.enrollment"].create({ + "student_id": self.student.id, "program_id": self.program.id, + "batch_id": self.batch.id, "year_id": self.year.id, "state": "draft", + }) + member = self.env["slide.channel.partner"].search([ + ("channel_id", "=", self.channel.id), ("partner_id", "=", self.student.partner_id.id), + ]) + self.assertFalse(member) + + def test_activating_a_draft_enrollment_joins_the_channel(self): + enrollment = self.env["mc.enrollment"].create({ + "student_id": self.student.id, "program_id": self.program.id, + "batch_id": self.batch.id, "year_id": self.year.id, "state": "draft", + }) + enrollment.write({"state": "active"}) + member = self.env["slide.channel.partner"].search([ + ("channel_id", "=", self.channel.id), ("partner_id", "=", self.student.partner_id.id), + ]) + self.assertTrue(member) + + def test_batch_without_channel_does_not_error(self): + batch_no_channel = self.env["mc.batch"].create({ + "name": "TEST LMS Batch No Channel", "program_id": self.program.id, "year_id": self.year.id, + }) + # Should not raise. + self.env["mc.enrollment"].create({ + "student_id": self.student.id, "program_id": self.program.id, + "batch_id": batch_no_channel.id, "year_id": self.year.id, "state": "active", + }) diff --git a/addons/mc_education_lms/views/mc_batch_views.xml b/addons/mc_education_lms/views/mc_batch_views.xml new file mode 100644 index 0000000..9a6c646 --- /dev/null +++ b/addons/mc_education_lms/views/mc_batch_views.xml @@ -0,0 +1,13 @@ + + + + mc.batch.form.inherit.mc.education.lms + mc.batch + + + + + + + +