diff --git a/virtualmeetings/__init__.py b/virtualmeetings/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/virtualmeetings/admin.py b/virtualmeetings/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..c831a447b58b6d95edd086fda5750bbfc5e315c6 --- /dev/null +++ b/virtualmeetings/admin.py @@ -0,0 +1,31 @@ +from django.contrib import admin + +from .models import VGM, Feedback, Nomination, Motion + + +class VGMAdmin(admin.ModelAdmin): + search_fields = ['start_date'] + + +admin.site.register(VGM, VGMAdmin) + + +class FeedbackAdmin(admin.ModelAdmin): + search_fields = ['feedback', 'by'] + + +admin.site.register(Feedback, FeedbackAdmin) + + +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) diff --git a/virtualmeetings/apps.py b/virtualmeetings/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..9bbfd4ea04576759b428cbd1b1127a2767b82216 --- /dev/null +++ b/virtualmeetings/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class VirtualmeetingsConfig(AppConfig): + name = 'virtualmeetings' diff --git a/virtualmeetings/constants.py b/virtualmeetings/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..f75281f3af9bd47307eb81ce6542ae3b89b8ffc8 --- /dev/null +++ b/virtualmeetings/constants.py @@ -0,0 +1,9 @@ +MOTION_AMENDMENTS = 'ByLawAmend' +MOTION_WORKFLOW = 'Workflow' +MOTION_GENERAL = 'General' +MOTION_CATEGORIES = ( + (MOTION_AMENDMENTS, 'Amendments to by-laws'), + (MOTION_WORKFLOW, 'Editorial workflow improvements'), + (MOTION_GENERAL, 'General'), +) +motion_categories_dict = dict(MOTION_CATEGORIES) diff --git a/virtualmeetings/forms.py b/virtualmeetings/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..54fb4a68e81dfe0f7a1f54be8405a5ae5cfe60e4 --- /dev/null +++ b/virtualmeetings/forms.py @@ -0,0 +1,61 @@ +from django import forms + +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Div, Field, HTML, Submit + +from .models import Feedback, Nomination, Motion + +from scipost.constants import SCIPOST_SUBJECT_AREAS + + +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', 'webpage'] + + 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/virtualmeetings/migrations/0001_initial.py b/virtualmeetings/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..cacb195873ad261c4348c4945685cf29326a61c5 --- /dev/null +++ b/virtualmeetings/migrations/0001_initial.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-03-01 21:15 +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): + + initial = True + + dependencies = [ + ('scipost', '0039_auto_20170301_2215'), + ] + + state_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()), + ], + options={ + 'db_table': 'scipost_Feedback', + }, + ), + migrations.CreateModel( + name='Motion', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('category', models.CharField(choices=[('ByLawAmend', 'Amendments to by-laws'), ('Workflow', 'Editorial workflow improvements'), ('General', 'General')], default='General', max_length=10)), + ('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()), + ], + options={ + 'db_table': 'scipost_Motion', + }, + ), + 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)), + ('webpage', models.URLField(default='')), + ('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()), + ], + options={ + 'db_table': 'scipost_Nomination', + }, + ), + 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()), + ('information', models.TextField(default='')), + ], + options={ + 'db_table': 'scipost_VGM', + }, + ), + migrations.AddField( + model_name='nomination', + name='VGM', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='virtualmeetings.VGM'), + ), + migrations.AddField( + model_name='nomination', + name='by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor'), + ), + migrations.AddField( + model_name='nomination', + name='in_agreement', + field=models.ManyToManyField(blank=True, related_name='in_agreement_with_nomination', to='scipost.Contributor'), + ), + migrations.AddField( + model_name='nomination', + name='in_disagreement', + field=models.ManyToManyField(blank=True, related_name='in_disagreement_with_nomination', to='scipost.Contributor'), + ), + migrations.AddField( + model_name='nomination', + name='in_notsure', + field=models.ManyToManyField(blank=True, related_name='in_notsure_with_nomination', to='scipost.Contributor'), + ), + migrations.AddField( + model_name='motion', + name='VGM', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='virtualmeetings.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'), + ), + migrations.AddField( + model_name='feedback', + name='VGM', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='virtualmeetings.VGM'), + ), + migrations.AddField( + model_name='feedback', + name='by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor'), + ), + ] + + operations = [ + migrations.SeparateDatabaseAndState(state_operations=state_operations) + ] diff --git a/virtualmeetings/migrations/__init__.py b/virtualmeetings/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/virtualmeetings/models.py b/virtualmeetings/models.py new file mode 100644 index 0000000000000000000000000000000000000000..6346e7cb60b91f399e680747a2009c3334b6c258 --- /dev/null +++ b/virtualmeetings/models.py @@ -0,0 +1,223 @@ +from django.db import models +from django.shortcuts import get_object_or_404 +from django.template import Context, Template +from django.utils import timezone + +from .constants import MOTION_CATEGORIES + +from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\ + subject_areas_dict, disciplines_dict +from scipost.models import Contributor, ChoiceArrayField + + +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='') + + class Meta: + db_table = 'scipost_VGM' + + 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() + + class Meta: + db_table = 'scipost_Feedback' + + 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) + webpage = models.URLField(default='') + 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() + + class Meta: + db_table = 'scipost_Nomination' + + 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 }}" ' + 'style="background-color: #eeeeee;">' + '<div class="row">' + '<div class="col-4">' + '<h3><em> {{ name }}</em></h3>' + '<p>Nominated by {{ proposer }}</p>' + '</div>' + '<div class="col-4">' + '<p><a href="{{ webpage }}">Webpage</a></p>' + '<p>Discipline: {{ discipline }}</p></div>' + '<div class="col-4"><p>expertise:<ul>') + for exp in self.expertises: + html += '<li>%s</li>' % subject_areas_dict[exp] + html += '</ul></div></div></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, + 'discipline': disciplines_dict[self.discipline], + 'webpage': self.webpage, + }) + 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() + + +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() + + class Meta: + db_table = 'scipost_Motion' + + 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() diff --git a/virtualmeetings/templates/virtualmeetings/VGM_detail.html b/virtualmeetings/templates/virtualmeetings/VGM_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..0420363c16e093409a430ceee2b8d259bc6f9dd2 --- /dev/null +++ b/virtualmeetings/templates/virtualmeetings/VGM_detail.html @@ -0,0 +1,352 @@ +{% 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(); + }); + + $("#FellowshipListing").hide(); + $("#FellowshipListingButton").click( function() { + $("#FellowshipListing").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> + <div class="flex-container"> + <div class="flex-whitebox"> + <h2>On this page:</h2> + <ul> + <li><a href="#Information">Information message</a></li> + <li><a href="#Feedback">Feedback</a></li> + <li><a href="#Nominations">Nominations</a></li> + <li><a href="#Motions">Motions</a></li> + </ul> + </div> + </div> + <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> + <br/> + <div class="flex-whitebox"> + <h3>Quick bullet points:</h3> + <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> + </div> + <br/> + <hr class="hr12"/> +</section> + +<section id="Feedback"> + <div class="flex-container"> + <div class="flex-greybox" id="FeedbackBox"> + <h2>Feedback on SciPost</h2> + <button id="submitFeedbackButton">Provide feedback</button> + <form id="submitFeedbackForm" action="{% url 'virtualmeetings:feedback' VGM_id=VGM.id %}" method="post"> + {% csrf_token %} + {{ feedback_form.as_p }} + <input type="submit" value="Submit"/> + </form> + </div> + </div> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>General Feedback provided</h2> + </div> + </div> + <div class="row"> + <div class="col-1"></div> + <div class="col-10"> + <ul> + {% for feedback in feedback_received %} + <li>{{ feedback.as_li }}</li> + <button class="submitRemarkButton" id="remarkButton{{ nomination.id }}">Add a remark on this Feedback</button> + <div class="submitRemarkForm" id="remarkForm{{ feedback.id }}"> + <form action="{% url 'virtualmeetings:add_remark_on_feedback' VGM_id=VGM.id feedback_id=feedback.id %}" method="post"> + {% csrf_token %} + {{ remark_form.as_p }} + <input type="submit" value="Submit" /> + </form> + </div> + {% if feedback.remark_set.all %} + <h3>Remarks on this feedback:</h3> + <ul> + {% for rem in feedback.remark_set.all %} + {{ rem.as_li }} + {% endfor %} + </ul> + {% endif %} + {% endfor %} + </ul> + </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 'virtualmeetings:nominate_Fellow' VGM_id=VGM.id %}" method="post"> + {% csrf_token %} + {{ nomination_form.as_p }} + <input type="submit" value="Submit"/> + </form> + </div> + </div> + <button id="FellowshipListingButton">View/hide Fellows and Invitations listings</button> + <div class="row" id="FellowshipListing"> + <div class="col-6"> + <div class="flex-container"> + <div class="flex-greybox"> + <h3>Current Fellows</h3> + </div> + </div> + <div class="flex-container"> + <div class="flex-whitebox"> + <table class="tableofInviteesResponded"> + {% for Fellow in current_Fellows %} + <tr><td>{{ Fellow }}</td><td>{{ Fellow.discipline_as_string }}</td> + <td>{{ Fellow.expertises_as_string }}</td></tr> + {% endfor %} + </table> + </div> + </div> + </div> + <div class="col-6"> + <div class="flex-container"> + <div class="flex-greybox"> + <h3>Invitations currently outstanding</h3> + </div> + </div> + <div class="flex-container"> + <div class="flex-whitebox"> + <table class="tableofInvitees"> + {% for invitee in pending_inv_Fellows %} + <tr><td>{{ invitee.first_name }} {{ invitee.last_name }}</td></tr> + {% endfor %} + </table> + </div> + </div> + <div class="flex-container"> + <div class="flex-greybox"> + <h3>Invitations which have been turned down</h3> + </div> + </div> + <div class="flex-container"> + <div class="flex-whitebox"> + <table class="tableofInviteesDeclined"> + {% for invitee in declined_inv_Fellows %} + <tr><td>{{ invitee.first_name }} {{ invitee.last_name }}</td></tr> + {% endfor %} + </table> + </div> + </div> + </div> + </div> + + {% if nominations %} + <div class="row"> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Nominations under consideration</h2> + </div> + </div> + </div> + <div class="row"> + <div class="col-1"></div> + <div class="col-10"> + <ul style="list-style-type: none;"> + {% 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 'virtualmeetings: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 'virtualmeetings: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 'virtualmeetings: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/> + <button class="submitRemarkButton" id="remarkButton{{ nomination.id }}">Add a remark on this Nomination</button> + <div class="submitRemarkForm" id="remarkForm{{ nomination.id }}"> + <form action="{% url 'virtualmeetings: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> + {% 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 %} + <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 'virtualmeetings: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 'virtualmeetings: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 'virtualmeetings: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 'virtualmeetings: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/> + <button class="submitRemarkButton" id="remarkButton{{ motion.id }}">Add a remark on this Motion</button> + <div class="submitRemarkForm" id="remarkForm{{ motion.id }}"> + <form action="{% url 'virtualmeetings:add_remark_on_motion' motion_id=motion.id %}" method="post"> + {% csrf_token %} + {{ remark_form.as_p }} + <input type="submit" value="Submit" /> + </form> + </div> + {% 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 %} + <hr class="hr6"/> + <br/> + </li> + {% endif %} + {% endfor %} + </ul> + </div> + </div> + {% endfor %} + +</section> + + +{% endblock bodysup %} diff --git a/virtualmeetings/templates/virtualmeetings/VGMs.html b/virtualmeetings/templates/virtualmeetings/VGMs.html new file mode 100644 index 0000000000000000000000000000000000000000..a482a21dfadd11a7121e88458dd9bdb2df7ef9a0 --- /dev/null +++ b/virtualmeetings/templates/virtualmeetings/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 'virtualmeetings:VGM_detail' VGM_id=VGM.id %}">{{ VGM }}</a></li> + {% endfor %} + </ul> + +</section> + + +{% endblock bodysup %} diff --git a/virtualmeetings/tests.py b/virtualmeetings/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/virtualmeetings/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/virtualmeetings/urls.py b/virtualmeetings/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..556a7eb2b1e4dca34f7330c19ab6779aaac8a080 --- /dev/null +++ b/virtualmeetings/urls.py @@ -0,0 +1,24 @@ + +from django.conf.urls import include, url +from django.views.generic import TemplateView + +from . import views + +urlpatterns = [ + url(r'^$', 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'^add_remark_on_feedback/(?P<VGM_id>[0-9]+)/(?P<feedback_id>[0-9]+)$', + views.add_remark_on_feedback, name='add_remark_on_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'), +] diff --git a/virtualmeetings/views.py b/virtualmeetings/views.py new file mode 100644 index 0000000000000000000000000000000000000000..ee357ca3f74aac592483d38e4500a75d1c855bba --- /dev/null +++ b/virtualmeetings/views.py @@ -0,0 +1,252 @@ +import datetime + +from django.contrib.auth.decorators import login_required, permission_required +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404, render +from django.template import Context, Template +from django.utils import timezone + +from .constants import motion_categories_dict +from .forms import FeedbackForm, NominationForm, MotionForm +from .models import VGM, Feedback, Nomination, Motion + +from scipost.forms import RegistrationInvitation, RemarkForm +from scipost.models import Contributor, Remark + + +@login_required +@permission_required('scipost.can_attend_VGMs') +def VGMs(request): + VGM_list = VGM.objects.all().order_by('start_date') + context = {'VGM_list': VGM_list} + return render(request, 'virtualmeetings/VGMs.html', context) + + +@login_required +@permission_required('scipost.can_attend_VGMs') +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() + current_Fellows = Contributor.objects.filter( + user__groups__name='Editorial College').order_by('user__last_name') + sent_inv_Fellows = RegistrationInvitation.objects.filter( + invitation_type='F', responded=False) + pending_inv_Fellows = sent_inv_Fellows.filter(declined=False).order_by('last_name') + declined_inv_Fellows = sent_inv_Fellows.filter(declined=True).order_by('last_name') + 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, + 'current_Fellows': current_Fellows, + 'pending_inv_Fellows': pending_inv_Fellows, + 'declined_inv_Fellows': declined_inv_Fellows, + 'nominations': nominations, + 'nomination_form': nomination_form, + 'motion_categories_dict': motion_categories_dict, + 'motion_form': motion_form, + 'remark_form': remark_form, + } + return render(request, 'virtualmeetings/VGM_detail.html', context) + + +@login_required +@permission_required('scipost.can_attend_VGMs') +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('virtualmeetings: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}) + + +@login_required +@permission_required('scipost.can_attend_VGMs', raise_exception=True) +def add_remark_on_feedback(request, VGM_id, feedback_id): + # contributor = request.user.contributor + feedback = get_object_or_404(Feedback, pk=feedback_id) + if request.method == 'POST': + remark_form = RemarkForm(request.POST) + if remark_form.is_valid(): + remark = Remark(contributor=request.user.contributor, + feedback=feedback, + date=timezone.now(), + remark=remark_form.cleaned_data['remark']) + remark.save() + return HttpResponseRedirect('/VGM/' + str(VGM_id) + + '/#feedback_id' + str(feedback.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}) + + +@login_required +@permission_required('scipost.can_attend_VGMs') +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'], + webpage=nomination_form.cleaned_data['webpage'], + 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('virtualmeetings: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}) + + +@login_required +@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}) + + +@login_required +@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)) + + +@login_required +@permission_required('scipost.can_attend_VGMs') +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('virtualmeetings: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}) + + +@login_required +@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}) + + +@login_required +@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))