diff --git a/calendar_utils/models.py b/calendar_utils/models.py
index 939e7c02d2a9c7ea24019a8ca838b71588e60946..2f11fa7439611706197d4ac24615b8e3f0ad44fd 100644
--- a/calendar_utils/models.py
+++ b/calendar_utils/models.py
@@ -1,3 +1,4 @@
+import json
 import logging
 from datetime import date, timedelta
 
@@ -102,3 +103,39 @@ class CalendarMixin(models.Model):
             self.calendar = None
 
         super().save(*args, **kwargs)
+
+
+class PersonCalendarMixin(CalendarMixin):
+    def get_parsed_calendar_data(self) -> list:
+        calendar_format_events = []
+
+        for event in (
+            self.calendar.past_events
+            + self.calendar.future_events
+        ):
+            parsed_event = {
+                "allDay": event["all_day"],
+                "start": event["start"].isoformat(),
+                "end": event["end"].isoformat(),
+            }
+
+            if event["summary"] is not None:
+                parsed_event["title"] = event["summary"]
+
+            if event["url"] is not None:
+                parsed_event["url"] = event["url"]
+
+            calendar_format_events.append(parsed_event)
+
+        return calendar_format_events
+
+    def get_context(self, request) -> dict:
+        context = super().get_context(request)
+
+        if self.calendar:
+            context["calendar_data"] = json.dumps(self.get_parsed_calendar_data())
+
+        return context
+
+    class Meta:
+        abstract = True
diff --git a/calendar_utils/parser.py b/calendar_utils/parser.py
index 1cefdbd6318438a4a03df51cc6004f3ef9e80f7c..d5795aa0908a81d4f39ac78e7ceaf2109c2e043b 100644
--- a/calendar_utils/parser.py
+++ b/calendar_utils/parser.py
@@ -9,7 +9,7 @@ from django.conf import settings
 if TYPE_CHECKING:
     from icalevents.icalparser import Event
 
-EVENT_KEYS = ("start", "end", "all_day", "summary", "description", "location")
+EVENT_KEYS = ("start", "end", "all_day", "summary", "description", "location", "url")
 
 
 def split_event_dict_list(event_list: "list[dict]") -> tuple[list[dict], list[dict]]:
diff --git a/district/migrations/0110_remove_districtpersonpage_ical_calendar_url_and_more.py b/district/migrations/0110_remove_districtpersonpage_ical_calendar_url_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..aaba09b6dde33a50c5a8235ece2f89bb45e4f949
--- /dev/null
+++ b/district/migrations/0110_remove_districtpersonpage_ical_calendar_url_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.1.8 on 2023-04-16 12:45
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('calendar_utils', '0004_auto_20220505_1228'),
+        ('district', '0109_districtpersonpage_ical_calendar_url'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='districtpersonpage',
+            name='ical_calendar_url',
+        ),
+        migrations.AddField(
+            model_name='districtpersonpage',
+            name='calendar',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='calendar_utils.calendar'),
+        ),
+        migrations.AddField(
+            model_name='districtpersonpage',
+            name='calendar_url',
+            field=models.URLField(blank=True, null=True, verbose_name='URL kalendáře ve formátu iCal'),
+        ),
+    ]
diff --git a/district/models.py b/district/models.py
index 9f99aee7eed335c7fc8e0fce67d7671380889fad..16e9c001837465d04977833a2be3b3f1afb39c1d 100644
--- a/district/models.py
+++ b/district/models.py
@@ -26,7 +26,7 @@ from wagtail.fields import RichTextField, StreamField
 from wagtail.models import Orderable, Page
 from wagtailmetadata.models import MetadataPageMixin
 
-from calendar_utils.models import CalendarMixin
+from calendar_utils.models import CalendarMixin, PersonCalendarMixin
 from maps_utils.blocks import MapPointBlock
 from maps_utils.const import (
     DEFAULT_MAP_STYLE,
@@ -50,7 +50,6 @@ from shared.models import (
     ExtendedMetadataHomePageMixin,
     ExtendedMetadataPageMixin,
     MenuMixin,
-    PersonCalendarMixin,
     SubpageMixin,
 )
 from shared.utils import make_promote_panels, strip_all_html_tags, trim_to_length
@@ -694,7 +693,7 @@ class DistrictPersonPage(
             ],
             "Kontaktní informace",
         ),
-        FieldPanel("ical_calendar_url"),
+        FieldPanel("calendar_url"),
         MultiFieldPanel(
             [
                 FieldPanel("facebook_url"),
diff --git a/district/templates/district/district_person_page.html b/district/templates/district/district_person_page.html
index 45097a431c43259987fcd5d40efb21534ca58700..a21b9f4bf4c2c5458f4ec7e1a4f9745822db25f8 100644
--- a/district/templates/district/district_person_page.html
+++ b/district/templates/district/district_person_page.html
@@ -24,7 +24,7 @@
       <div class="content-block w-full mb-16">
         {{ page.text|richtext }}
       </div>
-      {% if page.ical_calendar_url %}
+      {% if calendar_data %}
         <section>
           <h2 class="head-alt-md mb-3"><i class="ico--calendar mr-4"></i>Kalendář</h2>
           <ui-person-calendar events='{{ calendar_data|safe }}'></ui-person-calendar>
diff --git a/main/migrations/0056_remove_mainpersonpage_ical_calendar_url_and_more.py b/main/migrations/0056_remove_mainpersonpage_ical_calendar_url_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c828cdaa95ce9859687ec1eac3ce992a175be48
--- /dev/null
+++ b/main/migrations/0056_remove_mainpersonpage_ical_calendar_url_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.1.8 on 2023-04-16 12:45
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('calendar_utils', '0004_auto_20220505_1228'),
+        ('main', '0055_remove_mainpersonpage_nextcloud_calendar_url_and_more'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='mainpersonpage',
+            name='ical_calendar_url',
+        ),
+        migrations.AddField(
+            model_name='mainpersonpage',
+            name='calendar',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='calendar_utils.calendar'),
+        ),
+        migrations.AddField(
+            model_name='mainpersonpage',
+            name='calendar_url',
+            field=models.URLField(blank=True, null=True, verbose_name='URL kalendáře ve formátu iCal'),
+        ),
+    ]
diff --git a/main/models.py b/main/models.py
index 40eb46fc5618c12ce615cf8ea96f7a4570d1a1d4..a4c0f86b4184521a416599d4ec425d0542df53f9 100644
--- a/main/models.py
+++ b/main/models.py
@@ -29,6 +29,7 @@ from wagtail.models import Page
 from wagtail.search import index
 from wagtailmetadata.models import MetadataPageMixin
 
+from calendar_utils.models import PersonCalendarMixin
 from elections2021.constants import REGION_CHOICES  # pozor, import ze sousedního modulu
 from instagram_utils.models import InstagramPost
 from shared.forms import SubscribeForm
@@ -36,7 +37,6 @@ from shared.models import (  # MenuMixin,
     ArticleMixin,
     ExtendedMetadataHomePageMixin,
     ExtendedMetadataPageMixin,
-    PersonCalendarMixin,
     SubpageMixin,
 )
 from shared.utils import make_promote_panels, subscribe_to_newsletter
@@ -813,7 +813,7 @@ class MainPersonPage(
         FieldPanel("text"),
         FieldPanel("email"),
         FieldPanel("phone"),
-        FieldPanel("ical_calendar_url"),
+        FieldPanel("calendar_url"),
         FieldPanel("social_links"),
         FieldPanel("people"),
     ]
diff --git a/main/templates/main/main_person_page.html b/main/templates/main/main_person_page.html
index 4dedd0ef5e60ef8bb092468962979ff47087e3b2..5d5605757efdf68d73d281147241d26e25554f9f 100644
--- a/main/templates/main/main_person_page.html
+++ b/main/templates/main/main_person_page.html
@@ -109,7 +109,7 @@
       </section>
     {% endif %}
 
-    {% if page.ical_calendar_url %}
+    {% if calendar_data %}
       <section class="grid-container no-max mr-0 mb-4 xl:mb-20">
         <div class="grid-content-with-right-side">
           <h2 class="head-4xl text-left">
diff --git a/shared/models.py b/shared/models.py
index 0eee12b66bc5cabf255171bc1d414e61c34e0833..61789c0a9c82876174a7ba48fa5b7b5afec44541 100644
--- a/shared/models.py
+++ b/shared/models.py
@@ -198,66 +198,3 @@ class ExtendedMetadataPageMixin(models.Model):
             return super().get_meta_title()
 
         return f"{super().get_meta_title()} | {self.get_meta_title_suffix()}"
-
-
-class PersonCalendarMixin(models.Model):
-    ical_calendar_url = models.URLField(
-        "iCal adresa kalendáře",
-        max_length=256,
-        blank=True,
-        null=True,
-        help_text=(
-            "Podporuje Mrak, Google Kalendář a další. Návod na synchronizaci najdeš "
-            "na pi2.cz/kalendare"
-        ),
-    )
-
-    def get_context(self, request) -> dict:
-        context = super().get_context(request)
-
-        if self.ical_calendar_url:
-            context["calendar_data"] = self.get_ical_data()
-
-        return context
-
-    def get_ical_data(self) -> list:
-        ical_response = cache.get(f"calendar_{self.ical_calendar_url}")
-
-        if ical_response is None:
-            ical_response = requests.get(self.ical_calendar_url)
-            ical_response.raise_for_status()
-            ical_response = ical_response.text
-
-            cache.set(
-                f"calendar_{self.ical_calendar_url}",
-                ical_response,
-                timeout=3600,  # 1 hour
-            )
-
-        parsed_events = icalevents.parse_events(
-            ical_response,
-            start=date.today() - timedelta(days=30),
-            end=date.today() + timedelta(days=60),
-        )
-
-        calendar_format_events = []
-
-        for event in parsed_events:
-            parsed_event = {
-                "allDay": event.all_day,
-                "start": event.start.isoformat(),
-                "end": event.end.isoformat(),
-            }
-
-            if event.summary is not None:
-                parsed_event["title"] = event.summary
-
-            if event.url is not None:
-                parsed_event["url"] = event.url
-
-            calendar_format_events.append(parsed_event)
-
-        return json.dumps(calendar_format_events)
-
-    class Meta:
-        abstract = True