__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" import hashlib import os import random import re import string from datetime import datetime from django import forms from django.conf import settings from django.forms import BaseModelFormSet, modelformset_factory from django.template import loader from django.utils import timezone from ajax_select.fields import AutoCompleteSelectField from .constants import STATUS_DRAFT, STATUS_PUBLICLY_OPEN,\ PUBLICATION_PREPUBLISHED, PUBLICATION_PUBLISHED from .exceptions import PaperNumberingError from .models import ( Issue, Publication, Reference, Volume, PublicationAuthorsTable, OrgPubFraction) from .utils import JournalUtils from .signals import notify_manuscript_published from funders.models import Grant, Funder from journals.models import Journal from mails.utils import DirectMailUtil from production.constants import PROOFS_PUBLISHED from production.models import ProductionEvent from production.signals import notify_stream_status_change from scipost.forms import RequestFormMixin from scipost.services import DOICaller from submissions.constants import STATUS_PUBLISHED from submissions.models import Submission class CitationListBibitemsForm(forms.ModelForm): latex_bibitems = forms.CharField(widget=forms.Textarea()) class Meta: model = Publication fields = () def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['latex_bibitems'].widget.attrs.update( {'placeholder': 'Paste the .tex bibitems here'}) def extract_dois(self): entries_list = self.cleaned_data['latex_bibitems'] entries_list = re.sub(r'(?m)^\%.*\n?', '', entries_list) entries_list = entries_list.split('\doi{') dois = [] n_entry = 1 for entry in entries_list[1:]: # drop first bit before first \doi{ dois.append( {'key': 'ref' + str(n_entry), 'doi': entry.partition('}')[0], } ) n_entry += 1 return dois def save(self, *args, **kwargs): self.instance.metadata['citation_list'] = self.extract_dois() return super().save(*args, **kwargs) class AbstractJATSForm(forms.ModelForm): abstract_jats = forms.CharField(widget=forms.Textarea({ 'placeholder': 'Paste the JATS abstract here (use pandoc to generate; see docs)'})) class Meta: model = Publication fields = () def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['abstract_jats'].initial = self.instance.abstract_jats def save(self, *args, **kwargs): self.instance.abstract_jats = self.cleaned_data['abstract_jats'] return super().save(*args, **kwargs) class FundingInfoForm(forms.ModelForm): funding_statement = forms.CharField(widget=forms.Textarea({ 'placeholder': 'Paste the funding info statement here'})) class Meta: model = Publication fields = () def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['funding_statement'].initial = self.instance.metadata.get('funding_statement') def save(self, *args, **kwargs): self.instance.metadata['funding_statement'] = self.cleaned_data['funding_statement'] return super().save(*args, **kwargs) class BasePublicationAuthorsTableFormSet(BaseModelFormSet): def save(self, *args, **kwargs): objects = super().save(*args, **kwargs) for form in self.ordered_forms: form.instance.order = form.cleaned_data['ORDER'] form.instance.save() return objects PublicationAuthorOrderingFormSet = modelformset_factory( PublicationAuthorsTable, fields=(), can_order=True, extra=0, formset=BasePublicationAuthorsTableFormSet) class AuthorsTableOrganizationSelectForm(forms.ModelForm): organization = AutoCompleteSelectField('organization_lookup') class Meta: model = PublicationAuthorsTable fields = [] class CreateMetadataXMLForm(forms.ModelForm): class Meta: model = Publication fields = ['metadata_xml'] def __init__(self, *args, **kwargs): kwargs['initial'] = { 'metadata_xml': self.new_xml(kwargs.get('instance')) } super().__init__(*args, **kwargs) def save(self, *args, **kwargs): self.instance.latest_metadata_update = timezone.now() return super().save(*args, **kwargs) def new_xml(self, publication): """ Create new XML structure, return as a string """ # Create a doi_batch_id salt = "" for i in range(5): salt = salt + random.choice(string.ascii_letters) salt = salt.encode('utf8') idsalt = publication.title[:10] idsalt = idsalt.encode('utf8') doi_batch_id = hashlib.sha1(salt+idsalt).hexdigest() funders = (Funder.objects.filter(grants__in=publication.grants.all()) | publication.funders_generic.all()).distinct() # Render from template template = loader.get_template('xml/publication_crossref.html') context = { 'publication': publication, 'doi_batch_id': doi_batch_id, 'deposit_email': settings.CROSSREF_DEPOSIT_EMAIL, 'funders': funders, } return template.render(context) class CreateMetadataDOAJForm(forms.ModelForm): class Meta: model = Publication fields = ['metadata_DOAJ'] def __init__(self, *args, **kwargs): self.request = kwargs.pop('request') kwargs['initial'] = { 'metadata_DOAJ': self.generate(kwargs.get('instance')) } super().__init__(*args, **kwargs) def generate(self, publication): if publication.in_issue: issn = str(publication.in_issue.in_volume.in_journal.issn) else: issn = str(publication.in_journal.issn) md = { 'bibjson': { 'author': [{'name': publication.author_list}], 'title': publication.title, 'abstract': publication.abstract, 'year': publication.publication_date.strftime('%Y'), 'month': publication.publication_date.strftime('%m'), 'identifier': [ { 'type': 'eissn', 'id': issn }, { 'type': 'doi', 'id': publication.doi_string } ], 'link': [ { 'url': self.request.build_absolute_uri(publication.get_absolute_url()), 'type': 'fulltext', } ], } } if publication.in_issue: md['bibjson']['journal'] = { 'publisher': 'SciPost', 'volume': str(publication.in_issue.in_volume.number), 'number': str(publication.in_issue.number), 'start_page': publication.get_paper_nr(), 'identifier': [{ 'type': 'eissn', 'id': issn }], 'license': [ { 'url': self.request.build_absolute_uri( publication.in_issue.in_volume.in_journal.get_absolute_url()), 'open_access': 'true', 'type': publication.get_cc_license_display(), 'title': publication.get_cc_license_display(), } ], 'language': ['EN'], 'title': publication.in_issue.in_volume.in_journal.name, } else: md['bibjson']['journal'] = { 'publisher': 'SciPost', 'start_page': publication.get_paper_nr(), 'identifier': [{ 'type': 'eissn', 'id': issn }], 'license': [ { 'url': self.request.build_absolute_uri( publication.in_journal.get_absolute_url()), 'open_access': 'true', 'type': publication.get_cc_license_display(), 'title': publication.get_cc_license_display(), } ], 'language': ['EN'], 'title': publication.in_journal.name, } return md class BaseReferenceFormSet(BaseModelFormSet): """ BaseReferenceFormSet is used to help fill the Reference list for Publications It is required to add the required keyword argument `publication` to this FormSet. """ initial_references = [] def __init__(self, *args, **kwargs): self.publication = kwargs.pop('publication') extra = kwargs.pop('extra') self.extra = int(extra if extra else '0') kwargs['form_kwargs'] = {'publication': self.publication} super().__init__(*args, **kwargs) def prefill(self): citations = self.publication.metadata.get('citation_list', []) for cite in citations: caller = DOICaller(cite['doi']) if caller.is_valid: # Authors author_list = [] for author in caller._crossref_data['author'][:3]: try: author_list.append('{}. {}'.format(author['given'][0], author['family'])) except KeyError: author_list.append(author['name']) if len(author_list) > 2: authors = ', '.join(author_list[:-1]) authors += ' and ' + author_list[-1] else: authors = ' and '.join(author_list) # Citation citation = '<em>{}</em> {} <b>{}</b>, {} ({})'.format( caller.data['title'], caller.data['journal'], caller.data['volume'], caller.data['pages'], datetime.strptime(caller.data['pub_date'], '%Y-%m-%d').year) self.initial_references.append({ 'reference_number': cite['key'][3:], 'authors': authors, 'citation': citation, 'identifier': cite['doi'], 'link': 'https://doi.org/{}'.format(cite['doi']), }) else: self.initial_references.append({ 'reference_number': cite['key'][3:], 'identifier': cite['doi'], 'link': 'https://doi.org/{}'.format(cite['doi']), }) # Add prefill information to the form if not self.initial_extra: self.initial_extra = self.initial_references else: self.initial_extra.extend(self.initial_references) self.extra += len(self.initial_extra) class ReferenceForm(forms.ModelForm): class Meta: model = Reference fields = [ 'reference_number', 'authors', 'citation', 'identifier', 'link', ] def __init__(self, *args, **kwargs): self.publication = kwargs.pop('publication') super().__init__(*args, **kwargs) def save(self, *args, **kwargs): self.instance.publication = self.publication super().save(*args, **kwargs) ReferenceFormSet = modelformset_factory(Reference, formset=BaseReferenceFormSet, form=ReferenceForm, can_delete=True) class DraftPublicationForm(forms.ModelForm): """ This Form is used by the Production Supervisors to create a new Publication object and prefill all data. It is only able to create a `draft` version of a Publication object. """ class Meta: model = Publication fields = [ 'doi_label', 'pdf_file', 'in_issue', 'paper_nr', 'title', 'author_list', 'abstract', 'discipline', 'subject_area', 'secondary_areas', 'approaches', 'cc_license', 'BiBTeX_entry', 'submission_date', 'acceptance_date', 'publication_date'] def __init__(self, data=None, identifier_w_vn_nr=None, issue_id=None, *args, **kwargs): # Use separate instance to be able to prefill the form without any existing Publication self.submission = None self.issue = None self.to_journal = None if identifier_w_vn_nr: try: self.submission = Submission.objects.accepted().get( preprint__identifier_w_vn_nr=identifier_w_vn_nr) except Submission.DoesNotExist: self.submission = None # Check if the Submission is related to a Journal with individual Publications only if self.submission: try: self.to_journal = Journal.objects.has_individual_publications().get( name=self.submission.editorial_decision.for_journal.name) except Journal.DoesNotExist: self.to_journal = None # If the Journal is not for individual publications, choose a Issue for Publication if issue_id and not self.to_journal: try: self.issue = self.get_possible_issues().get(id=issue_id) except Issue.DoesNotExist: self.issue = None super().__init__(data, *args, **kwargs) if kwargs.get('instance') or self.issue or self.to_journal: # When updating: fix in_issue, because many fields are directly related to the issue. del self.fields['in_issue'] self.prefill_fields() else: self.fields['in_issue'].queryset = self.get_possible_issues() self.delete_secondary_fields() def get_possible_issues(self): issues = Issue.objects.filter(until_date__gte=timezone.now()) if self.submission: issues = issues.for_journal(self.submission.editorial_decision.for_journal.name) return issues def delete_secondary_fields(self): """ Delete fields from the self.fields dictionary. Later on, this submitted sparse form can be used to prefill these secondary fields. """ del self.fields['doi_label'] del self.fields['pdf_file'] del self.fields['paper_nr'] del self.fields['title'] del self.fields['author_list'] del self.fields['abstract'] del self.fields['discipline'] del self.fields['subject_area'] del self.fields['secondary_areas'] del self.fields['approaches'] del self.fields['cc_license'] del self.fields['BiBTeX_entry'] del self.fields['submission_date'] del self.fields['acceptance_date'] del self.fields['publication_date'] def clean(self): data = super().clean() if not self.instance.id: if self.submission: self.instance.accepted_submission = self.submission if self.issue: self.instance.in_issue = self.issue if self.to_journal: self.instance.in_journal = self.to_journal return data def save(self, *args, **kwargs): """ Save the Publication object always as a draft and prefill the Publication with related Submission data only when appending the Publication. """ do_prefill = False if not self.instance.id: do_prefill = True super().save(*args, **kwargs) if do_prefill: self.first_time_fill() return self.instance def first_time_fill(self): """ Take over fields from related Submission object. This can only be done after the Publication object has been added to the database due to m2m relations. """ self.instance.status = STATUS_DRAFT if self.submission: # Copy all existing author and non-author relations to Publication for submission_author in self.submission.authors.all(): PublicationAuthorsTable.objects.create( publication=self.instance, profile=submission_author.profile) self.instance.topics.add(*self.submission.topics.all()) self.instance.authors_claims.add(*self.submission.authors_claims.all()) self.instance.authors_false_claims.add(*self.submission.authors_false_claims.all()) def prefill_fields(self): if self.submission: self.fields['title'].initial = self.submission.title self.fields['author_list'].initial = self.submission.author_list self.fields['abstract'].initial = self.submission.abstract self.fields['discipline'].initial = self.submission.discipline self.fields['subject_area'].initial = self.submission.subject_area self.fields['secondary_areas'].initial = self.submission.secondary_areas self.fields['approaches'].initial = self.submission.approaches self.fields['submission_date'].initial = self.submission.submission_date self.fields['acceptance_date'].initial = self.submission.acceptance_date self.fields['publication_date'].initial = timezone.now() # Fill data for Publications grouped by Issues (or Issue+Volume). if hasattr(self.instance, 'in_issue') and self.instance.in_issue: self.issue = self.instance.in_issue if self.issue: self.prefill_with_issue(self.issue) # Fill data for Publications ungrouped; directly linked to a Journal. if hasattr(self.instance, 'in_journal') and self.instance.in_journal: self.to_journal = self.instance.in_issue if self.to_journal: self.prefill_with_journal(self.to_journal) def prefill_with_issue(self, issue): # Determine next available paper number: if issue.in_volume: # Issue/Volume paper_nr = Publication.objects.filter(in_issue__in_volume=issue.in_volume).count() + 1 elif issue.in_journal: # Issue only paper_nr = Publication.objects.filter(in_issue=issue).count() + 1 if paper_nr > 999: raise PaperNumberingError(paper_nr) self.fields['paper_nr'].initial = str(paper_nr) if issue.in_volume: doi_label = '{journal}.{vol}.{issue}.{paper}'.format( journal=issue.in_volume.in_journal.doi_label, vol=issue.in_volume.number, issue=issue.number, paper=str(paper_nr).rjust(3, '0')) elif issue.in_journal: doi_label = '{journal}.{issue}.{paper}'.format( journal=issue.in_journal.doi_label, issue=issue.number, paper=str(paper_nr).rjust(3, '0')) self.fields['doi_label'].initial = doi_label doi_string = '10.21468/{doi}'.format(doi=doi_label) # Initiate a BibTex entry bibtex_entry = ( '@Article{%s,\n' '\ttitle={{%s}},\n' '\tauthor={%s},\n' ) % ( doi_string, self.submission.title, self.submission.author_list.replace(',', ' and')) if issue.in_volume: bibtex_entry += '\tjournal={%s},\n\tvolume={%i},\n' % ( issue.in_volume.in_journal.name_abbrev, issue.in_volume.number) elif issue.in_journal: bibtex_entry += '\tjournal={%s},\n' % (issue.in_journal.name_abbrev) bibtex_entry += ( '\tissue={%i},\n' '\tpages={%i},\n' '\tyear={%s},\n' '\tpublisher={SciPost},\n' '\tdoi={%s},\n' '\turl={https://scipost.org/%s},\n' '}' ) % ( issue.number, paper_nr, issue.until_date.strftime('%Y'), doi_string, doi_string) self.fields['BiBTeX_entry'].initial = bibtex_entry if not self.instance.BiBTeX_entry: self.instance.BiBTeX_entry = bibtex_entry def prefill_with_journal(self, journal): # Determine next available paper number: paper_nr = journal.publications.count() + 1 self.fields['paper_nr'].initial = str(paper_nr) doi_label = '{journal}.{paper}'.format( journal=journal.doi_label, paper=paper_nr) self.fields['doi_label'].initial = doi_label doi_string = '10.21468/{doi}'.format(doi=doi_label) bibtex_entry = ( '@Article{%s,\n' '\ttitle={{%s}},\n' '\tauthor={%s},\n' '\tjournal={%s},\n' '\tpages={%i},\n' '\tyear={%s},\n' '\tpublisher={SciPost},\n' '\tdoi={%s},\n' '\turl={https://scipost.org/%s},\n' '}' ) % ( doi_string, self.submission.title, self.submission.author_list.replace(',', ' and'), journal.name_abbrev, paper_nr, timezone.now().year, doi_string, doi_string) self.fields['BiBTeX_entry'].initial = bibtex_entry if not self.instance.BiBTeX_entry: self.instance.BiBTeX_entry = bibtex_entry class DraftPublicationApprovalForm(forms.ModelForm): class Meta: model = Publication fields = () def save(self, commit=True): self.instance.status = PUBLICATION_PREPUBLISHED if commit: self.instance.save() mail_sender = DirectMailUtil('publication_ready', instance=self.instance) mail_sender.send_mail() return self.instance class PublicationGrantsForm(forms.ModelForm): grant = forms.ModelChoiceField(queryset=Grant.objects.none()) class Meta: model = Publication fields = [] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['grant'].queryset = Grant.objects.exclude( id__in=self.instance.grants.values_list('id', flat=True)) def save(self, commit=True): if commit: self.instance.grants.add(self.cleaned_data['grant']) return self.instance class PublicationPublishForm(RequestFormMixin, forms.ModelForm): class Meta: model = Publication fields = [] def move_pdf(self): """ To keep the Publication pdfs organized we move the pdfs to their own folder organized by journal and optional issue folder. """ initial_path = self.instance.pdf_file.path new_dir = '' if self.instance.in_issue: new_dir += self.instance.in_issue.path elif self.instance.in_journal: new_dir += 'SCIPOST_JOURNALS/{name}'.format(name=self.instance.in_journal.doi_label) new_dir += '/{paper_nr}'.format(paper_nr=self.instance.get_paper_nr()) os.makedirs(settings.MEDIA_ROOT + new_dir, exist_ok=True) new_dir += '/{doi}.pdf'.format(doi=self.instance.doi_label.replace('.', '_')) os.rename(initial_path, settings.MEDIA_ROOT + new_dir) self.instance.pdf_file.name = new_dir self.instance.status = PUBLICATION_PUBLISHED self.instance.save() def update_submission(self): # Mark the submission as having been published: submission = self.instance.accepted_submission submission.published_as = self.instance submission.status = STATUS_PUBLISHED submission.save() # Add SubmissionEvents submission.add_general_event( 'The Submission has been published as %s.' % self.instance.doi_label) def update_stream(self): # Update ProductionStream submission = self.instance.accepted_submission if hasattr(submission, 'production_stream'): stream = submission.production_stream stream.status = PROOFS_PUBLISHED stream.save() if self.request.user.production_user: prodevent = ProductionEvent( stream=stream, event='status', comments=' published the manuscript.', noted_by=self.request.user.production_user ) prodevent.save() notify_stream_status_change(self.request.user, stream, False) def save(self, commit=True): if commit: self.move_pdf() self.update_submission() self.update_stream() # Email authors JournalUtils.load({'publication': self.instance}) JournalUtils.send_authors_paper_published_email() notify_manuscript_published(self.request.user, self.instance, False) return self.instance class VolumeForm(forms.ModelForm): """ Add or Update a Volume instance which is directly related to either a Journal. """ class Meta: model = Volume fields = ( 'in_journal', 'start_date', 'until_date', 'doi_label', ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.instance.id: del self.fields['doi_label'] def save(self): if self.instance.id: # Use regular save method if updating existing instance. return super().save() # Obtain next number, path and DOI if creating new Issue. volume = super().save(commit=False) volume.number = volume.in_journal.volumes.count() + 1 volume.doi_label = '{}.{}'.format(volume.in_journal.doi_label, volume.number) volume.save() return volume class IssueForm(forms.ModelForm): """ Add or Update an Issue instance which is directly related to either a Journal or a Volume. """ class Meta: model = Issue fields = ( 'in_journal', 'in_volume', 'start_date', 'until_date', 'status', 'doi_label', ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.instance.id: del self.fields['doi_label'] def save(self): if self.instance.id: # Use regular save method if updating existing instance. return super().save() # Obtain next number, path and DOI if creating new Issue. issue = super().save(commit=False) journal = issue.get_journal() path = settings.JOURNALS_DIR if journal.has_volumes: volume = journal.volumes.first() number = volume.issues.count() + 1 doi = '{}.{}.{}'.format(journal.doi_label, volume.number, number) path += '/{}/{}/{}'.format(journal.doi_label, volume.number, number) else: if self.cleaned_data['status'] == STATUS_PUBLICLY_OPEN: # Temporary park this issue with a number generated from the timestamp # This is useful for e.g. Proceedings, which only get their final # number when published (not when they are made publicly open for submission). # The format is [YYYY][MM][3-digit code] # where the next-available 3-digit code is found number_check = 1000 * int(timezone.now().strftime('%Y%m')) number = number_check + journal.issues.filter(number__gt=number_check).count() + 1 else: number = journal.issues.count() + 1 volume = None doi = '{}.{}'.format(journal.doi_label, number) path += '/{}/{}'.format(journal.doi_label, number) issue.number = number issue.slug = str(number) issue.doi_label = doi issue.path = path issue.save() return issue class SetOrgPubFractionForm(forms.ModelForm): class Meta: model = OrgPubFraction fields = ['organization', 'publication', 'fraction'] def __init__(self, *args, **kwargs): super(SetOrgPubFractionForm, self).__init__(*args, **kwargs) if self.instance.id: self.fields['organization'].disabled = True self.fields['publication'].widget = forms.HiddenInput() class BaseOrgPubFractionsFormSet(BaseModelFormSet): def clean(self): """ Checks that the fractions add up to one. """ norm = 0 for form in self.forms: form.is_valid() norm += 1000 * form.cleaned_data.get('fraction', 0) if norm != 1000: raise forms.ValidationError('The fractions do not add up to one!') OrgPubFractionsFormSet = modelformset_factory(OrgPubFraction, fields=('publication', 'organization', 'fraction'), formset=BaseOrgPubFractionsFormSet, form=SetOrgPubFractionForm, extra=0)