Skip to content

Commit 9bcef6f

Browse files
committed
Generate course's LMS URLs for Canvas
1 parent 5480561 commit 9bcef6f

File tree

2 files changed

+70
-0
lines changed

2 files changed

+70
-0
lines changed

lms/models/lms_course.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99

1010
from datetime import datetime
1111
from typing import TYPE_CHECKING
12+
from urllib.parse import urljoin
1213

1314
import sqlalchemy as sa
1415
from sqlalchemy.orm import Mapped, mapped_column, relationship
1516

1617
from lms.db import Base
1718
from lms.models import ApplicationInstance
1819
from lms.models._mixins import CreatedUpdatedMixin
20+
from lms.models.family import Family
1921

2022
if TYPE_CHECKING:
2123
from lms.models import LMSTerm, LMSUser, LTIRole
@@ -63,6 +65,26 @@ class LMSCourse(CreatedUpdatedMixin, Base):
6365
)
6466
lms_term: Mapped["LMSTerm"] = relationship()
6567

68+
application_instances: Mapped[list[ApplicationInstance]] = relationship(
69+
secondary="lms_course_application_instance",
70+
order_by="desc(LMSCourseApplicationInstance.updated)",
71+
viewonly=True,
72+
)
73+
74+
@property
75+
def lms_url(self) -> str | None:
76+
"""The URL of the course in the LMS."""
77+
ai = self.application_instances[0]
78+
if ai.family != Family.CANVAS:
79+
# We only support Canvas for now
80+
return None
81+
82+
if not ai.lms_url or not self.lms_api_course_id:
83+
# We need both the LMS base URL and the course ID
84+
return None
85+
86+
return urljoin(ai.lms_url, f"/courses/{self.lms_api_course_id}")
87+
6688

6789
class LMSCourseApplicationInstance(CreatedUpdatedMixin, Base):
6890
"""Record of on which installs (application instances) we have seen one course."""
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from datetime import UTC, datetime
2+
3+
import pytest
4+
5+
from lms.models.family import Family
6+
from tests import factories
7+
8+
9+
def test_lms_course_application_instance(db_session):
10+
old_ai = factories.ApplicationInstance()
11+
new_ai = factories.ApplicationInstance()
12+
13+
lms_course = factories.LMSCourse()
14+
factories.LMSCourseApplicationInstance(
15+
lms_course=lms_course,
16+
application_instance=old_ai,
17+
updated=datetime(2021, 1, 1, tzinfo=UTC),
18+
)
19+
factories.LMSCourseApplicationInstance(
20+
lms_course=lms_course,
21+
application_instance=new_ai,
22+
updated=datetime(2025, 1, 1, tzinfo=UTC),
23+
)
24+
db_session.flush()
25+
26+
assert lms_course.application_instances == [new_ai, old_ai]
27+
28+
29+
@pytest.mark.parametrize("family", Family)
30+
@pytest.mark.parametrize("lms_api_course_id", [None, "COURSE_ID"])
31+
@pytest.mark.parametrize(
32+
"lms_url", ["", "https://example.com", "https://example.com//"]
33+
)
34+
def test_lms_url(family, db_session, lms_api_course_id, lms_url):
35+
ai = factories.ApplicationInstance(
36+
tool_consumer_info_product_family_code=family, lms_url=lms_url
37+
)
38+
lms_course = factories.LMSCourse(lms_api_course_id=lms_api_course_id)
39+
40+
factories.LMSCourseApplicationInstance(
41+
application_instance=ai, lms_course=lms_course
42+
)
43+
db_session.flush()
44+
45+
if family != Family.CANVAS or not lms_api_course_id or not lms_url:
46+
assert lms_course.lms_url is None
47+
else:
48+
assert lms_course.lms_url == "https://example.com/courses/COURSE_ID"

0 commit comments

Comments
 (0)