diff --git a/.coveragerc b/.coveragerc index 1f4f2d7..e65876a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,5 @@ [run] branch = True -source = xblocks_contrib +source = + xblocks_contrib + xblock_pdf diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9d3401b..c27b7f9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,14 @@ Change Log Unreleased ********** +0.11.0 - 2026-01-26 +********************************************** + +Added +===== + +* Implemented PDF Block, extracted from third party plugin. + 0.6.0 – 2025-08-13 ********************************************** diff --git a/README.rst b/README.rst index a68fe68..9886800 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,7 @@ These are the XBlocks being moved here, and each of their statuses: * ``word_cloud`` -- Ready to Use * ``annotatable`` -- Ready to Use * ``lti`` -- In Development +* ``pdf`` -- Done * ``html`` -- Ready to Use * ``discussion`` -- Placeholder * ``problem`` -- In Development diff --git a/package.json b/package.json index 6dc1769..3413197 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "private": true, "workspaces": [ - "xblocks_contrib/*/static" + "xblocks_contrib/*/static", + "xblock_pdf/*/static" ], "scripts": { "test": "npm run test --workspaces", diff --git a/setup.py b/setup.py index dfae3d0..0c4038b 100644 --- a/setup.py +++ b/setup.py @@ -175,7 +175,7 @@ def package_data(pkg, sub_roots): author_email="oscm@openedx.org", url="https://github.com/openedx/xblocks-contrib", packages=find_packages( - include=["xblocks_contrib", "xblocks_contrib.*"], + include=["xblocks_contrib", "xblocks_contrib.*", "xblock_pdf"], exclude=["*tests"], ), include_package_data=True, @@ -204,7 +204,18 @@ def package_data(pkg, sub_roots): "_problem_extracted = xblocks_contrib:ProblemBlock", "_video_extracted = xblocks_contrib:VideoBlock", "_word_cloud_extracted = xblocks_contrib:WordCloudBlock", + # 'Done' XBlocks-- ones that are ready for general use today, + # and have been migrated fully from edx-platform or their original + # repository. + "pdf = xblock_pdf:PDFBlock", ] }, - package_data=package_data("xblocks_contrib", ["static", "public", "templates"]), + package_data={ + **package_data( + "xblocks_contrib", ["static", "public", "templates"], + ), + **package_data( + "xblock_pdf", ["static", "templates"], + ), + }, ) diff --git a/tox.ini b/tox.ini index fe9fb23..7752772 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ match-dir = (?!migrations) [pytest] DJANGO_SETTINGS_MODULE = xblocks_contrib.test_settings django_find_project = false -addopts = --cov xblocks_contrib --cov-report term-missing --cov-report xml +addopts = --cov xblocks_contrib --cov xblocks_pdf --cov-report term-missing --cov-report xml norecursedirs = .* docs requirements site-packages [testenv] @@ -62,8 +62,8 @@ allowlist_externals = deps = -r{toxinidir}/requirements/quality.txt commands = - pylint xblocks_contrib - pycodestyle xblocks_contrib - pydocstyle xblocks_contrib - isort --check-only --diff xblocks_contrib + pylint xblocks_contrib xblock_pdf + pycodestyle xblocks_contrib xblock_pdf + pydocstyle xblocks_contrib xblock_pdf + isort --check-only --diff xblocks_contrib xblock_pdf make selfcheck diff --git a/xblock_pdf/__init__.py b/xblock_pdf/__init__.py new file mode 100644 index 0000000..ae2b9e9 --- /dev/null +++ b/xblock_pdf/__init__.py @@ -0,0 +1,3 @@ +"""Init for PDFBlock.""" + +from .pdf import PDFBlock diff --git a/xblock_pdf/pdf.py b/xblock_pdf/pdf.py new file mode 100644 index 0000000..7118392 --- /dev/null +++ b/xblock_pdf/pdf.py @@ -0,0 +1,135 @@ +"""pdfXBlock main Python class.""" + +from django.utils.translation import gettext_noop as _ +from xblock.core import XBlock +from xblock.fields import Boolean, Scope, String +from xblock.fragment import Fragment +from xblock.utils.resources import ResourceLoader + +from .utils import bool_from_str, is_all_download_disabled + +resource_loader = ResourceLoader(__name__) + + +@XBlock.needs('i18n') +class PDFBlock(XBlock): + """PDF XBlock. Allows authors to embed PDFs in their courses.""" + + icon_class = "other" + + display_name = String( + display_name=_("Display Name"), + default=_("PDF"), + scope=Scope.settings, + help=_("This name appears in the horizontal navigation at the top of the page.") + ) + + url = String( + display_name=_("PDF URL"), + default=_("https://tutorial.math.lamar.edu/pdf/Trig_Cheat_Sheet.pdf"), + scope=Scope.content, + help=_("The URL for your PDF.") + ) + + allow_download = Boolean( + display_name=_("PDF Download Allowed"), + default=True, + scope=Scope.content, + help=_("Display a download button for this PDF.") + ) + + source_text = String( + display_name=_("Source document button text"), + default="", + scope=Scope.content, + help=_( + "Add a download link for the source file of your PDF. " + "Use it for example to provide the PowerPoint file used to create this PDF." + ) + ) + + source_url = String( + display_name=_("Source document URL"), + default="", + scope=Scope.content, + help=_( + "Add a download link for the source file of your PDF. " + "Use it for example to provide the PowerPoint file used to create this PDF." + ) + ) + + def student_view(self, context=None): + """Primary view of the XBlock, shown to students when viewing courses.""" + context = { + 'display_name': self.display_name, + 'url': self.url, + 'allow_download': self.allow_download, + 'disable_all_download': is_all_download_disabled(), + 'source_text': self.source_text, + 'source_url': self.source_url, + } + html = resource_loader.render_django_template( + 'templates/html/pdf_view.html', + context=context, + i18n_service=self.runtime.service(self, "i18n"), + ) + + event_type = 'edx.pdf.loaded' + event_data = { + 'url': self.url, + 'source_url': self.source_url, + } + self.runtime.publish(self, event_type, event_data) + frag = Fragment(html) + frag.add_javascript(resource_loader.load_unicode("static/js/pdf_view.js")) + frag.initialize_js('pdfXBlockInitView') + return frag + + def studio_view(self, context=None): + """ + Secondary view of the XBlock. + + Shown to teachers when editing the XBlock. + """ + context = { + 'display_name': self.display_name, + 'url': self.url, + 'allow_download': self.allow_download, + 'disable_all_download': is_all_download_disabled(), + 'source_text': self.source_text, + 'source_url': self.source_url + } + html = resource_loader.render_django_template( + 'templates/html/pdf_edit.html', + context=context, + i18n_service=self.runtime.service(self, "i18n"), + ) + frag = Fragment(html) + frag.add_javascript(resource_loader.load_unicode("static/js/pdf_edit.js")) + frag.initialize_js('pdfXBlockInitEdit') + return frag + + @XBlock.json_handler + def on_download(self, data, suffix=''): # pylint: disable=unused-argument + """Download file event handler.""" + event_type = 'edx.pdf.downloaded' + event_data = { + 'url': self.url, + 'source_url': self.source_url, + } + self.runtime.publish(self, event_type, event_data) + + @XBlock.json_handler + def save_pdf(self, data, suffix=''): # pylint: disable=unused-argument + """Save handler.""" + self.display_name = data['display_name'] + self.url = data['url'] + + if not is_all_download_disabled(): + self.allow_download = bool_from_str(data['allow_download']) + self.source_text = data['source_text'] + self.source_url = data['source_url'] + + return { + 'result': 'success', + } diff --git a/xblock_pdf/static/js/pdf_edit.js b/xblock_pdf/static/js/pdf_edit.js new file mode 100644 index 0000000..73a537c --- /dev/null +++ b/xblock_pdf/static/js/pdf_edit.js @@ -0,0 +1,28 @@ +/* Javascript for pdfXBlock. */ +function pdfXBlockInitEdit(runtime, element) { + $(element).find('.action-cancel').bind('click', function () { + runtime.notify('cancel', {}); + }); + + $(element).find('.action-save').bind('click', function () { + var data = { + 'display_name': $('#pdf_edit_display_name').val(), + 'url': $('#pdf_edit_url').val(), + 'allow_download': $('#pdf_edit_allow_download').val() || '', + 'source_text': $('#pdf_edit_source_text').val() || '', + 'source_url': $('#pdf_edit_source_url').val() || '' + }; + + 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 { + runtime.notify('error', { msg: response.message }); + } + }); + }); +} diff --git a/xblock_pdf/static/js/pdf_view.js b/xblock_pdf/static/js/pdf_view.js new file mode 100644 index 0000000..4a6a873 --- /dev/null +++ b/xblock_pdf/static/js/pdf_view.js @@ -0,0 +1,17 @@ +/* Javascript for pdfXBlock. */ +function pdfXBlockInitView(runtime, element) { + /* Weird behaviour : + * In the LMS, element is the DOM container. + * In the CMS, element is the jQuery object associated* + * So here I make sure element is the jQuery object */ + if (element.innerHTML) { + element = $(element); + } + + $(function () { + element.find('.pdf-download-button').on('click', function () { + var handlerUrl = runtime.handlerUrl(element, 'on_download'); + $.post(handlerUrl, '{}'); + }); + }); +} diff --git a/xblock_pdf/templates/html/pdf_edit.html b/xblock_pdf/templates/html/pdf_edit.html new file mode 100644 index 0000000..bf6f4f8 --- /dev/null +++ b/xblock_pdf/templates/html/pdf_edit.html @@ -0,0 +1,58 @@ +{% load i18n %} +
diff --git a/xblock_pdf/templates/html/pdf_view.html b/xblock_pdf/templates/html/pdf_view.html new file mode 100644 index 0000000..65dc13e --- /dev/null +++ b/xblock_pdf/templates/html/pdf_view.html @@ -0,0 +1,22 @@ +{% load i18n %} +