diff --git a/.isort.cfg b/.isort.cfg index 40051662fb4d21148bb4f2c84e389f51a78f37e2..eb80b51e45c418feda298120198438578b6cd6ff 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -4,4 +4,4 @@ line_length = 88 multi_line_output = 3 default_sectiont = "THIRDPARTY" include_trailing_comma = true -known_third_party = arrow,django,environ,faker,ics,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,snapshottest,taggit,wagtail,wagtailmetadata +known_third_party = arrow,django,environ,faker,ics,modelcluster,nbconvert,pirates,pytest,pytz,requests,sentry_sdk,snapshottest,taggit,traitlets,wagtail,wagtailmetadata diff --git a/README.md b/README.md index cffb51fbaeaad5f95656828834bd8a29b17a989b..98be5d3389d87536bbfa355f58e2598e61f0ca63 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,18 @@ Kalendář se stáhne při uložení modelu obsahujícího `CalendarMixin`. Appka přidává management command `update_callendars`, který stahuje a updatuje kalendáře. Je třeba ho pravidelně volat na pozadí (přes CRON). +### Jupyter notebooky + +Appka Uniweb umí vložit do stránky Jupyter notebook a zobrazit jeho výstup. + +Pokud některé buňky nechceš generovat do výstupní stránky, nastav u nich tag "exclude". + +Pokud chceš generovat jen výstup dané buňky, použij tag "output" + +Pozor: u plotly grafů je nutno zadat tagem "output" výstup buňky s inicializací knihovny, tedy něco kde je "import plotly" apod. +Pokud celou takovou buňku vynecháš tagem "exclude", žádné grafy se nezobrazí. + + ## Deployment ### Konfigurace diff --git a/uniweb/jupyter-templates/basic.tpl b/uniweb/jupyter-templates/basic.tpl new file mode 100644 index 0000000000000000000000000000000000000000..58fc189d5fed4b17e144a16ae7d65553449291af --- /dev/null +++ b/uniweb/jupyter-templates/basic.tpl @@ -0,0 +1,269 @@ +{%- extends 'display_priority.tpl' -%} +{% from 'celltags.tpl' import celltags %} + +{% block codecell %} +<div class="cell border-box-sizing code_cell rendered{{ celltags(cell) }}"> +{{ super() }} +</div> +{%- endblock codecell %} + +{% block input_group -%} +<div class="input"> +{{ super() }} +</div> +{% endblock input_group %} + +{% block output_group %} +<div class="output_wrapper"> + <div class="output"> + {{ super() }} + </div> + <div class="output_divider" style="margin-bottom: 5px;"> + </div> +</div> +{% endblock output_group %} + +{% block in_prompt -%} +<div class="prompt input_prompt"> + {%- if cell.execution_count is defined -%} + In [{{ cell.execution_count|replace(None, " ") }}]: + {%- else -%} + In [ ]: + {%- endif -%} +</div> +{%- endblock in_prompt %} + +{% block empty_in_prompt -%} +<div class="prompt input_prompt"> +</div> +{%- endblock empty_in_prompt %} + +{# + output_prompt doesn't do anything in HTML, + because there is a prompt div in each output area (see output block) +#} +{% block output_prompt %} +{% endblock output_prompt %} + +{% block input %} +<div class="inner_cell"> + <div class="input_area"> +{{ cell.source }} + </div> +</div> +{%- endblock input %} + +{% block output_area_prompt %} +{%- if output.output_type == 'execute_result' -%} + <div class="prompt output_prompt"> + {%- if cell.execution_count is defined -%} + Out[{{ cell.execution_count|replace(None, " ") }}]: + {%- else -%} + Out[ ]: + {%- endif -%} +{%- else -%} + <div class="prompt"> +{%- endif -%} + </div> +{% endblock output_area_prompt %} + +{% block output %} +<div class="output_area"> +{% if resources.global_content_filter.include_output_prompt %} + {{ self.output_area_prompt() }} +{% endif %} +{{ super() }} +</div> +{% endblock output %} + +{% block markdowncell scoped %} +<div class="cell border-box-sizing text_cell rendered{{ celltags(cell) }}"> +{%- if resources.global_content_filter.include_input_prompt-%} + {{ self.empty_in_prompt() }} +{%- endif -%} +<div class="inner_cell"> +<div class="text_cell_render border-box-sizing rendered_html"> +{{ cell.source | markdown2html | strip_files_prefix }} +</div> +</div> +</div> +{%- endblock markdowncell %} + +{% block unknowncell scoped %} +unknown type {{ cell.type }} +{% endblock unknowncell %} + +{% block execute_result -%} +{%- set extra_class="output_execute_result" -%} +{% block data_priority scoped %} +{{ super() }} +{% endblock data_priority %} +{%- set extra_class="" -%} +{%- endblock execute_result %} + +{% block stream_stdout -%} +<div class="output_subarea output_stream output_stdout output_text"> +<pre> +{{- output.text | ansi2html -}} +</pre> +</div> +{%- endblock stream_stdout %} + +{% block stream_stderr -%} +<div class="output_subarea output_stream output_stderr output_text"> +<pre> +{{- output.text | ansi2html -}} +</pre> +</div> +{%- endblock stream_stderr %} + +{% block data_svg scoped -%} +<div class="output_svg output_subarea {{ extra_class }}"> +{%- if output.svg_filename %} +<img src="{{ output.svg_filename | posix_path }}"> +{%- else %} +{{ output.data['image/svg+xml'] }} +{%- endif %} +</div> +{%- endblock data_svg %} + +{% block data_html scoped -%} +<div class="output_html rendered_html output_subarea {{ extra_class }}"> +{{ output.data['text/html'] }} +</div> +{%- endblock data_html %} + +{% block data_markdown scoped -%} +<div class="output_markdown rendered_html output_subarea {{ extra_class }}"> +{{ output.data['text/markdown'] | markdown2html }} +</div> +{%- endblock data_markdown %} + +{% block data_png scoped %} +<div class="output_png output_subarea {{ extra_class }}"> +{%- if 'image/png' in output.metadata.get('filenames', {}) %} +<img src="{{ output.metadata.filenames['image/png'] | posix_path }}" +{%- else %} +<img src="data:image/png;base64,{{ output.data['image/png'] }}" +{%- endif %} +{%- set width=output | get_metadata('width', 'image/png') -%} +{%- if width is not none %} +width={{ width }} +{%- endif %} +{%- set height=output | get_metadata('height', 'image/png') -%} +{%- if height is not none %} +height={{ height }} +{%- endif %} +{%- if output | get_metadata('unconfined', 'image/png') %} +class="unconfined" +{%- endif %} +{%- set alttext=(output | get_metadata('alt', 'image/png')) or (cell | get_metadata('alt')) -%} +{%- if alttext is not none %} +alt="{{ alttext }}" +{%- endif %} +> +</div> +{%- endblock data_png %} + +{% block data_jpg scoped %} +<div class="output_jpeg output_subarea {{ extra_class }}"> +{%- if 'image/jpeg' in output.metadata.get('filenames', {}) %} +<img src="{{ output.metadata.filenames['image/jpeg'] | posix_path }}" +{%- else %} +<img src="data:image/jpeg;base64,{{ output.data['image/jpeg'] }}" +{%- endif %} +{%- set width=output | get_metadata('width', 'image/jpeg') -%} +{%- if width is not none %} +width={{ width }} +{%- endif %} +{%- set height=output | get_metadata('height', 'image/jpeg') -%} +{%- if height is not none %} +height={{ height }} +{%- endif %} +{%- if output | get_metadata('unconfined', 'image/jpeg') %} +class="unconfined" +{%- endif %} +{%- set alttext=(output | get_metadata('alt', 'image/jpeg')) or (cell | get_metadata('alt')) -%} +{%- if alttext is not none %} +alt="{{ alttext }}" +{%- endif %} +> +</div> +{%- endblock data_jpg %} + +{% block data_latex scoped %} +<div class="output_latex output_subarea {{ extra_class }}"> +{{ output.data['text/latex'] }} +</div> +{%- endblock data_latex %} + +{% block error -%} +<div class="output_subarea output_text output_error"> +<pre> +{{- super() -}} +</pre> +</div> +{%- endblock error %} + +{%- block traceback_line %} +{{ line | ansi2html }} +{%- endblock traceback_line %} + +{%- block data_text scoped %} +<div class="output_text output_subarea {{ extra_class }}"> +<pre> +{{- output.data['text/plain'] | ansi2html -}} +</pre> +</div> +{%- endblock -%} + +{%- block data_javascript scoped %} +{% set div_id = uuid4() %} +<div id="{{ div_id }}"></div> +<div class="output_subarea output_javascript {{ extra_class }}"> +<script type="text/javascript"> +var element = $('#{{ div_id }}'); +{{ output.data['application/javascript'] }} +</script> +</div> +{%- endblock -%} + +{%- block data_widget_state scoped %} +{% set div_id = uuid4() %} +{% set datatype_list = output.data | filter_data_type %} +{% set datatype = datatype_list[0]%} +<div id="{{ div_id }}"></div> +<div class="output_subarea output_widget_state {{ extra_class }}"> +<script type="text/javascript"> +var element = $('#{{ div_id }}'); +</script> +<script type="{{ datatype }}"> +{{ output.data[datatype] | json_dumps }} +</script> +</div> +{%- endblock data_widget_state -%} + +{%- block data_widget_view scoped %} +{% set div_id = uuid4() %} +{% set datatype_list = output.data | filter_data_type %} +{% set datatype = datatype_list[0]%} +<div id="{{ div_id }}"></div> +<div class="output_subarea output_widget_view {{ extra_class }}"> +<script type="text/javascript"> +var element = $('#{{ div_id }}'); +</script> +<script type="{{ datatype }}"> +{{ output.data[datatype] | json_dumps }} +</script> +</div> +{%- endblock data_widget_view -%} + +{%- block footer %} +{% set mimetype = 'application/vnd.jupyter.widget-state+json'%} +{% if mimetype in nb.metadata.get("widgets",{})%} +<script type="{{ mimetype }}"> +{{ nb.metadata.widgets[mimetype] | json_dumps }} +</script> +{% endif %} +{{ super() }} +{%- endblock footer-%} diff --git a/uniweb/jupyter-templates/celltags.tpl b/uniweb/jupyter-templates/celltags.tpl new file mode 100644 index 0000000000000000000000000000000000000000..4722b17ef177d0da6f68206d6ae7fb52029072e1 --- /dev/null +++ b/uniweb/jupyter-templates/celltags.tpl @@ -0,0 +1,7 @@ +{%- macro celltags(cell) -%} + {% if cell.metadata.tags | length > 0 -%} + {% for tag in cell.metadata.tags -%} + {{ ' celltag_' ~ tag -}} + {%- endfor -%} + {%- endif %} +{%- endmacro %} diff --git a/uniweb/jupyter-templates/mathjax.tpl b/uniweb/jupyter-templates/mathjax.tpl new file mode 100644 index 0000000000000000000000000000000000000000..43e49c6693c63fa281792b7ca055f99778877e45 --- /dev/null +++ b/uniweb/jupyter-templates/mathjax.tpl @@ -0,0 +1,23 @@ +{%- macro mathjax(url='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/latest.js?config=TeX-AMS_HTML') -%} + <!-- Load mathjax --> + <script src="{{url}}"></script> + <!-- MathJax configuration --> + <script type="text/x-mathjax-config"> + MathJax.Hub.Config({ + tex2jax: { + inlineMath: [ ['$','$'], ["\\(","\\)"] ], + displayMath: [ ['$$','$$'], ["\\[","\\]"] ], + processEscapes: true, + processEnvironments: true + }, + // Center justify equations in code and markdown cells. Elsewhere + // we use CSS to left justify single line equations in code cells. + displayAlign: 'center', + "HTML-CSS": { + styles: {'.MathJax_Display': {"margin": 0}}, + linebreaks: { automatic: true } + } + }); + </script> + <!-- End of mathjax configuration --> +{%- endmacro %} diff --git a/uniweb/jupyter-templates/my.tpl b/uniweb/jupyter-templates/my.tpl new file mode 100644 index 0000000000000000000000000000000000000000..e3f693e4ac3cd394f3bc96278da815e619dd1d77 --- /dev/null +++ b/uniweb/jupyter-templates/my.tpl @@ -0,0 +1,96 @@ +{%- extends 'basic.tpl' -%} +{% from 'mathjax.tpl' import mathjax %} + + +{%- block header -%} +{%- block html_head -%} + +<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.10/require.min.js"></script> +<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script> + +{% block ipywidgets %} +{%- if "widgets" in nb.metadata -%} +<script> +(function() { + function addWidgetsRenderer() { + var mimeElement = document.querySelector('script[type="application/vnd.jupyter.widget-view+json"]'); + var scriptElement = document.createElement('script'); + var widgetRendererSrc = '{{ resources.ipywidgets_base_url }}@jupyter-widgets/html-manager@*/dist/embed-amd.js'; + var widgetState; + + // Fallback for older version: + try { + widgetState = mimeElement && JSON.parse(mimeElement.innerHTML); + + if (widgetState && (widgetState.version_major < 2 || !widgetState.version_major)) { + widgetRendererSrc = '{{ resources.ipywidgets_base_url }}jupyter-js-widgets@*/dist/embed.js'; + } + } catch(e) {} + + scriptElement.src = widgetRendererSrc; + document.body.appendChild(scriptElement); + } + + document.addEventListener('DOMContentLoaded', addWidgetsRenderer); +}()); +</script> +{%- endif -%} +{% endblock ipywidgets %} + +{% for css in resources.inlining.css -%} + <style type="text/css"> + {{ css }} + </style> +{% endfor %} + +<style type="text/css"> +/* Overrides of notebook CSS for static HTML export */ +body { + overflow: visible; + padding: 8px; +} + +div#notebook { + overflow: visible; + border-top: none; +} + +{%- if resources.global_content_filter.no_prompt-%} +div#notebook-container{ + padding: 6ex 12ex 8ex 12ex; +} +{%- endif -%} + +@media print { + div.cell { + display: block; + page-break-inside: avoid; + } + div.output_wrapper { + display: block; + page-break-inside: avoid; + } + div.output { + display: block; + page-break-inside: avoid; + } +} +</style> + +<!-- Loading mathjax macro --> +{{ mathjax() }} +{%- endblock html_head -%} + +{%- endblock header -%} + +{% block body %} + <div tabindex="-1" id="notebook" class="border-box-sizing"> + <div class="container" id="notebook-container"> +{{ super() }} + </div> + </div> +{%- endblock body %} + +{% block footer %} +{{ super() }} +{% endblock footer %} diff --git a/uniweb/templates/uniweb/base.html b/uniweb/templates/uniweb/base.html index a9a7ed11ec664fa04992b3df7b3fa2ff1cde54d9..e7960a3d35ccfbdc4274049566e9c4b827a27e27 100644 --- a/uniweb/templates/uniweb/base.html +++ b/uniweb/templates/uniweb/base.html @@ -1,4 +1,4 @@ -{% load uniweb_filters static wagtailcore_tags wagtailimages_tags wagtailmetadata_tags %} +{% load static wagtailcore_tags wagtailimages_tags wagtailmetadata_tags %} <!doctype html> <html lang="cs"> <head> diff --git a/uniweb/templates/uniweb/snippet_sections.html b/uniweb/templates/uniweb/snippet_sections.html index 3f535a06b4f6dae1aac45375f89cb8a860a85bd1..63fb8dae14570f060530b54ebce899166ec83b95 100644 --- a/uniweb/templates/uniweb/snippet_sections.html +++ b/uniweb/templates/uniweb/snippet_sections.html @@ -1,4 +1,4 @@ -{% load wagtailcore_tags wagtailimages_tags %} +{% load uniweb_filters wagtailcore_tags wagtailimages_tags %} <section class="mb-8 lg:mb-16"> {% for block in page.content %} @@ -44,7 +44,7 @@ {% if block.block_type == "jupyter" %} <div class="content-block my-4 clearfix{% if forloop.first %} mt-8 lg:mt-12{% endif %}"> - {{ block.value|jupyterize }} + {{ block|jupyterize }} </div> {% endif %} diff --git a/uniweb/templatetags/uniweb_filters.py b/uniweb/templatetags/uniweb_filters.py index 63241b3e68644da490b0e09ff3fea0889fb057b6..7350ba5dd5b5998e614fc6b196a303cad0ee8022 100644 --- a/uniweb/templatetags/uniweb_filters.py +++ b/uniweb/templatetags/uniweb_filters.py @@ -1,10 +1,38 @@ # definice vlastnich tagu pro templates +import os + +import nbconvert from django import template +from django.utils.safestring import mark_safe +from traitlets.config import Config register = template.Library() -@register.filter() +@register.filter def jupyterize(value): - return value.value + " HIHI" + """ Exportuje jupyterovsky notebook do raw HTML s vyuzitim vlastnich templates. + Vynechava tagy + """ + + # lokalni soubor + filename = os.path.join(value.value.file.storage.location, value.value.file.name) + + # konvertuj do HTML s pouzitim vlastni template + c = Config() + + c.TagRemovePreprocessor.enabled = True # Nutne + c.TagRemovePreprocessor.remove_cell_tags = ["exclude"] + c.TagRemovePreprocessor.remove_input_tags = ["output"] + c.preprocessors = ["TagRemovePreprocessor"] + + nb_body, _ = nbconvert.TemplateExporter( + config=c, template_file="uniweb/jupyter-templates/my.tpl" + ).from_filename(filename) + + # HACK: fucking ugly. Ale netusim kde se to tam bere, asi nekde v hloubi notebookovskych templates + if nb_body.startswith("None"): + nb_body = nb_body[4:] + + return mark_safe(nb_body)