diff --git a/district/migrations/0240_alter_districthomepage_content.py b/district/migrations/0240_alter_districthomepage_content.py index 787a77c6f0aa3aaf71dd01e37feaf2498e6af071..7142e4c37b912acce8a68b5f01215c33dac3ad17 100644 --- a/district/migrations/0240_alter_districthomepage_content.py +++ b/district/migrations/0240_alter_districthomepage_content.py @@ -10,7 +10,7 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('district', '0239_districthomepage_custom_css'), + ('district', '0240_alter_districthomepage_content'), ] operations = [ diff --git a/district/migrations/0239_alter_districtarticlepage_content_and_more.py b/district/migrations/0241_alter_districtarticlepage_content_and_more.py similarity index 100% rename from district/migrations/0239_alter_districtarticlepage_content_and_more.py rename to district/migrations/0241_alter_districtarticlepage_content_and_more.py diff --git a/district/migrations/0240_alter_districtarticlepage_content_and_more.py b/district/migrations/0242_alter_districtarticlepage_content_and_more.py similarity index 99% rename from district/migrations/0240_alter_districtarticlepage_content_and_more.py rename to district/migrations/0242_alter_districtarticlepage_content_and_more.py index 813508f879570151c8d465eb2f307ccff8777f97..c74b8e265341ccc67f7aaa73c10459ce1c2cacf6 100644 --- a/district/migrations/0240_alter_districtarticlepage_content_and_more.py +++ b/district/migrations/0242_alter_districtarticlepage_content_and_more.py @@ -12,7 +12,7 @@ import shared.blocks.base class Migration(migrations.Migration): dependencies = [ - ("district", "0239_alter_districtarticlepage_content_and_more"), + ("district", "0241_alter_districtarticlepage_content_and_more"), ] operations = [ diff --git a/district/migrations/0241_auto_20240701_1616.py b/district/migrations/0243_auto_20240701_1616.py similarity index 88% rename from district/migrations/0241_auto_20240701_1616.py rename to district/migrations/0243_auto_20240701_1616.py index 02770a03e25a41b1868000a4f154bada564e1b6c..52e5434ed6f2d0e0d1edaf844c7af1b92a43f20c 100644 --- a/district/migrations/0241_auto_20240701_1616.py +++ b/district/migrations/0243_auto_20240701_1616.py @@ -17,7 +17,7 @@ def fix_menu(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('district', '0240_alter_districthomepage_content'), + ('district', '0242_alter_districthomepage_content'), ] operations = [ diff --git a/shared/blocks/__init__.py b/shared/blocks/__init__.py index d1bcf2914596f973d680748e03c2ee796e67842c..0e97324baf18f3d90f7dd9425efcec9ef07b4e50 100644 --- a/shared/blocks/__init__.py +++ b/shared/blocks/__init__.py @@ -1,3 +1,34 @@ from .parents import * # noqa from .children import * # noqa -from .base import * # noqa \ No newline at end of file + + +DEFAULT_CONTENT_BLOCKS = [ + ( + "text", + blocks.RichTextBlock( + label="Textový editor", + features=RICH_TEXT_DEFAULT_FEATURES, + template="styleguide2/includes/atoms/text/prose_richtext.html", + ), + ), + ("advanced_text", AdvancedTextBlock()), + ("two_columns_text", ColumnsTextBlock()), + ("headline", HeadlineBlock()), + ("headline_with_picture", PictureHeadlineBlock()), + ( + "table", + TableBlock( + template="styleguide2/includes/atoms/table/table.html", label="Tabulka" + ), + ), + ("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()), +] \ No newline at end of file diff --git a/shared/blocks/base.py b/shared/blocks/base.py deleted file mode 100644 index 5ab5a34524cff8595ed15c2b9807ebfa333f3c16..0000000000000000000000000000000000000000 --- a/shared/blocks/base.py +++ /dev/null @@ -1,1111 +0,0 @@ -class MenuItemBlock(blocks.StructBlock): - title = blocks.CharBlock( - label="Titulek", - help_text="Pokud není odkazovaná stránka na Majáku, použij možnost zadání samotné adresy níže.", - 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(), label="Položky menu") - - class Meta: - label = "Vyskakovací menu" - template = "styleguide2/includes/molecules/dropdown/dropdown.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 = "styleguide2/includes/atoms/youtube_video/youtube_video.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) - - headline = blocks.TextBlock(label="Titulek", required=False) - - content = blocks.StreamBlock( - label="Obsah", - local_blocks=[ - ( - "text", - blocks.RichTextBlock( - label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES - ), - ), - ( - "table", - TableBlock( - template="styleguide2/includes/atoms/table/table.html", - label="Tabulka", - ), - ), - ("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 = "styleguide2/includes/molecules/boxes/card_box_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) - - color = blocks.ChoiceBlock( - choices=( - ("black", "Černá"), - ("white", "Bílá"), - ("pirati-yellow", "Žlutá"), - ("grey-125", "Světle šedá"), - ("blue-300", "Modrá"), - ("cyan-200", "Tyrkysová"), - ("green-400", "Zelená"), - ("violet-400", "Vínová"), - ("red-600", "Červená"), - ), - label="Barva", - default="black", - ) - - hoveractive = blocks.BooleanBlock( - label="Animovat na hover", - default=True, - help_text="Pokud je zapnuto, tlačítko při najetí kurzorem ukáže žlutou šipku.", - 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 = "styleguide2/includes/atoms/buttons/round_button_block.html" - - def get_context(self, value, parent_context=None): - context = super().get_context(value, parent_context) - - context["background_color"] = f"bg-{value['color']}" - - context["text_color"] = ( - "text-white" - if value["color"] - in ( - "black", - "red-600", - "blue-300", - "cyan-200", - "green-400", - "violet-400", - "red-600", - ) - else "text-black" - ) - - context[ - "color_classes" - ] = f"{context['background_color']} {context['text_color']}" - - return context - - 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 value["hoveractive"] and value["color"] not in ( - "black", - "white", - "grey-125", - ): - errors["hoveractive"] = ErrorList( - ["Šipku lze ukazovat pouze s černým, bílým nebo šedým pozadím."] - ) - - 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 = "styleguide2/includes/atoms/buttons/round_button_group_block.html" - - -class FullSizeHeaderBlock(blocks.StructBlock): - title = blocks.CharBlock(label="Titulek", required=True) - image_background = ImageChooserBlock(label="Obrázek v pozadí", required=True) - image_foreground = ImageChooserBlock(label="Obrázek v popředí", required=False) - button_group = blocks.ListBlock(ButtonBlock(), label="Tlačítka") - - class Meta: - template = "styleguide/2.3.x/blocks/full_size_header_block.html" - icon = "placeholder" - label = "Nadpis s obrázkem a tlačítky přes celou stránku" - - -class HeadlineBlock(blocks.StructBlock): - headline = blocks.CharBlock(label="Nadpis", max_length=300, 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", - ) - - style = blocks.ChoiceBlock( - choices=( - ("head-alt-xl", "Velký, Bebas Neue - 6XL"), - ("head-alt-lg", "Střední, Bebas Neue - 4XL"), - ("head-alt-md", "Základní velikost - Roboto - MD"), - ("head-alt-sm", "Malý - Roboto - SM"), - ("head-alt-xs", "Extra malý - Roboto - XS"), - ), - label="Velikost", - help_text="Náhled si prohlédněte na https://styleguide2.pirati.cz/pattern/patterns/atoms/text/headings.html.", - default="head-alt-xl", - required=True, - ) - - 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-6xl", - "head-alt-lg": "head-4xl", - "head-alt-md": "head-base", - "head-alt-sm": "head-sm", - "head-alt-xs": "head-xs", - }.get(value["style"], "head-4xl") - - return context - - class Meta: - label = "Nadpis" - icon = "bold" - template = "styleguide2/includes/atoms/text/heading.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="styleguide2/includes/atoms/table/table.html", - label="Tabulka", - ), - ), - ("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="styleguide2/includes/atoms/table/table.html", - label="Tabulka", - ), - ), - ("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 = "styleguide2/includes/atoms/grids/two_columns.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="styleguide2/includes/atoms/table/table.html", - label="Tabulka", - ), - ), - ("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="styleguide2/includes/atoms/table/table.html", - label="Tabulka", - ), - ), - ("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="styleguide2/includes/atoms/table/table.html", - label="Tabulka", - ), - ), - ("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 = "styleguide2/includes/atoms/grids/three_columns.html" - - -class CardLinkBlockMixin(blocks.StructBlock): - """ - Shared mixin for cards, which vary in templates and the types of pages - they're allowed to link to. - """ - - image = ImageChooserBlock(label="Obrázek") - title = blocks.CharBlock(label="Titulek", required=True) - text = blocks.RichTextBlock(label="Krátký text pod nadpisem", required=False) - - page = blocks.PageChooserBlock( - label="Stránka", - page_type=[], - required=False, - ) - link = blocks.URLBlock(label="Odkaz", required=False) - - class Meta: - # template = "" - icon = "link" - label = "Karta s odkazem" - - 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."] - ) - elif not value["page"] and not value["link"]: - errors["page"] = ErrorList(["Zvolte stránku nebo vyplňte odkaz."]) - errors["link"] = ErrorList(["Vyplňte odkaz nebo zvolte stránku."]) - if errors: - raise StructBlockValidationError(errors) - return super().clean(value) - - -class CardLinkWithHeadlineBlockMixin(blocks.StructBlock): - headline = blocks.CharBlock(label="Titulek bloku", required=False) - card_items = blocks.ListBlock(CardLinkBlockMixin(), label="Karty s odkazy") - - class Meta: - # template = "" - icon = "link" - label = "Karty odkazů s nadpisem" - - -class ChartDataset(blocks.StructBlock): - label = blocks.CharBlock( - label="Označení zdroje dat", - max_length=120, - ) - - data = blocks.ListBlock( - blocks.IntegerBlock(), - label="Data", - default=[0], - ) - - class Meta: - label = "Zdroj dat" - - -def get_redmine_projects(): - projects = requests.get("https://redmine.pirati.cz/projects.json?limit=10000") - - if projects.ok: - projects = projects.json()["projects"] - else: - projects = [] - - return [(project["id"], project["name"]) for project in projects] - - -class ChartRedmineIssueDataset(blocks.StructBlock): - projects = blocks.MultipleChoiceBlock( - label="Projekty", choices=get_redmine_projects - ) - - is_open = blocks.BooleanBlock( - label="Jen otevřené", - required=False, - ) - is_closed = blocks.BooleanBlock( - label="Jen uzavřené", - required=False, - ) - - created_on_min_date = blocks.DateBlock(label="Min. datum vytvoření", required=True) - created_on_max_date = blocks.DateBlock(label="Max. datum vytvoření", required=True) - updated_on = blocks.CharBlock( - label="Filtr pro datum aktualizace", - max_length=128, - help_text="Např. <=2023-01-01. Více informací na pi2.cz/redmine-api", - required=False, - ) - - issue_label = blocks.CharBlock( - label="Označení úkolů uvnitř grafu", - max_length=128, - required=True, - ) - - split_per_project = blocks.BooleanBlock( - label="Rozdělit podle projektu", - required=False, - ) - - only_grow = blocks.BooleanBlock( - label="Pouze růst nahoru", - required=False, - ) - - def _get_issues_url( - self, value, project_id: typing.Union[None, str, list[str]] = None - ): - url = "https://redmine.pirati.cz/issues.json" - params = [ - ("sort", "created_on"), - ("limit", "100"), - ( - "created_on", - f"><{value['created_on_min_date']}|{value['created_on_max_date']}", - ), - ] - - if isinstance(project_id, str): - params.append(("project_id", project_id)) - elif isinstance(project_id, list): - params.append(("project_id", ",".join(project_id))) - - is_open = value.get("is_open", False) - is_closed = value.get("is_closed", False) - - if is_open and is_closed: - params.append(("status_id", "*")) - elif is_open: - params.append(("status_id", "open")) - elif is_closed: - params.append(("status_id", "closed")) - - if value.get("updated_on", "") != "": - params.append(("updated_on", value["updated_on"])) - - is_first = True - - for param_set in params: - param, param_value = param_set - - url += "?" if is_first else "&" - url += f"{param}={urllib.parse.quote(param_value)}" - - is_first = False - - return url - - def _get_parsed_issues(self, value, labels, issues_url) -> tuple: - issues_response = cache.get(f"redmine_{issues_url}") - - if issues_response is None: - issues_response = requests.get(issues_url) - issues_response.raise_for_status() - issues_response = issues_response.json() - - cache.set( - f"redmine_{issues_url}", - issues_response, - timeout=604800, # 1 week - ) - - only_grow = value.get("only_grow", False) - - collected_issues = issues_response["issues"] - offset = 0 - - while issues_response["total_count"] - offset > len(issues_response["issues"]): - offset += 100 - url_with_offset = f"{issues_url}&offset={offset}" - - issues_response = cache.get(f"redmine_{url_with_offset}") - - if issues_response is None: - issues_response = requests.get(url_with_offset) - issues_response.raise_for_status() - issues_response = issues_response.json() - - cache.set( - f"redmine_{url_with_offset}", - issues_response, - timeout=604800, # 1 week - ) - - collected_issues += issues_response["issues"] - - ending_position = len(collected_issues) - 1 - - data = None - - current_issue_count = 0 - current_label = datetime.date.fromisoformat( - collected_issues[0]["created_on"].split("T")[0] - ) - - if not only_grow: - data = [0] * len(labels) - - for position, issue in enumerate( - collected_issues - ): # Assume correct sorting order - created_on_date = datetime.date.fromisoformat( - issue["created_on"].split("T")[0] - ) - - if current_label != created_on_date or position == ending_position: - data[ - labels.index(current_label) - ] = current_issue_count # Assume labels are unique - current_label = created_on_date - - if position != ending_position: - current_issue_count = 0 - else: - data[labels.index(current_label)] = 1 - break - - current_issue_count += 1 - else: - data = [] - issue_count_by_date = {} - - for position, issue in enumerate( - collected_issues - ): # Assume correct sorting order - created_on_date = datetime.date.fromisoformat( - issue["created_on"].split("T")[0] - ) - - if current_label not in issue_count_by_date: - issue_count_by_date[current_label] = 0 - - if current_label != created_on_date or position == ending_position: - issue_count_by_date[ - current_label - ] = current_issue_count # Assume labels are unique - current_label = created_on_date - - if position == ending_position: - issue_count_by_date[current_label] = current_issue_count + 1 - break - - current_issue_count += 1 - - previous_date = None - - for date in labels: - if date not in issue_count_by_date: - if previous_date is None: - data.append(0) - continue - - data.append(issue_count_by_date[previous_date]) - continue - - data.append(issue_count_by_date[date]) - previous_date = date - - return data - - def get_context(self, value) -> list: - context = super().get_context(value) - - labels = [] - datasets = [] - - for day_count in range( - (value["created_on_max_date"] - value["created_on_min_date"]).days + 1 - ): - day = value["created_on_min_date"] + datetime.timedelta(days=day_count) - labels.append(day) - - if value.get("split_per_project", False): - project_choices_lookup = dict(get_redmine_projects()) - - for project_id in value["projects"]: - issues_url = self._get_issues_url(value, project_id) - - datasets.append( - { - "label": f"{value['issue_label']} - {project_choices_lookup[int(project_id)]}", - "data": self._get_parsed_issues(value, labels, issues_url), - } - ) - else: - issues_url = self._get_issues_url(value, value["projects"]) - - datasets.append( - { - "label": value["issue_label"], - "data": self._get_parsed_issues(value, labels, issues_url), - } - ) - - labels = [date.strftime("%d. %m. %Y") for date in labels] - - context["parsed_issue_labels"] = labels - context["parsed_issues"] = datasets - - return context - - class Meta: - label = "Zdroj dat z Redmine (úkoly vytvořené za den)" - help_text = ( - "Po prvním otevření se bude stránka otevírat delší dobu, " - "zatímco se na pozadí načítají data do grafu. Poté bude " - "fungovat běžně." - ) - - -class ChartBlock(blocks.StructBlock): - title = blocks.CharBlock( - label="Název", - max_length=120, - ) - chart_type = blocks.ChoiceBlock( - label="Typ", - choices=[ - ("bar", "Graf se sloupci"), - ("horizontalBar", "Graf s vodorovnými sloupci"), - ("pie", "Koláčový graf"), - ("doughnut", "Donutový graf"), - ("polarArea", "Graf polární oblasti"), - ("radar", "Radarový graf"), - ("line", "Graf s liniemi"), - ], - default="bar", - ) - - hide_points = blocks.BooleanBlock( - label="Schovat body", - required=False, - help_text="Mění vzhled pouze u linových grafů.", - ) - - local_labels = blocks.ListBlock( - blocks.CharBlock( - max_length=40, - label="Skupina", - ), - default=[], - blank=True, - required=False, - collapsed=True, - label="Místně definované skupiny", - ) - local_datasets = blocks.ListBlock( - ChartDataset(), - default=[], - blank=True, - required=False, - collapsed=True, - label="Místní zdroje dat", - ) - - redmine_issue_datasets = blocks.ListBlock( - ChartRedmineIssueDataset(label="Redmine úkoly"), - default=[], - blank=True, - required=False, - label="Zdroje dat z Redmine (úkoly)", - help_text=( - "Úkoly, podle doby vytvoření. Pokud definuješ " - "více zdrojů, datumy v nich musí být stejné." - ), - ) - - def clean(self, value): - result = super().clean(value) - - redmine_issues_exist = len(value.get("redmine_issue_datasets", [])) != 0 - - if len(value.get("local_datasets", [])) != 0 and redmine_issues_exist: - raise ValidationError( - "Definuj pouze jeden typ zdroje dat - místní, nebo z Redmine." - ) - - if redmine_issues_exist: - min_date = value["redmine_issue_datasets"][0]["created_on_min_date"] - max_date = value["redmine_issue_datasets"][0]["created_on_max_date"] - - if len(value["redmine_issue_datasets"]) > 1: - for dataset in value["redmine_issue_datasets"]: - if ( - dataset["created_on_min_date"] != min_date - or dataset["created_on_max_date"] != max_date - ): - raise ValidationError( - "Maximální a minimální data všech zdrojů z Redmine musí být stejné" - ) - - return result - - def get_context(self, value, parent_context=None): - context = super().get_context(value, parent_context=parent_context) - - datasets = [] - labels = [] - - if len(value["local_datasets"]) != 0: - labels = value["local_labels"] - - for dataset in value["local_datasets"]: - datasets.append( - { - "label": dataset["label"], - "data": [item for item in dataset["data"]], - } - ) - elif len(value["redmine_issue_datasets"]) != 0: - for dataset_wrapper in value["redmine_issue_datasets"]: - redmine_context = ChartRedmineIssueDataset().get_context( - dataset_wrapper - ) - - labels = redmine_context["parsed_issue_labels"] - datasets += redmine_context["parsed_issues"] - - value["datasets"] = json.dumps(datasets) - value["labels"] = json.dumps([label for label in labels]) - - return context - - class Meta: - template = "styleguide2/includes/molecules/blocks/chart.html" - label = "Graf" - icon = "form" - help_text = """Všechny položky zdrojů dat se chovají jako sloupce. -Zobrazí se tolik definovaných sloupců, kolik existuje skupin.""" - - -class NewsletterSubscriptionBlock(blocks.StructBlock): - list_id = blocks.CharBlock(label="ID newsletteru", required=True) - - title_line_1 = blocks.CharBlock( - label = "Nadpis bloku (1. řádek)", - required=True, - default="Odebírej náš" - ) - - title_line_2 = blocks.CharBlock( - label = "Nadpis bloku (2. řádek)", - required=True, - default="newsletter" - ) - - description = blocks.CharBlock( - label="Popis newsletteru", - required=True, - default="Fake news tam nenajdeš, ale dozvíš se, co chystáme doopravdy!", - ) - - class Meta: - label = "Formulář pro odebírání newsletteru" - icon = "form" - template = "styleguide2/includes/organisms/main_section/newsletter_section.html" - - -class AdvancedTextBlock(ColorBlock, AlignBlock): - text = blocks.RichTextBlock( - label="Textový editor", - features=RICH_TEXT_DEFAULT_FEATURES, - ) - - class Meta: - label = "Textový editor (pokročilý)" - icon = "doc-full" - template = "styleguide2/includes/atoms/text/prose_advanced_richtext.html" - - -class PictureHeadlineBlock(ColorBlock): - title = blocks.CharBlock(label="nadpis") - picture = ImageChooserBlock( - label="obrázek", - help_text="rozměr na výšku 75px nebo více (obrázek bude zmenšen na výšku 75px)", - ) - - class Meta: - label = "Nadpis (s obrázkem)" - icon = "title" - template = "styleguide2/includes/atoms/text/heading_with_image.html" - - -class ColumnsTextBlock(blocks.StructBlock): - left_text = blocks.RichTextBlock( - label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES - ) - right_text = blocks.RichTextBlock( - label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES - ) - - class Meta: - label = "Text dva sloupce" - icon = "doc-full" - template = "styleguide2/includes/atoms/text/two_columns_richtext.html" - - -class AdvancedColumnsTextBlock(ColorBlock, AlignBlock): - left_text = blocks.RichTextBlock( - label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES - ) - right_text = blocks.RichTextBlock( - label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES - ) - - class Meta: - label = "Text dva sloupce (pokročilý)" - icon = "doc-full" - template = "styleguide2/includes/atoms/text/advanced_two_columns_richtext.html" - - -class PictureListBlock(ColorBlock): - items = blocks.ListBlock( - blocks.RichTextBlock(label="Odstavec", features=RICH_TEXT_DEFAULT_FEATURES), - label="Odstavce", - ) - picture = ImageChooserBlock( - label="Obrázek", - help_text="Rozměr 30x30px nebo více (obrázek bude zmenšen na 30x30px)", - ) - - class Meta: - label = "Seznam z obrázkovými odrážkami" - icon = "list-ul" - template = "styleguide2/includes/molecules/lists/image_list.html" - - -DEFAULT_CONTENT_BLOCKS = [ - ( - "text", - blocks.RichTextBlock( - label="Textový editor", - features=RICH_TEXT_DEFAULT_FEATURES, - template="styleguide2/includes/atoms/text/prose_richtext.html", - ), - ), - ("advanced_text", AdvancedTextBlock()), - ("two_columns_text", ColumnsTextBlock()), - ("headline", HeadlineBlock()), - ("headline_with_picture", PictureHeadlineBlock()), - ( - "table", - TableBlock( - template="styleguide2/includes/atoms/table/table.html", label="Tabulka" - ), - ), - ("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()), -] diff --git a/shared/blocks/children/__init__.py b/shared/blocks/children/__init__.py index 829cb54a2778d9f91bf5506c8919895fcff57823..5b480cd0b1014b47cb8313ceccd6fb860c628e3e 100644 --- a/shared/blocks/children/__init__.py +++ b/shared/blocks/children/__init__.py @@ -1,4 +1,6 @@ from .candidates import * # noqa from .misc import * # noqa from .programs import * # noqa -from .mixins import * # noqa \ No newline at end of file +from .mixins import * # noqa +from .struct import * # noqa +from .chart import * # noqa \ No newline at end of file diff --git a/shared/blocks/children/chart.py b/shared/blocks/children/chart.py new file mode 100644 index 0000000000000000000000000000000000000000..fb8f313dd1c5c9c8574172517e5dfad855a2fdb8 --- /dev/null +++ b/shared/blocks/children/chart.py @@ -0,0 +1,313 @@ +from django.utils.text import slugify +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StructBlock, + TextBlock, + URLBlock, +) +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +import datetime +import json +import logging +import re +import typing +import urllib + +import requests +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.files.images import ImageFile +from django.forms.utils import ErrorList +from wagtail import blocks +from wagtail.blocks.struct_block import StructBlockValidationError +from wagtail.contrib.table_block.blocks import TableBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.models import Image +from wagtail.models import Collection + +from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock +from shared.const import ( + ALIGN_CHOICES, + ALIGN_CSS, + BLACK_ON_WHITE, + COLOR_CHOICES, + COLOR_CSS, + LEFT, + RICH_TEXT_DEFAULT_FEATURES, +) + + +class ChartDataset(blocks.StructBlock): + label = blocks.CharBlock( + label="Označení zdroje dat", + max_length=120, + ) + + data = blocks.ListBlock( + blocks.IntegerBlock(), + label="Data", + default=[0], + ) + + class Meta: + label = "Zdroj dat" + + +def get_redmine_projects(): + projects = requests.get("https://redmine.pirati.cz/projects.json?limit=10000") + + if projects.ok: + projects = projects.json()["projects"] + else: + projects = [] + + return [(project["id"], project["name"]) for project in projects] + + +class ChartRedmineIssueDataset(blocks.StructBlock): + projects = blocks.MultipleChoiceBlock( + label="Projekty", choices=get_redmine_projects + ) + + is_open = blocks.BooleanBlock( + label="Jen otevřené", + required=False, + ) + is_closed = blocks.BooleanBlock( + label="Jen uzavřené", + required=False, + ) + + created_on_min_date = blocks.DateBlock(label="Min. datum vytvoření", required=True) + created_on_max_date = blocks.DateBlock(label="Max. datum vytvoření", required=True) + updated_on = blocks.CharBlock( + label="Filtr pro datum aktualizace", + max_length=128, + help_text="Např. <=2023-01-01. Více informací na pi2.cz/redmine-api", + required=False, + ) + + issue_label = blocks.CharBlock( + label="Označení úkolů uvnitř grafu", + max_length=128, + required=True, + ) + + split_per_project = blocks.BooleanBlock( + label="Rozdělit podle projektu", + required=False, + ) + + only_grow = blocks.BooleanBlock( + label="Pouze růst nahoru", + required=False, + ) + + def _get_issues_url( + self, value, project_id: typing.Union[None, str, list[str]] = None + ): + url = "https://redmine.pirati.cz/issues.json" + params = [ + ("sort", "created_on"), + ("limit", "100"), + ( + "created_on", + f"><{value['created_on_min_date']}|{value['created_on_max_date']}", + ), + ] + + if isinstance(project_id, str): + params.append(("project_id", project_id)) + elif isinstance(project_id, list): + params.append(("project_id", ",".join(project_id))) + + is_open = value.get("is_open", False) + is_closed = value.get("is_closed", False) + + if is_open and is_closed: + params.append(("status_id", "*")) + elif is_open: + params.append(("status_id", "open")) + elif is_closed: + params.append(("status_id", "closed")) + + if value.get("updated_on", "") != "": + params.append(("updated_on", value["updated_on"])) + + is_first = True + + for param_set in params: + param, param_value = param_set + + url += "?" if is_first else "&" + url += f"{param}={urllib.parse.quote(param_value)}" + + is_first = False + + return url + + def _get_parsed_issues(self, value, labels, issues_url) -> tuple: + issues_response = cache.get(f"redmine_{issues_url}") + + if issues_response is None: + issues_response = requests.get(issues_url) + issues_response.raise_for_status() + issues_response = issues_response.json() + + cache.set( + f"redmine_{issues_url}", + issues_response, + timeout=604800, # 1 week + ) + + only_grow = value.get("only_grow", False) + + collected_issues = issues_response["issues"] + offset = 0 + + while issues_response["total_count"] - offset > len(issues_response["issues"]): + offset += 100 + url_with_offset = f"{issues_url}&offset={offset}" + + issues_response = cache.get(f"redmine_{url_with_offset}") + + if issues_response is None: + issues_response = requests.get(url_with_offset) + issues_response.raise_for_status() + issues_response = issues_response.json() + + cache.set( + f"redmine_{url_with_offset}", + issues_response, + timeout=604800, # 1 week + ) + + collected_issues += issues_response["issues"] + + ending_position = len(collected_issues) - 1 + + data = None + + current_issue_count = 0 + current_label = datetime.date.fromisoformat( + collected_issues[0]["created_on"].split("T")[0] + ) + + if not only_grow: + data = [0] * len(labels) + + for position, issue in enumerate( + collected_issues + ): # Assume correct sorting order + created_on_date = datetime.date.fromisoformat( + issue["created_on"].split("T")[0] + ) + + if current_label != created_on_date or position == ending_position: + data[ + labels.index(current_label) + ] = current_issue_count # Assume labels are unique + current_label = created_on_date + + if position != ending_position: + current_issue_count = 0 + else: + data[labels.index(current_label)] = 1 + break + + current_issue_count += 1 + else: + data = [] + issue_count_by_date = {} + + for position, issue in enumerate( + collected_issues + ): # Assume correct sorting order + created_on_date = datetime.date.fromisoformat( + issue["created_on"].split("T")[0] + ) + + if current_label not in issue_count_by_date: + issue_count_by_date[current_label] = 0 + + if current_label != created_on_date or position == ending_position: + issue_count_by_date[ + current_label + ] = current_issue_count # Assume labels are unique + current_label = created_on_date + + if position == ending_position: + issue_count_by_date[current_label] = current_issue_count + 1 + break + + current_issue_count += 1 + + previous_date = None + + for date in labels: + if date not in issue_count_by_date: + if previous_date is None: + data.append(0) + continue + + data.append(issue_count_by_date[previous_date]) + continue + + data.append(issue_count_by_date[date]) + previous_date = date + + return data + + def get_context(self, value) -> list: + context = super().get_context(value) + + labels = [] + datasets = [] + + for day_count in range( + (value["created_on_max_date"] - value["created_on_min_date"]).days + 1 + ): + day = value["created_on_min_date"] + datetime.timedelta(days=day_count) + labels.append(day) + + if value.get("split_per_project", False): + project_choices_lookup = dict(get_redmine_projects()) + + for project_id in value["projects"]: + issues_url = self._get_issues_url(value, project_id) + + datasets.append( + { + "label": f"{value['issue_label']} - {project_choices_lookup[int(project_id)]}", + "data": self._get_parsed_issues(value, labels, issues_url), + } + ) + else: + issues_url = self._get_issues_url(value, value["projects"]) + + datasets.append( + { + "label": value["issue_label"], + "data": self._get_parsed_issues(value, labels, issues_url), + } + ) + + labels = [date.strftime("%d. %m. %Y") for date in labels] + + context["parsed_issue_labels"] = labels + context["parsed_issues"] = datasets + + return context + + class Meta: + label = "Zdroj dat z Redmine (úkoly vytvořené za den)" + help_text = ( + "Po prvním otevření se bude stránka otevírat delší dobu, " + "zatímco se na pozadí načítají data do grafu. Poté bude " + "fungovat běžně." + ) \ No newline at end of file diff --git a/shared/blocks/children/struct.py b/shared/blocks/children/struct.py new file mode 100644 index 0000000000000000000000000000000000000000..862791a076146f2905b5473db9ab1a1a5ad08eea --- /dev/null +++ b/shared/blocks/children/struct.py @@ -0,0 +1,92 @@ +from django.utils.text import slugify +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StructBlock, + TextBlock, + URLBlock, +) +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +import datetime +import json +import logging +import re +import typing +import urllib + +import requests +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.files.images import ImageFile +from django.forms.utils import ErrorList +from wagtail import blocks +from wagtail.blocks.struct_block import StructBlockValidationError +from wagtail.contrib.table_block.blocks import TableBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.models import Image +from wagtail.models import Collection + +from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock +from shared.const import ( + ALIGN_CHOICES, + ALIGN_CSS, + BLACK_ON_WHITE, + COLOR_CHOICES, + COLOR_CSS, + LEFT, + RICH_TEXT_DEFAULT_FEATURES, +) + + +class CardLinkBlockMixin(blocks.StructBlock): + """ + Shared mixin for cards, which vary in templates and the types of pages + they're allowed to link to. + """ + + image = ImageChooserBlock(label="Obrázek") + title = blocks.CharBlock(label="Titulek", required=True) + text = blocks.RichTextBlock(label="Krátký text pod nadpisem", required=False) + + page = blocks.PageChooserBlock( + label="Stránka", + page_type=[], + required=False, + ) + link = blocks.URLBlock(label="Odkaz", required=False) + + class Meta: + # template = "" + icon = "link" + label = "Karta s odkazem" + + 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."] + ) + elif not value["page"] and not value["link"]: + errors["page"] = ErrorList(["Zvolte stránku nebo vyplňte odkaz."]) + errors["link"] = ErrorList(["Vyplňte odkaz nebo zvolte stránku."]) + if errors: + raise StructBlockValidationError(errors) + return super().clean(value) + + +class CardLinkWithHeadlineBlockMixin(blocks.StructBlock): + headline = blocks.CharBlock(label="Titulek bloku", required=False) + card_items = blocks.ListBlock(CardLinkBlockMixin(), label="Karty s odkazy") + + class Meta: + # template = "" + icon = "link" + label = "Karty odkazů s nadpisem" \ No newline at end of file diff --git a/shared/blocks/main.py b/shared/blocks/main.py deleted file mode 100644 index 24a4338f2de36ddeb625853841f2c54ce21502cb..0000000000000000000000000000000000000000 --- a/shared/blocks/main.py +++ /dev/null @@ -1,421 +0,0 @@ -from django.utils.text import slugify -from wagtail.blocks import ( - CharBlock, - IntegerBlock, - ListBlock, - PageChooserBlock, - RichTextBlock, - StructBlock, - TextBlock, - URLBlock, -) -from wagtail.documents.blocks import DocumentChooserBlock -from wagtail.images.blocks import ImageChooserBlock - -# Mixins (or used as such) - - -PROGRAM_RICH_TEXT_FEATURES = [ - "h3", - "h4", - "h5", - "bold", - "italic", - "ol", - "ul", - "hr", - "link", - "document-link", - "image", - "superscript", - "subscript", - "strikethrough", - "blockquote", - "embed", -] - - -class CTAMixin(StructBlock): - button_link = URLBlock(label="Odkaz tlačítka") - button_text = CharBlock(label="Text tlačítka") - - class Meta: - icon = "doc-empty" - label = "Tlačítko s odkazem" - - -class LinkBlock(StructBlock): - text = CharBlock(label="Název") - link = URLBlock(label="Odkaz") - - class Meta: - icon = "link" - label = "Odkaz" - - -# Navbar - - -class NavbarMenuItemBlock(CTAMixin): - class Meta: - label = "Tlačítko" - template = "styleguide2/includes/molecules/navbar/additional_button.html" - - -class SocialLinkBlock(LinkBlock): - icon = CharBlock( - label="Ikona", - help_text="Seznam ikon - https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons <br/>" - "Název ikony zadejte bez tečky na začátku", - ) # TODO CSS class name or somthing better? - - class Meta: - icon = "link" - label = "Odkaz" - - -# Articles - - -class NewsBlock(StructBlock): - title = CharBlock( - label="Titulek", - help_text="Nejnovější články se načtou automaticky", - ) - description = TextBlock(label="Popis", required=False) - - class Meta: - icon = "doc-full-inverse" - label = "Novinky" - - -class ArticleQuoteBlock(StructBlock): - quote = CharBlock(label="Citace") - autor_name = CharBlock(label="Jméno autora") - - class Meta: - icon = "user" - label = "Blok citace" - template = "styleguide2/includes/legacy/article_quote_block.html" - - -class ArticleDownloadBlock(StructBlock): - file = DocumentChooserBlock(label="Stáhnutelný soubor") - - class Meta: - icon = "user" - label = "Blok stáhnutelného dokumentu" - template = "styleguide2/includes/molecules/blocks/download_block.html" - - -# People - - -class TwoTextColumnBlock(StructBlock): - text_column_1 = RichTextBlock(label="První sloupec textu") - text_column_2 = RichTextBlock(label="Druhý sloupec textu") - - class Meta: - icon = "doc-full" - label = "Text ve dvou sloupcích" - - -class PersonContactBoxBlock(StructBlock): - title = CharBlock(label="Titulek") - image = ImageChooserBlock(label="Ikona") - subtitle = CharBlock(label="Podtitulek") - - class Meta: - icon = "mail" - label = "Kontakty" - - -class PersonContactBlockMixin(StructBlock): - position = CharBlock(label="Název pozice", required=False) - - @property - def person(self): - # NOTE: Needs to be implemented - - raise NotImplementedError - - # email, phone? - - class Meta: - abstract = True - icon = "user" - label = "Osoba s volitelnou pozicí" - - -# Footer - - -class OtherLinksBlock(StructBlock): - title = CharBlock(label="Titulek") - list = ListBlock(LinkBlock, label="Seznam odkazů") - - class Meta: - icon = "link" - label = "Odkazy" - template = "main/blocks/article_quote_block.html" - - -class ProgramBlockPopout(StructBlock): - title = CharBlock(label="Titulek vyskakovacího bloku") - content = RichTextBlock( - label="Obsah", - features=PROGRAM_RICH_TEXT_FEATURES, - ) - - # TODO: Change in mixed-in blocks - guarantor = PageChooserBlock( - label="Garant", page_type=["district.DistrictPersonPage"], required=False - ) - - class Meta: - icon = "date" - label = "Blok programu" - - -class ProgramPopoutCategory(StructBlock): - name = CharBlock(label="Název") - icon = ImageChooserBlock(label="Ikona", required=False) - - description = RichTextBlock(label="Popis", required=False) - - point_list = ListBlock(ProgramBlockPopout(), label="Jednotlivé bloky programu") - - class Meta: - icon = "date" - label = "Kategorie programu" - - -class ProgramGroupBlockPopout(StructBlock): - categories = ListBlock(ProgramPopoutCategory(), label="Kategorie programu") - - class Meta: - icon = "date" - label = "Vyskakovací program" - - -class FlipCardBlock(StructBlock): - bg_color = CharBlock(label="Barva pozadí", default="FEC900") - - image = ImageChooserBlock(label="Obrázek", required=False) - - title = TextBlock(label="Nadpis", help_text="Řádkování je manuální.") - - content = RichTextBlock(label="Obsah") - - button_text = CharBlock( - label="Nadpis tlačítka", - help_text="Pokud není vyplněn, tlačítko se neukáže.", - required=False, - ) - button_url = CharBlock(label="Odkaz tlačítka", required=False) - - class Meta: - icon = "view" - label = "Obracecí karta" - template = "styleguide2/includes/molecules/boxes/flip_card_box.html" - - -class FlipCardsBlock(StructBlock): - cards = ListBlock( - FlipCardBlock(label="Karta"), - label="Karty", - ) - - class Meta: - icon = "group" - label = "Seznam obracecích karet" - template = "styleguide2/includes/organisms/cards/flip_card_list.html" - - -class BoxBlock(CTAMixin, StructBlock): - title = CharBlock(label="Nadpis") - image = ImageChooserBlock(label="Logo/obrázek") - - class Meta: - icon = "form" - label = "Box" - - -class PeopleOverviewBlock(StructBlock): - title_line_1 = CharBlock(label="První řádek titulku") - title_line_2 = CharBlock(label="Druhý řádek titulku") - - description = TextBlock(label="Popis") - - list = ListBlock(BoxBlock, label="Boxíky") - - class Meta: - template = ( - "styleguide2/includes/organisms/main_section/representatives_section.html" - ) - icon = "group" - label = "Skupina osob" - - -# Program - - -class ProgramGroupBlockMixin(StructBlock): - title = CharBlock( - label="Název programu", - help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", - ) - # point_list = ListBlock(ProgramBlock(), label="Jednotlivé články programu") - - class Meta: - icon = "date" - template = "styleguide2/includes/molecules/program/program_block.html" - # label = "Skupina programů" - - def get_prep_value(self, value): - value = super().get_prep_value(value) - value["slug"] = slugify(value["title"]) - return value - - -class ProgramBlock(StructBlock): - url = URLBlock( - label="Odkaz pokrývající celou tuto část", - required=False, - ) - icon = ImageChooserBlock( - label="Ikona", - required=False, - ) - title = CharBlock(label="Titulek článku programu") - text = RichTextBlock( - label="Obsah", - features=PROGRAM_RICH_TEXT_FEATURES, - ) - - class Meta: - icon = "date" - label = "Článek programu" - - -class ProgramGroupBlock(ProgramGroupBlockMixin): - point_list = ListBlock(ProgramBlock(), label="Jednotlivé články programu") - - class Meta: - icon = "date" - label = "Běžný program" - - -# Candidates - - -class CandidateBlock(StructBlock): - # NOTE: Page type should be restricted in mixed-in classes - page = PageChooserBlock(label="Stránka") - - image = ImageChooserBlock( - label="Obrázek", - help_text="Pokud není vybrán, použije se obrázek ze stránky kandidáta", - required=False, - ) - - description = TextBlock(label="Popis", required=False) - - class Meta: - template = ( - "styleguide2/includes/molecules/candidates/candidate_primary_box.html" - ) - icon = "form" - label = "Kandidát" - - -class SecondaryCandidateBlock(StructBlock): - number = CharBlock(label="Číslo") - - # NOTE: Page type should be restricted in mixed-in classes - page = PageChooserBlock(label="Stránka") - - image = ImageChooserBlock( - label="Obrázek", - help_text="Pokud není vybrán, použije se obrázek ze stránky kandidáta", - required=False, - ) - - class Meta: - template = ( - "styleguide2/includes/molecules/candidates/candidate_secondary_box.html" - ) - icon = "form" - label = "Kandidát" - - -class CandidateListBlock(StructBlock): - # NOTE: should be changed in mixed-in blocks. - candidates = ListBlock( - CandidateBlock(), - label="Kandidáti", - ) - - class Meta: - template = ( - "styleguide2/includes/organisms/candidates/candidate_primary_list.html" - ) - icon = "form" - label = "Seznam kandidátů" - - -class CandidateSecondaryListBlock(StructBlock): - heading = CharBlock(label="Nadpis zbytku kandidátky", default="Ostatní kandidátky") - - # NOTE: should be changed in mixed-in blocks. - candidates = ListBlock( - SecondaryCandidateBlock(), - label="Zbylí kandidáti na listině", - ) - - class Meta: - template = ( - "styleguide2/includes/organisms/candidates/candidate_secondary_list.html" - ) - icon = "form" - label = "Sekundární seznam kandidátů" - - -class CarouselProgramCategoryItemBlock(StructBlock): - content = TextBlock(label="Obsah") - - class Meta: - icon = "form" - label = "Bod" - - -class CarouselProgramCategoryBlock(StructBlock): - number = IntegerBlock(label="Číslo") - - name = CharBlock(label="Název") - - points = ListBlock(CarouselProgramCategoryItemBlock(), label="Body") - - class Meta: - icon = "form" - label = "Kategorie" - - -class CarouselProgramBlock(StructBlock): - label = CharBlock(label="Nadpis", help_text="Např. 'Program'", default="Program") - - categories = ListBlock(CarouselProgramCategoryBlock(), label="Kategorie") - - long_version_url = URLBlock( - label="Odkaz na celou verzi programu", - help_text="Pro zobrazení odkazu na celou verzi programu musí být obě následující pole vyplněná.", - required=False, - ) - long_version_text = CharBlock( - label="Nadpis odkazu na celou verzi programu", required=False - ) - - class Meta: - icon = "form" - label = "Priority programu, carousel" - template = "styleguide2/includes/molecules/program/card_program.html" diff --git a/shared/blocks/parents/__init__.py b/shared/blocks/parents/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2299fc337517a65943eaa5efad00c6c37e5a637d --- /dev/null +++ b/shared/blocks/parents/__init__.py @@ -0,0 +1,7 @@ +from .text import * # noqa +from .programs import * # noqa +from .struct import * # noqa +from .images import * # noqa +from .video import * # noqa +from .navbar import * # noqa +from .button import * # noqa \ No newline at end of file diff --git a/shared/blocks/parents/button.py b/shared/blocks/parents/button.py new file mode 100644 index 0000000000000000000000000000000000000000..f782d1588dd6540ca595fddfce515879e3035f81 --- /dev/null +++ b/shared/blocks/parents/button.py @@ -0,0 +1,156 @@ +from django.utils.text import slugify +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StructBlock, + TextBlock, + URLBlock, +) +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +import datetime +import json +import logging +import re +import typing +import urllib + +import requests +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.files.images import ImageFile +from django.forms.utils import ErrorList +from wagtail import blocks +from wagtail.blocks.struct_block import StructBlockValidationError +from wagtail.contrib.table_block.blocks import TableBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.models import Image +from wagtail.models import Collection + +from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock +from shared.const import ( + ALIGN_CHOICES, + ALIGN_CSS, + BLACK_ON_WHITE, + COLOR_CHOICES, + COLOR_CSS, + LEFT, + RICH_TEXT_DEFAULT_FEATURES, +) +from .children import ( + CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock, + CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock, + ColorBlock, AlignBlock +) + + +class ButtonBlock(blocks.StructBlock): + title = blocks.CharBlock(label="Titulek", max_length=128, required=True) + + color = blocks.ChoiceBlock( + choices=( + ("black", "Černá"), + ("white", "Bílá"), + ("pirati-yellow", "Žlutá"), + ("grey-125", "Světle šedá"), + ("blue-300", "Modrá"), + ("cyan-200", "Tyrkysová"), + ("green-400", "Zelená"), + ("violet-400", "Vínová"), + ("red-600", "Červená"), + ), + label="Barva", + default="black", + ) + + hoveractive = blocks.BooleanBlock( + label="Animovat na hover", + default=True, + help_text="Pokud je zapnuto, tlačítko při najetí kurzorem ukáže žlutou šipku.", + 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 = "styleguide2/includes/atoms/buttons/round_button_block.html" + + def get_context(self, value, parent_context=None): + context = super().get_context(value, parent_context) + + context["background_color"] = f"bg-{value['color']}" + + context["text_color"] = ( + "text-white" + if value["color"] + in ( + "black", + "red-600", + "blue-300", + "cyan-200", + "green-400", + "violet-400", + "red-600", + ) + else "text-black" + ) + + context[ + "color_classes" + ] = f"{context['background_color']} {context['text_color']}" + + return context + + 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 value["hoveractive"] and value["color"] not in ( + "black", + "white", + "grey-125", + ): + errors["hoveractive"] = ErrorList( + ["Šipku lze ukazovat pouze s černým, bílým nebo šedým pozadím."] + ) + + 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 = "styleguide2/includes/atoms/buttons/round_button_group_block.html" \ No newline at end of file diff --git a/shared/blocks/parents/chart.py b/shared/blocks/parents/chart.py new file mode 100644 index 0000000000000000000000000000000000000000..ebfc6ca1382f095f45928e676966e65762fec0cc --- /dev/null +++ b/shared/blocks/parents/chart.py @@ -0,0 +1,168 @@ +from django.utils.text import slugify +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StructBlock, + TextBlock, + URLBlock, +) +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +import datetime +import json +import logging +import re +import typing +import urllib + +import requests +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.files.images import ImageFile +from django.forms.utils import ErrorList +from wagtail import blocks +from wagtail.blocks.struct_block import StructBlockValidationError +from wagtail.contrib.table_block.blocks import TableBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.models import Image +from wagtail.models import Collection + +from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock +from shared.const import ( + ALIGN_CHOICES, + ALIGN_CSS, + BLACK_ON_WHITE, + COLOR_CHOICES, + COLOR_CSS, + LEFT, + RICH_TEXT_DEFAULT_FEATURES, +) +from .children import ( + CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock, + CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock, + ColorBlock, AlignBlock +) + + +class ChartBlock(blocks.StructBlock): + title = blocks.CharBlock( + label="Název", + max_length=120, + ) + chart_type = blocks.ChoiceBlock( + label="Typ", + choices=[ + ("bar", "Graf se sloupci"), + ("horizontalBar", "Graf s vodorovnými sloupci"), + ("pie", "Koláčový graf"), + ("doughnut", "Donutový graf"), + ("polarArea", "Graf polární oblasti"), + ("radar", "Radarový graf"), + ("line", "Graf s liniemi"), + ], + default="bar", + ) + + hide_points = blocks.BooleanBlock( + label="Schovat body", + required=False, + help_text="Mění vzhled pouze u linových grafů.", + ) + + local_labels = blocks.ListBlock( + blocks.CharBlock( + max_length=40, + label="Skupina", + ), + default=[], + blank=True, + required=False, + collapsed=True, + label="Místně definované skupiny", + ) + local_datasets = blocks.ListBlock( + ChartDataset(), + default=[], + blank=True, + required=False, + collapsed=True, + label="Místní zdroje dat", + ) + + redmine_issue_datasets = blocks.ListBlock( + ChartRedmineIssueDataset(label="Redmine úkoly"), + default=[], + blank=True, + required=False, + label="Zdroje dat z Redmine (úkoly)", + help_text=( + "Úkoly, podle doby vytvoření. Pokud definuješ " + "více zdrojů, datumy v nich musí být stejné." + ), + ) + + def clean(self, value): + result = super().clean(value) + + redmine_issues_exist = len(value.get("redmine_issue_datasets", [])) != 0 + + if len(value.get("local_datasets", [])) != 0 and redmine_issues_exist: + raise ValidationError( + "Definuj pouze jeden typ zdroje dat - místní, nebo z Redmine." + ) + + if redmine_issues_exist: + min_date = value["redmine_issue_datasets"][0]["created_on_min_date"] + max_date = value["redmine_issue_datasets"][0]["created_on_max_date"] + + if len(value["redmine_issue_datasets"]) > 1: + for dataset in value["redmine_issue_datasets"]: + if ( + dataset["created_on_min_date"] != min_date + or dataset["created_on_max_date"] != max_date + ): + raise ValidationError( + "Maximální a minimální data všech zdrojů z Redmine musí být stejné" + ) + + return result + + def get_context(self, value, parent_context=None): + context = super().get_context(value, parent_context=parent_context) + + datasets = [] + labels = [] + + if len(value["local_datasets"]) != 0: + labels = value["local_labels"] + + for dataset in value["local_datasets"]: + datasets.append( + { + "label": dataset["label"], + "data": [item for item in dataset["data"]], + } + ) + elif len(value["redmine_issue_datasets"]) != 0: + for dataset_wrapper in value["redmine_issue_datasets"]: + redmine_context = ChartRedmineIssueDataset().get_context( + dataset_wrapper + ) + + labels = redmine_context["parsed_issue_labels"] + datasets += redmine_context["parsed_issues"] + + value["datasets"] = json.dumps(datasets) + value["labels"] = json.dumps([label for label in labels]) + + return context + + class Meta: + template = "styleguide2/includes/molecules/blocks/chart.html" + label = "Graf" + icon = "form" + help_text = """Všechny položky zdrojů dat se chovají jako sloupce. +Zobrazí se tolik definovaných sloupců, kolik existuje skupin.""" \ No newline at end of file diff --git a/shared/blocks/parents/images.py b/shared/blocks/parents/images.py new file mode 100644 index 0000000000000000000000000000000000000000..ff0a33c8330b3d23de32202a497cb84cf9929788 --- /dev/null +++ b/shared/blocks/parents/images.py @@ -0,0 +1,73 @@ +from django.utils.text import slugify +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StructBlock, + TextBlock, + URLBlock, +) +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +import datetime +import json +import logging +import re +import typing +import urllib + +import requests +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.files.images import ImageFile +from django.forms.utils import ErrorList +from wagtail import blocks +from wagtail.blocks.struct_block import StructBlockValidationError +from wagtail.contrib.table_block.blocks import TableBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.models import Image +from wagtail.models import Collection + +from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock +from shared.const import ( + ALIGN_CHOICES, + ALIGN_CSS, + BLACK_ON_WHITE, + COLOR_CHOICES, + COLOR_CSS, + LEFT, + RICH_TEXT_DEFAULT_FEATURES, +) +from .children import ( + CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock, + CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock, + ColorBlock, AlignBlock +) + + +class GalleryBlock(StructBlock): + gallery_items = ListBlock( + ImageChooserBlock(label="obrázek", required=True), + label="Galerie", + icon="image", + group="ostatní", + ) + + class Meta: + label = "Galerie" + icon = "image" + template = "styleguide2/includes/molecules/gallery/gallery.html" + + +class FigureBlock(StructBlock): + img = ImageChooserBlock(label="Obrázek", required=True) + caption = TextBlock(label="Popisek", required=False) + + class Meta: + label = "Obrázek" + icon = "image" + template = "styleguide2/includes/atoms/figure/figure.html" + + \ No newline at end of file diff --git a/shared/blocks/parents/navbar.py b/shared/blocks/parents/navbar.py new file mode 100644 index 0000000000000000000000000000000000000000..1b8a2f34931e11243727e71e75bedec0bff12f55 --- /dev/null +++ b/shared/blocks/parents/navbar.py @@ -0,0 +1,84 @@ +from django.utils.text import slugify +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StructBlock, + TextBlock, + URLBlock, +) +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +import datetime +import json +import logging +import re +import typing +import urllib + +import requests +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.files.images import ImageFile +from django.forms.utils import ErrorList +from wagtail import blocks +from wagtail.blocks.struct_block import StructBlockValidationError +from wagtail.contrib.table_block.blocks import TableBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.models import Image +from wagtail.models import Collection + +from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock +from shared.const import ( + ALIGN_CHOICES, + ALIGN_CSS, + BLACK_ON_WHITE, + COLOR_CHOICES, + COLOR_CSS, + LEFT, + RICH_TEXT_DEFAULT_FEATURES, +) +from .children import ( + CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock, + CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock, + ColorBlock, AlignBlock +) + + +class MenuItemBlock(blocks.StructBlock): + title = blocks.CharBlock( + label="Titulek", + help_text="Pokud není odkazovaná stránka na Majáku, použij možnost zadání samotné adresy níže.", + 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(), label="Položky menu") + + class Meta: + label = "Vyskakovací menu" + template = "styleguide2/includes/molecules/dropdown/dropdown.html" \ No newline at end of file diff --git a/shared/blocks/parents.py b/shared/blocks/parents/programs.py similarity index 51% rename from shared/blocks/parents.py rename to shared/blocks/parents/programs.py index ce57a14565bd2a0481b9705295dc62bfbb21c593..91992ce060931259697da803b7615c8f0c97fb05 100644 --- a/shared/blocks/parents.py +++ b/shared/blocks/parents/programs.py @@ -46,108 +46,6 @@ from .children import ( ColorBlock, AlignBlock ) -logger = logging.getLogger(__name__) - - -class SocialLinkBlock(LinkBlock): - icon = CharBlock( - label="Ikona", - help_text="Seznam ikon - https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons <br/>" - "Název ikony zadejte bez tečky na začátku", - ) # TODO CSS class name or somthing better? - - class Meta: - icon = "link" - label = "Odkaz" - - -class NewsBlock(StructBlock): - title = CharBlock( - label="Titulek", - help_text="Nejnovější články se načtou automaticky", - ) - description = TextBlock(label="Popis", required=False) - - class Meta: - icon = "doc-full-inverse" - label = "Novinky" - - -class ArticleQuoteBlock(StructBlock): - quote = CharBlock(label="Citace") - autor_name = CharBlock(label="Jméno autora") - - class Meta: - icon = "user" - label = "Blok citace" - template = "styleguide2/includes/legacy/article_quote_block.html" - - -class ArticleDownloadBlock(StructBlock): - file = DocumentChooserBlock(label="Stáhnutelný soubor") - - class Meta: - icon = "user" - label = "Blok stáhnutelného dokumentu" - template = "styleguide2/includes/molecules/blocks/download_block.html" - - -# TODO: Merge -class TwoTextColumnBlock(StructBlock): - text_column_1 = RichTextBlock(label="První sloupec textu") - text_column_2 = RichTextBlock(label="Druhý sloupec textu") - - class Meta: - icon = "doc-full" - label = "Text ve dvou sloupcích" - - -class PersonContactBoxBlock(StructBlock): - title = CharBlock(label="Titulek") - image = ImageChooserBlock(label="Ikona") - subtitle = CharBlock(label="Podtitulek") - - class Meta: - icon = "mail" - label = "Kontakty" - - -class OtherLinksBlock(StructBlock): - title = CharBlock(label="Titulek") - list = ListBlock(LinkBlock, label="Seznam odkazů") - - class Meta: - icon = "link" - label = "Odkazy" - - -class FlipCardsBlock(StructBlock): - cards = ListBlock( - FlipCardBlock(label="Karta"), - label="Karty", - ) - - class Meta: - icon = "group" - label = "Seznam obracecích karet" - template = "styleguide2/includes/organisms/cards/flip_card_list.html" - - -class PeopleOverviewBlock(StructBlock): - title_line_1 = CharBlock(label="První řádek titulku") - title_line_2 = CharBlock(label="Druhý řádek titulku") - - description = TextBlock(label="Popis") - - list = ListBlock(PersonBoxBlock, label="Boxíky") - - class Meta: - template = ( - "styleguide2/includes/organisms/main_section/representatives_section.html" - ) - icon = "group" - label = "Skupina osob" - class ProgramGroupBlockPopout(StructBlock): categories = ListBlock(ProgramPopoutCategory(), label="Kategorie programu") @@ -217,25 +115,9 @@ class CarouselProgramBlock(StructBlock): template = "styleguide2/includes/molecules/program/card_program.html" -class GalleryBlock(StructBlock): - gallery_items = ListBlock( - ImageChooserBlock(label="obrázek", required=True), - label="Galerie", - icon="image", - group="ostatní", +class ProgramItemBlock(blocks.StructBlock): + title = blocks.CharBlock(label="Název", required=True) + completion_percentage = blocks.IntegerBlock( + label="Procento dokončení", required=True ) - - class Meta: - label = "Galerie" - icon = "image" - template = "styleguide2/includes/molecules/gallery/gallery.html" - - -class FigureBlock(StructBlock): - img = ImageChooserBlock(label="Obrázek", required=True) - caption = TextBlock(label="Popisek", required=False) - - class Meta: - label = "Obrázek" - icon = "image" - template = "styleguide2/includes/atoms/figure/figure.html" \ No newline at end of file + issue_link = blocks.URLBlock(label="Odkaz na Redmine issue", required=False) \ No newline at end of file diff --git a/shared/blocks/parents/struct.py b/shared/blocks/parents/struct.py new file mode 100644 index 0000000000000000000000000000000000000000..92a18fe9103f45917623ba487fa8a3b015fbb229 --- /dev/null +++ b/shared/blocks/parents/struct.py @@ -0,0 +1,202 @@ +from django.utils.text import slugify +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StructBlock, + TextBlock, + URLBlock, +) +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +import datetime +import json +import logging +import re +import typing +import urllib + +import requests +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.files.images import ImageFile +from django.forms.utils import ErrorList +from wagtail import blocks +from wagtail.blocks.struct_block import StructBlockValidationError +from wagtail.contrib.table_block.blocks import TableBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.models import Image +from wagtail.models import Collection + +from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock +from shared.const import ( + ALIGN_CHOICES, + ALIGN_CSS, + BLACK_ON_WHITE, + COLOR_CHOICES, + COLOR_CSS, + LEFT, + RICH_TEXT_DEFAULT_FEATURES, +) +from .children import ( + CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock, + CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock, + ColorBlock, AlignBlock +) + +logger = logging.getLogger(__name__) + + +class SocialLinkBlock(LinkBlock): + icon = CharBlock( + label="Ikona", + help_text="Seznam ikon - https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons <br/>" + "Název ikony zadejte bez tečky na začátku", + ) # TODO CSS class name or somthing better? + + class Meta: + icon = "link" + label = "Odkaz" + + +class NewsBlock(StructBlock): + title = CharBlock( + label="Titulek", + help_text="Nejnovější články se načtou automaticky", + ) + description = TextBlock(label="Popis", required=False) + + class Meta: + icon = "doc-full-inverse" + label = "Novinky" + + +class PersonContactBoxBlock(StructBlock): + title = CharBlock(label="Titulek") + image = ImageChooserBlock(label="Ikona") + subtitle = CharBlock(label="Podtitulek") + + class Meta: + icon = "mail" + label = "Kontakty" + + +class OtherLinksBlock(StructBlock): + title = CharBlock(label="Titulek") + list = ListBlock(LinkBlock, label="Seznam odkazů") + + class Meta: + icon = "link" + label = "Odkazy" + + +class FlipCardsBlock(StructBlock): + cards = ListBlock( + FlipCardBlock(label="Karta"), + label="Karty", + ) + + class Meta: + icon = "group" + label = "Seznam obracecích karet" + template = "styleguide2/includes/organisms/cards/flip_card_list.html" + + +class PeopleOverviewBlock(StructBlock): + title_line_1 = CharBlock(label="První řádek titulku") + title_line_2 = CharBlock(label="Druhý řádek titulku") + + description = TextBlock(label="Popis") + + list = ListBlock(PersonBoxBlock, label="Boxíky") + + class Meta: + template = ( + "styleguide2/includes/organisms/main_section/representatives_section.html" + ) + icon = "group" + label = "Skupina osob" + + +class CardBlock(blocks.StructBlock): + img = ImageChooserBlock(label="Obrázek", required=False) + + headline = blocks.TextBlock(label="Titulek", required=False) + + content = blocks.StreamBlock( + label="Obsah", + local_blocks=[ + ( + "text", + blocks.RichTextBlock( + label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES + ), + ), + ( + "table", + TableBlock( + template="styleguide2/includes/atoms/table/table.html", + label="Tabulka", + ), + ), + ("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 = "styleguide2/includes/molecules/boxes/card_box_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 NewsletterSubscriptionBlock(blocks.StructBlock): + list_id = blocks.CharBlock(label="ID newsletteru", required=True) + + title_line_1 = blocks.CharBlock( + label = "Nadpis bloku (1. řádek)", + required=True, + default="Odebírej náš" + ) + + title_line_2 = blocks.CharBlock( + label = "Nadpis bloku (2. řádek)", + required=True, + default="newsletter" + ) + + description = blocks.CharBlock( + label="Popis newsletteru", + required=True, + default="Fake news tam nenajdeš, ale dozvíš se, co chystáme doopravdy!", + ) + + class Meta: + label = "Formulář pro odebírání newsletteru" + icon = "form" + template = "styleguide2/includes/organisms/main_section/newsletter_section.html" \ No newline at end of file diff --git a/shared/blocks/parents/text.py b/shared/blocks/parents/text.py new file mode 100644 index 0000000000000000000000000000000000000000..2d75ec6726c66331ff42c84b2660af0049cb319c --- /dev/null +++ b/shared/blocks/parents/text.py @@ -0,0 +1,352 @@ +from django.utils.text import slugify +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StructBlock, + TextBlock, + URLBlock, +) +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +import datetime +import json +import logging +import re +import typing +import urllib + +import requests +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.files.images import ImageFile +from django.forms.utils import ErrorList +from wagtail import blocks +from wagtail.blocks.struct_block import StructBlockValidationError +from wagtail.contrib.table_block.blocks import TableBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.models import Image +from wagtail.models import Collection + +from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock +from shared.const import ( + ALIGN_CHOICES, + ALIGN_CSS, + BLACK_ON_WHITE, + COLOR_CHOICES, + COLOR_CSS, + LEFT, + RICH_TEXT_DEFAULT_FEATURES, +) +from .children import ( + CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock, + CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock, + ColorBlock, AlignBlock +) + + +class AdvancedTextBlock(ColorBlock, AlignBlock): + text = blocks.RichTextBlock( + label="Textový editor", + features=RICH_TEXT_DEFAULT_FEATURES, + ) + + class Meta: + label = "Textový editor (pokročilý)" + icon = "doc-full" + template = "styleguide2/includes/atoms/text/prose_advanced_richtext.html" + + +class ColumnsTextBlock(blocks.StructBlock): + left_text = blocks.RichTextBlock( + label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES + ) + right_text = blocks.RichTextBlock( + label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES + ) + + class Meta: + label = "Text dva sloupce" + icon = "doc-full" + template = "styleguide2/includes/atoms/text/two_columns_richtext.html" + + +# TODO: Merge +class TwoTextColumnBlock(StructBlock): + text_column_1 = RichTextBlock(label="První sloupec textu") + text_column_2 = RichTextBlock(label="Druhý sloupec textu") + + class Meta: + icon = "doc-full" + label = "Text ve dvou sloupcích" + + +class ArticleQuoteBlock(StructBlock): + quote = CharBlock(label="Citace") + autor_name = CharBlock(label="Jméno autora") + + class Meta: + icon = "user" + label = "Blok citace" + template = "styleguide2/includes/legacy/article_quote_block.html" + + +class ArticleDownloadBlock(StructBlock): + file = DocumentChooserBlock(label="Stáhnutelný soubor") + + class Meta: + icon = "user" + label = "Blok stáhnutelného dokumentu" + template = "styleguide2/includes/molecules/blocks/download_block.html" + + +class HeadlineBlock(blocks.StructBlock): + headline = blocks.CharBlock(label="Nadpis", max_length=300, 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", + ) + + style = blocks.ChoiceBlock( + choices=( + ("head-alt-xl", "Velký, Bebas Neue - 6XL"), + ("head-alt-lg", "Střední, Bebas Neue - 4XL"), + ("head-alt-md", "Základní velikost - Roboto - MD"), + ("head-alt-sm", "Malý - Roboto - SM"), + ("head-alt-xs", "Extra malý - Roboto - XS"), + ), + label="Velikost", + help_text="Náhled si prohlédněte na https://styleguide2.pirati.cz/pattern/patterns/atoms/text/headings.html.", + default="head-alt-xl", + required=True, + ) + + 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-6xl", + "head-alt-lg": "head-4xl", + "head-alt-md": "head-base", + "head-alt-sm": "head-sm", + "head-alt-xs": "head-xs", + }.get(value["style"], "head-4xl") + + return context + + class Meta: + label = "Nadpis" + icon = "bold" + template = "styleguide2/includes/atoms/text/heading.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="styleguide2/includes/atoms/table/table.html", + label="Tabulka", + ), + ), + ("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="styleguide2/includes/atoms/table/table.html", + label="Tabulka", + ), + ), + ("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 = "styleguide2/includes/atoms/grids/two_columns.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="styleguide2/includes/atoms/table/table.html", + label="Tabulka", + ), + ), + ("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="styleguide2/includes/atoms/table/table.html", + label="Tabulka", + ), + ), + ("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="styleguide2/includes/atoms/table/table.html", + label="Tabulka", + ), + ), + ("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 = "styleguide2/includes/atoms/grids/three_columns.html" + + +class PictureListBlock(ColorBlock): + items = blocks.ListBlock( + blocks.RichTextBlock(label="Odstavec", features=RICH_TEXT_DEFAULT_FEATURES), + label="Odstavce", + ) + picture = ImageChooserBlock( + label="Obrázek", + help_text="Rozměr 30x30px nebo více (obrázek bude zmenšen na 30x30px)", + ) + + class Meta: + label = "Seznam z obrázkovými odrážkami" + icon = "list-ul" + template = "styleguide2/includes/molecules/lists/image_list.html" + + +class PictureHeadlineBlock(ColorBlock): + title = blocks.CharBlock(label="nadpis") + picture = ImageChooserBlock( + label="obrázek", + help_text="rozměr na výšku 75px nebo více (obrázek bude zmenšen na výšku 75px)", + ) + + class Meta: + label = "Nadpis (s obrázkem)" + icon = "title" + template = "styleguide2/includes/atoms/text/heading_with_image.html" + + +class AdvancedColumnsTextBlock(ColorBlock, AlignBlock): + left_text = blocks.RichTextBlock( + label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES + ) + right_text = blocks.RichTextBlock( + label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES + ) + + class Meta: + label = "Text dva sloupce (pokročilý)" + icon = "doc-full" + template = "styleguide2/includes/atoms/text/advanced_two_columns_richtext.html" \ No newline at end of file diff --git a/shared/blocks/parents/video.py b/shared/blocks/parents/video.py new file mode 100644 index 0000000000000000000000000000000000000000..80d5c61ac90db132af9471d335baaff5af95bf06 --- /dev/null +++ b/shared/blocks/parents/video.py @@ -0,0 +1,162 @@ +from django.utils.text import slugify +from wagtail.blocks import ( + CharBlock, + IntegerBlock, + ListBlock, + PageChooserBlock, + RichTextBlock, + StructBlock, + TextBlock, + URLBlock, +) +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +import datetime +import json +import logging +import re +import typing +import urllib + +import requests +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.core.files.images import ImageFile +from django.forms.utils import ErrorList +from wagtail import blocks +from wagtail.blocks.struct_block import StructBlockValidationError +from wagtail.contrib.table_block.blocks import TableBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.models import Image +from wagtail.models import Collection + +from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock +from shared.const import ( + ALIGN_CHOICES, + ALIGN_CSS, + BLACK_ON_WHITE, + COLOR_CHOICES, + COLOR_CSS, + LEFT, + RICH_TEXT_DEFAULT_FEATURES, +) +from .children import ( + CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock, + CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock, + ColorBlock, AlignBlock +) + + +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 = "styleguide2/includes/atoms/youtube_video/youtube_video.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 \ No newline at end of file