diff --git a/.gitignore b/.gitignore
index fcc12cfa5af171b811f3f92ddc29a783364d538c..4e43c2cca29a68a028594844be13bbe590de8ca6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,3 +56,8 @@ docs/_build/
 
 # Generated files
 biscuit/static/
+biscuit/node_modules/
+
+.coverage
+.tox/
+maintenance_mode_state.txt
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4222609531b74ee07ca97ff4f53cc1711c95839c..e88765a522ee299d86ed6d2965ead5ef22035c74 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,7 +9,8 @@ New features
 
 * Two-factor authentication with TOTP (Google Authenticator), Yubikey, SMS
   and phone call.
-
+* Devs: CRUDMixin provides a crud_event relation that returns all CRUD
+  events for an object
 
 `1.0a2`_
 --------
diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst
new file mode 100644
index 0000000000000000000000000000000000000000..9618c5f7d4285f366f05a5f8b2fd4cef43cf829b
--- /dev/null
+++ b/CODE_OF_CONDUCT.rst
@@ -0,0 +1,144 @@
+Contributor Covenant Code of Conduct
+====================================
+
+Our Pledge
+----------
+
+We as members, contributors, and leaders pledge to make participation in
+our community a harassment-free experience for everyone, regardless of
+age, body size, visible or invisible disability, ethnicity, sex
+characteristics, gender identity and expression, level of experience,
+education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open,
+welcoming, diverse, inclusive, and healthy community.
+
+Our Standards
+-------------
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+-  Demonstrating empathy and kindness toward other people
+-  Being respectful of differing opinions, viewpoints, and experiences
+-  Giving and gracefully accepting constructive feedback
+-  Accepting responsibility and apologizing to those affected by our
+   mistakes, and learning from the experience
+-  Focusing on what is best not just for us as individuals, but for the
+   overall community
+
+Examples of unacceptable behavior include:
+
+-  The use of sexualized language or imagery, and sexual attention or
+   advances of any kind
+-  Trolling, insulting or derogatory comments, and personal or political
+   attacks
+-  Public or private harassment
+-  Publishing others’ private information, such as a physical or email
+   address, without their explicit permission
+-  Other conduct which could reasonably be considered inappropriate in a
+   professional setting
+
+Enforcement Responsibilities
+----------------------------
+
+Community leaders are responsible for clarifying and enforcing our
+standards of acceptable behavior and will take appropriate and fair
+corrective action in response to any behavior that they deem
+inappropriate, threatening, offensive, or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other
+contributions that are not aligned to this Code of Conduct, and will
+communicate reasons for moderation decisions when appropriate.
+
+Scope
+-----
+
+This Code of Conduct applies within all community spaces, and also
+applies when an individual is officially representing the community in
+public spaces. Examples of representing our community include using an
+official e-mail address, posting via an official social media account,
+or acting as an appointed representative at an online or offline event.
+
+Enforcement
+-----------
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may
+be reported to the community leaders responsible for enforcement at
+foss@teckids.org. All complaints will be reviewed and investigated
+promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security
+of the reporter of any incident.
+
+Enforcement Guidelines
+----------------------
+
+Community leaders will follow these Community Impact Guidelines in
+determining the consequences for any action they deem in violation of
+this Code of Conduct:
+
+1. Correction
+~~~~~~~~~~~~~
+
+**Community Impact**: Use of inappropriate language or other behavior
+deemed unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders,
+providing clarity around the nature of the violation and an explanation
+of why the behavior was inappropriate. A public apology may be
+requested.
+
+2. Warning
+~~~~~~~~~~
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, for a specified period of
+time. This includes avoiding interactions in community spaces as well as
+external channels like social media. Violating these terms may lead to a
+temporary or permanent ban.
+
+3. Temporary Ban
+~~~~~~~~~~~~~~~~
+
+**Community Impact**: A serious violation of community standards,
+including sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No
+public or private interaction with the people involved, including
+unsolicited interaction with those enforcing the Code of Conduct, is
+allowed during this period. Violating these terms may lead to a
+permanent ban.
+
+4. Permanent Ban
+~~~~~~~~~~~~~~~~
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of
+individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction
+within the project community.
+
+Attribution
+-----------
+
+This Code of Conduct is adapted from the `Contributor
+Covenant <https://www.contributor-covenant.org>`__, version 2.0,
+available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by `Mozilla’s code of conduct
+enforcement ladder <https://github.com/mozilla/diversity>`__.
+
+For answers to common questions about this code of conduct, see the FAQ
+at https://www.contributor-covenant.org/faq. Translations are available
+at https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000000000000000000000000000000000000..74f35ea4898a7e78de7d9927504601da7f442aee
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,108 @@
+Development principles and contribution guidelines
+==================================================
+
+In order to create a high-quality software product, the BiscuIT developers
+have agreed upon fundamental principles governing the code layout, coding
+style and repository management for BiscuIT and all official apps.
+
+
+Coding layout and style
+-----------------------
+
+The coding style is defined in `PEP 8`_, with the following differences and
+decisions:
+
+- The maximum line length is 100 characters
+- Imports are structured in five blocks, each of them sorted as defined in
+  PEP 8:
+
+  1. Standard library imports
+  2. Django imports
+  3. Third-party imports
+  4. Imports from other BiscuIT apps (absolute imports)
+  5. Imports from the same BiscuIT app (realtive imports)
+
+- All string literals use single quotes
+
+For the layout of source trees and style recommendations specific to Django,
+the `Django coding style`_ is a good source of information, together with
+the `Django Best Practices`_ collection.
+
+
+Working with the Git repository
+-------------------------------
+
+The Git repository shall be used as a historic documentation of development
+and as change management. It is important that the Git commit history
+describes waht was changed, by whom and why.
+
+Feature branches
+~~~~~~~~~~~~~~~~
+
+All features and bug fixes should be developed in their own branch and later
+merged into the master branch as a whole. Of course, sometimes, it is
+sensible to not do that, e.g. for fixing mere typos and the like
+
+WIthin the feature branch, every logical step should be commited separately.
+It is neither required nor desired to do micro-commits about every
+development step. The commit history should describe the trains of thought
+the design and implementation is based on.
+
+Commit messages
+~~~~~~~~~~~~~~~
+
+Commit messages should be written as described in `How to Write a Git Commit
+Message`_.
+
+Commit messages should mention or even close any related issues. For merely
+mentioning progress on an issue, use the keyword `advances`; for closing an
+issue, use `closes`; for referring to a related issue for informational
+purposes, use `cf.`. This should be done in the body of the commit message.
+
+The subject of a commit message can (and should) be prepended with a tag in
+square brackets if it relates to a certain part of the repository, e.g. [CI]
+when changing CI/CD configuration or support code, [Dev] when changing
+something in the development utilities, etc.
+
+Manifestos governing development
+--------------------------------
+
+The FOSS community has created some manifestos describing several aspects of
+software development, to agree upon a baseline for these aspects. The
+BiscuIT developers have agreed to adhere to the following manifestos:
+
+- The `Sane software manifesto`_
+- The `Accessibility Manifesto`_
+- The `User Data Manifesto`_
+
+Not all theses from these manifestos are applicable. For example, most data
+about persons in a school information system are dictated by the school and
+probably governed by laws defining what and when to store. In that case,
+giving the user control over these decisions is not possible. Developers
+need to decide what should resonably be followed.
+
+
+Text documents
+--------------
+
+If there is no objective reason against it, all text documents accompanying
+the source use `reStructuredText`_.
+
+
+Contributing to upstream
+------------------------
+
+If possible and reasonable, code that can be of use to others in the general
+Django ecosystem shall be contributed to any upstream dependency, or a new
+generalised upstream dependency be created, under the most permissive
+licence possible.
+
+
+.. _PEP 8: https://pep8.org/
+.. _Django coding style: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/
+.. _Django Best Practices: https://django-best-practices.readthedocs.io/en/latest/index.html
+.. _How to Write a Git Commit Message: https://chris.beams.io/posts/git-commit/
+.. _Sane software manifesto: https://sane-software.globalcode.info/
+.. _Accessibility Manifesto: http://accessibilitymanifesto.com/
+.. _User Data Manifesto: https://userdatamanifesto.org/
+.. _reStructuredText: http://docutils.sourceforge.net/rst.html
diff --git a/Dockerfile b/Dockerfile
index 19605c6bfe73ea9ea2ef0eb71998a1c4e3d0f9b5..ec6f771d02496a26629df0f0959af7ff7cbf69c3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -21,11 +21,11 @@ RUN apt-get update && \
     apt-get install -y --no-install-recommends \
         build-essential \
 	gettext \
-	libjs-bootstrap4 \
 	libpq5 \
 	libpq-dev \
 	libssl-dev \
-	netcat-openbsd
+	netcat-openbsd \
+	yarnpkg
 
 # Install core dependnecies
 WORKDIR /usr/src/app
@@ -42,6 +42,7 @@ RUN mkdir -p /var/lib/biscuit/media /var/lib/biscuit/static /var/lib/biscuit/bac
 
 # Build messages and assets
 RUN python manage.py compilemessages; \
+    python manage.py yarn install; \
     python manage.py collectstatic --no-input --clear
 
 # Clean up build dependencies
@@ -50,12 +51,14 @@ RUN apt-get remove --purge -y \
         gettext \
         libpq-dev \
         libssl-dev \
-        python3-dev; \
+        yarnpkg; \
     apt-get autoremove --purge -y; \
     apt-get clean -y; \
     pip uninstall -y poetry; \
     rm -f /var/lib/apt/lists/*_*; \
-    rm -rf /root/.cache
+    rm -rf /root/.cache; \
+    rm -rf biscuit/node_modules; \
+    rm -rf /usr/local/lib/node_modules
 
 # Declare a persistent volume for all data
 VOLUME /var/lib/biscuit
diff --git a/MANIFEST.in b/MANIFEST.in
index 01c061d501e1dc451f69f0e7524590e3fb789755..9145ce4a03a7244a2637966e38c631672d9531ff 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,8 @@
+include CODE_OF_CONDUCT.rst
+include CONTRIBUTING.rst
 include LICENCE
+include manage.py
 recursive-include biscuit/core/static *
 recursive-include biscuit/core/templates *
 recursive-include biscuit/core/migrations *
+recursive-include docs *
diff --git a/apps/official/BiscuIT-App-Alsijil b/apps/official/BiscuIT-App-Alsijil
index a52358af2722329ae43916adee4b93208991ebc6..9014661e518c6d9457723571d7b0ef1ac9be6c6b 160000
--- a/apps/official/BiscuIT-App-Alsijil
+++ b/apps/official/BiscuIT-App-Alsijil
@@ -1 +1 @@
-Subproject commit a52358af2722329ae43916adee4b93208991ebc6
+Subproject commit 9014661e518c6d9457723571d7b0ef1ac9be6c6b
diff --git a/apps/official/BiscuIT-App-Chronos b/apps/official/BiscuIT-App-Chronos
index c3dffaa6d1cd91af1f79103e71286e11daf4d62d..d4da734afa405a6af2adbe607f553e2b759737d6 160000
--- a/apps/official/BiscuIT-App-Chronos
+++ b/apps/official/BiscuIT-App-Chronos
@@ -1 +1 @@
-Subproject commit c3dffaa6d1cd91af1f79103e71286e11daf4d62d
+Subproject commit d4da734afa405a6af2adbe607f553e2b759737d6
diff --git a/apps/official/BiscuIT-App-Exlibris b/apps/official/BiscuIT-App-Exlibris
index f18823b68731884e9ed661ce92474a54708a6f72..b03369df8214f062a18a70824c6a257cf4ab427d 160000
--- a/apps/official/BiscuIT-App-Exlibris
+++ b/apps/official/BiscuIT-App-Exlibris
@@ -1 +1 @@
-Subproject commit f18823b68731884e9ed661ce92474a54708a6f72
+Subproject commit b03369df8214f062a18a70824c6a257cf4ab427d
diff --git a/biscuit/core/admin.py b/biscuit/core/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..0810d4504654e0e1337c70810c41696d6e2abbd0
--- /dev/null
+++ b/biscuit/core/admin.py
@@ -0,0 +1,9 @@
+from django.contrib import admin
+
+from .models import Group, Person, School, SchoolTerm
+
+
+admin.site.register(Person)
+admin.site.register(Group)
+admin.site.register(School)
+admin.site.register(SchoolTerm)
diff --git a/biscuit/core/apps.py b/biscuit/core/apps.py
index 78aba32f89cab4c759e912dd7378459ac17f26de..ef62cfb3085fa7d763f4506bc94613fcc77c7c53 100644
--- a/biscuit/core/apps.py
+++ b/biscuit/core/apps.py
@@ -1,32 +1,13 @@
-from glob import glob
-import os
-from warnings import warn
-
 from django.apps import AppConfig, apps
-from django.conf import settings
-from django.db.utils import ProgrammingError
+from django.db.models.signals import post_save
+
+from .signals import clean_scss
 
 
 class CoreConfig(AppConfig):
     name = 'biscuit.core'
     verbose_name = 'BiscuIT - The Free School Information System'
 
-    def clean_scss(self) -> None:
-        for source_map in glob(os.path.join(settings.STATIC_ROOT, '*.css.map')):
-            try:
-                os.unlink(source_map)
-            except OSError:
-                # Ignore because old is better than nothing
-                pass  # noqa
-
-    def setup_data(self) -> None:
-        try:
-            apps.get_model('otp_yubikey', 'ValidationService').objects.update_or_create(
-                name='default', defaults={'use_ssl': True, 'param_sl': '', 'param_timeout': ''}
-            )
-        except ProgrammingError:
-            warn('Yubikey validation service could not be created yet. If you are currently in a migration, this is expected.')
-
     def ready(self) -> None:
-        self.clean_scss()
-        self.setup_data()
+        clean_scss()
+        post_save.connect(clean_scss, sender=apps.get_model('dbsettings', 'Setting'))
diff --git a/biscuit/core/migrations/0001_initial.py b/biscuit/core/migrations/0001_initial.py
index d0c78a5ae09b6a42c6721bc7d64b741e94ad6877..1a88ae010c8497c6e432931ec15438feda3e0aa1 100644
--- a/biscuit/core/migrations/0001_initial.py
+++ b/biscuit/core/migrations/0001_initial.py
@@ -1,6 +1,5 @@
 # Generated by Django 2.2.5 on 2019-09-03 18:30
 
-import biscuit.core.util.core_helpers
 from django.conf import settings
 from django.db import migrations, models
 from django.utils.translation import ugettext_lazy as _
@@ -70,7 +69,7 @@ class Migration(migrations.Migration):
                 ('import_ref', models.CharField(blank=True, editable=False, max_length=64, null=True, verbose_name='Reference ID of import source')),
                 ('guardians', models.ManyToManyField(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')),
-                ('school', models.ForeignKey(default=biscuit.core.util.core_helpers.get_current_school, on_delete=django.db.models.deletion.CASCADE, to='core.School')),
+                ('school', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School')),
                 ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='person', to=settings.AUTH_USER_MODEL)),
             ],
             options={
@@ -96,7 +95,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='group',
             name='school',
-            field=models.ForeignKey(default=biscuit.core.util.core_helpers.get_current_school, on_delete=django.db.models.deletion.CASCADE, to='core.School'),
+            field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School'),
         ),
         migrations.AlterUniqueTogether(
             name='group',
diff --git a/biscuit/core/migrations/0002_school_term.py b/biscuit/core/migrations/0002_school_term.py
index e3d25010df8d9f81116e2c5d1ccb7b7516a3f9bd..026d7e3a518fe4a4d299ca01500605afbdced4ca 100644
--- a/biscuit/core/migrations/0002_school_term.py
+++ b/biscuit/core/migrations/0002_school_term.py
@@ -1,6 +1,5 @@
 # Generated by Django 2.2.5 on 2019-09-14 12:55
 
-import biscuit.core.util.core_helpers
 from django.db import migrations, models
 import django.db.models.deletion
 from django.utils.translation import ugettext_lazy as _
@@ -32,7 +31,7 @@ class Migration(migrations.Migration):
                 ('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')),
-                ('school', models.ForeignKey(default=biscuit.core.util.core_helpers.get_current_school, on_delete=django.db.models.deletion.CASCADE, to='core.School')),
+                ('school', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.School')),
             ],
             options={
                 'abstract': False,
diff --git a/biscuit/core/migrations/0004_yubi_otp.py b/biscuit/core/migrations/0004_yubi_otp.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa7c40346d627fbbe00f27122a979941334a5e86
--- /dev/null
+++ b/biscuit/core/migrations/0004_yubi_otp.py
@@ -0,0 +1,19 @@
+from django.db import migrations
+
+
+def create_validation_service(apps, schema_editor):
+    apps.get_model('otp_yubikey', 'ValidationService').objects.update_or_create(
+        name='default', defaults={'use_ssl': True, 'param_sl': '', 'param_timeout': ''}
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0003_school_logo'),
+        ('otp_yubikey', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RunPython(create_validation_service),
+    ]
diff --git a/biscuit/core/migrations/0005_unlink_school.py b/biscuit/core/migrations/0005_unlink_school.py
new file mode 100644
index 0000000000000000000000000000000000000000..de030eb21bcf2d30a1537497e16f76f3fbc439d5
--- /dev/null
+++ b/biscuit/core/migrations/0005_unlink_school.py
@@ -0,0 +1,53 @@
+# Generated by Django 2.2.8 on 2019-12-09 08:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0004_yubi_otp'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='schoolterm',
+            name='school',
+        ),
+        migrations.AlterField(
+            model_name='group',
+            name='name',
+            field=models.CharField(max_length=60, unique=True, verbose_name='Long name of group'),
+        ),
+        migrations.AlterField(
+            model_name='group',
+            name='short_name',
+            field=models.CharField(max_length=16, unique=True, verbose_name='Short name of group'),
+        ),
+        migrations.AlterField(
+            model_name='person',
+            name='import_ref',
+            field=models.CharField(blank=True, editable=False, max_length=64, null=True, unique=True, verbose_name='Reference ID of import source'),
+        ),
+        migrations.AlterField(
+            model_name='person',
+            name='short_name',
+            field=models.CharField(blank=True, max_length=5, null=True, unique=True, verbose_name='Short name'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='group',
+            unique_together=set(),
+        ),
+        migrations.AlterUniqueTogether(
+            name='person',
+            unique_together=set(),
+        ),
+        migrations.RemoveField(
+            model_name='group',
+            name='school',
+        ),
+        migrations.RemoveField(
+            model_name='person',
+            name='school',
+        ),
+    ]
diff --git a/biscuit/core/mixins.py b/biscuit/core/mixins.py
index 024dd4870d076da301dd3c51be1a0e603e399144..9ec842a5ea99b793c688a63a9a2e8c1d5d9203b4 100644
--- a/biscuit/core/mixins.py
+++ b/biscuit/core/mixins.py
@@ -6,8 +6,6 @@ from django.db.models import QuerySet
 
 from easyaudit.models import CRUDEvent
 
-from .util.core_helpers import get_current_school
-
 
 class ExtensibleModel(object):
     """ Allow injection of code from BiscuIT apps to extend model functionality.
@@ -69,16 +67,10 @@ class ExtensibleModel(object):
 
         cls._safe_add(func, func.__name__)
 
-
-class SchoolRelated(models.Model):
+class CRUDMixin(models.Model):
     class Meta:
         abstract = True
 
-#    objects = SchoolRelatedManager()
-
-    school = models.ForeignKey(
-        'core.School', on_delete=models.CASCADE, default=get_current_school)
-
     @property
     def crud_events(self) -> QuerySet:
         """Get all CRUD events connected to this object from easyaudit."""
diff --git a/biscuit/core/models.py b/biscuit/core/models.py
index 9260fcbd3cd600240bb3967d2a6d4888a22928aa..ffd0189e1a901c1fa7349b8cf164b9da8f00b49d 100644
--- a/biscuit/core/models.py
+++ b/biscuit/core/models.py
@@ -4,10 +4,24 @@ from django.contrib.auth import get_user_model
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 
+import dbsettings
 from image_cropping import ImageCropField, ImageRatioField
 from phonenumber_field.modelfields import PhoneNumberField
 
-from .mixins import ExtensibleModel, SchoolRelated
+from .mixins import ExtensibleModel
+
+
+class ThemeSettings(dbsettings.Group):
+    colour_primary = dbsettings.StringValue(default='#007bff')
+    colour_secondary = dbsettings.StringValue(default='#6c757d')
+    colour_success = dbsettings.StringValue(default='#28a745')
+    colour_info = dbsettings.StringValue(default='#17a2b8')
+    colour_warning = dbsettings.StringValue(default='#ffc107')
+    colour_danger = dbsettings.StringValue(default='#dc3545')
+    colour_light = dbsettings.StringValue(default='#f8f9fa')
+    colour_dark = dbsettings.StringValue(default='#343a40')
+
+theme_settings = ThemeSettings('Global theme settings')
 
 
 class School(models.Model):
@@ -30,7 +44,7 @@ class School(models.Model):
         ordering = ['name', 'name_official']
 
 
-class SchoolTerm(SchoolRelated):
+class SchoolTerm(models.Model):
     """ Information about a term (limited time frame) that data can
     be linked to.
     """
@@ -44,13 +58,12 @@ class SchoolTerm(SchoolRelated):
         'Effective end date of term'), null=True)
 
 
-class Person(SchoolRelated, ExtensibleModel):
+class Person(models.Model, ExtensibleModel):
     """ A model describing any person related to a school, including, but not
     limited to, students, teachers and guardians (parents).
     """
 
     class Meta:
-        unique_together = [['school', 'short_name'], ['school', 'import_ref']]
         ordering = ['last_name', 'first_name']
 
     SEX_CHOICES = [
@@ -70,7 +83,7 @@ class Person(SchoolRelated, ExtensibleModel):
         'Additional name(s)'), max_length=30, blank=True)
 
     short_name = models.CharField(verbose_name=_(
-        'Short name'), max_length=5, blank=True, null=True)
+        'Short name'), max_length=5, blank=True, null=True, unique=True)
 
     street = models.CharField(verbose_name=_(
         'Street'), max_length=30, blank=True)
@@ -96,7 +109,8 @@ class Person(SchoolRelated, ExtensibleModel):
     photo_cropping = ImageRatioField('photo', '600x800', size_warning=True)
 
     import_ref = models.CharField(verbose_name=_(
-        'Reference ID of import source'), max_length=64, blank=True, null=True, editable=False)
+        'Reference ID of import source'), max_length=64,
+        blank=True, null=True, editable=False, unique=True)
 
     guardians = models.ManyToManyField('self', verbose_name=_('Guardians / Parents'),
                                        symmetrical=False, related_name='children')
@@ -132,19 +146,18 @@ class Person(SchoolRelated, ExtensibleModel):
         return self.full_name
 
 
-class Group(SchoolRelated, ExtensibleModel):
+class Group(models.Model, ExtensibleModel):
     """Any kind of group of persons in a school, including, but not limited
     classes, clubs, and the like.
     """
 
     class Meta:
-        unique_together = [['school', 'name'], ['school', 'short_name']]
         ordering = ['short_name', 'name']
 
     name = models.CharField(verbose_name=_(
-        'Long name of group'), max_length=60)
+        'Long name of group'), max_length=60, unique=True)
     short_name = models.CharField(verbose_name=_(
-        'Short name of group'), max_length=16)
+        'Short name of group'), max_length=16, unique=True)
 
     members = models.ManyToManyField('Person', related_name='member_of')
     owners = models.ManyToManyField('Person', related_name='owner_of')
diff --git a/biscuit/core/settings.py b/biscuit/core/settings.py
index 449d3483634e8acd49a0748c4320cd2ac3bf336e..61c92d3ea50a109873dbdb3e13f8e1bbd4100f9c 100644
--- a/biscuit/core/settings.py
+++ b/biscuit/core/settings.py
@@ -52,10 +52,12 @@ INSTALLED_APPS = [
     'sass_processor',
     'easyaudit',
     'dbbackup',
+    'dbsettings',
     'django_cron',
     'bootstrap4',
     'fa',
     'django_any_js',
+    'django_yarnpkg',
     'django_tables2',
     'easy_thumbnails',
     'image_cropping',
@@ -80,16 +82,10 @@ INSTALLED_APPS += get_app_packages()
 STATICFILES_FINDERS = [
     'django.contrib.staticfiles.finders.FileSystemFinder',
     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+    'django_yarnpkg.finders.NodeModulesFinder',
     'sass_processor.finders.CssFinder'
 ]
 
-SASS_PROCESSOR_AUTO_INCLUDE = False
-SASS_PROCESSOR_CUSTOM_FUNCTIONS = {
-    'get-colour': 'biscuit.core.util.sass_helpers.get_colour',
-}
-SASS_PROCESSOR_INCLUDE_DIRS = [
-    _settings.get('bootstrap.sass_path', '/usr/share/sass/bootstrap')
-]
 
 MIDDLEWARE = [
     #    'django.middleware.cache.UpdateCacheMiddleware',
@@ -233,6 +229,7 @@ USE_TZ = True
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/2.1/howto/static-files/
 
+
 STATIC_URL = _settings.get('static.url', '/static/')
 MEDIA_URL = _settings.get('media.url', '/media/')
 
@@ -241,41 +238,54 @@ LOGOUT_REDIRECT_URL = 'index'
 
 STATIC_ROOT = _settings.get('static.root', os.path.join(BASE_DIR, 'static'))
 MEDIA_ROOT = _settings.get('media.root', os.path.join(BASE_DIR, 'media'))
+NODE_MODULES_ROOT = _settings.get('node_modules.root', os.path.join(BASE_DIR, 'node_modules'))
+
+YARN_INSTALLED_APPS = [
+    'bootstrap',
+    'font-awesome',
+    'jquery',
+    'popper.js',
+    'datatables',
+    'select2'
+]
 
-FONT_AWESOME = {'url': _settings.get(
-    'bootstrap.fa_url', '/javascript/font-awesome/css/font-awesome.min.css')}
+JS_URL = _settings.get('js_assets.url', STATIC_URL)
+JS_ROOT = _settings.get('js_assets.root', NODE_MODULES_ROOT+'/node_modules')
+
+FONT_AWESOME = {'url': JS_URL+'/font-awesome/css/font-awesome.min.css'}
 
 BOOTSTRAP4 = {
-    'css_url': _settings.get('bootstrap.css_url', '/javascript/bootstrap4/css/bootstrap.min.css'),
-    'javascript_url': _settings.get('bootstrap.js_url', '/javascript/bootstrap4/js/bootstrap.min.js'),
-    'jquery_url': _settings.get('bootstrap.jquery_url', '/javascript/jquery/jquery.min.js'),
-    'popper_url': _settings.get('bootstrap.popper_url', '/javascript/popper.js/umd/popper.min.js'),
+    'css_url': JS_URL+'/bootstrap/dist//css/bootstrap.min.css',
+    'javascript_url': JS_URL+'/bootstrap/dist/js/bootstrap.min.js',
+    'jquery_url': JS_URL+'/jquery/dist/jquery.min.js',
+    'popper_url': JS_URL+'/popper.js/dist/umd/popper.min.js',
     'include_jquery': True,
     'include_popper': True,
     'javascript_in_head': True
 }
 
-DATATABLES_BASE = _settings.get(
-    'bootstrap.datatables_base', '/javascript/jquery-datatables')
+SELECT2_CSS = JS_URL+'/select2/dist/css/select2.min.css'
+SELECT2_JS = JS_URL+'/select2/dist/js/select2.min.js'
+SELECT2_I18N_PATH = JS_URL+'/select2/dist/js/i18n'
 
 ANY_JS = {
     'DataTables': {
-        'js_url': DATATABLES_BASE + '/jquery.dataTables.min.js'
+        'js_url': JS_URL+'/datatables/media/js/jquery.dataTables.min.js'
     },
     'DataTables-Bootstrap4': {
-        'css_url': DATATABLES_BASE + '/css/dataTables.bootstrap4.min.css',
-        'js_url': DATATABLES_BASE + '/dataTables.bootstrap4.min.js'
+        'css_url': JS_URL+'/datatables/media/css/dataTables.bootstrap4.min.css',
+        'js_url': JS_URL+'/datatables/media/js/dataTables.bootstrap4.min.js'
     }
 }
 
-COLOUR_PRIMARY = _settings.get('theme.colours.primary', '#007bff')
-COLOUR_SECONDARY = _settings.get('theme.colours.secondary', '#6c757d')
-COLOUR_SUCCESS = _settings.get('theme.colours.success', '#28a745')
-COLOUR_INFO = _settings.get('theme.colours.info', '#17a2b8')
-COLOUR_WARNING = _settings.get('theme.colours.warning', '#ffc107')
-COLOUR_DANGER = _settings.get('theme.colours.danger', '#dc3545')
-COLOUR_LIGHT = _settings.get('theme.colours.light', '#f8f9fa')
-COLOUR_DARK = _settings.get('theme.colours.dark', '#343a40')
+SASS_PROCESSOR_AUTO_INCLUDE = False
+SASS_PROCESSOR_CUSTOM_FUNCTIONS = {
+    'get-colour': 'biscuit.core.util.sass_helpers.get_colour',
+    'get-theme-setting': 'biscuit.core.util.sass_helpers.get_theme_setting',
+}
+SASS_PROCESSOR_INCLUDE_DIRS = [
+    _settings.get('bootstrap.sass_path', JS_ROOT+'/bootstrap/scss/')
+]
 
 ADMINS = _settings.get('contact.admins', [])
 SERVER_EMAIL = _settings.get('contact.from', 'root@localhost')
diff --git a/biscuit/core/signals.py b/biscuit/core/signals.py
new file mode 100644
index 0000000000000000000000000000000000000000..e531c5b9ed0158c3986144ab68e6c0eaeea9eda9
--- /dev/null
+++ b/biscuit/core/signals.py
@@ -0,0 +1,13 @@
+from glob import glob
+import os
+
+from django.conf import settings
+
+
+def clean_scss(*args, **kwargs) -> None:
+    for source_map in glob(os.path.join(settings.STATIC_ROOT, '*.css.map')):
+        try:
+            os.unlink(source_map)
+        except OSError:
+            # Ignore because old is better than nothing
+            pass  # noqa
diff --git a/biscuit/core/static/bootstrap_modified.scss b/biscuit/core/static/bootstrap_modified.scss
index d77585eb8d6c3137db9f0f35513e5185dbd7195a..a1e918db91287ca16e560ac3851790ba90fb6891 100644
--- a/biscuit/core/static/bootstrap_modified.scss
+++ b/biscuit/core/static/bootstrap_modified.scss
@@ -1,12 +1,12 @@
 $theme-colors: (
-    "primary":    adjust-color(get-colour(get-setting(COLOUR_PRIMARY)), $alpha: 1),
-    "secondary":  adjust-color(get-colour(get-setting(COLOUR_SECONDARY)), $alpha: 1),
-    "success":    adjust-color(get-colour(get-setting(COLOUR_SUCCESS)), $alpha: 1),
-    "info":       adjust-color(get-colour(get-setting(COLOUR_INFO)), $alpha: 1),
-    "warning":    adjust-color(get-colour(get-setting(COLOUR_WARNING)), $alpha: 1),
-    "danger":     adjust-color(get-colour(get-setting(COLOUR_DANGER)), $alpha: 1),
-    "light":      adjust-color(get-colour(get-setting(COLOUR_LIGHT)), $alpha: 1),
-    "dark":       adjust-color(get-colour(get-setting(COLOUR_DARK)), $alpha: 1),
+    "primary":    adjust-color(get-colour(get-theme-setting(colour_primary)), $alpha: 1),
+    "secondary":  adjust-color(get-colour(get-theme-setting(colour_secondary)), $alpha: 1),
+    "success":    adjust-color(get-colour(get-theme-setting(colour_success)), $alpha: 1),
+    "info":       adjust-color(get-colour(get-theme-setting(colour_info)), $alpha: 1),
+    "warning":    adjust-color(get-colour(get-theme-setting(colour_warning)), $alpha: 1),
+    "danger":     adjust-color(get-colour(get-theme-setting(colour_danger)), $alpha: 1),
+    "light":      adjust-color(get-colour(get-theme-setting(colour_light)), $alpha: 1),
+    "dark":       adjust-color(get-colour(get-theme-setting(colour_dark)), $alpha: 1),
 );
 
 @import "bootstrap";
diff --git a/biscuit/core/tests/models/test_person.py b/biscuit/core/tests/models/test_person.py
new file mode 100644
index 0000000000000000000000000000000000000000..e454d3b60709498564c1c005242694b08ad805bb
--- /dev/null
+++ b/biscuit/core/tests/models/test_person.py
@@ -0,0 +1,13 @@
+import pytest
+
+from biscuit.core.models import Person
+
+
+@pytest.mark.django_db
+def test_full_name():
+    _person = Person.objects.create(
+            first_name='Jane',
+            last_name='Doe'
+    )
+
+    assert _person.full_name == 'Doe, Jane'
diff --git a/biscuit/core/tests/templatetags/test_data_helpers.py b/biscuit/core/tests/templatetags/test_data_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..096903947c8c21870ad5c05fcffb5b0a8e522a12
--- /dev/null
+++ b/biscuit/core/tests/templatetags/test_data_helpers.py
@@ -0,0 +1,22 @@
+from biscuit.core.templatetags.data_helpers import get_dict
+
+def test_get_dict_object():
+    class _Foo(object):
+        bar = 12
+
+    assert _Foo.bar == get_dict(_Foo, 'bar')
+
+def test_get_dict_dict():
+    _foo = {'bar': 12}
+
+    assert _foo['bar'] == get_dict(_foo, 'bar')
+
+def test_get_dict_list():
+    _foo = [10, 11, 12]
+
+    assert _foo[2] == get_dict(_foo, 2)
+
+def test_get_dict_invalid():
+    _foo = 12
+
+    assert get_dict(_foo, 'bar') is None
diff --git a/biscuit/core/tests/test_person.py b/biscuit/core/tests/test_person.py
deleted file mode 100644
index f267e5ae0500f51c83572e5a3d0f76d80dc14cb4..0000000000000000000000000000000000000000
--- a/biscuit/core/tests/test_person.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from django.test import TestCase
-
-from biscuit.core.models import Person
-
-
-class PersonTestCase(TestCase):
-    def setUp(self):
-        self._person = Person.objects.create(
-            first_name='Jane',
-            last_name='Doe'
-        )
-
-    def test_full_name(self):
-        assert self._person.full_name == 'Doe, Jane'
diff --git a/biscuit/core/tests/views/test_account.py b/biscuit/core/tests/views/test_account.py
new file mode 100644
index 0000000000000000000000000000000000000000..3bffcf17e9e226ac04230d367219e3cc5942a835
--- /dev/null
+++ b/biscuit/core/tests/views/test_account.py
@@ -0,0 +1,53 @@
+import pytest
+
+from django.conf import settings
+from django.urls import reverse
+
+@pytest.mark.django_db
+def test_index_not_logged_in(client):
+    response = client.get('/')
+
+    assert response.status_code == 200
+    assert reverse(settings.LOGIN_URL) in response.content.decode('utf-8')
+
+@pytest.mark.django_db
+def test_login(client, django_user_model):
+    username = 'foo'
+    password = 'bar'
+
+    django_user_model.objects.create_user(username=username, password=password)
+    client.login(username=username, password=password)
+
+    response = client.get('/')
+
+    assert response.status_code == 200
+    assert reverse(settings.LOGIN_URL) not in response.content.decode('utf-8')
+
+@pytest.mark.django_db
+def test_index_not_linked_to_person(client, django_user_model):
+    username = 'foo'
+    password = 'bar'
+
+    django_user_model.objects.create_user(username=username, password=password)
+    client.login(username=username, password=password)
+
+    response = client.get('/')
+
+    assert response.status_code == 200
+    assert 'You are not linked to a person' in response.content.decode('utf-8')
+
+@pytest.mark.django_db
+def test_logout(client, django_user_model):
+    username = 'foo'
+    password = 'bar'
+
+    django_user_model.objects.create_user(username=username, password=password)
+    client.login(username=username, password=password)
+
+    response = client.get('/')
+    assert response.status_code == 200
+
+    response = client.get(reverse('logout'), follow=True)
+
+    assert response.status_code == 200
+    assert reverse(settings.LOGIN_URL) in response.content.decode('utf-8')
diff --git a/biscuit/core/urls.py b/biscuit/core/urls.py
index 414aa9b24aa78acf2f9f8e4114a65a6b8eeda585..dd98dc0175b1e6a13605208615f6fa2ca1482342 100644
--- a/biscuit/core/urls.py
+++ b/biscuit/core/urls.py
@@ -1,4 +1,5 @@
 from django.apps import apps
+from django.contrib import admin
 from django.conf import settings
 from django.conf.urls.static import static
 from django.contrib.auth import views as auth_views
@@ -10,6 +11,7 @@ from two_factor.urls import urlpatterns as tf_urls
 from . import views
 
 urlpatterns = [
+    path('admin/', admin.site.urls),
     path('data_management/', views.data_management, name='data_management'),
     path('status/', views.system_status, name='system_status'),
     path('school_management', views.school_management, name='school_management'),
@@ -35,9 +37,14 @@ urlpatterns = [
     path('contact/', include('contact_form.urls')),
     path('impersonate/', include('impersonate.urls')),
     path('__i18n__/', include('django.conf.urls.i18n')),
-    path('select2/', include('django_select2.urls'))
+    path('select2/', include('django_select2.urls')),
+    path('settings/', include('dbsettings.urls'))
 ]
 
+# Serve static files from STATIC_ROOT to make it work with runserver
+# collectstatic is also required in development for this
+urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+
 # Add URLs for optional features
 if hasattr(settings, 'TWILIO_ACCOUNT_SID'):
     from two_factor.gateways.twilio.urls import urlpatterns as tf_twilio_urls  # noqa
@@ -45,8 +52,6 @@ if hasattr(settings, 'TWILIO_ACCOUNT_SID'):
 
 # Serve javascript-common if in development
 if settings.DEBUG:
-    urlpatterns += static('/javascript/',
-                          document_root='/usr/share/javascript/')
     urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
 
 # Automatically mount URLs from all installed BiscuIT apps
diff --git a/biscuit/core/util/core_helpers.py b/biscuit/core/util/core_helpers.py
index bf19da058ae245bf4e9f47247c40786ab1d2d761..e8c0b1fe28c167b25f39eae946077c2d28832723 100644
--- a/biscuit/core/util/core_helpers.py
+++ b/biscuit/core/util/core_helpers.py
@@ -29,10 +29,6 @@ def get_app_packages() -> Sequence[str]:
     return pkgs
 
 
-def get_current_school() -> int:
-    return 1
-
-
 def is_impersonate(request: HttpRequest) -> bool:
     if hasattr(request, 'user'):
         return getattr(request.user, 'is_impersonate', False)
diff --git a/biscuit/core/util/sass_helpers.py b/biscuit/core/util/sass_helpers.py
index aa28accf32e4cbd42711c7b69ccc2bf8e8dfbc22..c609c0ed9941c1485edfb1d7e8111c1a06b414eb 100644
--- a/biscuit/core/util/sass_helpers.py
+++ b/biscuit/core/util/sass_helpers.py
@@ -1,9 +1,15 @@
 from colour import web2hex
 from sass import SassColor
 
+from biscuit.core.models import theme_settings
+
 
 def get_colour(html_colour: str) -> SassColor:
     rgb = web2hex(html_colour, force_long=True)[1:]
     r, g, b = int(rgb[0:2], 16), int(rgb[2:4], 16), int(rgb[4:6], 16)
 
     return SassColor(r, g, b, 255)
+
+
+def get_theme_setting(setting: str) -> str:
+    return getattr(theme_settings, setting, '')
diff --git a/biscuit/core/views.py b/biscuit/core/views.py
index a32b9abe19b3131304c0a568d643a8336acd70d1..585d049ae72bd65c0a8ffd16bba88056850f2f04 100644
--- a/biscuit/core/views.py
+++ b/biscuit/core/views.py
@@ -10,7 +10,7 @@ from django_cron.models import CronJobLog
 
 from .decorators import admin_required
 from .forms import PersonsAccountsFormSet, EditPersonForm, EditGroupForm, EditSchoolForm, EditTermForm
-from .models import Person, Group
+from .models import Person, Group, School
 from .tables import PersonsTable, GroupsTable
 from .util import messages
 
@@ -198,7 +198,7 @@ def school_management(request: HttpRequest) -> HttpResponse:
 def edit_school(request: HttpRequest) -> HttpResponse:
     context = {}
 
-    school = request.user.person.school
+    school = School.objects.first()
     edit_school_form = EditSchoolForm(request.POST or None, request.FILES or None, instance=school)
 
     context['school'] = school
@@ -219,7 +219,7 @@ def edit_school(request: HttpRequest) -> HttpResponse:
 def edit_schoolterm(request: HttpRequest) -> HttpResponse:
     context = {}
 
-    term = request.user.person.school.current_term
+    term = School.objects.first().current_term
     edit_term_form = EditTermForm(request.POST or None, instance=term)
 
     if request.method == 'POST':
diff --git a/code_of_conduct.md b/code_of_conduct.md
deleted file mode 100644
index d9f811167cd7975114f6cfc8efdd90bc8dd1f9cc..0000000000000000000000000000000000000000
--- a/code_of_conduct.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-We as members, contributors, and leaders pledge to make participation in our
-community a harassment-free experience for everyone, regardless of age, body
-size, visible or invisible disability, ethnicity, sex characteristics,
-gender identity and expression, level of experience, education,
-socio-economic status, nationality, personal appearance, race, religion, or
-sexual identity and orientation.
-
-We pledge to act and interact in ways that contribute to an open, welcoming,
-diverse, inclusive, and healthy community.
-
-## Our Standards
-
-Examples of behavior that contributes to a positive environment for our
-community include:
-
-* Demonstrating empathy and kindness toward other people
-* Being respectful of differing opinions, viewpoints, and experiences
-* Giving and gracefully accepting constructive feedback
-* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the overall community
-
-Examples of unacceptable behavior include:
-
-* The use of sexualized language or imagery, and sexual attention or
-  advances of any kind
-* Trolling, insulting or derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or email
-  address, without their explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
-  professional setting
-
-## Enforcement Responsibilities
-
-Community leaders are responsible for clarifying and enforcing our standards
-of acceptable behavior and will take appropriate and fair corrective action
-in response to any behavior that they deem inappropriate, threatening,
-offensive, or harmful.
-
-Community leaders have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, and will communicate reasons
-for moderation decisions when appropriate.
-
-## Scope
-
-This Code of Conduct applies within all community spaces, and also applies
-when an individual is officially representing the community in public
-spaces.  Examples of representing our community include using an official
-e-mail address, posting via an official social media account, or acting as
-an appointed representative at an online or offline event.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the community leaders responsible for enforcement at
-<foss@teckids.org>.  All complaints will be reviewed and investigated
-promptly and fairly.
-
-All community leaders are obligated to respect the privacy and security of
-the reporter of any incident.
-
-## Enforcement Guidelines
-
-Community leaders will follow these Community Impact Guidelines in
-determining the consequences for any action they deem in violation of this
-Code of Conduct:
-
-### 1. Correction
-
-**Community Impact**: Use of inappropriate language or other behavior deemed
-unprofessional or unwelcome in the community.
-
-**Consequence**: A private, written warning from community leaders,
-providing clarity around the nature of the violation and an explanation of
-why the behavior was inappropriate.  A public apology may be requested.
-
-### 2. Warning
-
-**Community Impact**: A violation through a single incident or series of
-actions.
-
-**Consequence**: A warning with consequences for continued behavior.  No
-interaction with the people involved, including unsolicited interaction with
-those enforcing the Code of Conduct, for a specified period of time.  This
-includes avoiding interactions in community spaces as well as external
-channels like social media.  Violating these terms may lead to a temporary
-or permanent ban.
-
-### 3. Temporary Ban
-
-**Community Impact**: A serious violation of community standards, including
-sustained inappropriate behavior.
-
-**Consequence**: A temporary ban from any sort of interaction or public
-communication with the community for a specified period of time.  No public
-or private interaction with the people involved, including unsolicited
-interaction with those enforcing the Code of Conduct, is allowed during this
-period.  Violating these terms may lead to a permanent ban.
-
-### 4. Permanent Ban
-
-**Community Impact**: Demonstrating a pattern of violation of community
-standards, including sustained inappropriate behavior,  harassment of an
-individual, or aggression toward or disparagement of classes of individuals.
-
-**Consequence**: A permanent ban from any sort of public interaction within
-the project community.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
-available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
-
-Community Impact Guidelines were inspired by [Mozilla's code of conduct
-enforcement ladder](https://github.com/mozilla/diversity).
-
-[homepage]: https://www.contributor-covenant.org
-
-For answers to common questions about this code of conduct, see the FAQ at
-https://www.contributor-covenant.org/faq.  Translations are available at
-https://www.contributor-covenant.org/translations.
diff --git a/dev.sh b/dev.sh
index aa951f4d73254b51fbab0eda25a4521678a28b35..6a183f4000d6ce6e9ccf71c186e35193ec1a34c7 100755
--- a/dev.sh
+++ b/dev.sh
@@ -17,6 +17,7 @@ case "$1" in
 	remove_pip_metadata
 	poetry run ./manage.py migrate
 	poetry run ./manage.py compilemessages
+	poetry run ./manage.py yarn install
 	poetry run ./manage.py collectstatic --no-input
 	set +e
 	;;
diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile
index 9645e8f005e8fcb5df44150d101e5166da4a60d0..e47ed67f547c508c1d18dc5ae6184f1fef90f873 100644
--- a/docker/nginx/Dockerfile
+++ b/docker/nginx/Dockerfile
@@ -3,7 +3,4 @@ FROM nginx
 RUN rm /etc/nginx/conf.d/default.conf
 COPY nginx.conf /etc/nginx/conf.d
 
-RUN apt-get update && apt-get upgrade -y
-RUN apt-get install -y libjs-bootstrap4 fonts-font-awesome libjs-jquery libjs-popper.js libjs-jquery-datatables
-
 RUN mkdir /var/lib/biscuit
diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf
index 41f0bc39eeeb1233fdad9460ba953ca4e0dc3319..39c2ce0be8a1992be9215f64da3ce7ed02527c7d 100644
--- a/docker/nginx/nginx.conf
+++ b/docker/nginx/nginx.conf
@@ -19,8 +19,4 @@ server {
     location /static/ {
         alias /var/lib/biscuit/static/;
     }
-
-    location /javascript/ {
-        alias /usr/share/javascript/;
-    }
 }
diff --git a/docs/dev/01_setup.rst b/docs/dev/01_setup.rst
index c036f5c831a0b18f0c43bbb71a75b6b5edb475b0..8d4cd0d8b9cfae6c631d7803eec7565274f5149d 100644
--- a/docs/dev/01_setup.rst
+++ b/docs/dev/01_setup.rst
@@ -9,6 +9,7 @@ Poetry makes a lot of stuff very easy, especially managing a virtual
 environment that contains BiscuIT and everything you need to run the
 framework and selected apps.
 
+Also, `Yarn`_ is needed to resolve JavaScript dependencies.
 
 Get the source tree
 -------------------
@@ -16,7 +17,7 @@ Get the source tree
 To download BiscuIT and all officially bundled apps in their
 development version, use Git like so::
 
-  git clone --recurse-submodules https://edugit.org/Teckids/BiscuIT/BiscuIT-ng
+  git clone --recurse-submodules https://edugit.org/BiscuIT/BiscuIT-ng
 
 If you do not want to download the bundled apps, leave out the
 ``--recurse-submodules`` option.
@@ -45,13 +46,36 @@ installing BiscuIT is a matter of::
   poetry install
 
 
-Running commands in the virtual environment
--------------------------------------------
+Regular tasks
+-------------
 
-To run commands in the virtual environment, use Poetry's ``run``
-command::
+After making changes to the environment, e.g. installing apps or updates,
+some maintenance tasks need to be done:
 
-  poetry run ./manage.py runserver
+1. Download and install JavaScript dependencies
+2. Collect static files
+3. Run database migrations
+
+All three steps can be done with the ``poetry run`` command and
+``manage.py``::
+
+  poetry run ./manage.py yarn install
+  poetry run ./manage.py collectstatic
+  poetry run ./manage.py migrate
+
+(You might need database settings for the `migrate` command; see below.)
+
+Running the development server
+------------------------------
+
+The development server can be started using Django's ``runserver`` command.
+You can either configure BiscuIT like in a production environment, or pass
+basic settings in as environment variable. Here is an example that runs the
+development server against a local PostgreSQL database with password
+`biscuit` (all else remains default) and with the `debug` setting enabled::
+
+  BISCUIT_debug=true BISCUIT_database__password=biscuit poetry run ./manage.py runserver
 
 .. _Poetry: https://poetry.eustace.io/
 .. _Poetry installation methods: https://poetry.eustace.io/docs/#installation
+.. _Yarn: https://yarnpkg.com
diff --git a/docs/dev/02_install_apps.rst b/docs/dev/02_install_apps.rst
index 9ecfcc595f24cf6a6577264aea617ce2088c1c37..b083baf6cb5710db1a84a7cb8255ee678e9db07e 100644
--- a/docs/dev/02_install_apps.rst
+++ b/docs/dev/02_install_apps.rst
@@ -17,11 +17,5 @@ This will install the Exlibris app (library management) app by using a
 shell for first ``cd``'ing into the app directory and then using
 poetry to install the app.
 
-
-Migrate the database
---------------------
-
-After installing or updating any apps, the database must be updated as
-well by running Django's ``migrate`` command::
-
-  poetry run ./manage.py migrate
+DO not forget to run the maintenance tasks described earlier after
+installign any app.
diff --git a/docs/dev/03_run.rst b/docs/dev/03_run.rst
deleted file mode 100644
index ab28dc471cf7d90a74a1602c5eb5a571d131797a..0000000000000000000000000000000000000000
--- a/docs/dev/03_run.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-Running BiscuIT in development mode
-===================================
-
-Using Django's development server
----------------------------------
-
-After you installed the framework and all desired apps and migrated
-the database, you are ready to run BiscuIT::
-
-  poetry run ./manage.py runserver
diff --git a/docs/dev/99_contributing.rst b/docs/dev/99_contributing.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a5ffcc710e824f4cdcf235f12b25914efcf5ae28
--- /dev/null
+++ b/docs/dev/99_contributing.rst
@@ -0,0 +1,2 @@
+.. include:: ../../CONTRIBUTING.rst
+.. include:: ../../CODE_OF_CONDUCT.rst
diff --git a/poetry.lock b/poetry.lock
index 774884826a0bdec3bc84cb43e43a050e91dc3a3c..a4288e1562a264faa06ac0fa0fe564f5c100b045 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -93,8 +93,8 @@ category = "main"
 description = "Cross-platform colored terminal text."
 name = "colorama"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "0.4.1"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+version = "0.4.3"
 
 [[package]]
 category = "main"
@@ -249,6 +249,14 @@ Django = ">=1.5"
 pytz = "*"
 six = "*"
 
+[[package]]
+category = "main"
+description = "Application settings whose values can be updated while a project is up and running."
+name = "django-dbsettings"
+optional = false
+python-versions = "*"
+version = "0.11.0"
+
 [[package]]
 category = "main"
 description = "A configurable set of panels that display various debug information about the current request/response."
@@ -473,6 +481,18 @@ twilio = ">=6.0"
 reference = "bf9d0812ab11320a6cadc6709c382a03184f2e31"
 type = "git"
 url = "https://github.com/Bouke/django-two-factor-auth"
+[[package]]
+category = "main"
+description = "Integrate django with yarnpkg"
+name = "django-yarnpkg"
+optional = false
+python-versions = "*"
+version = "6.0.0"
+
+[package.dependencies]
+django = "*"
+six = "*"
+
 [[package]]
 category = "dev"
 description = "Docutils -- Python Documentation Utilities"
@@ -487,7 +507,7 @@ description = "The dynamic configurator for your Python Project"
 name = "dynaconf"
 optional = false
 python-versions = "*"
-version = "2.2.0"
+version = "2.2.1"
 
 [package.dependencies]
 PyYAML = "*"
@@ -659,7 +679,7 @@ description = "More routines for operating on iterables, beyond itertools"
 name = "more-itertools"
 optional = false
 python-versions = ">=3.5"
-version = "8.0.0"
+version = "8.0.2"
 
 [[package]]
 category = "dev"
@@ -1207,7 +1227,7 @@ description = "Fast, Extensible Progress Meter"
 name = "tqdm"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*"
-version = "4.40.0"
+version = "4.40.1"
 
 [[package]]
 category = "main"
@@ -1296,7 +1316,7 @@ more-itertools = "*"
 ldap = ["django-auth-ldap"]
 
 [metadata]
-content-hash = "3a9826926228eb3fc31663fd44bde2bcaa49f29673a679465789cb3ef316f568"
+content-hash = "908e1e56f87aef8ccff2ce9a79bf335b5cb09896566d1dddb45c62c799c86310"
 python-versions = "^3.7"
 
 [metadata.hashes]
@@ -1310,7 +1330,7 @@ beautifulsoup4 = ["5279c36b4b2ec2cb4298d723791467e3000e5384a43ea0cdf5d45207c7e97
 certifi = ["017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", "25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"]
 chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"]
 click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
-colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"]
+colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"]
 colour = ["33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c", "af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee"]
 configobj = ["a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902"]
 coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"]
@@ -1325,6 +1345,7 @@ django-common-helpers = ["2d56be6fa261d829a6a224f189bf276267b9082a17d613fe5f015d
 django-contact-form = ["b42b7e04d6af3318b8427c1eaf62385ec66da252aa79b607ee55d956c7af4a2d", "c31f73faa13f52efa81ac95f41007f3a84eca617f92773a1bed7ca90c61cb3ed"]
 django-cron = ["08d22708c8b2ecab8cda989019a66c7e1e2424c59d822796fd45abf7731d261d"]
 django-dbbackup = ["9470e5d8bdaee4feb878b1b66c59eb9b27a131cccd648bf7cbfe70930acd4fc0"]
+django-dbsettings = ["e3147ced54b7db3371df10df8845e4514aeae96720000bca1a01d0a6490a1404"]
 django-debug-toolbar = ["24c157bc6c0e1648e0a6587511ecb1b007a00a354ce716950bff2de12693e7a8", "77cfba1d6e91b9bc3d36dc7dc74a9bb80be351948db5f880f2562a0cbf20b6c5"]
 django-easy-audit = ["1c5d5e6d6a33f50f696ed53cdaf51de0a4ae2f110ef8c41b33bc139b737729a6", "4b40a30599fe721eb0a9946f5023254fa0904d531c9f4adb23ee52601efaf89b"]
 django-fa = ["e3ebf97b90e374b5ccb5b8a70e4a932c8787f2ee995c09a97a63bf9a1366c3ff"]
@@ -1345,8 +1366,9 @@ django-settings-context-processor = ["d37c853d69a3069f5abbf94c7f4f6fc0fac38bbd05
 django-stubs = ["cd6a7333d518b9168f001b8a31c4ea89a91dea40a9dd1535c798635f69a5f80a", "e3673348a42c7259e81a4ea141dae2b2e711220ec631a6215ba9dc23cdcabdf4"]
 django-tables2 = ["0d9b17f5c030ba1b5fcaeb206d8397bf58f1fdfc6beaf56e7874841b8647aa94", "6afa0496695e15b332e98537265d09fe01a55b28c75a85323d8e6b0dc2350280"]
 django-two-factor-auth = []
+django-yarnpkg = ["010af70049cca94496d4c96ca45e62f13339edd1c22653ab8bfe055acbccd41b", "0d63c7b17e4b9c6c144c4093de3877ce70152f957b36fd7a50b259dc500a4948"]
 docutils = ["6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", "9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", "a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"]
-dynaconf = ["117b7e52698af82a535bcb71012f3221e1ab9c869bb8163ebf156569f32ff07d", "abeb44db4249c443083584cdd4d9c5c10cd773f11067e270660d15e6eef668d7"]
+dynaconf = ["52e3e41290763e405723b13a893592f8bca06f676854e59623052ffeee1a658f", "75691e9dd4093a1a2dc530d33369ae9296cfba30d29b72b00715dfb98b3f82e4"]
 easy-thumbnails = ["23fbe3415c93b2369ece8ebdfb5faa05540943bef8b941b3118ce769ba95e275"]
 entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"]
 faker = ["202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11", "92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432"]
@@ -1362,7 +1384,7 @@ libsass = ["003a65b4facb4c5dbace53fb0f70f61c5aae056a04b4d112a198c3c9674b31f2", "
 mando = ["4ce09faec7e5192ffc3c57830e26acba0fd6cd11e1ee81af0d4df0657463bd1c", "79feb19dc0f097daa64a1243db578e7674909b75f88ac2220f1c065c10a0d960"]
 markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"]
 mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
-more-itertools = ["53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2", "a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"]
+more-itertools = ["b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", "c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"]
 mypy = ["0107bff4f46a289f0e4081d59b77cef1c48ea43da5a0dbf0005d54748b26df2a", "07957f5471b3bb768c61f08690c96d8a09be0912185a27a68700f3ede99184e4", "10af62f87b6921eac50271e667cc234162a194e742d8e02fc4ddc121e129a5b0", "11fd60d2f69f0cefbe53ce551acf5b1cec1a89e7ce2d47b4e95a84eefb2899ae", "15e43d3b1546813669bd1a6ec7e6a11d2888db938e0607f7b5eef6b976671339", "352c24ba054a89bb9a35dd064ee95ab9b12903b56c72a8d3863d882e2632dc76", "437020a39417e85e22ea8edcb709612903a9924209e10b3ec6d8c9f05b79f498", "49925f9da7cee47eebf3420d7c0e00ec662ec6abb2780eb0a16260a7ba25f9c4", "6724fcd5777aa6cebfa7e644c526888c9d639bd22edd26b2a8038c674a7c34bd", "7a17613f7ea374ab64f39f03257f22b5755335b73251d0d253687a69029701ba", "cdc1151ced496ca1496272da7fc356580e95f2682be1d32377c22ddebdf73c91"]
 mypy-extensions = ["090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", "2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"]
 packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"]
@@ -1415,7 +1437,7 @@ sqlparse = ["40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
 text-unidecode = ["1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", "bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"]
 toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
 tox = ["7efd010a98339209f3a8292f02909b51c58417bfc6838ab7eca14cf90f96117a", "8dd653bf0c6716a435df363c853cad1f037f9d5fddd0abc90d0f48ad06f39d03"]
-tqdm = ["156a0565f09d1f0ef8242932a0e1302462c93827a87ba7b4423d90f01befe94c", "c0ffb55959ea5f3eaeece8d2db0651ba9ced9c72f40a6cce3419330256234289"]
+tqdm = ["895796ea8df435b6f502bf122f2b2034a3d48e6d8ff52175606ac1051b0e3e12", "e405d16c98fcf30725d0c9d493ed07302a18846b5452de5253030ccd18996f87"]
 twilio = ["da282a9c02bd9dfb190b798528b478833d8d28cb51464e8c45da0f0794384cde"]
 typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"]
 typing-extensions = ["091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", "910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", "cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575"]
diff --git a/pyproject.toml b/pyproject.toml
index 1bcd8b1b532fac82e7bc924ba2edd4c169bbd8ed..93c9ddd9143721f55a77f4487b6abd10ebfbb6e2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,6 +51,8 @@ psycopg2 = "^2.8"
 django_select2 = "^7.1"
 requests = "^2.22"
 django-two-factor-auth = { git = "https://github.com/Bouke/django-two-factor-auth", rev = "bf9d0812ab11320a6cadc6709c382a03184f2e31", extras = [ "YubiKey", "phonenumbers", "Call", "SMS" ] }
+django-yarnpkg = "^6.0"
+django-dbsettings = "^0.11.0"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]