diff --git a/colleges/migrations/0015_auto_20200906_0714.py b/colleges/migrations/0015_auto_20200906_0714.py new file mode 100644 index 0000000000000000000000000000000000000000..fa513656155d7624392548d52cbd40147735daad --- /dev/null +++ b/colleges/migrations/0015_auto_20200906_0714.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.11 on 2020-09-06 05:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ontology', '0007_Branch_Field_Specialty'), + ('colleges', '0014_auto_20190419_1150'), + ] + + operations = [ + migrations.CreateModel( + name='College', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Official name of the College (default: name of the discipline)', max_length=256, unique=True)), + ('order', models.PositiveSmallIntegerField()), + ('acad_field', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='colleges', to='ontology.AcademicField')), + ], + options={ + 'ordering': ['acad_field', 'order'], + }, + ), + migrations.AddConstraint( + model_name='college', + constraint=models.UniqueConstraint(fields=('name', 'acad_field'), name='college_unique_name_acad_field'), + ), + migrations.AddConstraint( + model_name='college', + constraint=models.UniqueConstraint(fields=('acad_field', 'order'), name='college_unique_acad_field_order'), + ), + ] diff --git a/colleges/models/__init__.py b/colleges/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5e7aeb5facfaeb700d6dfb74b11fab5240d3a597 --- /dev/null +++ b/colleges/models/__init__.py @@ -0,0 +1,9 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from .college import College + +from .fellowship import Fellowship + +from .potential_fellowship import PotentialFellowship, PotentialFellowshipEvent diff --git a/colleges/models/college.py b/colleges/models/college.py new file mode 100644 index 0000000000000000000000000000000000000000..51a64d7222a06872b7207b069951dc6569e3d30a --- /dev/null +++ b/colleges/models/college.py @@ -0,0 +1,64 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.db import models + +from scipost.constants import SCIPOST_DISCIPLINES + +from ontology.models import Specialty + + +class College(models.Model): + """ + Anchor for a set of Fellows handling a set of Journals. + + A College has a ForeignKey to AcademicField. + + Specialties are defined as a `@property` and extracted via the Journals + which are ForeignKey-related back to College. + + The `@property` `is_field_wide` checks the Journals run by the College and + returns a Boolean specifying whether the College operates field-wide, or is specialized. + """ + + name = models.CharField( + max_length=256, + help_text='Official name of the College (default: name of the discipline)', + unique=True + ) + + acad_field = models.ForeignKey( + 'ontology.AcademicField', + on_delete=models.PROTECT, + related_name='colleges' + ) + + order = models.PositiveSmallIntegerField() + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['name', 'acad_field',], + name='college_unique_name_acad_field' + ), + models.UniqueConstraint( + fields=['acad_field', 'order'], + name='college_unique_acad_field_order' + ), + ] + ordering = [ + 'acad_field', + 'order' + ] + + def __str__(self): + return "Editorial College (%s)" % self.name + + @property + def specialties(self): + return Specialty.objects.filter(journals__college__pk=self.id) + + @property + def is_field_wide(self): + return len(self.specialties) == 0 diff --git a/colleges/models/fellowship.py b/colleges/models/fellowship.py new file mode 100644 index 0000000000000000000000000000000000000000..9553161ce886fbc2333b12ac71dc1b73bbc977de --- /dev/null +++ b/colleges/models/fellowship.py @@ -0,0 +1,62 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +import datetime + +from django.db import models +from django.urls import reverse + +from ..managers import FellowQuerySet + +from scipost.behaviors import TimeStampedModel +from scipost.models import get_sentinel_user + + +class Fellowship(TimeStampedModel): + """A Fellowship gives access to the Submission Pool to Contributors. + + Editorial College Fellowship connects the Editorial College and Contributors, + possibly with a limiting start/until date and/or a Proceedings event. + + The date range will effectively be used while determining 'the pool' for a specific + Submission, so it has a direct effect on the submission date. + """ + + contributor = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, + related_name='fellowships') + start_date = models.DateField(null=True, blank=True) + until_date = models.DateField(null=True, blank=True) + + guest = models.BooleanField('Guest Fellowship', default=False) + + objects = FellowQuerySet.as_manager() + + class Meta: + ordering = ['contributor__user__last_name'] + unique_together = ('contributor', 'start_date', 'until_date') + + def __str__(self): + _str = self.contributor.__str__() + if self.guest: + _str += ' (guest fellowship)' + return _str + + def get_absolute_url(self): + """Return the admin fellowship page.""" + return reverse('colleges:fellowship_detail', kwargs={'pk': self.id}) + + def sibling_fellowships(self): + """Return all Fellowships that are directly related to the Fellow of this Fellowship.""" + return self.contributor.fellowships.all() + + def is_active(self): + """Check if the instance is within start and until date.""" + today = datetime.date.today() + if not self.start_date: + if not self.until_date: + return True + return today <= self.until_date + elif not self.until_date: + return today >= self.start_date + return today >= self.start_date and today <= self.until_date diff --git a/colleges/models/potential_fellowship.py b/colleges/models/potential_fellowship.py new file mode 100644 index 0000000000000000000000000000000000000000..7c829c17e01a234c22743d7f2ee9ca99b72420d1 --- /dev/null +++ b/colleges/models/potential_fellowship.py @@ -0,0 +1,77 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +import datetime + +from django.db import models +from django.utils import timezone + +from scipost.models import get_sentinel_user + +from ..constants import POTENTIAL_FELLOWSHIP_STATUSES,\ + POTENTIAL_FELLOWSHIP_IDENTIFIED, POTENTIAL_FELLOWSHIP_EVENTS +from ..managers import PotentialFellowshipQuerySet + + + +class PotentialFellowship(models.Model): + """ + A PotentialFellowship is defined when a researcher has been identified by + Admin or EdAdmin as a potential member of an Editorial College, + or when a current Advisory Board member or Fellow nominates the person. + + It is linked to Profile as ForeignKey and not as OneToOne, since the same + person can eventually be approached on different occasions. + + Using Profile allows to consider both registered Contributors + and non-registered people. + """ + + profile = models.ForeignKey('profiles.Profile', on_delete=models.CASCADE) + status = models.CharField(max_length=32, choices=POTENTIAL_FELLOWSHIP_STATUSES, + default=POTENTIAL_FELLOWSHIP_IDENTIFIED) + in_agreement = models.ManyToManyField( + 'scipost.Contributor', + related_name='in_agreement_with_election', blank=True) + in_abstain = models.ManyToManyField( + 'scipost.Contributor', + related_name='in_abstain_with_election', blank=True) + in_disagreement = models.ManyToManyField( + 'scipost.Contributor', + related_name='in_disagreement_with_election', blank=True) + voting_deadline = models.DateTimeField('voting deadline', default=timezone.now) + elected = models.NullBooleanField() + + objects = PotentialFellowshipQuerySet.as_manager() + + class Meta: + ordering = ['profile__last_name'] + + def __str__(self): + return '%s, %s' % (self.profile.__str__(), self.get_status_display()) + + def latest_event_details(self): + event = self.potentialfellowshipevent_set.order_by('-noted_on').first() + if not event: + return 'No event recorded' + return '%s [%s]' % (event.get_event_display(), event.noted_on.strftime('%Y-%m-%d')) + + +class PotentialFellowshipEvent(models.Model): + """Any event directly related to a PotentialFellowship instance registered as plain text.""" + + potfel = models.ForeignKey('colleges.PotentialFellowship', on_delete=models.CASCADE) + event = models.CharField(max_length=32, choices=POTENTIAL_FELLOWSHIP_EVENTS) + comments = models.TextField(blank=True) + + noted_on = models.DateTimeField(auto_now_add=True) + 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.potfel.profile.last_name, + self.potfel.profile.get_title_display(), + self.potfel.profile.first_name, + self.get_event_display())