From e1bf8fdf8e94aaa74c14093d6b22034e9021db5c Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Fri, 5 Sep 2025 21:30:28 -0500 Subject: [PATCH 1/6] Make minor consistency tweaks. --- consts.py | 4 ++++ dev-requirements.txt | 3 +-- forms.py | 4 ++++ logic.py | 4 ++++ .../editorial_manager_transfer_service/manager.html | 12 +++++++----- urls.py | 4 ++++ views.py | 4 ++++ 7 files changed, 28 insertions(+), 7 deletions(-) diff --git a/consts.py b/consts.py index 6d84160..ba2d444 100644 --- a/consts.py +++ b/consts.py @@ -1,3 +1,7 @@ +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + import os from django.conf import settings diff --git a/dev-requirements.txt b/dev-requirements.txt index 2da04d8..c6a47fd 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1 @@ -hypothesis==6.138.7 -pytest==8.4.1 \ No newline at end of file +hypothesis==6.138.7 \ No newline at end of file diff --git a/forms.py b/forms.py index 6cb0577..59ac6bf 100644 --- a/forms.py +++ b/forms.py @@ -1,3 +1,7 @@ +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + from django import forms diff --git a/logic.py b/logic.py index aa8dc4e..71659fc 100644 --- a/logic.py +++ b/logic.py @@ -1,3 +1,7 @@ +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + import plugins.editorial_manager_transfer_service.consts as consts from journal.models import Journal from utils import setting_handler diff --git a/templates/editorial_manager_transfer_service/manager.html b/templates/editorial_manager_transfer_service/manager.html index 5752626..a42675b 100644 --- a/templates/editorial_manager_transfer_service/manager.html +++ b/templates/editorial_manager_transfer_service/manager.html @@ -8,11 +8,13 @@
{% csrf_token %}
-
-

Configuration

-
-
- {{ form|foundation }} +
+
+

Editorial Manager Transfer Service Configuration

+
+
+ {{ form|foundation }} +
diff --git a/urls.py b/urls.py index 2bf794c..9b43176 100644 --- a/urls.py +++ b/urls.py @@ -1,3 +1,7 @@ +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + from django.urls import re_path from plugins.editorial_manager_transfer_service import views diff --git a/views.py b/views.py index 2e25840..5baf3ca 100644 --- a/views.py +++ b/views.py @@ -1,3 +1,7 @@ +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + from django.contrib.admin.views.decorators import staff_member_required from django.shortcuts import render From 036e99b2f82abe0ae201a1bcabd62fc791739d67 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Fri, 5 Sep 2025 21:48:38 -0500 Subject: [PATCH 2/6] Improve code quality of file exporter. --- file_exporter.py | 76 +++++++++++++++++++++++++++++++--------------- logger_messages.py | 45 +++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 25 deletions(-) diff --git a/file_exporter.py b/file_exporter.py index 9310934..55255ec 100644 --- a/file_exporter.py +++ b/file_exporter.py @@ -12,11 +12,10 @@ from collections.abc import Sequence from typing import List -from django.core.exceptions import ObjectDoesNotExist - import plugins.editorial_manager_transfer_service.consts as consts import plugins.editorial_manager_transfer_service.logger_messages as logger_messages from core.models import File +from django.core.exceptions import ObjectDoesNotExist from journal.models import Journal from submission.models import Article from utils import setting_handler @@ -42,7 +41,7 @@ class ExportFileCreation: A class for managing the export file creation process. """ - def __init__(self, journal_code: str, article_id: str): + def __init__(self, janeway_journal_code: str, article_id: str): self.zip_filepath: str | None = None self.go_filepath: str | None = None self.in_error_state: bool = False @@ -54,29 +53,14 @@ def __init__(self, journal_code: str, article_id: str): self.journal: Journal | None = None self.export_folder: str | None = None - # If no article ID, return an error. - if not self.article_id or len(self.article_id) <= 0: - logger.error(logger_messages.process_failed_no_article_id_provided()) - self.in_error_state = True - return - - # Attempt to get the journal. - try: - self.journal: Journal = Journal.objects.get(code=journal_code) - except Journal.DoesNotExist: - logger.error(logger_messages.process_failed_fetching_journal(article_id)) - self.in_error_state = True + # Gets the journal + self.journal: Journal | None = self.__fetch_journal(janeway_journal_code) + if self.in_error_state: return # Get the article based upon the given article ID. - logger.info(logger_messages.process_fetching_article(article_id)) - try: - self.article: Article = self.__fetch_article(self.journal, article_id) - if not self.article: - raise Article.DoesNotExist - except Article.DoesNotExist: - logger.error(logger_messages.process_failed_fetching_article(article_id)) - self.in_error_state = True + self.article: Article | None = self.__fetch_article(self.journal, article_id) + if self.in_error_state: return # Get the export folder. @@ -261,14 +245,56 @@ def __create_metadata_file(self, article: Article) -> File | None: pass @staticmethod - def __fetch_article(journal: Journal, article_id: str) -> Article: + def __fetch_article(journal: Journal | None, article_id: str | None) -> Article | None: """ Gets the article object for the given article ID. :param journal: The journal to fetch the article from. :param article_id: The ID of the article. :return: The article object with the given article ID. """ - return Article.get_article(journal, "id", article_id) + # If no article ID or journal, return an error. + if not article_id or len(article_id) <= 0: + logger.error(logger_messages.process_failed_no_article_id_provided()) + self.in_error_state = True + return None + + article: Article | None = None + + logger.debug(logger_messages.process_fetching_article(article_id)) + try: + article = Article.get_article(journal, "id", article_id) + logger.debug(logger_messages.process_finished_fetching_article(article_id)) + except Article.DoesNotExist: + logger.error(logger_messages.process_failed_fetching_article(article_id)) + self.in_error_state = True + + return article + + @staticmethod + def __fetch_journal(janeway_journal_code: str | None) -> Journal | None: + """ + Gets the journal from the database given the Janeway journal code. + :param janeway_journal_code: The code of the Janeway journal to fetch. + :return: The journal object with the given Janeway journal code, if there is one. None otherwise. + """ + # If no journal code, return an error. + if not janeway_journal_code or len(janeway_journal_code) <= 0: + logger.error(logger_messages.process_failed_no_janeway_journal_code_provided()) + self.in_error_state = True + return None + + journal: Journal | None = None + + # Attempt to get the journal. + logger.debug(logger_messages.process_fetching_journal(janeway_journal_code)) + try: + journal = Journal.objects.get(code=journal_code) + logger.debug(logger_messages.process_finished_fetching_journal(janeway_journal_code)) + except Journal.DoesNotExist: + logger.error(logger_messages.process_failed_fetching_journal(janeway_journal_code)) + self.in_error_state = True + + return journal @staticmethod def __fetch_article_files(article: Article) -> List[File]: diff --git a/logger_messages.py b/logger_messages.py index 0b0e489..1d5ae10 100644 --- a/logger_messages.py +++ b/logger_messages.py @@ -73,6 +73,15 @@ def process_fetching_article(article_id: str) -> str: return "Fetching article from database (ID: {0})...".format(article_id) +def process_finished_fetching_article(article_id: str) -> str: + """ + Gets the log message for when an article is being fetched from the database. + :param: article_id: The ID of the article being fetched. + :return: The logger message. + """ + return "Completed fetching article from database (ID: {0})...".format(article_id) + + def process_failed_fetching_article(article_id: str) -> str: """ Gets the log message for when an article failed to be fetched. @@ -82,6 +91,34 @@ def process_failed_fetching_article(article_id: str) -> str: return "Fetching article from database (ID: {0}) failed. Discontinuing export process.".format(article_id) +def process_fetching_journal(janway_journal_code: str) -> str: + """ + Gets the log message for when a journal is being fetched from the database. + :param: janway_journal_code: The code of the journal being fetched. + :return: The logger message. + """ + return "Fetching journal from database (Code: {0})...".format(janway_journal_code) + + +def process_finished_fetching_journal(janway_journal_code: str) -> str: + """ + Gets the log message for when a journal is being fetched from the database. + :param: janway_journal_code: The code of the journal being fetched. + :return: The logger message. + """ + return "Completed fetching journal from database (Code: {0})...".format(janway_journal_code) + + +def process_failed_fetching_journal(janway_journal_code: str) -> str: + """ + Gets the log message for when a journal failed to be fetched. + :param: janway_journal_code: The code of the journal being fetched. + :return: The logger message. + """ + return "Fetching journal from database (Code: {0}) failed. Discontinuing export process.".format( + janway_journal_code) + + def process_failed_fetching_metadata(article_id) -> str: """ Gets the log message for when an article's metadata failed to be fetched. @@ -117,6 +154,14 @@ def process_failed_no_article_id_provided() -> str: return "No article ID provided. Discontinuing export process." +def process_failed_no_janeway_journal_code_provided() -> str: + """ + Gets the log message for when no journal code was provided. + :return: The logger message. + """ + return "No Janeway journal code was provided. Discontinuing export process." + + def export_process_failed_no_export_folder() -> str: """ Gets the log message for when an export folder was not created. From a3ae10745daf9fc8fac73621d859b7adc40b1dda Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Sat, 6 Sep 2025 00:01:18 -0500 Subject: [PATCH 3/6] Add logging tables which track the success or failure of article exports. --- dev-requirements.txt | 4 ++- enums/__init__.py | 0 enums/transfer_log_message_type.py | 13 +++++++++ file_exporter.py | 43 +++++++++++++++++++++--------- file_transfer_service.py | 39 +++++++++++++++++++++++++++ logger_messages.py | 14 ++++++++++ models.py | 36 +++++++++++++++++++++++++ tests/test_file_creation.py | 41 +++++++++++++++++++--------- 8 files changed, 163 insertions(+), 27 deletions(-) create mode 100644 enums/__init__.py create mode 100644 enums/transfer_log_message_type.py create mode 100644 models.py diff --git a/dev-requirements.txt b/dev-requirements.txt index c6a47fd..64e5c6c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1 +1,3 @@ -hypothesis==6.138.7 \ No newline at end of file +hypothesis==6.138.7 +pytest==8.4.1 +python-magic==0.4.27 \ No newline at end of file diff --git a/enums/__init__.py b/enums/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/enums/transfer_log_message_type.py b/enums/transfer_log_message_type.py new file mode 100644 index 0000000..d33a6eb --- /dev/null +++ b/enums/transfer_log_message_type.py @@ -0,0 +1,13 @@ +""" +A file for tracking the transfer log message types. +""" +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + +import django.db.models as models +from django.utils.translation import gettext_lazy as _ + +class TransferLogMessageType(models.TextChoices): + EXPORT = "EX", _("Export Message") + IMPORT = "IM", _("Import Message") \ No newline at end of file diff --git a/file_exporter.py b/file_exporter.py index 55255ec..8cd8301 100644 --- a/file_exporter.py +++ b/file_exporter.py @@ -12,11 +12,14 @@ from collections.abc import Sequence from typing import List +from django.core.exceptions import ObjectDoesNotExist + import plugins.editorial_manager_transfer_service.consts as consts import plugins.editorial_manager_transfer_service.logger_messages as logger_messages from core.models import File -from django.core.exceptions import ObjectDoesNotExist from journal.models import Journal +from plugins.editorial_manager_transfer_service.enums.transfer_log_message_type import TransferLogMessageType +from plugins.editorial_manager_transfer_service.models import TransferLogs from submission.models import Article from utils import setting_handler from utils.logger import get_logger @@ -66,7 +69,7 @@ def __init__(self, janeway_journal_code: str, article_id: str): # Get the export folder. export_folders: str = get_article_export_folders() if len(export_folders) <= 0: - logger.error(logger_messages.export_process_failed_no_export_folder()) + self.log_error(logger_messages.export_process_failed_no_export_folder()) self.in_error_state = True return self.export_folder = export_folders @@ -115,7 +118,7 @@ def __create_export_file(self): # Attempt to fetch the article files. article_files: Sequence[File] = self.__fetch_article_files(self.article) if len(article_files) <= 0: - logger.error(logger_messages.process_failed_fetching_article_files(self.article_id)) + self.log_error(logger_messages.process_failed_fetching_article_files(self.article_id)) self.in_error_state = True return @@ -183,7 +186,7 @@ def get_setting(self, setting_name: str) -> str: return setting_handler.get_setting(setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME, setting_name=setting_name, journal=self.journal, ).processed_value except ObjectDoesNotExist: - logger.error("Could not get the following setting, '{0}'".format(setting_name)) + self.log_error("Could not get the following setting, '{0}'".format(setting_name)) self.in_error_state = True return "" @@ -244,8 +247,7 @@ def __create_metadata_file(self, article: Article) -> File | None: """ pass - @staticmethod - def __fetch_article(journal: Journal | None, article_id: str | None) -> Article | None: + def __fetch_article(self, journal: Journal | None, article_id: str | None) -> Article | None: """ Gets the article object for the given article ID. :param journal: The journal to fetch the article from. @@ -254,7 +256,7 @@ def __fetch_article(journal: Journal | None, article_id: str | None) -> Article """ # If no article ID or journal, return an error. if not article_id or len(article_id) <= 0: - logger.error(logger_messages.process_failed_no_article_id_provided()) + self.log_error(logger_messages.process_failed_no_article_id_provided()) self.in_error_state = True return None @@ -265,13 +267,12 @@ def __fetch_article(journal: Journal | None, article_id: str | None) -> Article article = Article.get_article(journal, "id", article_id) logger.debug(logger_messages.process_finished_fetching_article(article_id)) except Article.DoesNotExist: - logger.error(logger_messages.process_failed_fetching_article(article_id)) + self.log_error(logger_messages.process_failed_fetching_article(article_id)) self.in_error_state = True return article - @staticmethod - def __fetch_journal(janeway_journal_code: str | None) -> Journal | None: + def __fetch_journal(self, janeway_journal_code: str | None) -> Journal | None: """ Gets the journal from the database given the Janeway journal code. :param janeway_journal_code: The code of the Janeway journal to fetch. @@ -279,7 +280,7 @@ def __fetch_journal(janeway_journal_code: str | None) -> Journal | None: """ # If no journal code, return an error. if not janeway_journal_code or len(janeway_journal_code) <= 0: - logger.error(logger_messages.process_failed_no_janeway_journal_code_provided()) + self.log_error(logger_messages.process_failed_no_janeway_journal_code_provided()) self.in_error_state = True return None @@ -288,14 +289,30 @@ def __fetch_journal(janeway_journal_code: str | None) -> Journal | None: # Attempt to get the journal. logger.debug(logger_messages.process_fetching_journal(janeway_journal_code)) try: - journal = Journal.objects.get(code=journal_code) + journal = Journal.objects.get(code=janeway_journal_code) logger.debug(logger_messages.process_finished_fetching_journal(janeway_journal_code)) except Journal.DoesNotExist: - logger.error(logger_messages.process_failed_fetching_journal(janeway_journal_code)) + self.log_error(logger_messages.process_failed_fetching_journal(janeway_journal_code)) self.in_error_state = True return journal + def log_error(self, message: str) -> None: + """ + Logs the given error message in both the database and plaintext logs. + :param message: The message to log. + """ + logger.error(message) + TransferLogs.objects.create(journal=self.journal, article=self.article, message=message, + message_type=TransferLogMessageType.EXPORT, success=False) + + def log_success(self) -> None: + """ + Logs a success message in both the database and plaintext logs. + """ + TransferLogs.objects.create(journal=self.journal, article=self.article, message=logger_messages.export_process_succeeded(self.article_id), + message_type=TransferLogMessageType.EXPORT, success=True) + @staticmethod def __fetch_article_files(article: Article) -> List[File]: """ diff --git a/file_transfer_service.py b/file_transfer_service.py index 0a883a4..b0778d0 100644 --- a/file_transfer_service.py +++ b/file_transfer_service.py @@ -8,6 +8,7 @@ import os from typing import List +from plugins.editorial_manager_transfer_service import logger_messages from plugins.editorial_manager_transfer_service.file_exporter import ExportFileCreation from utils.logger import get_logger @@ -53,6 +54,12 @@ def get_export_file_creator(self, journal_code: str, article_id: str) -> ExportF @staticmethod def __get_dictionary_identifier(journal_code: str, article_id: str) -> str: + """ + Gets the dictionary identifier for the given article. + :param journal_code: The journal code of the journal where the article lives. + :param article_id: The article id. + :return: The dictionary identifier. + """ return f"{journal_code}-{article_id}" def get_export_zip_filepath(self, journal_code: str, article_id: str) -> str | None: @@ -75,7 +82,32 @@ def get_export_go_filepath(self, journal_code: str, article_id: str) -> str | No file_export_creator = self.get_export_file_creator(journal_code, article_id) return file_export_creator.get_go_filepath() if file_export_creator else None + def log_export_error(self, journal_code: str, article_id: str) -> None: + """ + Logs an error message in both the database and plaintext logs. + :param journal_code: The journal code of the journal where the article lives. + :param article_id: The article id. + """ + file_export_creator = self.get_export_file_creator(journal_code, article_id) + if file_export_creator: + file_export_creator.log_error(logger_messages.export_process_failed_ingest(article_id)) + + def log_export_success(self, journal_code: str, article_id: str) -> None: + """ + Logs the success message for when an article has completed a journey to Editorial Manager. + :param journal_code: The journal code of the journal where the article lives. + :param article_id: The article id. + """ + file_export_creator = self.get_export_file_creator(journal_code, article_id) + if file_export_creator: + file_export_creator.log_success() + def delete_export_files(self, journal_code: str, article_id: str) -> None: + """ + Deletes the export files for the given article. + :param journal_code: The journal code of the journal the article lives in. + :param article_id: The article id. + """ dictionary_identifier: str = self.__get_dictionary_identifier(journal_code, article_id) if dictionary_identifier not in self.exports: return @@ -93,6 +125,11 @@ def __delete_files(self) -> None: @staticmethod def __delete_file(filepath: str) -> bool: + """ + Deletes the given file. + :param filepath: The file path of the file to delete. + :return: True if the file was deleted, false otherwise. + """ if not os.path.exists(filepath): return True try: @@ -129,6 +166,7 @@ def export_success_callback(journal_code: str, article_id: str) -> None: :param journal_code: The journal code of the journal the article lives in. :param article_id: The article id. """ + FileTransferService().log_export_success(journal_code, article_id) FileTransferService().delete_export_files(journal_code, article_id) @@ -138,4 +176,5 @@ def export_failure_callback(journal_code: str, article_id: str) -> None: :param journal_code: The journal code of the journal the article lives in. :param article_id: The article id. """ + FileTransferService().log_export_error(journal_code, article_id) FileTransferService().delete_export_files(journal_code, article_id) diff --git a/logger_messages.py b/logger_messages.py index 1d5ae10..5b33812 100644 --- a/logger_messages.py +++ b/logger_messages.py @@ -168,3 +168,17 @@ def export_process_failed_no_export_folder() -> str: :return: The logger message. """ return "No export folder provided. Discontinuing export process." + +def export_process_failed_ingest(article_id: str) -> str: + """ + Gets the log message for when the article failed to be ingested into Editorial Manager. + :return: The logger message. + """ + return "Export process failed during ingest to Editorial Manager for article (ID: {0}).".format(article_id) + +def export_process_succeeded(article_id: str) -> str: + """ + Gets the log message for when the export process was successful. + :return: The logger message. + """ + return "Export process succeeded for article (ID: {0}).".format(article_id) diff --git a/models.py b/models.py new file mode 100644 index 0000000..92bc6fb --- /dev/null +++ b/models.py @@ -0,0 +1,36 @@ +""" +The models used for the production transporter. +""" +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + +from django.db import models +from plugins.editorial_manager_transfer_service.enums.transfer_log_message_type import TransferLogMessageType + + +class TransferLogs(models.Model): + """ + The model used for transfer logs. + """ + message_type = models.CharField( + max_length=2, + choices=TransferLogMessageType.choices, + default=TransferLogMessageType.EXPORT, + ) + + journal = models.ForeignKey( + "journal.Journal", + on_delete=models.CASCADE, + null=True, blank=True + ) + + article = models.ForeignKey( + "submission.Article", + on_delete=models.CASCADE, + null=True, blank=True + ) + + message = models.TextField() + message_date_time = models.DateTimeField(auto_now_add=True) + success = models.BooleanField(default=False) \ No newline at end of file diff --git a/tests/test_file_creation.py b/tests/test_file_creation.py index b5efd22..b1de54b 100644 --- a/tests/test_file_creation.py +++ b/tests/test_file_creation.py @@ -1,25 +1,27 @@ import os import re import shutil -import unittest import xml.etree.ElementTree as ElementTree from typing import Sequence from unittest.mock import patch -from hypothesis import given -from hypothesis import settings as hypothesis_settings -from hypothesis.strategies import from_regex, SearchStrategy, lists +import hypothesis.strategies as hypothesis_strategies +from hypothesis import settings as hypothesis_settings, given +from hypothesis.extra.django import TestCase import plugins.editorial_manager_transfer_service.consts as consts import plugins.editorial_manager_transfer_service.file_exporter as file_exporter import plugins.editorial_manager_transfer_service.tests.utils.article_creation_utils as article_utils from journal.models import Journal +from plugins.editorial_manager_transfer_service.enums.transfer_log_message_type import TransferLogMessageType +from plugins.editorial_manager_transfer_service.models import TransferLogs uuid4_regex = re.compile('^([a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12})$') valid_filename_regex = re.compile("^[\w\-. ]+$") -valid_filenames: SearchStrategy[list[str]] = lists(from_regex(valid_filename_regex), unique=True, min_size=0, - max_size=20) +valid_filenames: hypothesis_strategies.SearchStrategy[list[str]] = hypothesis_strategies.lists( + hypothesis_strategies.from_regex(valid_filename_regex), unique=True, min_size=0, + max_size=20) def _get_setting(self, setting_name: str) -> str: @@ -32,7 +34,7 @@ def _get_setting(self, setting_name: str) -> str: return "SUBMISSION_PARTNER" -class TestFileCreation(unittest.TestCase): +class TestFileCreation(TestCase): def setUp(self): """ Sets up the export folder structure. @@ -49,7 +51,8 @@ def tearDown(self): """ shutil.rmtree(article_utils._get_article_export_folders()) - @given(article_id=from_regex(uuid4_regex), manuscript_filename=from_regex(valid_filename_regex), + @given(article_id=hypothesis_strategies.from_regex(uuid4_regex), + manuscript_filename=hypothesis_strategies.from_regex(valid_filename_regex), data_figure_filenames=valid_filenames) @patch.object(file_exporter.ExportFileCreation, 'get_setting', new=_get_setting) @patch('plugins.editorial_manager_transfer_service.file_exporter.get_article_export_folders', @@ -67,7 +70,8 @@ def test_regular_article_creation_process(self, mock_get_article, article_id: st journal_code = "TEST" # Set the return - mock_get_article.return_value = article_utils._create_article(Journal.objects.get(code=journal_code), article_id, manuscript_filename, + mock_get_article.return_value = article_utils._create_article(Journal.objects.get(code=journal_code), + article_id, manuscript_filename, data_figure_filenames) exporter = file_exporter.ExportFileCreation(journal_code, article_id) @@ -76,6 +80,21 @@ def test_regular_article_creation_process(self, mock_get_article, article_id: st self.__check_go_file(exporter.get_go_filepath(), len(data_figure_filenames) + 1) + @given(message=hypothesis_strategies.text(), + message_type=hypothesis_strategies.sampled_from(TransferLogMessageType.choices), + success=hypothesis_strategies.booleans()) + def test_blank_transfer_logs(self, message: str, message_type: TransferLogMessageType, success: bool): + """ + Tests creating an empty TransferLogs object. + """ + TransferLogs.objects.create( + journal=None, + article=None, + message=message, + message_type=message_type, + success=success + ) + def __check_go_file(self, go_filepath: str, number_of_files: int) -> None: if not os.path.exists(go_filepath): self.fail("Go_filepath {} does not exist".format(go_filepath)) @@ -96,7 +115,3 @@ def __check_go_file(self, go_filepath: str, number_of_files: int) -> None: files: list[ElementTree.Element] = filegroup.findall(consts.GO_FILE_ELEMENT_TAG_FILE) self.assertEqual(number_of_files, len(files)) - - -if __name__ == '__main__': - unittest.main() From 1fac0d241b7e171d747265beff5c8ed7097ad891 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Sat, 6 Sep 2025 10:26:05 -0500 Subject: [PATCH 4/6] Update the tests to use composite and add more realistic testing paths. --- file_exporter.py | 11 +- logger_messages.py | 14 +- ...file_creation.py => test_file_exporter.py} | 59 ++---- tests/test_message_logs.py | 66 +++++++ tests/utils/article_creation_utils.py | 170 +++++++++++++++--- 5 files changed, 234 insertions(+), 86 deletions(-) rename tests/{test_file_creation.py => test_file_exporter.py} (51%) create mode 100644 tests/test_message_logs.py diff --git a/file_exporter.py b/file_exporter.py index 8cd8301..3c48e26 100644 --- a/file_exporter.py +++ b/file_exporter.py @@ -44,14 +44,14 @@ class ExportFileCreation: A class for managing the export file creation process. """ - def __init__(self, janeway_journal_code: str, article_id: str): + def __init__(self, janeway_journal_code: str, article_id: int | None) -> None: self.zip_filepath: str | None = None self.go_filepath: str | None = None self.in_error_state: bool = False self.__license_code: str | None = None self.__journal_code: str | None = None self.__submission_partner_code: str | None = None - self.article_id: str | None = article_id.strip() if article_id else None + self.article_id: int | None = article_id self.article: Article | None = None self.journal: Journal | None = None self.export_folder: str | None = None @@ -247,7 +247,7 @@ def __create_metadata_file(self, article: Article) -> File | None: """ pass - def __fetch_article(self, journal: Journal | None, article_id: str | None) -> Article | None: + def __fetch_article(self, journal: Journal | None, article_id: int | None) -> Article | None: """ Gets the article object for the given article ID. :param journal: The journal to fetch the article from. @@ -255,7 +255,7 @@ def __fetch_article(self, journal: Journal | None, article_id: str | None) -> Ar :return: The article object with the given article ID. """ # If no article ID or journal, return an error. - if not article_id or len(article_id) <= 0: + if not article_id or article_id <= 0: self.log_error(logger_messages.process_failed_no_article_id_provided()) self.in_error_state = True return None @@ -310,7 +310,8 @@ def log_success(self) -> None: """ Logs a success message in both the database and plaintext logs. """ - TransferLogs.objects.create(journal=self.journal, article=self.article, message=logger_messages.export_process_succeeded(self.article_id), + TransferLogs.objects.create(journal=self.journal, article=self.article, + message=logger_messages.export_process_succeeded(self.article_id), message_type=TransferLogMessageType.EXPORT, success=True) @staticmethod diff --git a/logger_messages.py b/logger_messages.py index 5b33812..c235f56 100644 --- a/logger_messages.py +++ b/logger_messages.py @@ -64,7 +64,7 @@ def import_folder_created() -> str: return '{0} import folder already exists.'.format(PLUGIN_NAME) -def process_fetching_article(article_id: str) -> str: +def process_fetching_article(article_id: int) -> str: """ Gets the log message for when an article is being fetched from the database. :param: article_id: The ID of the article being fetched. @@ -73,7 +73,7 @@ def process_fetching_article(article_id: str) -> str: return "Fetching article from database (ID: {0})...".format(article_id) -def process_finished_fetching_article(article_id: str) -> str: +def process_finished_fetching_article(article_id: int) -> str: """ Gets the log message for when an article is being fetched from the database. :param: article_id: The ID of the article being fetched. @@ -82,7 +82,7 @@ def process_finished_fetching_article(article_id: str) -> str: return "Completed fetching article from database (ID: {0})...".format(article_id) -def process_failed_fetching_article(article_id: str) -> str: +def process_failed_fetching_article(article_id: int) -> str: """ Gets the log message for when an article failed to be fetched. :param: article_id: The ID of the article being fetched. @@ -116,7 +116,7 @@ def process_failed_fetching_journal(janway_journal_code: str) -> str: :return: The logger message. """ return "Fetching journal from database (Code: {0}) failed. Discontinuing export process.".format( - janway_journal_code) + janway_journal_code) def process_failed_fetching_metadata(article_id) -> str: @@ -169,14 +169,16 @@ def export_process_failed_no_export_folder() -> str: """ return "No export folder provided. Discontinuing export process." -def export_process_failed_ingest(article_id: str) -> str: + +def export_process_failed_ingest(article_id: int) -> str: """ Gets the log message for when the article failed to be ingested into Editorial Manager. :return: The logger message. """ return "Export process failed during ingest to Editorial Manager for article (ID: {0}).".format(article_id) -def export_process_succeeded(article_id: str) -> str: + +def export_process_succeeded(article_id: int) -> str: """ Gets the log message for when the export process was successful. :return: The logger message. diff --git a/tests/test_file_creation.py b/tests/test_file_exporter.py similarity index 51% rename from tests/test_file_creation.py rename to tests/test_file_exporter.py index b1de54b..0d676f1 100644 --- a/tests/test_file_creation.py +++ b/tests/test_file_exporter.py @@ -1,27 +1,19 @@ +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + import os -import re import shutil import xml.etree.ElementTree as ElementTree -from typing import Sequence from unittest.mock import patch -import hypothesis.strategies as hypothesis_strategies from hypothesis import settings as hypothesis_settings, given from hypothesis.extra.django import TestCase import plugins.editorial_manager_transfer_service.consts as consts import plugins.editorial_manager_transfer_service.file_exporter as file_exporter import plugins.editorial_manager_transfer_service.tests.utils.article_creation_utils as article_utils -from journal.models import Journal -from plugins.editorial_manager_transfer_service.enums.transfer_log_message_type import TransferLogMessageType -from plugins.editorial_manager_transfer_service.models import TransferLogs - -uuid4_regex = re.compile('^([a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12})$') -valid_filename_regex = re.compile("^[\w\-. ]+$") - -valid_filenames: hypothesis_strategies.SearchStrategy[list[str]] = hypothesis_strategies.lists( - hypothesis_strategies.from_regex(valid_filename_regex), unique=True, min_size=0, - max_size=20) +from submission.models import Article def _get_setting(self, setting_name: str) -> str: @@ -39,6 +31,7 @@ def setUp(self): """ Sets up the export folder structure. """ + article_utils.database_crafter_do_preqs() if not os.path.exists(article_utils._get_article_export_folders()): try: os.makedirs(article_utils._get_article_export_folders()) @@ -51,49 +44,23 @@ def tearDown(self): """ shutil.rmtree(article_utils._get_article_export_folders()) - @given(article_id=hypothesis_strategies.from_regex(uuid4_regex), - manuscript_filename=hypothesis_strategies.from_regex(valid_filename_regex), - data_figure_filenames=valid_filenames) + @given(article=article_utils.create_article()) @patch.object(file_exporter.ExportFileCreation, 'get_setting', new=_get_setting) @patch('plugins.editorial_manager_transfer_service.file_exporter.get_article_export_folders', new=article_utils._get_article_export_folders) - @patch.object(Journal.objects, "get", new=article_utils._get_journal) - @patch('submission.models.Article.get_article') @hypothesis_settings(max_examples=5) - def test_regular_article_creation_process(self, mock_get_article, article_id: str, manuscript_filename: str, - data_figure_filenames: Sequence[str]): + def test_regular_article_creation_process(self, article: Article) -> None: """ Tests a basic end to end use case of exporting articles. - :param mock_get_article: Mock the get_article method. - :param article_id: The id of the article. """ - journal_code = "TEST" - - # Set the return - mock_get_article.return_value = article_utils._create_article(Journal.objects.get(code=journal_code), - article_id, manuscript_filename, - data_figure_filenames) + journal = article.journal + article_id: int = article.pk - exporter = file_exporter.ExportFileCreation(journal_code, article_id) + exporter = file_exporter.ExportFileCreation(journal.code, article_id) self.assertTrue(exporter.can_export()) - self.assertEqual(article_id.strip(), exporter.article_id) # add assertion here + self.assertEqual(article_id, exporter.article_id) # add assertion here - self.__check_go_file(exporter.get_go_filepath(), len(data_figure_filenames) + 1) - - @given(message=hypothesis_strategies.text(), - message_type=hypothesis_strategies.sampled_from(TransferLogMessageType.choices), - success=hypothesis_strategies.booleans()) - def test_blank_transfer_logs(self, message: str, message_type: TransferLogMessageType, success: bool): - """ - Tests creating an empty TransferLogs object. - """ - TransferLogs.objects.create( - journal=None, - article=None, - message=message, - message_type=message_type, - success=success - ) + self.__check_go_file(exporter.get_go_filepath(), len(article.data_figure_files.all()) + 1) def __check_go_file(self, go_filepath: str, number_of_files: int) -> None: if not os.path.exists(go_filepath): diff --git a/tests/test_message_logs.py b/tests/test_message_logs.py new file mode 100644 index 0000000..76849da --- /dev/null +++ b/tests/test_message_logs.py @@ -0,0 +1,66 @@ +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + +import os +import shutil + +import hypothesis.strategies as hypothesis_strategies +from hypothesis import given +from hypothesis.extra.django import TestCase + +import plugins.editorial_manager_transfer_service.tests.utils.article_creation_utils as article_utils +from plugins.editorial_manager_transfer_service.enums.transfer_log_message_type import TransferLogMessageType +from plugins.editorial_manager_transfer_service.models import TransferLogs +from submission.models import Article + + +class TestMessageLogs(TestCase): + def setUp(self): + """ + Sets up the export folder structure. + """ + article_utils.database_crafter_do_preqs() + if not os.path.exists(article_utils._get_article_export_folders()): + try: + os.makedirs(article_utils._get_article_export_folders()) + except FileExistsError: + pass + + def tearDown(self): + """ + Tears down after each test to ensure each test is unique. + """ + shutil.rmtree(article_utils._get_article_export_folders()) + + @given(message=hypothesis_strategies.text(), + message_type=hypothesis_strategies.sampled_from(TransferLogMessageType.choices), + success=hypothesis_strategies.booleans()) + def test_blank_transfer_logs(self, message: str, message_type: TransferLogMessageType, success: bool): + """ + Tests creating an empty TransferLogs object. + """ + TransferLogs.objects.create( + journal=None, + article=None, + message=message, + message_type=message_type, + success=success + ) + + @given(article=article_utils.create_article(), + message=hypothesis_strategies.text(), + message_type=hypothesis_strategies.sampled_from(TransferLogMessageType.choices), + success=hypothesis_strategies.booleans()) + def test_normal_transfer_logs(self, article: Article, message: str, message_type: TransferLogMessageType, + success: bool): + """ + Tests creating an empty TransferLogs object. + """ + TransferLogs.objects.create( + journal=article.journal, + article=article, + message=message, + message_type=message_type, + success=success + ) diff --git a/tests/utils/article_creation_utils.py b/tests/utils/article_creation_utils.py index 4e30466..bd39c88 100644 --- a/tests/utils/article_creation_utils.py +++ b/tests/utils/article_creation_utils.py @@ -1,13 +1,30 @@ +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + +import codecs +import json import os -from typing import Sequence -from unittest.mock import MagicMock +import re + +from django.conf import settings as django_settings +from django.core.files import File as DjangoFile +from hypothesis import strategies as st +from hypothesis.extra.django import from_field +from core import ( + models as core_models, + files as core_files, +) from core import settings -from core.models import File +from core.models import File, Account from journal.models import Journal from plugins.editorial_manager_transfer_service import consts from submission.models import Article +uuid4_regex = re.compile('^([a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12})$') +valid_filename_regex = re.compile("^[\w\-. ]+$") + EXPORT_FOLDER = os.path.join(settings.BASE_DIR, "collected-static", consts.SHORT_NAME, "export") @@ -20,46 +37,141 @@ def _get_article_export_folders() -> str: return EXPORT_FOLDER -def _get_journal(code: str) -> Journal: - journal: Journal = MagicMock(Journal) - journal.code = code +def database_crafter_do_preqs() -> None: + """ + A number of items are required to be setup in advance within the database. + """ + create_default_settings() + database_crafter_create_default_xsl() + + +def create_default_settings() -> None: + """ + Creates the default settings for to test this plugin. + """ + with codecs.open( + os.path.join(settings.BASE_DIR, "utils/install/submission_items.json") + ) as json_data: + submission_items = json.load(json_data) + for i, setting in enumerate(submission_items): + if not setting.get("group") == "special": + setting_group_obj, c = core_models.SettingGroup.objects.get_or_create( + name=setting.get("group"), + ) + core_models.Setting.objects.get_or_create( + group=setting_group_obj, + name=setting.get("name"), + ) + + +def database_crafter_create_default_xsl() -> None: + """ + Creates the default XSL file if it doesn't exist. + """ + try: + core_models.XSLFile.objects.get(label=django_settings.DEFAULT_XSL_FILE_LABEL) + except core_models.XSLFile.DoesNotExist: + core_models.XSLFile.objects.create(label=django_settings.DEFAULT_XSL_FILE_LABEL) + + +@st.composite +def create_journal(draw) -> Journal: + """ + Creates a journal object from the given settings. + :param draw: The Hypothesis object provided by the hypothesis framework. + :return: The newly created journal. + """ + code = draw(st.text(min_size=1, max_size=40)) + journal: Journal = Journal.objects.create(code=code) return journal -def _create_txt_file(filename: str) -> File: - manuscript_filepath = os.path.join(_get_article_export_folders(), "{0}.txt".format(filename)) +@st.composite +def create_username(draw) -> str | None: + """ + Creates a unique username or None, if the username created was not unique. + :param draw: The Hypothesis object provided by the hypothesis framework. + :return: A unique username or None, if the username created was not unique. + """ + username = draw(from_field(Account._meta.get_field('username'))) + try: + Account.objects.get(username=username) + return None + except Account.DoesNotExist: + return username + + +@st.composite +def create_unique_email(draw) -> str | None: + """ + Creates a unique email address or None, if the email address was not unique. + :param draw: A Hypothesis object provided by the hypothesis framework. + :return: A unique email address or None, if the email address was not unique. + """ + email = draw(st.emails()) + try: + Account.objects.get(email=email) + return None + except Account.DoesNotExist: + return email + + +@st.composite +def create_account(draw) -> Account: + """ + Creates a new account object from the given settings. + :param draw: A Hypothesis object provided by the hypothesis framework. + :return: A newly created account. + """ + username = draw(create_username()) + while not username: + username = draw(create_username()) + + email = draw(create_unique_email()) + while not email: + email = draw(create_unique_email()) + + return Account.objects.create( + email=email, + username=username, + ) + + +@st.composite +def create_txt_file(draw, article: Article) -> File: + filename: str = draw(st.from_regex(valid_filename_regex)) + manuscript_filepath = os.path.join(_get_article_export_folders(), + "{0}.txt".format(filename)) content = "This is the first line.\nThis is the second line." with open(manuscript_filepath, 'w') as file: try: file.write(content) + file.flush() file.close() except FileExistsError: pass + with open(manuscript_filepath, 'rb+') as file: + django_file = DjangoFile(file, name=filename) + return core_files.save_file_to_article(file_to_handle=django_file, article=article, + owner=draw(create_account())) - manuscript: File = MagicMock(File) - manuscript.get_file_path.return_value = manuscript_filepath - - return manuscript +@st.composite +def create_article(draw) -> Article: + journal: Journal = draw(create_journal()) + article: Article = Article.objects.create( + journal=journal, + ) -def _create_article(journal: Journal, article_id: str, manuscript_filename: str, data_figure_filenames: Sequence[str]) -> Article: - manuscript: File = _create_txt_file(manuscript_filename) + manuscript: File = draw(create_txt_file(article=article)) + article.manuscript_files.add(manuscript) - article: Article = MagicMock(Article) - article.article_id = article_id - article.journal = journal + # Handle the data figure files. + number_of_files: int = draw(st.integers(min_value=0, max_value=20)) + for i in range(number_of_files): + data_figure: File = draw(create_txt_file(article=article)) + article.data_figure_files.add(data_figure) - # Handle the manuscript files. - article.manuscript_files = MagicMock(File.objects) - manuscript_files: list[File] = list() - manuscript_files.append(manuscript) - article.manuscript_files.all.return_value = manuscript_files + article.save() - # Handle the data figure files. - article.data_figure_files = MagicMock(File.objects) - data_figure_files: list[File] = list() - for data_figure_filename in data_figure_filenames: - data_figure: File = _create_txt_file(data_figure_filename) - data_figure_files.append(data_figure) - article.data_figure_files.all.return_value = data_figure_files return article From 41ee0085579f0d235f4fee5e142ff4267a983aa6 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 17 Sep 2025 08:12:05 -0500 Subject: [PATCH 5/6] Fix some errors and typos. --- file_exporter.py | 18 ++++++++++-------- file_transfer_service.py | 22 ++++++++++++++++------ logger_messages.py | 27 +++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/file_exporter.py b/file_exporter.py index 3c48e26..96ebd42 100644 --- a/file_exporter.py +++ b/file_exporter.py @@ -185,8 +185,8 @@ def get_setting(self, setting_name: str) -> str: try: return setting_handler.get_setting(setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME, setting_name=setting_name, journal=self.journal, ).processed_value - except ObjectDoesNotExist: - self.log_error("Could not get the following setting, '{0}'".format(setting_name)) + except ObjectDoesNotExist as e: + self.log_error("Could not get the following setting, '{0}'".format(setting_name), e) self.in_error_state = True return "" @@ -212,7 +212,7 @@ def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequen version.set(consts.GO_FILE_VERSION_ELEMENT_ATTRIBUTE_NUMBER_KEY, consts.GO_FILE_VERSION_ELEMENT_ATTRIBUTE_NUMBER_VALUE) journal: ETree.Element = ETree.SubElement(header, consts.GO_FILE_ELEMENT_TAG_JOURNAL) - journal.set(consts.GO_FILE_JOURNAL_ELEMENT_ATTRIBUTE_CODE_KEY, self.get_license_code()) + journal.set(consts.GO_FILE_JOURNAL_ELEMENT_ATTRIBUTE_CODE_KEY, self.get_journal_code()) import_type: ETree.Element = ETree.SubElement(header, consts.GO_FILE_ELEMENT_TAG_IMPORT_TYPE) import_type.set(consts.GO_FILE_IMPORT_TYPE_ELEMENT_ATTRIBUTE_ID_KEY, consts.GO_FILE_IMPORT_TYPE_ELEMENT_ATTRIBUTE_ID_VALUE) @@ -266,8 +266,8 @@ def __fetch_article(self, journal: Journal | None, article_id: int | None) -> Ar try: article = Article.get_article(journal, "id", article_id) logger.debug(logger_messages.process_finished_fetching_article(article_id)) - except Article.DoesNotExist: - self.log_error(logger_messages.process_failed_fetching_article(article_id)) + except Article.DoesNotExist as e: + self.log_error(logger_messages.process_failed_fetching_article(article_id), e) self.in_error_state = True return article @@ -291,17 +291,19 @@ def __fetch_journal(self, janeway_journal_code: str | None) -> Journal | None: try: journal = Journal.objects.get(code=janeway_journal_code) logger.debug(logger_messages.process_finished_fetching_journal(janeway_journal_code)) - except Journal.DoesNotExist: - self.log_error(logger_messages.process_failed_fetching_journal(janeway_journal_code)) + except Journal.DoesNotExist as e: + self.log_error(logger_messages.process_failed_fetching_journal(janeway_journal_code), e) self.in_error_state = True return journal - def log_error(self, message: str) -> None: + def log_error(self, message: str, error: Exception = None) -> None: """ Logs the given error message in both the database and plaintext logs. :param message: The message to log. + :param error: The exception, if there is one. """ + logger.exception(error) logger.error(message) TransferLogs.objects.create(journal=self.journal, article=self.article, message=message, message_type=TransferLogMessageType.EXPORT, success=False) diff --git a/file_transfer_service.py b/file_transfer_service.py index b0778d0..ad5737c 100644 --- a/file_transfer_service.py +++ b/file_transfer_service.py @@ -82,17 +82,23 @@ def get_export_go_filepath(self, journal_code: str, article_id: str) -> str | No file_export_creator = self.get_export_file_creator(journal_code, article_id) return file_export_creator.get_go_filepath() if file_export_creator else None - def log_export_error(self, journal_code: str, article_id: str) -> None: + def log_export_error(self, journal_code: str, + article_id: str, + error_message: str = None, + error: Exception = None) -> None: """ Logs an error message in both the database and plaintext logs. + :param error: The exception, if there is one. + :param error_message: The error message to print out. :param journal_code: The journal code of the journal where the article lives. :param article_id: The article id. """ file_export_creator = self.get_export_file_creator(journal_code, article_id) if file_export_creator: - file_export_creator.log_error(logger_messages.export_process_failed_ingest(article_id)) + file_export_creator.log_error(logger_messages.export_process_failed_ingest(article_id, error_message), error) - def log_export_success(self, journal_code: str, article_id: str) -> None: + def log_export_success(self, journal_code: str, + article_id: str) -> None: """ Logs the success message for when an article has completed a journey to Editorial Manager. :param journal_code: The journal code of the journal where the article lives. @@ -134,7 +140,9 @@ def __delete_file(filepath: str) -> bool: return True try: os.remove(filepath) - except OSError: + except OSError as e: + logger.exception(e) + logger.error(logger_messages.excport_process_failed_delete_file(filepath)) return False return True @@ -170,11 +178,13 @@ def export_success_callback(journal_code: str, article_id: str) -> None: FileTransferService().delete_export_files(journal_code, article_id) -def export_failure_callback(journal_code: str, article_id: str) -> None: +def export_failure_callback(journal_code: str, article_id: str, error_message: str = None, error: Exception = None) -> None: """ The callback in case of a failed export. + :param error: The exception, if there is one. + :param error_message: The error message to print. :param journal_code: The journal code of the journal the article lives in. :param article_id: The article id. """ - FileTransferService().log_export_error(journal_code, article_id) + FileTransferService().log_export_error(journal_code, article_id, error_message, error) FileTransferService().delete_export_files(journal_code, article_id) diff --git a/logger_messages.py b/logger_messages.py index c235f56..8a025aa 100644 --- a/logger_messages.py +++ b/logger_messages.py @@ -170,12 +170,26 @@ def export_process_failed_no_export_folder() -> str: return "No export folder provided. Discontinuing export process." -def export_process_failed_ingest(article_id: int) -> str: +def __export_process_failed_ingest(article_id: str) -> str: """ Gets the log message for when the article failed to be ingested into Editorial Manager. + :param article_id: The ID of the article being fetched. :return: The logger message. """ - return "Export process failed during ingest to Editorial Manager for article (ID: {0}).".format(article_id) + "Export process failed during ingest to Editorial Manager for article (ID: {0})".format(article_id) + + +def export_process_failed_ingest(article_id: str, error_message: str = None) -> str: + """ + Gets the log message for when the article failed to be ingested into Editorial Manager. + :param article_id: The ID of the article being fetched. + :param error_message: The error message to be logged. + :param error: The exception to be logged. + :return: The logger message. + """ + if error_message: + return error_message + return __export_process_failed_ingest(article_id) def export_process_succeeded(article_id: int) -> str: @@ -184,3 +198,12 @@ def export_process_succeeded(article_id: int) -> str: :return: The logger message. """ return "Export process succeeded for article (ID: {0}).".format(article_id) + + +def excport_process_failed_delete_file(filepath: str) -> str: + """ + Gets the log message for when an export file failed to be deleted. + :param filepath: The path to the export file. + :return: The logger message. + """ + return "Export process failed to delete file at filepath: {0}.".format(filepath) From fb93f1ebff9e5d0d4afa48a68f4718da473ac3f4 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Thu, 18 Sep 2025 10:34:02 -0500 Subject: [PATCH 6/6] Add initial migration for transfer log data. --- migrations/0001_initial.py | 29 +++++++++++++++++++++++++++++ migrations/__init__.py | 0 2 files changed, 29 insertions(+) create mode 100644 migrations/0001_initial.py create mode 100644 migrations/__init__.py diff --git a/migrations/0001_initial.py b/migrations/0001_initial.py new file mode 100644 index 0000000..04d2a0e --- /dev/null +++ b/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.22 on 2025-09-18 15:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('journal', '0068_issue_cached_display_title_a11y_and_more'), + ('submission', '0088_auto_20250506_1214'), + ] + + operations = [ + migrations.CreateModel( + name='TransferLogs', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('message_type', models.CharField(choices=[('EX', 'Export Message'), ('IM', 'Import Message')], default='EX', max_length=2)), + ('message', models.TextField()), + ('message_date_time', models.DateTimeField(auto_now_add=True)), + ('success', models.BooleanField(default=False)), + ('article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='submission.article')), + ('journal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='journal.journal')), + ], + ), + ] diff --git a/migrations/__init__.py b/migrations/__init__.py new file mode 100644 index 0000000..e69de29