diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py
index b56426d508f6fc38af093e3d5601e78dd247052a..87f579dc23d225cbb052ad35ef175bf691d330d5 100644
--- a/SciPost_v1/settings/base.py
+++ b/SciPost_v1/settings/base.py
@@ -94,6 +94,7 @@ INSTALLED_APPS = (
     'django_celery_results',
     'django_celery_beat',
     'finances',
+    'forums',
     'guides',
     'invitations',
     'journals',
diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py
index f77a4e7437771b6ed4477f64b44322eae1c1ffc1..ff3d8f0be9e8609ef65d924bed3ed4c447c55853 100644
--- a/SciPost_v1/urls.py
+++ b/SciPost_v1/urls.py
@@ -46,6 +46,7 @@ urlpatterns = [
     url(r'^commentaries/', include('commentaries.urls', namespace="commentaries")),
     url(r'^commentary/', include('commentaries.urls', namespace="_commentaries")),
     url(r'^comments/', include('comments.urls', namespace="comments")),
+    url(r'^forums/', include('forums.urls', namespace="forums")),
     url(r'^funders/', include('funders.urls', namespace="funders")),
     url(r'^finances/', include('finances.urls', namespace="finances")),
     url(r'^guides/', include('guides.urls', namespace="guides")),
diff --git a/forums/__init__.py b/forums/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/forums/admin.py b/forums/admin.py
new file mode 100644
index 0000000000000000000000000000000000000000..eb93544bf80a3350f869917de301142b8a7899cf
--- /dev/null
+++ b/forums/admin.py
@@ -0,0 +1,28 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.contrib import admin
+
+from guardian.admin import GuardedModelAdmin
+
+from .models import Forum, Post, Motion
+
+
+class ForumAdmin(GuardedModelAdmin):
+    prepopulated_fields = {'slug': ('name',)}
+    search_fields = ['name',]
+
+admin.site.register(Forum, ForumAdmin)
+
+
+class PostAdmin(admin.ModelAdmin):
+    search_fields = ['posted_by', 'subject', 'text']
+
+admin.site.register(Post, PostAdmin)
+
+
+class MotionAdmin(admin.ModelAdmin):
+    search_fields = ['posted_by', 'subject', 'text']
+
+admin.site.register(Motion, MotionAdmin)
diff --git a/forums/apps.py b/forums/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..54a0867c3793ca6b1838c382fdcf594a236bc406
--- /dev/null
+++ b/forums/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class ForumsConfig(AppConfig):
+    name = 'forums'
diff --git a/forums/forms.py b/forums/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9cbb48e951acaa0f93e2627f24105f41ca97812
--- /dev/null
+++ b/forums/forms.py
@@ -0,0 +1,87 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django import forms
+
+from ajax_select.fields import AutoCompleteSelectField
+
+from .models import Forum, Meeting, Post, Motion
+
+
+class ForumForm(forms.ModelForm):
+    class Meta:
+        model = Forum
+        fields = ['name', 'slug', 'description',
+                  'publicly_visible', 'moderators',
+                  'parent_content_type', 'parent_object_id']
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields['parent_content_type'].widget = forms.HiddenInput()
+        self.fields['parent_object_id'].widget = forms.HiddenInput()
+
+
+class MeetingForm(ForumForm):
+    class Meta:
+        model = Meeting
+        fields = ['name', 'slug', 'description',
+                  'publicly_visible', 'moderators',
+                  'parent_content_type', 'parent_object_id',
+                  'date_from', 'date_until', 'preamble']
+
+
+class ForumGroupPermissionsForm(forms.ModelForm):
+    """
+    Used for granting a specific Group access to a given Forum.
+    """
+    group = AutoCompleteSelectField('group_lookup')
+    can_view = forms.BooleanField(required=False)
+    can_post = forms.BooleanField(required=False)
+
+    class Meta:
+        model = Forum
+        fields = []
+
+
+class ForumOrganizationPermissionsForm(forms.Form):
+    organization = AutoCompleteSelectField('organization_lookup')
+    can_view = forms.BooleanField()
+    can_post = forms.BooleanField()
+
+
+class PostForm(forms.ModelForm):
+    """
+    Create a new Post. The parent must be defined, the model class and
+    instance being defined by url parameters.
+    """
+    class Meta:
+        model = Post
+        fields = ['posted_by', 'posted_on', 'needs_vetting',
+                  'parent_content_type', 'parent_object_id',
+                  'subject', 'text']
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields['posted_by'].widget = forms.HiddenInput()
+        self.fields['posted_on'].widget = forms.HiddenInput()
+        self.fields['needs_vetting'].widget = forms.HiddenInput()
+        self.fields['parent_content_type'].widget = forms.HiddenInput()
+        self.fields['parent_object_id'].widget = forms.HiddenInput()
+
+
+class MotionForm(PostForm):
+    """
+    Form for creating a Motion to be voted on in a Forum or during a Meeting.
+    """
+    class Meta:
+        model = Motion
+        fields = ['posted_by', 'posted_on', 'needs_vetting',
+                  'parent_content_type', 'parent_object_id',
+                  'subject', 'text',
+                  'eligible_for_voting', 'voting_deadline']
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields['eligible_for_voting'].widget = forms.HiddenInput()
+        self.fields['eligible_for_voting'].disabled = True
diff --git a/forums/managers.py b/forums/managers.py
new file mode 100644
index 0000000000000000000000000000000000000000..e7799a8ef68021305906561e12634540c3d075dd
--- /dev/null
+++ b/forums/managers.py
@@ -0,0 +1,19 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.db import models
+
+
+class ForumQuerySet(models.QuerySet):
+
+    def anchors(self):
+        """Return only the Forums which do not have a parent."""
+        return self.filter(parent_object_id__isnull=True)
+
+
+class PostQuerySet(models.QuerySet):
+
+    def motions_excluded(self):
+        """Filter all Motions out of the Post queryset."""
+        return self.filter(motion__isnull=True)
diff --git a/forums/migrations/0001_initial.py b/forums/migrations/0001_initial.py
new file mode 100644
index 0000000000000000000000000000000000000000..974b33dbafd111b77029018a7f18a87e0f0fd111
--- /dev/null
+++ b/forums/migrations/0001_initial.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-03-03 09:36
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Forum',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=256)),
+                ('slug', models.SlugField(allow_unicode=True)),
+                ('publicly_visible', models.BooleanField(default=False)),
+                ('parent_object_id', models.PositiveIntegerField(blank=True, null=True)),
+                ('moderators', models.ManyToManyField(related_name='moderated_forums', to=settings.AUTH_USER_MODEL)),
+                ('parent_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+            ],
+            options={
+                'ordering': ['name'],
+            },
+        ),
+        migrations.CreateModel(
+            name='Post',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('posted_on', models.DateTimeField(default=django.utils.timezone.now)),
+                ('needs_vetting', models.BooleanField(default=True)),
+                ('parent_object_id', models.PositiveIntegerField()),
+                ('subject', models.CharField(max_length=256)),
+                ('text', models.TextField()),
+                ('parent_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+                ('posted_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+                ('vetted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vetted_posts', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['posted_on'],
+            },
+        ),
+    ]
diff --git a/forums/migrations/0002_auto_20190306_0807.py b/forums/migrations/0002_auto_20190306_0807.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e5c4f9b1af14c1a1abb3b0ab36266a1a7f35833
--- /dev/null
+++ b/forums/migrations/0002_auto_20190306_0807.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-03-06 07:07
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('forums', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='forum',
+            options={'ordering': ['name'], 'permissions': [('can_view_forum', 'Can view Forum')]},
+        ),
+    ]
diff --git a/forums/migrations/0003_auto_20190307_0908.py b/forums/migrations/0003_auto_20190307_0908.py
new file mode 100644
index 0000000000000000000000000000000000000000..955e9d1d2151c6289e5e6966fee6eac99d34f55d
--- /dev/null
+++ b/forums/migrations/0003_auto_20190307_0908.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-03-07 08:08
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('forums', '0002_auto_20190306_0807'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='forum',
+            options={'ordering': ['name'], 'permissions': [('can_view_forum', 'Can view Forum'), ('can_post_to_forum', 'Can add Post to Forum')]},
+        ),
+    ]
diff --git a/forums/migrations/0004_auto_20190308_1055.py b/forums/migrations/0004_auto_20190308_1055.py
new file mode 100644
index 0000000000000000000000000000000000000000..442372dc6830832e2a2f3cf1c54408a0c5bac95e
--- /dev/null
+++ b/forums/migrations/0004_auto_20190308_1055.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-03-08 09:55
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('forums', '0003_auto_20190307_0908'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='post',
+            name='text',
+            field=models.TextField(help_text='You can use ReStructuredText, see a <a href="https://devguide.python.org/documenting/#restructuredtext-primer" target="_blank">primer on python.org</a>'),
+        ),
+    ]
diff --git a/forums/migrations/0005_forum_description.py b/forums/migrations/0005_forum_description.py
new file mode 100644
index 0000000000000000000000000000000000000000..609fd9d50e9c35893595cb686385bbdf13a4b35b
--- /dev/null
+++ b/forums/migrations/0005_forum_description.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-03-09 05:18
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('forums', '0004_auto_20190308_1055'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='forum',
+            name='description',
+            field=models.TextField(blank=True, help_text='You can use ReStructuredText, see a <a href="https://devguide.python.org/documenting/#restructuredtext-primer" target="_blank">primer on python.org</a>', null=True),
+        ),
+    ]
diff --git a/forums/migrations/0006_meeting.py b/forums/migrations/0006_meeting.py
new file mode 100644
index 0000000000000000000000000000000000000000..02cf93d9f53e582bd87c52e03995d939f1ef2b9c
--- /dev/null
+++ b/forums/migrations/0006_meeting.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-03-09 08:56
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('forums', '0005_forum_description'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Meeting',
+            fields=[
+                ('forum', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='forums.Forum')),
+                ('date_from', models.DateField()),
+                ('date_until', models.DateField()),
+                ('preamble', models.TextField(help_text='Explanatory notes for the meeting.\nYou can use ReStructuredText, see a <a href="https://devguide.python.org/documenting/#restructuredtext-primer" target="_blank">primer on python.org</a>')),
+                ('minutes', models.TextField(blank=True, help_text='To be filled in after completion of the meeting.\nYou can use ReStructuredText, see a <a href="https://devguide.python.org/documenting/#restructuredtext-primer" target="_blank">primer on python.org</a>', null=True)),
+            ],
+            bases=('forums.forum',),
+        ),
+    ]
diff --git a/forums/migrations/0007_motion.py b/forums/migrations/0007_motion.py
new file mode 100644
index 0000000000000000000000000000000000000000..764efd14aa5fcd9943723cd6374b4263554b9f6e
--- /dev/null
+++ b/forums/migrations/0007_motion.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-03-09 17:29
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('forums', '0006_meeting'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Motion',
+            fields=[
+                ('post_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='forums.Post')),
+                ('voting_deadline', models.DateField()),
+                ('accepted', models.NullBooleanField()),
+                ('eligible_for_voting', models.ManyToManyField(blank=True, related_name='eligible_to_vote_on_motion', to=settings.AUTH_USER_MODEL)),
+                ('in_abstain', models.ManyToManyField(blank=True, related_name='abstain_with_motion', to=settings.AUTH_USER_MODEL)),
+                ('in_agreement', models.ManyToManyField(blank=True, related_name='agree_on_motion', to=settings.AUTH_USER_MODEL)),
+                ('in_disagreement', models.ManyToManyField(blank=True, related_name='disagree_with_motion', to=settings.AUTH_USER_MODEL)),
+                ('in_doubt', models.ManyToManyField(blank=True, related_name='doubt_on_motion', to=settings.AUTH_USER_MODEL)),
+            ],
+            bases=('forums.post',),
+        ),
+    ]
diff --git a/forums/migrations/0008_auto_20190310_0713.py b/forums/migrations/0008_auto_20190310_0713.py
new file mode 100644
index 0000000000000000000000000000000000000000..f03b76d94c575b03271279eca737b9aebff2e757
--- /dev/null
+++ b/forums/migrations/0008_auto_20190310_0713.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-03-10 06:13
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('forums', '0007_motion'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='motion',
+            name='post_ptr',
+            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='forums.Post'),
+        ),
+    ]
diff --git a/forums/migrations/0009_auto_20190310_0715.py b/forums/migrations/0009_auto_20190310_0715.py
new file mode 100644
index 0000000000000000000000000000000000000000..6291e88dd54cb5bd775a4161e5ff51eb0fb34448
--- /dev/null
+++ b/forums/migrations/0009_auto_20190310_0715.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.4 on 2019-03-10 06:15
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('forums', '0008_auto_20190310_0713'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='motion',
+            old_name='post_ptr',
+            new_name='post',
+        ),
+    ]
diff --git a/forums/migrations/__init__.py b/forums/migrations/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/forums/models.py b/forums/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..47b56585aee6dec7b173de21acf4f2c1d8efb798
--- /dev/null
+++ b/forums/models.py
@@ -0,0 +1,259 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+import datetime
+
+from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
+from django.contrib.contenttypes.models import ContentType
+from django.core.urlresolvers import reverse
+from django.db import models
+from django.utils import timezone
+
+from .managers import ForumQuerySet, PostQuerySet
+
+
+class Forum(models.Model):
+    """
+    A Forum is a discussion location for a defined set of Users.
+
+    A Forum instance can be publicly visible. For publicly invisible forums,
+    as well as for thread creation and posting rights,
+    access is specified flexibly on a per-Group and/or per-User basis
+    via object-level permissions (through the django-guardian required app).
+
+    Forums can be related to parent/children via parent [GenericForeignKey]
+    and child_forums [GenericRelation] fields.
+
+    Similarly, Posts in a Forum are listed in the posts [GenericRelation] field.
+    """
+    name = models.CharField(max_length=256)
+    slug = models.SlugField(allow_unicode=True)
+    description = models.TextField(
+        blank=True, null=True,
+        help_text=(
+            'You can use ReStructuredText, see a '
+            '<a href="https://devguide.python.org/documenting/#restructuredtext-primer" '
+            'target="_blank">primer on python.org</a>')
+        )
+    publicly_visible = models.BooleanField(default=False)
+    moderators = models.ManyToManyField('auth.User', related_name='moderated_forums')
+
+    parent_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,
+                                            blank=True, null=True)
+    parent_object_id = models.PositiveIntegerField(blank=True, null=True)
+    parent = GenericForeignKey('parent_content_type', 'parent_object_id')
+
+    child_forums = GenericRelation('forums.Forum',
+                                   content_type_field='parent_content_type',
+                                   object_id_field='parent_object_id',
+                                   related_query_name='parent_forums')
+    posts = GenericRelation('forums.Post',
+                            content_type_field='parent_content_type',
+                            object_id_field='parent_object_id',
+                            related_query_name='parent_forums')
+    motions = GenericRelation('forums.Motion',
+                            content_type_field='parent_content_type',
+                            object_id_field='parent_object_id',
+                            related_query_name='parent_forums')
+
+    objects = ForumQuerySet.as_manager()
+
+    class Meta:
+        ordering = ['name',]
+        permissions = [
+            ('can_view_forum', 'Can view Forum'),
+            ('can_post_to_forum', 'Can add Post to Forum'),
+        ]
+
+    def __str__(self):
+        return self.name
+
+    def get_absolute_url(self):
+        return reverse('forums:forum_detail', kwargs={'slug': self.slug})
+
+    @property
+    def nr_posts(self):
+        """Recursively counts the number of posts in this Forum."""
+        nr = 0
+        for post in self.posts.all():
+            nr += post.nr_followups
+        if self.posts.all():
+            nr += self.posts.all().count()
+        return nr
+
+    def posts_hierarchy_id_list(self):
+        id_list = []
+        for post in self.posts.all():
+            id_list += post.posts_hierarchy_id_list()
+        return id_list
+
+    @property
+    def latest_post(self):
+        id_list = self.posts_hierarchy_id_list()
+        print ('forum post id_list: %s' % id_list)
+        try:
+            return Post.objects.filter(id__in=id_list).order_by('-posted_on').first()
+        except:
+            return None
+
+
+class Meeting(Forum):
+    """
+    A Meeting is a Forum but with fixed start and end dates,
+    and with additional descriptor fields (preamble, minutes).
+
+    By definition, adding new Posts is allowed up to and including
+    the date specified in ``date_until``. The Meeting can however
+    be viewed in perpetuity by users who have viewing rights.
+    """
+    forum = models.OneToOneField('forums.Forum', on_delete=models.CASCADE,
+                                 parent_link=True)
+    date_from = models.DateField()
+    date_until = models.DateField()
+    preamble = models.TextField(
+        help_text=(
+            'Explanatory notes for the meeting.\n'
+            'You can use ReStructuredText, see a '
+            '<a href="https://devguide.python.org/documenting/#restructuredtext-primer" '
+            'target="_blank">primer on python.org</a>')
+        )
+    minutes = models.TextField(
+        blank=True, null=True,
+        help_text=(
+            'To be filled in after completion of the meeting.\n'
+            'You can use ReStructuredText, see a '
+            '<a href="https://devguide.python.org/documenting/#restructuredtext-primer" '
+            'target="_blank">primer on python.org</a>')
+        )
+
+    def __str__(self):
+        return '%s, [%s to %s]' % (self.forum,
+                                   self.date_from.strftime('%Y-%m-%d'),
+                                   self.date_until.strftime('%Y-%m-%d'))
+
+    @property
+    def future(self):
+        return datetime.date.today() < self.date_from
+
+    @property
+    def ongoing(self):
+        today = datetime.date.today()
+        return today >= self.date_from and today <= self.date_until
+
+    @property
+    def past(self):
+        return datetime.date.today() > self.date_until
+
+    @property
+    def context_colors(self):
+        """If meeting is future: primary; ongoing: success; voting: warning; finished: info."""
+        today = datetime.date.today()
+        if today < self.date_from:
+            return {'bg': 'primary', 'text': 'white', 'message': 'Meeting is coming up'}
+        elif today <= self.date_until:
+            return {'bg': 'success', 'text': 'light', 'message': 'Meeting is ongoing'}
+        elif today < self.date_until + datetime.timedelta(days=8):
+            return {'bg': 'warning', 'text': 'dark', 'message': 'Meeting is finished, voting open'}
+        else:
+            return {'bg': 'info', 'text': 'dark', 'message': 'Meeting is finished'}
+
+
+class Post(models.Model):
+    """
+    A comment, feedback, question or similar, with a specified parent object.
+
+    If the Post is submitted by Admin, Advisory Board members or Fellows,
+    it is marked as not needing vetting before becoming visible.
+    Similarly, for Posts created by organizations.Contacts, no vetting is required.
+    Otherwise, e.g. for Contributors-submitted Posts to a publicly-visible
+    Forum, vetting by Admin is required.
+
+    A Post must have a parent object (represented here as a GenericForeignKey).
+    If the parent is a Forum, the Post is interpreted as the head of
+    a new discussion thread. If the parent is a Post, then it is interpreted as
+    part of an ongoing thread.
+
+    The text field can contain ReStructuredText markup, formatted in templates
+    through the ``scipost`` app's ``restructuredtext`` template filter, which
+    relies on the ``docutils`` required app.
+    """
+    posted_by = models.ForeignKey('auth.User', on_delete=models.CASCADE)
+    posted_on = models.DateTimeField(default=timezone.now)
+    needs_vetting = models.BooleanField(default=True)
+    vetted_by = models.ForeignKey('auth.User', related_name='vetted_posts',
+                                  blank=True, null=True, on_delete=models.PROTECT)
+    parent_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
+    parent_object_id = models.PositiveIntegerField()
+    parent = GenericForeignKey('parent_content_type', 'parent_object_id')
+    followup_posts = GenericRelation('forums.Post',
+                                     content_type_field='parent_content_type',
+                                     object_id_field='parent_object_id',
+                                     related_query_name='parent_posts')
+    subject = models.CharField(max_length=256)
+    text = models.TextField(
+        help_text=(
+            'You can use ReStructuredText, see a '
+            '<a href="https://devguide.python.org/documenting/#restructuredtext-primer" '
+            'target="_blank">primer on python.org</a>')
+        )
+
+    objects = PostQuerySet.as_manager()
+
+    class Meta:
+        ordering = ['posted_on',]
+
+    def __str__(self):
+        return '%s %s: %s' % (self.posted_by.first_name,
+                              self.posted_by.last_name, self.subject[:32])
+
+    def get_absolute_url(self):
+        return '%s#post%s' % (self.get_forum().get_absolute_url(), self.id)
+
+    @property
+    def nr_followups(self):
+        nr = 0
+        for followup in self.followup_posts.all():
+            nr += followup.nr_followups
+        if self.followup_posts:
+            nr += self.followup_posts.all().count()
+        return nr
+
+    def posts_hierarchy_id_list(self):
+        id_list = [self.id]
+        for post in self.followup_posts.all():
+            id_list += post.posts_hierarchy_id_list()
+        print ('post %s id_list: %s' % (self.id, id_list))
+        return id_list
+
+    def get_forum(self):
+        """
+        Climb back the hierarchy up to the original Forum.
+        If no Forum is found, return None.
+        """
+        type_forum = ContentType.objects.get_by_natural_key('forums', 'forum')
+        if self.parent_content_type == type_forum:
+            return self.parent
+        else:
+            return self.parent.get_forum()
+
+
+class Motion(Post):
+    """
+    A Motion is a posting to a Forum or Meeting, on which Forum participants
+    can vote.
+    """
+    post = models.OneToOneField('forums.Post', on_delete=models.CASCADE,
+                                parent_link=True)
+    eligible_for_voting = models.ManyToManyField('auth.User', blank=True,
+                                                 related_name='eligible_to_vote_on_motion')
+    in_agreement = models.ManyToManyField('auth.User', blank=True,
+                                          related_name='agree_on_motion')
+    in_doubt = models.ManyToManyField('auth.User', blank=True,
+                                      related_name='doubt_on_motion')
+    in_disagreement = models.ManyToManyField('auth.User', blank=True,
+                                             related_name='disagree_with_motion')
+    in_abstain = models.ManyToManyField('auth.User', blank=True,
+                                        related_name='abstain_with_motion')
+    voting_deadline = models.DateField()
+    accepted = models.NullBooleanField()
diff --git a/forums/templates/forums/base.html b/forums/templates/forums/base.html
new file mode 100644
index 0000000000000000000000000000000000000000..83076f2ac5c43e84223489d441e1ca50836c9e5b
--- /dev/null
+++ b/forums/templates/forums/base.html
@@ -0,0 +1,13 @@
+{% extends 'scipost/base.html' %}
+
+{% block breadcrumb %}
+    <div class="container-outside header">
+        <div class="container">
+            <nav class="breadcrumb hidden-sm-down">
+                {% block breadcrumb_items %}
+                    <a href="{% url 'forums:forums' %}" class="breadcrumb-item">Forums</a>
+                {% endblock %}
+            </nav>
+        </div>
+    </div>
+{% endblock %}
diff --git a/forums/templates/forums/forum_as_li.html b/forums/templates/forums/forum_as_li.html
new file mode 100644
index 0000000000000000000000000000000000000000..b4afb1dd469eea6c1cdb78accc13a0b4977e1124
--- /dev/null
+++ b/forums/templates/forums/forum_as_li.html
@@ -0,0 +1,11 @@
+<li class="d-flex flex-wrap justify-content-between">
+  <a href="{{ forum.get_absolute_url }}">{{ forum }}</a>
+  <span class="badge badge-secondary badge-pill">{% with nr_posts=forum.nr_posts %}{{ nr_posts }} post{{ nr_posts|pluralize }}{% endwith %}</span>
+  {% if forum.child_forums.all|length > 0 %}
+  <ul class="list-unstyled forumList">
+    {% for child in forum.child_forums.all %}
+    {% include 'forums/forum_as_li.html' with forum=child %}
+    {% endfor %}
+  </ul>
+  {% endif %}
+</li>
diff --git a/forums/templates/forums/forum_confirm_delete.html b/forums/templates/forums/forum_confirm_delete.html
new file mode 100644
index 0000000000000000000000000000000000000000..e1b83558d653e628e3a1a2e7c9c3b05447eb5904
--- /dev/null
+++ b/forums/templates/forums/forum_confirm_delete.html
@@ -0,0 +1,35 @@
+{% extends 'forums/base.html' %}
+
+{% load bootstrap %}
+{% load restructuredtext %}
+
+{% block pagetitle %}: Delete Forum{% endblock pagetitle %}
+
+{% block content %}
+<div class="row">
+    <div class="col-12">
+        <h1 class="highlight">Delete Forum</h1>
+
+	<h3 class="highlight">Description</h3>
+	{{ object.description|restructuredtext }}
+
+	<h3 class="highlight">Posts</h3>
+	{% for post in object.posts.all %}
+	{% include 'forums/post_card.html' with forum=object post=post %}
+	{% endfor %}
+
+    </div>
+</div>
+
+<div class="row">
+  <div class="col-12">
+
+    <form method="post">
+      {% csrf_token %}
+      <h3 class="mb-2">Are you sure you want to delete this Forum (and all associated Posts)?</h3>
+      <input type="submit" class="btn btn-danger" value="Yes, delete it" />
+    </form>
+  </div>
+</div>
+
+{% endblock content %}
diff --git a/forums/templates/forums/forum_detail.html b/forums/templates/forums/forum_detail.html
new file mode 100644
index 0000000000000000000000000000000000000000..17ef9d9607f9def4404f1742de6de7770aa6c0ff
--- /dev/null
+++ b/forums/templates/forums/forum_detail.html
@@ -0,0 +1,167 @@
+{% extends 'forums/base.html' %}
+
+{% load bootstrap %}
+{% load guardian_tags %}
+{% load restructuredtext %}
+
+{% block breadcrumb_items %}
+    {{ block.super }}
+<span class="breadcrumb-item">{% if forum.meeting %}Meeting{% else %}Forum{% endif %} Details</span>
+{% endblock %}
+
+{% load scipost_extras %}
+
+{% block pagetitle %}: {% if forum.meeting %}Meeting{% else %}Forum{% endif %} details{% endblock pagetitle %}
+
+{% get_obj_perms request.user for forum as "user_perms" %}
+
+{% block content %}
+
+<div class="row">
+  <div class="col-12">
+    <h3 class="highlight">
+      {% if forum.meeting %}
+      {% with context_colors=forum.meeting.context_colors %}
+      <span class="badge badge-{{ context_colors.bg }} mx-0 mb-2 p-2 text-{{ context_colors.text }}">
+	{{ context_colors.message }}
+	<span class="small text-muted"> [{{ forum.meeting.date_from|date:"Y-m-d" }} to {{ forum.meeting.date_until|date:"Y-m-d" }}]</span>
+      </span>
+      {% endwith %}
+      <br/>
+      {% endif %}
+
+      <span class="d-flex flex-wrap justify-content-between">
+	<a href="{{ forum.get_absolute_url }}">{{ forum }}</a>
+	<span class="badge badge-primary badge-pill">{% with nr_posts=forum.nr_posts %}{{ nr_posts }} post{{ nr_posts|pluralize }}{% endwith %}</span>
+      </span>
+    </h3>
+
+    {% if forum.parent %}
+    <p>Parent: <a href="{{ forum.parent.get_absolute_url }}">{{ forum.parent }}</a></p>
+    {% endif %}
+    {% if forum.child_forums.all|length > 0 %}
+    <p>Descendants: {% for child in forum.child_forums.all %}<a href="{{ child.get_absolute_url }}">{{ child }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</p>
+    {% endif %}
+
+    {% if perms.forums.add_forum or "can_change_forum" in user_perms %}
+    <div class="container border border-danger">
+    <h4>Admin actions:</h4>
+    <ul>
+      <li><a href="{% url 'forums:forum_update' slug=forum.slug %}" class="text-warning">Update this {% if forum.meeting %}Meeting{% else %}Forum{% endif %}</a></li>
+      <li>
+	{% if not forum.child_forums.all|length > 0 %}
+	<a href="{% url 'forums:forum_delete' slug=forum.slug %}" class="text-danger">Delete this {% if forum.meeting %}Meeting{% else %}Forum{% endif %} (and all Posts {% if forum.meeting %}and Motions {% endif %}it contains)</a>
+	{% else %}
+	<span class="text-danger" style="text-decoration: line-through;">Delete this Forum</span> Please delete descendant Forums first.
+	{% endif %}
+      </li>
+      {% if not forum.meeting %}
+      <li><a href="{% url 'forums:forum_create' parent_model='forum' parent_id=forum.id %}">Create a (sub)Forum within this one</a></li>
+      <li><a href="{% url 'forums:meeting_create' parent_model='forum' parent_id=forum.id %}">Create a Meeting within this Forum</a></li>
+      {% endif %}
+    </ul>
+
+    <div class="card">
+      <div class="card-header">
+	Permissions on this {% if forum.meeting %}Meeting{% else %}Forum{% endif %} instance
+	<button class="btn btn-link small" data-toggle="collapse" data-target="#permissionsCard">
+	View/manage</button>
+      </div>
+      <div class="card-body collapse" id="permissionsCard">
+	<p><a href="{% url 'forums:forum_permissions' slug=forum.slug %}">Grant permissions to a new group</a></p>
+	<p>Groups with permissions [click on the Group's name to manage permissions]:</p>
+	<ul>
+	  {% for group in groups_with_perms %}
+	  {% get_obj_perms group for forum as "group_perms" %}
+	  <li><a href="{% url 'forums:forum_permissions' slug=forum.slug group_id=group.id %}">{{ group.name }}</a>: {{ group_perms }}</li>
+	  {% empty %}
+	  <li>No group has permissions on this Forum</li>
+	  {% endfor %}
+	</ul>
+
+	<p>Users with permissions:</p>
+	<ul>
+	  {% for u in users_with_perms %}
+	  {% get_obj_perms u for forum as "u_perms" %}
+	  <li>{{ u.first_name }} {{ u.last_name }}: {{ u_perms }}</li>
+	  {% endfor %}
+	</ul>
+      </div>
+    </div>
+    </div>
+    {% endif %}
+
+
+    <h3>Table of Contents</h3>
+    <ul>
+      <li><a href="#Description">Description</a></li>
+      {% if forum.meeting %}
+      <li><a href="#Preamble">Preamble</a></li>
+      <li><a href="#Motions">Motions</a></li>
+      {% endif %}
+      <li><a href="#Posts">Posts</a></li>
+      {% if forum.meeting %}
+      <li><a href="#Minutes">Minutes</a></li>
+      {% endif %}
+    </ul>
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-12">
+    <h3 class="highlight" id="Description">Description</h3>
+    {{ forum.description|restructuredtext }}
+  </div>
+</div>
+
+{% if forum.meeting %}
+<div class="row">
+  <div class="col-12">
+    <h3 class="highlight" id="Preamble">Preamble</h3>
+    {{ forum.preamble|restructuredtext }}
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-12">
+    <h3 class="highlight" id="Motions">Motions</h3>
+    <ul>
+      {% if forum.meeting.future %}
+      <li>Adding Motions will be activated once the meeting starts</li>
+      {% elif forum.meeting.past %}
+      <li><span class="text-danger">Adding Motions is deactivated</span> (Meeting is over)</li>
+      {% else %}
+      <li><a href="{% url 'forums:motion_create' slug=forum.slug parent_model='forum' parent_id=forum.id %}">Add a new Motion</a></li>
+      {% endif %}
+    </ul>
+    {% for motion in forum.motions.all %}
+    {% include 'forums/post_card.html' with forum=forum post=motion.post %}
+    {% endfor %}
+  </div>
+</div>
+{% endif %}
+
+<div class="row">
+  <div class="col-12">
+    <h3 class="highlight" id="Posts">Posts</h3>
+    <ul>
+      <li><a href="{% url 'forums:post_create' slug=forum.slug parent_model='forum' parent_id=forum.id %}">Add a new Post</a></li>
+    </ul>
+
+    {% for post in forum.posts.motions_excluded %}
+    {% include 'forums/post_card.html' with forum=forum post=post %}
+    {% endfor %}
+
+  </div>
+</div>
+
+{% if forum.meeting %}
+<div class="row">
+  <div class="col-12">
+    <h3 class="highlight" id="Minutes">Minutes</h3>
+    {{ forum.minutes|restructuredtext }}
+  </div>
+</div>
+{% endif %}
+
+{% endblock content %}
diff --git a/forums/templates/forums/forum_form.html b/forums/templates/forums/forum_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..75e06ae88d97432ee6ec375c71dd435c91d55473
--- /dev/null
+++ b/forums/templates/forums/forum_form.html
@@ -0,0 +1,37 @@
+{% extends 'forums/base.html' %}
+
+{% load bootstrap %}
+
+{% block headsup %}
+<script>
+$(document).ready(function() {
+
+$("#id_name").keyup(function() {
+  slug_value = this.value.split(" ").join("_");
+  $("#id_slug").val(slug_value);
+});
+
+});
+</script>
+{% endblock headsup %}
+
+{% block breadcrumb_items %}
+{{ block.super }}
+<span class="breadcrumb-item">{% if form.instance.id %}Update {{ form.instance }}{% else %}Add new Forum{% endif %}</span>
+{% endblock %}
+
+{% block pagetitle %}: Forums{% endblock pagetitle %}
+
+
+{% block content %}
+
+<div class="row">
+  <div class="col-12">
+    <form action="" method="post">
+      {% csrf_token %}
+      {{ form|bootstrap }}
+      <input type="submit" value="Submit" class="btn btn-primary">
+  </div>
+</div>
+
+{% endblock content %}
diff --git a/forums/templates/forums/forum_list.html b/forums/templates/forums/forum_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..81e76c3a1f5fd688d31d1d538eb6c77749582fba
--- /dev/null
+++ b/forums/templates/forums/forum_list.html
@@ -0,0 +1,93 @@
+{% extends 'forums/base.html' %}
+
+{% load bootstrap %}
+{% load restructuredtext %}
+
+
+{% block breadcrumb_items %}
+{{ block.super }}
+<span class="breadcrumb-item">Forums</span>
+{% endblock %}
+
+
+{% block pagetitle %}: Forums{% endblock pagetitle %}
+
+{% block content %}
+
+<div class="row">
+  <div class="col-12">
+
+    <h3 class="highlight">Forums</h3>
+    {% if perms.forums.can_add_forum %}
+    <ul>
+      <li><a href="{% url 'forums:forum_create' %}">Create a new Forum</a></li>
+      <li><a href="{% url 'forums:meeting_create' %}">Create a new Meeting</a></li>
+    </ul>
+    {% endif %}
+
+    <div class="card-columns">
+      {% for forum in object_list %}
+      <div class="card">
+	<div class="card-header">
+	  {% if forum.meeting %}
+	  {% with context_colors=forum.meeting.context_colors %}
+	  <span class="badge badge-{{ context_colors.bg }} mx-0 mb-2 p-2 text-{{ context_colors.text }}">{{ context_colors.message }}</span>
+	  {% endwith %}
+	  <br/>
+	  {% endif %}
+	  <span class="d-flex flex-wrap justify-content-between">
+	    <a href="{{ forum.get_absolute_url }}">{{ forum|truncatechars:30 }}</a>
+	    <span class="badge badge-primary badge-pill">{% with nr_posts=forum.nr_posts %}{{ nr_posts }} post{{ nr_posts|pluralize }}{% endwith %}</span>
+	  </span>
+	</div>
+	<div class="card-body">
+	  {{ forum.description|restructuredtext }}
+	  {% if forum.child_forums.all|length > 0 %}
+	  <hr/>
+	  <p>Descendants:</p>
+	  <ul class="list-unstyled forumList">
+	    {% for child in forum.child_forums.all %}
+	    {% include 'forums/forum_as_li.html' with forum=child %}
+	    {% endfor %}
+	  </ul>
+	  {% endif %}
+	</div>
+      </div>
+      {% empty %}
+      <p>No visible Forums found.</p>
+      {% endfor %}
+    </div>
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-12">
+    <h3 class="highlight">Latest postings</h3>
+    <table class="table">
+      <thead class="thead-default">
+	<tr>
+	  <th>Forum</th>
+	  <th>Latest post</th>
+	  <th>Posted on</th>
+	  <th>Nr posts</th>
+	</tr>
+      </thead>
+      <tbody>
+	{% for forum in object_list %}
+	<tr>
+	  <td><a href="{{ forum.get_absolute_url }}">{{ forum }}</a></td>
+	  <td>{{ forum.latest_post }}</td>
+	  <td>{{ forum.latest_post.posted_on|date:"Y-m-d" }}</td>
+	  <td><span class="badge badge-primary badge-pill">{{ forum.nr_posts }}</span></td>
+	</tr>
+	{% empty %}
+	<tr>
+	  <td>No visible Posts found.</td>
+	</tr>
+	{% endfor %}
+      </tbody>
+    </table>
+  </div>
+</div>
+
+{% endblock content %}
diff --git a/forums/templates/forums/forum_permissions.html b/forums/templates/forums/forum_permissions.html
new file mode 100644
index 0000000000000000000000000000000000000000..55f0c5580b2655f0aced06f55af1bc7ac0bbe531
--- /dev/null
+++ b/forums/templates/forums/forum_permissions.html
@@ -0,0 +1,37 @@
+{% extends 'forums/base.html' %}
+
+{% load bootstrap %}
+{% load guardian_tags %}
+
+{% block breadcrumb_items %}
+    {{ block.super }}
+<span class="breadcrumb-item">Forum permissions</span>
+{% endblock %}
+
+{% load scipost_extras %}
+
+{% block pagetitle %}: Forum permissions{% endblock pagetitle %}
+
+
+{% block content %}
+
+<div class="row">
+  <div class="col-12">
+
+    <h3 class="highlight">{{ forum.name }}: permissions{% if group %} for Group {{ group.name }}{% endif %}</h3>
+
+    <form action="{% url 'forums:forum_permissions' slug=forum.slug %}" method="post">
+      {% csrf_token %}
+      {{ form|bootstrap }}
+      <input type="submit" value="Assign permissions" class="btn btn-primary">
+    </form>
+
+  </div>
+</div>
+
+{% endblock content %}
+
+{% block footer_script %}
+{{ block.super }}
+{{ form.media }}
+{% endblock footer_script %}
diff --git a/forums/templates/forums/post_card.html b/forums/templates/forums/post_card.html
new file mode 100644
index 0000000000000000000000000000000000000000..78da1449f3b3de3c22d50cd9f098583781fc83f5
--- /dev/null
+++ b/forums/templates/forums/post_card.html
@@ -0,0 +1,80 @@
+{% load restructuredtext %}
+
+<div class="card {% if post.motion %}text-white bg-dark{% else %}text-body{% endif %}" id="post{{ post.id }}">
+  <div class="card-header">
+    {{ post.subject }}
+    <div class="postInfo">
+      {{ post.posted_by.first_name }} {{ post.posted_by.last_name }} on {{ post.posted_on|date:"Y-m-d" }}
+      - <a href="{{ post.get_absolute_url }}"><i class="fa fa-link" title="permalink to this Post"></i></a>
+      {% if post.parent and not post.motion %}
+      - regarding <a href="{{ post.parent.get_absolute_url }}">{{ post.parent }}</a>
+      {% endif %}
+    </div>
+  </div>
+  <div class="card-body">
+    {{ post.text|restructuredtext }}
+  </div>
+  <div class="card-footer">
+    <div class="d-flex flex-wrap justify-content-end align-items-center">
+      <div class="flex-grow-1">
+	{% if post.followup_posts.all|length > 0 %}
+	<span class="badge badge-primary badge-pill">{% with nr_followups=post.nr_followups %}{{ nr_followups }} repl{{ nr_followups|pluralize:"y,ies" }}{% endwith %}</span>
+	{% endif %}&nbsp;
+	{% if post.post_ptr %}
+	<a href="{% url 'forums:post_create' slug=forum.slug parent_model='post' parent_id=post.post_ptr.id %}">{% if post.motion %}Post a Comment on this Motion{% else %}Reply to the above post{% endif %}</a>
+	{% else %}
+	<a href="{% url 'forums:post_create' slug=forum.slug parent_model='post' parent_id=post.id %}">{% if post.motion %}Post a Comment on this Motion{% else %}Reply to the above post{% endif %}</a>
+	{% endif %}
+      </div>
+
+      {% if post.motion %}
+      <div class="align-self-center px-2">
+	Voting results
+	<span class="text-white-50">
+	{% if request.user in post.motion.in_agreement.all %}
+	<br/>You have voted <span class="text-success">Agree</span>
+	{% elif request.user in post.motion.in_doubt.all %}
+	<br/>You have voted <span class="text-warning">Doubt</span>
+	{% elif request.user in post.motion.in_disagreement.all %}
+	<br/>You have voted <span class="text-danger">Disagree</span>
+	{% elif request.user in post.motion.in_abstain.all %}
+	<br/>You have <span class="text-white-50">Abstained</span>
+	{% elif request.user in post.motion.eligible_for_voting.all %}
+	<br/>[click to vote]
+	{% endif %}
+	</span>
+      </div>
+      <div>
+	<form action="{% url 'forums:motion_vote' slug=forum.slug motion_id=post.motion.id vote='Y' %}" method="post">{% csrf_token %}
+	  <input type="submit" class="btn btn-success" data-toggle="tooltip" data-placement="top" title="Agree" value="{{ post.motion.in_agreement.all|length }}">
+	</form>
+      </div>
+      <div>
+	<form action="{% url 'forums:motion_vote' slug=forum.slug motion_id=post.motion.id vote='M' %}" method="post">{% csrf_token %}
+	  <input type="submit" class="btn btn-warning" data-toggle="tooltip" data-placement="top" title="Doubt" value="{{ post.motion.in_doubt.all|length }}">
+	</form>
+      </div>
+      <div>
+	<form action="{% url 'forums:motion_vote' slug=forum.slug motion_id=post.motion.id vote='N' %}" method="post">{% csrf_token %}
+	  <input type="submit" class="btn btn-danger" data-toggle="tooltip" data-placement="top" title="Disagree" value="{{ post.motion.in_disagreement.all|length }}">
+	</form>
+      </div>
+      <div>
+	<form action="{% url 'forums:motion_vote' slug=forum.slug motion_id=post.motion.id vote='A' %}" method="post">{% csrf_token %}
+	  <input type="submit" class="btn btn-secondary" data-toggle="tooltip" data-placement="top" title="Abstain" value="{{ post.motion.in_abstain.all|length }}">
+	</form>
+      </div>
+      <div class="align-self-center px-2">
+	Voting deadline:<br/>{{ post.motion.voting_deadline|date:'Y-m-d' }}
+      </div>
+      {% endif %}
+    </div>
+  </div>
+  {% if post.followup_posts.all|length > 0 %}
+  <div class="followupPosts{% if post.motion %}AfterMotion{% endif %}">
+    {% for followup in post.followup_posts.all %}
+    {% include 'forums/post_card.html' with post=followup %}
+    {% endfor %}
+  </div>
+  {% endif %}
+</div>
diff --git a/forums/templates/forums/post_confirm_create.html b/forums/templates/forums/post_confirm_create.html
new file mode 100644
index 0000000000000000000000000000000000000000..145af977bfdfa1f595174601cc5f2a67085013b0
--- /dev/null
+++ b/forums/templates/forums/post_confirm_create.html
@@ -0,0 +1,58 @@
+{% extends 'forums/base.html' %}
+
+{% load bootstrap %}
+{% load restructuredtext %}
+
+
+{% block breadcrumb_items %}
+{{ block.super }}
+<span class="breadcrumb-item">Confirm Post creation</span>
+{% endblock %}
+
+{% block pagetitle %}: Post: confirm creation{% endblock pagetitle %}
+
+{% block content %}
+
+<div class="row">
+  <div class="col-12">
+    <h3 class="highlight">Preview</h3>
+
+    <div class="card">
+      <div class="card-header">
+	{{ form.initial.subject }}
+      </div>
+      <div class="card-body">
+	{% if form.initial.text %}
+	{{ form.initial.text|restructuredtext }}
+	{% else %}
+	<span class="text-danger">No text given</span>
+	{% endif %}
+      </div>
+    </div>
+
+    {% if form.errors %}
+    {% for field in form %}
+    {% for error in field.errors %}
+    <div class="alert alert-danger">
+      <strong>{{ field.name }} - {{ error|escape }}</strong>
+    </div>
+    {% endfor %}
+    {% endfor %}
+    {% for error in form.non_field_errors %}
+    <div class="alert alert-danger">
+      <strong>{{ error|escape }}</strong>
+    </div>
+    {% endfor %}
+    {% endif %}
+
+    <form action="" method="post">
+      {% csrf_token %}
+      {{ form|bootstrap }}
+      <input type="submit" value="Confirm Post creation" class="btn btn-primary">
+      <span class="text-danger">&nbsp;<strong>Not satisfied?</strong> Hit your browser's back button and redraft your Post</span>
+    </form>
+
+  </div>
+</div>
+
+{% endblock content %}
diff --git a/forums/templates/forums/post_form.html b/forums/templates/forums/post_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..867b3e610d6ce4bc9dce735f54e7bd3a657de793
--- /dev/null
+++ b/forums/templates/forums/post_form.html
@@ -0,0 +1,27 @@
+{% extends 'forums/base.html' %}
+
+{% load bootstrap %}
+{% load restructuredtext %}
+
+
+{% block breadcrumb_items %}
+{{ block.super }}
+<span class="breadcrumb-item">{% if form.instance.id %}Update {{ form.instance }}{% else %}New Post{% endif %}</span>
+{% endblock %}
+
+{% block pagetitle %}: Post{% endblock pagetitle %}
+
+{% block content %}
+
+<div class="row">
+  <div class="col-12">
+    <h3 class="highlight">Compose a new Post</h3>
+    <form action="" method="post">
+      {% csrf_token %}
+      {{ form|bootstrap }}
+      <input type="submit" value="Run preview" class="btn btn-primary">
+    </form>
+  </div>
+</div>
+
+{% endblock content %}
diff --git a/forums/tests.py b/forums/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6
--- /dev/null
+++ b/forums/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/forums/urls.py b/forums/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d6e766cd90a953f3c2e26520d517bef9d048a40
--- /dev/null
+++ b/forums/urls.py
@@ -0,0 +1,85 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+    url(
+        r'^forum/(?P<parent_model>[a-z]+)/(?P<parent_id>[0-9]+)/add/$',
+        views.ForumCreateView.as_view(),
+        name='forum_create'
+    ),
+    url(
+        r'^add/$',
+        views.ForumCreateView.as_view(),
+        name='forum_create'
+    ),
+    url(
+        r'^meeting/(?P<parent_model>[a-z]+)/(?P<parent_id>[0-9]+)/add/$',
+        views.MeetingCreateView.as_view(),
+        name='meeting_create'
+    ),
+    url(
+        r'^meeting/add/$',
+        views.MeetingCreateView.as_view(),
+        name='meeting_create'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/$',
+        views.ForumDetailView.as_view(),
+        name='forum_detail'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/update/$',
+        views.ForumUpdateView.as_view(),
+        name='forum_update'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/delete/$',
+        views.ForumDeleteView.as_view(),
+        name='forum_delete'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/permissions/(?P<group_id>[0-9]+)/$',
+        views.ForumPermissionsView.as_view(),
+        name='forum_permissions'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/permissions/$',
+        views.ForumPermissionsView.as_view(),
+        name='forum_permissions'
+    ),
+    url(
+        r'^$',
+        views.ForumListView.as_view(),
+        name='forums'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/post/(?P<parent_model>[a-z]+)/(?P<parent_id>[0-9]+)/add/$',
+        views.PostCreateView.as_view(),
+        name='post_create'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/motion/(?P<parent_model>[a-z]+)/(?P<parent_id>[0-9]+)/add/$',
+        views.MotionCreateView.as_view(),
+        name='motion_create'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/post/(?P<parent_model>[a-z]+)/(?P<parent_id>[0-9]+)/add/confirm/$',
+        views.PostConfirmCreateView.as_view(),
+        name='post_confirm_create'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/motion/(?P<parent_model>[a-z]+)/(?P<parent_id>[0-9]+)/add/confirm/$',
+        views.MotionConfirmCreateView.as_view(),
+        name='motion_confirm_create'
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/motion/(?P<motion_id>[0-9]+)/(?P<vote>[YMNA])/$',
+        views.motion_vote,
+        name='motion_vote'
+    ),
+]
diff --git a/forums/views.py b/forums/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc9eb86728d7b1d623d1779db96f6280ec8aa782
--- /dev/null
+++ b/forums/views.py
@@ -0,0 +1,358 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+import datetime
+
+from django import forms
+from django.contrib import messages
+from django.contrib.auth.models import User, Group
+from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import PermissionDenied
+from django.core import serializers
+from django.http import Http404
+from django.shortcuts import get_object_or_404, redirect
+from django.urls import reverse, reverse_lazy
+from django.utils import timezone
+from django.views.generic.detail import DetailView
+from django.views.generic.edit import CreateView, UpdateView, DeleteView
+from django.views.generic.list import ListView
+
+from guardian.decorators import permission_required_or_403
+from guardian.mixins import PermissionRequiredMixin
+from guardian.shortcuts import (assign_perm, remove_perm,
+    get_objects_for_user, get_perms, get_users_with_perms, get_groups_with_perms)
+
+from .models import Forum, Meeting, Post, Motion
+from .forms import (ForumForm, ForumGroupPermissionsForm, ForumOrganizationPermissionsForm,
+                    MeetingForm, PostForm, MotionForm)
+
+from scipost.mixins import PermissionsMixin
+
+
+class ForumCreateView(PermissionsMixin, CreateView):
+    permission_required = 'forums.add_forum'
+    model = Forum
+    form_class = ForumForm
+    template_name = 'forums/forum_form.html'
+    success_url = reverse_lazy('forums:forums')
+
+    def get_initial(self):
+        initial = super().get_initial()
+        parent_model = self.kwargs.get('parent_model')
+        parent_content_type = None
+        parent_object_id = self.kwargs.get('parent_id')
+        if parent_model == 'forum':
+            parent_content_type = ContentType.objects.get(app_label='forums', model='forum')
+        initial.update({
+            'moderators': self.request.user,
+            'parent_content_type': parent_content_type,
+            'parent_object_id': parent_object_id,
+        })
+        return initial
+
+
+class MeetingCreateView(ForumCreateView):
+    model = Meeting
+    form_class = MeetingForm
+
+
+class ForumUpdateView(PermissionRequiredMixin, UpdateView):
+    permission_required = 'forums.update_forum'
+    template_name = 'forums/forum_form.html'
+
+    def get_object(self, queryset=None):
+        try:
+            return Meeting.objects.get(slug=self.kwargs['slug'])
+        except Meeting.DoesNotExist:
+            return Forum.objects.get(slug=self.kwargs['slug'])
+
+    def get_form(self, form_class=None):
+        try:
+            self.object.meeting
+            return MeetingForm(**self.get_form_kwargs())
+        except Meeting.DoesNotExist:
+            return ForumForm(**self.get_form_kwargs())
+
+
+class ForumDeleteView(PermissionRequiredMixin, DeleteView):
+    permission_required = 'forums.delete_forum'
+    model = Forum
+    success_url = reverse_lazy('forums:forums')
+
+    def delete(self, request, *args, **kwargs):
+        """
+        A Forum can only be deleted if it does not have any descendants.
+        Upon deletion, all object-level permissions associated to the
+        Forum are explicitly removed, to avoid orphaned permissions.
+        """
+        forum = get_object_or_404(Forum, slug=self.kwargs.get('slug'))
+        groups_perms_dict = get_groups_with_perms(forum, attach_perms=True)
+        if forum.child_forums.all().count() > 0:
+            messages.warning(request, 'A Forum with descendants cannot be deleted.')
+            return redirect(forum.get_absolute_url())
+        for group, perms_list in groups_perms_dict.items():
+            for perm in perms_list:
+                remove_perm(perm, group, forum)
+        return super().delete(request, *args, **kwargs)
+
+
+class ForumDetailView(PermissionRequiredMixin, DetailView):
+    permission_required = 'forums.can_view_forum'
+    model = Forum
+    template_name = 'forums/forum_detail.html'
+
+    def get_context_data(self, *args, **kwargs):
+        context = super().get_context_data(*args, **kwargs)
+        context['groups_with_perms'] = get_groups_with_perms(self.object).order_by('name')
+        context['users_with_perms'] = get_users_with_perms(self.object)
+        context['group_permissions_form'] = ForumGroupPermissionsForm()
+        context['organization_permissions_form'] = ForumOrganizationPermissionsForm()
+        return context
+
+
+class ForumPermissionsView(PermissionRequiredMixin, UpdateView):
+    permission_required = 'forums.can_change_forum'
+    model = Forum
+    form_class = ForumGroupPermissionsForm
+    template_name = 'forums/forum_permissions.html'
+
+    def get_context_data(self, *args, **kwargs):
+        context = super().get_context_data(*args, **kwargs)
+        try:
+            context['group'] = Group.objects.get(pk=self.kwargs.get('group_id'))
+        except Group.DoesNotExist:
+            pass
+        return context
+
+    def get_initial(self, *args, **kwargs):
+        initial = super().get_initial(*args, **kwargs)
+        try:
+            group = Group.objects.get(pk=self.kwargs.get('group_id'))
+            perms = get_perms (group, self.object)
+            initial['group'] = group.id
+            initial['can_view'] = 'can_view_forum' in perms
+            initial['can_post'] = 'can_post_to_forum' in perms
+        except Group.DoesNotExist:
+            pass
+        return initial
+
+    def form_valid(self, form):
+        if form.cleaned_data['can_view']:
+            assign_perm('can_view_forum', form.cleaned_data['group'], self.object)
+        else:
+            remove_perm('can_view_forum', form.cleaned_data['group'], self.object)
+        if form.cleaned_data['can_post']:
+            assign_perm('can_post_to_forum', form.cleaned_data['group'], self.object)
+        else:
+            remove_perm('can_post_to_forum', form.cleaned_data['group'], self.object)
+        return super().form_valid(form)
+
+
+class ForumListView(LoginRequiredMixin, ListView):
+    model = Forum
+    template_name = 'forum_list.html'
+
+    def get_queryset(self):
+        queryset = get_objects_for_user(self.request.user, 'forums.can_view_forum').anchors()
+        return queryset
+
+
+class PostCreateView(UserPassesTestMixin, CreateView):
+    """
+    First step of a two-step Post creation process.
+    This view, upon successful POST, redirects to the
+    PostConfirmCreateView confirmation view.
+
+    To transfer form data from this view to the next (confirmation) one,
+    two session variables are used, ``post_subject`` and ``post_text``.
+    """
+    model = Post
+    form_class= PostForm
+
+    def test_func(self):
+        if self.request.user.has_perm('forums.add_forum'):
+            return True
+        forum = get_object_or_404(Forum, slug=self.kwargs.get('slug'))
+        if not self.request.user.has_perm('can_post_to_forum', forum):
+            raise PermissionDenied
+        # Only allow posting if it's within a Forum, or within an ongoing meeting.
+        try:
+            if datetime.date.today() > forum.meeting.date_until:
+                raise Http404('You cannot Post to a Meeting which is finished.')
+            elif datetime.date.today () < forum.meeting.date_from:
+                raise Http404('This meeting has not started yet, please come back later!')
+        except Meeting.DoesNotExist:
+            pass
+        return True
+
+
+    def get_initial(self, *args, **kwargs):
+        initial = super().get_initial(*args, **kwargs)
+        parent_model = self.kwargs.get('parent_model')
+        parent_object_id = self.kwargs.get('parent_id')
+        subject = ''
+        if parent_model == 'forum':
+            parent_content_type = ContentType.objects.get(app_label='forums', model='forum')
+        elif parent_model == 'post':
+            parent_content_type = ContentType.objects.get(app_label='forums', model='post')
+            parent = parent_content_type.get_object_for_this_type(pk=parent_object_id)
+            if parent.subject.startswith('Re: ...'):
+                subject = parent.subject
+            elif parent.subject.startswith('Re:'):
+                subject = '%s%s' % ('Re: ...', parent.subject.lstrip('Re:'))
+            else:
+                subject = 'Re: %s' % parent.subject
+        else:
+            raise Http404
+        initial.update({
+            'posted_by': self.request.user,
+            'posted_on': timezone.now(),
+            'parent_content_type': parent_content_type,
+            'parent_object_id': parent_object_id,
+            'subject': subject,
+        })
+        return initial
+
+    def form_valid(self, form):
+        """
+        Save the form data to session variables only, redirect to confirmation view.
+        """
+        self.request.session['post_subject'] = form.cleaned_data['subject']
+        self.request.session['post_text'] = form.cleaned_data['text']
+        return redirect(reverse('forums:post_confirm_create',
+                                kwargs={'slug': self.kwargs.get('slug'),
+                                        'parent_model': self.kwargs.get('parent_model'),
+                                        'parent_id': self.kwargs.get('parent_id')}))
+
+
+class MotionCreateView(PostCreateView):
+    """
+    Specialization of PostCreateView to Motion-class objects.
+
+    By default, all users who can create a Post on the associated
+    Forum are given voting rights.
+    """
+    model = Motion
+    form_class = MotionForm
+    template_name = 'forums/post_form.html'
+
+    def get_initial(self, *args, **kwargs):
+        initial = super().get_initial(*args, **kwargs)
+        forum = get_object_or_404(Forum, slug=self.kwargs.get('slug'))
+        voters = get_users_with_perms(forum)
+        ineligible_ids = []
+        for voter in voters.all():
+            if not voter.has_perm('can_post_to_forum', forum):
+                ineligible_ids.append(voter.id)
+        initial.update({
+            'eligible_for_voting': voters.exclude(id__in=ineligible_ids),
+            })
+        return initial
+
+    def form_valid(self, form):
+        """
+        Save the form data to session variables only, redirect to confirmation view.
+        """
+        self.request.session['post_subject'] = form.cleaned_data['subject']
+        self.request.session['post_text'] = form.cleaned_data['text']
+        self.request.session['eligible_for_voting_ids'] = list(
+            form.cleaned_data['eligible_for_voting'].values_list('pk', flat=True))
+        self.request.session['voting_deadline_year'] = form.cleaned_data['voting_deadline'].year
+        self.request.session['voting_deadline_month'] = form.cleaned_data['voting_deadline'].month
+        self.request.session['voting_deadline_day'] = form.cleaned_data['voting_deadline'].day
+        return redirect(reverse('forums:motion_confirm_create',
+                                kwargs={'slug': self.kwargs.get('slug'),
+                                        'parent_model': self.kwargs.get('parent_model'),
+                                        'parent_id': self.kwargs.get('parent_id')}))
+
+
+class PostConfirmCreateView(PostCreateView):
+    """
+    Second (confirmation) step of Post creation process.
+
+    Upon successful POST, the Post object is saved and the
+    two session variables ``post_subject`` and ``post_text`` are deleted.
+    """
+    form_class = PostForm
+    template_name = 'forums/post_confirm_create.html'
+
+    def get_form(self, form_class=None):
+        form = super().get_form(form_class)
+        form.fields['subject'].widget = forms.HiddenInput()
+        form.fields['text'].widget = forms.HiddenInput()
+        return form
+
+    def get_initial(self, *args, **kwargs):
+        initial = super().get_initial(*args, **kwargs)
+        initial.update({
+            'subject': self.request.session.get('post_subject'),
+            'text': self.request.session.get('post_text'),
+        })
+        return initial
+
+    def form_valid(self, form):
+        """
+        After deleting the session variables used for the confirmation step,
+        simply perform the form_valid calls of form_valid from ancestor classes
+        ModelFormMixin and FormMixin, due to the fact that the form_valid
+        method in the PostCreateView superclass was overriden to a redirect.
+        """
+        del self.request.session['post_subject']
+        del self.request.session['post_text']
+        self.object = form.save()
+        return redirect(self.get_success_url())
+
+
+class MotionConfirmCreateView(PostConfirmCreateView):
+    """
+    Specialization of PostConfirmCreateView to Motion-class objects.
+    """
+    form_class = MotionForm
+
+    def get_initial(self, *args, **kwargs):
+        initial = super().get_initial(*args, **kwargs)
+        voting_deadline = datetime.date(self.request.session.get('voting_deadline_year'),
+                                        self.request.session.get('voting_deadline_month'),
+                                        self.request.session.get('voting_deadline_day'))
+        eligible_for_voting_ids = self.request.session.get('eligible_for_voting_ids')
+        eligible_for_voting = User.objects.filter(id__in=eligible_for_voting_ids)
+        initial.update({
+            'eligible_for_voting': eligible_for_voting,
+            'voting_deadline': voting_deadline,
+        })
+        return initial
+
+    def form_valid(self, form):
+        del self.request.session['eligible_for_voting_ids']
+        del self.request.session['voting_deadline_year']
+        del self.request.session['voting_deadline_month']
+        del self.request.session['voting_deadline_day']
+        self.object = form.save()
+        return super().form_valid(form)
+
+
+@permission_required_or_403('forums.can_post_to_forum',
+                            (Forum, 'slug', 'slug'))
+def motion_vote(request, slug, motion_id, vote):
+    motion = get_object_or_404(Motion, pk=motion_id)
+    if datetime.date.today() > motion.voting_deadline:
+        messages.warning(request, 'The voting deadline on this Motion has passed.')
+    elif motion.eligible_for_voting.filter(pk=request.user.id).exists():
+        motion.in_agreement.remove(request.user)
+        motion.in_doubt.remove(request.user)
+        motion.in_disagreement.remove(request.user)
+        motion.in_abstain.remove(request.user)
+        if vote == 'Y':
+            motion.in_agreement.add(request.user)
+        elif vote == 'M':
+            motion.in_doubt.add(request.user)
+        elif vote == 'N':
+            motion.in_disagreement.add(request.user)
+        elif vote == 'A':
+            motion.in_abstain.add(request.user)
+        motion.save()
+    else:
+        messages.warning(request, 'You do not have voting rights on this Motion.')
+    return redirect(motion.get_absolute_url())
diff --git a/organizations/templates/organizations/dashboard.html b/organizations/templates/organizations/dashboard.html
index 50201f2bc1a5cbdd2a222a361ad23c5bb0a98507..d0f01b9b68adfb988bdc291544e3ff610dafad44 100644
--- a/organizations/templates/organizations/dashboard.html
+++ b/organizations/templates/organizations/dashboard.html
@@ -45,6 +45,9 @@ $(document).ready(function($) {
 	  <li class="nav-item btn btn-outline-secondary">
 	    <a href="{% url 'finances:subsidies' %}" class="nav-link" target="_blank">Subsidies<br/><span class="small text-muted">[full list]</span></a>
 	  </li>
+	  <li class="nav-item btn btn-outline-secondary">
+	    <a href="{% url 'forums:forums' %}" class="nav-link" target="_blank">Forums<br/><span class="small text-muted">[discussion boards<br/>and meetings]</span></a>
+	  </li>
 	  <li class="nav-item btn btn-outline-secondary">
 	    <a href="#board" class="nav-link" data-toggle="tab">Sponsors Board<br/><span class="small text-muted">[registered Contacts]</span></a>
 	  </li>
diff --git a/organizations/views.py b/organizations/views.py
index df80d6f32dd7fee4722a2eae33c087c1283857c3..e93a4f1b7cf9efb5b44df8f0bf6451011dd297c1 100644
--- a/organizations/views.py
+++ b/organizations/views.py
@@ -8,7 +8,8 @@ from django.contrib.auth.mixins import UserPassesTestMixin
 from django.core.exceptions import PermissionDenied
 from django.core.urlresolvers import reverse_lazy
 from django.db import transaction
-from django.shortcuts import get_object_or_404, render, reverse, redirect
+from django.shortcuts import get_object_or_404, render, redirect
+from django.urls import reverse
 from django.utils import timezone
 from django.views.generic.detail import DetailView
 from django.views.generic.edit import CreateView, UpdateView, DeleteView
diff --git a/requirements.txt b/requirements.txt
index 1cb0595fbc83ac7f0fb4fbe8f5aa25faa3b1ab6d..6484cb07dbde59a6afc55e5ffebdcc73ad982e1e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -25,7 +25,7 @@ django-maintenancemode-2==1.1.11
 
 
 # Documentation Packages
-docutils==0.12  # What's this thing?
+docutils==0.14
 Pygments==2.2.0  # Syntax highlighter
 Sphinx==1.4.9
 sphinx-rtd-theme==0.1.9  # Sphinx theme
diff --git a/scipost/static/scipost/assets/css/_forums.scss b/scipost/static/scipost/assets/css/_forums.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f7629b9103c4be03c8b39f45753da737d214edd1
--- /dev/null
+++ b/scipost/static/scipost/assets/css/_forums.scss
@@ -0,0 +1,24 @@
+/**
+* For forums
+*/
+
+.postInfo {
+    float: right;
+    font-size: 90%;
+}
+
+.followupPosts {
+    border-left: 1px solid black;
+    margin: 1rem;
+    padding-left: 0.4rem;
+}
+
+.followupPostsAfterMotion {
+    border-left: 1px solid white;
+    margin: 1rem;
+    padding-left: 0.4rem;
+}
+
+.forumList {
+    padding-left: 0.4rem;
+}
diff --git a/scipost/static/scipost/assets/css/style.scss b/scipost/static/scipost/assets/css/style.scss
index 20477d6909fb0efb2e305fa57f8abfae4e100525..0b24c7e6dcb36cce1f8fe3393b5a0252bde91952 100644
--- a/scipost/static/scipost/assets/css/style.scss
+++ b/scipost/static/scipost/assets/css/style.scss
@@ -22,6 +22,7 @@
 @import "cards";
 @import "code";
 @import "form";
+@import "forums";
 @import "grid";
 @import "homepage";
 @import "labels";
diff --git a/scipost/templatetags/lookup.py b/scipost/templatetags/lookup.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f481a1426a96e34f77660436cf5f96600eefdf6
--- /dev/null
+++ b/scipost/templatetags/lookup.py
@@ -0,0 +1,35 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django.contrib.auth.models import User, Group
+
+from ajax_select import register, LookupChannel
+
+
+@register('user_lookup')
+class UserLookup(LookupChannel):
+    model = User
+
+    def get_query(self, q, request):
+        return self.model.objects.filter(last_name__icontains=q)[:10]
+
+    def format_item_display(self, item):
+        return "<span class='auto_lookup_display'>%s, %s</span>" % (item.last_name, item.first_name)
+
+    def format_match(self, item):
+        return "%s, %s" % (item.last_name, item.first_name)
+
+
+@register('group_lookup')
+class GroupLookup(LookupChannel):
+    model = Group
+
+    def get_query(self, q, request):
+        return self.model.objects.filter(name__icontains=q)[:10]
+
+    def format_item_display(self, item):
+        return "<span class='auto_lookup_display'>%s</span>" % item.name
+
+    def format_match(self, item):
+        return item.name
diff --git a/scipost/templatetags/restructuredtext.py b/scipost/templatetags/restructuredtext.py
new file mode 100644
index 0000000000000000000000000000000000000000..a13399059e21a450dc9e25be57dcf159a253094a
--- /dev/null
+++ b/scipost/templatetags/restructuredtext.py
@@ -0,0 +1,20 @@
+__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
+__license__ = "AGPL v3"
+
+
+from django import template
+from django.utils.encoding import force_text
+from django.utils.safestring import mark_safe
+
+
+register = template.Library()
+
+
+@register.filter(name='restructuredtext')
+def restructuredtext(text):
+    if not text:
+        return ''
+    from docutils.core import publish_parts
+    parts = publish_parts(source=text,
+                          writer_name='html5_polyglot')
+    return mark_safe(force_text(parts['html_body']))