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..64e5c6c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,3 @@ hypothesis==6.138.7 -pytest==8.4.1 \ No newline at end of file +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 9310934..96ebd42 100644 --- a/file_exporter.py +++ b/file_exporter.py @@ -18,6 +18,8 @@ import plugins.editorial_manager_transfer_service.logger_messages as logger_messages from core.models import File 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 @@ -42,47 +44,32 @@ 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: 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 - # 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. 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 @@ -131,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 @@ -198,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: - logger.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 "" @@ -225,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) @@ -260,15 +247,74 @@ def __create_metadata_file(self, article: Article) -> File | None: """ pass - @staticmethod - def __fetch_article(journal: Journal, article_id: str) -> Article: + 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. :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 article_id <= 0: + self.log_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 as e: + self.log_error(logger_messages.process_failed_fetching_article(article_id), e) + self.in_error_state = True + + return article + + 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. + :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: + self.log_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=janeway_journal_code) + logger.debug(logger_messages.process_finished_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, 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) + + 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..ad5737c 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,38 @@ 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, + 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, error_message), error) + + 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,11 +131,18 @@ 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: 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 @@ -129,13 +174,17 @@ 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) -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, error_message, error) FileTransferService().delete_export_files(journal_code, article_id) 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/logger_messages.py b/logger_messages.py index 0b0e489..8a025aa 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,16 @@ def process_fetching_article(article_id: str) -> str: return "Fetching article from database (ID: {0})...".format(article_id) -def process_failed_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. + :return: The logger message. + """ + return "Completed fetching article from database (ID: {0})...".format(article_id) + + +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. @@ -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,9 +154,56 @@ 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. :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. + :param article_id: The ID of the article being fetched. + :return: The logger message. + """ + "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: + """ + 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) + + +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) 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/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 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/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 @@