diff --git a/calendar_utils/models.py b/calendar_utils/models.py
index 1b3f4abf15dffcb62388505cc32393407cba7142..6caf230f352a2887a93dcea798ee7e170091770b 100644
--- a/calendar_utils/models.py
+++ b/calendar_utils/models.py
@@ -43,3 +43,36 @@ class Calendar(models.Model):
             self.future_events = list(map(_convert_arrow_to_datetime, future))
         self.last_update = arrow.utcnow().datetime
         self.save()
+
+
+class CalendarMixin(models.Model):
+    """
+    Mixin to be used in other models, like site settings, which adds relation
+    to Calendar.
+    """
+
+    calendar_url = models.URLField(
+        "URL kalendáře ve formátu iCal", blank=True, null=True
+    )
+    calendar = models.ForeignKey(Calendar, null=True, on_delete=models.PROTECT)
+
+    class Meta:
+        abstract = True
+
+    def save(self, *args, **kwargs):
+        # create or update related Calendar
+        if self.calendar_url:
+            if self.calendar:
+                if self.calendar.url != self.calendar_url:
+                    self.calendar.url = self.calendar_url
+                    self.calendar.save()
+            else:
+                self.calendar = Calendar.objects.create(url=self.calendar_url)
+            self.calendar.update_source()
+
+        # delete related Calendar when URL is cleared
+        if not self.calendar_url and self.calendar:
+            self.calendar.delete()
+            self.calendar = None
+
+        super().save(*args, **kwargs)
diff --git a/tests/calendar_utils/conftest.py b/tests/calendar_utils/conftest.py
index 736f38060ae6167881bc105562e4a609d52dc5e8..36da9cecb1f63e7d2e8e5ce4f7ba79e82adf8dc8 100644
--- a/tests/calendar_utils/conftest.py
+++ b/tests/calendar_utils/conftest.py
@@ -1,8 +1,27 @@
 from pathlib import Path
 
 import pytest
+from django.db import connection
+
+from calendar_utils.models import CalendarMixin
 
 
 @pytest.fixture(scope="session")
 def sample():
     return (Path(__file__).parent / "sample.ics").read_text()
+
+
+class DummyModel(CalendarMixin):
+    class Meta:
+        app_label = "__tests"
+
+
+@pytest.fixture(scope="session")
+def Dummy(django_db_setup, django_db_blocker):
+    with django_db_blocker.unblock():
+        with connection.schema_editor() as editor:
+            editor.create_model(DummyModel)
+    yield DummyModel
+    with django_db_blocker.unblock():
+        with connection.schema_editor() as editor:
+            editor.delete_model(DummyModel)
diff --git a/tests/calendar_utils/test_models.py b/tests/calendar_utils/test_models.py
index b81e21484bcc58e40b00f2601fd09b5137adc378..b24dd63841829389a7accf621ab1b3ed700f0faf 100644
--- a/tests/calendar_utils/test_models.py
+++ b/tests/calendar_utils/test_models.py
@@ -79,3 +79,64 @@ def test_calendar__save_and_load_events__no_values():
     cal.refresh_from_db()
     assert cal.past_events is None
     assert cal.future_events is None
+
+
+def test_calendar_mixin__no_calendar_url(Dummy):
+    obj = Dummy.objects.create()
+    assert obj.calendar is None
+
+
+def test_calendar_mixin__set_calendar_url(Dummy, mocker):
+    m_update = mocker.patch.object(Calendar, "update_source")
+    url = fake.url()
+
+    obj = Dummy.objects.create(calendar_url=url)
+
+    obj.refresh_from_db()
+    assert obj.calendar_url == url
+    assert obj.calendar.url == url
+    assert m_update.call_count == 1
+
+
+def test_calendar_mixin__update_calendar_url(Dummy, mocker):
+    m_update = mocker.patch.object(Calendar, "update_source")
+    obj = Dummy.objects.create(calendar_url=fake.url())
+    url = fake.url()
+    m_update.reset_mock()
+
+    obj.calendar_url = url
+    obj.save()
+
+    obj.refresh_from_db()
+    assert obj.calendar_url == url
+    assert obj.calendar.url == url
+    assert m_update.call_count == 1
+
+
+def test_calendar_mixin__unchanged_calendar_url(Dummy, mocker):
+    m_update = mocker.patch.object(Calendar, "update_source")
+    url = fake.url()
+    obj = Dummy.objects.create(calendar_url=url)
+    m_update.reset_mock()
+
+    obj.save()
+
+    obj.refresh_from_db()
+    assert obj.calendar_url == url
+    assert obj.calendar.url == url
+    assert m_update.call_count == 1
+
+
+def test_calendar_mixin__clear_calendar_url(Dummy, mocker):
+    m_update = mocker.patch.object(Calendar, "update_source")
+    obj = Dummy.objects.create(calendar_url=fake.url())
+    m_update.reset_mock()
+
+    obj.calendar_url = None
+    obj.save()
+
+    obj.refresh_from_db()
+    assert obj.calendar_url is None
+    assert obj.calendar is None
+    assert m_update.call_count == 0
+    assert Calendar.objects.count() == 0