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..839329e 100644
--- a/pdf/static/js/pdf_edit.js
+++ b/pdf/static/js/pdf_edit.js
@@ -1,187 +1,400 @@
/* 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() {
+ var fileInputEl = $fileInput[0];
+
+ $uploadZone.on('click', function(e) {
+ if (e.target !== fileInputEl) {
+ fileInputEl.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 assetUrl = asset.url;
+ var isSelected = selectedPdfUrl && (
+ selectedPdfUrl === assetUrl ||
+ selectedPdfUrl === asset.external_url ||
+ selectedPdfUrl.indexOf(assetUrl) !== -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(assetUrl, 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');
+
+ // Always update the name field when selecting a different file
+ var $nameInput = $element.find('#pdf_edit_display_name');
+ // 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.url;
+ $urlInput.val(selectedPdfUrl);
+
+ // Update name for newly uploaded file
+ var $nameInput = $element.find('#pdf_edit_display_name');
+ 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..0d2f5ec 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 _
%>
-