diff --git a/colleges/admin.py b/colleges/admin.py index 8ff31a1391e86982716866e6e4d0ea2ead5d25d2..31c6ef976aabc76bc1b5fac42e96a2e394fe52cc 100644 --- a/colleges/admin.py +++ b/colleges/admin.py @@ -4,7 +4,7 @@ __license__ = "AGPL v3" from django.contrib import admin -from .models import Fellowship +from .models import Fellowship, ProspectiveFellow, ProspectiveFellowEvent def fellowhip_is_active(fellowship): @@ -20,3 +20,13 @@ class FellowshipAdmin(admin.ModelAdmin): admin.site.register(Fellowship, FellowshipAdmin) + + +class ProspectiveFellowEventInline(admin.TabularInline): + model = ProspectiveFellowEvent + +class ProspectiveFellowAdmin(admin.ModelAdmin): + inlines = (ProspectiveFellowEventInline,) + search_fields = ['last_name', 'email'] + +admin.site.register(ProspectiveFellow, ProspectiveFellowAdmin) diff --git a/colleges/constants.py b/colleges/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..937d89623290e5e2fdc490d0d25a767a4e3bd6c2 --- /dev/null +++ b/colleges/constants.py @@ -0,0 +1,49 @@ +__copyright__ = "Copyright 2016-2018, Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +PROSPECTIVE_FELLOW_IDENTIFIED = 'identified' +PROSPECTIVE_FELLOW_INVITED = 'invited' +PROSPECTIVE_FELLOW_REINVITED = 'reinvited' +PROSPECTIVE_FELLOW_MULTIPLY_REINVITED = 'multiplyreinvited' +PROSPECTIVE_FELLOW_DECLINED = 'declined' +PROSPECTIVE_FELLOW_UNRESPONSIVE = 'unresponsive' +PROSPECTIVE_FELLOW_RETIRED = 'retired' +PROSPECTIVE_FELLOW_DECEASED = 'deceased' +PROSPECTIVE_FELLOW_INTERESTED = 'interested' +PROSPECTIVE_FELLOW_REGISTERED = 'registered' +PROSPECTIVE_FELLOW_ACTIVE_IN_COLLEGE = 'activeincollege' +PROSPECTIVE_FELLOW_SCIPOST_EMERITUS = 'emeritus' + +PROSPECTIVE_FELLOW_STATUSES = ( + (PROSPECTIVE_FELLOW_IDENTIFIED, 'Identified as potential Fellow'), + (PROSPECTIVE_FELLOW_INVITED, 'Invited to become Fellow'), + (PROSPECTIVE_FELLOW_REINVITED, 'Reinvited after initial invitation'), + (PROSPECTIVE_FELLOW_MULTIPLY_REINVITED, 'Multiply reinvited'), + (PROSPECTIVE_FELLOW_DECLINED, 'Declined the invitation'), + (PROSPECTIVE_FELLOW_UNRESPONSIVE, 'Marked as unresponsive'), + (PROSPECTIVE_FELLOW_RETIRED, 'Retired'), + (PROSPECTIVE_FELLOW_DECEASED, 'Deceased'), + (PROSPECTIVE_FELLOW_INTERESTED, 'Marked as interested, Fellowship being set up'), + (PROSPECTIVE_FELLOW_REGISTERED, 'Registered as Contributor'), + (PROSPECTIVE_FELLOW_ACTIVE_IN_COLLEGE, 'Currently active in a College'), + (PROSPECTIVE_FELLOW_SCIPOST_EMERITUS, 'SciPost Emeritus'), +) +prospective_Fellow_statuses_dict = dict(PROSPECTIVE_FELLOW_STATUSES) + + +PROSPECTIVE_FELLOW_EVENT_DEFINED = 'defined' +PROSPECTIVE_FELLOW_EVENT_EMAILED = 'emailed' +PROSPECTIVE_FELLOW_EVENT_RESPONDED = 'responded' +PROSPECTIVE_FELLOW_EVENT_STATUSUPDATED = 'statusupdated' +PROSPECTIVE_FELLOW_EVENT_COMMENT = 'comment' +PROSPECTIVE_FELLOW_EVENT_DEACTIVATION = 'deactivation' + +PROSPECTIVE_FELLOW_EVENTS = ( + (PROSPECTIVE_FELLOW_EVENT_DEFINED, 'Defined in database'), + (PROSPECTIVE_FELLOW_EVENT_EMAILED, 'Emailed with invitation'), + (PROSPECTIVE_FELLOW_EVENT_RESPONDED, 'Response received'), + (PROSPECTIVE_FELLOW_EVENT_STATUSUPDATED, 'Status updated'), + (PROSPECTIVE_FELLOW_EVENT_COMMENT, 'Comment'), + (PROSPECTIVE_FELLOW_EVENT_DEACTIVATION, 'Deactivation: not considered anymore'), +) diff --git a/colleges/forms.py b/colleges/forms.py index abeef20baa7e6810e26b4ae428757a7d20bf385e..0ebff9c83b576c75a47606b74c80db7e8df8fee8 100644 --- a/colleges/forms.py +++ b/colleges/forms.py @@ -10,7 +10,7 @@ from proceedings.models import Proceedings from submissions.models import Submission from scipost.models import Contributor -from .models import Fellowship +from .models import Fellowship, ProspectiveFellow, ProspectiveFellowEvent class AddFellowshipForm(forms.ModelForm): @@ -217,3 +217,25 @@ class FellowshipAddProceedingsForm(forms.ModelForm): fellowship = self.instance proceedings.fellowships.add(fellowship) return fellowship + + +class ProspectiveFellowForm(forms.ModelForm): + + class Meta: + model = ProspectiveFellow + fields = ['title', 'first_name', 'last_name', 'email', + 'discipline', 'expertises', 'webpage', 'status', 'contributor'] + + +class ProspectiveFellowStatusForm(forms.ModelForm): + + class Meta: + model = ProspectiveFellow + fields = ['status'] + + +class ProspectiveFellowEventForm(forms.ModelForm): + + class Meta: + model = ProspectiveFellowEvent + fields = ['event', 'comments'] diff --git a/colleges/migrations/0003_prospectivefellow_prospectivefellowevent.py b/colleges/migrations/0003_prospectivefellow_prospectivefellowevent.py new file mode 100644 index 0000000000000000000000000000000000000000..aeff42134e660ba5c457c44fe843a885af7f4f07 --- /dev/null +++ b/colleges/migrations/0003_prospectivefellow_prospectivefellowevent.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-06-27 17:14 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import scipost.fields +import scipost.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0014_auto_20180414_2218'), + ('colleges', '0002_auto_20171229_1435'), + ] + + operations = [ + migrations.CreateModel( + name='ProspectiveFellow', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs'), ('MS', 'Ms')], max_length=4)), + ('first_name', models.CharField(max_length=30)), + ('last_name', models.CharField(max_length=150)), + ('email', models.EmailField(max_length=254)), + ('discipline', models.CharField(choices=[('physics', 'Physics'), ('astrophysics', 'Astrophysics'), ('mathematics', 'Mathematics'), ('computerscience', 'Computer Science')], default='physics', max_length=20, verbose_name='Main discipline')), + ('expertises', scipost.fields.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(blank=True)), + ('status', models.CharField(choices=[('identified', 'Identified as potential Fellow'), ('invited', 'Invited to become Fellow'), ('reinvited', 'Reinvited after initial invitation'), ('multiplyreinvited', 'Multiply reinvited'), ('declined', 'Declined the invitation'), ('unresponsive', 'Marked as unresponsive'), ('retired', 'Retired'), ('deceased', 'Deceased'), ('interested', 'Marked as interested, Fellowship being set up'), ('registered', 'Registered as Contributor'), ('activeincollege', 'Currently active in a College'), ('emeritus', 'SciPost Emeritus')], default='identified', max_length=32)), + ('contributor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='scipost.Contributor')), + ], + options={ + 'ordering': ['-last_name'], + }, + ), + migrations.CreateModel( + name='ProspectiveFellowEvent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event', models.CharField(choices=[('defined', 'Defined in database'), ('emailed', 'Emailed with invitation'), ('responded', 'Response received'), ('comment', 'Comment'), ('deactivation', 'Deactivation: not considered anymore')], max_length=32)), + ('comments', models.TextField(blank=True)), + ('noted_on', models.DateTimeField(auto_now_add=True)), + ('noted_by', models.ForeignKey(blank=True, null=True, on_delete=models.SET(scipost.models.get_sentinel_user), to='scipost.Contributor')), + ('prosfellow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colleges.ProspectiveFellow')), + ], + ), + ] diff --git a/colleges/migrations/0004_auto_20180629_0825.py b/colleges/migrations/0004_auto_20180629_0825.py new file mode 100644 index 0000000000000000000000000000000000000000..20ed86ad9bcf2cb55c414dc72dc87ab97fc75950 --- /dev/null +++ b/colleges/migrations/0004_auto_20180629_0825.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-06-29 06:25 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('colleges', '0003_prospectivefellow_prospectivefellowevent'), + ] + + operations = [ + migrations.AlterModelOptions( + name='prospectivefellow', + options={'ordering': ['last_name']}, + ), + ] diff --git a/colleges/migrations/0005_auto_20180701_2110.py b/colleges/migrations/0005_auto_20180701_2110.py new file mode 100644 index 0000000000000000000000000000000000000000..f111f7e205c0632a19f02b8cb351a8188140dfd5 --- /dev/null +++ b/colleges/migrations/0005_auto_20180701_2110.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-07-01 19:10 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('colleges', '0004_auto_20180629_0825'), + ] + + operations = [ + migrations.AlterField( + model_name='prospectivefellowevent', + name='noted_on', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/colleges/migrations/0006_auto_20180703_1208.py b/colleges/migrations/0006_auto_20180703_1208.py new file mode 100644 index 0000000000000000000000000000000000000000..33e5fefdf019827a4694033fa542aa717e8f4774 --- /dev/null +++ b/colleges/migrations/0006_auto_20180703_1208.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-07-03 10:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('colleges', '0005_auto_20180701_2110'), + ] + + operations = [ + migrations.AlterField( + model_name='prospectivefellowevent', + name='event', + field=models.CharField(choices=[('defined', 'Defined in database'), ('emailed', 'Emailed with invitation'), ('responded', 'Response received'), ('statusupdated', 'Status updated'), ('comment', 'Comment'), ('deactivation', 'Deactivation: not considered anymore')], max_length=32), + ), + ] diff --git a/colleges/models.py b/colleges/models.py index 312021771eb5501790b86f011e264e206628d2c9..095400aa72a8ce2cd4e415a89a75816a3e3a97c1 100644 --- a/colleges/models.py +++ b/colleges/models.py @@ -6,11 +6,18 @@ import datetime from django.db import models from django.urls import reverse +from django.utils import timezone -from scipost.behaviors import TimeStampedModel - +from .constants import PROSPECTIVE_FELLOW_STATUSES, PROSPECTIVE_FELLOW_IDENTIFIED,\ + PROSPECTIVE_FELLOW_EVENTS from .managers import FellowQuerySet +from scipost.behaviors import TimeStampedModel +from scipost.constants import SCIPOST_DISCIPLINES, DISCIPLINE_PHYSICS,\ + SCIPOST_SUBJECT_AREAS, TITLE_CHOICES +from scipost.fields import ChoiceArrayField +from scipost.models import get_sentinel_user, Contributor + class Fellowship(TimeStampedModel): """A Fellowship gives access to the Submission Pool to Contributors. @@ -58,3 +65,46 @@ class Fellowship(TimeStampedModel): elif not self.until_date: return today >= self.start_date return today >= self.start_date and today <= self.until_date + + + +class ProspectiveFellow(models.Model): + """ + A ProspectiveFellow is somebody who has been identified as + a potential member of an Editorial College. + """ + title = models.CharField(max_length=4, choices=TITLE_CHOICES) + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=150) + email = models.EmailField() + discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, + default=DISCIPLINE_PHYSICS, verbose_name='Main discipline') + expertises = ChoiceArrayField( + models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS), + blank=True, null=True) + webpage = models.URLField(blank=True) + status = models.CharField(max_length=32, choices=PROSPECTIVE_FELLOW_STATUSES, + default=PROSPECTIVE_FELLOW_IDENTIFIED) + contributor = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, + null=True, blank=True, related_name='+') + + class Meta: + ordering = ['last_name'] + + def __str__(self): + return '%s, %s %s (%s)' % (self.last_name, self.get_title_display(), self.first_name, + self.get_status_display()) + + +class ProspectiveFellowEvent(models.Model): + prosfellow = models.ForeignKey('colleges.ProspectiveFellow', on_delete=models.CASCADE) + event = models.CharField(max_length=32, choices=PROSPECTIVE_FELLOW_EVENTS) + comments = models.TextField(blank=True) + noted_on = models.DateTimeField(default=timezone.now) + noted_by = models.ForeignKey('scipost.Contributor', + on_delete=models.SET(get_sentinel_user), + blank=True, null=True) + + def __str__(self): + return '%s, %s %s: %s' % (self.prosfellow.last_name, self.prosfellow.get_title_display(), + self.prosfellow.first_name, self.get_event_display()) diff --git a/colleges/templates/colleges/_prospectivefellow_card.html b/colleges/templates/colleges/_prospectivefellow_card.html new file mode 100644 index 0000000000000000000000000000000000000000..48edb8808caf78bf103b322a52aba5096739b7e7 --- /dev/null +++ b/colleges/templates/colleges/_prospectivefellow_card.html @@ -0,0 +1,55 @@ +{% load bootstrap %} + +<div class="card-body"> + <div class="row"> + <div class="col-6"> + <p> + {{ prosfel.last_name }}, {{ prosfel.get_title_display }} {{ prosfel.first_name }} + <br/> + {{ prosfel.email }} + <br/> + {% if prosfel.webpage %}<a href="{{ prosfel.webpage }}">webpage</a> + {% else %}No personal webpage given + {% endif %} + <br/> + {% if prosfel.contributor %}Associated to Contributor <a href="{{ prosfel.contributor.get_absolute_url }}">{{ prosfel.contributor }}</a></li> + {% else %}No associated Contributor + {% endif %} + </p> + </div> + <div class="col-6"> + <ul> + <li><a href="{% url 'colleges:prospective_Fellow_update' pk=prosfel.id %}">Update</a> the data</li> + <li><a href="{% url 'colleges:prospective_Fellow_delete' pk=prosfel.id %}">Delete</a> this Prosepective Fellow</li> + <li><a href="{% url 'colleges:prospective_Fellow_email_initial' pk=prosfel.id %}">Prepare and send initial email</a></li> + </ul> + </div> + </div> + + <div class="row"> + <div class="col-md-6 ml-auto"> + <h3>Events</h3> + <ul> + {% for event in prosfel.prospectivefellowevent_set.all %} + {% include 'colleges/_prospectivefellow_event_li.html' with event=event %} + {% empty %} + <li>No events found.</li> + {% endfor %} + </ul> + </div> + <div class="col-md-5"> + <h3>Update the status of this Prospective Fellow</h3> + <form class="d-block mt-2 mb-3" action="{% url 'colleges:prospective_Fellow_update_status' pk=prosfel.id %}" method="post"> + {% csrf_token %} + {{ pfstatus_form|bootstrap }} + <input type="submit" name="submit" value="Update status" class="btn btn-outline-secondary"> + </form> + <hr/> + <h3>Add an event for this Prospective Fellow</h3> + <form class="d-block mt-2 mb-3" action="{% url 'colleges:prospective_Fellow_event_create' pk=prosfel.id %}" method="post"> + {% csrf_token %} + {{ pfevent_form|bootstrap }} + <input type="submit" name="submit" value="Submit" class="btn btn-outline-secondary"> + </form> + +</div> diff --git a/colleges/templates/colleges/_prospectivefellow_event_li.html b/colleges/templates/colleges/_prospectivefellow_event_li.html new file mode 100644 index 0000000000000000000000000000000000000000..f3f28e66a70f6ae47a254a9d09173e3463f89835 --- /dev/null +++ b/colleges/templates/colleges/_prospectivefellow_event_li.html @@ -0,0 +1,6 @@ +<li id="{{ event.id }}"> + <div class="font-weight-bold">{{ event.get_event_display }} <small class="text-muted">noted {{ event.noted_on }} {% if event.noted_by %}by {{ event.noted_by }}{% endif %}</small></div> + {% if event.comments %} + <div>{{ event.comments|linebreaks }}</div> + {% endif %} +</li> diff --git a/colleges/templates/colleges/prospectivefellow_confirm_delete.html b/colleges/templates/colleges/prospectivefellow_confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..cd80fe388a78bb185d1296703742b107c01231f3 --- /dev/null +++ b/colleges/templates/colleges/prospectivefellow_confirm_delete.html @@ -0,0 +1,25 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Delete Prospective Fellow{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Delete Prospective Fellow</h1> + {{ object }} + </div> +</div> +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + <h3 class="mb-2">Are you sure you want to delete this Prospective Fellow?</h3> + <input type="submit" class="btn btn-danger" value="Yes, delete it" /> + </form> + </ul> + </div> +</div> + +{% endblock content %} diff --git a/colleges/templates/colleges/prospectivefellow_form.html b/colleges/templates/colleges/prospectivefellow_form.html new file mode 100644 index 0000000000000000000000000000000000000000..31e3ee3472ad4fa4bf3165bd2a5a016e28d6104a --- /dev/null +++ b/colleges/templates/colleges/prospectivefellow_form.html @@ -0,0 +1,16 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Prospective Fellows{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + <form action="{% url 'colleges:prospective_Fellow_create' %}" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" value="Submit" class="btn btn-primary"> + </div> +</div> +{% endblock content %} diff --git a/colleges/templates/colleges/prospectivefellow_list.html b/colleges/templates/colleges/prospectivefellow_list.html new file mode 100644 index 0000000000000000000000000000000000000000..e4b864c10aa20a8984ced1946c564fff1829df77 --- /dev/null +++ b/colleges/templates/colleges/prospectivefellow_list.html @@ -0,0 +1,68 @@ +{% extends 'scipost/base.html' %} + +{% load scipost_extras %} + +{% load bootstrap %} + +{% block pagetitle %}: Prospective Fellows{% endblock pagetitle %} + +{% block content %} +<div class="row"> + <div class="col-12"> + <p> + <a href="{% url 'colleges:prospective_Fellows' %}">View all</a> or view by discipline/subject area: + {% for discipline in subject_areas %} + <a class="btn btn-primary" data-toggle="collapse" href="#collapse{{ discipline.0|cut:" " }}" role="button" aria-expanded="false" aria-controls="collapse{{ discipline.0|cut:" " }}">{{ discipline.0 }}</a> + {% endfor %} + </p> + {% for discipline in subject_areas %} + <div class="collapse" id="collapse{{ discipline.0|cut:" " }}"> + <p> + {% for area in discipline.1 %} + <a href="{% url 'colleges:prospective_Fellows' %}?discipline={{ discipline.0|cut:" " }}&expertise={{ area.0 }}" method="get">{{ area.0 }}</a> + {% endfor %} + </p> + </div> + {% endfor %} + </div> +</div> +<div class="row"> + <div class="col-12"> + <a href="{% url 'colleges:prospective_Fellow_create' %}">Add a Prospective Fellow</a> + <br/><br/> + <table class="table table-hover mb-5"> + <thead class="thead-default"> + <tr> + <th>Name</th> + <th>Discipline</th> + <th>Expertises</th> + <th>Status</th> + </tr> + </thead> + <tbody id="accordion" role="tablist" aria-multiselectable="true"> + {% for prosfel in object_list %} + <tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ prosfel.id }}" aria-expanded="false" aria-controls="collapse{{ prosfel.id }}" style="cursor: pointer;"> + <td>{{ prosfel.last_name }}, {{ prosfel.get_title_display }} {{ prosfel.first_name }}</td> + <td>{{ prosfel.get_discipline_display }}</td> + <td> + {% for expertise in prosfel.expertises %} + <div class="single d-inline" data-specialization="{{expertise|lower}}" data-toggle="tooltip" data-placement="bottom" title="{{expertise|get_specialization_display}}">{{expertise|get_specialization_code}}</div> + {% endfor %} + </td> + <td>{{ prosfel.get_status_display }}</td> + </tr> + <tr id="collapse{{ prosfel.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ prosfel.id }}" style="background-color: #fff;"> + <td colspan="4"> + {% include 'colleges/_prospectivefellow_card.html' with prosfel=prosfel pfevent_form=pfevent_form %} + </td> + </tr> + {% empty %} + <tr> + <td colspan="4">No Prospective Fellows found</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> +</div> +{% endblock content %} diff --git a/colleges/urls.py b/colleges/urls.py index fa557c7f82cb8ace50133a54aef45565e2cbdb0b..fbb098dc1a0b0cf678246e1c0f83ea834572e257 100644 --- a/colleges/urls.py +++ b/colleges/urls.py @@ -42,4 +42,41 @@ urlpatterns = [ views.fellowship_add_proceedings, name='fellowship_add_proceedings'), url(r'^fellowships/(?P<id>[0-9]+)/proceedings/(?P<proceedings_id>[0-9]+)/remove$', views.fellowship_remove_proceedings, name='fellowship_remove_proceedings'), + + # Prospective Fellows + url( + r'^prospectivefellows/$', + views.ProspectiveFellowListView.as_view(), + name='prospective_Fellows' + ), + url( + r'^prospectivefellows/add/$', + views.ProspectiveFellowCreateView.as_view(), + name='prospective_Fellow_create' + ), + url( + r'^prospectivefellows/(?P<pk>[0-9]+)/update/$', + views.ProspectiveFellowUpdateView.as_view(), + name='prospective_Fellow_update' + ), + url( + r'^prospectivefellows/(?P<pk>[0-9]+)/update_status/$', + views.ProspectiveFellowUpdateStatusView.as_view(), + name='prospective_Fellow_update_status' + ), + url( + r'^prospectivefellows/(?P<pk>[0-9]+)/delete/$', + views.ProspectiveFellowDeleteView.as_view(), + name='prospective_Fellow_delete' + ), + url( + r'^prospectivefellows/(?P<pk>[0-9]+)/events/add$', + views.ProspectiveFellowEventCreateView.as_view(), + name='prospective_Fellow_event_create' + ), + url( + r'^prospectivefellows/(?P<pk>[0-9]+)/email/$', + views.ProspectiveFellowInitialEmailView.as_view(), + name='prospective_Fellow_email_initial' + ), ] diff --git a/colleges/views.py b/colleges/views.py index babe8a45304bc2f92231d82ffa63a6c7a3f2ecec..aa3a47a95175f5e7f8407e1b40ad8be23ab6ecc6 100644 --- a/colleges/views.py +++ b/colleges/views.py @@ -5,15 +5,30 @@ __license__ = "AGPL v3" from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.shortcuts import get_object_or_404, render, redirect -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, reverse_lazy +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.views.generic.list import ListView from submissions.models import Submission +from .constants import PROSPECTIVE_FELLOW_INVITED,\ + prospective_Fellow_statuses_dict,\ + PROSPECTIVE_FELLOW_EVENT_EMAILED, PROSPECTIVE_FELLOW_EVENT_STATUSUPDATED,\ + PROSPECTIVE_FELLOW_EVENT_COMMENT from .forms import FellowshipForm, FellowshipTerminateForm, FellowshipRemoveSubmissionForm,\ FellowshipAddSubmissionForm, AddFellowshipForm, SubmissionAddFellowshipForm,\ FellowshipRemoveProceedingsForm, FellowshipAddProceedingsForm, SubmissionAddVotingFellowForm,\ - FellowVotingRemoveSubmissionForm -from .models import Fellowship + FellowVotingRemoveSubmissionForm,\ + ProspectiveFellowForm, ProspectiveFellowStatusForm, ProspectiveFellowEventForm +from .models import Fellowship, ProspectiveFellow, ProspectiveFellowEvent + +from scipost.constants import SCIPOST_SUBJECT_AREAS +from scipost.mixins import PermissionsMixin + +from mails.forms import EmailTemplateForm +from mails.views import MailView @login_required @@ -292,3 +307,123 @@ def fellowship_add_proceedings(request, id): 'form': form, } return render(request, 'colleges/fellowship_proceedings_add.html', context) + + + +class ProspectiveFellowCreateView(PermissionsMixin, CreateView): + """ + Formview to create a new Prospective Fellow. + """ + permission_required = 'scipost.can_manage_college_composition' + form_class = ProspectiveFellowForm + template_name = 'colleges/prospectivefellow_form.html' + success_url = reverse_lazy('colleges:prospective_Fellows') + + +class ProspectiveFellowUpdateView(PermissionsMixin, UpdateView): + """ + Formview to update a Prospective Fellow. + """ + permission_required = 'scipost.can_manage_college_composition' + model = ProspectiveFellow + form_class = ProspectiveFellowForm + template_name = 'colleges/prospectivefellow_form.html' + success_url = reverse_lazy('colleges:prospective_Fellows') + + +class ProspectiveFellowUpdateStatusView(PermissionsMixin, UpdateView): + """ + Formview to update the status of a Prospective Fellow. + """ + permission_required = 'scipost.can_manage_college_composition' + model = ProspectiveFellow + fields = ['status'] + success_url = reverse_lazy('colleges:prospective_Fellows') + + def form_valid(self, form): + event = ProspectiveFellowEvent( + prosfellow=self.object, + event=PROSPECTIVE_FELLOW_EVENT_STATUSUPDATED, + comments=('Status updated to %s' + % prospective_Fellow_statuses_dict[form.cleaned_data['status']]), + noted_on=timezone.now(), + noted_by=self.request.user.contributor) + event.save() + return super().form_valid(form) + + +class ProspectiveFellowDeleteView(PermissionsMixin, DeleteView): + """ + Delete a Prospective Fellow. + """ + permission_required = 'scipost.can_manage_college_composition' + model = ProspectiveFellow + success_url = reverse_lazy('colleges:prospective_Fellows') + + +class ProspectiveFellowListView(PermissionsMixin, ListView): + """ + List the ProspectiveFellow object instances. + """ + permission_required = 'scipost.can_manage_college_composition' + model = ProspectiveFellow + paginate_by = 50 + + def get_queryset(self): + """ + Return a queryset of ProspectiveFellows using optional GET data. + """ + queryset = ProspectiveFellow.objects.all() + if 'discipline' in self.request.GET: + queryset = queryset.filter(discipline=self.request.GET['discipline'].lower()) + if 'expertise' in self.request.GET: + queryset = queryset.filter(expertises__contains=[self.request.GET['expertise']]) + return queryset + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['subject_areas'] = SCIPOST_SUBJECT_AREAS + context['pfstatus_form'] = ProspectiveFellowStatusForm() + context['pfevent_form'] = ProspectiveFellowEventForm() + return context + + +class ProspectiveFellowInitialEmailView(PermissionsMixin, MailView): + """ + Send a templated email to a Prospective Fellow. + """ + permission_required = 'scipost.can_manage_college_composition' + queryset = ProspectiveFellow.objects.all() + mail_code = 'prospectivefellows/invite_prospective_fellow_initial' + success_url = reverse_lazy('colleges:prospective_Fellows') + + def form_valid(self, form): + """ + Create an event associated to this outgoing email. + """ + event = ProspectiveFellowEvent( + prosfellow=self.object, + event=PROSPECTIVE_FELLOW_EVENT_EMAILED, + comments='Emailed initial template', + noted_on=timezone.now(), + noted_by=self.request.user.contributor) + event.save() + self.object.status=PROSPECTIVE_FELLOW_INVITED + self.object.save() + return super().form_valid(form) + + +class ProspectiveFellowEventCreateView(PermissionsMixin, CreateView): + """ + Add an event for a Prospective Fellow. + """ + permission_required = 'scipost.can_manage_college_composition' + form_class = ProspectiveFellowEventForm + success_url = reverse_lazy('colleges:prospective_Fellows') + + def form_valid(self, form): + form.instance.prosfellow = get_object_or_404(ProspectiveFellow, id=self.kwargs['pk']) + form.instance.noted_on = timezone.now() + form.instance.noted_by = self.request.user.contributor + messages.success(self.request, 'Event added successfully') + return super().form_valid(form) diff --git a/mails/forms.py b/mails/forms.py index 392a77ebccb8956100e0b4740d69a9f30ece14ac..df72600a17c82c15c4f1f8492143720246025c35 100644 --- a/mails/forms.py +++ b/mails/forms.py @@ -18,7 +18,8 @@ class EmailTemplateForm(forms.Form, MailUtilsMixin): self.pre_validation(*args, **kwargs) # This form shouldn't be is_bound==True is there is any non-relavant POST data given. - data = args[0] or {} + data = args[0] if args else {} + data = kwargs['data'] if 'data' in kwargs else data if '%s-subject' % self.prefix in data.keys(): data = { '%s-subject' % self.prefix: data.get('%s-subject' % self.prefix), @@ -29,7 +30,7 @@ class EmailTemplateForm(forms.Form, MailUtilsMixin): data = None super().__init__(data or None) - if not self.original_recipient: + if self.original_recipient == '': self.fields['extra_recipient'].label = "Send this email to" self.fields['extra_recipient'].required = True @@ -49,7 +50,7 @@ class EmailTemplateForm(forms.Form, MailUtilsMixin): self.bcc_list.append(self.cleaned_data.get('extra_recipient')) elif self.cleaned_data.get('extra_recipient') and not self.original_recipient: self.original_recipient = [self.cleaned_data.get('extra_recipient')] - elif not self.original_recipient: + elif self.original_recipient == '': self.add_error('extra_recipient', 'Please fill the bcc field to send the mail.') self.validate_recipients() @@ -60,6 +61,10 @@ class EmailTemplateForm(forms.Form, MailUtilsMixin): self.save_data() return data + def save(self): + self.send() + return self.instance + class HiddenDataForm(forms.Form): def __init__(self, form, *args, **kwargs): diff --git a/mails/views.py b/mails/views.py index 42657da3f7c649f97b4b0c56be84dc8ffea93024..3edfaece211d481dc122acf1e5fde210d8abff6c 100644 --- a/mails/views.py +++ b/mails/views.py @@ -4,6 +4,7 @@ __license__ = "AGPL v3" from django.contrib import messages from django.shortcuts import render +from django.views.generic.edit import UpdateView from .forms import EmailTemplateForm, HiddenDataForm @@ -113,3 +114,18 @@ class MailEditorMixin: raise AttributeError('Did you check the order in which MailEditorMixin is used?') messages.success(self.request, 'Mail sent') return response + + +class MailView(UpdateView): + template_name = 'mails/mail_form.html' + form_class = EmailTemplateForm + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['mail_code'] = self.mail_code + return kwargs + + def form_valid(self, form): + response = super().form_valid(form) + form.send() + return response diff --git a/templates/email/prospectivefellows/invite_prospective_fellow_initial.html b/templates/email/prospectivefellows/invite_prospective_fellow_initial.html new file mode 100644 index 0000000000000000000000000000000000000000..b9ffed2640e3d8f35befa2a0f918bd826ed5ffe5 --- /dev/null +++ b/templates/email/prospectivefellows/invite_prospective_fellow_initial.html @@ -0,0 +1,8 @@ +<p>Dear {{ prosfel.get_title_display }} {{ prosfel.last_name }},</p> +<p>On behalf of the SciPost Foundation and in view of your professional expertise and reputation, I hereby would like to invite you to join our Editorial College and become one of our Editorial Fellows. By joining, you would be greatly helping us in our mission to establish a more healthy infrastructure for scientific publishing.</p> +<p>Please note that only well-known and respected senior academics are being contacted for this purpose. Academic reputation and involvement in the community are the most important criteria guiding our considerations of who should belong to the Editorial College. The current list of Fellows can be found at <a href="{% url 'scipost:about' %}">scipost.org/about</a>; on this page, you will also find basic information on SciPost and its guiding principles (you can also take a look at our <a href="{% url 'scipost:FAQ' %}">frequently asked questions page</a>). Functioning of the College proceeds according to the by-laws set out at <a href="{% url 'scipost:EdCol_by-laws' %}">scipost.org/EdCol_by-laws</a>. A short summary of the editorial workflow can be found at <a href="{% url 'submissions:editorial_workflow' %}">this page</a>. +<p>We do not pose any conditions on your involvement, and you would always remain in complete control of your level of commitment (devoting even a couple of hours per month would be enough to help out significantly).</p> +<p>I would be very happy to provide you with more information should you require it. Could I beg you to give us a response (by replying to this email) within the next couple of weeks?</p> +<p>Many thanks in advance,</p> +<p>Prof. J.-S. Caux, on behalf of the SciPost Foundation</p> +{% include 'email/_footer.html' %} diff --git a/templates/email/prospectivefellows/invite_prospective_fellow_initial.json b/templates/email/prospectivefellows/invite_prospective_fellow_initial.json new file mode 100644 index 0000000000000000000000000000000000000000..c29403d121e45719f9e8b50321b1bc08cee15871 --- /dev/null +++ b/templates/email/prospectivefellows/invite_prospective_fellow_initial.json @@ -0,0 +1,8 @@ +{ + "subject": "Invitation to become a Fellow at SciPost", + "to_address": "email", + "bcc_to": "admin@scipost.org", + "from_address_name": "SciPost Admin", + "from_address": "admin@scipost.org", + "context_object": "prosfel" +}