Skip to content

Commit d1b7b6e

Browse files
refactor: split libraries API & REST API up into smaller modules
1 parent d125b04 commit d1b7b6e

File tree

13 files changed

+236
-107
lines changed

13 files changed

+236
-107
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""
2+
Python API for working with content libraries
3+
"""
4+
from .libraries import *
5+
from .blocks import *
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
Content libraries API methods related to XBlocks/Components.
3+
4+
These methods don't enforce permissions (only the REST APIs do).
5+
"""
6+
# pylint: disable=unused-import
7+
8+
# TODO: move all the API methods related to blocks and assets in here from 'libraries.py'
9+
# TODO: use __all__ to limit what symbols are public.
10+
11+
from .libraries import (
12+
LibraryXBlockMetadata,
13+
LibraryXBlockStaticFile,
14+
LibraryXBlockType,
15+
get_library_components,
16+
get_library_block,
17+
set_library_block_olx,
18+
library_component_usage_key,
19+
get_component_from_usage_key,
20+
validate_can_add_block_to_library,
21+
create_library_block,
22+
import_staged_content_from_user_clipboard,
23+
get_or_create_olx_media_type,
24+
delete_library_block,
25+
restore_library_block,
26+
get_library_block_static_asset_files,
27+
add_library_block_static_asset_file,
28+
delete_library_block_static_asset_file,
29+
publish_component_changes,
30+
)

openedx/core/djangoapps/content_libraries/api.py renamed to openedx/core/djangoapps/content_libraries/api/libraries.py

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
import requests
6363

6464
from django.conf import settings
65-
from django.contrib.auth.models import AbstractUser, Group
65+
from django.contrib.auth.models import AbstractUser, AnonymousUser, Group
6666
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
6767
from django.core.validators import validate_unicode_slug
6868
from django.db import IntegrityError, transaction
@@ -116,12 +116,60 @@
116116
from openedx.core.types import User as UserType
117117
from xmodule.modulestore.django import modulestore
118118

119-
from . import permissions, tasks
120-
from .constants import ALL_RIGHTS_RESERVED
121-
from .models import ContentLibrary, ContentLibraryPermission, ContentLibraryBlockImportTask
119+
from .. import permissions, tasks
120+
from ..constants import ALL_RIGHTS_RESERVED
121+
from ..models import ContentLibrary, ContentLibraryPermission, ContentLibraryBlockImportTask
122122

123123
log = logging.getLogger(__name__)
124124

125+
# The public API is only the following symbols:
126+
__all__ = [
127+
# Exceptions - maybe move them to a new file?
128+
"ContentLibraryNotFound",
129+
"ContentLibraryCollectionNotFound",
130+
"ContentLibraryBlockNotFound",
131+
"LibraryAlreadyExists",
132+
"LibraryCollectionAlreadyExists",
133+
"LibraryBlockAlreadyExists",
134+
"BlockLimitReachedError",
135+
"IncompatibleTypesError",
136+
"InvalidNameError",
137+
"LibraryPermissionIntegrityError",
138+
# Library Models
139+
"ContentLibrary", # Should this be public or not?
140+
"ContentLibraryMetadata",
141+
"AccessLevel",
142+
"ContentLibraryPermissionEntry",
143+
"CollectionMetadata",
144+
# Library API methods
145+
"user_can_create_library",
146+
"get_libraries_for_user",
147+
"get_metadata",
148+
"require_permission_for_library_key",
149+
"get_library",
150+
"create_library",
151+
"get_library_team",
152+
"get_library_user_permissions",
153+
"set_library_user_permissions",
154+
"set_library_group_permissions",
155+
"update_library",
156+
"delete_library",
157+
"get_allowed_block_types",
158+
"publish_changes",
159+
"revert_changes",
160+
# Collections - TODO: move to a new file
161+
"create_library_collection",
162+
"update_library_collection",
163+
"update_library_collection_components",
164+
"set_library_component_collections",
165+
"get_library_collection_usage_key",
166+
"get_library_collection_from_usage_key",
167+
# Import - TODO: move to a new file
168+
"EdxModulestoreImportClient",
169+
"EdxApiImportClient",
170+
"import_blocks_create_task",
171+
]
172+
125173

126174
# Exceptions
127175
# ==========
@@ -416,6 +464,7 @@ def get_library(library_key: LibraryLocatorV2) -> ContentLibraryMetadata:
416464
"""
417465
ref = ContentLibrary.objects.get_by_key(library_key)
418466
learning_package = ref.learning_package
467+
assert learning_package is not None # Shouldn't happen - this is just for the type checker
419468
num_blocks = authoring_api.get_all_drafts(learning_package.id).count()
420469
last_publish_log = authoring_api.get_last_publish(learning_package.id)
421470
last_draft_log = authoring_api.get_entities_with_unpublished_changes(learning_package.id) \
@@ -455,7 +504,7 @@ def get_library(library_key: LibraryLocatorV2) -> ContentLibraryMetadata:
455504
return ContentLibraryMetadata(
456505
key=library_key,
457506
title=learning_package.title,
458-
description=ref.learning_package.description,
507+
description=learning_package.description,
459508
num_blocks=num_blocks,
460509
version=version,
461510
last_published=None if last_publish_log is None else last_publish_log.published_at,
@@ -557,6 +606,8 @@ def get_library_user_permissions(library_key: LibraryLocatorV2, user: UserType)
557606
Fetch the specified user's access information. Will return None if no
558607
permissions have been granted.
559608
"""
609+
if isinstance(user, AnonymousUser):
610+
return None # Mostly here for the type checker
560611
ref = ContentLibrary.objects.get_by_key(library_key)
561612
grant = ref.permission_grants.filter(user=user).first()
562613
if grant is None:
@@ -574,6 +625,8 @@ def set_library_user_permissions(library_key: LibraryLocatorV2, user: UserType,
574625
575626
access_level should be one of the AccessLevel values defined above.
576627
"""
628+
if isinstance(user, AnonymousUser):
629+
raise TypeError("Invalid user type") # Mostly here for the type checker
577630
ref = ContentLibrary.objects.get_by_key(library_key)
578631
current_grant = get_library_user_permissions(library_key, user)
579632
if current_grant and current_grant.access_level == AccessLevel.ADMIN_LEVEL:
@@ -633,6 +686,8 @@ def update_library(
633686
return
634687

635688
content_lib = ContentLibrary.objects.get_by_key(library_key)
689+
learning_package_id = content_lib.learning_package_id
690+
assert learning_package_id is not None
636691

637692
with transaction.atomic():
638693
# We need to make updates to both the ContentLibrary and its linked
@@ -643,12 +698,12 @@ def update_library(
643698
if allow_public_read is not None:
644699
content_lib.allow_public_read = allow_public_read
645700
if library_license is not None:
646-
content_lib.library_license = library_license
701+
content_lib.license = library_license
647702
content_lib.save()
648703

649704
if learning_pkg_changed:
650705
authoring_api.update_learning_package(
651-
content_lib.learning_package_id,
706+
learning_package_id,
652707
title=title,
653708
description=description,
654709
)
@@ -675,7 +730,8 @@ def delete_library(library_key: LibraryLocatorV2) -> None:
675730
# TODO: We should eventually detach the LearningPackage and delete it
676731
# asynchronously, especially if we need to delete a bunch of stuff
677732
# on the filesystem for it.
678-
learning_package.delete()
733+
if learning_package:
734+
learning_package.delete()
679735

680736
CONTENT_LIBRARY_DELETED.send_event(
681737
content_library=ContentLibraryData(
@@ -709,6 +765,7 @@ def get_library_components(
709765
"""
710766
lib = ContentLibrary.objects.get_by_key(library_key) # type: ignore[attr-defined]
711767
learning_package = lib.learning_package
768+
assert learning_package is not None
712769
components = authoring_api.get_components(
713770
learning_package.id,
714771
draft=True,
@@ -860,7 +917,8 @@ def validate_can_add_block_to_library(
860917
content_library = ContentLibrary.objects.get_by_key(library_key) # type: ignore[attr-defined]
861918

862919
# If adding a component would take us over our max, return an error.
863-
component_count = authoring_api.get_all_drafts(content_library.learning_package.id).count()
920+
assert content_library.learning_package_id is not None
921+
component_count = authoring_api.get_all_drafts(content_library.learning_package_id).count()
864922
if component_count + 1 > settings.MAX_BLOCKS_PER_CONTENT_LIBRARY:
865923
raise BlockLimitReachedError(
866924
_("Library cannot have more than {} Components").format(
@@ -1356,7 +1414,7 @@ def publish_changes(library_key: LibraryLocatorV2, user_id: int | None = None):
13561414
Publish all pending changes to the specified library.
13571415
"""
13581416
learning_package = ContentLibrary.objects.get_by_key(library_key).learning_package
1359-
1417+
assert learning_package is not None # shouldn't happen but it's technically possible.
13601418
authoring_api.publish_all_drafts(learning_package.id, published_by=user_id)
13611419

13621420
CONTENT_LIBRARY_UPDATED.send_event(
@@ -1398,6 +1456,7 @@ def revert_changes(library_key: LibraryLocatorV2) -> None:
13981456
last published version.
13991457
"""
14001458
learning_package = ContentLibrary.objects.get_by_key(library_key).learning_package
1459+
assert learning_package is not None # shouldn't happen but it's technically possible.
14011460
authoring_api.reset_drafts_to_published(learning_package.id)
14021461

14031462
CONTENT_LIBRARY_UPDATED.send_event(
@@ -1652,6 +1711,7 @@ def get_library_collection_from_usage_key(
16521711
library_key = collection_usage_key.library_key
16531712
collection_key = collection_usage_key.collection_id
16541713
content_library = ContentLibrary.objects.get_by_key(library_key) # type: ignore[attr-defined]
1714+
assert content_library.learning_package_id is not None # shouldn't happen but it's technically possible.
16551715
try:
16561716
return authoring_api.get_collection(
16571717
content_library.learning_package_id,

openedx/core/djangoapps/content_libraries/models.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
import contextlib
3838
import logging
39+
from typing import ClassVar
3940
import uuid
4041

4142
from django.contrib.auth import get_user_model
@@ -67,11 +68,11 @@
6768
User = get_user_model()
6869

6970

70-
class ContentLibraryManager(models.Manager):
71+
class ContentLibraryManager(models.Manager["ContentLibrary"]):
7172
"""
7273
Custom manager for ContentLibrary class.
7374
"""
74-
def get_by_key(self, library_key):
75+
def get_by_key(self, library_key) -> "ContentLibrary":
7576
"""
7677
Get the ContentLibrary for the given LibraryLocatorV2 key.
7778
"""
@@ -92,7 +93,7 @@ class ContentLibrary(models.Model):
9293
9394
.. no_pii:
9495
"""
95-
objects: ContentLibraryManager[ContentLibrary] = ContentLibraryManager()
96+
objects: ClassVar[ContentLibraryManager] = ContentLibraryManager()
9697

9798
id = models.AutoField(primary_key=True)
9899
# Every Library is uniquely and permanently identified by an 'org' and a

openedx/core/djangoapps/content_libraries/rest_api/__init__.py

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
Content Library REST APIs related to XBlocks/Components and their static assets
3+
"""
4+
# pylint: disable=unused-import
5+
6+
# TODO: move the block and block asset related views from 'libraries' into this file
7+
from .libraries import (
8+
LibraryBlockAssetListView,
9+
LibraryBlockAssetView,
10+
LibraryBlockCollectionsView,
11+
LibraryBlockLtiUrlView,
12+
LibraryBlockOlxView,
13+
LibraryBlockPublishView,
14+
LibraryBlockRestore,
15+
LibraryBlocksView,
16+
LibraryBlockView,
17+
LibraryComponentAssetView,
18+
LibraryComponentDraftAssetView,
19+
)

openedx/core/djangoapps/content_libraries/views_collections.py renamed to openedx/core/djangoapps/content_libraries/rest_api/collections.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""
22
Collections API Views
33
"""
4-
54
from __future__ import annotations
65

76
from django.db.models import QuerySet
@@ -17,10 +16,10 @@
1716
from openedx_learning.api import authoring as authoring_api
1817
from openedx_learning.api.authoring_models import Collection
1918

20-
from openedx.core.djangoapps.content_libraries import api, permissions
21-
from openedx.core.djangoapps.content_libraries.models import ContentLibrary
22-
from openedx.core.djangoapps.content_libraries.views import convert_exceptions
23-
from openedx.core.djangoapps.content_libraries.serializers import (
19+
from .. import api, permissions
20+
from ..models import ContentLibrary
21+
from .utils import convert_exceptions
22+
from .serializers import (
2423
ContentLibraryCollectionSerializer,
2524
ContentLibraryCollectionComponentsUpdateSerializer,
2625
ContentLibraryCollectionUpdateSerializer,

openedx/core/djangoapps/content_libraries/views.py renamed to openedx/core/djangoapps/content_libraries/rest_api/libraries.py

Lines changed: 3 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@
6262
to Learning Core) atomic:
6363
https://github.com/openedx/edx-platform/pull/30456
6464
"""
65-
66-
from functools import wraps
6765
import itertools
6866
import json
6967
import logging
@@ -86,7 +84,6 @@
8684
from pylti1p3.exception import LtiException, OIDCException
8785

8886
import edx_api_doc_tools as apidocs
89-
from opaque_keys import InvalidKeyError
9087
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2
9188
from openedx_learning.api import authoring
9289
from organizations.api import ensure_organization
@@ -105,7 +102,7 @@
105102
user_can_create_organizations,
106103
)
107104
from openedx.core.djangoapps.content_libraries import api, permissions
108-
from openedx.core.djangoapps.content_libraries.serializers import (
105+
from openedx.core.djangoapps.content_libraries.rest_api.serializers import (
109106
ContentLibraryBlockImportTaskCreateSerializer,
110107
ContentLibraryBlockImportTaskSerializer,
111108
ContentLibraryFilterSerializer,
@@ -129,50 +126,14 @@
129126
from openedx.core.djangoapps.xblock import api as xblock_api
130127
from openedx.core.types.http import RestRequest
131128

132-
from .models import ContentLibrary, LtiGradedResource, LtiProfile
129+
from .utils import convert_exceptions
130+
from ..models import ContentLibrary, LtiGradedResource, LtiProfile
133131

134132

135133
User = get_user_model()
136134
log = logging.getLogger(__name__)
137135

138136

139-
def convert_exceptions(fn):
140-
"""
141-
Catch any Content Library API exceptions that occur and convert them to
142-
DRF exceptions so DRF will return an appropriate HTTP response
143-
"""
144-
145-
@wraps(fn)
146-
def wrapped_fn(*args, **kwargs):
147-
try:
148-
return fn(*args, **kwargs)
149-
except InvalidKeyError as exc:
150-
log.exception(str(exc))
151-
raise NotFound # lint-amnesty, pylint: disable=raise-missing-from
152-
except api.ContentLibraryNotFound:
153-
log.exception("Content library not found")
154-
raise NotFound # lint-amnesty, pylint: disable=raise-missing-from
155-
except api.ContentLibraryBlockNotFound:
156-
log.exception("XBlock not found in content library")
157-
raise NotFound # lint-amnesty, pylint: disable=raise-missing-from
158-
except api.ContentLibraryCollectionNotFound:
159-
log.exception("Collection not found in content library")
160-
raise NotFound # lint-amnesty, pylint: disable=raise-missing-from
161-
except api.LibraryCollectionAlreadyExists as exc:
162-
log.exception(str(exc))
163-
raise ValidationError(str(exc)) # lint-amnesty, pylint: disable=raise-missing-from
164-
except api.LibraryBlockAlreadyExists as exc:
165-
log.exception(str(exc))
166-
raise ValidationError(str(exc)) # lint-amnesty, pylint: disable=raise-missing-from
167-
except api.InvalidNameError as exc:
168-
log.exception(str(exc))
169-
raise ValidationError(str(exc)) # lint-amnesty, pylint: disable=raise-missing-from
170-
except api.BlockLimitReachedError as exc:
171-
log.exception(str(exc))
172-
raise ValidationError(str(exc)) # lint-amnesty, pylint: disable=raise-missing-from
173-
return wrapped_fn
174-
175-
176137
class LibraryApiPaginationDocs:
177138
"""
178139
API docs for query params related to paginating ContentLibraryMetadata objects.

openedx/core/djangoapps/content_libraries/serializers.py renamed to openedx/core/djangoapps/content_libraries/rest_api/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
ContentLibrary
2020
)
2121
from openedx.core.lib.api.serializers import CourseKeyField
22-
from . import permissions
22+
from .. import permissions
2323

2424

2525
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'

0 commit comments

Comments
 (0)