Skip to content
Snippets Groups Projects
Commit 7b83e726 authored by jan.bednarik's avatar jan.bednarik
Browse files

elections2021: Program point import and detail mostly done

parent 433f7e3d
Branches
No related tags found
2 merge requests!220elections2021: Program point import and detail mostly done,!219elections2021: Program point import and detail mostly done
Pipeline #3511 passed
......@@ -4,4 +4,4 @@ line_length = 88
multi_line_output = 3
default_section = "THIRDPARTY"
include_trailing_comma = true
known_third_party = arrow,captcha,django,environ,faker,ics,markdown,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,snapshottest,taggit,wagtail,wagtailmetadata
known_third_party = arrow,bleach,bs4,captcha,django,environ,faker,ics,markdown,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,snapshottest,taggit,wagtail,wagtailmetadata
......@@ -146,8 +146,3 @@ TARGET_CHOICES = (
(HOUSING, "bydlení"),
(EDUCATION, "vzdělávání"),
)
YEARS = "years"
MONTHS = "months"
TIME_HORIZON_CHOICES = ((YEARS, "roky"), (MONTHS, "měsíce"))
import zipfile
from django import forms
from django.utils.text import slugify
from wagtail.admin.forms import WagtailAdminPageForm
from .parser import (
parse_program_html,
prepare_benefits,
prepare_faq,
prepare_horizon,
prepare_point,
)
class ProgramPointPageForm(WagtailAdminPageForm):
import_file = forms.FileField(label="soubor s programem", required=False)
def clean(self):
cleaned_data = super().clean()
# extract data from ZIP file with HTML export
import_file = cleaned_data["import_file"]
if import_file:
try:
with zipfile.ZipFile(import_file) as archive:
name = archive.namelist()[0]
data = parse_program_html(archive.read(name))
except zipfile.BadZipFile:
self.add_error("import_file", "Vadný ZIP soubor. Nelze rozbalit.")
cleaned_data["_import_data"] = data
else:
cleaned_data["_import_data"] = None
return cleaned_data
def save(self, commit=True):
page = super().save(commit=False)
if self.cleaned_data["_import_data"]:
point = prepare_point(self.cleaned_data["_import_data"]["sekce"])
benefits = prepare_benefits(self.cleaned_data["_import_data"]["benefity"])
page.title = point["nadpis"]
page.slug = slugify(page.title)
page.annotation = point["anotace"]
page.problem = point["problem"]
page.context = point["kontext-problemu"]
page.ideal = point["ideal"]
page.proposal = point["navrhovana-opatreni"]
page.already_done = point["co-jsme-uz-udelali"]
page.sources = point["zdroje"]
text, number, unit = prepare_horizon(point["casovy-horizont"])
page.time_horizon_text = text
page.time_horizon_number = number
page.time_horizon_unit = unit
page.faq = prepare_faq(point["faq"])
if commit:
page.save()
return page
# Generated by Django 3.1.7 on 2021-05-10 22:48
import wagtail.core.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("elections2021", "0005_auto_20210505_0945"),
]
operations = [
migrations.AlterField(
model_name="elections2021programpointpage",
name="already_done",
field=wagtail.core.fields.RichTextField(
blank=True, null=True, verbose_name="co jsme už udělali"
),
),
migrations.AlterField(
model_name="elections2021programpointpage",
name="annotation",
field=wagtail.core.fields.RichTextField(
blank=True, null=True, verbose_name="anotace"
),
),
migrations.AlterField(
model_name="elections2021programpointpage",
name="context",
field=wagtail.core.fields.RichTextField(
blank=True, null=True, verbose_name="kontext problému"
),
),
migrations.AlterField(
model_name="elections2021programpointpage",
name="ideal",
field=wagtail.core.fields.RichTextField(
blank=True, null=True, verbose_name="ideál"
),
),
migrations.AlterField(
model_name="elections2021programpointpage",
name="problem",
field=wagtail.core.fields.RichTextField(
blank=True, null=True, verbose_name="problém"
),
),
migrations.AlterField(
model_name="elections2021programpointpage",
name="proposal",
field=wagtail.core.fields.RichTextField(
blank=True, null=True, verbose_name="navrhovaná opatření"
),
),
migrations.AlterField(
model_name="elections2021programpointpage",
name="sources",
field=wagtail.core.fields.RichTextField(
blank=True, null=True, verbose_name="zdroje"
),
),
migrations.AlterField(
model_name="elections2021programpointpage",
name="time_horizon_unit",
field=models.CharField(
blank=True,
max_length=20,
null=True,
verbose_name="časový horizont jednotka",
),
),
]
......@@ -41,11 +41,10 @@ from .constants import (
STANDARD_FEATURES,
STYLE_CHOICES,
STYLE_CSS,
TIME_HORIZON_CHOICES,
TOP_CANDIDATES_NUM,
WHITE,
YEARS,
)
from .forms import ProgramPointPageForm
NO_SEARCH_IMAGE_USE_PHOTO = (
"Pokud není zadán <strong>Search image</strong>, použije se <strong>hlavní "
......@@ -520,11 +519,19 @@ class QuestionBlock(blocks.StructBlock):
class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
### FIELDS
annotation = RichTextField("anotace", features=RESTRICTED_FEATURES)
problem = RichTextField("problém", features=RESTRICTED_FEATURES)
context = RichTextField("kontext problému", features=RESTRICTED_FEATURES)
ideal = RichTextField("ideál", features=RESTRICTED_FEATURES)
proposal = RichTextField("navrhovaná opatření", features=EXTRA_FEATURES)
annotation = RichTextField(
"anotace", blank=True, null=True, features=RESTRICTED_FEATURES
)
problem = RichTextField(
"problém", blank=True, null=True, features=RESTRICTED_FEATURES
)
context = RichTextField(
"kontext problému", blank=True, null=True, features=RESTRICTED_FEATURES
)
ideal = RichTextField("ideál", blank=True, null=True, features=RESTRICTED_FEATURES)
proposal = RichTextField(
"navrhovaná opatření", blank=True, null=True, features=EXTRA_FEATURES
)
time_horizon_text = RichTextField(
"časový horizont textově", blank=True, null=True, features=STANDARD_FEATURES
)
......@@ -532,13 +539,12 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
"časový horizont číslo", blank=True, null=True
)
time_horizon_unit = models.CharField(
"časový horizont jednotka",
choices=TIME_HORIZON_CHOICES,
default=YEARS,
max_length=6,
"časový horizont jednotka", max_length=20, blank=True, null=True
)
already_done = RichTextField("co jsme už udělali", features=EXTRA_FEATURES)
sources = RichTextField("zdroje", features=STANDARD_FEATURES)
already_done = RichTextField(
"co jsme už udělali", blank=True, null=True, features=EXTRA_FEATURES
)
sources = RichTextField("zdroje", blank=True, null=True, features=STANDARD_FEATURES)
faq = StreamField(
[("question", QuestionBlock())],
......@@ -635,7 +641,7 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
MultiFieldPanel(
[
FieldPanel("time_horizon_number"),
FieldPanel("time_horizon_unit", widget=forms.RadioSelect),
FieldPanel("time_horizon_unit"),
FieldPanel("time_horizon_text"),
],
"časový horizont",
......@@ -658,7 +664,9 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
]
faq_panels = [StreamFieldPanel("faq")]
related_panels = [StreamFieldPanel("related_points")]
weights_panels = [
MultiFieldPanel(
[
......@@ -722,6 +730,19 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
),
]
import_panels = [
MultiFieldPanel(
[
FieldPanel("import_file"),
HelpPanel(
'Soubor z Goodle Docs stáhněte jako "Webová stránka (komprimovaný '
' HTML soubor)" a ten nahrajte do formuláře.'
),
],
"import programového bodu",
),
]
edit_handler = TabbedInterface(
[
ObjectList(content_panels, heading=gettext_lazy("Content")),
......@@ -729,6 +750,7 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
ObjectList(faq_panels, heading="FAQ"),
ObjectList(related_panels, heading="související"),
ObjectList(weights_panels, heading="váhy"),
ObjectList(import_panels, heading="import"),
]
)
......@@ -739,5 +761,7 @@ class Elections2021ProgramPointPage(SubpageMixin, MetadataPageMixin, Page):
### OTHERS
base_form_class = ProgramPointPageForm
class Meta:
verbose_name = "Programový bod"
import json
import re
import sys
import urllib
from collections import defaultdict
import bleach
import bs4
from django.utils.text import slugify
KNOWN_KEYS = [
"nadpis",
"anotace",
"problem",
"kontext-problemu",
"ideal",
"navrhovana-opatreni",
"casovy-horizont",
"co-jsme-uz-udelali",
"faq",
"souvisejici-body",
"zdroje",
]
def parse_program_html(fp):
# Načteme celý dokument.
html = bs4.BeautifulSoup(fp, "html5lib")
# Vyházíme odkazy na komentáře.
for cmnt in html.select('*[id^="cmnt"]'):
cmnt.parent.extract()
# Bod má svůj pracovní název
nazev_bodu = html.select_one("h1, h2, h3").text.strip()
# Bod má své pojmenované sekce.
bod = {}
SEKCE = [
"Nadpis",
"Anotace",
"Problém",
"Kontext problému",
"Ideál",
"Navrhovaná opatření",
"Časový horizont",
"FAQ",
"Související body",
"Zdroje",
"Co jsme už udělali",
]
# Tabulka benefitů má své pojmenované cílové skupiny.
benefity = {}
# Chceme očesat HTML na výstupu a nechat jenom tyto atributy.
ATRIBUTY = set(["id", "href"])
# Dokument má právě dvě tabulky. První je s bodem a druhá s jeho benefity.
bod_html, bene_html = html.select("table")
# Z CSS je potřeba vyvodit, které třídy odpovídají tučnému textu a
# které superskriptu. Protože Google.
strong = []
sup = []
for style in html.select("style"):
strong = re.findall(r"(\.c[0-9]+)\{[^{]*font-weight:700", style.text)
sup = re.findall(r"(\.c[0-9]+)\{[^{]*vertical-align:super", style.text)
assert strong, "Nenašel jsem styl pro tučný text"
assert sup, "Nenašel jsem styl pro superskript"
def vycisti(sekce):
sekce.name = "div"
sekce.attrs.clear()
# Nahradíme třídy nativními HTML prvky.
for tag in sekce.select(", ".join(sup)):
tag.name = "sup"
for tag in sekce.select(", ".join(strong)):
tag.name = "strong"
# Zbavíme se <span>ů.
for tag in sekce.select("span"):
tag.unwrap()
# Ořízneme nežádoucí atributy.
for tag in sekce.find_all():
for attr in list(tag.attrs):
if attr not in ATRIBUTY:
del tag.attrs[attr]
if tag.name != "a" and attr == "id":
del tag.attrs[attr]
# Ořízneme prázdné tagy.
for tag in sekce.find_all():
if tag.text == "" and tag.name not in ["br", "hr"]:
tag.extract()
# Opravíme odkazy, které se nakazily Googlem.
for tag in sekce.select("*[href]"):
_proto, _loc, _path, query, _frag = urllib.parse.urlsplit(tag.attrs["href"])
qs = urllib.parse.parse_qs(query)
if "q" in qs:
tag.attrs["href"] = qs["q"][0]
# Opravíme odkazy, které se nakazily Facebookem.
for tag in sekce.select("*[href]"):
proto, loc, path, query, frag = urllib.parse.urlsplit(tag.attrs["href"])
qs = urllib.parse.parse_qs(query)
if "fbclid" in qs:
del qs["fbclid"]
query = urllib.parse.urlencode(qs, doseq=True)
tag.attrs["href"] = urllib.parse.urlunsplit(
(proto, loc, path, query, frag)
)
# Spojíme po sobě následující prvky některých typů.
for fst in sekce.select("ul, sup, strong"):
if fst.parent is None:
continue
snd = fst.next_sibling
while snd is not None:
if snd.name == fst.name:
snd.extract()
for child in snd:
fst.append(child)
else:
break
snd = fst.next_sibling
# Nejprve zpracujeme bod.
radky = list(bod_html.select("tr"))
for radek in radky[1:]:
nazev, sekce = radek.select("td")
nazev = nazev.text
nazev = nazev.strip(" \u00a0\t\r\n:")
nazev = re.sub("[ \u00a0]+", " ", nazev)
if nazev not in SEKCE:
print("Přebývá neznámá sekce: {!r}".format(nazev), file=sys.stderr)
vycisti(sekce)
bod[nazev] = sekce
for nazev in SEKCE:
if nazev not in bod:
print("Chybí povinná sekce {!r}".format(nazev), file=sys.stderr)
# Benefity
for radek in bene_html.select("tr")[1:]:
cilovka, benefit, _info = radek.select("td")
cilovka = cilovka.text
cilovka = re.sub(r"\(.*\)", "", cilovka)
cilovka = cilovka.strip(" \u00a0\t\r\n:")
cilovka = re.sub("[ \u00a0]+", " ", cilovka)
vycisti(benefit)
if benefit.text.strip() != "":
benefity[cilovka] = benefit
# Pro případnou zběžnou kontrolu:
# print("<h1>" + nazev_bodu + "</h1>")
# for nazev, sekce in bod.items():
# print("<hr/>")
# print("<h2>" + nazev + "</h2>")
# print(sekce)
# print("<hr/>")
# print("<h2>Benefity</h2>")
# for cilovka, benefit in benefity.items():
# print("<h3>" + cilovka + "</h3>")
# print(benefit)
return {
"nazev": nazev_bodu,
"sekce": {nazev: str(sekce) for nazev, sekce in bod.items()},
"benefity": {cilovka: str(benefit) for cilovka, benefit in benefity.items()},
}
def strip_html(value):
return bleach.clean(value, tags=[], attributes={}, styles=[], strip=True)
def replace_tags(value):
value = value.replace("<div>", "")
value = value.replace("</div>", "")
if not value.startswith("<p>"):
value = f"<p>{value}</p>"
return value
def set_fancy_lists(value):
value = value.replace("<ul>", '<ul class="unordered-list unordered-list-checks">')
value = value.replace("<li>", '<li class="mb-4">')
return value
def clean_point(point):
out = {}
for old_key, val in point.items():
key = slugify(old_key)
if key not in KNOWN_KEYS:
raise ValueError(f"Unknown key: {old_key}")
if key in ["nadpis"]:
out[key] = strip_html(val)
else:
out[key] = replace_tags(val)
return out
def prepare_faq(value):
soup = bs4.BeautifulSoup(value, "html.parser")
questions = defaultdict(list)
for tag in soup.children:
if tag.strong:
key = tag.strong.string
else:
questions[key].append(str(tag))
data = []
for key, val in questions.items():
data.append(
{"type": "question", "value": {"question": key, "answer": "".join(val)}}
)
return json.dumps(data)
def prepare_horizon(value):
raw = strip_html(value)
m = re.match(r"^(\d+)\s(\w+)$", raw)
if m:
return None, m.group(1), m.group(2)
return value, None, None
def print_preview(point):
print("")
for key, val in point.items():
print(key, ":", val[:120])
def print_full(point):
for key, val in point.items():
print("")
print(key)
print("-" * 100)
print(val)
print("-" * 100)
def prepare_point(source):
point = clean_point(source)
# print_full(point)
return point
def prepare_benefits(benefits):
return None
{% load wagtailcore_tags %}
{% load wagtailcore_tags elections2021_extras %}
<div class="accordeon-row">
<div class="accordeon-row-head" onclick="if(this.parentElement.classList.contains('accordeon-row--open')) this.parentElement.classList.remove('accordeon-row--open'); else this.parentElement.classList.add('accordeon-row--open');">
<h3 class="accordeon-row-heading head-alt-xs">{{ block.value.question }}</h3>
......@@ -6,7 +6,7 @@
</div>
<div class="accordeon-row-body" style="max-height: 216px;">
<div>
<p>{{ block.value.answer|richtext }}</p>
<p>{{ block.value.answer|richtext|format_sources }}</p>
</div>
</div>
</div>
{% extends "elections2021/base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% load wagtailcore_tags wagtailimages_tags elections2021_extras %}
{% block content %}
<div class="container container-default container-collapsible pt-4 pb-20 container-collapsible-open">
......@@ -27,7 +27,7 @@
<div class="mt-10 md:mt-12">
<h1 class="head-alt-md md:head-alt-lg head-alt-highlighted">{{ page.title }}</h1>
<h2 class="head-alt-sm md:head-alt-md mb-20 mt-9">
<div class="leading-tight">{{ page.annotation|richtext }}</div>
<div class="leading-tight">{{ page.annotation|richtext|format_sources }}</div>
</h2>
</div>
</header>
......@@ -38,18 +38,18 @@
<div class="problem-inner content-block">
<h2 class="head-alt-sm md:head-alt-md mb-9">Problém</h2>
<p class="text-base mb-8 para">
{{ page.problem|richtext }}
{{ page.problem|richtext|format_sources }}
</p>
<h3 class="head-alt-xs md:head-alt-sm mb-9">Kontext problému</h3>
<p class="text-base mb-8 para">
{{ page.context|richtext }}
{{ page.context|richtext|format_sources }}
</p>
</div>
<div class="ideal-check"><i class="ico--check text-xs sm:text-xl text-black"></i></div>
<div class="ideal-inner bg-lemon content-block">
<h2 class="head-alt-sm md:head-alt-md mb-9">Ideál</h2>
<p class="text-base">
{{ page.ideal|richtext }}
{{ page.ideal|richtext|format_sources }}
</p>
</div>
</section>
......@@ -57,7 +57,7 @@
<h3 class="head-alt-base mb-8">Navrhovaná opatření:</h3>
<div class="content-block">
{{ page.proposal|richtext }}
{{ page.proposal|richtext|format_sources }}
</div>
<div class="grid grid-cols-1 gap-8 my-20">
......@@ -75,7 +75,7 @@
<b data-value="{{ page.time_horizon_number }}" class="flip-card__back"></b>
<b data-value="{{ page.time_horizon_number }}" class="flip-card__back-bottom"></b>
</span>
<span class="flip-clock__slot font-alt text-2xl">{{ page.get_time_horizon_unit_display }}</span>
<span class="flip-clock__slot font-alt text-2xl">{{ page.time_horizon_unit }}</span>
</span>
</div>
</div>
......@@ -87,7 +87,7 @@
<div class="card shadow-none bg-grey-125">
<div class="card__body content-block">
<h3 class="card-headline mb-8">Časový horizont</h3>
<p class="card-body-text para">{{ page.time_horizon_text|richtext }}</p>
<p class="card-body-text para">{{ page.time_horizon_text|richtext|format_sources }}</p>
</div>
</div>
{% endif %}
......@@ -176,7 +176,7 @@
<h3 class="head-alt-base mb-10">Co už jsme udělali:</h3>
<div class="content-block">
{{ page.already_done|richtext }}
{{ page.already_done|richtext|format_sources }}
</div>
<h3 class="head-alt-base mb-8 mt-20">FAQ</h3>
......@@ -185,6 +185,7 @@
{% for block in page.faq %}
{% include_block block %}
{% endfor %}
</div>
<script>
document.addEventListener("DOMContentLoaded", setMaxHeights);
......@@ -203,111 +204,61 @@
</script>
{% comment %}
{% if page.related_points %}
<h3 id="relatedpoints" class="head-alt-base mb-8 mt-20">Související body:</h3>
<div class="">
<div class="__js-root"><div><div class="VueCarousel article-card-list pb-8"><div class="VueCarousel-wrapper"><div class="VueCarousel-inner" style="transform: translate(0px, 0px); transition: transform 0.5s ease 0s; flex-basis: 868px; visibility: visible; height: auto;"><div tabindex="-1" role="tabpanel" class="VueCarousel-slide VueCarousel-slide-active VueCarousel-slide-center"><div class="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-8"><div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. </p></div></div></div></div> <div tabindex="-1" role="tabpanel" class="VueCarousel-slide" aria-hidden="true"><div class="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-8"><div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Short description</p></div></div> <div class="card shadow-none bg-grey-125 card--hoveractive"><div class="card__body"><h1 class="card-headline mb-8"><a href="#">Otevřenost a zapojení občanů</a></h1> <p class="card-body-text">Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. Long description. </p></div></div></div></div></div></div> <div data-v-453ad8cd="" class="VueCarousel-navigation"><button data-v-453ad8cd="" type="button" aria-label="Previous page" tabindex="-1" class="VueCarousel-navigation-button VueCarousel-navigation-prev VueCarousel-navigation--disabled" style="padding: 8px; margin-right: -8px;"></button> <button data-v-453ad8cd="" type="button" aria-label="Next page" tabindex="0" class="VueCarousel-navigation-button VueCarousel-navigation-next" style="padding: 8px; margin-left: -8px;"></button></div> <div data-v-438fd353="" class="VueCarousel-pagination" style=""><div data-v-438fd353="" role="tablist" class="VueCarousel-dot-container" style="margin-top: 20px;"><button data-v-438fd353="" aria-hidden="false" role="tab" title="Item 0" value="Item 0" aria-label="Item 0" aria-selected="true" class="VueCarousel-dot VueCarousel-dot--active" style="margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);"></button><button data-v-438fd353="" aria-hidden="false" role="tab" title="Item 1" value="Item 1" aria-label="Item 1" aria-selected="false" class="VueCarousel-dot" style="margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(239, 239, 239);"></button></div></div></div></div></div>
</div>
<h3 class="head-alt-base mb-3 mt-20">Které části jsou závazné</h3>
<p><a href="#" class="text-acidgreen underline">Zjistit více</a></p>
<h3 class="head-alt-base mb-8 mt-8">Zdroje:</h3>
<div id="source1" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference1">[1]</a></div>
<div class="inline-block align-top max-w-2xl">
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
<div class="__js-root">
<div>
<div class="VueCarousel article-card-list pb-8">
<div class="VueCarousel-wrapper">
<div class="VueCarousel-inner" style="transform: translate(0px, 0px); transition: transform 0.5s ease 0s; flex-basis: 868px; visibility: visible; height: auto;">
<div tabindex="-1" role="tabpanel" class="VueCarousel-slide VueCarousel-slide-active VueCarousel-slide-center">
<div class="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-8">
<div class="card shadow-none bg-grey-125 card--hoveractive">
<div class="card__body">
<h1 class="card-headline mb-8">
<a href="#">Otevřenost a zapojení občanů</a>
</h1>
<p class="card-body-text">Short description</p>
</div>
</div>
<div id="source2" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference2">[2]</a></div>
<div class="inline-block align-top max-w-2xl">
<p>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</p>
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
<div class="card shadow-none bg-grey-125 card--hoveractive">
<div class="card__body">
<h1 class="card-headline mb-8">
<a href="#">Otevřenost a zapojení občanů</a>
</h1>
<p class="card-body-text">Short description</p>
</div>
</div>
<div id="source3" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference3">[3]</a></div>
<div class="inline-block align-top max-w-2xl">
<p>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</p>
<blockquote class="quote quote-pirati-stan">
„Curabitizzle fo shizzle diam quizzle nisi nizzle mollizzle. Suspendisse boofron. Morbi odio. Sure pizzle. Crazy orci. Shut the shizzle up maurizzle get down get down, check out this a, go to hizzle sit amizzle, malesuada izzle, pede. Pellentesque gravida. Vestibulizzle check it out mi, volutpat izzle, shiz sed, shiznit sempizzle, da bomb. Funky fresh in ipsum. Da bomb volutpat felis vizzle daahng dawg. Crizzle quis dope izzle fo shizzle my ni.“
</blockquote>
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
</div>
</div>
<div id="source4" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference4">[4]</a></div>
<div class="inline-block align-top max-w-2xl">
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
</div>
</div>
<div id="source5" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference5">[5]</a></div>
<div class="inline-block align-top max-w-2xl">
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
<div data-v-453ad8cd="" class="VueCarousel-navigation">
<button data-v-453ad8cd="" type="button" aria-label="Previous page" tabindex="-1" class="VueCarousel-navigation-button VueCarousel-navigation-prev VueCarousel-navigation--disabled" style="padding: 8px; margin-right: -8px;"></button>
<button data-v-453ad8cd="" type="button" aria-label="Next page" tabindex="0" class="VueCarousel-navigation-button VueCarousel-navigation-next" style="padding: 8px; margin-left: -8px;"></button>
</div>
<div data-v-438fd353="" class="VueCarousel-pagination" style="">
<div data-v-438fd353="" role="tablist" class="VueCarousel-dot-container" style="margin-top: 20px;">
<button data-v-438fd353="" aria-hidden="false" role="tab" title="Item 0" value="Item 0" aria-label="Item 0" aria-selected="true" class="VueCarousel-dot VueCarousel-dot--active" style="margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);"></button>
<button data-v-438fd353="" aria-hidden="false" role="tab" title="Item 1" value="Item 1" aria-label="Item 1" aria-selected="false" class="VueCarousel-dot" style="margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(239, 239, 239);"></button>
</div>
<div id="source6" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference6">[6]</a></div>
<div class="inline-block align-top max-w-2xl">
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
</div>
</div>
<div id="source7" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference7">[7]</a></div>
<div class="inline-block align-top max-w-2xl">
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
</div>
</div>
<div id="source8" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference8">[8]</a></div>
<div class="inline-block align-top max-w-2xl">
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
</div>
</div>
<div id="source9" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference9">[9]</a></div>
{% endif %}
{% endcomment %}
<div class="inline-block align-top max-w-2xl">
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
</div>
</div>
<div id="source10" class="source leading-loose text-sm">
<div class="inline-block align-top w-9"><a class="text-fxactivecolor hover:no-underline" href="#reference10">[10]</a></div>
<h3 class="head-alt-base mb-3 mt-20">Které části jsou závazné</h3>
<p><a href="#" class="text-acidgreen underline">Zjistit více</a></p>
<div class="inline-block align-top max-w-2xl">
<a href="mailto:example@example.com" class="text-fxactivecolor underline ">
<span>Rizzle adipiscing elizzle. Nullam sapien velizzle, shit volutpizzle, my</span>
</a>
</div>
</div>
{% endcomment %}
<h3 id="zdroje" class="head-alt-base mb-8 mt-8">Zdroje:</h3>
{{ page.sources|richtext|format_sources_block }}
</div>
......
import re
import bs4
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter(is_safe=True)
@stringfilter
def format_sources_block(value):
soup = bs4.BeautifulSoup(value, "html.parser")
out = []
for item in soup.children:
for a in item.find_all("a"):
a["class"] = "text-fxactivecolor underline"
text = "".join(str(c) for c in item.contents)
number = ""
m = re.match(r"^\[(\d+)\]\s(.+)", text)
if m:
number = m.group(1)
text = m.group(2)
out.append(
f"""
<div id="zdroj{number}" class="source leading-loose text-sm">
<div class="inline-block align-top w-9 text-fxactivecolor">[{number}]</div>
<div class="inline-block align-top max-w-2xl">{text}</div>
</div>
"""
)
return "".join(out)
@register.filter(is_safe=True)
@stringfilter
def format_sources(value):
soup = bs4.BeautifulSoup(value, "html.parser")
for sup in soup.find_all("sup"):
sup.wrap(soup.new_tag("a", id="reference", href="#zdroje"))
return str(soup)
......@@ -15,3 +15,5 @@ ics
arrow
sentry-sdk
Markdown
beautifulsoup4
bleach
......@@ -4,63 +4,169 @@
#
# pip-compile base.in
#
anyascii==0.1.7 # via wagtail
arrow==0.14.7 # via -r base.in, ics
asgiref==3.3.1 # via django
beautifulsoup4==4.8.2 # via wagtail
certifi==2020.12.5 # via requests, sentry-sdk
cffi==1.14.5 # via cryptography
chardet==4.0.0 # via requests
cryptography==3.4.7 # via josepy, mozilla-django-oidc, pyopenssl
django-environ==0.4.5 # via -r base.in
django-extensions==3.1.1 # via -r base.in
django-filter==2.4.0 # via wagtail
django-modelcluster==5.1 # via wagtail
django-ranged-response==0.2.0 # via django-simple-captcha
django-redis==4.12.1 # via -r base.in
django-settings-export==1.2.1 # via -r base.in
django-simple-captcha==0.5.14 # via -r base.in
django-taggit==1.3.0 # via wagtail
django-treebeard==4.5.1 # via wagtail
django-widget-tweaks==1.4.8 # via -r base.in
django==3.1.7 # via django-filter, django-ranged-response, django-redis, django-settings-export, django-simple-captcha, django-taggit, django-treebeard, djangorestframework, mozilla-django-oidc, wagtail
djangorestframework==3.12.4 # via wagtail
draftjs-exporter==2.1.7 # via wagtail
et-xmlfile==1.0.1 # via openpyxl
html5lib==1.1 # via wagtail
ics==0.7 # via -r base.in
idna==2.10 # via requests
josepy==1.8.0 # via mozilla-django-oidc
l18n==2020.6.1 # via wagtail
markdown==3.3.4 # via -r base.in
mozilla-django-oidc==1.2.4 # via pirates
numpy==1.20.2 # via opencv-python
opencv-python==4.5.1.48 # via -r base.in
openpyxl==3.0.7 # via tablib
pillow==8.1.2 # via django-simple-captcha, wagtail
pirates==0.5.0 # via -r base.in
psycopg2-binary==2.8.6 # via -r base.in
pycparser==2.20 # via cffi
pyopenssl==20.0.1 # via josepy
python-dateutil==2.8.1 # via arrow, ics
pytz==2021.1 # via django, django-modelcluster, l18n
redis==3.5.3 # via django-redis
requests==2.25.1 # via -r base.in, mozilla-django-oidc, wagtail
sentry-sdk==1.0.0 # via -r base.in
six==1.15.0 # via django-simple-captcha, html5lib, ics, l18n, mozilla-django-oidc, pyopenssl, python-dateutil
soupsieve==2.2.1 # via beautifulsoup4
sqlparse==0.4.1 # via django
tablib[xls,xlsx]==3.0.0 # via wagtail
tatsu==5.6.1 # via ics
urllib3==1.26.4 # via requests, sentry-sdk
wagtail-metadata==3.4.0 # via -r base.in
wagtail==2.12.3 # via -r base.in, wagtail-metadata
webencodings==0.5.1 # via html5lib
whitenoise==5.2.0 # via -r base.in
willow==1.4 # via wagtail
xlrd==2.0.1 # via tablib
xlsxwriter==1.3.7 # via wagtail
xlwt==1.3.0 # via tablib
anyascii==0.1.7
# via wagtail
arrow==0.14.7
# via
# -r base.in
# ics
asgiref==3.3.1
# via django
beautifulsoup4==4.8.2
# via
# -r base.in
# wagtail
bleach==3.3.0
# via -r base.in
certifi==2020.12.5
# via
# requests
# sentry-sdk
cffi==1.14.5
# via cryptography
chardet==4.0.0
# via requests
cryptography==3.4.7
# via
# josepy
# mozilla-django-oidc
# pyopenssl
django-environ==0.4.5
# via -r base.in
django-extensions==3.1.1
# via -r base.in
django-filter==2.4.0
# via wagtail
django-modelcluster==5.1
# via wagtail
django-ranged-response==0.2.0
# via django-simple-captcha
django-redis==4.12.1
# via -r base.in
django-settings-export==1.2.1
# via -r base.in
django-simple-captcha==0.5.14
# via -r base.in
django-taggit==1.3.0
# via wagtail
django-treebeard==4.5.1
# via wagtail
django-widget-tweaks==1.4.8
# via -r base.in
django==3.1.7
# via
# django-filter
# django-ranged-response
# django-redis
# django-settings-export
# django-simple-captcha
# django-taggit
# django-treebeard
# djangorestframework
# mozilla-django-oidc
# wagtail
djangorestframework==3.12.4
# via wagtail
draftjs-exporter==2.1.7
# via wagtail
et-xmlfile==1.0.1
# via openpyxl
html5lib==1.1
# via wagtail
ics==0.7
# via -r base.in
idna==2.10
# via requests
josepy==1.8.0
# via mozilla-django-oidc
l18n==2020.6.1
# via wagtail
markdown==3.3.4
# via -r base.in
mozilla-django-oidc==1.2.4
# via pirates
numpy==1.20.2
# via opencv-python
opencv-python==4.5.1.48
# via -r base.in
openpyxl==3.0.7
# via tablib
packaging==20.9
# via bleach
pillow==8.1.2
# via
# django-simple-captcha
# wagtail
pirates==0.5.0
# via -r base.in
psycopg2-binary==2.8.6
# via -r base.in
pycparser==2.20
# via cffi
pyopenssl==20.0.1
# via josepy
pyparsing==2.4.7
# via packaging
python-dateutil==2.8.1
# via
# arrow
# ics
pytz==2021.1
# via
# django
# django-modelcluster
# l18n
redis==3.5.3
# via django-redis
requests==2.25.1
# via
# -r base.in
# mozilla-django-oidc
# wagtail
sentry-sdk==1.0.0
# via -r base.in
six==1.15.0
# via
# bleach
# django-simple-captcha
# html5lib
# ics
# l18n
# mozilla-django-oidc
# pyopenssl
# python-dateutil
soupsieve==2.2.1
# via beautifulsoup4
sqlparse==0.4.1
# via django
tablib[xls,xlsx]==3.0.0
# via wagtail
tatsu==5.6.1
# via ics
urllib3==1.26.4
# via
# requests
# sentry-sdk
wagtail-metadata==3.4.0
# via -r base.in
wagtail==2.12.3
# via
# -r base.in
# wagtail-metadata
webencodings==0.5.1
# via
# bleach
# html5lib
whitenoise==5.2.0
# via -r base.in
willow==1.4
# via wagtail
xlrd==2.0.1
# via tablib
xlsxwriter==1.3.7
# via wagtail
xlwt==1.3.0
# via tablib
# The following packages are considered to be unsafe in a requirements file:
# setuptools
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment