diff --git a/aleksis/core/managers.py b/aleksis/core/managers.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b791a5d1971161afcae59b6e59ec00414a435b6
--- /dev/null
+++ b/aleksis/core/managers.py
@@ -0,0 +1,81 @@
+from datetime import date
+from typing import Union
+
+from django.contrib.sites.managers import CurrentSiteManager as _CurrentSiteManager
+from django.db.models import QuerySet
+
+from calendarweek import CalendarWeek
+
+
+class CurrentSiteManagerWithoutMigrations(_CurrentSiteManager):
+    """CurrentSiteManager for auto-generating managers just by query sets."""
+
+    use_in_migrations = False
+
+
+class DateRangeQuerySetMixin:
+    """QuerySet with custom query methods for models with date ranges.
+
+    Filterable fields: date_start, date_end
+    """
+
+    def within_dates(self, start: date, end: date):
+        """Filter for all objects within a date range."""
+        return self.filter(date_start__lte=end, date_end__gte=start)
+
+    def in_week(self, wanted_week: CalendarWeek):
+        """Filter for all objects within a calendar week."""
+        return self.within_dates(wanted_week[0], wanted_week[6])
+
+    def on_day(self, day: date):
+        """Filter for all objects on a certain day."""
+        return self.within_dates(day, day)
+
+
+class SchoolYearQuerySet(QuerySet, DateRangeQuerySetMixin):
+    """Custom query set for school years."""
+
+
+class SchoolYearRelatedQuerySet(QuerySet):
+    """Custom query set for all models related to school years."""
+
+    def within_dates(self, start: date, end: date) -> "SchoolYearRelatedQuerySet":
+        """Filter for all objects within a date range."""
+        return self.filter(school_year__date_start__lte=end, school_year__date_end__gte=start)
+
+    def in_week(self, wanted_week: CalendarWeek) -> "SchoolYearRelatedQuerySet":
+        """Filter for all objects within a calendar week."""
+        return self.within_dates(wanted_week[0], wanted_week[6])
+
+    def on_day(self, day: date) -> "SchoolYearRelatedQuerySet":
+        """Filter for all objects on a certain day."""
+        return self.within_dates(day, day)
+
+    def for_school_year(self, school_year: "SchoolYear") -> "SchoolYearRelatedQuerySet":
+        return self.filter(school_year=school_year)
+
+    def for_current_school_year_or_all(self) -> "SchoolYearRelatedQuerySet":
+        """Get all objects related to current school year.
+
+        If there is no current school year, it will return all objects.
+        """
+        from aleksis.core.models import SchoolYear
+
+        current_school_year = SchoolYear.current
+        if current_school_year:
+            return self.for_school_year(current_school_year)
+        else:
+            return self
+
+    def for_current_school_year_or_none(self) -> Union["SchoolYearRelatedQuerySet", None]:
+        """Get all objects related to current school year.
+
+        If there is no current school year, it will return `None`.
+        """
+        from aleksis.core.models import SchoolYear
+
+        current_school_year = SchoolYear.current
+        if current_school_year:
+            return self.for_school_year(current_school_year)
+        else:
+            return None
diff --git a/aleksis/core/migrations/0002_school_year.py b/aleksis/core/migrations/0002_school_year.py
new file mode 100644
index 0000000000000000000000000000000000000000..ec66b7ab4ee9a34d382095e6b891113b95942f18
--- /dev/null
+++ b/aleksis/core/migrations/0002_school_year.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.0.6 on 2020-06-04 09:50
+
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sites', '0002_alter_domain_unique'),
+        ('core', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SchoolYear',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)),
+                ('name', models.CharField(max_length=255, unique=True, verbose_name='Name')),
+                ('date_start', models.DateField(verbose_name='Start date')),
+                ('date_end', models.DateField(verbose_name='End date')),
+                ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+            ],
+            options={
+                'verbose_name': 'School year',
+                'verbose_name_plural': 'School years',
+            },
+        ),
+    ]
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index b5bb204c25139c463e6d53c27b7c1ddf51b75576..5b0cbf720839240daad4edb2b708c734d8d45a0c 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -8,11 +8,13 @@ from django.contrib.auth.models import Group as DjangoGroup
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.sites.models import Site
+from django.core.exceptions import ValidationError
 from django.db import models
 from django.db.models import QuerySet
 from django.forms.widgets import Media
 from django.urls import reverse
 from django.utils import timezone
+from django.utils.decorators import classproperty
 from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
 
@@ -22,7 +24,8 @@ from image_cropping import ImageCropField, ImageRatioField
 from phonenumber_field.modelfields import PhoneNumberField
 from polymorphic.models import PolymorphicModel
 
-from .mixins import ExtensibleModel, PureDjangoModel
+from .managers import CurrentSiteManagerWithoutMigrations, SchoolYearQuerySet
+from .mixins import ExtensibleModel, PureDjangoModel, SchoolYearRelatedExtensibleModel
 from .tasks import send_notification
 from .util.core_helpers import get_site_preferences, now_tomorrow
 from .util.model_helpers import ICONS
@@ -43,6 +46,53 @@ FIELD_CHOICES = (
 )
 
 
+class SchoolYear(ExtensibleModel):
+    """School year model.
+
+    This is used to manage start and end times of a school year and link data to it.
+    """
+
+    objects = CurrentSiteManagerWithoutMigrations.from_queryset(SchoolYearQuerySet)()
+
+    name = models.CharField(verbose_name=_("Name"), max_length=255, unique=True)
+
+    date_start = models.DateField(verbose_name=_("Start date"))
+    date_end = models.DateField(verbose_name=_("End date"))
+
+    @classmethod
+    def get_current(cls, day: Optional[date] = None):
+        if not day:
+            day = timezone.now().date()
+        try:
+            return cls.objects.on_day(day).first()
+        except SchoolYear.DoesNotExist:
+            return None
+
+    @classproperty
+    def current(cls):
+        return cls.get_current()
+
+    def clean(self):
+        """Ensure there is only one school year of each point of time."""
+        if self.date_end < self.date_start:
+            raise ValidationError(_("The start date must be earlier than the end date."))
+
+        qs = SchoolYear.objects.within_dates(self.date_start, self.date_end)
+        if self.pk:
+            qs.exclude(pk=self.pk)
+        if qs.exists():
+            raise ValidationError(
+                _("There is already a school year for this time or a part of this time.")
+            )
+
+    def __str__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = _("School year")
+        verbose_name_plural = _("School years")
+
+
 class Person(ExtensibleModel):
     """Person model.
 
@@ -272,7 +322,7 @@ class Group(ExtensibleModel):
 
     name = models.CharField(verbose_name=_("Long name"), max_length=255, unique=True)
     short_name = models.CharField(
-        verbose_name=_("Short name"), max_length=255, unique=True, blank=True, null=True  # noqa
+        verbose_name=_("Short name"), max_length=255, blank=True, null=True  # noqa
     )
 
     members = models.ManyToManyField(