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