Skip to content

Commit a9ef1ba

Browse files
authored
Endpoints for userlist/learningpath memberships (#1808)
1 parent da4b0e7 commit a9ef1ba

File tree

7 files changed

+337
-0
lines changed

7 files changed

+337
-0
lines changed

frontends/api/src/generated/v1/api.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18233,6 +18233,45 @@ export const LearningpathsApiAxiosParamCreator = function (
1823318233
options: localVarRequestOptions,
1823418234
}
1823518235
},
18236+
/**
18237+
* Get a list of all learning path items
18238+
* @summary List
18239+
* @param {*} [options] Override http request option.
18240+
* @throws {RequiredError}
18241+
*/
18242+
learningpathsMembershipList: async (
18243+
options: RawAxiosRequestConfig = {},
18244+
): Promise<RequestArgs> => {
18245+
const localVarPath = `/api/v1/learningpaths/membership/`
18246+
// use dummy base URL string because the URL constructor only accepts absolute URLs.
18247+
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
18248+
let baseOptions
18249+
if (configuration) {
18250+
baseOptions = configuration.baseOptions
18251+
}
18252+
18253+
const localVarRequestOptions = {
18254+
method: "GET",
18255+
...baseOptions,
18256+
...options,
18257+
}
18258+
const localVarHeaderParameter = {} as any
18259+
const localVarQueryParameter = {} as any
18260+
18261+
setSearchParams(localVarUrlObj, localVarQueryParameter)
18262+
let headersFromBaseOptions =
18263+
baseOptions && baseOptions.headers ? baseOptions.headers : {}
18264+
localVarRequestOptions.headers = {
18265+
...localVarHeaderParameter,
18266+
...headersFromBaseOptions,
18267+
...options.headers,
18268+
}
18269+
18270+
return {
18271+
url: toPathString(localVarUrlObj),
18272+
options: localVarRequestOptions,
18273+
}
18274+
},
1823618275
/**
1823718276
* Update individual fields of a learning path
1823818277
* @summary Update
@@ -18672,6 +18711,35 @@ export const LearningpathsApiFp = function (configuration?: Configuration) {
1867218711
configuration,
1867318712
)(axios, operationBasePath || basePath)
1867418713
},
18714+
/**
18715+
* Get a list of all learning path items
18716+
* @summary List
18717+
* @param {*} [options] Override http request option.
18718+
* @throws {RequiredError}
18719+
*/
18720+
async learningpathsMembershipList(
18721+
options?: RawAxiosRequestConfig,
18722+
): Promise<
18723+
(
18724+
axios?: AxiosInstance,
18725+
basePath?: string,
18726+
) => AxiosPromise<Array<MicroLearningPathRelationship>>
18727+
> {
18728+
const localVarAxiosArgs =
18729+
await localVarAxiosParamCreator.learningpathsMembershipList(options)
18730+
const index = configuration?.serverIndex ?? 0
18731+
const operationBasePath =
18732+
operationServerMap["LearningpathsApi.learningpathsMembershipList"]?.[
18733+
index
18734+
]?.url
18735+
return (axios, basePath) =>
18736+
createRequestFunction(
18737+
localVarAxiosArgs,
18738+
globalAxios,
18739+
BASE_PATH,
18740+
configuration,
18741+
)(axios, operationBasePath || basePath)
18742+
},
1867518743
/**
1867618744
* Update individual fields of a learning path
1867718745
* @summary Update
@@ -18917,6 +18985,19 @@ export const LearningpathsApiFactory = function (
1891718985
)
1891818986
.then((request) => request(axios, basePath))
1891918987
},
18988+
/**
18989+
* Get a list of all learning path items
18990+
* @summary List
18991+
* @param {*} [options] Override http request option.
18992+
* @throws {RequiredError}
18993+
*/
18994+
learningpathsMembershipList(
18995+
options?: RawAxiosRequestConfig,
18996+
): AxiosPromise<Array<MicroLearningPathRelationship>> {
18997+
return localVarFp
18998+
.learningpathsMembershipList(options)
18999+
.then((request) => request(axios, basePath))
19000+
},
1892019001
/**
1892119002
* Update individual fields of a learning path
1892219003
* @summary Update
@@ -19456,6 +19537,19 @@ export class LearningpathsApi extends BaseAPI {
1945619537
.then((request) => request(this.axios, this.basePath))
1945719538
}
1945819539

19540+
/**
19541+
* Get a list of all learning path items
19542+
* @summary List
19543+
* @param {*} [options] Override http request option.
19544+
* @throws {RequiredError}
19545+
* @memberof LearningpathsApi
19546+
*/
19547+
public learningpathsMembershipList(options?: RawAxiosRequestConfig) {
19548+
return LearningpathsApiFp(this.configuration)
19549+
.learningpathsMembershipList(options)
19550+
.then((request) => request(this.axios, this.basePath))
19551+
}
19552+
1945919553
/**
1946019554
* Update individual fields of a learning path
1946119555
* @summary Update
@@ -24126,6 +24220,45 @@ export const UserlistsApiAxiosParamCreator = function (
2412624220
options: localVarRequestOptions,
2412724221
}
2412824222
},
24223+
/**
24224+
* Get a list of all userlist items for a user
24225+
* @summary List
24226+
* @param {*} [options] Override http request option.
24227+
* @throws {RequiredError}
24228+
*/
24229+
userlistsMembershipList: async (
24230+
options: RawAxiosRequestConfig = {},
24231+
): Promise<RequestArgs> => {
24232+
const localVarPath = `/api/v1/userlists/membership/`
24233+
// use dummy base URL string because the URL constructor only accepts absolute URLs.
24234+
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
24235+
let baseOptions
24236+
if (configuration) {
24237+
baseOptions = configuration.baseOptions
24238+
}
24239+
24240+
const localVarRequestOptions = {
24241+
method: "GET",
24242+
...baseOptions,
24243+
...options,
24244+
}
24245+
const localVarHeaderParameter = {} as any
24246+
const localVarQueryParameter = {} as any
24247+
24248+
setSearchParams(localVarUrlObj, localVarQueryParameter)
24249+
let headersFromBaseOptions =
24250+
baseOptions && baseOptions.headers ? baseOptions.headers : {}
24251+
localVarRequestOptions.headers = {
24252+
...localVarHeaderParameter,
24253+
...headersFromBaseOptions,
24254+
...options.headers,
24255+
}
24256+
24257+
return {
24258+
url: toPathString(localVarUrlObj),
24259+
options: localVarRequestOptions,
24260+
}
24261+
},
2412924262
/**
2413024263
* Viewset for UserLists
2413124264
* @summary Update
@@ -24504,6 +24637,33 @@ export const UserlistsApiFp = function (configuration?: Configuration) {
2450424637
configuration,
2450524638
)(axios, operationBasePath || basePath)
2450624639
},
24640+
/**
24641+
* Get a list of all userlist items for a user
24642+
* @summary List
24643+
* @param {*} [options] Override http request option.
24644+
* @throws {RequiredError}
24645+
*/
24646+
async userlistsMembershipList(
24647+
options?: RawAxiosRequestConfig,
24648+
): Promise<
24649+
(
24650+
axios?: AxiosInstance,
24651+
basePath?: string,
24652+
) => AxiosPromise<Array<MicroUserListRelationship>>
24653+
> {
24654+
const localVarAxiosArgs =
24655+
await localVarAxiosParamCreator.userlistsMembershipList(options)
24656+
const index = configuration?.serverIndex ?? 0
24657+
const operationBasePath =
24658+
operationServerMap["UserlistsApi.userlistsMembershipList"]?.[index]?.url
24659+
return (axios, basePath) =>
24660+
createRequestFunction(
24661+
localVarAxiosArgs,
24662+
globalAxios,
24663+
BASE_PATH,
24664+
configuration,
24665+
)(axios, operationBasePath || basePath)
24666+
},
2450724667
/**
2450824668
* Viewset for UserLists
2450924669
* @summary Update
@@ -24722,6 +24882,19 @@ export const UserlistsApiFactory = function (
2472224882
)
2472324883
.then((request) => request(axios, basePath))
2472424884
},
24885+
/**
24886+
* Get a list of all userlist items for a user
24887+
* @summary List
24888+
* @param {*} [options] Override http request option.
24889+
* @throws {RequiredError}
24890+
*/
24891+
userlistsMembershipList(
24892+
options?: RawAxiosRequestConfig,
24893+
): AxiosPromise<Array<MicroUserListRelationship>> {
24894+
return localVarFp
24895+
.userlistsMembershipList(options)
24896+
.then((request) => request(axios, basePath))
24897+
},
2472524898
/**
2472624899
* Viewset for UserLists
2472724900
* @summary Update
@@ -25127,6 +25300,19 @@ export class UserlistsApi extends BaseAPI {
2512725300
.then((request) => request(this.axios, this.basePath))
2512825301
}
2512925302

25303+
/**
25304+
* Get a list of all userlist items for a user
25305+
* @summary List
25306+
* @param {*} [options] Override http request option.
25307+
* @throws {RequiredError}
25308+
* @memberof UserlistsApi
25309+
*/
25310+
public userlistsMembershipList(options?: RawAxiosRequestConfig) {
25311+
return UserlistsApiFp(this.configuration)
25312+
.userlistsMembershipList(options)
25313+
.then((request) => request(this.axios, this.basePath))
25314+
}
25315+
2513025316
/**
2513125317
* Viewset for UserLists
2513225318
* @summary Update

learning_resources/permissions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ def has_object_permission(self, request, view, obj): # noqa: ARG002
4747
return can_edit
4848

4949

50+
class HasLearningPathMembershipPermissions(BasePermission):
51+
"""
52+
Permission to view all LearningPath memberships
53+
"""
54+
55+
def has_permission(self, request, view): # noqa: ARG002
56+
return is_admin_user(request) or is_learning_path_editor(request)
57+
58+
5059
class HasLearningPathItemPermissions(BasePermission):
5160
"""Permission to view/create/modify LearningPathItems"""
5261

learning_resources/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@
100100
router.register(r"offerors", views.OfferedByViewSet, basename="offerors_api")
101101

102102
v1_urls = [
103+
path(
104+
"learningpaths/membership/",
105+
views.LearningPathMembershipViewSet.as_view({"get": "list"}),
106+
name="learningpaths_api-membership",
107+
),
108+
path(
109+
"userlists/membership/",
110+
views.UserListMembershipViewSet.as_view({"get": "list"}),
111+
name="userlists_api-membership",
112+
),
103113
*router.urls,
104114
*nested_learning_resources_router.urls,
105115
*nested_courses_router.urls,

learning_resources/views.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from rest_framework.filters import OrderingFilter
1919
from rest_framework.generics import get_object_or_404
2020
from rest_framework.pagination import LimitOffsetPagination
21+
from rest_framework.permissions import IsAuthenticated
2122
from rest_framework.response import Response
2223
from rest_framework_nested.viewsets import NestedViewSetMixin
2324

@@ -71,6 +72,8 @@
7172
LearningResourceSchoolSerializer,
7273
LearningResourceSerializer,
7374
LearningResourceTopicSerializer,
75+
MicroLearningPathRelationshipSerializer,
76+
MicroUserListRelationshipSerializer,
7477
PodcastEpisodeResourceSerializer,
7578
PodcastResourceSerializer,
7679
ProgramResourceSerializer,
@@ -428,6 +431,32 @@ def get_queryset(self):
428431
return queryset
429432

430433

434+
@extend_schema_view(
435+
list=extend_schema(
436+
summary="List", description="Get a list of all learning path items"
437+
),
438+
)
439+
class LearningPathMembershipViewSet(viewsets.ReadOnlyModelViewSet):
440+
"""Viewset for listing all learning path relationships"""
441+
442+
serializer_class = MicroLearningPathRelationshipSerializer
443+
permission_classes = (permissions.HasLearningPathMembershipPermissions,)
444+
http_method_names = ["get"]
445+
446+
def get_queryset(self):
447+
"""
448+
Generate a QuerySet for fetching all LearningResourceRelationships
449+
with a parent of resource type "learning_path"
450+
451+
Returns:
452+
QuerySet of LearningResourceRelationships objects with learning path parents
453+
"""
454+
return LearningResourceRelationship.objects.filter(
455+
child__published=True,
456+
parent__resource_type=LearningResourceType.learning_path.name,
457+
).order_by("child", "parent")
458+
459+
431460
@extend_schema_view(
432461
list=extend_schema(
433462
summary="Nested Learning Resource List",
@@ -872,6 +901,31 @@ def podcast_rss_feed(request): # noqa: ARG001
872901
)
873902

874903

904+
@extend_schema_view(
905+
list=extend_schema(
906+
summary="List", description="Get a list of all userlist items for a user"
907+
),
908+
)
909+
class UserListMembershipViewSet(viewsets.ReadOnlyModelViewSet):
910+
"""Viewset for all user list relationships"""
911+
912+
serializer_class = MicroUserListRelationshipSerializer
913+
permission_classes = (IsAuthenticated,)
914+
http_method_names = ["get"]
915+
916+
def get_queryset(self):
917+
"""
918+
Generate a QuerySet for fetching all UserListRelationships for the user
919+
920+
Returns:
921+
QuerySet of UserListRelationship objects authored by the user
922+
"""
923+
return UserListRelationship.objects.filter(
924+
child__published=True,
925+
parent__author=self.request.user,
926+
).order_by("child", "parent")
927+
928+
875929
@method_decorator(blocked_ip_exempt, name="dispatch")
876930
class WebhookOCWView(views.APIView):
877931
"""

learning_resources/views_learningpath_test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,28 @@ def test_learning_path_endpoint_delete(client, user, is_editor):
360360
)
361361

362362

363+
@pytest.mark.parametrize("is_editor", [True, False])
364+
def test_learning_path_endpoint_membership_get(client, user, is_editor):
365+
"""Test learning path membership endpoint"""
366+
update_editor_group(user, is_editor)
367+
learning_paths = factories.LearningResourceFactory.create_batch(
368+
3, is_learning_path=True
369+
)
370+
relationships = models.LearningResourceRelationship.objects.filter(
371+
parent__in=learning_paths
372+
).order_by("child", "parent")
373+
374+
client.force_login(user)
375+
resp = client.get(reverse("lr:v1:learningpaths_api-membership"))
376+
if is_editor:
377+
assert len(resp.data) == relationships.count()
378+
for idx, relationship in enumerate(relationships):
379+
assert resp.data[idx]["parent"] == relationship.parent_id
380+
assert resp.data[idx]["child"] == relationship.child_id
381+
else:
382+
assert resp.status_code == 403
383+
384+
363385
@pytest.mark.parametrize("is_editor", [True, False])
364386
def test_get_resource_learning_paths(user_client, user, is_editor):
365387
"""Test that the learning paths are returned for a resource"""

0 commit comments

Comments
 (0)