diff --git a/aleksis/core/admin.py b/aleksis/core/admin.py
index 106da0677831f722aefa1d28d23dde9fea838b06..0725c0aa5545797317a3bfac955cd854d28f227e 100644
--- a/aleksis/core/admin.py
+++ b/aleksis/core/admin.py
@@ -1,8 +1,10 @@
 from django.contrib import admin
 
-from .models import Group, Person, School, SchoolTerm
+from .models import Group, Person, School, SchoolTerm, Activity, Notification
 
 admin.site.register(Person)
 admin.site.register(Group)
 admin.site.register(School)
 admin.site.register(SchoolTerm)
+admin.site.register(Activity)
+admin.site.register(Notification)
diff --git a/aleksis/core/dashboard/__init__.py b/aleksis/core/dashboard/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/aleksis/core/dashboard/admin.py b/aleksis/core/dashboard/admin.py
deleted file mode 100644
index a9286d91effa36ce9fdb9e4cc020c2a3506da854..0000000000000000000000000000000000000000
--- a/aleksis/core/dashboard/admin.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from django.contrib import admin
-from .models import Activity, Notification, Cache
-
-
-class CacheAdmin(admin.ModelAdmin):
-    readonly_fields = ["id", "site_cache", "last_time_updated"]
-
-
-admin.site.register(Activity)
-admin.site.register(Notification)
-admin.site.register(Cache, CacheAdmin)
diff --git a/aleksis/core/dashboard/apps.py b/aleksis/core/dashboard/apps.py
deleted file mode 100644
index bc6084702acf8de9b788588f5813d689a8bd82bf..0000000000000000000000000000000000000000
--- a/aleksis/core/dashboard/apps.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.apps import AppConfig
-
-
-class DashboardConfig(AppConfig):
-    name = 'dashboard'
-    verbose_name = "Dashboard"
diff --git a/aleksis/core/dashboard/migrations/__init__.py b/aleksis/core/dashboard/migrations/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/aleksis/core/dashboard/models.py b/aleksis/core/dashboard/models.py
deleted file mode 100644
index e9b8e8467ec9ad44bb0de41205277022a97a1ec8..0000000000000000000000000000000000000000
--- a/aleksis/core/dashboard/models.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import datetime
-
-from django.core.cache import cache
-from django.db import models
-from django.contrib.auth.models import User
-from django.utils import timezone
-
-from mailer import send_mail_with_template
-
-
-class Activity(models.Model):
-    user = models.ForeignKey(to=User, on_delete=models.CASCADE)
-
-    title = models.CharField(max_length=150)
-    description = models.TextField(max_length=500)
-
-    app = models.CharField(max_length=100)
-
-    created_at = models.DateTimeField(default=timezone.now)
-
-    def __str__(self):
-        return self.title
-
-
-class Notification(models.Model):
-    user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="notifications")
-    title = models.CharField(max_length=150)
-    description = models.TextField(max_length=500)
-    link = models.URLField(blank=True)
-
-    app = models.CharField(max_length=100)
-
-    read = models.BooleanField(default=False)
-    mailed = models.BooleanField(default=False)
-    created_at = models.DateTimeField(default=timezone.now)
-
-    def __str__(self):
-        return self.title
-
-    def save(self):
-        super().save()
-        if not self.mailed:
-            send_mail_with_template(title, [user.email], "mail/notification.txt", "mail/notification.html", context)
-            self.mailed = True
diff --git a/aleksis/core/migrations/0001_initial.py b/aleksis/core/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..f8cc0d4d5294f0ca7b9243a3ce704d94169d855c
--- /dev/null
+++ b/aleksis/core/migrations/0001_initial.py
@@ -0,0 +1,119 @@
+# Generated by Django 3.0.2 on 2020-01-03 19:18
+
+import aleksis.core.mixins
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import image_cropping.fields
+import phonenumber_field.modelfields
+
+
+class Migration(migrations.Migration):
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Group',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=60, unique=True, verbose_name='Long name of group')),
+                ('short_name', models.CharField(max_length=16, unique=True, verbose_name='Short name of group')),
+            ],
+            options={
+                'ordering': ['short_name', 'name'],
+            },
+            bases=(models.Model, aleksis.core.mixins.ExtensibleModel),
+        ),
+        migrations.CreateModel(
+            name='School',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=30, verbose_name='Name')),
+                ('name_official',
+                 models.CharField(help_text='Official name of the school, e.g. as given by supervisory authority',
+                                  max_length=200, verbose_name='Official name')),
+                ('logo',
+                 image_cropping.fields.ImageCropField(blank=True, null=True, upload_to='', verbose_name='School logo')),
+                ('logo_cropping',
+                 image_cropping.fields.ImageRatioField('logo', '600x600', adapt_rotation=False, allow_fullsize=False,
+                                                       free_crop=False, help_text=None, hide_image_field=False,
+                                                       size_warning=True, verbose_name='logo cropping')),
+            ],
+            options={
+                'ordering': ['name', 'name_official'],
+            },
+        ),
+        migrations.CreateModel(
+            name='SchoolTerm',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('caption', models.CharField(max_length=30, verbose_name='Visible caption of the term')),
+                ('date_start', models.DateField(null=True, verbose_name='Effective start date of term')),
+                ('date_end', models.DateField(null=True, verbose_name='Effective end date of term')),
+                ('current', models.NullBooleanField(default=None, unique=True)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Person',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('is_active', models.BooleanField(default=True, verbose_name='Is person active?')),
+                ('first_name', models.CharField(max_length=30, verbose_name='First name')),
+                ('last_name', models.CharField(max_length=30, verbose_name='Last name')),
+                ('additional_name', models.CharField(blank=True, max_length=30, verbose_name='Additional name(s)')),
+                ('short_name',
+                 models.CharField(blank=True, max_length=5, null=True, unique=True, verbose_name='Short name')),
+                ('street', models.CharField(blank=True, max_length=30, verbose_name='Street')),
+                ('housenumber', models.CharField(blank=True, max_length=10, verbose_name='Street number')),
+                ('postal_code', models.CharField(blank=True, max_length=5, verbose_name='Postal code')),
+                ('place', models.CharField(blank=True, max_length=30, verbose_name='Place')),
+                ('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None,
+                                                                                verbose_name='Home phone')),
+                ('mobile_number',
+                 phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None,
+                                                                verbose_name='Mobile phone')),
+                ('email', models.EmailField(blank=True, max_length=254, verbose_name='E-mail address')),
+                ('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of birth')),
+                ('sex', models.CharField(blank=True, choices=[('f', 'female'), ('m', 'male')], max_length=1,
+                                         verbose_name='Sex')),
+                ('photo',
+                 image_cropping.fields.ImageCropField(blank=True, null=True, upload_to='', verbose_name='Photo')),
+                ('photo_cropping',
+                 image_cropping.fields.ImageRatioField('photo', '600x800', adapt_rotation=False, allow_fullsize=False,
+                                                       free_crop=False, help_text=None, hide_image_field=False,
+                                                       size_warning=True, verbose_name='photo cropping')),
+                ('import_ref', models.CharField(blank=True, editable=False, max_length=64, null=True, unique=True,
+                                                verbose_name='Reference ID of import source')),
+                ('guardians', models.ManyToManyField(blank=True, related_name='children', to='core.Person',
+                                                     verbose_name='Guardians / Parents')),
+                ('primary_group',
+                 models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.Group')),
+                ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
+                                              related_name='person', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['last_name', 'first_name'],
+            },
+            bases=(models.Model, aleksis.core.mixins.ExtensibleModel),
+        ),
+        migrations.AddField(
+            model_name='group',
+            name='members',
+            field=models.ManyToManyField(blank=True, related_name='member_of', to='core.Person'),
+        ),
+        migrations.AddField(
+            model_name='group',
+            name='owners',
+            field=models.ManyToManyField(blank=True, related_name='owner_of', to='core.Person'),
+        ),
+        migrations.AddField(
+            model_name='group',
+            name='parent_groups',
+            field=models.ManyToManyField(blank=True, related_name='child_groups', to='core.Group',
+                                         verbose_name='Parent groups'),
+        ),
+    ]
diff --git a/aleksis/core/migrations/0002_activity_notification.py b/aleksis/core/migrations/0002_activity_notification.py
new file mode 100644
index 0000000000000000000000000000000000000000..9679e4707063223fbc5fe5f9fdc5bcf926566874
--- /dev/null
+++ b/aleksis/core/migrations/0002_activity_notification.py
@@ -0,0 +1,42 @@
+# Generated by Django 3.0.2 on 2020-01-03 19:19
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('core', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Notification',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(max_length=150)),
+                ('description', models.TextField(max_length=500)),
+                ('link', models.URLField(blank=True)),
+                ('app', models.CharField(max_length=100)),
+                ('read', models.BooleanField(default=False)),
+                ('mailed', models.BooleanField(default=False)),
+                ('created_at', models.DateTimeField(default=django.utils.timezone.now)),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications',
+                                           to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Activity',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(max_length=150)),
+                ('description', models.TextField(max_length=500)),
+                ('app', models.CharField(max_length=100)),
+                ('created_at', models.DateTimeField(default=django.utils.timezone.now)),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+    ]
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index ab347b7e3451a5aacb7d34976b758ae968b7e600..811cfaeb03c17393a95668abce719e4e7470544c 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -1,13 +1,16 @@
 from typing import Optional
 
 from django.contrib.auth import get_user_model
+from django.contrib.auth.models import User
 from django.db import models
+from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 
 import dbsettings
 from image_cropping import ImageCropField, ImageRatioField
 from phonenumber_field.modelfields import PhoneNumberField
 
+from .mailer import send_mail_with_template
 from .mixins import ExtensibleModel
 
 
@@ -170,3 +173,40 @@ class Group(models.Model, ExtensibleModel):
 
     def __str__(self) -> str:
         return "%s (%s)" % (self.name, self.short_name)
+
+
+class Activity(models.Model):
+    user = models.ForeignKey(to=User, on_delete=models.CASCADE)
+
+    title = models.CharField(max_length=150)
+    description = models.TextField(max_length=500)
+
+    app = models.CharField(max_length=100)
+
+    created_at = models.DateTimeField(default=timezone.now)
+
+    def __str__(self):
+        return self.title
+
+
+class Notification(models.Model):
+    user = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name="notifications")
+    title = models.CharField(max_length=150)
+    description = models.TextField(max_length=500)
+    link = models.URLField(blank=True)
+
+    app = models.CharField(max_length=100)
+
+    read = models.BooleanField(default=False)
+    mailed = models.BooleanField(default=False)
+    created_at = models.DateTimeField(default=timezone.now)
+
+    def __str__(self):
+        return self.title
+
+    def save(self, **kwargs):
+        super().save(**kwargs)
+        if not self.mailed:
+            send_mail_with_template(self.title, [self.user.email], "mail/notification.txt", "mail/notification.html",
+                                    {"notification": self})
+            self.mailed = True