-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: course package import and export #2286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
pateljannat
merged 15 commits into
frappe:develop
from
pateljannat:course-package-import
Apr 3, 2026
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
aaa866e
feat: export course zip
pateljannat a4a0a76
feat: import course zip
pateljannat 03e5bae
chore: resolved conflicts
pateljannat cd85c5c
fix: import package as private
pateljannat e1e2c08
fix: import modal ui
pateljannat 564d10f
fix: cleanup of import functionality
pateljannat c0df21c
fix: save export zip as private
pateljannat 55f01dc
test: course package export and import
pateljannat 6ebaf0e
test: corrected zip path
pateljannat 6338a59
fix: misc issues
pateljannat ab96e35
fix: cleanup and more tests
pateljannat f9f17ef
fix: roles before user is saved
pateljannat 3ece2fc
fix: assessments replace logic
pateljannat 7c18698
ci: codecov rules
pateljannat 7fe8d6c
fix: asset export path
pateljannat File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,9 @@ | ||
| coverage: | ||
| status: | ||
| project: | ||
| default: | ||
| target: auto | ||
| threshold: 1% | ||
|
|
||
| ignore: | ||
| - "**/test_helper.py" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| <template> | ||
| <Dialog | ||
| v-model="show" | ||
| :options="{ | ||
| title: __('Import Course from ZIP'), | ||
| }" | ||
| > | ||
| <template #body-content> | ||
| <div class="text-p-base"> | ||
| <div | ||
| v-if="!zip" | ||
| @dragover.prevent | ||
| @drop.prevent="(e) => uploadFile(e)" | ||
| class="h-[120px] flex flex-col items-center justify-center bg-surface-gray-1 border border-dashed border-outline-gray-3 rounded-md" | ||
| > | ||
| <div v-if="!uploading" class="w-4/5 text-center"> | ||
| <UploadCloud | ||
| class="size-6 stroke-1.5 text-ink-gray-6 mx-auto mb-2.5" | ||
| /> | ||
| <input | ||
| ref="fileInput" | ||
| type="file" | ||
| class="hidden" | ||
| accept=".zip" | ||
| @change="(e) => uploadFile(e)" | ||
| /> | ||
| <div class="leading-5 text-ink-gray-9"> | ||
| {{ __('Drag and drop a ZIP file, or upload from your') }} | ||
| <span | ||
| @click="openFileSelector" | ||
| class="cursor-pointer font-semibold hover:underline" | ||
| > | ||
| {{ __('Device') }} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| <div | ||
| v-else-if="uploading" | ||
| class="w-fit bg-surface-white border rounded-md p-2 my-4" | ||
| > | ||
| <div class="space-y-2"> | ||
| <div class="font-medium"> | ||
| {{ uploadingFile.name }} | ||
| </div> | ||
| <div class="text-ink-gray-6"> | ||
| {{ convertToMB(uploaded) }} of {{ convertToMB(total) }} | ||
| </div> | ||
| </div> | ||
| <div class="w-full bg-surface-gray-1 h-1 rounded-full mt-3"> | ||
| <div | ||
| class="bg-surface-gray-7 h-1 rounded-full transition-all duration-500 ease-in-out" | ||
| :style="`width: ${uploadProgress}%`" | ||
| ></div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div | ||
| v-else-if="zip" | ||
| class="h-[120px] flex items-center justify-center bg-surface-gray-1 border border-dashed border-outline-gray-3 rounded-md" | ||
| > | ||
| <div | ||
| class="w-fit bg-surface-white border rounded-md p-2 flex items-center justify-between items-center space-x-4" | ||
| > | ||
| <div class="space-y-2"> | ||
| <div class="font-medium leading-5 text-ink-gray-9"> | ||
| {{ zip.file_name || zip.name }} | ||
| </div> | ||
| <div v-if="zip.file_size" class="text-ink-gray-6"> | ||
| {{ convertToMB(zip.file_size) }} | ||
| </div> | ||
| </div> | ||
| <Trash2 | ||
| class="size-4 stroke-1.5 text-ink-red-3 cursor-pointer" | ||
| @click="deleteFile" | ||
| /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </template> | ||
| <template #actions> | ||
| <div class="flex justify-end"> | ||
| <Button variant="solid" @click="importZip"> | ||
| {{ __('Import') }} | ||
| </Button> | ||
| </div> | ||
| </template> | ||
| </Dialog> | ||
| </template> | ||
| <script setup lang="ts"> | ||
| import { Button, call, Dialog, FileUploadHandler, toast } from 'frappe-ui' | ||
| import { computed, ref } from 'vue' | ||
| import { Trash2, UploadCloud } from 'lucide-vue-next' | ||
| import { useRouter } from 'vue-router' | ||
|
|
||
| const fileInput = ref<HTMLInputElement | null>(null) | ||
| const show = defineModel<boolean>({ required: true, default: false }) | ||
| const zip = ref<any | null>(null) | ||
| const uploaded = ref(0) | ||
| const total = ref(0) | ||
| const uploading = ref(false) | ||
| const uploadingFile = ref<any | null>(null) | ||
| const router = useRouter() | ||
|
|
||
| const openFileSelector = () => { | ||
| fileInput.value?.click() | ||
| } | ||
|
|
||
| const uploadProgress = computed(() => { | ||
| if (total.value === 0) return 0 | ||
| return Math.floor((uploaded.value / total.value) * 100) | ||
| }) | ||
|
|
||
| const extractFile = (e: Event): File | null => { | ||
| const inputFiles = (e.target as HTMLInputElement)?.files | ||
| const dt = (e as DragEvent).dataTransfer?.files | ||
|
|
||
| return inputFiles?.[0] || dt?.[0] || null | ||
| } | ||
|
|
||
| const validateFile = (file: File) => { | ||
| const extension = file.name.split('.').pop()?.toLowerCase() | ||
| if (extension !== 'zip') { | ||
| toast.error('Please upload a valid ZIP file.') | ||
| console.error('Please upload a valid ZIP file.') | ||
| } | ||
| return extension | ||
| } | ||
|
|
||
| const uploadFile = (e: Event) => { | ||
| const file = extractFile(e) | ||
| if (!file) return | ||
|
|
||
| let fileType = validateFile(file) | ||
| if (fileType !== 'zip') return | ||
|
|
||
| uploadingFile.value = file | ||
| const uploader = new FileUploadHandler() | ||
|
|
||
| uploader.on('start', () => { | ||
| uploading.value = true | ||
| }) | ||
|
|
||
| uploader.on('progress', (data: { uploaded: number; total: number }) => { | ||
| uploaded.value = data.uploaded | ||
| total.value = data.total | ||
| }) | ||
|
|
||
| uploader.on('error', (error: any) => { | ||
| uploading.value = false | ||
| toast.error(__('File upload failed. Please try again.')) | ||
| console.error('File upload error:', error) | ||
| }) | ||
|
|
||
| uploader.on('finish', () => { | ||
| uploading.value = false | ||
| }) | ||
| uploader | ||
| .upload(file, { | ||
| private: 1, | ||
| }) | ||
| .then((data: any) => { | ||
| zip.value = data | ||
| }) | ||
| .catch((error: any) => { | ||
| console.error('File upload error:', error) | ||
| toast.error(__('File upload failed. Please try again.')) | ||
| uploading.value = false | ||
| uploadingFile.value = null | ||
| uploaded.value = 0 | ||
| total.value = 0 | ||
| }) | ||
| } | ||
|
|
||
| const importZip = () => { | ||
| if (!zip.value) return | ||
| call('lms.lms.api.import_course_from_zip', { | ||
| zip_file_path: zip.value.file_url, | ||
| }) | ||
| .then((data: any) => { | ||
| toast.success('Course imported successfully!') | ||
| show.value = false | ||
| deleteFile() | ||
| router.push({ | ||
| name: 'CourseDetail', | ||
| params: { courseName: data }, | ||
| }) | ||
| }) | ||
| .catch((error: any) => { | ||
| toast.error('Error importing course: ' + error.message) | ||
| console.error('Error importing course:', error) | ||
| }) | ||
| } | ||
|
|
||
| const deleteFile = () => { | ||
| zip.value = null | ||
pateljannat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| const convertToMB = (bytes: number) => { | ||
| return (bytes / 1024 / 1024).toFixed(2) + ' MB' | ||
| } | ||
| </script> | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.