Skip to content

Commit 85edf82

Browse files
fix: enums added for course and library with util to get content details
1 parent 28ec587 commit 85edf82

6 files changed

Lines changed: 122 additions & 90 deletions

File tree

src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/admin.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
from django.db.models import Q
88
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
99

10+
from ol_openedx_git_auto_export.constants import (
11+
LIBRARY_V1_PREFIX,
12+
LIBRARY_V2_PREFIX,
13+
ContentType,
14+
)
1015
from ol_openedx_git_auto_export.models import ContentGitRepository
1116

1217

@@ -19,22 +24,22 @@ class ContentTypeFilter(SimpleListFilter):
1924
def lookups(self, request, model_admin): # noqa: ARG002
2025
"""Return filter options."""
2126
return (
22-
("course", "Course"),
23-
("library", "Library"),
27+
(ContentType.COURSE.value, ContentType.COURSE.display_name),
28+
(ContentType.LIBRARY.value, ContentType.LIBRARY.display_name),
2429
)
2530

2631
def queryset(self, request, queryset): # noqa: ARG002
2732
"""Filter the queryset based on the selected content type."""
28-
if self.value() == "course":
33+
if self.value() == ContentType.COURSE.value:
2934
# Filter for courses (exclude libraries)
30-
return queryset.exclude(content_key__startswith="library-v1:").exclude(
31-
content_key__startswith="lib:" # lib v2
35+
return queryset.exclude(content_key__startswith=LIBRARY_V1_PREFIX).exclude(
36+
content_key__startswith=LIBRARY_V2_PREFIX
3237
)
33-
elif self.value() == "library":
38+
elif self.value() == ContentType.LIBRARY.value:
3439
# Filter for libraries
3540
return queryset.filter(
36-
Q(content_key__startswith="library-v1:")
37-
| Q(content_key__startswith="lib:") # lib v2
41+
Q(content_key__startswith=LIBRARY_V1_PREFIX)
42+
| Q(content_key__startswith=LIBRARY_V2_PREFIX)
3843
)
3944
return queryset
4045

@@ -61,8 +66,8 @@ class ContentGitRepositoryAdmin(admin.ModelAdmin):
6166
def content_type_display(self, obj):
6267
"""Display whether the content is a course or library."""
6368
if isinstance(obj.content_key, (LibraryLocator, LibraryLocatorV2)):
64-
return "Library"
65-
return "Course"
69+
return ContentType.LIBRARY.display_name
70+
return ContentType.COURSE.display_name
6671

6772
def has_delete_permission(self, request, obj=None): # noqa: ARG002
6873
"""

src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/constants.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
from enum import StrEnum
2+
3+
4+
class ContentType(StrEnum):
5+
"""Enumeration for content types (Course or Library)."""
6+
7+
COURSE = "course"
8+
LIBRARY = "library"
9+
10+
@property
11+
def display_name(self):
12+
"""Return the human-readable display name."""
13+
return self.value.capitalize()
14+
15+
16+
# Library key prefixes for different versions
17+
LIBRARY_V1_PREFIX = "library-v1:"
18+
LIBRARY_V2_PREFIX = "lib:"
19+
120
ENABLE_GIT_AUTO_EXPORT = "ENABLE_GIT_AUTO_EXPORT"
221
ENABLE_AUTO_GITHUB_REPO_CREATION = "ENABLE_AUTO_GITHUB_REPO_CREATION"
322
GITHUB_ORG = "GITHUB_ORG"

src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/migrations/0002_contentgitrepository.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Generated migration for renaming CourseGitRepository to ContentGitRepository
22

3+
import opaque_keys.edx.django.models
34
from django.db import migrations
45

56

@@ -13,4 +14,16 @@ class Migration(migrations.Migration):
1314
old_name="CourseGitRepository",
1415
new_name="ContentGitRepository",
1516
),
17+
migrations.RenameField(
18+
model_name="contentgitrepository",
19+
old_name="course_key",
20+
new_name="content_key",
21+
),
22+
migrations.AlterField(
23+
model_name="contentgitrepository",
24+
name="content_key",
25+
field=opaque_keys.edx.django.models.LearningContextKeyField(
26+
max_length=255, primary_key=True, serialize=False
27+
),
28+
),
1629
]

src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/migrations/0003_migrate_course_to_content_repo.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/tasks.py

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@
1111
from cms.djangoapps.contentstore.git_export_utils import GitExportError, export_to_git
1212
from django.conf import settings
1313
from opaque_keys.edx.keys import LearningContextKey
14-
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
15-
from openedx.core.djangoapps.content_libraries.api import get_library
1614
from rest_framework import status
17-
from xmodule.modulestore.django import modulestore
1815

1916
from ol_openedx_git_auto_export.models import ContentGitRepository
2017
from ol_openedx_git_auto_export.utils import (
18+
get_content_info,
2119
github_repo_name_format,
2220
is_auto_repo_creation_enabled,
2321
)
@@ -36,65 +34,57 @@ def async_export_to_git(content_key_string, user=None):
3634
# Parse as LearningContextKey to support all learning contexts
3735
try:
3836
content_key = LearningContextKey.from_string(content_key_string)
39-
is_v1_library = isinstance(content_key, LibraryLocator)
40-
is_v2_library = isinstance(content_key, LibraryLocatorV2)
37+
content_info = get_content_info(content_key)
4138
except Exception:
4239
LOGGER.exception("Failed to parse content key: %s", content_key_string)
4340
return
4441

45-
# Get the content module (course or library)
46-
if is_v2_library:
47-
# V2 libraries use content_libraries API
48-
content_module = get_library(content_key)
49-
content_type = "library"
50-
elif is_v1_library:
51-
# V1 libraries use modulestore
52-
content_module = modulestore().get_library(content_key)
53-
content_type = "library"
54-
else:
55-
content_module = modulestore().get_course(content_key)
56-
content_type = "course"
57-
5842
try:
5943
content_repo = ContentGitRepository.objects.get(content_key=content_key)
6044

6145
if content_repo.is_export_enabled:
6246
LOGGER.info(
6347
"Starting async %s content export to git (%s id: %s)",
64-
content_type,
65-
content_type,
66-
content_module.id if hasattr(content_module, "id") else content_key,
48+
content_info["content_type"],
49+
content_info["content_type"],
50+
content_info["content_module"].id
51+
if hasattr(content_info["content_module"], "id")
52+
else content_key,
6753
)
6854
# Use unified export_to_git that handles both courses and libraries
6955
export_to_git(content_key, content_repo.git_url, user=user)
7056
else:
7157
LOGGER.info(
7258
"Git export is disabled for %s %s. Skipping export.",
73-
content_type,
59+
content_info["content_type"],
7460
content_key_string,
7561
)
7662
except GitExportError:
7763
LOGGER.exception(
7864
"Failed async %s content export to git (%s id: %s)",
79-
content_type,
80-
content_type,
81-
content_module.id if hasattr(content_module, "id") else content_key,
65+
content_info["content_type"],
66+
content_info["content_type"],
67+
content_info["content_module"].id
68+
if hasattr(content_info["content_module"], "id")
69+
else content_key,
8270
)
8371
except ContentGitRepository.DoesNotExist:
8472
LOGGER.exception(
8573
"Git repository does not exist for %s %s. "
8674
"Creating repository and exporting content.",
87-
content_type,
75+
content_info["content_type"],
8876
content_key_string,
8977
)
90-
if is_auto_repo_creation_enabled(is_library=is_v1_library or is_v2_library):
78+
if is_auto_repo_creation_enabled(is_library=content_info["is_library"]):
9179
async_create_github_repo.delay(str(content_key), export_content=True)
9280
except Exception:
9381
LOGGER.exception(
9482
"Unknown error occurred during async %s content export to git (%s id: %s)",
95-
content_type,
96-
content_type,
97-
content_module.id if hasattr(content_module, "id") else content_key,
83+
content_info["content_type"],
84+
content_info["content_type"],
85+
content_info["content_module"].id
86+
if hasattr(content_info["content_module"], "id")
87+
else content_key,
9888
)
9989

10090

@@ -120,40 +110,28 @@ def async_create_github_repo(self, content_key_str, export_content=False): # no
120110
# Parse as LearningContextKey to support all learning contexts
121111
try:
122112
content_key = LearningContextKey.from_string(content_key_str)
123-
is_v1_library = isinstance(content_key, LibraryLocator)
124-
is_v2_library = isinstance(content_key, LibraryLocatorV2)
113+
content_info = get_content_info(content_key)
125114
except Exception:
126115
LOGGER.exception("Failed to parse content key: %s", content_key_str)
127116
return False, f"Invalid content key: {content_key_str}"
128117

129-
content_type = "library" if is_v1_library or is_v2_library else "course"
130118
content_id_slugified = github_repo_name_format(str(content_key))
131-
132119
response_msg = ""
133120

134121
# Check if repository already exists
135122
if ContentGitRepository.objects.filter(content_key=content_key).exists():
136-
response_msg = f"GitHub repository already exists for {content_type} {content_key}. Skipping creation." # noqa: E501
123+
response_msg = f"GitHub repository already exists for {content_info['content_type']} {content_key}. Skipping creation." # noqa: E501
137124
LOGGER.info(response_msg)
138125
return False, response_msg
139126

140-
# Get the content module (course or library)
141-
if is_v2_library:
142-
# V2 libraries use content_libraries API
143-
content_module = get_library(content_key)
144-
url_path = f"library/{content_key_str}"
145-
elif is_v1_library:
146-
# V1 libraries use modulestore
147-
content_module = modulestore().get_library(content_key)
148-
url_path = f"library/{content_key_str}"
149-
else:
150-
content_module = modulestore().get_course(content_key)
151-
url_path = f"course/{content_key_str}"
127+
# Determine URL path based on content type
128+
url_path = f"{content_info['content_type']}/{content_key_str}"
152129

153-
if is_v2_library:
154-
display_name = content_module.title
130+
# Get display name (v2 libraries use 'title', others use 'display_name')
131+
if content_info["is_v2_library"]:
132+
display_name = content_info["content_module"].title
155133
else:
156-
display_name = content_module.display_name
134+
display_name = content_info["content_module"].display_name
157135

158136
url = f"{settings.GITHUB_ORG_API_URL}/repos"
159137
# https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28
@@ -174,15 +152,15 @@ def async_create_github_repo(self, content_key_str, export_content=False): # no
174152
}
175153
response = requests.post(url, headers=headers, json=payload, timeout=30)
176154
if response.status_code != status.HTTP_201_CREATED:
177-
response_msg = f"Failed to create GitHub repository for {content_type} {content_key}: {response.json()}" # noqa: E501
155+
response_msg = f"Failed to create GitHub repository for {content_info['content_type']} {content_key}: {response.json()}" # noqa: E501
178156
LOGGER.error(response_msg)
179157

180158
# Retry the task if we haven't exceeded max retries
181159
max_retries = self.retry_kwargs.get("max_retries", 3)
182160
if self.request.retries < max_retries:
183161
LOGGER.info(
184162
"Retrying GitHub repository creation for %s %s (attempt %d/%d)",
185-
content_type,
163+
content_info["content_type"],
186164
content_key,
187165
self.request.retries + 1,
188166
max_retries,
@@ -202,14 +180,14 @@ def async_create_github_repo(self, content_key_str, export_content=False): # no
202180
)
203181
LOGGER.info(
204182
"GitHub repository created for %s %s: %s",
205-
content_type,
183+
content_info["content_type"],
206184
content_key,
207185
ssh_url,
208186
)
209187
else:
210188
response_msg = f"""
211189
Failed to retrieve SSH URL from GitHub response
212-
for {content_type} {content_key}.
190+
for {content_info["content_type"]} {content_key}.
213191
Response data: {repo_data}
214192
"""
215193
LOGGER.error(response_msg)

src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/utils.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from django.conf import settings
1010
from django.contrib.auth.models import User
1111
from django.core.exceptions import ImproperlyConfigured
12-
from opaque_keys.edx.locator import LibraryLocatorV2
12+
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
1313
from openedx.core.djangoapps.content_libraries.api import get_library
1414
from xmodule.modulestore.django import modulestore
1515

@@ -19,11 +19,53 @@
1919
ENABLE_GIT_AUTO_EXPORT,
2020
ENABLE_GIT_AUTO_LIBRARY_EXPORT,
2121
REPOSITORY_NAME_MAX_LENGTH,
22+
ContentType,
2223
)
2324

2425
log = logging.getLogger(__name__)
2526

2627

28+
def get_content_info(content_key):
29+
"""
30+
Get information about a content item (course or library).
31+
32+
Args:
33+
content_key: A LearningContextKey
34+
35+
Returns:
36+
dict: Dictionary containing:
37+
- content_type: The ContentType enum value (str)
38+
- content_module: The actual course/library object
39+
- is_v1_library: Boolean flag
40+
- is_v2_library: Boolean flag
41+
- is_library: Boolean flag (True if v1 or v2 library)
42+
"""
43+
is_v1_library = isinstance(content_key, LibraryLocator)
44+
is_v2_library = isinstance(content_key, LibraryLocatorV2)
45+
46+
# Get the content module based on type
47+
if is_v2_library:
48+
# V2 libraries use content_libraries API
49+
content_module = get_library(content_key)
50+
content_type = ContentType.LIBRARY.value
51+
elif is_v1_library:
52+
# V1 libraries use modulestore
53+
content_module = modulestore().get_library(content_key)
54+
content_type = ContentType.LIBRARY.value
55+
else:
56+
# Courses use modulestore
57+
content_module = modulestore().get_course(content_key)
58+
content_type = ContentType.COURSE.value
59+
60+
return {
61+
"content_type": content_type,
62+
"content_module": content_module,
63+
"is_v1_library": is_v1_library,
64+
"is_v2_library": is_v2_library,
65+
"is_library": is_v1_library or is_v2_library,
66+
}
67+
68+
2769
def get_publisher_username(course_module):
2870
"""
2971
Return the username of the user who published the course.

0 commit comments

Comments
 (0)