Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions frontends/api/src/generated/v1/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18233,6 +18233,45 @@ export const LearningpathsApiAxiosParamCreator = function (
options: localVarRequestOptions,
}
},
/**
* Get a list of all learning path items
* @summary List
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
learningpathsMembershipList: async (
options: RawAxiosRequestConfig = {},
): Promise<RequestArgs> => {
const localVarPath = `/api/v1/learningpaths/membership/`
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
let baseOptions
if (configuration) {
baseOptions = configuration.baseOptions
}

const localVarRequestOptions = {
method: "GET",
...baseOptions,
...options,
}
const localVarHeaderParameter = {} as any
const localVarQueryParameter = {} as any

setSearchParams(localVarUrlObj, localVarQueryParameter)
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers,
}

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
}
},
/**
* Update individual fields of a learning path
* @summary Update
Expand Down Expand Up @@ -18672,6 +18711,35 @@ export const LearningpathsApiFp = function (configuration?: Configuration) {
configuration,
)(axios, operationBasePath || basePath)
},
/**
* Get a list of all learning path items
* @summary List
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async learningpathsMembershipList(
options?: RawAxiosRequestConfig,
): Promise<
(
axios?: AxiosInstance,
basePath?: string,
) => AxiosPromise<Array<MicroLearningPathRelationship>>
> {
const localVarAxiosArgs =
await localVarAxiosParamCreator.learningpathsMembershipList(options)
const index = configuration?.serverIndex ?? 0
const operationBasePath =
operationServerMap["LearningpathsApi.learningpathsMembershipList"]?.[
index
]?.url
return (axios, basePath) =>
createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration,
)(axios, operationBasePath || basePath)
},
/**
* Update individual fields of a learning path
* @summary Update
Expand Down Expand Up @@ -18917,6 +18985,19 @@ export const LearningpathsApiFactory = function (
)
.then((request) => request(axios, basePath))
},
/**
* Get a list of all learning path items
* @summary List
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
learningpathsMembershipList(
options?: RawAxiosRequestConfig,
): AxiosPromise<Array<MicroLearningPathRelationship>> {
return localVarFp
.learningpathsMembershipList(options)
.then((request) => request(axios, basePath))
},
/**
* Update individual fields of a learning path
* @summary Update
Expand Down Expand Up @@ -19456,6 +19537,19 @@ export class LearningpathsApi extends BaseAPI {
.then((request) => request(this.axios, this.basePath))
}

/**
* Get a list of all learning path items
* @summary List
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof LearningpathsApi
*/
public learningpathsMembershipList(options?: RawAxiosRequestConfig) {
return LearningpathsApiFp(this.configuration)
.learningpathsMembershipList(options)
.then((request) => request(this.axios, this.basePath))
}

/**
* Update individual fields of a learning path
* @summary Update
Expand Down Expand Up @@ -24126,6 +24220,45 @@ export const UserlistsApiAxiosParamCreator = function (
options: localVarRequestOptions,
}
},
/**
* Get a list of all userlist items for a user
* @summary List
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
userlistsMembershipList: async (
options: RawAxiosRequestConfig = {},
): Promise<RequestArgs> => {
const localVarPath = `/api/v1/userlists/membership/`
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
let baseOptions
if (configuration) {
baseOptions = configuration.baseOptions
}

const localVarRequestOptions = {
method: "GET",
...baseOptions,
...options,
}
const localVarHeaderParameter = {} as any
const localVarQueryParameter = {} as any

setSearchParams(localVarUrlObj, localVarQueryParameter)
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers,
}

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
}
},
/**
* Viewset for UserLists
* @summary Update
Expand Down Expand Up @@ -24504,6 +24637,33 @@ export const UserlistsApiFp = function (configuration?: Configuration) {
configuration,
)(axios, operationBasePath || basePath)
},
/**
* Get a list of all userlist items for a user
* @summary List
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async userlistsMembershipList(
options?: RawAxiosRequestConfig,
): Promise<
(
axios?: AxiosInstance,
basePath?: string,
) => AxiosPromise<Array<MicroUserListRelationship>>
> {
const localVarAxiosArgs =
await localVarAxiosParamCreator.userlistsMembershipList(options)
const index = configuration?.serverIndex ?? 0
const operationBasePath =
operationServerMap["UserlistsApi.userlistsMembershipList"]?.[index]?.url
return (axios, basePath) =>
createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration,
)(axios, operationBasePath || basePath)
},
/**
* Viewset for UserLists
* @summary Update
Expand Down Expand Up @@ -24722,6 +24882,19 @@ export const UserlistsApiFactory = function (
)
.then((request) => request(axios, basePath))
},
/**
* Get a list of all userlist items for a user
* @summary List
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
userlistsMembershipList(
options?: RawAxiosRequestConfig,
): AxiosPromise<Array<MicroUserListRelationship>> {
return localVarFp
.userlistsMembershipList(options)
.then((request) => request(axios, basePath))
},
/**
* Viewset for UserLists
* @summary Update
Expand Down Expand Up @@ -25127,6 +25300,19 @@ export class UserlistsApi extends BaseAPI {
.then((request) => request(this.axios, this.basePath))
}

/**
* Get a list of all userlist items for a user
* @summary List
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof UserlistsApi
*/
public userlistsMembershipList(options?: RawAxiosRequestConfig) {
return UserlistsApiFp(this.configuration)
.userlistsMembershipList(options)
.then((request) => request(this.axios, this.basePath))
}

/**
* Viewset for UserLists
* @summary Update
Expand Down
9 changes: 9 additions & 0 deletions learning_resources/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ def has_object_permission(self, request, view, obj): # noqa: ARG002
return can_edit


class HasLearningPathMembershipPermissions(BasePermission):
"""
Permission to view all LearningPath memberships
"""

def has_permission(self, request, view): # noqa: ARG002
return is_admin_user(request) or is_learning_path_editor(request)


class HasLearningPathItemPermissions(BasePermission):
"""Permission to view/create/modify LearningPathItems"""

Expand Down
10 changes: 10 additions & 0 deletions learning_resources/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@
router.register(r"offerors", views.OfferedByViewSet, basename="offerors_api")

v1_urls = [
path(
"learningpaths/membership/",
views.LearningPathMembershipViewSet.as_view({"get": "list"}),
name="learningpaths_api-membership",
),
path(
"userlists/membership/",
views.UserListMembershipViewSet.as_view({"get": "list"}),
name="userlists_api-membership",
),
*router.urls,
*nested_learning_resources_router.urls,
*nested_courses_router.urls,
Expand Down
54 changes: 54 additions & 0 deletions learning_resources/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from rest_framework.filters import OrderingFilter
from rest_framework.generics import get_object_or_404
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework_nested.viewsets import NestedViewSetMixin

Expand Down Expand Up @@ -71,6 +72,8 @@
LearningResourceSchoolSerializer,
LearningResourceSerializer,
LearningResourceTopicSerializer,
MicroLearningPathRelationshipSerializer,
MicroUserListRelationshipSerializer,
PodcastEpisodeResourceSerializer,
PodcastResourceSerializer,
ProgramResourceSerializer,
Expand Down Expand Up @@ -428,6 +431,32 @@ def get_queryset(self):
return queryset


@extend_schema_view(
list=extend_schema(
summary="List", description="Get a list of all learning path items"
),
)
class LearningPathMembershipViewSet(viewsets.ReadOnlyModelViewSet):
"""Viewset for listing all learning path relationships"""

serializer_class = MicroLearningPathRelationshipSerializer
permission_classes = (permissions.HasLearningPathMembershipPermissions,)
http_method_names = ["get"]

def get_queryset(self):
"""
Generate a QuerySet for fetching all LearningResourceRelationships
with a parent of resource type "learning_path"

Returns:
QuerySet of LearningResourceRelationships objects with learning path parents
"""
return LearningResourceRelationship.objects.filter(
child__published=True,
parent__resource_type=LearningResourceType.learning_path.name,
).order_by("child", "parent")


@extend_schema_view(
list=extend_schema(
summary="Nested Learning Resource List",
Expand Down Expand Up @@ -872,6 +901,31 @@ def podcast_rss_feed(request): # noqa: ARG001
)


@extend_schema_view(
list=extend_schema(
summary="List", description="Get a list of all userlist items for a user"
),
)
class UserListMembershipViewSet(viewsets.ReadOnlyModelViewSet):
"""Viewset for all user list relationships"""

serializer_class = MicroUserListRelationshipSerializer
permission_classes = (IsAuthenticated,)
http_method_names = ["get"]

def get_queryset(self):
"""
Generate a QuerySet for fetching all UserListRelationships for the user

Returns:
QuerySet of UserListRelationship objects authored by the user
"""
return UserListRelationship.objects.filter(
child__published=True,
parent__author=self.request.user,
).order_by("child", "parent")


@method_decorator(blocked_ip_exempt, name="dispatch")
class WebhookOCWView(views.APIView):
"""
Expand Down
22 changes: 22 additions & 0 deletions learning_resources/views_learningpath_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,28 @@ def test_learning_path_endpoint_delete(client, user, is_editor):
)


@pytest.mark.parametrize("is_editor", [True, False])
def test_learning_path_endpoint_membership_get(client, user, is_editor):
"""Test learning path membership endpoint"""
update_editor_group(user, is_editor)
learning_paths = factories.LearningResourceFactory.create_batch(
3, is_learning_path=True
)
relationships = models.LearningResourceRelationship.objects.filter(
parent__in=learning_paths
).order_by("child", "parent")

client.force_login(user)
resp = client.get(reverse("lr:v1:learningpaths_api-membership"))
if is_editor:
assert len(resp.data) == relationships.count()
for idx, relationship in enumerate(relationships):
assert resp.data[idx]["parent"] == relationship.parent_id
assert resp.data[idx]["child"] == relationship.child_id
else:
assert resp.status_code == 403


@pytest.mark.parametrize("is_editor", [True, False])
def test_get_resource_learning_paths(user_client, user, is_editor):
"""Test that the learning paths are returned for a resource"""
Expand Down
Loading
Loading