diff --git a/lms/models/lms_course.py b/lms/models/lms_course.py index 5d00b88de8..939da3f340 100644 --- a/lms/models/lms_course.py +++ b/lms/models/lms_course.py @@ -9,6 +9,7 @@ from datetime import datetime from typing import TYPE_CHECKING +from urllib.parse import urljoin import sqlalchemy as sa from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -16,6 +17,7 @@ from lms.db import Base from lms.models import ApplicationInstance from lms.models._mixins import CreatedUpdatedMixin +from lms.models.family import Family if TYPE_CHECKING: from lms.models import LMSTerm, LMSUser, LTIRole @@ -63,6 +65,26 @@ class LMSCourse(CreatedUpdatedMixin, Base): ) lms_term: Mapped["LMSTerm"] = relationship() + application_instances: Mapped[list[ApplicationInstance]] = relationship( + secondary="lms_course_application_instance", + order_by="desc(LMSCourseApplicationInstance.updated)", + viewonly=True, + ) + + @property + def lms_url(self) -> str | None: + """The URL of the course in the LMS.""" + ai = self.application_instances[0] + if ai.family != Family.CANVAS: + # We only support Canvas for now + return None + + if not ai.lms_url or not self.lms_api_course_id: + # We need both the LMS base URL and the course ID + return None + + return urljoin(ai.lms_url, f"/courses/{self.lms_api_course_id}") + class LMSCourseApplicationInstance(CreatedUpdatedMixin, Base): """Record of on which installs (application instances) we have seen one course.""" diff --git a/tests/unit/lms/models/lms_course_test.py b/tests/unit/lms/models/lms_course_test.py new file mode 100644 index 0000000000..6e4a44a90e --- /dev/null +++ b/tests/unit/lms/models/lms_course_test.py @@ -0,0 +1,48 @@ +from datetime import UTC, datetime + +import pytest + +from lms.models.family import Family +from tests import factories + + +def test_lms_course_application_instance(db_session): + old_ai = factories.ApplicationInstance() + new_ai = factories.ApplicationInstance() + + lms_course = factories.LMSCourse() + factories.LMSCourseApplicationInstance( + lms_course=lms_course, + application_instance=old_ai, + updated=datetime(2021, 1, 1, tzinfo=UTC), + ) + factories.LMSCourseApplicationInstance( + lms_course=lms_course, + application_instance=new_ai, + updated=datetime(2025, 1, 1, tzinfo=UTC), + ) + db_session.flush() + + assert lms_course.application_instances == [new_ai, old_ai] + + +@pytest.mark.parametrize("family", Family) +@pytest.mark.parametrize("lms_api_course_id", [None, "COURSE_ID"]) +@pytest.mark.parametrize( + "lms_url", ["", "https://example.com", "https://example.com//"] +) +def test_lms_url(family, db_session, lms_api_course_id, lms_url): + ai = factories.ApplicationInstance( + tool_consumer_info_product_family_code=family, lms_url=lms_url + ) + lms_course = factories.LMSCourse(lms_api_course_id=lms_api_course_id) + + factories.LMSCourseApplicationInstance( + application_instance=ai, lms_course=lms_course + ) + db_session.flush() + + if family != Family.CANVAS or not lms_api_course_id or not lms_url: + assert lms_course.lms_url is None + else: + assert lms_course.lms_url == "https://example.com/courses/COURSE_ID"