diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index e359cec9f3feb85bf06fade6d5e00791ad08771f..b4f1a68cd6d4cc2e4357d3fea55dd5bc9c579a60 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -248,6 +248,10 @@ class Command(BaseCommand): codename='can_view_all_funding_info', name='Can view all Funders info', content_type=content_type) + can_create_grants, created = Permission.objects.get_or_create( + codename='can_create_grants', + name='Can create Grant', + content_type=content_type) can_draft_publication, created = Permission.objects.get_or_create( codename='can_draft_publication', name='Can draft Publication', @@ -331,6 +335,7 @@ class Command(BaseCommand): can_publish_accepted_submission, can_draft_publication, can_view_all_funding_info, + can_create_grants, can_attend_VGMs, can_manage_reports, can_assign_production_supervisor, @@ -382,6 +387,7 @@ class Command(BaseCommand): can_assign_production_officer, can_take_decisions_related_to_proofs, can_draft_publication, + can_create_grants, can_view_all_production_streams, can_run_proofs_by_authors, can_view_docs_scipost, diff --git a/scipost/management/commands/models.py b/scipost/management/commands/models.py new file mode 100644 index 0000000000000000000000000000000000000000..0447660e2a189556e389b610e0c5d6bc797218aa --- /dev/null +++ b/scipost/management/commands/models.py @@ -0,0 +1,188 @@ +import datetime +import hashlib +import random +import string + +from django.db import models, IntegrityError +from django.conf import settings +from django.utils import timezone + +from . import constants +from .managers import RegistrationInvitationQuerySet, CitationNotificationQuerySet + +from scipost.constants import TITLE_CHOICES + + +class RegistrationInvitation(models.Model): + """ + Invitation to particular persons for registration + """ + 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() + status = models.CharField(max_length=8, choices=constants.REGISTATION_INVITATION_STATUSES, + default=constants.STATUS_DRAFT) + + # Text content + message_style = models.CharField(max_length=1, choices=constants.INVITATION_STYLE, + default=constants.INVITATION_FORMAL) + personal_message = models.TextField(blank=True) + invited_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, + blank=True, null=True, related_name='invitations_sent') + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='invitations_created') + + # Related to objects + invitation_type = models.CharField(max_length=2, choices=constants.INVITATION_TYPE, + default=constants.INVITATION_CONTRIBUTOR) + + # Response keys + invitation_key = models.CharField(max_length=40, unique=True) + key_expires = models.DateTimeField(default=timezone.now) + + # Timestamps + date_sent_first = models.DateTimeField(null=True, blank=True) + date_sent_last = models.DateTimeField(null=True, blank=True) + times_sent = models.PositiveSmallIntegerField(default=0) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + objects = RegistrationInvitationQuerySet.as_manager() + + class Meta: + ordering = ['last_name'] + + def __str__(self): + return '{} {} on {}'.format(self.first_name, self.last_name, + self.created.strftime("%Y-%m-%d")) + + def save(self, *args, **kwargs): + self.refresh_keys(commit=False) + return super().save(*args, **kwargs) + + def refresh_keys(self, force_new_key=False, commit=True): + # Generate email activation key and link + if not self.invitation_key or force_new_key: + # TODO: Replace this all by the `secrets` package available from python 3.6(!) + salt = '' + for i in range(5): + salt += random.choice(string.ascii_letters) + salt = salt.encode('utf8') + invitationsalt = self.last_name.encode('utf8') + self.invitation_key = hashlib.sha1(salt + invitationsalt).hexdigest() + self.key_expires = timezone.now() + datetime.timedelta(days=365) + if commit: + self.save() + + def mail_sent(self, user=None): + """ + Update instance fields as if a new invitation mail has been sent out. + """ + if self.status == constants.STATUS_DRAFT: + self.status = constants.STATUS_SENT + if not self.date_sent_first: + self.date_sent_first = timezone.now() + self.date_sent_last = timezone.now() + self.invited_by = user or self.created_by + self.times_sent += 1 + self.citation_notifications.update(processed=True) + self.save() + + @property + def has_responded(self): + return self.status in [constants.STATUS_DECLINED, constants.STATUS_REGISTERED] + + +class CitationNotification(models.Model): + invitation = models.ForeignKey('invitations.RegistrationInvitation', + on_delete=models.SET_NULL, + null=True, blank=True) + contributor = models.ForeignKey('scipost.Contributor', + on_delete=models.CASCADE, + null=True, blank=True, + related_name='+') + + # Content + submission = models.ForeignKey('submissions.Submission', null=True, blank=True, + related_name='+') + publication = models.ForeignKey('journals.Publication', null=True, blank=True, + related_name='+') + processed = models.BooleanField(default=False) + + # Meta info + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notifications_created') + date_sent = models.DateTimeField(null=True, blank=True) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + objects = CitationNotificationQuerySet.as_manager() + + class Meta: + default_related_name = 'citation_notifications' + unique_together = ( + ('invitation', 'submission'), + ('invitation', 'publication'), + ('contributor', 'submission'), + ('contributor', 'publication'), + ) + + def __str__(self): + _str = 'Citation for ' + if self.invitation: + _str += ' Invitation ({} {})'.format( + self.invitation.first_name, + self.invitation.last_name, + ) + elif self.contributor: + _str += ' Contributor ({})'.format(self.contributor) + + _str += ' on ' + if self.submission: + _str += 'Submission ({})'.format(self.submission.arxiv_identifier_w_vn_nr) + elif self.publication: + _str += 'Publication ({})'.format(self.publication.doi_label) + return _str + + def save(self, *args, **kwargs): + if not self.submission and not self.publication: + raise IntegrityError(('CitationNotification needs to be related to either a ' + 'Submission or Publication object.')) + return super().save(*args, **kwargs) + + def mail_sent(self): + """ + Update instance fields as if a new citation notification mail has been sent out. + """ + self.processed = True + if not self.date_sent: + # Don't overwrite by accident... + self.date_sent = timezone.now() + self.save() + + def related_notifications(self): + return CitationNotification.objects.unprocessed().filter( + models.Q(contributor=self.contributor) | models.Q(invitation=self.invitation)) + + def get_first_related_contributor(self): + return self.related_notifications().filter(contributor__isnull=False).first() + + @property + def email(self): + if self.invitation: + return self.invitation.email + elif self.contributor: + return self.contributor.user.email + + @property + def last_name(self): + if self.invitation: + return self.invitation.last_name + elif self.contributor: + return self.contributor.user.last_name + + @property + def get_title(self): + if self.invitation: + return self.invitation.get_title_display() + elif self.contributor: + return self.contributor.get_title_display()