Skip to content

Commit 096ec6c

Browse files
author
Rose Reatherford
committed
Merge branch 'develop' into 'main'
Add Deployment Trigger See merge request plos/editorial_manager_transfer_service!14
2 parents 05fe7b0 + 06ce2e4 commit 096ec6c

37 files changed

Lines changed: 1866 additions & 528 deletions

.gitlab-ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
trigger_pipeline_dev:
2+
stage: deploy
3+
script:
4+
- 'curl --fail --request POST --form token=$TRIGGER_TOKEN --form ref=develop "${CI_API_V4_URL}/projects/67747905/trigger/pipeline"'
5+
rules:
6+
- if: $CI_PIPELINE_SOURCE == "schedule"
7+
when: never
8+
- if: $CI_COMMIT_BRANCH == "develop"
9+
environment: develop
10+
11+
trigger_pipeline_prod:
12+
stage: deploy
13+
script:
14+
- 'curl --fail --request POST --form token=$TRIGGER_TOKEN --form ref=main "${CI_API_V4_URL}/projects/67747905/trigger/pipeline"'
15+
rules:
16+
- if: $CI_PIPELINE_SOURCE == "schedule"
17+
when: never
18+
- if: $CI_COMMIT_BRANCH == "main"
19+
environment: prod

consts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,5 @@
5050
GO_FILE_ELEMENT_TAG_FILEGROUP = "filegroup"
5151
GO_FILE_ELEMENT_TAG_FILE = "file"
5252
GO_FILE_ELEMENT_TAG_METADATA_FILE = "metadata-file"
53+
54+
JATS_XML_FILE = 'editorial_manager_transfer_service/encoding/article_jats_1_2_aries.xml'

dev-requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
hypothesis==6.138.7
2-
pytest==8.4.1
3-
python-magic==0.4.27
2+
python-magic==0.4.27
3+
lxml

enums/report_state.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
A file for tracking the transfer log message types.
3+
"""
4+
__author__ = "Rosetta Reatherford"
5+
__license__ = "AGPL v3"
6+
__maintainer__ = "The Public Library of Science (PLOS)"
7+
8+
import django.db.models as models
9+
from django.utils.translation import gettext_lazy as _
10+
11+
class ReportState(models.TextChoices):
12+
NORMAL = "000", _("No Error Detected")
13+
IN_FLIGHT = "001", _("Article is in Flight")
14+
FAILED_BUNDLING = "002", _("Failed Bundling")
15+
FAILED_INGEST = "003", _("Editorial Manager Rejected at SFTP")

file_exporter.py

Lines changed: 80 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,26 @@
88
import os
99
import uuid
1010
import xml.etree.cElementTree as ETree
11-
import zipfile
1211
from collections.abc import Sequence
1312
from typing import List
1413

15-
from django.core.exceptions import ObjectDoesNotExist
14+
from janeway_ftp import helpers as deposit_helpers
1615

1716
import plugins.editorial_manager_transfer_service.consts as consts
1817
import plugins.editorial_manager_transfer_service.logger_messages as logger_messages
1918
from core.models import File
2019
from journal.models import Journal
20+
from plugins.editorial_manager_transfer_service.enums.report_state import ReportState
2121
from plugins.editorial_manager_transfer_service.enums.transfer_log_message_type import TransferLogMessageType
2222
from plugins.editorial_manager_transfer_service.models import TransferLogs
23+
from plugins.editorial_manager_transfer_service.utils.jats import get_xml_license_code, generate_jats_metadata
24+
from plugins.editorial_manager_transfer_service.utils.settings import get_license_code, get_submission_partner_code, \
25+
get_journal_code
26+
from plugins.editorial_manager_transfer_service.utils.transfer_report import get_or_create_transfer_report, \
27+
resolve_transfer_report
28+
from plugins.production_transporter.utilities import data_fetch
29+
from plugins.production_transporter.utilities.file_utils import copy_files_to_temp_deposit_folder
2330
from submission.models import Article
24-
from utils import setting_handler
2531
from utils.logger import get_logger
2632

2733
logger = get_logger(__name__)
@@ -55,6 +61,8 @@ def __init__(self, janeway_journal_code: str, article_id: int | None) -> None:
5561
self.article: Article | None = None
5662
self.journal: Journal | None = None
5763
self.export_folder: str | None = None
64+
self.xml_filepath: str | None = None
65+
self.__temp_folder: str | None = None
5866

5967
# Gets the journal
6068
self.journal: Journal | None = self.__fetch_journal(janeway_journal_code)
@@ -74,6 +82,9 @@ def __init__(self, janeway_journal_code: str, article_id: int | None) -> None:
7482
return
7583
self.export_folder = export_folders
7684

85+
# Creates or fetches a report to track where this process is.
86+
self.transfer_report = get_or_create_transfer_report(self.journal, self.article)
87+
7788
# Start export process
7889
self.__create_export_file()
7990

@@ -108,12 +119,17 @@ def __create_export_file(self):
108119
self.in_error_state = True
109120
return
110121

111-
# TODO: Attempt to create the metadata file.
112-
# metadata_file: File | None = self.__create_metadata_file(self.article)
113-
# if metadata_file is None:
114-
# logger.error(logger_messages.process_failed_fetching_metadata(self.article_id))
115-
# self.in_error_state = True
116-
# return
122+
prefix: str = "{0}_{1}".format(self.get_submission_partner_code(), uuid.uuid4())
123+
124+
self.zip_filepath: str = os.path.join(self.export_folder, "{0}.zip".format(prefix))
125+
self.__temp_folder = os.path.join(self.export_folder, "{0}".format(prefix))
126+
os.makedirs(self.__temp_folder, exist_ok=True)
127+
128+
# Attempt to get the metadata file.
129+
if self.__get_xml_filepath() is None:
130+
logger.error(logger_messages.process_failed_fetching_metadata(self.article_id))
131+
self.in_error_state = True
132+
return
117133

118134
# Attempt to fetch the article files.
119135
article_files: Sequence[File] = self.__fetch_article_files(self.article)
@@ -122,28 +138,26 @@ def __create_export_file(self):
122138
self.in_error_state = True
123139
return
124140

125-
prefix: str = "{0}_{1}".format(self.get_submission_partner_code(), uuid.uuid4())
141+
filenames: List[str] = []
126142

127-
self.zip_filepath: str = os.path.join(self.export_folder, "{0}.zip".format(prefix))
128-
with zipfile.ZipFile(self.zip_filepath, "w") as zipf:
129-
# TODO: zipf.write(metadata_file.get_file_path(self.article))
130-
for article_file in article_files:
131-
zipf.write(article_file.get_file_path(self.article))
132-
filenames: Sequence[str] = zipf.namelist()
133-
zipf.close()
143+
# Move files to temp folder.
144+
for article_file in article_files:
145+
filepath: str = article_file.get_file_path(self.article)
146+
copy_files_to_temp_deposit_folder(filepath, self.__temp_folder)
147+
filenames.append(os.path.basename(filepath))
134148

135-
# Remove the manuscript
149+
deposit_helpers.zip_temp_folder(temp_folder=self.__temp_folder)
136150

137-
# TODO: Remove below and replace with 'self.__create_go_xml_file(metadata_file.uuid_filename, filenames, prefix)'
138-
self.__create_go_xml_file("fake name", filenames, prefix)
151+
# Remove the manuscript
152+
self.__create_go_xml_file(os.path.basename(self.__get_xml_filepath()), filenames, prefix)
139153

140154
def get_license_code(self) -> str:
141155
"""
142156
Gets the license code for exporting files.
143157
:return: The license code or None, if the process failed.
144158
"""
145159
if not self.__license_code:
146-
self.__license_code: str = self.get_setting(consts.PLUGIN_SETTINGS_LICENSE_CODE)
160+
self.__license_code: str = get_license_code(self.journal)
147161
return self.__license_code
148162

149163
def get_journal_code(self) -> str:
@@ -152,7 +166,7 @@ def get_journal_code(self) -> str:
152166
:return: The journal code or None, if the process failed.
153167
"""
154168
if not self.__journal_code:
155-
self.__journal_code: str = self.get_setting(consts.PLUGIN_SETTINGS_JOURNAL_CODE)
169+
self.__journal_code: str = get_journal_code(self.journal)
156170
return self.__journal_code
157171

158172
def get_submission_partner_code(self) -> str:
@@ -161,7 +175,7 @@ def get_submission_partner_code(self) -> str:
161175
:return: The submission partner code or None, if the process failed.
162176
"""
163177
if not self.__submission_partner_code:
164-
self.__submission_partner_code: str = self.get_setting(consts.PLUGIN_SETTINGS_SUBMISSION_PARTNER_CODE)
178+
self.__submission_partner_code: str = get_submission_partner_code(self.journal)
165179
return self.__submission_partner_code
166180

167181
def can_export(self) -> bool:
@@ -176,20 +190,6 @@ def can_export(self) -> bool:
176190
self.get_journal_code() is not None and
177191
self.get_submission_partner_code() is not None)
178192

179-
def get_setting(self, setting_name: str) -> str:
180-
"""
181-
Gets the setting for the given setting name.
182-
:param setting_name: The name of the setting to get the value for.
183-
:return: The value for the given setting or a blank string, if the process failed.
184-
"""
185-
try:
186-
return setting_handler.get_setting(setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME,
187-
setting_name=setting_name, journal=self.journal, ).processed_value
188-
except ObjectDoesNotExist as e:
189-
self.log_error("Could not get the following setting, '{0}'".format(setting_name), e)
190-
self.in_error_state = True
191-
return ""
192-
193193
def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequence[str], filename: str):
194194
"""
195195
Creates the go xml file for the export process for Editorial Manager.
@@ -219,8 +219,7 @@ def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequen
219219
parameters: ETree.Element = ETree.SubElement(header, consts.GO_FILE_ELEMENT_TAG_PARAMETERS)
220220
parameter: ETree.Element = ETree.SubElement(parameters, consts.GO_FILE_ELEMENT_TAG_PARAMETER)
221221
parameter.set(consts.GO_FILE_ATTRIBUTE_ELEMENT_NAME_KEY, consts.GO_FILE_PARAMETER_ELEMENT_NAME_VALUE)
222-
parameter.set(consts.GO_FILE_PARAMETER_ELEMENT_VALUE_KEY,
223-
"{0}_{1}".format(self.get_submission_partner_code(), self.get_license_code()))
222+
parameter.set(consts.GO_FILE_PARAMETER_ELEMENT_VALUE_KEY, get_xml_license_code(self.journal))
224223

225224
# Begin the filegroup.
226225
filegroup: ETree.Element = ETree.SubElement(go, consts.GO_FILE_ELEMENT_TAG_FILEGROUP)
@@ -239,13 +238,20 @@ def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequen
239238
self.go_filepath = os.path.join(self.export_folder, "{0}.go.xml".format(filename))
240239
tree.write(self.go_filepath)
241240

242-
def __create_metadata_file(self, article: Article) -> File | None:
241+
def __get_xml_filepath(self) -> str | None:
243242
"""
244243
Creates the metadata file based on the given article.
245-
:param article: The article to convert to JATS.
246-
:return:
244+
:return:The filepath to the JATS XML file.
247245
"""
248-
pass
246+
if not self.xml_filepath:
247+
filepath = generate_jats_metadata(self.journal, self.article, self.__temp_folder)
248+
249+
if not filepath:
250+
self.in_error_state = True
251+
return None
252+
253+
self.xml_filepath = filepath
254+
return self.xml_filepath
249255

250256
def __fetch_article(self, journal: Journal | None, article_id: int | None) -> Article | None:
251257
"""
@@ -254,22 +260,14 @@ def __fetch_article(self, journal: Journal | None, article_id: int | None) -> Ar
254260
:param article_id: The ID of the article.
255261
:return: The article object with the given article ID.
256262
"""
257-
# If no article ID or journal, return an error.
258-
if not article_id or article_id <= 0:
259-
self.log_error(logger_messages.process_failed_no_article_id_provided())
260-
self.in_error_state = True
261-
return None
262-
263-
article: Article | None = None
264-
265263
logger.debug(logger_messages.process_fetching_article(article_id))
266-
try:
267-
article = Article.get_article(journal, "id", article_id)
268-
logger.debug(logger_messages.process_finished_fetching_article(article_id))
269-
except Article.DoesNotExist as e:
270-
self.log_error(logger_messages.process_failed_fetching_article(article_id), e)
264+
265+
article: Article | None = data_fetch.fetch_article(journal, article_id)
266+
if article is None:
271267
self.in_error_state = True
268+
return None
272269

270+
logger.debug(logger_messages.process_finished_fetching_article(article_id))
273271
return article
274272

275273
def __fetch_journal(self, janeway_journal_code: str | None) -> Journal | None:
@@ -278,43 +276,51 @@ def __fetch_journal(self, janeway_journal_code: str | None) -> Journal | None:
278276
:param janeway_journal_code: The code of the Janeway journal to fetch.
279277
:return: The journal object with the given Janeway journal code, if there is one. None otherwise.
280278
"""
281-
# If no journal code, return an error.
282-
if not janeway_journal_code or len(janeway_journal_code) <= 0:
283-
self.log_error(logger_messages.process_failed_no_janeway_journal_code_provided())
284-
self.in_error_state = True
285-
return None
286-
287-
journal: Journal | None = None
288-
289279
# Attempt to get the journal.
290280
logger.debug(logger_messages.process_fetching_journal(janeway_journal_code))
291-
try:
292-
journal = Journal.objects.get(code=janeway_journal_code)
293-
logger.debug(logger_messages.process_finished_fetching_journal(janeway_journal_code))
294-
except Journal.DoesNotExist as e:
295-
self.log_error(logger_messages.process_failed_fetching_journal(janeway_journal_code), e)
281+
282+
journal: Journal | None = data_fetch.fetch_journal_data(janeway_journal_code)
283+
if journal is None:
296284
self.in_error_state = True
285+
return None
286+
287+
logger.debug(logger_messages.process_finished_fetching_journal(janeway_journal_code))
297288

298289
return journal
299290

300-
def log_error(self, message: str, error: Exception = None) -> None:
291+
def log_error(self, message: str, error: Exception = None,
292+
stage: ReportState = ReportState.FAILED_BUNDLING) -> None:
301293
"""
302294
Logs the given error message in both the database and plaintext logs.
303295
:param message: The message to log.
304296
:param error: The exception, if there is one.
297+
:param stage: Specify which stage this transfer is in.
305298
"""
306299
logger.exception(error)
307300
logger.error(message)
308-
TransferLogs.objects.create(journal=self.journal, article=self.article, message=message,
301+
if self.transfer_report.report_state != stage:
302+
self.transfer_report.report_state = stage
303+
self.transfer_report.save()
304+
TransferLogs.objects.create(report=self.transfer_report, journal=self.journal, article=self.article,
305+
message=message,
309306
message_type=TransferLogMessageType.EXPORT, success=False)
310307

311-
def log_success(self) -> None:
308+
def log_success_go_file(self) -> None:
309+
"""
310+
Logs a success message in both the database and plaintext logs.
311+
"""
312+
TransferLogs.objects.create(report=self.transfer_report, journal=self.journal, article=self.article,
313+
message=logger_messages.export_go_file_process_succeeded(self.article_id),
314+
message_type=TransferLogMessageType.EXPORT, success=True)
315+
316+
def log_success_zip_file(self) -> None:
312317
"""
313318
Logs a success message in both the database and plaintext logs.
314319
"""
315-
TransferLogs.objects.create(journal=self.journal, article=self.article,
316-
message=logger_messages.export_process_succeeded(self.article_id),
320+
TransferLogs.objects.create(report=self.transfer_report, journal=self.journal, article=self.article,
321+
message=logger_messages.export_zip_file_process_succeeded(self.article_id),
317322
message_type=TransferLogMessageType.EXPORT, success=True)
323+
resolve_transfer_report(self.transfer_report)
318324

319325
@staticmethod
320326
def __fetch_article_files(article: Article) -> List[File]:

0 commit comments

Comments
 (0)