Newer
Older
__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.utils.functional import cached_property
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db.models import Value
from django.db.models.functions import Concat
from django.conf import settings
from .constants import (
PRODUCTION_STREAM_STATUS,
PRODUCTION_STREAM_INITIATED,
PRODUCTION_EVENTS,
EVENT_MESSAGE,
EVENT_HOUR_REGISTRATION,
PRODUCTION_STREAM_COMPLETED,
PROOFS_STATUSES,
PROOFS_UPLOADED,
PROOFS_REPO_STATUSES,
PROOFS_REPO_UNINITIALIZED,
)
from .managers import (
ProductionStreamQuerySet,
ProductionEventManager,
ProofsQuerySet,
ProductionUserQuerySet,
)
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()
return "%s, %s" % (self.user.last_name, self.user.first_name)
return "%s (deactivated)" % self.name
class ProductionStream(models.Model):
submission = models.OneToOneField(
"submissions.Submission",
on_delete=models.CASCADE,
related_name="production_stream",
)
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")
("can_work_for_stream", "Can work for stream"),
("can_perform_supervisory_actions", "Can perform supervisory actions"),
return "{arxiv}, {title}".format(
arxiv=self.submission.preprint.identifier_w_vn_nr,
title=self.submission.title,
)
return reverse("production:stream", args=(self.id,))
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 latest_activity(self):
if self.events.last():
return self.events.last().noted_on
return self.closed or self.opened
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()
return "%s: %s" % (self.stream, self.get_event_display())
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
def production_event_upload_location(instance, filename):
submission = instance.production_event.stream.submission
return "UPLOADS/PRODSTREAMS/{year}/{thread_hash_head}/{filename}".format(
thread_hash_head=str(submission.thread_hash).partition("-")[0],
filename=filename,
)
class ProductionEventAttachment(models.Model):
"""
An ProductionEventAttachment is in general used by authors to reply to a 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())
return reverse(
"production:production_event_attachment_pdf",
args=(
self.production_event.stream.id,
self.id,
),
)
def proofs_upload_location(instance, filename):
submission = instance.stream.submission
return "UPLOADS/PROOFS/{year}/{thread_hash_head}/{filename}".format(
thread_hash_head=str(submission.thread_hash).partition("-")[0],
filename=filename,
)
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)
ordering = ["stream", "version"]
verbose_name_plural = "Proofs"
return reverse("production:proofs_pdf", kwargs={"slug": self.slug})
return "Proofs {version} for Stream {stream}".format(version=self.version, stream=self.stream.submission.title)
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):
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
class ProofsRepository(models.Model):
"""
ProofsRepository is a GitLab repository of Proofs for a Submission.
"""
stream = models.OneToOneField(
ProductionStream,
on_delete=models.CASCADE,
related_name="proofs_repository",
)
status = models.CharField(
max_length=32,
choices=PROOFS_REPO_STATUSES,
default=PROOFS_REPO_UNINITIALIZED,
)
@property
def name(self) -> str:
"""
Return the name of the repository in the form of "id_lastname".
"""
# Get the last name of the first author by getting the first author string from the submission
first_author_str = self.stream.submission.authors_as_list[0]
first_author_profile = (
Profile.objects.annotate(
full_name=Concat("first_name", Value(" "), "last_name")
)
.filter(full_name=first_author_str)
.first()
)
if first_author_profile is None:
first_author_last_name = first_author_str.split(" ")[-1]
else:
first_author_last_name = first_author_profile.last_name
# Keep only the last of the last names
first_author_last_name = first_author_last_name.split(" ")[-1]
return "{preprint_id}_{last_name}".format(
preprint_id=self.stream.submission.preprint.identifier_w_vn_nr,
last_name=first_author_last_name,
)
@property
def journal_abbrev(self) -> str:
# The DOI label is used to determine the path of the repository and template
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
return self.stream.submission.editorial_decision.for_journal.doi_label
@property
def journal_subdivision(self) -> str:
"""
Return the subdivision of the repository depending on the journal type.
Regular journals are subdivided per year and month,
while proceedings are subdivided per year and conference.
"""
# TODO: Removing the whitespace should be more standardised
# Refactor: journal and year are common to both cases
# perhaps it is best to only return the subdivision month/conference
if proceedings_issue := self.stream.submission.proceedings:
return "{journal}/{year}/{conference}".format(
journal=self.journal_abbrev,
year=self.stream.submission.proceedings.event_end_date.year,
conference=proceedings_issue.event_suffix.replace(" ", ""),
)
else:
# Get creation date of the stream
# Warning: The month grouping of streams was done using the tasked date,
# but should now instead be the creation (opened) date.
opened_year, opened_month = self.stream.opened.strftime("%Y-%m").split("-")
return "{journal}/{year}/{month}".format(
journal=self.journal_abbrev,
year=opened_year,
month=opened_month,
)
@property
def git_path(self) -> str:
return "{ROOT}/Proofs/{journal_subdivision}/{repo_name}".format(
ROOT=settings.GITLAB_ROOT,
journal_subdivision=self.journal_subdivision,
@property
def git_url(self) -> str:
return "https://{GITLAB_URL}/{git_path}".format(
GITLAB_URL=settings.GITLAB_URL,
git_path=self.git_path,
)
@property
def git_ssh_clone_url(self) -> str:
return "git:{GITLAB_URL}/{git_path}.git".format(
GITLAB_URL=settings.GITLAB_URL,
git_path=self.git_path,
)
@property
def template_path(self) -> str:
"""
Return the path to the template repository.
"""
if self.stream.submission.proceedings is not None:
return "{ROOT}/Templates/{journal_subdivision}".format(
ROOT=settings.GITLAB_ROOT,
journal_subdivision=self.journal_subdivision,
)
else:
return "{ROOT}/Templates/{journal}".format(
ROOT=settings.GITLAB_ROOT,
journal=self.journal_abbrev,
)
def __str__(self) -> str:
return f"Proofs repo for {self.stream}"
class Meta:
verbose_name_plural = "proofs repositories"
@receiver(post_save, sender=ProductionStream)
def production_stream_create_proofs_repo(sender, instance, created, **kwargs):
"""
When a ProductionStream instance is created, a ProofsRepository instance is created
and linked to it.
"""
if created:
ProofsRepository.objects.create(
stream=instance,
status=PROOFS_REPO_UNINITIALIZED,
)
post_save.connect(production_stream_create_proofs_repo, sender=ProductionStream)
@receiver(post_save, sender=ProofsRepository)
def advance_repo_on_gitlab(sender, instance, created, **kwargs):
"""
When a ProofsRepository instance is created, run the advance_git_repos command on it.
"""
if created:
from django.core import management
management.call_command(
"advance_git_repos",
id=instance.stream.submission.preprint.identifier_w_vn_nr,
)
post_save.connect(advance_repo_on_gitlab, sender=ProofsRepository)