Skip to content
Snippets Groups Projects
Commit c060702d authored by xaralis's avatar xaralis Committed by jan.bednarik
Browse files

feat(maps_utils): add support for map embedding as well as map collection on district

parent 5d600d22
Branches
No related tags found
2 merge requests!487Release,!481Embedded map & map feature collection support
Showing
with 2103 additions and 3 deletions
......@@ -3,4 +3,4 @@
line_length = 88
multi_line_output = 3
include_trailing_comma = true
known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,django,environ,faker,ics,markdown,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,snapshottest,taggit,wagtail,wagtailmetadata,weasyprint,yaml
known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,django,environ,faker,fastjsonschema,ics,markdown,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,snapshottest,taggit,wagtail,wagtailmetadata,weasyprint,yaml
# Generated by Django 4.0.3 on 2022-05-01 09:49
import django.db.models.deletion
import modelcluster.fields
import wagtail.contrib.table_block.blocks
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.images.blocks
import wagtailmetadata.models
from django.db import migrations, models
import shared.models
class Migration(migrations.Migration):
dependencies = [
("wagtailcore", "0066_collection_management_permissions"),
("wagtailimages", "0023_add_choose_permissions"),
("district", "0061_alter_districtarticlepage_content_and_more"),
]
operations = [
migrations.CreateModel(
name="DistrictGeoFeatureCollectionCategory",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"sort_order",
models.IntegerField(blank=True, editable=False, null=True),
),
("name", models.CharField(max_length=100, verbose_name="Název")),
(
"hex_color",
models.CharField(
help_text="Zadejte barvu pomocí HEX notace (bez # na začátku).",
max_length=6,
verbose_name="Barva (HEX)",
),
),
],
options={
"verbose_name": "Kategorie mapové kolekce",
},
),
migrations.AddField(
model_name="districthomepage",
name="stadia_apikey",
field=models.CharField(
blank=True,
help_text="API klíč pro Stadia mapy. Získáte po registraci stadiamaps.com.",
max_length=128,
null=True,
verbose_name="Stadia API key",
),
),
migrations.AlterField(
model_name="districtcenterpage",
name="sidebar_content",
field=wagtail.core.fields.StreamField(
[
(
"map",
wagtail.core.blocks.StructBlock(
[
(
"lat",
wagtail.core.blocks.DecimalBlock(
help_text="Např. 50.04075",
label="Zeměpisná šířka",
),
),
(
"lon",
wagtail.core.blocks.DecimalBlock(
help_text="Např. 15.77659",
label="Zeměpisná délka",
),
),
(
"hex_color",
wagtail.core.blocks.CharBlock(
default="000000",
help_text="Zadejte barvu pomocí HEX notace (bez # na začátku).",
label="Barva špendlíku (HEX)",
),
),
(
"zoom",
wagtail.core.blocks.IntegerBlock(
default=15,
label="Výchozí zoom",
max_value=18,
min_value=1,
),
),
(
"style",
wagtail.core.blocks.ChoiceBlock(
choices=[
("osm-mapnik", "OSM Mapnik"),
("stamen-toner", "Stamen Toner"),
("stamen-terrain", "Stamen Terrain"),
(
"stadia-osm-bright",
"Stadia OSM Bright (vyžaduje API klíč)",
),
(
"stadia-outdoors",
"Stadia Outdoors (vyžaduje API klíč)",
),
],
label="Styl",
),
),
(
"height",
wagtail.core.blocks.IntegerBlock(
label="Výška v px",
max_value=1000,
min_value=100,
),
),
]
),
),
(
"address",
wagtail.core.blocks.StructBlock(
[
(
"title",
wagtail.core.blocks.CharBlock(
label="Titulek", required=True
),
),
(
"map_image",
wagtail.images.blocks.ImageChooserBlock(
label="Obrázek mapy", required=False
),
),
(
"map_link",
wagtail.core.blocks.URLBlock(
label="Odkaz na detail mapy", required=False
),
),
(
"address",
wagtail.core.blocks.TextBlock(
label="Adresa", required=True
),
),
(
"address_info",
wagtail.core.blocks.TextBlock(
label="Info k adrese", required=False
),
),
]
),
),
(
"contact",
wagtail.core.blocks.StructBlock(
[
(
"title",
wagtail.core.blocks.CharBlock(
label="Titulek", required=True
),
),
(
"contact_list",
wagtail.core.blocks.ListBlock(
wagtail.core.blocks.StructBlock(
[
(
"person",
wagtail.core.blocks.PageChooserBlock(
label="Osoba",
page_type=[
"district.DistrictPersonPage"
],
),
),
(
"position",
wagtail.core.blocks.CharBlock(
label="Pozice", required=False
),
),
]
)
),
),
]
),
),
],
blank=True,
verbose_name="Obsah bočního panelu",
),
),
migrations.CreateModel(
name="DistrictGeoFeatureDetailPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
("perex", models.TextField(verbose_name="Perex")),
(
"geojson",
models.TextField(
help_text="Vložte surový GeoJSON objekt typu 'Feature'. Vyrobit jej můžete např. pomocí online služby geojson.io.",
verbose_name="Geodata",
),
),
(
"content",
wagtail.core.fields.StreamField(
[
("text", wagtail.core.blocks.RichTextBlock()),
("table", wagtail.contrib.table_block.blocks.TableBlock()),
],
blank=True,
verbose_name="Obsah",
),
),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="district.districtgeofeaturecollectioncategory",
verbose_name="Kategorie",
),
),
(
"guarantor",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="district.districtpersonpage",
verbose_name="Garant",
),
),
(
"image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="wagtailimages.image",
verbose_name="obrázek",
),
),
(
"search_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.image",
verbose_name="Search image",
),
),
],
options={
"verbose_name": "Položka mapové kolekce",
},
bases=(
wagtailmetadata.models.WagtailImageMetadataMixin,
shared.models.SubpageMixin,
"wagtailcore.page",
models.Model,
),
),
migrations.CreateModel(
name="DistrictGeoFeatureCollectionPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
("perex", models.TextField(null=True, verbose_name="Perex")),
(
"content",
wagtail.core.fields.StreamField(
[
("text", wagtail.core.blocks.RichTextBlock()),
("table", wagtail.contrib.table_block.blocks.TableBlock()),
],
blank=True,
verbose_name="Obsah",
),
),
(
"style",
models.CharField(
choices=[
("osm-mapnik", "OSM Mapnik"),
("stamen-toner", "Stamen Toner"),
("stamen-terrain", "Stamen Terrain"),
(
"stadia-osm-bright",
"Stadia OSM Bright (vyžaduje API klíč)",
),
("stadia-outdoors", "Stadia Outdoors (vyžaduje API klíč)"),
],
default="osm-mapnik",
max_length=50,
verbose_name="Styl mapy",
),
),
(
"map_title",
models.TextField(
blank=True, null=True, verbose_name="Titulek mapy"
),
),
(
"category_list_title",
models.TextField(
blank=True,
null=True,
verbose_name="Titulek přehledu dle kategorie",
),
),
(
"image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="wagtailimages.image",
verbose_name="obrázek",
),
),
(
"search_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.image",
verbose_name="Search image",
),
),
],
options={
"verbose_name": "Stránka s mapovou kolekcí",
},
bases=(
shared.models.SubpageMixin,
wagtailmetadata.models.WagtailImageMetadataMixin,
"wagtailcore.page",
models.Model,
),
),
migrations.AddField(
model_name="districtgeofeaturecollectioncategory",
name="page",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="categories",
to="district.districtgeofeaturecollectionpage",
),
),
]
# Generated by Django 4.0.3 on 2022-05-01 10:04
import wagtail.core.blocks
import wagtail.core.fields
import wagtail.images.blocks
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("district", "0062_districtgeofeaturecollectioncategory_and_more"),
]
operations = [
migrations.AlterField(
model_name="districtarticlepage",
name="content",
field=wagtail.core.fields.StreamField(
[
(
"text",
wagtail.core.blocks.RichTextBlock(
features=[
"h2",
"h3",
"h4",
"bold",
"italic",
"ol",
"embed",
"ul",
"link",
"document-link",
"image",
],
label="Textový editor",
),
),
(
"gallery",
wagtail.core.blocks.StructBlock(
[
(
"gallery_items",
wagtail.core.blocks.ListBlock(
wagtail.images.blocks.ImageChooserBlock(
label="obrázek", required=True
),
group="ostatní",
icon="image",
label="Galerie",
),
)
],
label="Galerie",
),
),
(
"youtube",
wagtail.core.blocks.StructBlock(
[
(
"poster_image",
wagtail.images.blocks.ImageChooserBlock(
help_text="Není třeba vyplňovat, náhled bude dohledán automaticky.",
label="Náhled videa (automatické pole)",
required=False,
),
),
(
"video_url",
wagtail.core.blocks.URLBlock(
help_text="Odkaz na YouTube video bude automaticky zkonvertován na ID videa a NEBUDE uložen.",
label="Odkaz na video",
required=False,
),
),
(
"video_id",
wagtail.core.blocks.CharBlock(
help_text="Není třeba vyplňovat, bude automaticky načteno z odkazu.",
label="ID videa (automatické pole)",
required=False,
),
),
],
label="YouTube video",
),
),
(
"map_point",
wagtail.core.blocks.StructBlock(
[
(
"lat",
wagtail.core.blocks.DecimalBlock(
help_text="Např. 50.04075",
label="Zeměpisná šířka",
),
),
(
"lon",
wagtail.core.blocks.DecimalBlock(
help_text="Např. 15.77659",
label="Zeměpisná délka",
),
),
(
"hex_color",
wagtail.core.blocks.CharBlock(
default="000000",
help_text="Zadejte barvu pomocí HEX notace (bez # na začátku).",
label="Barva špendlíku (HEX)",
),
),
(
"zoom",
wagtail.core.blocks.IntegerBlock(
default=15,
label="Výchozí zoom",
max_value=18,
min_value=1,
),
),
(
"style",
wagtail.core.blocks.ChoiceBlock(
choices=[
("osm-mapnik", "OSM Mapnik"),
("stamen-toner", "Stamen Toner"),
("stamen-terrain", "Stamen Terrain"),
(
"stadia-osm-bright",
"Stadia OSM Bright (vyžaduje API klíč)",
),
(
"stadia-outdoors",
"Stadia Outdoors (vyžaduje API klíč)",
),
],
label="Styl",
),
),
(
"height",
wagtail.core.blocks.IntegerBlock(
label="Výška v px",
max_value=1000,
min_value=100,
),
),
],
label="Špendlík na mapě",
),
),
(
"map_collection",
wagtail.core.blocks.StructBlock(
[
(
"lat",
wagtail.core.blocks.DecimalBlock(
help_text="Např. 50.04075",
label="Zeměpisná šířka",
),
),
(
"lon",
wagtail.core.blocks.DecimalBlock(
help_text="Např. 15.77659",
label="Zeměpisná délka",
),
),
(
"hex_color",
wagtail.core.blocks.CharBlock(
default="000000",
help_text="Zadejte barvu pomocí HEX notace (bez # na začátku).",
label="Barva špendlíku (HEX)",
),
),
(
"zoom",
wagtail.core.blocks.IntegerBlock(
default=15,
label="Výchozí zoom",
max_value=18,
min_value=1,
),
),
(
"style",
wagtail.core.blocks.ChoiceBlock(
choices=[
("osm-mapnik", "OSM Mapnik"),
("stamen-toner", "Stamen Toner"),
("stamen-terrain", "Stamen Terrain"),
(
"stadia-osm-bright",
"Stadia OSM Bright (vyžaduje API klíč)",
),
(
"stadia-outdoors",
"Stadia Outdoors (vyžaduje API klíč)",
),
],
label="Styl",
),
),
(
"height",
wagtail.core.blocks.IntegerBlock(
label="Výška v px",
max_value=1000,
min_value=100,
),
),
],
label="Mapová kolekce",
),
),
],
blank=True,
verbose_name="Článek",
),
),
]
# Generated by Django 4.0.3 on 2022-05-01 12:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("district", "0063_alter_districtarticlepage_content"),
]
operations = [
migrations.AlterModelOptions(
name="districtgeofeaturedetailpage",
options={
"ordering": ["sort_order"],
"verbose_name": "Položka mapové kolekce",
},
),
migrations.AddField(
model_name="districtgeofeaturedetailpage",
name="sort_order",
field=models.IntegerField(
blank=True,
help_text="Čím větší hodnotu zadáte, tím později ve výpise položk abude.",
null=True,
verbose_name="Index řazení",
),
),
]
import json
import random
from django.core.exceptions import ValidationError
......@@ -11,6 +12,7 @@ from taggit.models import Tag, TaggedItemBase
from wagtail.admin.edit_handlers import (
FieldPanel,
HelpPanel,
InlinePanel,
MultiFieldPanel,
ObjectList,
PageChooserPanel,
......@@ -21,11 +23,14 @@ from wagtail.admin.forms import WagtailAdminPageForm
from wagtail.contrib.table_block.blocks import TableBlock
from wagtail.core.blocks import RichTextBlock
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Page
from wagtail.core.models import Orderable, Page
from wagtail.images.edit_handlers import ImageChooserPanel
from wagtailmetadata.models import MetadataPageMixin
from calendar_utils.models import CalendarMixin
from maps_utils.blocks import MapPointBlock
from maps_utils.const import DEFAULT_MAP_STYLE, MAP_STYLES, SUPPORTED_FEATURE_TYPES
from maps_utils.validation import validators as maps_validators
from shared.models import (
ArticleMixin,
ExtendedMetadataHomePageMixin,
......@@ -182,6 +187,15 @@ class DistrictHomePage(
related_name="+",
)
# API keys
stadia_apikey = models.CharField(
"Stadia API key",
max_length=128,
help_text="API klíč pro Stadia mapy. Získáte po registraci stadiamaps.com.",
null=True,
blank=True,
)
### PANELS
content_panels = Page.content_panels + [
......@@ -240,6 +254,12 @@ class DistrictHomePage(
],
gettext_lazy("Nastavení lišty s kalendářem a mapou"),
),
MultiFieldPanel(
[
FieldPanel("stadia_apikey"),
],
gettext_lazy("API klíče"),
),
ImageChooserPanel("fallback_image"),
]
......@@ -266,6 +286,7 @@ class DistrictHomePage(
"district.DistrictPeoplePage",
"district.DistrictProgramPage",
"district.DistrictTagsPage",
"district.DistrictGeoFeatureCollectionPage",
]
### OTHERS
......@@ -329,6 +350,12 @@ class DistrictHomePage(
def has_calendar(self):
return self.calendar_id is not None
def get_api_keys(self):
"""Get collection of all API keys."""
return {
"stadia": self.stadia_apikey,
}
class DistrictArticleTag(TaggedItemBase):
content_object = ParentalKey(
......@@ -929,7 +956,11 @@ class DistrictCenterPage(
)
text = RichTextField("Text", blank=True, null=True)
sidebar_content = StreamField(
[("address", blocks.AddressBlock()), ("contact", blocks.CenterContactBlock())],
[
("map", MapPointBlock()),
("address", blocks.AddressBlock()),
("contact", blocks.CenterContactBlock()),
],
verbose_name="Obsah bočního panelu",
blank=True,
)
......@@ -1023,6 +1054,7 @@ class DistrictCrossroadPage(
"district.DistrictPersonPage",
"district.DistrictProgramPage",
"district.DistrictTagsPage",
"district.DistrictGeoFeatureCollectionPage",
]
### OTHERS
......@@ -1065,3 +1097,275 @@ class DistrictCustomPage(
class Meta:
verbose_name = "Libovolná vlastní stránka"
def _build_geojson_feature_with_props(feature, request):
fwp = json.loads(feature.geojson)
fwp["properties"] = {
"id": feature.pk,
"index": feature.index,
"slug": f"{feature.pk}-{feature.slug}",
"title": feature.title,
"description": feature.perex,
"image": feature.image.get_rendition("fill-800x450|jpegquality-80").url
if feature.image
else None,
"link": feature.get_url(request),
"category": feature.category.name,
}
return fwp
class DistrictGeoFeatureCollectionPage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
perex = models.TextField("Perex", null=True)
content = StreamField(
[
("text", RichTextBlock()),
("table", TableBlock()),
],
verbose_name="Obsah",
blank=True,
)
image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="obrázek",
)
style = models.CharField(
"Styl mapy", choices=MAP_STYLES, max_length=50, default=DEFAULT_MAP_STYLE
)
map_title = models.TextField("Titulek mapy", blank=True, null=True)
category_list_title = models.TextField(
"Titulek přehledu dle kategorie", blank=True, null=True
)
### PANELS
content_panels = Page.content_panels + [
MultiFieldPanel(
[
FieldPanel("perex"),
StreamFieldPanel("content"),
ImageChooserPanel("image"),
],
"Obsah hlavní stránky kolekce",
),
MultiFieldPanel(
[
InlinePanel("categories"),
FieldPanel("category_list_title"),
],
"Kategorie",
),
MultiFieldPanel(
[
FieldPanel("map_title"),
FieldPanel("style"),
],
"Nastavení mapy",
),
]
parent_page_types = ["district.DistrictHomePage"]
subpage_types = ["district.DistrictGeoFeatureDetailPage"]
class Meta:
verbose_name = "Stránka s mapovou kolekcí"
def get_features_by_category(self):
features = self.get_children().live().specific().select_related("category")
categories = sorted(set(f.category for f in features), key=lambda c: c.name)
return [
(category, [f for f in features if f.category == category])
for category in categories
]
def get_context(self, request):
context = super().get_context(request)
features_by_category = self.get_features_by_category()
context["features_by_category"] = features_by_category
context["js_map"] = {
"apikeys": json.dumps(self.root_page.get_api_keys()),
"style": self.style,
"categories": json.dumps(
[
# Gather all categories used in collection
{"name": c.name, "color": c.hex_color}
for c, f in features_by_category
]
),
"geojson": json.dumps(
{
# Dump individual features in collection
"type": "FeatureCollection",
"features": [
_build_geojson_feature_with_props(f, request)
for c, features in features_by_category
for f in features
],
}
),
}
return context
class DistrictGeoFeatureCollectionCategory(Orderable):
name = models.CharField("Název", max_length=100)
hex_color = models.CharField(
"Barva (HEX)",
max_length=6,
help_text="Zadejte barvu pomocí HEX notace (bez # na začátku).",
)
page = ParentalKey(
DistrictGeoFeatureCollectionPage,
on_delete=models.CASCADE,
related_name="categories",
)
### PANELS
panels = [
FieldPanel("name"),
FieldPanel("hex_color"),
]
class Meta:
verbose_name = "Kategorie mapové kolekce"
def __str__(self) -> str:
return f"{self.page} / {self.name}"
class DistrictGeoFeatureDetailPage(
ExtendedMetadataPageMixin, MetadataPageMixin, SubpageMixin, Page, Orderable
):
perex = models.TextField("Perex", null=False)
geojson = models.TextField(
"Geodata",
help_text="Vložte surový GeoJSON objekt typu 'Feature'. Vyrobit jej můžete např. pomocí online služby geojson.io.",
null=False,
)
category = models.ForeignKey(
"district.DistrictGeoFeatureCollectionCategory",
verbose_name="Kategorie",
blank=False,
null=False,
on_delete=models.PROTECT,
)
guarantor = models.ForeignKey(
"district.DistrictPersonPage",
verbose_name="Garant",
on_delete=models.PROTECT,
null=True,
blank=True,
)
image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="obrázek",
)
content = StreamField(
[
("text", RichTextBlock()),
("table", TableBlock()),
],
verbose_name="Obsah",
blank=True,
)
sort_order = models.IntegerField(
"Index řazení",
null=True,
blank=True,
help_text="Čím větší hodnotu zadáte, tím později ve výpise položk abude.",
)
sort_order_field = "sort_order"
### PANELS
content_panels = [
MultiFieldPanel(
[
FieldPanel("title"),
FieldPanel("perex"),
StreamFieldPanel("content"),
ImageChooserPanel("image"),
FieldPanel("category"),
],
"Základní informace",
),
FieldPanel("geojson"),
PageChooserPanel("guarantor"),
FieldPanel("sort_order"),
]
promote_panels = make_promote_panels(
admin_help.build(admin_help.NO_SEO_TITLE, admin_help.NO_DESCRIPTION_USE_PEREX),
search_image=False,
)
parent_page_types = ["district.DistrictGeoFeatureCollectionPage"]
subpage_types = []
class Meta:
verbose_name = "Položka mapové kolekce"
ordering = ["sort_order"]
@property
def index(self):
if not hasattr(self, "__index"):
self.__index = (
list(
self.get_siblings(inclusive=True).values_list("pk", flat=True)
).index(self.pk)
+ 1
)
return self.__index
def get_meta_image(self):
return self.image
def get_meta_description(self):
if self.search_description:
return self.search_description
if len(self.perex) > 150:
return str(self.perex)[:150] + "..."
return self.perex
def get_context(self, request):
context = super().get_context(request)
context[
"features_by_category"
] = self.get_parent().specific.get_features_by_category()
context["js_map"] = {
"apikeys": json.dumps(self.root_page.get_api_keys()),
"style": self.get_parent().specific.style,
"categories": json.dumps(
[{"name": self.category.name, "color": self.category.hex_color}]
),
"geojson": json.dumps(
{
"type": "FeatureCollection",
"features": [_build_geojson_feature_with_props(self, request)],
}
),
}
return context
def clean(self):
try:
self.geojson = maps_validators.normalize_geojson_feature(
self.geojson, allowed_types=SUPPORTED_FEATURE_TYPES
)
except ValueError as exc:
raise ValidationError({"geojson": str(exc)}) from exc
{% extends "district/base.html" %}
{% load static wagtailcore_tags wagtailimages_tags %}
{% block subheader %}
{% image page.image width-1920 as bg_img %}
<header class="hero hero--image py-16 " style="--image-url: url({{ bg_img.url }})">
<div class="container container--default">
<h1 class="head-alt-md md:head-alt-lg max-w-2xl">
{{ page.title }}
</h1>
<h2 class="head-xs mt-4">
{{ page.perex }}
</h2>
</div>
</header>
{% endblock %}
{% block container_class %}container--default{% endblock %}
{% block container_spacing %}py-8 pb-0 lg:py-16{% endblock %}
{% block content %}
<article>
<div class="md:space-y-8 lg:space-y-16">
<div class="lg:flex lg:space-x-16">
<section class="lg:w-2/3">
<div class="content-block mb-4">
{% for block in page.content %}
{% include_block block %}
{% endfor %}
</div>
</section>
<div class="lg:w-1/3">
<aside class="sharebox pt-4 md:card md:elevation-10">
<div class="md:card__body">
<span class="head-alt-base md:head-alt-md">Sdílení je aktem lásky</span>
<div class="flex w-full space-x-4 pt-4 md:pt-8 text-center text-white">
<a
href="https://www.facebook.com/sharer/sharer.php?u={{ page.full_url|urlencode }}"
onclick="window.open(this.href, 'pop-up', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;"
class="bg-brands-facebook px-8 py-3 text-2xl w-full hover:no-underline"
><i class="ico--facebook"></i></a>
<a
href="https://twitter.com/intent/tweet?text={{ page.title|urlencode }}&url={{ page.full_url|urlencode }}"
onclick="window.open(this.href, 'pop-up', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;"
class="bg-brands-twitter px-8 py-3 text-2xl w-full hover:no-underline"
><i class="ico--twitter"></i></a>
</div>
</div>
<div class="h-52 overflow-hidden hidden md:block">
<img src="{% static "shared/img/flag.png" %}" alt="Pirátská strana" class="w-80 object-cover m-auto"/>
</div>
</aside>
</div>
</div>
<section>
<h2 class="head-heavy-base mt-8 mb-4">{{ page.category_list_title|default:"Přehled dle kategorií" }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8">
{% for category, features in features_by_category %}
<div class="card md:elevation-2" style="border-right: 4px #{{ category.hex_color }} solid;">
<div class="card__body p-4">
<h2 class="head-allcaps-heavy-2xs mb-2">{{ category.name }}</h2>
<ul class="leading-normal {% if not forloop.last %}mb-4{% endif %} md:mb-0">
{% for feature in features %}
<li>
<span class="rounded-full inline-flex items-center justify-center bg-grey-125 font-bold text-center text-xs w-5 h-5 mr-2 no-underline">
<a href="#{{feature.pk}}-{{feature.slug}}" class="no-underline">{{ feature.index }}</a>
</span>
<a href="#{{feature.pk}}-{{feature.slug}}"><span class="text-sm underline">{{ feature.title }}</span></a>
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endfor %}
</div>
</section>
<section>
<h2 id="mapa" class="head-heavy-base mt-8 mb-4">{{ page.map_title|default:"Interaktivní mapa" }}</h2>
<div
class="v-geo-feature-collection"
data-wrapper-class="container-padding--zero lg:container-padding--auto"
data-initial-zoom="14"
data-api-keys="{{ js_map.apikeys }}"
data-tile-style="{{ js_map.style }}"
data-categories="{{ js_map.categories }}"
data-geojson="{{ js_map.geojson }}"
></div>
</section>
{% include "shared/followus_snippet.html" %}
</div>
</article>
{% endblock %}
{% extends "district/base.html" %}
{% load static wagtailcore_tags wagtailimages_tags %}
{% block content %}
<article>
<link itemprop="mainEntityOfPage" href="{{ page.url }}">
<meta itemprop="datePublished" content="{{ page.last_published_at }}">
<meta itemprop="dateModified" content="{{ page.latest_revision_created_at }}">
<h2 class="head-heavy-xs sm:head-heavy-sm md:head-heavy-base max-w-5xl mb-2">
<a href="{% pageurl page.get_parent %}">{{ page.get_parent.title }}</a>
</h2>
<h1 itemprop="headline" class="flex items-center justify-between">
<span class="head-alt-sm sm:head-alt-md md:head-alt-lg max-w-5xl">{{ page.title }}</span>
<span v-if="page.index" class="rounded-full inline-flex items-center justify-center bg-grey-125 font-bold text-center text-sm sm:text-lg md:text-4xl w-6 h-6 sm:w-8 sm:h-8 md:w-16 md:h-16 ml-4 md:mb-2">{{ page.index }}</span>
</h1>
<div class="lg:flex mt-2 lg:space-x-16">
<div class="lg:w-2/3">
<figure class="figure mb-4">
{% image page.image width-2000 as img %}
<img src="{{ img.url }}" alt="{{ page.title }}" />
</figure>
<div class="content-block mb-8">
<p>
<strong>{{ page.perex }}</strong>
</p>
</div>
{% for block in page.content %}
<div class="content-block {% if not forloop.last %}mb-8{% endif %}">
{% include_block block %}
</div>
{% endfor %}
</div>
<div class="pt-8 lg:w-1/3 md:pt-0">
<div class="space-y-8">
<div class="lg:card lg:elevation-10">
<div class="lg:card__body">
<h2 class="head-heavy-sm mb-4"<a href="{% pageurl page.get_parent %}">{{ page.get_parent.title }}</a></h2>
<div
class="v-geo-feature-collection"
data-height="250px"
data-display-zoom-control="false"
data-display-legend="false"
data-display-popups="false"
data-initial-zoom="15"
data-api-keys="{{ js_map.apikeys }}"
data-tile-style="{{ js_map.style }}"
data-categories="{{ js_map.categories }}"
data-geojson="{{ js_map.geojson }}"
></div>
<a href="{% pageurl page.get_parent %}" class="block md:inline-block text-sm mt-4 btn btn--fullwidth btn--grey-125 btn--icon btn--hoveractive">
<div class="btn__body-wrap">
<div class="btn__body">Zobrazit celou mapu</div>
<div class="btn__icon"><i class="ico--map"></i></div>
</div>
</a>
<hr />
<h2 class="head-heavy-xs mb-2">Za tohle ručí</h2>
<div itemprop="author" itemtype="http://schema.org/Person" itemscope="">
<meta itemprop="name" content="{{ page.guarantor }}">
{% include "shared/person_badge_snippet.html" with person_page=page.guarantor %}
</div>
<hr />
<div class="space-y-4">
{% for category, features in features_by_category %}
<div class="js-feature-collection" style="border-right: 4px #{{ category.hex_color }} solid;">
<div class="flex items-center space-x-2" >
<h2 class="head-allcaps-4xs cursor-pointer js-feature-collection-toggle" data-target="{{ category.pk }}-items">{{ category.name }} ({{ features|length }})</h2>
</div>
<ul id="{{ category.pk }}-items" class="leading-normal text-sm js-feature-collection-items {% if page not in features %}hidden{% endif %}">
{% for feature in features %}
<li class="pr-2">
{% if feature.pk == page.pk %}
<span v-if="feature.index" class="rounded-full inline-flex items-center justify-center bg-grey-125 font-bold text-center text-2xs w-4 h-4 mr-1 no-underline">{{ feature.index }}</span>
<span class="font-bold">{{ feature.title }}</span>
{% else %}
<a v-if="feature.index" href="{% pageurl feature %}" class="rounded-full inline-flex items-center justify-center bg-grey-125 font-bold text-center text-2xs w-4 h-4 mr-1 no-underline">{{ feature.index }}</a>
<a href="{% pageurl feature %}" class="underline">{{ feature.title }}</a>
{% endif%}
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="sharebox md:card md:elevation-10 ">
<div class="md:card__body">
<span class="head-alt-base md:head-alt-md">Sdílení je aktem lásky</span>
<div class="flex w-full space-x-4 pt-4 md:pt-8 text-center text-white">
<a
href="https://www.facebook.com/sharer/sharer.php?u={{ page.full_url|urlencode }}"
onclick="window.open(this.href, 'pop-up', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;"
class="bg-brands-facebook px-8 py-3 text-2xl w-full hover:no-underline"
><i class="ico--facebook"></i></a>
<a
href="https://twitter.com/intent/tweet?text={{ page.title|urlencode }}&url={{ page.full_url|urlencode }}"
onclick="window.open(this.href, 'pop-up', 'left=20,top=20,width=500,height=500,toolbar=1,resizable=0'); return false;"
class="bg-brands-twitter px-8 py-3 text-2xl w-full hover:no-underline"
><i class="ico--twitter"></i></a>
</div>
</div>
<div class="h-52 overflow-hidden hidden md:block">
<img src="{% static "shared/img/flag.png" %}" alt="Pirátská strana" class="w-80 object-cover m-auto"/>
</div>
</div>
</div>
</div>
</div>
</article>
{% endblock %}
{% block scripts %}
<script>
document.querySelectorAll(".js-feature-collection-toggle").forEach((el) => {
const target = document.getElementById(el.dataset.target);
el.addEventListener("click", () => {
target.classList.toggle("hidden");
});
})
</script
{% endblock %}
......@@ -44,6 +44,7 @@ INSTALLED_APPS = [
"czech_inspirational",
"shared",
"calendar_utils",
"maps_utils",
"redmine_utils",
"users",
"pirates",
......
......
......@@ -8,6 +8,7 @@ from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
from elections2021 import views as elections2021_views
from maps_utils import urls as maps_utils_urls
handler404 = "shared.views.page_not_found"
......@@ -20,6 +21,7 @@ urlpatterns = [
elections2021_views.banner_orders_csv,
name="elections2021_banner_orders_csv",
),
path("maps/", include(maps_utils_urls)),
path("captcha/", include(captcha.urls)),
] + pirates_urlpatterns
......
......
from django.apps import AppConfig
class MapsUtilsConfig(AppConfig):
name = "maps_utils"
import json
from typing import Mapping, Optional
from uuid import uuid4
from django.forms.utils import ErrorList
from django.utils.text import slugify
from wagtail.core import blocks
from wagtail.core.blocks.struct_block import StructBlockValidationError
from wagtail.images.blocks import ImageChooserBlock
from .const import DEFAULT_MAP_STYLE, MAP_STYLES, SUPPORTED_FEATURE_TYPES
from .validation import validators
def _apikeys_from_parent_context(parent_context: Optional[Mapping]):
if parent_context:
root_page = parent_context["page"].root_page
if hasattr(root_page, "get_api_keys"):
return root_page.get_api_keys()
return {}
class MapPointBlock(blocks.StructBlock):
lat = blocks.DecimalBlock(label="Zeměpisná šířka", help_text="Např. 50.04075")
lon = blocks.DecimalBlock(label="Zeměpisná délka", help_text="Např. 15.77659")
hex_color = blocks.CharBlock(
label="Barva špendlíku (HEX)",
help_text="Zadejte barvu pomocí HEX notace (bez # na začátku).",
default="000000",
)
zoom = blocks.IntegerBlock(
label="Výchozí zoom", min_value=1, max_value=18, default=15
)
style = blocks.ChoiceBlock(
choices=MAP_STYLES, label="Styl", default=DEFAULT_MAP_STYLE
)
height = blocks.IntegerBlock(label="Výška v px", min_value=100, max_value=1000)
class Meta:
label = "Mapa se špendlíkem"
template = "maps_utils/blocks/map_point.html"
icon = "thumbtack"
def get_context(self, value, parent_context=None):
context = super().get_context(value, parent_context)
feature_id = str(uuid4())
context["js_map"] = {
"apikeys": json.dumps(_apikeys_from_parent_context(parent_context)),
"geojson": json.dumps(
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
float(value["lon"]),
float(value["lat"]),
],
},
"properties": {
"id": feature_id,
"slug": feature_id,
"title": "",
"description": "",
"image": None,
"color": value["hex_color"],
},
},
],
}
),
}
return context
class MapFeatureBlock(blocks.StructBlock):
title = blocks.CharBlock(label="Titulek", required=True)
description = blocks.TextBlock(label="Popisek", required=False)
geojson = blocks.TextBlock(
label="Geodata",
help_text="Vložte surový GeoJSON objekt typu 'Feature'. Vyrobit jej můžete např. pomocí online služby geojson.io.",
required=True,
)
image = ImageChooserBlock(label="Obrázek", required=False)
link = blocks.URLBlock(label="Odkaz", required=False)
hex_color = blocks.CharBlock(
label="Barva (HEX)",
help_text="Zadejte barvu pomocí HEX notace (bez # na začátku).",
default="000000",
)
class Meta:
label = "Položka mapové kolekce"
icon = "list-ul"
def clean(self, value):
errors = {}
if value["geojson"]:
try:
value["geojson"] = validators.normalize_geojson_feature(
value["geojson"], allowed_types=SUPPORTED_FEATURE_TYPES
)
except ValueError as exc:
errors["geojson"] = ErrorList(str(exc))
if errors:
raise StructBlockValidationError(errors)
return super().clean(value)
class MapFeatureCollectionBlock(blocks.StructBlock):
features = blocks.ListBlock(MapFeatureBlock(required=True), label="Součásti")
zoom = blocks.IntegerBlock(
label="Výchozí zoom", min_value=1, max_value=18, default=15
)
style = blocks.ChoiceBlock(
choices=MAP_STYLES, label="Styl", default=DEFAULT_MAP_STYLE
)
height = blocks.IntegerBlock(label="Výška v px", min_value=100, max_value=1000)
class Meta:
label = "Mapová kolekce"
template = "maps_utils/blocks/map_feature_collection.html"
icon = "site"
def get_context(self, value, parent_context=None):
def _geojson_feature_with_props(feature_id, feature):
fwp = json.loads(feature["geojson"])
fwp["properties"] = {
"id": feature_id,
"slug": f"{feature_id}-{slugify(feature['title'])}",
"title": feature["title"],
"description": feature["description"],
"image": feature["image"]
.get_rendition("fill-800x450|jpegquality-80")
.url,
"link": feature["link"],
"color": feature["hex_color"],
}
return fwp
context = super().get_context(value, parent_context)
context["js_map"] = {
"apikeys": json.dumps(_apikeys_from_parent_context(parent_context)),
"geojson": json.dumps(
{
# Dump individual features in collection
"type": "FeatureCollection",
"features": [
_geojson_feature_with_props(i, f)
for i, f in enumerate(value["features"])
],
}
),
}
return context
MAP_STYLES = (
("osm-mapnik", "OSM Mapnik"),
("stamen-toner", "Stamen Toner"),
("stamen-terrain", "Stamen Terrain"),
("stadia-osm-bright", "Stadia OSM Bright (vyžaduje API klíč)"),
("stadia-outdoors", "Stadia Outdoors (vyžaduje API klíč)"),
)
DEFAULT_MAP_STYLE = "osm-mapnik"
SUPPORTED_FEATURE_TYPES = ("Point", "Polygon", "LineString")
.marker-cluster {
border-radius: 50%;
background-clip: padding-box;
}
.marker-cluster div {
color: #fff;
font-family: Roboto;
font-weight: bold;
text-align: center;
font-size: 16px;
width: 40px;
height: 40px;
border-radius: 50%;
margin-left:0;
margin-top: 0;
}
.marker-cluster span {
line-height: 40px;
}
.marker-cluster-small,
.marker-cluster-medium,
.marker-cluster-small:hover,
.marker-cluster-medium:hover {
background-color: transparent;
}
.marker-cluster-small div,
.marker-cluster-medium div{
background-color: rgba(0, 0, 0, 0.7);
}
.marker-cluster-small:hover div,
.marker-cluster-medium:hover div {
background-color: rgba(0, 0, 0, 0.9);
}
.geo-feature-collection {
position: relative;
}
.geo-feature-collection__map-layer {
width: 100%;
z-index: -1;
}
.geo-feature-collection .modal__overlay {
z-index: 9999;
}
.modal__container-body {
position: relative;
}
.geo-feature-collection .modal__close {
position: absolute;
right: 0;
top: 0;
height: 2rem;
width: 2rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
margin: auto;
color: var(--color-black);
background: var(--color-white);
}
.geo-feature-collection .card__body {
position: relative;
}
.geo-feature-collection-item__category {
position: absolute;
left: 1rem;
top: -2rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
padding: .5rem 1rem 1rem;
background: var(--color-white);
}
.geo-feature-collection__legend {
padding: 1rem;
min-width: 18em;
background: var(--color-white);
/* border: 1px var(--color-grey-200) solid; */
border-radius: .5rem;
pointer-events: all;
}
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 40 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0.830095,-0.855097)">
<g transform="matrix(1.6,0,0,1.70554,-26.0301,-28.3709)">
<circle cx="28.25" cy="27.25" r="8.75" style="fill:#FFFFFF;"/>
<text text-anchor="middle" dominant-baseline="central" x="28.25" y="27.25" style="font-family:'Roboto-Bold', 'Roboto';font-weight:700;font-size:10px;">16</text>
</g>
<g transform="matrix(1,0,0,1,-0.830095,0.855097)">
<path d="M20,64L18,60L22,60L20,64ZM18,34.385C9.421,33.393 2.75,26.095 2.75,17.25C2.75,7.729 10.479,0 20,0C29.521,0 37.25,7.729 37.25,17.25C37.25,26.095 30.579,33.393 22,34.385L22,60L18,60L18,34.385ZM20,3.536C27.569,3.536 33.714,9.681 33.714,17.25C33.714,24.819 27.569,30.964 20,30.964C12.431,30.964 6.286,24.819 6.286,17.25C6.286,9.681 12.431,3.536 20,3.536Z" style="fill:#$FILLCOLOR$;"/>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 40 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0.830095,-0.855097)">
<g transform="matrix(1.4663,0,0,1.56302,-22.253,-24.4872)">
<ellipse cx="28.25" cy="27.25" rx="8.75" ry="8.25" style="fill:rgb(242,242,242);"/>
</g>
<g transform="matrix(1,0,0,1,-0.830095,0.855097)">
<path d="M20,64L18,60L22,60L20,64ZM18,34.385C9.421,33.393 2.75,26.095 2.75,17.25C2.75,7.729 10.479,0 20,0C29.521,0 37.25,7.729 37.25,17.25C37.25,26.095 30.579,33.393 22,34.385L22,60L18,60L18,34.385ZM20,3.536C27.569,3.536 33.714,9.681 33.714,17.25C33.714,24.819 27.569,30.964 20,30.964C12.431,30.964 6.286,24.819 6.286,17.25C6.286,9.681 12.431,3.536 20,3.536Z" style="fill:rgb(255,31,110);"/>
</g>
<g id="g4674" transform="matrix(0.631511,0,0,0.629547,3.63473,-0.246209)">
<path id="path4657" d="M24.7,7.2C18.8,7.2 13.3,9.5 9.2,13.6C5,17.7 2.7,23.3 2.7,29.1C2.7,35 5,40.5 9.1,44.7C13.3,48.8 18.8,51.1 24.6,51.1C30.5,51.1 36,48.8 40.1,44.7C44.3,40.6 46.5,35 46.5,29.2C46.5,23.3 44.2,17.8 40.1,13.7C36.1,9.4 30.6,7.2 24.7,7.2M24.7,49C13.7,49 4.8,40.1 4.8,29.1C4.8,18.1 13.7,9.2 24.7,9.2C35.7,9.2 44.6,18.1 44.6,29.1C44.6,40.1 35.7,49 24.7,49" style="fill-rule:nonzero;"/>
<path id="path4659" d="M18.1,16.1L18.1,13L16.2,13L16.2,16.6C14.9,17 14.1,17.4 14.3,17.7C14.7,17.6 15.4,17.5 16.2,17.6L16.2,36C14.2,39.8 17.1,45.7 17.1,45.7C17.1,45.7 15,39.4 19.7,36.4C24,33.7 39,34.9 38.9,26.5C38.8,14.6 25,14.6 18.1,16.1M24.2,27.7C23.5,30.9 20.1,32.5 18,33.9L18,17.8C21.5,18.6 25.6,21.3 24.2,27.7" style="fill-rule:nonzero;"/>
</g>
</g>
</svg>
<div
class="v-geo-feature-collection"
data-tile-style="{{ self.style }}"
data-wrapper-class="container-padding--zero lg:container-padding--auto"
data-initial-zoom="{{ self.zoom }}"
data-display-zoom-control="true"
data-display-legend="false"
data-display-popups="true"
data-height="{{ self.height }}px"
data-api-keys="{{ js_map.apikeys }}"
data-geojson="{{ js_map.geojson }}"
></div>
<div
class="v-geo-feature-collection"
data-tile-style="{{ self.style }}"
data-wrapper-class="container-padding--zero lg:container-padding--auto"
data-initial-zoom="{{ self.zoom }}"
data-display-zoom-control="false"
data-display-legend="false"
data-display-popups="false"
data-height="{{ self.height }}px"
data-api-keys="{{ js_map.apikeys }}"
data-geojson="{{ js_map.geojson }}"
></div>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 40 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0.830095,-0.855097)">
<g transform="matrix(1.6,0,0,1.70554,-26.0301,-28.3709)">
<circle cx="28.25" cy="27.25" r="8.75" style="fill:#FFFFFF;"/>
<text text-anchor="middle" dominant-baseline="central" x="28.25" y="27.25" style="font-family:'Roboto-Bold', 'Roboto';font-weight:700;font-size:10px;">$FILLNUMBER$</text>
</g>
<g transform="matrix(1,0,0,1,-0.830095,0.855097)">
<path d="M20,64L18,60L22,60L20,64ZM18,34.385C9.421,33.393 2.75,26.095 2.75,17.25C2.75,7.729 10.479,0 20,0C29.521,0 37.25,7.729 37.25,17.25C37.25,26.095 30.579,33.393 22,34.385L22,60L18,60L18,34.385ZM20,3.536C27.569,3.536 33.714,9.681 33.714,17.25C33.714,24.819 27.569,30.964 20,30.964C12.431,30.964 6.286,24.819 6.286,17.25C6.286,9.681 12.431,3.536 20,3.536Z" style="fill:#$FILLCOLOR$;"/>
</g>
</g>
</svg>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment