Skip to content
Merged
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/*/*"
"xblocks_contrib/*/*",
"xblock_pdf/*/*/"
],
"scripts": {
"build": "npm run build --workspaces --if-present",
Expand Down
17 changes: 14 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def run(self):
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 @@ -254,12 +254,23 @@ def run(self):
"_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"],
),
},
cmdclass={
"build_py": JSBuildPy,
"develop": JSDevelop,
"sdist": JSSdist,
},
}
)
14 changes: 7 additions & 7 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 = test_settings
django_find_project = false
addopts = --cov xblocks_contrib --cov-report term-missing --cov-report xml
addopts = --cov xblocks_contrib --cov xblock_pdf --cov-report term-missing --cov-report xml
norecursedirs = .* docs requirements site-packages

[testenv]
Expand All @@ -33,12 +33,12 @@ allowlist_externals =
commands =
pip install -e .
mkdir -p var
pytest {posargs} xblocks_contrib
pytest {posargs} xblocks_contrib xblock_pdf

[testenv:docs]
setenv =
PYTHONPATH = {toxinidir}
# Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by shpinx.
# Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by sphinx.
SPHINXOPTS = -W
allowlist_externals =
make
Expand Down Expand Up @@ -67,8 +67,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 web_fragments.fragment import Fragment
from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String
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. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed there are no JS tests. I realize there are no JS tests in the source block, either. And this repo doesn't have any JS tests yet for you to follow the pattern of, but it will shortly once we merge #63 .

Could I ask you to swing back any time in the 1-6 month window and add a couple trivial JS tests to the PDFBlock? The JS is very minimal, so all we'd need is a smoke-test-level thing to ensure that the block doesn't fully break as we do routine upgrades. I wrote up an issue for it here: #170 ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kdmccormick Yep, can do!

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