Source code for production.models

__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"


from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
from django.urls import reverse
from django.contrib.auth.models import User
from django.utils import timezone
from django.utils.functional import cached_property

from .constants import PRODUCTION_STREAM_STATUS, PRODUCTION_STREAM_INITIATED, PRODUCTION_EVENTS,\
                       EVENT_MESSAGE, EVENT_HOUR_REGISTRATION, PRODUCTION_STREAM_COMPLETED,\
                       PROOFS_STATUSES, PROOFS_UPLOADED
from .managers import ProductionStreamQuerySet, ProductionEventManager, ProofsQuerySet,\
                      ProductionUserQuerySet
from .utils import proofs_id_to_slug

from finances.models import WorkLog
from scipost.storage import SecureFileStorage


[docs]class ProductionUser(models.Model): """ Production Officers will have a ProductionUser object related to their account to relate all production related actions to. """ user = models.OneToOneField(User, on_delete=models.PROTECT, unique=True, related_name='production_user', null=True) name = models.CharField(max_length=128, blank=True) objects = ProductionUserQuerySet.as_manager() def __str__(self): if self.user: return '%s, %s' % (self.user.last_name, self.user.first_name) return '%s (deactivated)' % self.name
[docs]class ProductionStream(models.Model): submission = models.OneToOneField('submissions.Submission', on_delete=models.CASCADE, related_name='production_stream') opened = models.DateTimeField(auto_now_add=True) closed = models.DateTimeField(default=timezone.now) status = models.CharField(max_length=32, choices=PRODUCTION_STREAM_STATUS, default=PRODUCTION_STREAM_INITIATED) officer = models.ForeignKey('production.ProductionUser', blank=True, null=True, on_delete=models.SET_NULL, related_name='streams') supervisor = models.ForeignKey('production.ProductionUser', blank=True, null=True, on_delete=models.SET_NULL, related_name='supervised_streams') invitations_officer = models.ForeignKey('production.ProductionUser', blank=True, null=True, on_delete=models.SET_NULL, related_name='invitations_officer_streams') work_logs = GenericRelation(WorkLog, related_query_name='streams') objects = ProductionStreamQuerySet.as_manager() class Meta: permissions = ( ('can_work_for_stream', 'Can work for stream'), ('can_perform_supervisory_actions', 'Can perform supervisory actions'), ) def __str__(self): return '{arxiv}, {title}'.format(arxiv=self.submission.preprint.identifier_w_vn_nr, title=self.submission.title)
[docs] def get_absolute_url(self): return reverse('production:stream', args=(self.id,))
@cached_property def total_duration(self): totdur = self.work_logs.aggregate(models.Sum('duration')) return totdur['duration__sum'] @cached_property def completed(self): return self.status == PRODUCTION_STREAM_COMPLETED @property def notification_name(self): return self.submission.preprint.identifier_w_vn_nr @property def latest_activity(self): if self.events.last(): return self.events.last().noted_on return self.closed or self.opened
[docs]class ProductionEvent(models.Model): stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE, related_name='events') event = models.CharField(max_length=64, choices=PRODUCTION_EVENTS, default=EVENT_MESSAGE) comments = models.TextField(blank=True, null=True) noted_on = models.DateTimeField(default=timezone.now) noted_by = models.ForeignKey('production.ProductionUser', on_delete=models.CASCADE, related_name='events') noted_to = models.ForeignKey('production.ProductionUser', on_delete=models.CASCADE, blank=True, null=True, related_name='received_events') duration = models.DurationField(blank=True, null=True) objects = ProductionEventManager() class Meta: ordering = ['noted_on'] def __str__(self): return '%s: %s' % (self.stream, self.get_event_display())
[docs] def get_absolute_url(self): return self.stream.get_absolute_url()
@cached_property def editable(self): return self.event in [EVENT_MESSAGE, EVENT_HOUR_REGISTRATION] and not self.stream.completed @property def notification_name(self): return self.stream.notification_name
[docs]def production_event_upload_location(instance, filename): submission = instance.production_event.stream.submission return 'UPLOADS/PRODSTREAMS/{year}/{arxiv}/{filename}'.format( year=submission.submission_date.year, arxiv=submission.preprint.identifier_wo_vn_nr, filename=filename)
[docs]class ProductionEventAttachment(models.Model): """ An ProductionEventAttachment is in general used by authors to reply to an Proofs version with their version of the Proofs with comments. """ production_event = models.ForeignKey('production.ProductionEvent', on_delete=models.CASCADE, related_name='attachments') attachment = models.FileField(upload_to=production_event_upload_location, storage=SecureFileStorage())
[docs] def get_absolute_url(self): return reverse('production:production_event_attachment_pdf', args=(self.production_event.stream.id, self.id,))
[docs]def proofs_upload_location(instance, filename): submission = instance.stream.submission return 'UPLOADS/PROOFS/{year}/{arxiv}/{filename}'.format( year=submission.submission_date.year, arxiv=submission.preprint.identifier_wo_vn_nr, filename=filename)
[docs]class Proofs(models.Model): """ Proofs are directly related to a ProductionStream and Submission in SciPost. """ attachment = models.FileField(upload_to=proofs_upload_location, storage=SecureFileStorage()) version = models.PositiveSmallIntegerField(default=0) stream = models.ForeignKey('production.ProductionStream', on_delete=models.CASCADE, related_name='proofs') uploaded_by = models.ForeignKey('production.ProductionUser', on_delete=models.CASCADE, related_name='+') created = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=16, choices=PROOFS_STATUSES, default=PROOFS_UPLOADED) accessible_for_authors = models.BooleanField(default=False) objects = ProofsQuerySet.as_manager() class Meta: ordering = ['stream', 'version'] verbose_name_plural = 'Proofs'
[docs] def get_absolute_url(self): return reverse('production:proofs_pdf', kwargs={'slug': self.slug})
def __str__(self): return 'Proofs {version} for Stream {stream}'.format( version=self.version, stream=self.stream.submission.title)
[docs] def save(self, *args, **kwargs): # Control Report count per Submission. if not self.version: self.version = self.stream.proofs.count() + 1 return super().save(*args, **kwargs)
@property def slug(self): return proofs_id_to_slug(self.id)