From d92f6049aeb220aac624eb70d7beb5cc2cc33890 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valenta?= <tomas@imaniti.org>
Date: Tue, 4 Jun 2024 13:48:50 +0200
Subject: [PATCH] WIP - migrate programs

---
 .../migrations/0218_auto_20240601_1530.py     | 522 +++++++++++-------
 district/models.py                            |   2 +-
 .../0037_alter_electionshomepage_content.py   |  21 +
 .../0094_alter_mainprogrampage_program.py     |  21 +
 4 files changed, 366 insertions(+), 200 deletions(-)
 create mode 100644 elections/migrations/0037_alter_electionshomepage_content.py
 create mode 100644 main/migrations/0094_alter_mainprogrampage_program.py

diff --git a/district/migrations/0218_auto_20240601_1530.py b/district/migrations/0218_auto_20240601_1530.py
index eccb9122..2ae930bc 100644
--- a/district/migrations/0218_auto_20240601_1530.py
+++ b/district/migrations/0218_auto_20240601_1530.py
@@ -1,9 +1,10 @@
 # Generated by Django 5.0.4 on 2024-06-01 13:30
 
 from django.db import migrations, transaction
+import wagtail
 
-from district.blocks import ProgramGroupWithCandidatesBlock
-from shared.blocks import SocialLinkBlock
+from district.blocks import ProgramGroupWithCandidatesBlock, CandidateListBlock, CandidateBlock, CandidateSecondaryListBlock, SecondaryCandidateBlock, ProgramGroupBlockPopout
+from shared.blocks import SocialLinkBlock, ProgramBlockPopout
 
 
 def migrate_programs(apps, schema_editor):
@@ -66,28 +67,45 @@ def migrate_programs(apps, schema_editor):
         )
 
     for home_page in DistrictHomePage.objects.all():
-        election_data = {
-            # Title
-            "title": "",  # Done
-            "order": 0,
-            # Program lists
-            "program_points": [],
-            "post_election_strategies": [],
-            # Candidates
-            "candidate_list_number": "",
-            "candidate_list_title": "",
-            "candidate_pages": [],
-            "candidate_blocks": [],  # Page creation done
-            "combined_candidates": [],
-            "primary_candidate_count": 0,
-            # Program
-            "program_title": "",
-            "program_is_inline": False,
-            "program_content_before": "",  # Done
-            # Misc.
-            "funding_info": "",
-        }
+        # Create a new program page, even if this DistrictHomePage will
+        # end up having no programs.
 
+        max_path_page = (
+            Page.objects.filter(
+                path__startswith=home_page.path, depth=home_page.depth + 1
+            )
+            .order_by("-path")
+            .first()
+        )
+
+        if max_path_page:
+            max_path = max_path_page.path
+            next_path_suffix = (
+                int(max_path[-4:], 36) + 1
+            )  # Base-36 to handle alphanumeric paths
+            next_path = (
+                max_path[:-4] + f"{next_path_suffix:04X}"
+            )  # Convert back to base-36
+        else:
+            next_path = home_page.path + "0001"
+
+        # Create the new program page
+        with transaction.atomic():
+            new_program_page = DistrictNewProgramPage(
+                title="Programy",
+                slug="programy",  # Make sure the slug is unique among siblings
+                path=next_path,
+                depth=home_page.depth + 1,
+                numchild=0,
+                content_type_id=new_program_page_content_type.id,
+                locale_id=default_locale.id,
+            )
+            new_program_page.save()
+
+            # Update numchild on the home page
+            home_page.numchild = home_page.numchild + 1
+            home_page.save(update_fields=["numchild"])
+            
         # Iterate over all child DistrictElectionRootPage instances
         for election_root_page in get_children_of_type(
             DistrictElectionRootPage, home_page, root_page_content_type
@@ -100,39 +118,79 @@ def migrate_programs(apps, schema_editor):
             )
 
             for campaign_page in campaign_pages:
-                election_data["title"] = (
+                # Data for a single program
+
+                program_data = {
+                    # Title
+                    "title": "",  # Done
+                    "order": 0,
+
+                    # Post-election
+                    "post_election_strategies": [],  
+
+                    # Candidates
+                    "candidate_list_number": "",
+                    "candidate_list_title": "",  # Done
+                    "candidate_pages": [],  # Done
+                    "candidate_blocks": [],  # Done
+                    "combined_candidates": [],  # Done
+                    "primary_candidate_count": 0,  # Done
+                    
+                    # Program
+                    "program_title": "",
+                    "program_points": [],
+                    "program_is_inline": False,
+                    "program_content_before": "",  # Done
+                    
+                    # Misc.
+                    "funding_info": "",
+                }
+
+                program_data["title"] = (
                     election_root_page.title
                     if election_root_page.title
                     else campaign_page.title
                 )
 
-                election_data["order"] = campaign_page.order
+                program_data["order"] = campaign_page.order
 
-                election_data["candidate_list_number"] = campaign_page.number
-                election_data[
+                program_data["candidate_list_number"] = campaign_page.number
+                program_data[
                     "candidate_list_title"
                 ] = campaign_page.candidate_list_title
 
-                election_data["program_title"] = campaign_page.program_point_list_title
-                election_data[
+                program_data["program_title"] = (
+                    campaign_page.program_point_list_title
+                    if campaign_page.program_point_list_title
+                    else campaign_page.title
+                )
+
+                program_data[
                     "program_is_inline"
                 ] = campaign_page.show_program_points_inline
 
-                election_data["funding_info"] = campaign_page.campaign_funding_info
+                program_data["funding_info"] = campaign_page.campaign_funding_info
 
                 position = 0
 
+                # Parse candidates from blocks
                 for candidate_list_block in campaign_page.candidates.get_prep_value():
                     candidate_list = candidate_list_block["value"]
 
-                    election_data["primary_candidate_count"] = candidate_list[
+                    program_data["primary_candidate_count"] = candidate_list[
                         "candidate_list_big_count"
                     ]
 
                     for candidate in candidate_list["candidate_list"]:
-                        position = position + 1
-
                         if candidate["type"] == "person_page":
+                            if DistrictPersonPage.objects.filter(
+                                id=candidate["value"]
+                            ).first() is None:
+                                # We have nothing to do here, this page ID is either unfilled or points to a missing page
+                                continue
+
+                            position = position + 1
+
                             candidate_data = {
                                 "position": position,
                                 "page": DistrictPersonPage.objects.filter(
@@ -140,32 +198,38 @@ def migrate_programs(apps, schema_editor):
                                 ).first(),
                             }
 
-                            election_data["candidate_pages"].append(candidate_data)
-                            election_data["combined_candidates"].append(candidate_data)
+                            program_data["candidate_pages"].append(candidate_data)
+                            program_data["combined_candidates"].append(candidate_data)
 
                             continue
 
-                        if candidate["type"] == "person_blocks":
-                            election_data["candidate_blocks"].append(
+                        elif candidate["type"] == "person_blocks":
+                            position = position + 1
+
+                            program_data["candidate_blocks"].append(
                                 {"position": position, "data": candidate["value"]}
                             )
 
+                # Parse program points
                 program_pages = get_children_of_type(
                     DistrictElectionProgramPage,
-                    election_root_page,
+                    campaign_page,
                     program_page_content_type,
                 )
 
                 for program_page in program_pages:
-                    election_data["program_points"].append(
+                    program_data["program_points"].append(
                         {
-                            "guarantor_page": program_page.guarantor,
-                            "image": program_page.image,
-                            "perex": program_page.perex,
-                            "funding_info": program_page.campaign_funding_info,
+                            "title": program_page.title,  # Done
+                            "guarantor_page": program_page.guarantor,  # Done
+                            "image": program_page.image,  # Ignoring
+                            "perex": program_page.perex,  # Done
+                            "content": program_page.content,  # Done
+                            "funding_info": program_page.campaign_funding_info,  # TODO: Ignoring?
                         }
                     )
 
+                # Parse post-election strategies
                 post_election_strategy_pages = get_children_of_type(
                     DistrictPostElectionStrategyPage,
                     election_root_page,
@@ -173,184 +237,232 @@ def migrate_programs(apps, schema_editor):
                 )
 
                 for post_election_strategy_page in post_election_strategy_pages:
-                    election_data["post_election_strategies"].append(
+                    program_data["post_election_strategies"].append(
                         {
                             "perex": post_election_strategy_page.perex,
                         }
                     )
 
-        if (
-            len(election_data["candidate_pages"]) == 0
-            and len(election_data["candidate_blocks"]) == 0
-            and len(election_data["program_points"]) == 0
-        ):
-            continue  # Do nothing for these
+                # If there's no program data, skip this iteration.
+                if (
+                    len(program_data["candidate_pages"]) == 0
+                    and len(program_data["candidate_blocks"]) == 0
+                    and len(program_data["program_points"]) == 0
+                ):
+                    continue  # Do nothing for these
 
-        parent_people_page = get_children_of_type(
-            DistrictPeoplePage, home_page, people_page_content_type
-        )
-
-        parent_people_page = parent_people_page.first()
-
-        # Create corresponding pages for the candidate blocks
-        for candidate_block in election_data["candidate_blocks"]:
-            if parent_people_page is None:
-                break  # We can't do anything here
-
-            social_links = []
-
-            if candidate_block["data"]["facebook_url"]:
-                social_links.append(
-                    SocialLinkBlock().to_python(
-                        {
-                            "icon": "ico--facebook",
-                            "text": "Facebook",
-                            "link": candidate_block["data"]["facebook_url"],
-                        }
-                    )
+                parent_people_page = get_children_of_type(
+                    DistrictPeoplePage, home_page, people_page_content_type
                 )
 
-            if candidate_block["data"]["instagram_url"]:
-                social_links.append(
-                    SocialLinkBlock().to_python(
-                        {
-                            "icon": "ico--instagram",
-                            "text": "Instagram",
-                            "link": candidate_block["data"]["instagram_url"],
-                        }
-                    )
-                )
+                parent_people_page = parent_people_page.first()
 
-            if candidate_block["data"]["twitter_url"]:
-                social_links.append(
-                    SocialLinkBlock().to_python(
-                        {
-                            "icon": "ico--twitter",
-                            "text": "Twitter",
-                            "link": candidate_block["data"]["twitter_url"],
-                        }
-                    )
-                )
+                # Create corresponding pages for candidate blocks with no pages
+                for candidate_block in program_data["candidate_blocks"]:
+                    if parent_people_page is None:
+                        break  # We can't do anything here
 
-            if candidate_block["data"]["youtube_url"]:
-                social_links.append(
-                    SocialLinkBlock().to_python(
-                        {
-                            "icon": "ico--youtube",
-                            "text": "YouTube",
-                            "link": candidate_block["data"]["youtube_url"],
-                        }
+                    social_links = []
+
+                    if candidate_block["data"]["facebook_url"]:
+                        social_links.append(
+                            SocialLinkBlock().to_python(
+                                {
+                                    "icon": "ico--facebook",
+                                    "text": "Facebook",
+                                    "link": candidate_block["data"]["facebook_url"],
+                                }
+                            )
+                        )
+
+                    if candidate_block["data"]["instagram_url"]:
+                        social_links.append(
+                            SocialLinkBlock().to_python(
+                                {
+                                    "icon": "ico--instagram",
+                                    "text": "Instagram",
+                                    "link": candidate_block["data"]["instagram_url"],
+                                }
+                            )
+                        )
+
+                    if candidate_block["data"]["twitter_url"]:
+                        social_links.append(
+                            SocialLinkBlock().to_python(
+                                {
+                                    "icon": "ico--twitter",
+                                    "text": "Twitter",
+                                    "link": candidate_block["data"]["twitter_url"],
+                                }
+                            )
+                        )
+
+                    if candidate_block["data"]["youtube_url"]:
+                        social_links.append(
+                            SocialLinkBlock().to_python(
+                                {
+                                    "icon": "ico--youtube",
+                                    "text": "YouTube",
+                                    "link": candidate_block["data"]["youtube_url"],
+                                }
+                            )
+                        )
+
+                    if candidate_block["data"]["flickr_url"]:
+                        social_links.append(
+                            SocialLinkBlock().to_python(
+                                {
+                                    "icon": "ico--flickr",
+                                    "text": "Flickr",
+                                    "link": candidate_block["data"]["flickr_url"],
+                                }
+                            )
+                        )
+
+                    max_path_page = (
+                        Page.objects.filter(
+                            path__startswith=people_page.path,
+                            depth=parent_people_page.depth + 1,
+                        )
+                        .order_by("-path")
+                        .first()
                     )
-                )
 
-            if candidate_block["data"]["flickr_url"]:
-                social_links.append(
-                    SocialLinkBlock().to_python(
+                    if max_path_page:
+                        max_path = max_path_page.path
+                        next_path_suffix = (
+                            int(max_path[-4:], 36) + 1
+                        )  # Base-36 to handle alphanumeric paths
+                        next_path = (
+                            max_path[:-4] + f"{next_path_suffix:04X}"
+                        )  # Convert back to base-36
+                    else:
+                        next_path = parent_people_page.path + "0001"
+
+                    with transaction.atomic():
+                        candidate_page = DistrictPersonPage(
+                            title=candidate_block["data"]["title"],
+                            job=candidate_block["data"]["job"],
+                            profile_image_id=candidate_block["data"]["profile_photo"],
+                            email=candidate_block["data"]["email"],
+                            city=candidate_block["data"]["city"],
+                            age=candidate_block["data"]["age"],
+                            is_pirate=candidate_block["data"]["is_pirate"],
+                            other_party=candidate_block["data"]["other_party"],
+                            other_party_logo_id=candidate_block["data"]["other_party_logo"],
+                            social_links=social_links,
+                            content_type_id=people_page_content_type.id,
+                            locale_id=default_locale.id,
+                        )
+
+                        candidate_page.numchild = candidate_page.numchild + 1
+                        candidate_page.save(update_fields=["numchild"])
+
+                    program_data["combined_candidates"].append(
                         {
-                            "icon": "ico--flickr",
-                            "text": "Flickr",
-                            "link": candidate_block["data"]["flickr_url"],
+                            "position": candidate_block["position"],
+                            "page": candidate_page,
                         }
                     )
-                )
-
-            max_path_page = (
-                Page.objects.filter(
-                    path__startswith=people_page.path,
-                    depth=parent_people_page.depth + 1,
-                )
-                .order_by("-path")
-                .first()
-            )
 
-            if max_path_page:
-                max_path = max_path_page.path
-                next_path_suffix = (
-                    int(max_path[-4:], 36) + 1
-                )  # Base-36 to handle alphanumeric paths
-                next_path = (
-                    max_path[:-4] + f"{next_path_suffix:04X}"
-                )  # Convert back to base-36
-            else:
-                next_path = parent_people_page.path + "0001"
-
-            with transaction.atomic():
-                candidate_page = DistrictPersonPage(
-                    title=candidate_block["data"]["title"],
-                    job=candidate_block["data"]["job"],
-                    profile_image_id=candidate_block["data"]["profile_photo"],
-                    email=candidate_block["data"]["email"],
-                    city=candidate_block["data"]["city"],
-                    age=candidate_block["data"]["age"],
-                    is_pirate=candidate_block["data"]["is_pirate"],
-                    other_party=candidate_block["data"]["other_party"],
-                    other_party_logo_id=candidate_block["data"]["other_party_logo"],
-                    social_links=social_links,
-                    content_type_id=people_page_content_type.id,
-                    locale_id=default_locale.id,
+                # Sort candidates
+                program_data["combined_candidates"] = sorted(
+                    program_data["combined_candidates"],
+                    key=lambda value: value["position"],
                 )
 
-                candidate_page.numchild = candidate_page.numchild + 1
-                candidate_page.save(update_fields=["numchild"])
-
-            election_data["combined_candidates"].append(
-                {
-                    "position": candidate_block["position"],
-                    "data": candidate_page,
+                # Create candidate blocks
+                primary_candidates = []
+                secondary_candidates = []
+
+                for position, candidate in enumerate(program_data["combined_candidates"]):
+                    if position <= program_data["primary_candidate_count"]:
+                        primary_candidates.append({  # CandidateBlock value for CandidateListBlock
+                            "page": candidate["page"].id,
+                            "description": (
+                                candidate["page"].perex
+                                if len(candidate["page"].perex) > 32
+                                else (
+                                    candidate["page"].job
+                                    if candidate["page"].job
+                                    else candidate["page"].position
+                                )
+                            ),
+                            "image": (
+                                candidate["page"].profile_image.id
+                                # Not sure why there is a need to check for the second condition.
+                                # But, without it, the migration crashes on some images.
+                                if candidate["page"].profile_image is not None
+                                else 0
+                            )
+                        })
+                    else:
+                        secondary_candidates.append({  # SecondaryCandidateBlock value for CandidateSecondaryListBlock
+                            "number": candidate["position"],
+                            "page": candidate["page"].id,
+                        })
+
+                primary_candidates_block = {
+                    # Acts as CandidateListBlock
+                    "candidates": primary_candidates,
                 }
-            )
-
-        max_path_page = (
-            Page.objects.filter(
-                path__startswith=home_page.path, depth=home_page.depth + 1
-            )
-            .order_by("-path")
-            .first()
-        )
-
-        if max_path_page:
-            max_path = max_path_page.path
-            next_path_suffix = (
-                int(max_path[-4:], 36) + 1
-            )  # Base-36 to handle alphanumeric paths
-            next_path = (
-                max_path[:-4] + f"{next_path_suffix:04X}"
-            )  # Convert back to base-36
-        else:
-            next_path = home_page.path + "0001"
 
-        with transaction.atomic():
-            new_program_page = DistrictNewProgramPage(
-                title="Programy",
-                slug="programy",  # Make sure the slug is unique among siblings
-                path=next_path,
-                depth=home_page.depth + 1,
-                numchild=0,
-                content_type_id=new_program_page_content_type.id,
-                locale_id=default_locale.id,
-            )
-            new_program_page.save()
-
-            # Update numchild on the home page
-            home_page.numchild = home_page.numchild + 1
-            home_page.save(update_fields=["numchild"])
+                secondary_candidates_block = {
+                    # Acts as CandidateSecondaryListBlock
+                    "heading": "Další kandidáti",
+                    "candidates": secondary_candidates,
+                }
 
-        election_data["combined_candidates"] = sorted(
-            election_data["combined_candidates"],
-            key=lambda value: value["position"],
-        )
+                # if program_data["program_is_inline"]:
+
+                # There's no time left to set this up properly. We'll just use
+                # inline points for everything, at least for now.
+                
+                # Create program point blocks
+                program_points = []
+
+                for program_point in program_data["program_points"]:
+                    program_points.append({  # Act as ProgramBlockPopout
+                        "title": program_point["title"],
+                        "content": str(program_point["content"]),
+                        "guarantor": (
+                            program_point["guarantor_page"].id
+                            if program_point["guarantor_page"]
+                            else None
+                        )
+                    })
+
+                program_categories = [{  # Act as ProgramPopoutCategory
+                    "name": program_data["candidate_list_title"],
+                    "point_list": program_points
+                }]
+
+                program_block = {
+                    "type": "program_group_popout",
+                    "value": {
+                        "title": program_data["program_title"],
+                        "categories": program_categories
+                    }
+                }
 
-        new_program_block = ProgramGroupWithCandidatesBlock().to_python(
-            {
-                "title": election_data["title"],
-                "preamble_content": election_data["program_content_before"],
-                "primary_candidates": [],
-                "secondary_candidates": [],
-            }
-        )
+                # Finally, create a block for this program
+                new_program_block = ProgramGroupWithCandidatesBlock().to_python(
+                    {
+                        "title": program_data["title"],
+                        "preamble_content": program_data["program_content_before"],
+
+                        "primary_candidates": primary_candidates_block,
+                        "secondary_candidates": secondary_candidates_block,
+                        
+                        "program": [program_block]
+                    }
+                )
 
-    # Stop migration for debugging purposes
-    raise ValueError("Stopping migration for debugging purposes")
+                new_program_page.program.append((
+                    "program_group_with_candidates",
+                    new_program_block
+                ))
+                new_program_page.save()
 
 
 class Migration(migrations.Migration):
@@ -358,4 +470,16 @@ class Migration(migrations.Migration):
         ("district", "0217_alter_districthomepage_menu"),
     ]
 
-    operations = [migrations.RunPython(migrate_programs)]
+    operations = [
+        migrations.AlterField(
+            model_name='districtelectioncampaignpage',
+            name='candidates',
+            field=wagtail.fields.StreamField([('candidates', wagtail.blocks.StructBlock([('candidates', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['district.DistrictPersonPage'])), ('image', wagtail.images.blocks.ImageChooserBlock(help_text='Pokud není vybrán, použije se obrázek ze stránky kandidáta', label='Obrázek', required=False)), ('description', wagtail.blocks.TextBlock(label='Popis'))]), label=' '))]))], blank=True, verbose_name='Kandidátní listina'),
+        ),
+        migrations.AlterField(
+            model_name='districtnewprogrampage',
+            name='program',
+            field=wagtail.fields.StreamField([('program_group', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('point_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('url', wagtail.blocks.URLBlock(label='Odkaz pokrývající celou tuto část', required=False)), ('icon', wagtail.images.blocks.ImageChooserBlock(label='Ikona', required=False)), ('title', wagtail.blocks.CharBlock(label='Titulek článku programu')), ('text', wagtail.blocks.RichTextBlock(features=['h3', 'h4', 'h5', 'bold', 'italic', 'ol', 'ul', 'hr', 'link', 'document-link', 'image', 'superscript', 'subscript', 'strikethrough', 'blockquote', 'embed'], label='Obsah'))]), label='Jednotlivé články programu'))])), ('program_group_crossroad', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('point_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock(label='Obrázek')), ('title', wagtail.blocks.CharBlock(label='Titulek', required=True)), ('text', wagtail.blocks.RichTextBlock(label='Krátký text pod nadpisem', required=False)), ('page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['district.DistrictArticlePage', 'district.DistrictArticlesPage', 'district.DistrictCenterPage', 'district.DistrictContactPage', 'district.DistrictCrossroadPage', 'district.DistrictCustomPage', 'district.DistrictElectionCampaignPage', 'district.DistrictElectionProgramPage', 'district.DistrictElectionRootPage', 'district.DistrictPeoplePage', 'district.DistrictPersonPage', 'district.DistrictPostElectionStrategyPage', 'district.DistrictProgramPage'], required=False)), ('link', wagtail.blocks.URLBlock(label='Odkaz', required=False))]), label='Karty programu'))])), ('program_group_popout', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('categories', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('name', wagtail.blocks.CharBlock(label='Název')), ('icon', wagtail.images.blocks.ImageChooserBlock(label='Ikona', required=False)), ('description', wagtail.blocks.RichTextBlock(label='Popis', required=False)), ('point_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(label='Titulek vyskakovacího bloku')), ('content', wagtail.blocks.RichTextBlock(features=['h3', 'h4', 'h5', 'bold', 'italic', 'ol', 'ul', 'hr', 'link', 'document-link', 'image', 'superscript', 'subscript', 'strikethrough', 'blockquote', 'embed'], label='Obsah')), ('guarantor', wagtail.blocks.PageChooserBlock(label='Garant', page_type=['district.DistrictPersonPage'], required=False))]), label='Jednotlivé bloky programu'))]), label='Kategorie programu'))])), ('program_group_with_candidates', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('preamble_content', wagtail.blocks.RichTextBlock(help_text='Text, který se zobrazí před přepínačem mezi kandidáty a programem.', label='Preambule', required=False)), ('primary_candidates', wagtail.blocks.StructBlock([('candidates', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['district.DistrictPersonPage'])), ('image', wagtail.images.blocks.ImageChooserBlock(help_text='Pokud není vybrán, použije se obrázek ze stránky kandidáta', label='Obrázek', required=False)), ('description', wagtail.blocks.TextBlock(label='Popis'))]), label=' '))], help_text='Zobrazí se ve velkých blocích na začátku stránky.', label='Osoby na čele kandidátky')), ('secondary_candidates', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(default='Ostatní kandidátky', label='Nadpis zbytku kandidátky')), ('candidates', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('number', wagtail.blocks.CharBlock(label='Číslo')), ('page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['district.DistrictPersonPage'])), ('image', wagtail.images.blocks.ImageChooserBlock(help_text='Pokud není vybrán, použije se obrázek ze stránky kandidáta', label='Obrázek', required=False))]), label='Zbylí kandidáti na listině'))], help_text='Zobrazí se v kompaktním seznamu pod čelem kandidátky.', label='Ostatní osoby na kandidátce')), ('program', wagtail.blocks.StreamBlock([('program_group', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('point_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('url', wagtail.blocks.URLBlock(label='Odkaz pokrývající celou tuto část', required=False)), ('icon', wagtail.images.blocks.ImageChooserBlock(label='Ikona', required=False)), ('title', wagtail.blocks.CharBlock(label='Titulek článku programu')), ('text', wagtail.blocks.RichTextBlock(features=['h3', 'h4', 'h5', 'bold', 'italic', 'ol', 'ul', 'hr', 'link', 'document-link', 'image', 'superscript', 'subscript', 'strikethrough', 'blockquote', 'embed'], label='Obsah'))]), label='Jednotlivé články programu'))])), ('program_group_crossroad', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('point_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock(label='Obrázek')), ('title', wagtail.blocks.CharBlock(label='Titulek', required=True)), ('text', wagtail.blocks.RichTextBlock(label='Krátký text pod nadpisem', required=False)), ('page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['district.DistrictArticlePage', 'district.DistrictArticlesPage', 'district.DistrictCenterPage', 'district.DistrictContactPage', 'district.DistrictCrossroadPage', 'district.DistrictCustomPage', 'district.DistrictElectionCampaignPage', 'district.DistrictElectionProgramPage', 'district.DistrictElectionRootPage', 'district.DistrictPeoplePage', 'district.DistrictPersonPage', 'district.DistrictPostElectionStrategyPage', 'district.DistrictProgramPage'], required=False)), ('link', wagtail.blocks.URLBlock(label='Odkaz', required=False))]), label='Karty programu'))])), ('program_group_popout', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('categories', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('name', wagtail.blocks.CharBlock(label='Název')), ('icon', wagtail.images.blocks.ImageChooserBlock(label='Ikona', required=False)), ('description', wagtail.blocks.RichTextBlock(label='Popis', required=False)), ('point_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(label='Titulek vyskakovacího bloku')), ('content', wagtail.blocks.RichTextBlock(features=['h3', 'h4', 'h5', 'bold', 'italic', 'ol', 'ul', 'hr', 'link', 'document-link', 'image', 'superscript', 'subscript', 'strikethrough', 'blockquote', 'embed'], label='Obsah')), ('guarantor', wagtail.blocks.PageChooserBlock(label='Garant', page_type=['district.DistrictPersonPage'], required=False))]), label='Jednotlivé bloky programu'))]), label='Kategorie programu'))]))]))]))], blank=True, verbose_name='Programy'),
+        ),
+        migrations.RunPython(migrate_programs)
+    ]
diff --git a/district/models.py b/district/models.py
index 5b368e4d..d7a05d9c 100644
--- a/district/models.py
+++ b/district/models.py
@@ -797,7 +797,7 @@ class DistrictElectionProgramPage(
 
     ### RELATIONS
 
-    parent_page_types = ["district.DistrictElectionCampaignPage"]
+    parent_page_types = ["district.DistrictProgramPage"]
     subpage_types = []
 
     class Meta:
diff --git a/elections/migrations/0037_alter_electionshomepage_content.py b/elections/migrations/0037_alter_electionshomepage_content.py
new file mode 100644
index 00000000..376270d3
--- /dev/null
+++ b/elections/migrations/0037_alter_electionshomepage_content.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.4 on 2024-06-04 07:28
+
+import wagtail.blocks
+import wagtail.fields
+import wagtail.images.blocks
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('elections', '0036_alter_electionshomepage_menu'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='electionshomepage',
+            name='content',
+            field=wagtail.fields.StreamField([('carousel', wagtail.blocks.StructBlock([('desktop_image', wagtail.images.blocks.ImageChooserBlock(help_text='Pokud není vybráno video, ukáže se na desktopu.', label='Obrázek na pozadí (desktop)')), ('mobile_image', wagtail.images.blocks.ImageChooserBlock(help_text='Pokud je vybrán, ukáže se místo videa na mobilu.', label='Obrázek (mobil)', required=False)), ('video_url', wagtail.blocks.URLBlock(help_text='Pokud je vybráno, ukáže se na desktopech s povoleným autoplayem místo obrázku.', label='URL videa', required=False)), ('mobile_line_1', wagtail.blocks.TextBlock(label='První mobilní řádek')), ('mobile_line_2', wagtail.blocks.TextBlock(label='Druhý mobilní řádek'))])), ('candidates', wagtail.blocks.StructBlock([('candidates', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['elections.ElectionsCandidatePage'])), ('image', wagtail.images.blocks.ImageChooserBlock(help_text='Pokud není vybrán, použije se obrázek ze stránky kandidáta', label='Obrázek', required=False)), ('description', wagtail.blocks.TextBlock(label='Popis'))]), label='Kandidáti'))])), ('secondary_candidates', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(default='Ostatní kandidátky', label='Nadpis zbytku kandidátky')), ('candidates', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('number', wagtail.blocks.CharBlock(label='Číslo')), ('page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['elections.ElectionsCandidatePage'])), ('image', wagtail.images.blocks.ImageChooserBlock(help_text='Pokud není vybrán, použije se obrázek ze stránky kandidáta', label='Obrázek', required=False))]), label='Kandidáti'))])), ('program', wagtail.blocks.StructBlock([('label', wagtail.blocks.CharBlock(default='Program', help_text="Např. 'Program'", label='Nadpis')), ('categories', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('number', wagtail.blocks.IntegerBlock(label='Číslo')), ('name', wagtail.blocks.CharBlock(label='Název')), ('points', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('content', wagtail.blocks.TextBlock(label='Obsah'))]), label='Body'))]), label='Kategorie')), ('long_version_url', wagtail.blocks.URLBlock(help_text='Pro zobrazení odkazu na celou verzi programu musí být obě následující pole vyplněná.', label='Odkaz na celou verzi programu', required=False)), ('long_version_text', wagtail.blocks.CharBlock(label='Nadpis odkazu na celou verzi programu', required=False))])), ('news', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text='Nejnovější články se načtou automaticky', label='Titulek')), ('description', wagtail.blocks.TextBlock(label='Popis', required=False))], template='styleguide2/includes/organisms/articles/elections/articles_section.html')), ('calendar', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(label='Nadpis'))]))], blank=True, verbose_name='Hlavní obsah'),
+        ),
+    ]
diff --git a/main/migrations/0094_alter_mainprogrampage_program.py b/main/migrations/0094_alter_mainprogrampage_program.py
new file mode 100644
index 00000000..38666500
--- /dev/null
+++ b/main/migrations/0094_alter_mainprogrampage_program.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.4 on 2024-06-04 07:28
+
+import wagtail.blocks
+import wagtail.fields
+import wagtail.images.blocks
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('main', '0093_alter_mainhomepage_menu'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='mainprogrampage',
+            name='program',
+            field=wagtail.fields.StreamField([('program_group', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('point_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('url', wagtail.blocks.URLBlock(label='Odkaz pokrývající celou tuto část', required=False)), ('icon', wagtail.images.blocks.ImageChooserBlock(label='Ikona', required=False)), ('title', wagtail.blocks.CharBlock(label='Titulek článku programu')), ('text', wagtail.blocks.RichTextBlock(features=['h3', 'h4', 'h5', 'bold', 'italic', 'ol', 'ul', 'hr', 'link', 'document-link', 'image', 'superscript', 'subscript', 'strikethrough', 'blockquote', 'embed'], label='Obsah'))]), label='Jednotlivé články programu'))])), ('program_group_crossroad', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('point_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock(label='Obrázek')), ('title', wagtail.blocks.CharBlock(label='Titulek', required=True)), ('text', wagtail.blocks.RichTextBlock(label='Krátký text pod nadpisem', required=False)), ('page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['main.MainArticlesPage', 'main.MainArticlePage', 'main.MainProgramPage', 'main.MainPeoplePage', 'main.MainPersonPage', 'main.MainSimplePage', 'main.MainContactPage', 'main.MainCrossroadPage'], required=False)), ('link', wagtail.blocks.URLBlock(label='Odkaz', required=False))]), label='Karty programu'))])), ('program_group_popout', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('categories', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('name', wagtail.blocks.CharBlock(label='Název')), ('icon', wagtail.images.blocks.ImageChooserBlock(label='Ikona', required=False)), ('description', wagtail.blocks.RichTextBlock(label='Popis', required=False)), ('point_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(label='Titulek vyskakovacího bloku')), ('content', wagtail.blocks.RichTextBlock(features=['h3', 'h4', 'h5', 'bold', 'italic', 'ol', 'ul', 'hr', 'link', 'document-link', 'image', 'superscript', 'subscript', 'strikethrough', 'blockquote', 'embed'], label='Obsah')), ('guarantor', wagtail.blocks.PageChooserBlock(label='Garant', page_type=['district.DistrictPersonPage'], required=False))]), label='Jednotlivé bloky programu'))]), label='Kategorie programu'))])), ('elections_program', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...", label='Název programu')), ('program_page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['elections.ElectionsFullProgramPage'], required=False))]))], blank=True, verbose_name='Programy'),
+        ),
+    ]
-- 
GitLab