diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index 616204ef59c7..bfb01017c733 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -26,3 +26,4 @@ VideoUploadSerializer, VideoUsageSerializer, ) +from .course_templates import CourseSerializer, CourseMetadataSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_templates.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_templates.py new file mode 100644 index 000000000000..ba1e9bf122ca --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_templates.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +class CourseMetadataSerializer(serializers.Serializer): + course_id = serializers.CharField() + title = serializers.CharField() + description = serializers.CharField() + thumbnail = serializers.URLField() + active = serializers.BooleanField() + +class CourseSerializer(serializers.Serializer): + courses_name = serializers.CharField() + zip_url = serializers.URLField() + metadata = CourseMetadataSerializer() diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 349218679709..5997d12269b1 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -11,13 +11,13 @@ CourseDetailsView, CourseTeamView, CourseTextbooksView, + CourseTemplatesListView, CourseIndexView, CourseGradingView, CourseGroupConfigurationsView, CourseRerunView, CourseSettingsView, CourseVideosView, - CourseWaffleFlagsView, HomePageView, HomePageCoursesView, HomePageLibrariesView, @@ -133,11 +133,10 @@ name="container_vertical" ), re_path( - fr'^course_waffle_flags(?:/{COURSE_ID_PATTERN})?$', - CourseWaffleFlagsView.as_view(), - name="course_waffle_flags" + fr'^course_templates/{settings.COURSE_ID_PATTERN}$', + CourseTemplatesListView.as_view(), + name="course_templates_api" ), - # Authoring API # Do not use under v1 yet (Nov. 23). The Authoring API is still experimental and the v0 versions should be used ] diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index 89d8d56eaa11..938f4003f548 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -5,8 +5,8 @@ from .course_details import CourseDetailsView from .course_index import CourseIndexView from .course_rerun import CourseRerunView -from .course_waffle_flags import CourseWaffleFlagsView from .course_team import CourseTeamView +from .course_templates import CourseTemplatesListView from .grading import CourseGradingView from .group_configurations import CourseGroupConfigurationsView from .help_urls import HelpUrlsView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_templates.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_templates.py new file mode 100644 index 000000000000..19138b887e39 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_templates.py @@ -0,0 +1,102 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.exceptions import NotFound +from opaque_keys.edx.keys import CourseKey # Import CourseKey if using edX +from django.conf import settings +import requests +from openedx.core.lib.api.view_utils import ( + DeveloperErrorViewMixin, + view_auth_classes, +) +from rest_framework.request import Request +from ..serializers import CourseSerializer, CourseMetadataSerializer + + +# @view_auth_classes(is_authenticated=True) +class CourseTemplatesListView(DeveloperErrorViewMixin, APIView): + """ + API endpoint to fetch and return course data from a GitHub repository. + + This view dynamically fetches course data from a specified GitHub repository + and returns it in a structured JSON format. It processes directories and files + in the repository to extract course names, ZIP URLs, and metadata files. + + Example URL: + /api/courses// + + Query Parameters: + course_key_string (str): The course key in the format `org+course+run`. + + Example Response: + [ + { + "courses_name": "AI Courses", + "zip_url": "https://raw.githubusercontent.com/awais786/courses/main/edly/AI%20Courses/course._Rnm_t%20(1).tar.gz", + "metadata": { + "course_id": "course-v1:edX+DemoX+T2024", + "title": "Introduction to Open edX", + "description": "Learn the fundamentals of the Open edX platform, including how to create and manage courses.", + "thumbnail": "https://discover.ilmx.org/wp-content/uploads/2024/01/Course-image-2.webp", + "active": true + } + } + ] + + Raises: + NotFound: If there is an error fetching data from the repository. + + """ + def get(self, request: Request, course_id: str): + """ + Handle GET requests to fetch course data. + + Args: + request: The HTTP request object. + course_id (str): The course id. + + Returns: + Response: A structured JSON response containing course data. + + """ + try: + # Extract organization from course key + course_key = CourseKey.from_string(course_id) + organization = course_key.org + + # GitHub repository details. It should come from settings. + templates_repo_url = f"https://api.github.com/repos/awais786/courses/contents/{organization}" + + # Fetch data from GitHub + data = fetch_contents(templates_repo_url) + courses = [] + for directory in data: + course_data = {'courses_name': directory["name"]} + contents = fetch_contents(directory["url"]) # Assume directory contains URL to course contents + + for item in contents: + if item['name'].endswith('.tar.gz'): # Check if file is a ZIP file + course_data['zip_url'] = item['download_url'] + elif item['name'].endswith('.json'): # Check if file is a JSON metadata file + course_data['metadata'] = fetch_contents(item['download_url']) + + courses.append(course_data) + + # Serialize and return the data + serializer = CourseSerializer(courses, many=True) + return Response(serializer.data) + + except Exception as err: + raise NotFound(f"Error fetching course data: {str(err)}") + + +def fetch_contents(url): + headers = { + "Authorization": f"token {settings.GITHUB_TOKEN_COURSE_TEMPLATES}", + "Accept": "application/vnd.github.v3+json", + } + try: + response = requests.get(url, headers=headers) + response.raise_for_status() # Raise error for 4xx/5xx responses + return response.json() + except Exception as err: + return JsonResponseBadRequest({"error": err.message}) diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index 22310faa1ae2..c504116eaece 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -9,6 +9,7 @@ import logging import os import re +import requests import shutil from wsgiref.util import FileWrapper @@ -24,6 +25,9 @@ from django.views.decorators.cache import cache_control from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_GET, require_http_methods +from django.shortcuts import render +from django.urls import reverse + from edx_django_utils.monitoring import set_custom_attribute, set_custom_attributes_for_course_key from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import LibraryLocator @@ -32,6 +36,7 @@ from user_tasks.conf import settings as user_tasks_settings from user_tasks.models import UserTaskArtifact, UserTaskStatus +from cms.djangoapps.contentstore.utils import reverse_course_url from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.student.auth import has_course_author_access from common.djangoapps.util.json_request import JsonResponse @@ -465,3 +470,20 @@ def _latest_task_status(request, course_key_string, view_func=None): for status_filter in STATUS_FILTERS: task_status = status_filter().filter_queryset(request, task_status, view_func) return task_status.order_by('-created').first() + + +def course_templates(request, course_key_string): + courselike_key = CourseKey.from_string(course_key_string) + organization = courselike_key.org + successful_url = f"http://localhost:18010/api/contentstore/v1/course_templates/{course_key_string}" + + courses = [] + resp = requests.get(successful_url) + if resp.status_code == 200: + courses = json.loads(resp._content.decode('utf-8')) + + return render(request, 'course_templates.html', { + 'upload_zip_endpoint': successful_url, + 'courses': courses, + 'organization': organization + }) diff --git a/cms/templates/course_templates.html b/cms/templates/course_templates.html new file mode 100644 index 000000000000..d05fcf76a4d0 --- /dev/null +++ b/cms/templates/course_templates.html @@ -0,0 +1,90 @@ + + + + + + {{ organization }} Templates List + + + + + + +
+ {% for course in courses %} +
+
+ {{ course.metadata.title }} +
+
+

{{ course.courses_name }}

+

Course ID: {{ course.metadata.course_id }}

+

{{ course.metadata.title }}

+

{{ course.metadata.description }}

+
+
+ {% endfor %} +
+ + diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 8b14398fc378..fd2a9d27a4f3 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -252,6 +252,9 @@

${_("Tools")} ${_("Export to Git")} % endif + diff --git a/cms/urls.py b/cms/urls.py index 2e64d4bbeb79..3856e3060acf 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -23,6 +23,7 @@ from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance from openedx.core.djangoapps.password_policy.forms import PasswordPolicyAwareAdminAuthForm from openedx.core import toggles as core_toggles +from cms.djangoapps.contentstore.views.import_export import course_templates django_autodiscover() @@ -359,4 +360,6 @@ urlpatterns += [ re_path('^authoring-api/ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), re_path('^authoring-api/schema/', SpectacularAPIView.as_view(), name='schema'), + re_path(fr'^course_templates/{settings.COURSE_KEY_PATTERN}?$', course_templates, name='course_templates'), + ]