Skip to content

Commit 864a4ec

Browse files
feat: expand API, add tests
1 parent b7fced4 commit 864a4ec

File tree

2 files changed

+112
-5
lines changed

2 files changed

+112
-5
lines changed

src/openedx_catalog/api_impl.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import logging
6+
from typing import overload
67

78
from django.conf import settings
89
from opaque_keys.edx.keys import CourseKey
@@ -23,22 +24,49 @@
2324
]
2425

2526

26-
def get_catalog_course(org_code: str, course_code: str) -> CatalogCourse:
27+
@overload
28+
def get_catalog_course(*, org_code: str, course_code: str) -> CatalogCourse: ...
29+
@overload
30+
def get_catalog_course(*, url_slug: str) -> CatalogCourse: ...
31+
@overload
32+
def get_catalog_course(*, pk: int) -> CatalogCourse: ...
33+
34+
35+
def get_catalog_course(
36+
pk: int | None = None,
37+
url_slug: str = "",
38+
org_code: str = "",
39+
course_code: str = "",
40+
) -> CatalogCourse:
2741
"""
2842
Get a catalog course (set of runs).
2943
30-
Does not check permissions nor load related models.
44+
⚠️ Does not check permissions or visibility rules.
3145
3246
The CatalogCourse may not have any runs associated with it.
3347
"""
34-
return CatalogCourse.objects.get(org__short_name=org_code, course_code=course_code)
48+
if pk:
49+
assert not org_code
50+
assert not url_slug
51+
return CatalogCourse.objects.get(pk=pk)
52+
if url_slug:
53+
assert not org_code
54+
assert not course_code
55+
org_code, course_code = url_slug.split(":", 1)
56+
# We might as well select_related org because we're joining to check the org__short_name field anyways.
57+
return CatalogCourse.objects.select_related("org").get(org__short_name=org_code, course_code=course_code)
3558

3659

3760
def get_course_run(course_id: CourseKey) -> CourseRun:
3861
"""
39-
Get a single course run. Does not check permissions nor load related models.
62+
Get a single course run.
63+
64+
⚠️ Does not check permissions or visibility rules.
4065
4166
The CourseRun may or may not have content associated with it.
67+
68+
Tip: to get all runs associated with a CatalogCourse, use
69+
`get_catalog_course(...).runs`
4270
"""
4371
return CourseRun.objects.get(course_id__exact=course_id)
4472

@@ -73,7 +101,7 @@ def create_course_run_for_modulestore_course_with(
73101
course_id: CourseKey,
74102
*,
75103
display_name: str,
76-
# The short language code (one of settings.ALL_LANGUAGES), e.g. "en", "es", "zh_HANS"
104+
# The short language code (in openedx-platform, this is one of settings.ALL_LANGUAGES), e.g. "en", "es", "zh_HANS"
77105
language_short: str | None = None,
78106
) -> CourseRun:
79107
"""

tests/openedx_catalog/test_api.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Tests related to the openedx_catalog python API
3+
"""
4+
5+
import pytest
6+
from django.core.exceptions import ValidationError
7+
from django.db import transaction
8+
from django.db.utils import IntegrityError
9+
from django.test import TestCase, override_settings
10+
from opaque_keys.edx.locator import CourseLocator
11+
from organizations.api import ensure_organization # type: ignore[import]
12+
from organizations.models import Organization # type: ignore[import]
13+
14+
from openedx_catalog import api
15+
from openedx_catalog.models_api import CatalogCourse, CourseRun
16+
17+
18+
pytestmark = pytest.mark.django_db
19+
20+
21+
@pytest.fixture(name="python100")
22+
def _python100():
23+
"""Create a "Python100" course for use in these tests"""
24+
ensure_organization("Org1")
25+
cc = CatalogCourse.objects.create(org_code="Org1", course_code="Python100")
26+
assert cc.url_slug == "Org1:Python100"
27+
return cc
28+
29+
30+
@pytest.fixture(name="csharp200")
31+
def _csharp200():
32+
"""Create a "CSharp200" course for use in these tests"""
33+
ensure_organization("Org1")
34+
cc = CatalogCourse.objects.create(org_code="Org1", course_code="CSharp200")
35+
assert cc.url_slug == "Org1:CSharp200"
36+
return cc
37+
38+
39+
# get_catalog_course
40+
41+
42+
def test_get_catalog_course(python100: CatalogCourse, csharp200: CatalogCourse) -> None:
43+
"""
44+
Test using get_catalog_course to get a course in various ways:
45+
"""
46+
# Retrieve by ID:
47+
assert api.get_catalog_course(pk=python100.pk) == python100
48+
assert api.get_catalog_course(pk=csharp200.pk) == csharp200
49+
with pytest.raises(CatalogCourse.DoesNotExist):
50+
api.get_catalog_course(pk=8234758243)
51+
52+
# Retrieve by URL slug:
53+
assert api.get_catalog_course(url_slug="Org1:Python100") == python100
54+
assert api.get_catalog_course(url_slug="Org1:CSharp200") == csharp200
55+
with pytest.raises(CatalogCourse.DoesNotExist):
56+
api.get_catalog_course(url_slug="foo:bar")
57+
58+
# Retrieve by (org_code, course_code)
59+
assert api.get_catalog_course(org_code="Org1", course_code="Python100") == python100
60+
assert api.get_catalog_course(org_code="Org1", course_code="CSharp200") == csharp200
61+
with pytest.raises(CatalogCourse.DoesNotExist):
62+
api.get_catalog_course(org_code="Org2", course_code="CSharp200")
63+
64+
65+
def test_get_catalog_course_url_slug_case(python100: CatalogCourse) -> None:
66+
"""
67+
Test that get_catalog_course(url_slug=...) is case-insensitive
68+
"""
69+
# FIXME: The Organization model's short_code is case sensitive on SQLite but case insensitive on MySQL :/
70+
# So for now, we only make assertions about the 'course_code' field case, which we can control.
71+
assert api.get_catalog_course(url_slug="Org1:Python100") == python100 # Correct case
72+
assert api.get_catalog_course(url_slug="Org1:python100") == python100 # Wrong course code case
73+
assert api.get_catalog_course(url_slug="Org1:PYTHON100").url_slug == "Org1:Python100" # Gets normalized
74+
75+
76+
# get_course_run
77+
# sync_course_run_details
78+
# create_course_run_for_modulestore_course_with
79+
# update_catalog_course

0 commit comments

Comments
 (0)