From 06422affceb4c63e698f7d972c9622464797801b Mon Sep 17 00:00:00 2001 From: Jiayi Date: Wed, 4 Feb 2026 16:55:54 +0800 Subject: [PATCH 1/4] code upload --- .gitignore | 1 + pdf/pdf.py | 99 +++++- pdf/static/js/pdf_edit.js | 564 +++++++++++++++++++++---------- pdf/templates/html/pdf_edit.html | 139 +++----- 4 files changed, 527 insertions(+), 276 deletions(-) diff --git a/.gitignore b/.gitignore index 0f95ee5..3d0cfc6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc node_modules/ /.idea +.DS_Store diff --git a/pdf/pdf.py b/pdf/pdf.py index ea7da68..f19e4e2 100644 --- a/pdf/pdf.py +++ b/pdf/pdf.py @@ -2,17 +2,20 @@ import pkg_resources from django.template import Context, Template +from django.conf import settings from xblock.core import XBlock from xblock.fields import Scope, String, Boolean from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin -from xblock.scorable import ScorableXBlockMixin, Score from .utils import _, DummyTranslationService import pkg_resources from mako.template import Template -from xmodule.modulestore.django import modulestore +from xmodule.contentstore.content import StaticContent +from xmodule.contentstore.django import contentstore +from pymongo import DESCENDING +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers loader = ResourceLoader(__name__) @@ -147,6 +150,8 @@ def studio_view(self, context=None): The secondary view of the XBlock, shown to teachers when editing the XBlock. """ + max_file_size = getattr(settings, 'MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB', 10) + context = { 'display_name': self.display_name, 'name_help': _("This name appears in the horizontal navigation at the top of the page."), @@ -154,17 +159,17 @@ def studio_view(self, context=None): 'allow_download': self.allow_download, 'source_text': self.source_text, 'course_id': self.course_id, - 'source_url': self.source_url + 'source_url': self.source_url, + 'max_file_size_in_mbs': max_file_size, } html = get_html('templates/html/pdf_edit.html', context) - #html = loader.render_django_template( - # 'templates/html/pdf_edit.html', - # context=context, - # i18n_service=self.i18n_service, - #) frag = Fragment(html) frag.add_javascript(self.load_resource("static/js/pdf_edit.js")) - frag.initialize_js('pdfXBlockInitEdit') + frag.initialize_js('pdfXBlockInitEdit', { + 'course_id': unicode(self.course_id), + 'current_url': self.url, + 'max_file_size_in_mbs': max_file_size, + }) return frag @XBlock.json_handler @@ -186,14 +191,84 @@ def save_pdf(self, data, suffix=''): """ self.display_name = data['display_name'] self.url = data['url'] - self.allow_download = True if data['allow_download'] == "True" else False # Str to Bool translation - self.source_text = data['source_text'] - self.source_url = data['source_url'] + # Handle both string and boolean values for allow_download + allow_download = data.get('allow_download', True) + if isinstance(allow_download, bool): + self.allow_download = allow_download + else: + self.allow_download = allow_download == "True" or allow_download == "true" or allow_download is True + # Keep source_text and source_url for backwards compatibility + self.source_text = data.get('source_text', '') + self.source_url = data.get('source_url', '') return { 'result': 'success', } + @XBlock.json_handler + def get_pdf_assets(self, data, suffix=''): + """ + Fetch all PDF assets from the course. + Returns a list of PDF files. + """ + course_key = self.course_id + + pdf_content_types = [ + 'application/pdf', + 'application/x-pdf', + ] + + filter_params = { + 'contentType': {'$in': pdf_content_types} + } + + # Get search term if provided + search = data.get('search', '') + if search: + filter_params['displayname'] = {'$regex': search, '$options': 'i'} + + try: + # Fetch PDF assets from contentstore + assets, total_count = contentstore().get_all_content_for_course( + course_key, + start=0, + maxresults=100, # Limit to 100 PDFs + sort=[('uploadDate', DESCENDING)], + filter_params=filter_params + ) + + # Convert to unified format + pdf_assets = [] + for asset in assets: + asset_url = StaticContent.serialize_asset_key_with_slash(asset['asset_key']) + external_url = configuration_helpers.get_value( + 'SITE_LMS_DOMAIN_NAME', + settings.LMS_BASE + ) + asset_url + + pdf_assets.append({ + 'id': unicode(asset['asset_key']), + 'display_name': asset['displayname'], + 'content_type': asset['contentType'], + 'url': asset_url, + 'external_url': external_url, + 'date_added': asset['uploadDate'].isoformat() if hasattr(asset['uploadDate'], 'isoformat') else str(asset['uploadDate']), + 'size': asset.get('length', 0), + }) + + return { + 'result': 'success', + 'assets': pdf_assets, + 'total_count': total_count, + } + except Exception as e: + return { + 'result': 'error', + 'message': str(e), + 'assets': [], + 'total_count': 0, + } + @property def i18n_service(self): """ Obtains translation service """ diff --git a/pdf/static/js/pdf_edit.js b/pdf/static/js/pdf_edit.js index 4384c6e..48bc74c 100644 --- a/pdf/static/js/pdf_edit.js +++ b/pdf/static/js/pdf_edit.js @@ -1,187 +1,409 @@ /* Javascript for pdfXBlock. */ -function pdfXBlockInitEdit(runtime, element) { - $(element).find('.action-cancel').bind('click', function () { - runtime.notify('cancel', {}); - $('.pdf_placeholder').hide(); - }); - - $(element).find('.action-save').bind('click', function () { - var data = { - 'display_name': $('#pdf_edit_display_name').val(), - url: $('#pdf_edit_url').val().trim(), - 'allow_download': $('#pdf_edit_allow_download').val(), - 'source_text': $('#pdf_edit_source_text').val(), - 'source_url': $('#pdf_edit_source_url').val() - }; - - if (data.url && data.url[0] !== '/' && data.url[0] !== 'h') { - data.url = window.location.href.split(':')[0] + '://' + data.url; - // data.url = '/' + data.url.split('/')[1] +function pdfXBlockInitEdit(runtime, element, options) { + var courseId = options.course_id; + var currentUrl = options.current_url; + var maxFileSizeInMbs = options.max_file_size_in_mbs || 10; + var maxFileSizeBytes = maxFileSizeInMbs * 1024 * 1024; + + var selectedPdfUrl = currentUrl; + var pdfAssets = []; + var uploadingFiles = []; + + var $element = $(element); + var $uploadZone = $element.find('#pdf-upload-zone'); + var $fileInput = $element.find('#pdf-file-input'); + var $browseButton = $element.find('#pdf-browse-button'); + var $filesSection = $element.find('#pdf-files-section'); + var $filesGrid = $element.find('#pdf-files-grid'); + var $urlInput = $element.find('#pdf_edit_url'); + + init(); + + function init() { + bindEvents(); + fetchPdfAssets(); + } + + function bindEvents() { + // Browse button click + $browseButton.on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + $fileInput.click(); + }); + + // Upload zone click (excluding browse button) + $uploadZone.on('click', function(e) { + if (!$(e.target).is('#pdf-browse-button')) { + $fileInput.click(); + } + }); + + // File input change + $fileInput.on('change', function(e) { + handleFileSelect(e.target.files); + // Reset input so same file can be selected again + this.value = ''; + }); + + // Drag and drop + $uploadZone.on('dragenter dragover', function(e) { + e.preventDefault(); + e.stopPropagation(); + $uploadZone.addClass('drag-over'); + }); + + $uploadZone.on('dragleave', function(e) { + e.preventDefault(); + e.stopPropagation(); + $uploadZone.removeClass('drag-over'); + }); + + $uploadZone.on('drop', function(e) { + e.preventDefault(); + e.stopPropagation(); + $uploadZone.removeClass('drag-over'); + var files = e.originalEvent.dataTransfer.files; + handleFileSelect(files); + }); + + // Cancel button + $element.find('.action-cancel').on('click', function(e) { + e.preventDefault(); + runtime.notify('cancel', {}); + }); + + // Save button + $element.find('.action-save').on('click', function(e) { + e.preventDefault(); + savePdf(); + }); + } + + function fetchPdfAssets() { + var handlerUrl = runtime.handlerUrl(element, 'get_pdf_assets'); + + $.ajax({ + url: handlerUrl, + type: 'POST', + data: JSON.stringify({}), + contentType: 'application/json', + success: function(response) { + if (response.result === 'success') { + pdfAssets = response.assets || []; + renderPdfGrid(); + } + }, + error: function() { + console.error('Failed to fetch PDF assets'); + } + }); + } + + function renderPdfGrid() { + $filesGrid.empty(); + + if (pdfAssets.length === 0 && uploadingFiles.length === 0) { + $filesSection.hide(); + return; } - - runtime.notify('save', { state: 'start' }); - - var handlerUrl = runtime.handlerUrl(element, 'save_pdf'); - $.post(handlerUrl, JSON.stringify(data)).done(function (response) { - if (response.result === 'success') { - runtime.notify('save', { state: 'end' }); + + $filesSection.show(); + + // Render uploading files first + uploadingFiles.forEach(function(file) { + var $item = createUploadingPdfItem(file); + $filesGrid.append($item); + }); + + // Render existing PDF assets + pdfAssets.forEach(function(asset) { + var $item = createPdfItem(asset); + $filesGrid.append($item); + }); + + // Highlight currently selected PDF + updateSelection(); + } + + function createPdfItem(asset) { + var isSelected = selectedPdfUrl && ( + selectedPdfUrl === asset.url || + selectedPdfUrl === asset.external_url || + selectedPdfUrl.indexOf(asset.url) !== -1 + ); + + var $item = $('
'); + + var $preview = $('
'); + $preview.append('pdf'); + $preview.append('
'); + $item.append($preview); + + var $info = $('
'); + $info.append('
' + escapeHtml(asset.display_name) + '
'); + $info.append('
' + formatFileSize(asset.size) + '
'); + $item.append($info); + + // Click to select + $item.on('click', function() { + selectPdf(asset.external_url || asset.url, asset.display_name); + }); + + return $item; + } + + function createUploadingPdfItem(file) { + var $item = $('
'); + + var $preview = $('
'); + $preview.append('pdf'); + $preview.append('
'); + + // Progress bar + var $progress = $('
'); + $progress.append(''); + $preview.append($progress); + + $item.append($preview); + + var $info = $('
'); + $info.append('
' + escapeHtml(file.name) + '
'); + $info.append('
' + formatFileSize(file.size) + '
'); + $item.append($info); + + return $item; + } + + function selectPdf(url, displayName) { + selectedPdfUrl = url; + $urlInput.val(url); + + // Update visual selection + $filesGrid.find('.pdf-file-item').removeClass('selected'); + $filesGrid.find('.pdf-file-item[data-url="' + escapeHtml(url) + '"]').addClass('selected'); + + // Optionally update the name field if it's empty or is a placeholder + var $nameInput = $element.find('#pdf_edit_display_name'); + if (!$nameInput.val() || $nameInput.val() === 'PDF') { + // Extract filename without extension for display name + var nameWithoutExt = displayName.replace(/\.pdf$/i, ''); + $nameInput.val(nameWithoutExt); + } + } + + function updateSelection() { + $filesGrid.find('.pdf-file-item').removeClass('selected'); + if (selectedPdfUrl) { + // Try to find matching item + $filesGrid.find('.pdf-file-item').each(function() { + var itemUrl = $(this).data('url'); + if (itemUrl && ( + selectedPdfUrl === itemUrl || + selectedPdfUrl.indexOf(itemUrl) !== -1 || + itemUrl.indexOf(selectedPdfUrl) !== -1 + )) { + $(this).addClass('selected'); + } + }); + } + } + + function handleFileSelect(files) { + if (!files || files.length === 0) return; + + var validFiles = []; + + for (var i = 0; i < files.length; i++) { + var file = files[i]; + + // Validate file type + if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { + showNotification('error', gettext('Only PDF files are allowed.')); + continue; } - else { - runtime.notify('error', { msg: response.message }); + + // Validate file size + if (file.size > maxFileSizeBytes) { + showNotification('error', gettext('File size exceeds the maximum limit of {size}MB.').replace('{size}', maxFileSizeInMbs)); + continue; } + + validFiles.push(file); + } + + // Upload valid files + validFiles.forEach(function(file) { + uploadFile(file); }); - - $('.pdf_placeholder').hide(); - }); - - - $(element).find("#browse-button").bind('click', function (){ - $('#file-input').click(); - }); - - var fileInput = $('#file-input')[0]; - fileInput.onchange = function () { - var file = fileInput.files[0]; + } + + function uploadFile(file) { + var uploadingFile = { + name: file.name, + size: file.size, + progress: 0 + }; + + uploadingFiles.push(uploadingFile); + renderPdfGrid(); + var formData = new FormData(); formData.append('file', file); - var request = new XMLHttpRequest(); - var course_key = element[0].dataset['usageId'].split('+', 3).join('+').replace('block', 'course'); - var upload_url = '/assets/' + course_key + '/'; + + var courseKey = courseId; + var uploadUrl = '/assets/' + courseKey + '/'; var csrftoken = $.cookie('csrftoken'); - var alertField = $('.alert-field'); - request.open("POST", upload_url); - request.setRequestHeader("X-CSRFToken", csrftoken); - request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - request.setRequestHeader('Accept', 'application/json'); - alertField.removeClass('hidden'); - alertField.addClass('alert-info'); - alertField.removeClass('alert-success'); - alertField.removeClass('alert-error'); - $('#alert-info-text').removeClass('hidden'); - $('#alert-success-text').addClass('hidden'); - $('#alert-error-text').addClass('hidden'); - request.onerror = function () { - alertField.removeClass('alert-info'); - alertField.removeClass('alert-success'); - alertField.addClass('alert-error'); - $('#alert-info-text').addClass('hidden'); - $('#alert-success-text').addClass('hidden'); - $('#alert-error-text').removeClass('hidden'); - }; - request.onload = function () { - var response = JSON.parse(request.responseText); - if (request.status === 200) { - $('#pdf_edit_url').val(response.asset.external_url); - alertField.removeClass('alert-info'); - alertField.addClass('alert-success'); - alertField.removeClass('alert-error'); - $('#alert-info-text').addClass('hidden'); - $('#alert-success-text').removeClass('hidden'); - $('#alert-error-text').addClass('hidden'); + + var xhr = new XMLHttpRequest(); + + xhr.upload.addEventListener('progress', function(e) { + if (e.lengthComputable) { + var progress = Math.round((e.loaded / e.total) * 100); + uploadingFile.progress = progress; + updateUploadProgress(file.name, progress); + } + }); + + xhr.addEventListener('load', function() { + // Remove from uploading list + var index = uploadingFiles.indexOf(uploadingFile); + if (index > -1) { + uploadingFiles.splice(index, 1); + } + + if (xhr.status === 200) { + try { + var response = JSON.parse(xhr.responseText); + if (response.asset) { + // Add to assets list + var newAsset = { + id: response.asset.id, + display_name: response.asset.display_name || file.name, + content_type: 'application/pdf', + url: response.asset.url, + external_url: response.asset.external_url, + size: file.size + }; + + // Add to beginning of list + pdfAssets.unshift(newAsset); + + // Auto-select the newly uploaded file + selectedPdfUrl = response.asset.external_url || response.asset.url; + $urlInput.val(selectedPdfUrl); + + // Update name if empty + var $nameInput = $element.find('#pdf_edit_display_name'); + if (!$nameInput.val() || $nameInput.val() === 'PDF') { + var nameWithoutExt = file.name.replace(/\.pdf$/i, ''); + $nameInput.val(nameWithoutExt); + } + + showNotification('success', gettext('Upload completed')); + } + } catch (e) { + showNotification('error', gettext('Upload failed')); + } } else { - alertField.removeClass('alert-info'); - alertField.removeClass('alert-success'); - alertField.addClass('alert-error'); - $('#alert-info-text').addClass('hidden'); - $('#alert-success-text').addClass('hidden'); - $('#alert-error-text').removeClass('hidden'); + showNotification('error', gettext('Upload failed')); } + + renderPdfGrid(); + }); + + xhr.addEventListener('error', function() { + var index = uploadingFiles.indexOf(uploadingFile); + if (index > -1) { + uploadingFiles.splice(index, 1); + } + showNotification('error', gettext('Upload failed')); + renderPdfGrid(); + }); + + xhr.open('POST', uploadUrl); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(formData); + } + + function updateUploadProgress(filename, progress) { + var $item = $filesGrid.find('.pdf-file-item[data-filename="' + escapeHtml(filename) + '"]'); + $item.find('.pdf-item-progress').val(progress); + } + + function savePdf() { + var displayName = $element.find('#pdf_edit_display_name').val(); + var url = $urlInput.val().trim(); + var allowDownload = $element.find('#pdf_edit_allow_download').is(':checked'); + + if (!url) { + showNotification('error', gettext('Please select or upload a PDF file.')); + return; + } + + var data = { + 'display_name': displayName || 'PDF', + 'url': url, + 'allow_download': allowDownload ? 'True' : 'False', + 'source_text': '', + 'source_url': '' }; - request.send(formData); - }; - - var dropZone = $('.drop-zone')[0]; - $('.pdf_placeholder').show(); - - dropZone.ondragenter = function() { - dropZone.style.background = "rgba(0, 240, 240, 0.2)"; - }; - dropZone.ondragover = function() { - dropZone.style.background = "rgba(0, 240, 240, 0.2)"; - return false; - }; - dropZone.ondragleave = function() { - dropZone.style.background = "white"; - }; - dropZone.ondrop = function(ev) { - var oFile = ev.dataTransfer.files[0]; - var request = new XMLHttpRequest(); - var reader = new FileReader(); - var course_key = element[0].dataset['usageId'].split('+', 3).join('+').replace('block', 'course'); - var upload_url = '/assets/' + course_key + '/'; - var csrftoken = $.cookie('csrftoken'); - var alertField = $('.alert-field'); - reader.readAsDataURL(oFile, 'base64'); - var formData = new FormData(); - formData.append("file", oFile); - reader.onload = function(e) { - request.open("POST", upload_url); - request.setRequestHeader("X-CSRFToken", csrftoken); - request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - request.setRequestHeader('Accept', 'application/json'); - alertField.removeClass('hidden'); - alertField.addClass('alert-info'); - alertField.removeClass('alert-success'); - alertField.removeClass('alert-error'); - $('#alert-info-text').removeClass('hidden'); - $('#alert-success-text').addClass('hidden'); - $('#alert-error-text').addClass('hidden'); - request.onerror = function () { - alertField.removeClass('alert-info'); - alertField.removeClass('alert-success'); - alertField.addClass('alert-error'); - $('#alert-info-text').addClass('hidden'); - $('#alert-success-text').addClass('hidden'); - $('#alert-error-text').removeClass('hidden'); - }; - request.onload = function () { - var response = JSON.parse(request.responseText); - if (request.status === 200) { - $('#pdf_edit_url').val(response.asset.external_url); - alertField.removeClass('alert-info'); - alertField.addClass('alert-success'); - alertField.removeClass('alert-error'); - $('#alert-info-text').addClass('hidden'); - $('#alert-success-text').removeClass('hidden'); - $('#alert-error-text').addClass('hidden'); + + runtime.notify('save', { state: 'start' }); + + var handlerUrl = runtime.handlerUrl(element, 'save_pdf'); + $.post(handlerUrl, JSON.stringify(data)).done(function(response) { + if (response.result === 'success') { + runtime.notify('save', { state: 'end' }); } else { - alertField.removeClass('alert-info'); - alertField.removeClass('alert-success'); - alertField.addClass('alert-error'); - $('#alert-info-text').addClass('hidden'); - $('#alert-success-text').addClass('hidden'); - $('#alert-error-text').removeClass('hidden'); + runtime.notify('error', { msg: response.message || 'Save failed' }); } - }; - request.send(formData); - }; - dropZone.style.background = "white"; - return false; - }; - - $('#alert-field-close')[0].onclick = function () { - $('.alert-field').addClass('hidden'); - } - - if (LearningTribes && LearningTribes.QuestionMark) { - $wrappers = $('.wrapper-comp-settings .question-mark-wrapper') - $wrappers.each(function(i, wrapper){ - new LearningTribes.QuestionMark(wrapper) - }) - } - - if (LearningTribes && LearningTribes.Switcher) { - var switcher = $('.wrapper-comp-settings').find('.switcher-wrapper')[0]; - var $select = $('#pdf_edit_allow_download') - new LearningTribes.Switcher(switcher, $select.find('option[selected]')[0].value.toLowerCase(), - function(checked){ - var checkedStr = checked.toString(); - checkedStr = checkedStr[0].toUpperCase() + checkedStr.slice(1); - $('#pdf_edit_allow_download').find('option[value='+checkedStr+']') - .removeAttr('selected') - .attr('selected', true) + }).fail(function() { + runtime.notify('error', { msg: 'Save failed' }); + }); + } + + function showNotification(type, message) { + // Try to use dsAlert + if (typeof dsAlert !== 'undefined') { + dsAlert({ + title: message, + type: type, + timeout: type === 'error' ? 5000 : 3000, + confirmText: null, + cancelText: null + }); + } else { + // Fallback alert + console.log('[' + type.toUpperCase() + '] ' + message); + if (type === 'error') { + alert(message); } - ) + } + } + + function formatFileSize(bytes) { + if (!bytes || bytes === 0) return '0 B'; + + var units = ['B', 'KB', 'MB', 'GB']; + var k = 1024; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + var size = (bytes / Math.pow(k, i)).toFixed(i === 0 ? 0 : 1); + + return size + ' ' + units[i]; + } + + function escapeHtml(str) { + if (!str) return ''; + var div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + // if gettext not available + if (typeof gettext === 'undefined') { + window.gettext = function(str) { return str; }; } - } diff --git a/pdf/templates/html/pdf_edit.html b/pdf/templates/html/pdf_edit.html index a4e9eff..4cadf09 100644 --- a/pdf/templates/html/pdf_edit.html +++ b/pdf/templates/html/pdf_edit.html @@ -2,110 +2,63 @@ from django.utils.translation import ugettext as _ %> -
-
-
-