Skip to content
Snippets Groups Projects
  • xaralis's avatar
    4f6a81f1
    feat(maps_utils,district): Use FeatureCollection GeoJSON type by default · 4f6a81f1
    xaralis authored
    - All features in GeoFeatureDetailPage will be converted to FeatureCollection
    - Each collection may have multiple GeoJSON feature objects, each with
      its own properties. This enables us to provide customized title & description
      for individual features. You can set 'title' and 'description'
      properties and they will be used both in map and on detail page.
    - Each FeatureCollection sits on its own Leaflet layer.
    - Having each FeatureCollection on own layer enabled us to do precise
      muting support: layer will be completely removed/added which greatly
      helps to avoid visual clutter.
    - Muting support added to whole geo categories as well!
    - Fixed map collection block.
    4f6a81f1
    History
    feat(maps_utils,district): Use FeatureCollection GeoJSON type by default
    xaralis authored
    - All features in GeoFeatureDetailPage will be converted to FeatureCollection
    - Each collection may have multiple GeoJSON feature objects, each with
      its own properties. This enables us to provide customized title & description
      for individual features. You can set 'title' and 'description'
      properties and they will be used both in map and on detail page.
    - Each FeatureCollection sits on its own Leaflet layer.
    - Having each FeatureCollection on own layer enabled us to do precise
      muting support: layer will be completely removed/added which greatly
      helps to avoid visual clutter.
    - Muting support added to whole geo categories as well!
    - Fixed map collection block.
blocks.py 20.06 KiB
import logging
import re
import urllib

from django.core.files.images import ImageFile
from django.forms.utils import ErrorList
from wagtail.contrib.table_block.blocks import TableBlock
from wagtail.core import blocks
from wagtail.core.blocks.struct_block import StructBlockValidationError
from wagtail.core.models import Collection
from wagtail.images.blocks import ImageChooserBlock
from wagtail.images.models import Image

from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
from shared.const import RICH_TEXT_DEFAULT_FEATURES

logger = logging.getLogger(__name__)


class GalleryBlock(blocks.StructBlock):
    gallery_items = blocks.ListBlock(
        ImageChooserBlock(label="obrázek", required=True),
        label="Galerie",
        icon="image",
        group="ostatní",
    )

    class Meta:
        label = "Galerie"
        icon = "image"
        template = "styleguide/2.3.x/blocks/gallery_block.html"


class FigureBlock(blocks.StructBlock):
    img = ImageChooserBlock(label="Obrázek", required=True)
    caption = blocks.TextBlock(label="Popisek", required=False)

    class Meta:
        label = "Obrázek"
        icon = "image"
        template = "styleguide/2.3.x/blocks/figure_block.html"


class MenuItemBlock(blocks.StructBlock):
    title = blocks.CharBlock(label="Titulek", required=True)
    page = blocks.PageChooserBlock(label="Stránka", required=False)
    link = blocks.URLBlock(label="Odkaz", required=False)

    class Meta:
        label = "Položka v menu"
        template = "styleguide/2.3.x/menu_item.html"

    def clean(self, value):
        errors = {}

        if value["page"] and value["link"]:
            errors["page"] = ErrorList(
                ["Stránka nemůže být vybrána současně s odkazem."]
            )
            errors["link"] = ErrorList(
                ["Odkaz nemůže být vybrán současně se stránkou."]
            )
        if errors:
            raise StructBlockValidationError(errors)
        return super().clean(value)


class MenuParentBlock(blocks.StructBlock):
    title = blocks.CharBlock(label="Titulek", required=True)
    menu_items = blocks.ListBlock(
        MenuItemBlock(),
    )

    class Meta:
        label = "Podmenu"
        template = "styleguide/2.3.x/menu_parent.html"


class ProgramItemBlock(blocks.StructBlock):
    title = blocks.CharBlock(label="Název", required=True)
    completion_percentage = blocks.IntegerBlock(
        label="Procento dokončení", required=True
    )
    issue_link = blocks.URLBlock(label="Odkaz na Redmine issue", required=False)


class YouTubeVideoBlock(blocks.StructBlock):
    poster_image = ImageChooserBlock(
        label="Náhled videa (automatické pole)",
        required=False,
        help_text="Není třeba vyplňovat, náhled bude " "dohledán automaticky.",
    )
    video_url = blocks.URLBlock(
        label="Odkaz na video",
        required=False,
        help_text="Odkaz na YouTube video bude automaticky "
        "zkonvertován na ID videa a NEBUDE uložen.",
    )
    video_id = blocks.CharBlock(
        label="ID videa (automatické pole)",
        required=False,
        help_text="Není třeba vyplňovat, bude automaticky " "načteno z odkazu.",
    )

    class Meta:
        label = "YouTube video"
        icon = "media"
        template = "styleguide/2.3.x/blocks/video_block.html"

    def clean(self, value):
        errors = {}

        if not value["video_url"] and not value["video_id"]:
            errors["video_url"] = ErrorList(["Zadejte prosím odkaz na YouTube video."])

        if value["video_url"]:
            if not value["video_url"].startswith("https://youtu.be") and not value[
                "video_url"
            ].startswith("https://www.youtube.com"):
                errors["video_url"] = ErrorList(
                    [
                        'Odkaz na video musí začínat "https://www.youtube.com" '
                        'nebo "https://youtu.be"'
                    ]
                )

        if value["video_id"]:
            if not re.match("^[A-Za-z0-9_-]{11}$", value["video_id"]):
                errors["video_url"] = ErrorList(
                    ["Formát ID YouTube videa není validní"]
                )

        if errors:
            raise StructBlockValidationError(errors)
        return super().clean(value)

    def get_prep_value(self, value):
        value = super().get_prep_value(value)

        if value["video_url"]:
            value["video_id"] = self.convert_youtube_link_to_video_id(
                value["video_url"]
            )
            value["poster_image"] = self.get_wagtail_image_id_for_youtube_poster(
                value["video_id"]
            )

        value["video_url"] = ""

        return value

    @staticmethod
    def convert_youtube_link_to_video_id(url):
        reg_str = (
            "((?<=(v|V)/)|(?<=youtu\.be/)|(?<=youtube\.com/watch(\?|\&)v=)"
            "|(?<=embed/))([\w-]+)"
        )
        search_result = re.search(reg_str, url)

        if search_result:
            return search_result.group(0)

        logger.warning(
            "Nepodařilo se získat video ID z YouTube URL", extra={"url": url}
        )
        return ""

    @classmethod
    def get_wagtail_image_id_for_youtube_poster(cls, video_id) -> int or None:
        image_url = "https://img.youtube.com/vi/{}/hqdefault.jpg".format(video_id)

        img_path = "/tmp/{}.jpg".format(video_id)
        urllib.request.urlretrieve(image_url, img_path)
        file = ImageFile(open(img_path, "rb"), name=img_path)

        return cls.get_image_id(file, "YT_poster_v_{}".format(video_id))

    @classmethod
    def get_image_id(cls, file: ImageFile, image_title: str) -> int:
        try:
            image = Image.objects.get(title=image_title)
        except Image.DoesNotExist:
            image = Image(
                title=image_title, file=file, collection=cls.get_posters_collection()
            )
            image.save()
        return image.id

    @staticmethod
    def get_posters_collection() -> Collection:
        collection_name = "YouTube nahledy"

        try:
            collection = Collection.objects.get(name=collection_name)
        except Collection.DoesNotExist:
            root_collection = Collection.get_first_root_node()
            collection = root_collection.add_child(name=collection_name)

        return collection


class CardBlock(blocks.StructBlock):
    img = ImageChooserBlock(label="Obrázek", required=False)
    elevation = blocks.IntegerBlock(
        label="Velikost stínu",
        min_value=0,
        max_value=21,
        help_text="0 = žádný stín, 21 = maximální stín",
        default=2,
    )
    headline = blocks.TextBlock(label="Titulek", required=False)
    hoveractive = blocks.BooleanBlock(
        label="Zvýraznit stín na hover",
        default=False,
        help_text="Pokud je zapnuto, stín se zvýrazní, když na kartu uživatel najede myší.",
        required=False,
    )
    content = blocks.StreamBlock(
        label="Obsah",
        local_blocks=[
            (
                "text",
                blocks.RichTextBlock(
                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
                ),
            ),
            ("table", TableBlock(template="shared/blocks/table_block.html")),
            ("figure", FigureBlock()),
            ("youtube", YouTubeVideoBlock()),
            ("map_point", MapPointBlock(label="Špendlík na mapě")),
            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
        ],
        required=False,
    )
    page = blocks.PageChooserBlock(label="Stránka", required=False)
    link = blocks.URLBlock(label="Odkaz", required=False)

    class Meta:
        label = "Karta"
        icon = "form"
        template = "styleguide/2.3.x/blocks/card_block.html"

    def clean(self, value):
        errors = {}

        if value["page"] and value["link"]:
            errors["page"] = ErrorList(
                ["Stránka nemůže být vybrána současně s odkazem."]
            )
            errors["link"] = ErrorList(
                ["Odkaz nemůže být vybrán současně se stránkou."]
            )
        if errors:
            raise StructBlockValidationError(errors)

        return super().clean(value)


class ButtonBlock(blocks.StructBlock):
    title = blocks.CharBlock(label="Titulek", max_length=128, required=True)
    icon = blocks.CharBlock(
        label="Ikonka",
        max_length=128,
        required=False,
        help_text="Identifikátor ikonky ze styleguide (https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons), např. ico--key.",
    )
    size = blocks.ChoiceBlock(
        choices=(("sm", "Malá"), ("base", "Střední"), ("lg", "Velká")),
        label="Velikost",
        default="base",
    )
    color = blocks.ChoiceBlock(
        choices=(
            ("black", "Černá"),
            ("white", "Bílá"),
            ("grey-125", "Světle šedá"),
            ("blue-300", "Modrá"),
            ("cyan-200", "Tyrkysová"),
            ("green-400", "Zelené"),
            ("violet-400", "Vínová"),
            ("red-600", "Červená"),
        ),
        label="Barva",
        default="base",
    )
    hoveractive = blocks.BooleanBlock(
        label="Animovat na hover",
        default=True,
        help_text="Pokud je zapnuto, tlačítko mění barvu, když na něj uživatel najede myší.",
        required=False,
    )
    mobile_fullwidth = blocks.BooleanBlock(
        label="Plná šířka na mobilních zařízeních",
        default=True,
        help_text="Pokud je zapnuto, tlačítko se na mobilních zařízeních roztáhne na plnou šířku.",
        required=False,
    )
    page = blocks.PageChooserBlock(label="Stránka", required=False)
    link = blocks.URLBlock(label="Odkaz", required=False)
    align = blocks.ChoiceBlock(
        choices=(
            ("auto", "Automaticky"),
            ("center", "Na střed"),
        ),
        label="Zarovnání",
        default="auto",
    )

    class Meta:
        label = "Tlačítko"
        icon = "code"
        template = "styleguide/2.3.x/blocks/button_block.html"

    def clean(self, value):
        errors = {}

        if value["page"] and value["link"]:
            errors["page"] = ErrorList(
                ["Stránka nemůže být vybrána současně s odkazem."]
            )
            errors["link"] = ErrorList(
                ["Odkaz nemůže být vybrán současně se stránkou."]
            )

        if not value["page"] and not value["link"]:
            errors["page"] = ErrorList(["Stránka nebo odkaz musí být vyplněna."])
            errors["link"] = ErrorList(["Stránka nebo odkaz musí být vyplněna."])

        if errors:
            raise StructBlockValidationError(errors)

        return super().clean(value)


class ButtonGroupBlock(blocks.StructBlock):
    buttons = blocks.ListBlock(ButtonBlock(), label="Tlačítka")

    class Meta:
        label = "Skupina tlačítek"
        icon = "list-ul"
        template = "styleguide/2.3.x/blocks/button_group_block.html"


class HeadlineBlock(blocks.StructBlock):
    headline = blocks.CharBlock(label="Headline", max_length=300, required=True)
    style = blocks.ChoiceBlock(
        choices=(
            ("head-alt-xl", "Bebas XL"),
            ("head-alt-lg", "Bebas L"),
            ("head-alt-md", "Bebas M"),
            ("head-alt-base", "Bebas base"),
            ("head-alt-sm", "Bebas SM"),
            ("head-alt-xs", "Bebas XS"),
            ("head-alt-2xs", "Bebas 2XS"),
            ("head-heavy-base", "Roboto base"),
            ("head-heavy-sm", "Roboto SM"),
            ("head-heavy-xs", "Roboto XS"),
            ("head-heavy-2xs", "Roboto 2XS"),
            ("head-allcaps-2xs", "Allcaps 2XS"),
            ("head-allcaps-3xs", "Allcaps 3XS"),
            ("head-allcaps-4xs", "Allcaps 4XS"),
            ("head-heavy-allcaps-2xs", "Allcaps heavy 2XS"),
            ("head-heavy-allcaps-3xs", "Allcaps heavy 3XS"),
            ("head-heavy-allcaps-4xs", "Allcaps heavy 4XS"),
        ),
        label="Styl",
        help_text="Náhled si prohlédněte na https://styleguide.pir-test.eu/latest/?p=viewall-atoms-text.",
        default="head-alt-md",
        required=True,
    )
    tag = blocks.ChoiceBlock(
        choices=(
            ("h1", "H1"),
            ("h2", "H2"),
            ("h3", "H3"),
            ("h4", "H4"),
            ("h5", "H5"),
            ("h6", "H6"),
        ),
        label="Úroveň nadpisu",
        help_text="Čím nižší číslo, tím vyšší úroveň.",
        required=True,
        default="h1",
    )
    align = blocks.ChoiceBlock(
        choices=(
            ("auto", "Automaticky"),
            ("center", "Na střed"),
        ),
        label="Zarovnání",
        default="auto",
        required=True,
    )

    def get_context(self, value, parent_context=None):
        context = super().get_context(value, parent_context)
        context["responsive_style"] = {
            "head-alt-xl": "head-alt-lg md:head-alt-xl",
            "head-alt-lg": "head-alt-md md:head-alt-lg",
            "head-alt-md": "head-alt-md",
            "head-alt-base": "head-alt-base",
            "head-alt-sm": "head-alt-sm",
            "head-alt-xs": "head-alt-xs",
            "head-alt-2xs": "head-alt-2xs",
            "head-heavy-base": "head-heavy-base",
            "head-heavy-sm": "head-heavy-sm",
            "head-heavy-xs": "head-heavy-xs",
            "head-heavy-2xs": "head-heavy-2xs",
            "head-allcaps-2xs": "head-allcaps-2xs",
            "head-allcaps-3xs": "head-allcaps-3xs",
            "head-allcaps-4xs": "head-allcaps-4xs",
            "head-heavy-allcaps-2xs": "head-heavy-allcaps-2xs",
            "head-heavy-allcaps-3xs": "head-heavy-allcaps-3xs",
            "head-heavy-allcaps-4xs": "head-heavy-allcaps-4xs",
        }[value["style"]]
        return context

    class Meta:
        label = "Headline"
        icon = "bold"
        template = "styleguide/2.3.x/blocks/headline_block.html"


class TwoColumnBlock(blocks.StructBlock):
    left_column_content = blocks.StreamBlock(
        label="Obsah levého sloupce",
        local_blocks=[
            (
                "text",
                blocks.RichTextBlock(
                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
                ),
            ),
            ("table", TableBlock(template="shared/blocks/table_block.html")),
            ("card", CardBlock()),
            ("figure", FigureBlock()),
            ("youtube", YouTubeVideoBlock()),
            ("map_point", MapPointBlock(label="Špendlík na mapě")),
            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
            ("button", ButtonBlock()),
            ("button_group", ButtonGroupBlock()),
        ],
        required=True,
    )
    right_column_content = blocks.StreamBlock(
        label="Obsah pravého sloupce",
        local_blocks=[
            (
                "text",
                blocks.RichTextBlock(
                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
                ),
            ),
            ("table", TableBlock(template="shared/blocks/table_block.html")),
            ("card", CardBlock()),
            ("figure", FigureBlock()),
            ("youtube", YouTubeVideoBlock()),
            ("map_point", MapPointBlock(label="Špendlík na mapě")),
            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
            ("button", ButtonBlock()),
            ("button_group", ButtonGroupBlock()),
        ],
        required=True,
    )

    class Meta:
        label = "Dva sloupce"
        icon = "grip"
        template = "styleguide/2.3.x/blocks/two_column_block.html"


class ThreeColumnBlock(blocks.StructBlock):
    left_column_content = blocks.StreamBlock(
        label="Obsah levého sloupce",
        local_blocks=[
            (
                "text",
                blocks.RichTextBlock(
                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
                ),
            ),
            ("table", TableBlock(template="shared/blocks/table_block.html")),
            ("card", CardBlock()),
            ("figure", FigureBlock()),
            ("youtube", YouTubeVideoBlock()),
            ("map_point", MapPointBlock(label="Špendlík na mapě")),
            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
            ("button", ButtonBlock()),
            ("button_group", ButtonGroupBlock()),
        ],
        required=True,
    )
    middle_column_content = blocks.StreamBlock(
        label="Obsah prostředního sloupce",
        local_blocks=[
            (
                "text",
                blocks.RichTextBlock(
                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
                ),
            ),
            ("table", TableBlock(template="shared/blocks/table_block.html")),
            ("card", CardBlock()),
            ("figure", FigureBlock()),
            ("youtube", YouTubeVideoBlock()),
            ("map_point", MapPointBlock(label="Špendlík na mapě")),
            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
            ("button", ButtonBlock()),
            ("button_group", ButtonGroupBlock()),
        ],
        required=True,
    )
    right_column_content = blocks.StreamBlock(
        label="Obsah pravého sloupce",
        local_blocks=[
            (
                "text",
                blocks.RichTextBlock(
                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
                ),
            ),
            ("table", TableBlock(template="shared/blocks/table_block.html")),
            ("card", CardBlock()),
            ("figure", FigureBlock()),
            ("youtube", YouTubeVideoBlock()),
            ("map_point", MapPointBlock(label="Špendlík na mapě")),
            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
            ("button", ButtonBlock()),
            ("button_group", ButtonGroupBlock()),
        ],
        required=True,
    )

    class Meta:
        label = "Tři sloupce"
        icon = "grip"
        template = "styleguide/2.3.x/blocks/three_column_block.html"


class ImageBannerBlock(blocks.StructBlock):
    image = ImageChooserBlock(
        label="Obrázek",
        required=True,
    )
    headline = blocks.CharBlock(label="Headline", max_length=128, required=True)
    content = blocks.StreamBlock(
        label="Obsah pravého sloupce",
        local_blocks=[
            (
                "text",
                blocks.RichTextBlock(
                    label="Textový editor",
                    features=(
                        "h2",
                        "h3",
                        "h4",
                        "h5",
                        "bold",
                        "italic",
                        "ol",
                        "ul",
                        "hr",
                        "link",
                        "document-link",
                        "superscript",
                        "subscript",
                        "strikethrough",
                        "blockquote",
                    ),
                ),
            ),
            ("button", ButtonBlock()),
            ("button_group", ButtonGroupBlock()),
        ],
        required=False,
    )

    class Meta:
        label = "Obrázkový banner"
        icon = "image"
        template = "styleguide/2.3.x/blocks/image_banner_block.html"


DEFAULT_CONTENT_BLOCKS = [
    (
        "text",
        blocks.RichTextBlock(
            label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
        ),
    ),
    ("headline", HeadlineBlock()),
    ("table", TableBlock(template="shared/blocks/table_block.html")),
    ("gallery", GalleryBlock(label="Galerie")),
    ("figure", FigureBlock()),
    ("card", CardBlock()),
    ("two_columns", TwoColumnBlock()),
    ("three_columns", ThreeColumnBlock()),
    ("youtube", YouTubeVideoBlock(label="YouTube video")),
    ("map_point", MapPointBlock(label="Špendlík na mapě")),
    ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
    ("button", ButtonBlock()),
    ("button_group", ButtonGroupBlock()),
    ("image_banner", ImageBannerBlock()),
]