From 7b83e72629ec1d662d46547d47dd2e49be70b70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bedna=C5=99=C3=ADk?= <jan.bednarik@gmail.com> Date: Wed, 12 May 2021 10:12:01 +0200 Subject: [PATCH] elections2021: Program point import and detail mostly done --- .isort.cfg | 2 +- elections2021/constants.py | 5 - elections2021/forms.py | 62 ++++ .../migrations/0006_auto_20210511_0048.py | 73 +++++ elections2021/models.py | 52 +++- elections2021/parser.py | 282 ++++++++++++++++++ .../elections2021/_question_block.html | 4 +- .../elections2021_program_point_page.html | 173 ++++------- elections2021/templatetags/__init__.py | 0 .../templatetags/elections2021_extras.py | 46 +++ requirements/base.in | 2 + requirements/base.txt | 220 ++++++++++---- 12 files changed, 731 insertions(+), 190 deletions(-) create mode 100644 elections2021/forms.py create mode 100644 elections2021/migrations/0006_auto_20210511_0048.py create mode 100644 elections2021/parser.py create mode 100644 elections2021/templatetags/__init__.py create mode 100644 elections2021/templatetags/elections2021_extras.py diff --git a/.isort.cfg b/.isort.cfg index 24b9a93d..98158d88 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -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 diff --git a/elections2021/constants.py b/elections2021/constants.py index b056fc83..065b7194 100644 --- a/elections2021/constants.py +++ b/elections2021/constants.py @@ -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")) diff --git a/elections2021/forms.py b/elections2021/forms.py new file mode 100644 index 00000000..39381f81 --- /dev/null +++ b/elections2021/forms.py @@ -0,0 +1,62 @@ +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 diff --git a/elections2021/migrations/0006_auto_20210511_0048.py b/elections2021/migrations/0006_auto_20210511_0048.py new file mode 100644 index 00000000..da6f5e1d --- /dev/null +++ b/elections2021/migrations/0006_auto_20210511_0048.py @@ -0,0 +1,73 @@ +# 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", + ), + ), + ] diff --git a/elections2021/models.py b/elections2021/models.py index e5e1a22a..19437514 100644 --- a/elections2021/models.py +++ b/elections2021/models.py @@ -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" diff --git a/elections2021/parser.py b/elections2021/parser.py new file mode 100644 index 00000000..5440f507 --- /dev/null +++ b/elections2021/parser.py @@ -0,0 +1,282 @@ +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 diff --git a/elections2021/templates/elections2021/_question_block.html b/elections2021/templates/elections2021/_question_block.html index 0be0c8a2..0aa83c73 100644 --- a/elections2021/templates/elections2021/_question_block.html +++ b/elections2021/templates/elections2021/_question_block.html @@ -1,4 +1,4 @@ -{% 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> diff --git a/elections2021/templates/elections2021/elections2021_program_point_page.html b/elections2021/templates/elections2021/elections2021_program_point_page.html index de287829..34158efa 100644 --- a/elections2021/templates/elections2021/elections2021_program_point_page.html +++ b/elections2021/templates/elections2021/elections2021_program_point_page.html @@ -1,5 +1,5 @@ {% 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 %} - <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> - </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> - </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> - </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> + {% 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> + </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> - </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> diff --git a/elections2021/templatetags/__init__.py b/elections2021/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/elections2021/templatetags/elections2021_extras.py b/elections2021/templatetags/elections2021_extras.py new file mode 100644 index 00000000..5c257f2e --- /dev/null +++ b/elections2021/templatetags/elections2021_extras.py @@ -0,0 +1,46 @@ +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) diff --git a/requirements/base.in b/requirements/base.in index 76b21d69..ce93dbec 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -15,3 +15,5 @@ ics arrow sentry-sdk Markdown +beautifulsoup4 +bleach diff --git a/requirements/base.txt b/requirements/base.txt index 06638dde..6e42c7c8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -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 -- GitLab