|
66 | 66 | import json |
67 | 67 | import logging |
68 | 68 |
|
| 69 | +import edx_api_doc_tools as apidocs |
69 | 70 | from django.conf import settings |
70 | 71 | from django.contrib.auth import authenticate, get_user_model, login |
71 | 72 | from django.contrib.auth.models import Group |
|
78 | 79 | from django.views.decorators.csrf import csrf_exempt |
79 | 80 | from django.views.generic.base import TemplateResponseMixin, View |
80 | 81 | from drf_yasg.utils import swagger_auto_schema |
81 | | -from pylti1p3.contrib.django import DjangoCacheDataStorage, DjangoDbToolConf, DjangoMessageLaunch, DjangoOIDCLogin |
82 | | -from pylti1p3.exception import LtiException, OIDCException |
83 | | - |
84 | | -import edx_api_doc_tools as apidocs |
85 | 82 | from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2 |
86 | 83 | from organizations.api import ensure_organization |
87 | 84 | from organizations.exceptions import InvalidOrganizationException |
88 | 85 | from organizations.models import Organization |
| 86 | +from pylti1p3.contrib.django import DjangoCacheDataStorage, DjangoDbToolConf, DjangoMessageLaunch, DjangoOIDCLogin |
| 87 | +from pylti1p3.exception import LtiException, OIDCException |
89 | 88 | from rest_framework import status |
90 | 89 | from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError |
91 | 90 | from rest_framework.generics import GenericAPIView |
92 | 91 | from rest_framework.response import Response |
93 | 92 | from rest_framework.views import APIView |
94 | 93 | from rest_framework.viewsets import GenericViewSet |
95 | 94 |
|
| 95 | +import openedx.core.djangoapps.site_configuration.helpers as configuration_helpers |
96 | 96 | from cms.djangoapps.contentstore.views.course import ( |
97 | 97 | get_allowed_organizations_for_libraries, |
98 | | - user_can_create_organizations, |
| 98 | + user_can_create_organizations |
99 | 99 | ) |
100 | 100 | from openedx.core.djangoapps.content_libraries import api, permissions |
| 101 | +from openedx.core.djangoapps.content_libraries.api.libraries import get_backup_task_status |
101 | 102 | from openedx.core.djangoapps.content_libraries.rest_api.serializers import ( |
| 103 | + ContentLibraryAddPermissionByEmailSerializer, |
102 | 104 | ContentLibraryBlockImportTaskCreateSerializer, |
103 | 105 | ContentLibraryBlockImportTaskSerializer, |
104 | 106 | ContentLibraryFilterSerializer, |
105 | 107 | ContentLibraryMetadataSerializer, |
106 | 108 | ContentLibraryPermissionLevelSerializer, |
107 | 109 | ContentLibraryPermissionSerializer, |
108 | 110 | ContentLibraryUpdateSerializer, |
| 111 | + LibraryBackupResponseSerializer, |
| 112 | + LibraryBackupTaskStatusSerializer, |
109 | 113 | LibraryXBlockCreationSerializer, |
110 | 114 | LibraryXBlockMetadataSerializer, |
111 | 115 | LibraryXBlockTypeSerializer, |
112 | | - ContentLibraryAddPermissionByEmailSerializer, |
113 | | - PublishableItemSerializer, |
| 116 | + PublishableItemSerializer |
114 | 117 | ) |
115 | | -import openedx.core.djangoapps.site_configuration.helpers as configuration_helpers |
116 | | -from openedx.core.lib.api.view_utils import view_auth_classes |
| 118 | +from openedx.core.djangoapps.content_libraries.tasks import backup_library |
117 | 119 | from openedx.core.djangoapps.safe_sessions.middleware import mark_user_change_as_expected |
118 | 120 | from openedx.core.djangoapps.xblock import api as xblock_api |
| 121 | +from openedx.core.lib.api.view_utils import view_auth_classes |
119 | 122 |
|
120 | | -from .utils import convert_exceptions |
121 | 123 | from ..models import ContentLibrary, LtiGradedResource, LtiProfile |
122 | | - |
| 124 | +from .utils import convert_exceptions |
123 | 125 |
|
124 | 126 | User = get_user_model() |
125 | 127 | log = logging.getLogger(__name__) |
@@ -685,6 +687,109 @@ def retrieve(self, request, lib_key_str, pk=None): |
685 | 687 | return Response(ContentLibraryBlockImportTaskSerializer(import_task).data) |
686 | 688 |
|
687 | 689 |
|
| 690 | +# Library Backup Views |
| 691 | +# ==================== |
| 692 | + |
| 693 | +@method_decorator(non_atomic_requests, name="dispatch") |
| 694 | +@view_auth_classes() |
| 695 | +class LibraryBackupView(APIView): |
| 696 | + """ |
| 697 | + **Use Case** |
| 698 | + * Start an asynchronous task to back up the content of a library to a .zip file |
| 699 | + * Get a status on an asynchronous export task |
| 700 | +
|
| 701 | + **Example Requests** |
| 702 | + POST /api/libraries/v2/{library_id}/backup/ |
| 703 | + GET /api/libraries/v2/{library_id}/backup/?task_id={task_id} |
| 704 | +
|
| 705 | + **POST Response Values** |
| 706 | +
|
| 707 | + If the import task is started successfully, an HTTP 200 "OK" response is |
| 708 | + returned. |
| 709 | +
|
| 710 | + The HTTP 200 response has the following values: |
| 711 | +
|
| 712 | + * task_id: UUID of the created task, usable for checking status |
| 713 | +
|
| 714 | + **Example POST Response** |
| 715 | +
|
| 716 | + { |
| 717 | + "task_id": "7069b95b-ccea-4214-b6db-e00f27065bf7" |
| 718 | + } |
| 719 | +
|
| 720 | + **GET Parameters** |
| 721 | +
|
| 722 | + A GET request must include the following parameters: |
| 723 | +
|
| 724 | + * task_id: (required) The UUID of the task to check. |
| 725 | +
|
| 726 | + **GET Response Values** |
| 727 | +
|
| 728 | + If the import task is found successfully by the UUID provided, an HTTP |
| 729 | + 200 "OK" response is returned. |
| 730 | +
|
| 731 | + The HTTP 200 response has the following values: |
| 732 | +
|
| 733 | + * state: String description of the state of the task. |
| 734 | + Possible states: "Pending", "Exporting", "Succeeded", "Failed". |
| 735 | + * url: (may be null) If the task is complete, a URL to download the .zip file |
| 736 | +
|
| 737 | + **Example GET Response** |
| 738 | + { |
| 739 | + "state": "Succeeded", |
| 740 | + "url": "/media/user_tasks/2025/10/03/lib-wgu-csprob-2025-10-03-153633.zip" |
| 741 | + } |
| 742 | +
|
| 743 | + """ |
| 744 | + |
| 745 | + @apidocs.schema( |
| 746 | + body=None, |
| 747 | + responses={200: LibraryBackupResponseSerializer} |
| 748 | + ) |
| 749 | + @convert_exceptions |
| 750 | + def post(self, request, lib_key_str): |
| 751 | + """ |
| 752 | + Start backup task for the specified library. |
| 753 | + """ |
| 754 | + library_key = LibraryLocatorV2.from_string(lib_key_str) |
| 755 | + # Using CAN_EDIT_THIS_CONTENT_LIBRARY permission for now. This should eventually become its own permission |
| 756 | + api.require_permission_for_library_key(library_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) |
| 757 | + |
| 758 | + async_result = backup_library.delay(request.user.id, str(library_key)) |
| 759 | + result = {'task_id': async_result.task_id} |
| 760 | + |
| 761 | + return Response(LibraryBackupResponseSerializer(result).data) |
| 762 | + |
| 763 | + @apidocs.schema( |
| 764 | + parameters=[ |
| 765 | + apidocs.query_parameter( |
| 766 | + 'task_id', |
| 767 | + str, |
| 768 | + description="The ID of the backup task to retrieve." |
| 769 | + ), |
| 770 | + ], |
| 771 | + responses={200: LibraryBackupTaskStatusSerializer} |
| 772 | + ) |
| 773 | + @convert_exceptions |
| 774 | + def get(self, request, lib_key_str): |
| 775 | + """ |
| 776 | + Get the status of the specified backup task for the specified library. |
| 777 | + """ |
| 778 | + library_key = LibraryLocatorV2.from_string(lib_key_str) |
| 779 | + # Using CAN_EDIT_THIS_CONTENT_LIBRARY permission for now. This should eventually become its own permission |
| 780 | + api.require_permission_for_library_key(library_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY) |
| 781 | + |
| 782 | + task_id = request.query_params.get('task_id', None) |
| 783 | + if not task_id: |
| 784 | + raise ValidationError(detail={'task_id': _('This field is required.')}) |
| 785 | + result = get_backup_task_status(request.user.id, task_id) |
| 786 | + |
| 787 | + if not result: |
| 788 | + raise NotFound(detail="No backup found for this library.") |
| 789 | + |
| 790 | + return Response(LibraryBackupTaskStatusSerializer(result).data) |
| 791 | + |
| 792 | + |
688 | 793 | # LTI 1.3 Views |
689 | 794 | # ============= |
690 | 795 |
|
|
0 commit comments