Skip to content

Commit 671034b

Browse files
yusuf-muslehnavinkarkera
authored andcommitted
feat: list courses details by keys
This adds the ability to get a list of detailed courses based on their keys provided in the newly added `keys` query param in the `GET /courses/v1/courses/` endpoint. (cherry picked from commit 4ad8ba1)
1 parent 863fdfe commit 671034b

File tree

9 files changed

+108
-11
lines changed

9 files changed

+108
-11
lines changed

docs/swagger.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2084,6 +2084,21 @@ paths:
20842084
provided org code (e.g., "HarvardX") are returned.
20852085
Case-insensitive.
20862086
2087+
permissions (optional):
2088+
If specified, it filters visible `CourseOverview` objects by
2089+
checking if each permission specified is granted for the username.
2090+
Notice that Staff users are always granted permission to list any
2091+
course.
2092+
2093+
active_only (optional):
2094+
If this boolean is specified, only the courses that have not ended or do not have any end
2095+
date are returned. This is different from search_term because this filtering is done on
2096+
CourseOverview and not ElasticSearch.
2097+
2098+
course_keys (optional):
2099+
If specified, it filters visible `CourseOverview` objects by
2100+
the course keys (ids) provided
2101+
20872102
**Returns**
20882103
20892104
* 200 on success, with a list of course discovery objects as returned

lms/djangoapps/branding/__init__.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
1515

1616

17-
def get_visible_courses(org=None, filter_=None, active_only=False):
17+
def get_visible_courses(org=None, filter_=None, active_only=False, course_keys=None):
1818
"""
1919
Yield the CourseOverviews that should be visible in this branded
2020
instance.
@@ -25,6 +25,8 @@ def get_visible_courses(org=None, filter_=None, active_only=False):
2525
filter_ (dict): Optional parameter that allows custom filtering by
2626
fields on the course.
2727
active_only (bool): Optional parameter that enables fetching active courses only.
28+
course_keys (list[str]): Optional parameter that allows for selecting which
29+
courses to fetch the `CourseOverviews` for
2830
"""
2931
# Import is placed here to avoid model import at project startup.
3032
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
@@ -36,12 +38,16 @@ def get_visible_courses(org=None, filter_=None, active_only=False):
3638
if org:
3739
# Check the current site's orgs to make sure the org's courses should be displayed
3840
if not current_site_orgs or org in current_site_orgs:
39-
courses = CourseOverview.get_all_courses(orgs=[org], filter_=filter_, active_only=active_only)
41+
courses = CourseOverview.get_all_courses(
42+
orgs=[org], filter_=filter_, active_only=active_only, course_keys=course_keys
43+
)
4044
elif current_site_orgs:
4145
# Only display courses that should be displayed on this site
42-
courses = CourseOverview.get_all_courses(orgs=current_site_orgs, filter_=filter_, active_only=active_only)
46+
courses = CourseOverview.get_all_courses(
47+
orgs=current_site_orgs, filter_=filter_, active_only=active_only, course_keys=course_keys
48+
)
4349
else:
44-
courses = CourseOverview.get_all_courses(filter_=filter_, active_only=active_only)
50+
courses = CourseOverview.get_all_courses(filter_=filter_, active_only=active_only, course_keys=course_keys)
4551

4652
courses = courses.order_by('id')
4753

lms/djangoapps/course_api/api.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ def list_courses(request,
116116
filter_=None,
117117
search_term=None,
118118
permissions=None,
119-
active_only=False):
119+
active_only=False,
120+
course_keys=None):
120121
"""
121122
Yield all available courses.
122123
@@ -146,12 +147,17 @@ def list_courses(request,
146147
If specified, it filters visible `CourseOverview` objects by
147148
checking if each permission specified is granted for the username.
148149
active_only (bool): Optional parameter that enables fetching active courses only.
150+
course_keys (list[str]):
151+
If specified, it filters visible `CourseOverview` objects by
152+
the course keys (ids) provided
149153
150154
Return value:
151155
Yield `CourseOverview` objects representing the collection of courses.
152156
"""
153157
user = get_effective_user(request.user, username)
154-
course_qs = get_courses(user, org=org, filter_=filter_, permissions=permissions, active_only=active_only)
158+
course_qs = get_courses(
159+
user, org=org, filter_=filter_, permissions=permissions, active_only=active_only, course_keys=course_keys
160+
)
155161
course_qs = _filter_by_search(course_qs, search_term)
156162
return course_qs
157163

lms/djangoapps/course_api/forms.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class CourseListGetForm(UsernameValidatorMixin, Form):
6464
mobile = ExtendedNullBooleanField(required=False)
6565
active_only = ExtendedNullBooleanField(required=False)
6666
permissions = MultiValueField(required=False)
67+
course_keys = MultiValueField(required=False)
6768

6869
def clean(self):
6970
"""
@@ -80,6 +81,20 @@ def clean(self):
8081

8182
return cleaned_data
8283

84+
def clean_course_keys(self):
85+
"""
86+
Ensure valid course_keys were provided.
87+
"""
88+
course_keys = self.cleaned_data['course_keys']
89+
if course_keys:
90+
for course_key in course_keys:
91+
try:
92+
CourseKey.from_string(course_key)
93+
except InvalidKeyError:
94+
raise ValidationError(f"'{str(course_key)}' is not a valid course key.") # lint-amnesty, pylint: disable=raise-missing-from
95+
96+
return course_keys
97+
8398

8499
class CourseIdListGetForm(UsernameValidatorMixin, Form):
85100
"""

lms/djangoapps/course_api/tests/test_api.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ def _make_api_call(self,
107107
specified_user,
108108
org=None,
109109
filter_=None,
110-
permissions=None):
110+
permissions=None,
111+
course_keys=None):
111112
"""
112113
Call the list_courses api endpoint to get information about
113114
`specified_user` on behalf of `requesting_user`.
@@ -121,6 +122,7 @@ def _make_api_call(self,
121122
org=org,
122123
filter_=filter_,
123124
permissions=permissions,
125+
course_keys=course_keys,
124126
)
125127

126128
def verify_courses(self, courses):
@@ -244,6 +246,39 @@ def test_permissions(self):
244246

245247
self.assertEqual({c.id for c in filtered_courses}, {self.course.id})
246248

249+
def test_filter_by_keys(self):
250+
"""
251+
Verify that courses are filtered by the provided course keys.
252+
"""
253+
254+
# Create alternative courses to be included in the `course_keys` filter.
255+
alternative_course_1 = self.create_course(course='alternative-course-1')
256+
alternative_course_2 = self.create_course(course='alternative-course-2')
257+
258+
# No filtering.
259+
unfiltered_expected_courses = [
260+
self.course,
261+
alternative_course_1,
262+
alternative_course_2,
263+
]
264+
unfiltered_courses = self._make_api_call(self.honor_user, self.honor_user)
265+
assert {course.id for course in unfiltered_courses} == {course.id for course in unfiltered_expected_courses}
266+
267+
# With filtering.
268+
filtered_expected_courses = [
269+
alternative_course_1,
270+
alternative_course_2,
271+
]
272+
filtered_courses = self._make_api_call(
273+
self.honor_user,
274+
self.honor_user,
275+
course_keys={
276+
alternative_course_1.id,
277+
alternative_course_2.id
278+
}
279+
)
280+
assert {course.id for course in filtered_courses} == {course.id for course in filtered_expected_courses}
281+
247282

248283
class TestGetCourseListExtras(CourseListTestMixin, ModuleStoreTestCase):
249284
"""

lms/djangoapps/course_api/tests/test_forms.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def set_up_data(self, user):
7171
'filter_': None,
7272
'permissions': set(),
7373
'active_only': None,
74+
'course_keys': set(),
7475
}
7576

7677
def test_basic(self):
@@ -100,6 +101,14 @@ def test_filter(self, param_field_name, param_field_value):
100101

101102
self.assert_valid(self.cleaned_data)
102103

104+
def test_invalid_course_keys(self):
105+
"""
106+
Verify form checks for validity of course keys provided
107+
"""
108+
109+
self.form_data['course_keys'] = 'course-v1:edX+DemoX+Demo_Course,invalid_course_key'
110+
self.assert_error('course_keys', "'invalid_course_key' is not a valid course key.")
111+
103112

104113
class TestCourseIdListGetForm(FormTestMixin, UsernameTestMixin, SharedModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
105114
FORM_CLASS = CourseIdListGetForm

lms/djangoapps/course_api/views.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ class CourseListView(DeveloperErrorViewMixin, ListAPIView):
286286
date are returned. This is different from search_term because this filtering is done on
287287
CourseOverview and not ElasticSearch.
288288
289+
course_keys (optional):
290+
If specified, it fetches the `CourseOverview` objects for the
291+
the specified course keys
292+
289293
**Returns**
290294
291295
* 200 on success, with a list of course discovery objects as returned
@@ -343,7 +347,8 @@ def get_queryset(self):
343347
filter_=form.cleaned_data['filter_'],
344348
search_term=form.cleaned_data['search_term'],
345349
permissions=form.cleaned_data['permissions'],
346-
active_only=form.cleaned_data.get('active_only', False)
350+
active_only=form.cleaned_data.get('active_only', False),
351+
course_keys=form.cleaned_data['course_keys'],
347352
)
348353

349354

lms/djangoapps/courseware/courses.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ def get_course_syllabus_section(course, section_key):
753753

754754

755755
@function_trace('get_courses')
756-
def get_courses(user, org=None, filter_=None, permissions=None, active_only=False):
756+
def get_courses(user, org=None, filter_=None, permissions=None, active_only=False, course_keys=None):
757757
"""
758758
Return a LazySequence of courses available, optionally filtered by org code
759759
(case-insensitive) or a set of permissions to be satisfied for the specified
@@ -763,7 +763,8 @@ def get_courses(user, org=None, filter_=None, permissions=None, active_only=Fals
763763
courses = branding.get_visible_courses(
764764
org=org,
765765
filter_=filter_,
766-
active_only=active_only
766+
active_only=active_only,
767+
course_keys=course_keys
767768
).prefetch_related(
768769
'modes',
769770
).select_related(

openedx/core/djangoapps/content/course_overviews/models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@ def update_select_courses(cls, course_keys, force_update=False):
657657
log.info('Finished generating course overviews.')
658658

659659
@classmethod
660-
def get_all_courses(cls, orgs=None, filter_=None, active_only=False):
660+
def get_all_courses(cls, orgs=None, filter_=None, active_only=False, course_keys=None):
661661
"""
662662
Return a queryset containing all CourseOverview objects in the database.
663663
@@ -666,12 +666,17 @@ def get_all_courses(cls, orgs=None, filter_=None, active_only=False):
666666
filtering by organization.
667667
filter_ (dict): Optional parameter that allows custom filtering.
668668
active_only (bool): If provided, only the courses that have not ended will be returned.
669+
course_keys (list[string]): Optional parameter that allows case-insensitive
670+
filter by course ids
669671
"""
670672
# Note: If a newly created course is not returned in this QueryList,
671673
# make sure the "publish" signal was emitted when the course was
672674
# created. For tests using CourseFactory, use emit_signals=True.
673675
course_overviews = CourseOverview.objects.all()
674676

677+
if course_keys:
678+
course_overviews = course_overviews.filter(id__in=course_keys)
679+
675680
if orgs:
676681
# In rare cases, courses belonging to the same org may be accidentally assigned
677682
# an org code with a different casing (e.g., Harvardx as opposed to HarvardX).

0 commit comments

Comments
 (0)