From 98e9377fe55e28d32da2508d668fef6686ae61e9 Mon Sep 17 00:00:00 2001
From: "J.-S. Caux" <J.S.Caux@uva.nl>
Date: Sat, 21 Jan 2017 15:49:58 +0100
Subject: [PATCH] DO MIGRATE, also rerun add_groups_and_permissions management
 command.

Add facilities for Virtual General Meetings,
including Feedback, Nomination, Motion objects.
---
 scipost/admin.py                              |  18 ++
 scipost/forms.py                              |  55 ++++
 .../commands/add_groups_and_permissions.py    |   8 +
 scipost/migrations/0030_auto_20170118_1406.py |  64 +++++
 scipost/migrations/0031_remark_motion.py      |  21 ++
 scipost/migrations/0032_auto_20170121_1032.py |  43 +++
 scipost/migrations/0033_nomination_vgm.py     |  21 ++
 scipost/migrations/0034_motion_category.py    |  20 ++
 scipost/migrations/0035_vgm_information.py    |  20 ++
 scipost/migrations/0036_feedback.py           |  26 ++
 scipost/models.py                             | 205 +++++++++++++-
 scipost/templates/scipost/VGM_detail.html     | 264 ++++++++++++++++++
 scipost/templates/scipost/VGMs.html           |  26 ++
 scipost/templates/scipost/personal_page.html  |   6 +
 scipost/urls.py                               |  20 ++
 scipost/views.py                              | 192 +++++++++++++
 16 files changed, 1008 insertions(+), 1 deletion(-)
 create mode 100644 scipost/migrations/0030_auto_20170118_1406.py
 create mode 100644 scipost/migrations/0031_remark_motion.py
 create mode 100644 scipost/migrations/0032_auto_20170121_1032.py
 create mode 100644 scipost/migrations/0033_nomination_vgm.py
 create mode 100644 scipost/migrations/0034_motion_category.py
 create mode 100644 scipost/migrations/0035_vgm_information.py
 create mode 100644 scipost/migrations/0036_feedback.py
 create mode 100644 scipost/templates/scipost/VGM_detail.html
 create mode 100644 scipost/templates/scipost/VGMs.html

diff --git a/scipost/admin.py b/scipost/admin.py
index 3c7650cc2..69dbc4201 100644
--- a/scipost/admin.py
+++ b/scipost/admin.py
@@ -20,6 +20,24 @@ admin.site.unregister(User)
 admin.site.register(User, UserAdmin)
 
 
+class VGMAdmin(admin.ModelAdmin):
+    search_fields = ['start_date']
+
+admin.site.register(VGM, VGMAdmin)
+
+
+class NominationAdmin(admin.ModelAdmin):
+    search_fields = ['last_name', 'first_name', 'by']
+
+admin.site.register(Nomination, NominationAdmin)
+
+
+class MotionAdmin(admin.ModelAdmin):
+    search_fields = ['background', 'motion', 'put_forward_by']
+
+admin.site.register(Motion, MotionAdmin)
+
+
 class RemarkAdmin(admin.ModelAdmin):
     search_fields = ['contributor', 'remark']
 
diff --git a/scipost/forms.py b/scipost/forms.py
index d2e7a9008..39023c20f 100644
--- a/scipost/forms.py
+++ b/scipost/forms.py
@@ -370,3 +370,58 @@ class SPBMembershipForm(forms.ModelForm):
                     css_class="col-4"),
                 css_class="row"),
         )
+
+#################
+# VGMs, Motions #
+#################
+
+class FeedbackForm(forms.ModelForm):
+    class Meta:
+        model = Feedback
+        fields = ['feedback']
+
+
+class NominationForm(forms.ModelForm):
+    class Meta:
+        model = Nomination
+        fields = ['first_name', 'last_name', 'discipline', 'expertises']
+
+    def __init__(self, *args, **kwargs):
+        super(NominationForm, self).__init__(*args, **kwargs)
+        self.fields['expertises'].widget = forms.SelectMultiple(choices=SCIPOST_SUBJECT_AREAS)
+
+
+class MotionForm(forms.ModelForm):
+    class Meta:
+        model = Motion
+        fields = ['category', 'background', 'motion']
+
+    def __init__(self, *args, **kwargs):
+        super(MotionForm, self).__init__(*args, **kwargs)
+        self.fields['background'].label = ''
+        self.fields['background'].widget.attrs.update(
+            {'rows': 8, 'cols': 100,
+             'placeholder': 'Provide useful background information on your Motion.'})
+        self.fields['motion'].label = ''
+        self.fields['motion'].widget.attrs.update(
+            {'rows': 8, 'cols': 100,
+             'placeholder': 'Phrase your Motion as clearly and succinctly as possible.'})
+        self.helper = FormHelper()
+        self.helper.layout = Layout(
+            Field('category'),
+            Div(
+                Div(HTML('<p>Background:</p>'),
+                    css_class="col-2"),
+                Div(
+                    Field('background'),
+                    css_class="col-10"),
+                css_class="row"),
+            Div(
+                Div(HTML('<p>Motion:</p>'),
+                    css_class="col-2"),
+                Div(
+                    Field('motion'),
+                    css_class="col-10"),
+                css_class="row"),
+            Submit('submit', 'Submit'),
+        )
diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py
index a4feaec1d..01bedc7fd 100644
--- a/scipost/management/commands/add_groups_and_permissions.py
+++ b/scipost/management/commands/add_groups_and_permissions.py
@@ -68,6 +68,10 @@ class Command(BaseCommand):
             codename='view_bylaws',
             name='Can view By-laws of Editorial College',
             content_type=content_type)
+        can_attend_VGMs, created = Permission.objects.get_or_create(
+            codename='can_attend_VGMs',
+            name='Can attend Virtual General Meetings',
+            content_type=content_type)
 
         # Contributions (not related to submissions)
         can_submit_comments, created = Permission.objects.get_or_create(
@@ -172,9 +176,11 @@ class Command(BaseCommand):
             can_assign_submissions,
             can_prepare_recommendations_for_voting,
             can_fix_College_decision,
+            can_attend_VGMs,
         )
         AdvisoryBoard.permissions.add(
             can_manage_registration_invitations,
+            can_attend_VGMs,
         )
         EditorialAdmin.permissions.add(
             can_view_pool,
@@ -182,12 +188,14 @@ class Command(BaseCommand):
             can_prepare_recommendations_for_voting,
             can_fix_College_decision,
             can_publish_accepted_submission,
+            can_attend_VGMs,
             )
         EditorialCollege.permissions.add(
             can_view_pool,
             can_take_charge_of_submissions,
             can_vet_submitted_reports,
             view_bylaws,
+            can_attend_VGMs,
         )
         VettingEditors.permissions.add(
             can_vet_commentary_requests,
diff --git a/scipost/migrations/0030_auto_20170118_1406.py b/scipost/migrations/0030_auto_20170118_1406.py
new file mode 100644
index 000000000..c938c3a39
--- /dev/null
+++ b/scipost/migrations/0030_auto_20170118_1406.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-01-18 13:06
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('scipost', '0029_remark_submission'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Motion',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('background', models.TextField()),
+                ('motion', models.TextField()),
+                ('date', models.DateField()),
+                ('nr_A', models.PositiveIntegerField(default=0)),
+                ('nr_N', models.PositiveIntegerField(default=0)),
+                ('nr_D', models.PositiveIntegerField(default=0)),
+                ('voting_deadline', models.DateTimeField(default=django.utils.timezone.now, verbose_name='voting deadline')),
+                ('accepted', models.NullBooleanField()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='VGM',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('start_date', models.DateField()),
+                ('end_date', models.DateField()),
+            ],
+        ),
+        migrations.AddField(
+            model_name='motion',
+            name='VGM',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.VGM'),
+        ),
+        migrations.AddField(
+            model_name='motion',
+            name='in_agreement',
+            field=models.ManyToManyField(blank=True, related_name='in_agreement_with_motion', to='scipost.Contributor'),
+        ),
+        migrations.AddField(
+            model_name='motion',
+            name='in_disagreement',
+            field=models.ManyToManyField(blank=True, related_name='in_disagreement_with_motion', to='scipost.Contributor'),
+        ),
+        migrations.AddField(
+            model_name='motion',
+            name='in_notsure',
+            field=models.ManyToManyField(blank=True, related_name='in_notsure_with_motion', to='scipost.Contributor'),
+        ),
+        migrations.AddField(
+            model_name='motion',
+            name='put_forward_by',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor'),
+        ),
+    ]
diff --git a/scipost/migrations/0031_remark_motion.py b/scipost/migrations/0031_remark_motion.py
new file mode 100644
index 000000000..cd4f9fc8b
--- /dev/null
+++ b/scipost/migrations/0031_remark_motion.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-01-18 16:45
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('scipost', '0030_auto_20170118_1406'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='remark',
+            name='motion',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.Motion'),
+        ),
+    ]
diff --git a/scipost/migrations/0032_auto_20170121_1032.py b/scipost/migrations/0032_auto_20170121_1032.py
new file mode 100644
index 000000000..0ed75e0e5
--- /dev/null
+++ b/scipost/migrations/0032_auto_20170121_1032.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-01-21 09:32
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import scipost.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('scipost', '0031_remark_motion'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Nomination',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('date', models.DateField()),
+                ('first_name', models.CharField(default='', max_length=30)),
+                ('last_name', models.CharField(default='', max_length=30)),
+                ('discipline', models.CharField(choices=[('physics', 'Physics'), ('astrophysics', 'Astrophysics'), ('mathematics', 'Mathematics'), ('computerscience', 'Computer Science')], default='physics', max_length=20, verbose_name='Main discipline')),
+                ('expertises', scipost.models.ChoiceArrayField(base_field=models.CharField(choices=[('Physics', (('Phys:AE', 'Atomic, Molecular and Optical Physics - Experiment'), ('Phys:AT', 'Atomic, Molecular and Optical Physics - Theory'), ('Phys:BI', 'Biophysics'), ('Phys:CE', 'Condensed Matter Physics - Experiment'), ('Phys:CT', 'Condensed Matter Physics - Theory'), ('Phys:FD', 'Fluid Dynamics'), ('Phys:GR', 'Gravitation, Cosmology and Astroparticle Physics'), ('Phys:HE', 'High-Energy Physics - Experiment'), ('Phys:HT', 'High-Energy Physics- Theory'), ('Phys:HP', 'High-Energy Physics - Phenomenology'), ('Phys:MP', 'Mathematical Physics'), ('Phys:NE', 'Nuclear Physics - Experiment'), ('Phys:NT', 'Nuclear Physics - Theory'), ('Phys:QP', 'Quantum Physics'), ('Phys:SM', 'Statistical and Soft Matter Physics'))), ('Astrophysics', (('Astro:GA', 'Astrophysics of Galaxies'), ('Astro:CO', 'Cosmology and Nongalactic Astrophysics'), ('Astro:EP', 'Earth and Planetary Astrophysics'), ('Astro:HE', 'High Energy Astrophysical Phenomena'), ('Astro:IM', 'Instrumentation and Methods for Astrophysics'), ('Astro:SR', 'Solar and Stellar Astrophysics'))), ('Mathematics', (('Math:AG', 'Algebraic Geometry'), ('Math:AT', 'Algebraic Topology'), ('Math:AP', 'Analysis of PDEs'), ('Math:CT', 'Category Theory'), ('Math:CA', 'Classical Analysis and ODEs'), ('Math:CO', 'Combinatorics'), ('Math:AC', 'Commutative Algebra'), ('Math:CV', 'Complex Variables'), ('Math:DG', 'Differential Geometry'), ('Math:DS', 'Dynamical Systems'), ('Math:FA', 'Functional Analysis'), ('Math:GM', 'General Mathematics'), ('Math:GN', 'General Topology'), ('Math:GT', 'Geometric Topology'), ('Math:GR', 'Group Theory'), ('Math:HO', 'History and Overview'), ('Math:IT', 'Information Theory'), ('Math:KT', 'K-Theory and Homology'), ('Math:LO', 'Logic'), ('Math:MP', 'Mathematical Physics'), ('Math:MG', 'Metric Geometry'), ('Math:NT', 'Number Theory'), ('Math:NA', 'Numerical Analysis'), ('Math:OA', 'Operator Algebras'), ('Math:OC', 'Optimization and Control'), ('Math:PR', 'Probability'), ('Math:QA', 'Quantum Algebra'), ('Math:RT', 'Representation Theory'), ('Math:RA', 'Rings and Algebras'), ('Math:SP', 'Spectral Theory'), ('Math:ST', 'Statistics Theory'), ('Math:SG', 'Symplectic Geometry'))), ('Computer Science', (('Comp:AI', 'Artificial Intelligence'), ('Comp:CC', 'Computational Complexity'), ('Comp:CE', 'Computational Engineering, Finance, and Science'), ('Comp:CG', 'Computational Geometry'), ('Comp:GT', 'Computer Science and Game Theory'), ('Comp:CV', 'Computer Vision and Pattern Recognition'), ('Comp:CY', 'Computers and Society'), ('Comp:CR', 'Cryptography and Security'), ('Comp:DS', 'Data Structures and Algorithms'), ('Comp:DB', 'Databases'), ('Comp:DL', 'Digital Libraries'), ('Comp:DM', 'Discrete Mathematics'), ('Comp:DC', 'Distributed, Parallel, and Cluster Computing'), ('Comp:ET', 'Emerging Technologies'), ('Comp:FL', 'Formal Languages and Automata Theory'), ('Comp:GL', 'General Literature'), ('Comp:GR', 'Graphics'), ('Comp:AR', 'Hardware Architecture'), ('Comp:HC', 'Human-Computer Interaction'), ('Comp:IR', 'Information Retrieval'), ('Comp:IT', 'Information Theory'), ('Comp:LG', 'Learning'), ('Comp:LO', 'Logic in Computer Science'), ('Comp:MS', 'Mathematical Software'), ('Comp:MA', 'Multiagent Systems'), ('Comp:MM', 'Multimedia'), ('Comp:NI', 'Networking and Internet Architecture'), ('Comp:NE', 'Neural and Evolutionary Computing'), ('Comp:NA', 'Numerical Analysis'), ('Comp:OS', 'Operating Systems'), ('Comp:OH', 'Other Computer Science'), ('Comp:PF', 'Performance'), ('Comp:PL', 'Programming Languages'), ('Comp:RO', 'Robotics'), ('Comp:SI', 'Social and Information Networks'), ('Comp:SE', 'Software Engineering'), ('Comp:SD', 'Sound'), ('Comp:SC', 'Symbolic Computation'), ('Comp:SY', 'Systems and Control')))], max_length=10), blank=True, null=True, size=None)),
+                ('nr_A', models.PositiveIntegerField(default=0)),
+                ('nr_N', models.PositiveIntegerField(default=0)),
+                ('nr_D', models.PositiveIntegerField(default=0)),
+                ('voting_deadline', models.DateTimeField(default=django.utils.timezone.now, verbose_name='voting deadline')),
+                ('accepted', models.NullBooleanField()),
+                ('by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor')),
+                ('in_agreement', models.ManyToManyField(blank=True, related_name='in_agreement_with_nomination', to='scipost.Contributor')),
+                ('in_disagreement', models.ManyToManyField(blank=True, related_name='in_disagreement_with_nomination', to='scipost.Contributor')),
+                ('in_notsure', models.ManyToManyField(blank=True, related_name='in_notsure_with_nomination', to='scipost.Contributor')),
+            ],
+        ),
+        migrations.AddField(
+            model_name='remark',
+            name='nomination',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.Nomination'),
+        ),
+    ]
diff --git a/scipost/migrations/0033_nomination_vgm.py b/scipost/migrations/0033_nomination_vgm.py
new file mode 100644
index 000000000..ea1713d6a
--- /dev/null
+++ b/scipost/migrations/0033_nomination_vgm.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-01-21 09:53
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('scipost', '0032_auto_20170121_1032'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='nomination',
+            name='VGM',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.VGM'),
+        ),
+    ]
diff --git a/scipost/migrations/0034_motion_category.py b/scipost/migrations/0034_motion_category.py
new file mode 100644
index 000000000..2f3ab3c66
--- /dev/null
+++ b/scipost/migrations/0034_motion_category.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-01-21 10:12
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('scipost', '0033_nomination_vgm'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='motion',
+            name='category',
+            field=models.CharField(choices=[('ByLawAmend', 'Amendments to by-laws'), ('Workflow', 'Editorial workflow improvements'), ('General', 'General')], default='General', max_length=10),
+        ),
+    ]
diff --git a/scipost/migrations/0035_vgm_information.py b/scipost/migrations/0035_vgm_information.py
new file mode 100644
index 000000000..9037d4137
--- /dev/null
+++ b/scipost/migrations/0035_vgm_information.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-01-21 13:19
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('scipost', '0034_motion_category'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='vgm',
+            name='information',
+            field=models.TextField(default=''),
+        ),
+    ]
diff --git a/scipost/migrations/0036_feedback.py b/scipost/migrations/0036_feedback.py
new file mode 100644
index 000000000..ed0da2b20
--- /dev/null
+++ b/scipost/migrations/0036_feedback.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-01-21 14:17
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('scipost', '0035_vgm_information'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Feedback',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('date', models.DateField()),
+                ('feedback', models.TextField()),
+                ('VGM', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='scipost.VGM')),
+                ('by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor')),
+            ],
+        ),
+    ]
diff --git a/scipost/models.py b/scipost/models.py
index 9025eddd8..ed76709b1 100644
--- a/scipost/models.py
+++ b/scipost/models.py
@@ -4,6 +4,7 @@ from django import forms
 from django.contrib.auth.models import User, Group
 from django.contrib.postgres.fields import ArrayField, JSONField
 from django.db import models
+from django.shortcuts import get_object_or_404
 from django.template import Template, Context
 from django.utils import timezone
 from django.utils.safestring import mark_safe
@@ -348,6 +349,10 @@ class UnavailabilityPeriod(models.Model):
 
 class Remark(models.Model):
     contributor = models.ForeignKey(Contributor, on_delete=models.CASCADE)
+    nomination = models.ForeignKey('scipost.Nomination', on_delete=models.CASCADE,
+                               blank=True, null=True)
+    motion = models.ForeignKey('scipost.Motion', on_delete=models.CASCADE,
+                               blank=True, null=True)
     submission = models.ForeignKey('submissions.Submission',
                                    on_delete=models.CASCADE,
                                    blank=True, null=True)
@@ -363,7 +368,7 @@ class Remark(models.Model):
                 + self.date.strftime("%Y-%m-%d"))
 
     def as_li(self):
-        output = '<li>{{ by }}<p>{{ remark }}</p>'
+        output = '<li><em>{{ by }}</em><p>{{ remark }}</p>'
         context = Context({'by': str(self),
                            'remark': self.remark})
         template = Template(output)
@@ -566,6 +571,204 @@ class NewsItem(models.Model):
         return template.render(context)
 
 
+#####################################
+# Virtual General Meetings, Motions #
+#####################################
+
+class VGM(models.Model):
+    """
+    Each year, a Virtual General Meeting is held during which operations at
+    SciPost are discussed. A VGM can be attended by Administrators,
+    Advisory Board members and Editorial Fellows.
+    """
+    start_date = models.DateField()
+    end_date = models.DateField()
+    information = models.TextField(default='')
+
+    def __str__(self):
+        return 'From %s to %s' % (self.start_date.strftime('%Y-%m-%d'),
+                                  self.end_date.strftime('%Y-%m-%d'))
+
+
+class Feedback(models.Model):
+    """
+    Feedback, suggestion or criticism on any aspect of SciPost.
+    """
+    VGM = models.ForeignKey(VGM, blank=True, null=True)
+    by = models.ForeignKey(Contributor)
+    date = models.DateField()
+    feedback = models.TextField()
+
+    def __str__(self):
+        return '%s: %s' % (self.by, self.feedback[:50])
+
+    def as_li(self):
+        html = ('<div class="Feedback">'
+                '<h3><em>by {{ by }}</em></h3>'
+                '<p>{{ feedback|linebreaks }}</p>'
+                '</div>')
+        context = Context({
+            'feedback': self.feedback,
+            'by': '%s %s' % (self.by.user.first_name,
+                                   self.by.user.last_name)})
+        template = Template(html)
+        return template.render(context)
+
+
+class Nomination(models.Model):
+    """
+    Nomination to an Editorial Fellowship.
+    """
+    VGM = models.ForeignKey(VGM, blank=True, null=True)
+    by = models.ForeignKey(Contributor)
+    date = models.DateField()
+    first_name = models.CharField(max_length=30, default='')
+    last_name = models.CharField(max_length=30, default='')
+    discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES,
+                                  default='physics', verbose_name='Main discipline')
+    expertises = ChoiceArrayField(
+        models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS),
+        blank=True, null=True)
+    nr_A = models.PositiveIntegerField(default=0)
+    in_agreement = models.ManyToManyField(Contributor,
+                                          related_name='in_agreement_with_nomination', blank=True)
+    nr_N = models.PositiveIntegerField(default=0)
+    in_notsure = models.ManyToManyField(Contributor,
+                                        related_name='in_notsure_with_nomination', blank=True)
+    nr_D = models.PositiveIntegerField(default=0)
+    in_disagreement = models.ManyToManyField(Contributor,
+                                             related_name='in_disagreement_with_nomination',
+                                             blank=True)
+    voting_deadline = models.DateTimeField('voting deadline', default=timezone.now)
+    accepted = models.NullBooleanField()
+
+    def __str__(self):
+        return '%s %s (nominated by %s)' % (self.first_name,
+                                            self.last_name,
+                                            self.by)
+
+    def as_li(self):
+        html = ('<div class="Nomination" id="nomination_id{{ nomination_id }}">'
+                '<h3><em> {{ name }}, nominated by {{ proposer }}</em></h3>'
+                '</div>')
+        context = Context({
+            'nomination_id': self.id,
+            'proposer': '%s %s' % (self.by.user.first_name,
+                                   self.by.user.last_name),
+            'name': self.first_name + ' ' + self.last_name,})
+        template = Template(html)
+        return template.render(context)
+
+    def votes_as_ul(self):
+        template = Template('''
+        <ul class="opinionsDisplay">
+        <li style="background-color: #000099">Agree {{ nr_A }}</li>
+        <li style="background-color: #555555">Abstain {{ nr_N }}</li>
+        <li style="background-color: #990000">Disagree {{ nr_D }}</li>
+        </ul>
+        ''')
+        context = Context ({'nr_A': self.nr_A, 'nr_N': self.nr_N, 'nr_D': self.nr_D})
+        return template.render(context)
+
+    def update_votes(self, contributor_id, vote):
+        contributor = get_object_or_404(Contributor, pk=contributor_id)
+        self.in_agreement.remove(contributor)
+        self.in_notsure.remove(contributor)
+        self.in_disagreement.remove(contributor)
+        if vote == 'A':
+            self.in_agreement.add(contributor)
+        elif vote == 'N':
+            self.in_notsure.add(contributor)
+        elif vote == 'D':
+            self.in_disagreement.add(contributor)
+        self.nr_A = self.in_agreement.count()
+        self.nr_N = self.in_notsure.count()
+        self.nr_D = self.in_disagreement.count()
+        self.save()
+
+
+MOTION_CATEGORIES = (
+    ('ByLawAmend', 'Amendments to by-laws'),
+    ('Workflow', 'Editorial workflow improvements'),
+    ('General', 'General'),
+)
+motion_categories_dict=dict(MOTION_CATEGORIES)
+
+
+class Motion(models.Model):
+    """
+    Motion instances are put forward to the Advisory Board and Editorial College
+    and detail suggested changes to rules, procedures etc.
+    They are meant to be voted on at the annual VGM.
+    """
+    category = models.CharField(max_length=10, choices=MOTION_CATEGORIES,
+                                default='General')
+    VGM = models.ForeignKey(VGM, blank=True, null=True)
+    background = models.TextField()
+    motion = models.TextField()
+    put_forward_by = models.ForeignKey(Contributor)
+    date = models.DateField()
+    nr_A = models.PositiveIntegerField(default=0)
+    in_agreement = models.ManyToManyField(Contributor,
+                                          related_name='in_agreement_with_motion', blank=True)
+    nr_N = models.PositiveIntegerField(default=0)
+    in_notsure = models.ManyToManyField(Contributor,
+                                        related_name='in_notsure_with_motion', blank=True)
+    nr_D = models.PositiveIntegerField(default=0)
+    in_disagreement = models.ManyToManyField(Contributor,
+                                             related_name='in_disagreement_with_motion',
+                                             blank=True)
+    voting_deadline = models.DateTimeField('voting deadline', default=timezone.now)
+    accepted = models.NullBooleanField()
+
+    def __str__(self):
+        return self.motion[:32]
+
+    def as_li(self):
+        html = ('<div class="Motion" id="motion_id{{ motion_id }}">'
+                '<h3><em>Motion {{ motion_id }}, put forward by {{ proposer }}</em></h3>'
+                '<h3>Background:</h3><p>{{ background|linebreaks }}</p>'
+                '<h3>Motion:</h3>'
+                '<div class="flex-container"><div class="flex-greybox">'
+                '<p style="background-color: #eeeeee;">{{ motion|linebreaks }}</p>'
+                '</div></div>'
+                '</div>')
+        context = Context({
+            'motion_id': self.id,
+            'proposer': '%s %s' % (self.put_forward_by.user.first_name,
+                                   self.put_forward_by.user.last_name),
+            'background': self.background,
+            'motion': self.motion,})
+        template = Template(html)
+        return template.render(context)
+
+    def votes_as_ul(self):
+        template = Template('''
+        <ul class="opinionsDisplay">
+        <li style="background-color: #000099">Agree {{ nr_A }}</li>
+        <li style="background-color: #555555">Abstain {{ nr_N }}</li>
+        <li style="background-color: #990000">Disagree {{ nr_D }}</li>
+        </ul>
+        ''')
+        context = Context ({'nr_A': self.nr_A, 'nr_N': self.nr_N, 'nr_D': self.nr_D})
+        return template.render(context)
+
+    def update_votes(self, contributor_id, vote):
+        contributor = get_object_or_404(Contributor, pk=contributor_id)
+        self.in_agreement.remove(contributor)
+        self.in_notsure.remove(contributor)
+        self.in_disagreement.remove(contributor)
+        if vote == 'A':
+            self.in_agreement.add(contributor)
+        elif vote == 'N':
+            self.in_notsure.add(contributor)
+        elif vote == 'D':
+            self.in_disagreement.add(contributor)
+        self.nr_A = self.in_agreement.count()
+        self.nr_N = self.in_notsure.count()
+        self.nr_D = self.in_disagreement.count()
+        self.save()
+
 
 #########
 # Lists #
diff --git a/scipost/templates/scipost/VGM_detail.html b/scipost/templates/scipost/VGM_detail.html
new file mode 100644
index 000000000..a1e423d0f
--- /dev/null
+++ b/scipost/templates/scipost/VGM_detail.html
@@ -0,0 +1,264 @@
+{% extends 'scipost/base.html' %}
+
+{% block pagetitle %}: VGM detail{% endblock pagetitle %}
+
+{% load staticfiles %}
+
+{% block bodysup %}
+
+<script>
+$(document).ready(function(){
+
+  $("#submitFeedbackForm").hide();
+  $("#submitFeedbackButton").click( function() {
+     $(this).next("form").toggle();
+  });
+
+  $("#submitNominationForm").hide();
+  $("#submitNominationButton").click( function() {
+     $(this).next("form").toggle();
+  });
+
+  $("#submitMotionForm").hide();
+  $("#submitMotionButton").click( function() {
+     $(this).next("form").toggle();
+  });
+
+  $(".submitRemarkForm").hide();
+
+  $(".submitRemarkButton").click( function() {
+     $(this).next("div").toggle();
+  });
+  });
+
+</script>
+
+<section>
+  <div class="flex-container">
+    <div class="flex-greybox">
+      <h1>SciPost Virtual General Meeting</h1>
+    </div>
+  </div>
+
+  <ul>
+    <li>This VGM is scheduled from {{ VGM.start_date|date:'Y-m-d' }} to {{ VGM.end_date|date:'Y-m-d' }}.</li>
+    <li>Your feedback/suggestions/criticisms on any aspect of SciPost are greatly valued. Provide them by filling the <a href="#FeedbackBox">feedback form</a>.</li>
+    <li>Your nominations to the Editorial College are welcome. Simply fill the <a href="#NominationBox">nomination form</a>, and cast your vote on current nominations.</li>
+    <li>For substantial changes, for example to the by-laws, new Motions can be put forward until the end of the meeting using the <a href="#MotionBox">form</a>.</li>
+    <li>Voting on Motions is open until one week after the meeting.</li>
+    <li>You a referred to the <a href="{% url 'scipost:EdCol_by-laws' %}">by-laws</a>, section 2 for further details about the procedures.</li>
+  </ul>
+  <br/>
+  <h2>On this page:</h2>
+  <ul>
+    <li><a href="#Information">Information message</a></li>
+    <li><a href="#Nominations">Nominations</a></li>
+    <li><a href="#Motions">Motions</a></li>
+  </ul>
+  <br/>
+  <hr class="hr12"/>
+</section>
+
+
+<section id="Information">
+  <div class="flex-container">
+    <div class="flex-greybox">
+      <h2>Information message from SciPost Administration</h2>
+    </div>
+  </div>
+  <div class="flex-whitebox">
+    {{ VGM_information }}
+  </div>
+  <h3>Feedback received for this VGM:</h3>
+  <div class="row">
+    <div class="col-1"></div>
+    <div class="col-10">
+      <ul>
+	{% for feedback in feedback_received %}
+	<li>{{ feedback.as_li }}</li>
+	{% endfor %}
+      </ul>
+    </div>
+  </div>
+  <div class="flex-container">
+    <div class="flex-greybox" id="FeedbackBox">
+      <h3>Feedback on SciPost</h3>
+      <button id="submitFeedbackButton">Provide feedback</button>
+      <form id="submitFeedbackForm" action="{% url 'scipost:feedback' VGM_id=VGM.id %}" method="post">
+	{% csrf_token %}
+	{{ feedback_form.as_p }}
+	<input type="submit" value="Submit"/>
+      </form>
+    </div>
+  </div>
+  <hr class="hr12"/>
+</section>
+
+<section id="Nominations">
+  <div class="flex-container">
+    <div class="flex-greybox" id="NominationBox">
+      <h2>Nominations to the Editorial College</h2>
+      <button id="submitNominationButton">Nominate an Editorial Fellow candidate</button>
+      <form id="submitNominationForm" action="{% url 'scipost:nominate_Fellow' VGM_id=VGM.id %}" method="post">
+	{% csrf_token %}
+	{{ nomination_form.as_p }}
+        <input type="submit" value="Submit"/>
+      </form>
+    </div>
+  </div>
+
+  {% if nominations %}
+  <div class="row">
+    <div class="flex-container">
+      <div class="flex-greybox">
+	<h2>Nominations under consideration</h2>
+      </div>
+    </div>
+    <div class="col-1"></div>
+    <div class="col-10">
+    <ul>
+      {% for nomination in nominations %}
+      <li>
+	{{ nomination.as_li }}
+	<br/>
+	<div class="opinionsDisplay">
+	  <h4>Your opinion on this Nomination (voting deadline: {{ nomination.voting_deadline|date:'y-m-d' }}):</h4>
+	  <form action="{% url 'scipost:vote_on_nomination' nomination_id=nomination.id vote='A' %}" method="post">
+            {% csrf_token %}
+            <input type="submit" class="agree" value="Agree {{ nomination.nr_A }} "/>
+	  </form>
+	  <form action="{% url 'scipost:vote_on_nomination' nomination_id=nomination.id vote='N' %}" method="post">
+            {% csrf_token %}
+            <input type="submit" class="notsure" value="Not sure {{ nomination.nr_N }}"/>
+	  </form>
+	  <form action="{% url 'scipost:vote_on_nomination' nomination_id=nomination.id vote='D'%}" method="post">
+            {% csrf_token %}
+            <input type="submit" class="disagree" value="Disagree {{ nomination.nr_D }}"/>
+	  </form>
+	  {% if request.user.contributor in nomination.in_agreement.all %}
+	  <strong>(you have voted: Agreed)</strong>
+	  {% elif request.user.contributor in nomination.in_notsure.all %}
+	  <strong>(you have voted: Not sure)</strong>
+	  {% elif request.user.contributor in nomination.in_disagreement.all %}
+	  <strong>(you have voted: Disagree)</strong>
+	  {% endif %}
+	</div>
+	<br/><br/>
+	{% if nomination.remark_set.all %}
+	<h3>Remarks on this nomination:</h3>
+	<ul>
+	  {% for rem in nomination.remark_set.all %}
+	  {{ rem.as_li }}
+	  {% endfor %}
+	</ul>
+	{% endif %}
+	<button class="submitRemarkButton" id="remarkButton{{ nomination.id }}">Add a remark on this Nomination</button>
+	<div class="submitRemarkForm" id="remarkForm{{ nomination.id }}">
+	  <form action="{% url 'scipost:add_remark_on_nomination' VGM_id=VGM.id nomination_id=nomination.id %}" method="post">
+	    {% csrf_token %}
+	    {{ remark_form.as_p }}
+	    <input type="submit" value="Submit" />
+	  </form>
+	</div>
+	<hr class="hr6"/>
+	<br/>
+      </li>
+      {% endfor %}
+    </ul>
+    </div>
+  </div>
+  {% endif %}
+
+  <hr class="hr12"/>
+
+</section>
+
+<section id="Motions">
+  <div class="flex-container">
+    <div class="flex-greybox" id="MotionBox">
+      <h2>Submit a new Motion</h2>
+      <button id="submitMotionButton">Put a new Motion forward</button>
+      <form id="submitMotionForm" action="{% url 'scipost:put_motion_forward' VGM_id=VGM.id %}" method="post">
+	{% csrf_token %}
+	{% load crispy_forms_tags %}
+	{% crispy motion_form %}
+      </form>
+    </div>
+  </div>
+
+  <div class="row">
+    <div class="flex-container">
+      <div class="flex-greybox">
+	<h2>Motions under consideration</h2>
+      </div>
+    </div>
+  </div>
+  {% for key, val in motion_categories_dict.items %}
+  <div class="row">
+    <div class="col-1"></div>
+    <div class="flex-container">
+      <div class="flex-greybox">
+	<h3>{{ val }}:</h3>
+      </div>
+    </div>
+    <div class="col-1"></div>
+    <div class="col-10">
+    <ul>
+      {% for motion in VGM.motion_set.all %}
+      {% if motion.category == key %}
+      <li>
+	{{ motion.as_li }}
+	<br/>
+	<div class="opinionsDisplay">
+	  <h4>Your opinion on this Motion (voting deadline: {{ motion.voting_deadline|date:'y-m-d' }}):</h4>
+	  <form action="{% url 'scipost:vote_on_motion' motion_id=motion.id vote='A' %}" method="post">
+            {% csrf_token %}
+            <input type="submit" class="agree" value="Agree {{ motion.nr_A }} "/>
+	  </form>
+	  <form action="{% url 'scipost:vote_on_motion' motion_id=motion.id vote='N' %}" method="post">
+            {% csrf_token %}
+            <input type="submit" class="notsure" value="Not sure {{ motion.nr_N }}"/>
+	  </form>
+	  <form action="{% url 'scipost:vote_on_motion' motion_id=motion.id vote='D'%}" method="post">
+            {% csrf_token %}
+            <input type="submit" class="disagree" value="Disagree {{ motion.nr_D }}"/>
+	  </form>
+	  {% if request.user.contributor in motion.in_agreement.all %}
+	  <strong>(you have voted: Agreed)</strong>
+	  {% elif request.user.contributor in motion.in_notsure.all %}
+	  <strong>(you have voted: Not sure)</strong>
+	  {% elif request.user.contributor in motion.in_disagreement.all %}
+	  <strong>(you have voted: Disagree)</strong>
+	  {% endif %}
+	</div>
+	<br/><br/>
+	{% if motion.remark_set.all %}
+	<h3>Remarks on this motion:</h3>
+	<ul>
+	  {% for rem in motion.remark_set.all %}
+	  {{ rem.as_li }}
+	  {% endfor %}
+	</ul>
+	{% endif %}
+	<button class="submitRemarkButton" id="remarkButton{{ motion.id }}">Add a remark on this Motion</button>
+	<div class="submitRemarkForm" id="remarkForm{{ motion.id }}">
+	  <form action="{% url 'scipost:add_remark_on_motion' motion_id=motion.id %}" method="post">
+	    {% csrf_token %}
+	    {{ remark_form.as_p }}
+	    <input type="submit" value="Submit" />
+	  </form>
+	</div>
+	<hr class="hr6"/>
+	<br/>
+      </li>
+      {% endif %}
+      {% endfor %}
+    </ul>
+    </div>
+  </div>
+  {% endfor %}
+
+</section>
+
+
+{% endblock bodysup %}
diff --git a/scipost/templates/scipost/VGMs.html b/scipost/templates/scipost/VGMs.html
new file mode 100644
index 000000000..493eefde5
--- /dev/null
+++ b/scipost/templates/scipost/VGMs.html
@@ -0,0 +1,26 @@
+{% extends 'scipost/base.html' %}
+
+{% block pagetitle %}: VGMs{% endblock pagetitle %}
+
+{% load staticfiles %}
+
+{% block bodysup %}
+
+
+<section>
+  <div class="flex-container">
+    <div class="flex-greybox">
+      <h1>SciPost Virtual General Meetings</h1>
+    </div>
+  </div>
+
+  <ul>
+    {% for VGM in VGM_list %}
+    <li><a href="{% url 'scipost:VGM_detail' VGM_id=VGM.id %}">{{ VGM }}</a></li>
+    {% endfor %}
+  </ul>
+
+</section>
+
+
+{% endblock bodysup %}
diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html
index d2645698f..fba5575f0 100644
--- a/scipost/templates/scipost/personal_page.html
+++ b/scipost/templates/scipost/personal_page.html
@@ -282,6 +282,12 @@
       <li><a href="{% url 'submissions:vet_submitted_reports' %}">Vet submitted Reports</a> ({{ nr_reports_to_vet }})</li>
       {% endif %}
     </ul>
+    {% if perms.scipost.can_attend_VGMs %}
+    <h3>Virtual General Meetings</h3>
+    <ul>
+      <li><a href="{% url 'scipost:VGMs' %}">List of VGMs</a></li>
+    </ul>
+    {% endif %}
   </div>
 
   {% if request.user|is_in_group:'Editorial Administrators' or request.user|is_in_group:'Editorial College' %}
diff --git a/scipost/urls.py b/scipost/urls.py
index facaec838..7e50c2ed9 100644
--- a/scipost/urls.py
+++ b/scipost/urls.py
@@ -172,6 +172,26 @@ urlpatterns = [
         views.Fellow_activity_overview,
         name='Fellow_activity_overview'),
 
+    ############################
+    # Virtual General Meetings #
+    ############################
+    url(r'^VGMs$', views.VGMs, name='VGMs'),
+    url(r'^VGM/(?P<VGM_id>[0-9]+)/$', views.VGM_detail, name='VGM_detail'),
+    url(r'^feedback/(?P<VGM_id>[0-9]+)$',
+        views.feedback, name='feedback'),
+    url(r'^nominate_Fellow/(?P<VGM_id>[0-9]+)$',
+        views.nominate_Fellow, name='nominate_Fellow'),
+    url(r'^add_remark_on_nomination/(?P<VGM_id>[0-9]+)/(?P<nomination_id>[0-9]+)$',
+        views.add_remark_on_nomination, name='add_remark_on_nomination'),
+    url(r'^vote_on_nomination/(?P<nomination_id>[0-9]+)/(?P<vote>[AND])$',
+        views.vote_on_nomination, name='vote_on_nomination'),
+    url(r'^put_motion_forward/(?P<VGM_id>[0-9]+)$',
+        views.put_motion_forward, name='put_motion_forward'),
+    url(r'^add_remark_on_motion/(?P<motion_id>[0-9]+)$',
+        views.add_remark_on_motion, name='add_remark_on_motion'),
+    url(r'^vote_on_motion/(?P<motion_id>[0-9]+)/(?P<vote>[AND])$',
+        views.vote_on_motion, name='vote_on_motion'),
+
     ################
     # Publications #
     ################
diff --git a/scipost/views.py b/scipost/views.py
index c3029a670..3e93b6948 100644
--- a/scipost/views.py
+++ b/scipost/views.py
@@ -1472,6 +1472,198 @@ def Fellow_activity_overview(request, Fellow_id=None):
     return render(request, 'scipost/Fellow_activity_overview.html', context)
 
 
+@permission_required('scipost.can_attend_VGMs', return_403=True)
+def VGMs(request):
+    VGM_list = VGM.objects.all().order_by('start_date')
+    context = {'VGM_list': VGM_list}
+    return render(request, 'scipost/VGMs.html', context)
+
+
+@permission_required('scipost.can_attend_VGMs', return_403=True)
+def VGM_detail(request, VGM_id):
+    VGM_instance = get_object_or_404(VGM, id=VGM_id)
+    VGM_information = Template(VGM_instance.information).render(Context({}))
+    feedback_received = Feedback.objects.filter(VGM=VGM_instance).order_by('date')
+    feedback_form = FeedbackForm()
+    nomination_form = NominationForm()
+    nominations = Nomination.objects.filter(accepted=None).order_by('last_name')
+    motion_form = MotionForm()
+    remark_form = RemarkForm()
+    context = {'VGM': VGM_instance,
+               'VGM_information': VGM_information,
+               'feedback_received': feedback_received,
+               'feedback_form': feedback_form,
+               'nominations': nominations,
+               'nomination_form': nomination_form,
+               'motion_categories_dict': motion_categories_dict,
+               'motion_form': motion_form,
+               'remark_form': remark_form,
+    }
+    return render(request, 'scipost/VGM_detail.html', context)
+
+
+@permission_required('scipost.can_attend_VGMs', return_403=True)
+def feedback(request, VGM_id=None):
+    if request.method == 'POST':
+        feedback_form = FeedbackForm(request.POST)
+        if feedback_form.is_valid():
+            feedback = Feedback(by=request.user.contributor,
+                                date=timezone.now().date(),
+                                feedback=feedback_form.cleaned_data['feedback'],)
+            if VGM_id:
+                VGM_instance = get_object_or_404(VGM, id=VGM_id)
+                feedback.VGM = VGM_instance
+            feedback.save()
+            ack_message = 'Your feedback has been received.'
+            context = {'ack_message': ack_message}
+            if VGM_id:
+                context['followup_message'] = 'Return to the '
+                context['followup_link'] = reverse('scipost:VGM_detail',
+                                                   kwargs={'VGM_id': VGM_id})
+                context['followup_link_label'] = 'VGM page'
+            return render(request, 'scipost/acknowledgement.html', context)
+        else:
+            errormessage = 'The form was not filled properly.'
+            return render(request, 'scipost/error.html', {'errormessage': errormessage})
+    else:
+        errormessage = 'This view can only be posted to.'
+        return render(request, 'scipost/error.html', {'errormessage': errormessage})
+
+
+@permission_required('scipost.can_attend_VGMs', return_403=True)
+def nominate_Fellow(request, VGM_id):
+    VGM_instance = get_object_or_404(VGM, id=VGM_id)
+    if request.method == 'POST':
+        nomination_form = NominationForm(request.POST)
+        if nomination_form.is_valid():
+            nomination = Nomination(
+                VGM=VGM_instance,
+                by = request.user.contributor,
+                date = timezone.now().date(),
+                first_name=nomination_form.cleaned_data['first_name'],
+                last_name=nomination_form.cleaned_data['last_name'],
+                discipline=nomination_form.cleaned_data['discipline'],
+                expertises=nomination_form.cleaned_data['expertises'],
+                voting_deadline=VGM_instance.end_date + datetime.timedelta(days=7),
+            )
+            nomination.save()
+            nomination.update_votes (request.user.contributor.id, 'A')
+            ack_message = 'The nomination has been registered.'
+            context = {'ack_message': ack_message,
+                       'followup_message': 'Return to the ',
+                       'followup_link': reverse('scipost:VGM_detail', kwargs={'VGM_id': VGM_id}),
+                       'followup_link_label': 'VGM page'}
+            return render(request, 'scipost/acknowledgement.html', context)
+        else:
+            errormessage = 'The form was not filled properly.'
+            return render(request, 'scipost/error.html', {'errormessage': errormessage})
+    else:
+        errormessage = 'This view can only be posted to.'
+        return render(request, 'scipost/error.html', {'errormessage': errormessage})
+
+
+@permission_required('scipost.can_attend_VGMs', raise_exception=True)
+def add_remark_on_nomination(request, VGM_id, nomination_id):
+    contributor = request.user.contributor
+    nomination = get_object_or_404(Nomination, pk=nomination_id)
+    if request.method == 'POST':
+        remark_form = RemarkForm(request.POST)
+        if remark_form.is_valid():
+            remark = Remark(contributor=request.user.contributor,
+                            nomination=nomination,
+                            date=timezone.now(),
+                            remark=remark_form.cleaned_data['remark'])
+            remark.save()
+            return HttpResponseRedirect('/VGM/' + str(VGM_id) +
+                                        '/#nomination_id' + str(nomination.id))
+        else:
+            errormessage = 'The form was invalidly filled.'
+            return render(request, 'scipost/error.html', {'errormessage': errormessage})
+    else:
+        errormessage = 'This view can only be posted to.'
+        return render(request, 'scipost/error.html', {'errormessage': errormessage})
+
+
+@permission_required('scipost.can_attend_VGMs', raise_exception=True)
+def vote_on_nomination(request, nomination_id, vote):
+    contributor = request.user.contributor
+    nomination = get_object_or_404(Nomination, pk=nomination_id)
+    if timezone.now() > nomination.voting_deadline:
+        errormessage = 'The voting deadline on this nomination has passed.'
+        return render(request, 'scipost/error.html', {'errormessage': errormessage})
+    nomination.update_votes (contributor.id, vote)
+    return HttpResponseRedirect('/VGM/' + str(nomination.VGM.id) +
+                                '/#nomination_id' + str(nomination.id))
+
+
+@permission_required('scipost.can_attend_VGMs', return_403=True)
+def put_motion_forward(request, VGM_id):
+    VGM_instance = get_object_or_404(VGM, id=VGM_id)
+    if timezone.now().date() > VGM_instance.end_date:
+        errormessage = 'This VGM has ended. No new motions can be put forward.'
+        return render(request, 'scipost/error.html', {'errormessage': errormessage})
+    if request.method == 'POST':
+        motion_form = MotionForm(request.POST)
+        if motion_form.is_valid():
+            motion = Motion(
+                category=motion_form.cleaned_data['category'],
+                VGM=VGM_instance,
+                background=motion_form.cleaned_data['background'],
+                motion=motion_form.cleaned_data['motion'],
+                put_forward_by=request.user.contributor,
+                date=timezone.now().date(),
+                voting_deadline=VGM_instance.end_date + datetime.timedelta(days=7),
+            )
+            motion.save()
+            motion.update_votes (request.user.contributor.id, 'A')
+            ack_message = 'Your motion has been registered.'
+            context = {'ack_message': ack_message,
+                       'followup_message': 'Return to the ',
+                       'followup_link': reverse('scipost:VGM_detail', kwargs={'VGM_id': VGM_id}),
+                       'followup_link_label': 'VGM page'}
+            return render(request, 'scipost/acknowledgement.html', context)
+        else:
+            errormessage = 'The form was not filled properly.'
+            return render(request, 'scipost/error.html', {'errormessage': errormessage})
+    else:
+        errormessage = 'This view can only be posted to.'
+        return render(request, 'scipost/error.html', {'errormessage': errormessage})
+
+
+@permission_required('scipost.can_attend_VGMs', raise_exception=True)
+def add_remark_on_motion(request, motion_id):
+    contributor = request.user.contributor
+    motion = get_object_or_404(Motion, pk=motion_id)
+    if request.method == 'POST':
+        remark_form = RemarkForm(request.POST)
+        if remark_form.is_valid():
+            remark = Remark(contributor=request.user.contributor,
+                            motion=motion,
+                            date=timezone.now(),
+                            remark=remark_form.cleaned_data['remark'])
+            remark.save()
+            return HttpResponseRedirect('/VGM/' + str(motion.VGM.id) +
+                                        '/#motion_id' + str(motion.id))
+        else:
+            errormessage = 'The form was invalidly filled.'
+            return render(request, 'scipost/error.html', {'errormessage': errormessage})
+    else:
+        errormessage = 'This view can only be posted to.'
+        return render(request, 'scipost/error.html', {'errormessage': errormessage})
+
+
+@permission_required('scipost.can_attend_VGMs', raise_exception=True)
+def vote_on_motion(request, motion_id, vote):
+    contributor = request.user.contributor
+    motion = get_object_or_404(Motion, pk=motion_id)
+    if timezone.now() > motion.voting_deadline:
+        errormessage = 'The voting deadline on this motion has passed.'
+        return render(request, 'scipost/error.html', {'errormessage': errormessage})
+    motion.update_votes (contributor.id, vote)
+    return HttpResponseRedirect('/VGM/' + str(motion.VGM.id) +
+                                '/#motion_id' + str(motion.id))
+
+
 #########
 # Lists #
 #########
-- 
GitLab