Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[run]
branch = True
source = xblocks_contrib
source =
xblocks_contrib
xblock_pdf
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
**********************************************

Expand Down
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"private": true,
"workspaces": [
"xblocks_contrib/*/static"
"xblocks_contrib/*/static",
"xblock_pdf/*/static"
],
"scripts": {
"test": "npm run test --workspaces",
Expand Down
15 changes: 13 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"],
),
},
)
10 changes: 5 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions xblock_pdf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Init for PDFBlock."""

from .pdf import PDFBlock
135 changes: 135 additions & 0 deletions xblock_pdf/pdf.py
Original file line number Diff line number Diff line change
@@ -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',
}
28 changes: 28 additions & 0 deletions xblock_pdf/static/js/pdf_edit.js
Original file line number Diff line number Diff line change
@@ -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 });
}
});
});
}
17 changes: 17 additions & 0 deletions xblock_pdf/static/js/pdf_view.js
Original file line number Diff line number Diff line change
@@ -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, '{}');
});
});
}
58 changes: 58 additions & 0 deletions xblock_pdf/templates/html/pdf_edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{% load i18n %}
<div class="wrapper-comp-settings editor-with-buttons is-active" id="settings-tab">
<ul class="list-input settings-list">

<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="pdf_edit_display_name">{% trans "Name" %}</label>
<input class="input setting-input" id="pdf_edit_display_name" value="{{ display_name }}" type="text">
</div>
<span class="tip setting-help">{% trans "This name appears in the horizontal navigation at the top of the page." %}</span>
</li>

<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="pdf_edit_url">{% trans "PDF URL" %}</label>
<input class="input setting-input" id="pdf_edit_url" value="{{ url }}" type="text">
</div>
<span class="tip setting-help">{% trans "The URL for your PDF." %}</span>
</li>

{% if not disable_all_download %}
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="pdf_edit_allow_download">{% trans "PDF Download Allowed" %}</label>
<select class="input setting-input" id="pdf_edit_allow_download">
<option value="True" {% if allow_download %}selected{% endif %}>{% trans "True" %}</option>
<option value="False" {% if not allow_download %}selected{% endif %}>{% trans "False" %}</option>
</select>
</div>
<span class="tip setting-help">{% trans "Display a download link to this PDF for convenience. Please note that even if this is disabled, the embedded PDF viewer may still display its own download button." %}</span>
</li>

<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="pdf_edit_source_text">{% trans "Source document button text" %}</label>
<input class="input setting-input" id="pdf_edit_source_text" value="{{ source_text }}" type="text" placeholder="{% trans 'Default : Download the source document' %}">
</div>
<div class="wrapper-comp-setting">
<label class="label setting-label" for="pdf_edit_source_url">{% trans "Source document URL" %}</label>
<input class="input setting-input" id="pdf_edit_source_url" value="{{ source_url }}" type="text">
</div>
<span class="tip setting-help">{% trans "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." %}</span>
</li>
{% endif %}

</ul>

<div class="xblock-actions">
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-save">{% trans "Save" %}</a>
</li>
<li class="action-item">
<a href="#" class="button action-cancel">{% trans "Cancel" %}</a>
</li>
</ul>
</div>
</div>
22 changes: 22 additions & 0 deletions xblock_pdf/templates/html/pdf_view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% load i18n %}
<div class="pdf_block">
<h2>{{ display_name }}</h2>
<iframe src="{{ url }}" type="application/pdf" width="100%" height="600">

</iframe>
{% if not disable_all_download %}
<ul>
{% if allow_download %}
<li class="pdf-download-button">
<a href="{{ url }}" target="_blank" rel="noopener" download>{% trans "Download the PDF" %}</a>
(Right click or long press on the download link and select the "Save as" option to download. Clicking on the link might open it in a tab.)
</li>
{% endif %}
{% if source_url != "" %}
<li class="pdf-download-button">
<a href="{{ source_url }}" download>{% if source_text == "" %}{% trans "Download the source document" %}{% else %}{{ source_text }}{% endif %}</a>
</li>
{% endif %}
</ul>
{% endif %}
</div>
Loading