From b6128f65891e08ba18753638812b94d3d99850e2 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 27 Aug 2025 10:14:41 -0500 Subject: [PATCH 01/31] Complete most aspects of the transfer service. --- .gitignore | 1 + file_creation.py | 162 +++++++++++++++++++++++ install/__init__.py | 0 install/settings.json | 47 +++++++ logger_messages.py | 73 ++++++++++ management/__init__.py | 0 management/commands/__init__.py | 0 management/commands/create_export_zip.py | 13 ++ plugin_settings.py | 51 ++++++- utils.py | 2 + 10 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 file_creation.py create mode 100644 install/__init__.py create mode 100644 install/settings.json create mode 100644 logger_messages.py create mode 100644 management/__init__.py create mode 100644 management/commands/__init__.py create mode 100644 management/commands/create_export_zip.py create mode 100644 utils.py diff --git a/.gitignore b/.gitignore index ef81b1e..c11cac2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /.venv/ +/editorial_manager_transfer_service.iml diff --git a/file_creation.py b/file_creation.py new file mode 100644 index 0000000..1da45c8 --- /dev/null +++ b/file_creation.py @@ -0,0 +1,162 @@ +""" +A list of functions handling file exports and imports. +""" +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + +import os +import uuid +import zipfile +import xml.etree.cElementTree as ET +from collections.abc import Sequence +from typing import List + +from journal.models import Journal +from utils.logger import get_logger +from utils import setting_handler +import logger_messages + +from submission.models import Article +from core.models import File + +from plugins.editorial_manager_transfer_service.plugin_settings import EXPORT_FILE_PATH + +logger = get_logger(__name__) + + +def get_article_export_folders() -> List[str]: + """ + Gets the filepaths for the folders used for exporting articles. + + :return: A list of filepaths for the export folders. + """ + if os.path.exists(EXPORT_FILE_PATH): + return os.listdir(EXPORT_FILE_PATH) + else: + return [] + + +class FileCreation: + def __init__(self, journal: Journal): + self.zip_filepath = None + self.go_filepath = None + self.license_code: str = setting_handler.get_setting( + setting_group_name="plugin:editorial_manager_transfer_service", + setting_name="license_code", + journal=journal, + ).processed_value + self.journal_code: str = setting_handler.get_setting( + setting_group_name="plugin:editorial_manager_transfer_service", + setting_name="journal_code", + journal=journal, + ).processed_value + self.submission_partner_code: str = setting_handler.get_setting( + setting_group_name="plugin:editorial_manager_transfer_service", + setting_name="submission_partner_code", + journal=journal, + ).processed_value + + export_folders: Sequence[str] = get_article_export_folders() + self.export_folder: str | None = export_folders[0] if len(export_folders) > 0 else None + + def create_export_file(self, article_id: str) -> str: + """ + Creates the export file for + :param article_id: The ID of the article to create an export file for. + :return: The filepath to the created export file. + """ + + # Get the article based upon the given article ID. + logger.info(logger_messages.process_fetching_article(article_id)) + try: + article: Article = self.__fetch_article(article_id) + except Exception: + logger.error(logger_messages.process_failed_fetching_article(article_id)) + return "" + + metadata_file: File = self.__create_metadata_file(article) + article_files: List[File] = fetch_article_files(article) + + prefix: str = "{0}_{1}".format(self.submission_partner_code, uuid.uuid4()) + + self.zip_filepath: str = os.path.join(self.export_folder, "{0}.zip".format(prefix)) + with zipfile.ZipFile(self.zip_filepath, "w") as zipf: + zipf.write(metadata_file.get_file_path(article)) + for article_file in article_files: + zipf.write(article_file.get_file_path(article)) + filenames: Sequence[str] = zipf.namelist() + zipf.close() + + self.__create_go_xml_file(metadata_file.uuid_filename, filenames, prefix) + + return self.zip_filepath + + def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequence[str], filename: str) -> None: + go: ET.Element = ET.Element("GO") + go.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + go.set("xsi:noNamespaceSchemaLocation", "app://Aries.EditorialManager/Resources/XmlDefineTransformFiles/aries_import_go_file.xsd") + + # Format the header. + header: ET.Element = ET.SubElement(go, "header") + ET.SubElement(header, "version", number="1.0") + ET.SubElement(header, "journal", code=self.journal_code) + ET.SubElement(header, "import-type", id="2") + parameters: ET.Element = ET.SubElement(header, "parameters") + ET.SubElement(parameters, "parameter", name="license-code", value="{0}_{1}".format(self.submission_partner_code, self.license_code)) + + # Begin the filegroup. + filegroup: ET.Element = ET.SubElement(go, "filegroup") + + # Create the archive and metadata files. + ET.SubElement(filegroup, "archive-file", name="{0}.zip".format(filename)) + ET.SubElement(filegroup, "metadata-file", name=metadata_filename) + + for article_filename in article_filenames: + ET.SubElement(filegroup, "file", name=article_filename) + + tree = ET.ElementTree(go) + self.go_filepath = os.path.join(self.export_folder, "{0}.go.xml".format(filename)); + tree.write(self.go_filepath) + + def __fetch_article(self, article_id: str) -> Article: + # TODO: Finish method. + pass + + def __create_metadata_file(self, article: Article) -> File: + """ + Creates the metadata file based on the given article. + :param article: The article to convert to JATS. + :return: + """ + pass + + +def fetch_article_files(article: Article) -> List[File]: + """ + Fetches the manuscript (or content or body) of an article alongside any other files associated with it. + :param article: The article to fetch the manuscript files for. + :return: A list of all files related to the article. + """ + + files: List[File] = list() + + for manuscript in article.manuscript_files.all(): + files.append(manuscript) + + for data_file in article.data_figure_files.all(): + files.append(data_file) + + for source_file in article.source_files.all(): + files.append(source_file) + + for supplementary_file in article.supplementary_files.all(): + files.append(supplementary_file) + + return files + + + + + + diff --git a/install/__init__.py b/install/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/install/settings.json b/install/settings.json new file mode 100644 index 0000000..5a6dfaa --- /dev/null +++ b/install/settings.json @@ -0,0 +1,47 @@ +[ + { + "group": { + "name": "plugin:editorial_manager_transfer_service" + }, + "setting": { + "description": "Your journal's submission partner code for Editorial Manager. Capitalization DOES matter.", + "is_translatable": false, + "name": "submission_partner_code", + "pretty_name": "Submission Partner Code", + "type": "char" + }, + "value": { + "default": "" + } + }, + { + "group": { + "name": "plugin:editorial_manager_transfer_service" + }, + "setting": { + "description": "Your journal's license code for Editorial Manager.", + "is_translatable": false, + "name": "license_code", + "pretty_name": "License Code", + "type": "char" + }, + "value": { + "default": "" + } + }, + { + "group": { + "name": "plugin:editorial_manager_transfer_service" + }, + "setting": { + "description": "Your journal's journal code for Editorial Manager.", + "is_translatable": false, + "name": "journal_code", + "pretty_name": "Journal Code", + "type": "char" + }, + "value": { + "default": "" + } + } +] \ No newline at end of file diff --git a/logger_messages.py b/logger_messages.py new file mode 100644 index 0000000..588862c --- /dev/null +++ b/logger_messages.py @@ -0,0 +1,73 @@ +""" +Holds all log messages to remove "magic strings" from the codebase and make reviewing confusing messages easier. +""" +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + +from plugins.editorial_manager_transfer_service.plugin_settings import PLUGIN_NAME, EXPORT_FILE_PATH, IMPORT_FILE_PATH + +def plugin_installation_beginning() -> str: + """ + Gets the log message for a plugin beginning installation. + :return: The logger message. + """ + return '{0} installation beginning...'.format(PLUGIN_NAME) + +def plugin_installed() -> str: + """ + Gets the log message for a plugin being successfully installed. + :return: The logger message. + """ + return '{0} installed.'.format(PLUGIN_NAME) + +def plugin_already_installed() -> str: + """ + Gets the log message for a plugin has been previously installed. + :return: The logger message. + """ + return '{0} is already installed.'.format(PLUGIN_NAME) + +def export_folder_creating() -> str: + """ + Gets the log message for when an export folder is being created. + :return: The logger message. + """ + return '{0} creating export folder (Filepath: \"{1}\")...'.format(PLUGIN_NAME, EXPORT_FILE_PATH) + +def export_folder_created() -> str: + """ + Gets the log message for when an export folder has been created. + :return: The logger message. + """ + return '{0} export folder already exists.'.format(PLUGIN_NAME) + +def import_folder_creating() -> str: + """ + Gets the log message for when an import folder is being created. + :return: The logger message. + """ + return '{0} creating import folder (Filepath: \"{1}\")...'.format(PLUGIN_NAME, IMPORT_FILE_PATH) + +def import_folder_created() -> str: + """ + Gets the log message for when an import folder has been created. + :return: The logger message. + """ + return '{0} import folder already exists.'.format(PLUGIN_NAME) + +def process_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 "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. + :param: article_id: The ID of the article being fetched. + :return: The logger message. + """ + return "Fetching article from database (ID: {0})...".format(article_id) \ No newline at end of file diff --git a/management/__init__.py b/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/commands/__init__.py b/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/management/commands/create_export_zip.py b/management/commands/create_export_zip.py new file mode 100644 index 0000000..71ef178 --- /dev/null +++ b/management/commands/create_export_zip.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + """Creates an export ZIP from an article.""" + + help = "Creates an export ZIP from an article." + + def add_arguments(self, parser): + parser.add_argument('article_id', help="The ID of the article to create a zip file for.") + + def handle(self, *args, **options): + with open(options["article_id"], "r", encoding="utf-8-sig") as article_id: + print("Beginning bundling process for article...") \ No newline at end of file diff --git a/plugin_settings.py b/plugin_settings.py index 21db733..0dcf388 100644 --- a/plugin_settings.py +++ b/plugin_settings.py @@ -1,17 +1,34 @@ +""" +A plugin to provide information for Aries' Editorial Manager to enable automatic transfers. +""" +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + +import os +from utils.logger import get_logger +from django.conf import settings from utils import plugins +import logger_messages PLUGIN_NAME = 'Editorial Manager Transfer Service Plugin' DISPLAY_NAME = 'Editorial Manager Transfer Service' -DESCRIPTION = 'A plugin to provide information for Aries' Editorial Manager to enable automatic transfers.' +DESCRIPTION = 'A plugin to provide information for Aries\' Editorial Manager to enable automatic transfers.' AUTHOR = 'PLOS' VERSION = '0.1' SHORT_NAME = 'editorial_manager_transfer_service' MANAGER_URL = 'editorial_manager_transfer_service_manager' -JANEWAY_VERSION = "1.3.8" +JANEWAY_VERSION = "1.8.0" +EXPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'export') +IMPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'import') +logger = get_logger(__name__) class EditorialManagerTransferServicePlugin(plugins.Plugin): + """ + The plugin class for the Editorial Manager Transfer Service. + """ plugin_name = PLUGIN_NAME display_name = DISPLAY_NAME description = DESCRIPTION @@ -21,11 +38,35 @@ class EditorialManagerTransferServicePlugin(plugins.Plugin): version = VERSION janeway_version = JANEWAY_VERSION - - def install(): - EditorialManagerTransferServicePlugin.install() + """ + Installs the Editorial Manager Transfer Service. + """ + logger.info(logger_messages.plugin_installation_beginning()) + plugin, created = EditorialManagerTransferServicePlugin.install() + + if created: + # Create the export folder. + try: + logger.info(logger_messages.export_folder_creating()) + os.makedirs(EXPORT_FILE_PATH) + except FileExistsError: + logger.info(logger_messages.export_folder_created()) + pass + + # Create the import folder. + try: + logger.info(logger_messages.import_folder_creating()) + os.makedirs(IMPORT_FILE_PATH) + except FileExistsError: + logger.info(logger_messages.import_folder_created()) + pass + + # Log the plugin was installed. + logger.info(logger_messages.plugin_installed()) + else: + logger.info(logger_messages.plugin_already_installed()) def hook_registry(): diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..1ffb8d2 --- /dev/null +++ b/utils.py @@ -0,0 +1,2 @@ +from utils.logger import get_logger + From 11af59234faf2c92ce9ec7a988259d045eab6bd5 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 27 Aug 2025 13:04:41 -0500 Subject: [PATCH 02/31] Most changes completed minus final task to create article page and integrate with XML transformations from RCT-173. --- .gitignore | 1 + file_creation.py | 160 +++++++++++++++++++++++++++++---------------- logger_messages.py | 29 +++++++- plugin_settings.py | 8 ++- 4 files changed, 137 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index c11cac2..e1bff6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /.venv/ /editorial_manager_transfer_service.iml +/.idea/ diff --git a/file_creation.py b/file_creation.py index 1da45c8..2e947ee 100644 --- a/file_creation.py +++ b/file_creation.py @@ -7,24 +7,34 @@ import os import uuid -import zipfile import xml.etree.cElementTree as ET +import zipfile from collections.abc import Sequence from typing import List -from journal.models import Journal -from utils.logger import get_logger -from utils import setting_handler import logger_messages - -from submission.models import Article from core.models import File - +from journal.models import Journal from plugins.editorial_manager_transfer_service.plugin_settings import EXPORT_FILE_PATH +from submission.models import Article +from utils import setting_handler +from utils.logger import get_logger logger = get_logger(__name__) +def create_export_files(article_id: str, journal: Journal) -> List[str]: + """ + Returns a list of file paths to the files to be exported. + :param article_id: The id of the article. + :param journal: The journal where the article is located. + :return: A list of file paths to the paths to be exported. + """ + file_creator: FileCreation = FileCreation(article_id, journal) + + return List([file_creator.get_zip_filepath(), file_creator.get_go_filepath()]) + + def get_article_export_folders() -> List[str]: """ Gets the filepaths for the folders used for exporting articles. @@ -38,29 +48,49 @@ def get_article_export_folders() -> List[str]: class FileCreation: - def __init__(self, journal: Journal): - self.zip_filepath = None - self.go_filepath = None + """ + A class for managing the export file creation process. + """ + + def __init__(self, article_id: str, journal: Journal): + self.zip_filepath: str | None = None + self.go_filepath: str | None = None self.license_code: str = setting_handler.get_setting( - setting_group_name="plugin:editorial_manager_transfer_service", - setting_name="license_code", - journal=journal, - ).processed_value + setting_group_name="plugin:editorial_manager_transfer_service", setting_name="license_code", + journal=journal, ).processed_value self.journal_code: str = setting_handler.get_setting( - setting_group_name="plugin:editorial_manager_transfer_service", - setting_name="journal_code", - journal=journal, - ).processed_value + setting_group_name="plugin:editorial_manager_transfer_service", setting_name="journal_code", + journal=journal, ).processed_value self.submission_partner_code: str = setting_handler.get_setting( - setting_group_name="plugin:editorial_manager_transfer_service", - setting_name="submission_partner_code", - journal=journal, - ).processed_value + setting_group_name="plugin:editorial_manager_transfer_service", setting_name="submission_partner_code", + journal=journal, ).processed_value export_folders: Sequence[str] = get_article_export_folders() self.export_folder: str | None = export_folders[0] if len(export_folders) > 0 else None - def create_export_file(self, article_id: str) -> str: + self.__create_export_file(article_id) + + def get_zip_filepath(self) -> str | None: + """ + Gets the zip file path for the exported files. + :return: The zip file path. + """ + if self.zip_filepath is None: + return None + else: + return self.zip_filepath + + def get_go_filepath(self) -> str | None: + """ + Gets the filepath for the go.xml file for exporting to Editorial Manager. + :return: The filepath for the go.xml file or None, if the process failed. + """ + if self.go_filepath is None: + return None + else: + return self.go_filepath + + def __create_export_file(self, article_id: str): """ Creates the export file for :param article_id: The ID of the article to create an export file for. @@ -73,10 +103,19 @@ def create_export_file(self, article_id: str) -> str: article: Article = self.__fetch_article(article_id) except Exception: logger.error(logger_messages.process_failed_fetching_article(article_id)) - return "" + return + + # Attempt to create the metadata file. + metadata_file: File | None = self.__create_metadata_file(article) + if metadata_file is None: + logger.error(logger_messages.process_failed_fetching_metadata(article_id)) + return - metadata_file: File = self.__create_metadata_file(article) - article_files: List[File] = fetch_article_files(article) + # Attempt to fetch the article files. + article_files: Sequence[File] = fetch_article_files(article) + if len(article_files) <= 0: + logger.error(logger_messages.process_failed_fetching_article_files(article_id)) + return prefix: str = "{0}_{1}".format(self.submission_partner_code, uuid.uuid4()) @@ -90,12 +129,17 @@ def create_export_file(self, article_id: str) -> str: self.__create_go_xml_file(metadata_file.uuid_filename, filenames, prefix) - return self.zip_filepath - - def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequence[str], filename: str) -> None: + def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequence[str], filename: str): + """ + Creates the go xml file for the export process for Editorial Manager. + :param metadata_filename: The name of the metadata file. + :param article_filenames: The filenames of the article's associated files. + :param filename: The name to use for the go.xml file (Must match the name of the zip file). + """ go: ET.Element = ET.Element("GO") go.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") - go.set("xsi:noNamespaceSchemaLocation", "app://Aries.EditorialManager/Resources/XmlDefineTransformFiles/aries_import_go_file.xsd") + go.set("xsi:noNamespaceSchemaLocation", + "app://Aries.EditorialManager/Resources/XmlDefineTransformFiles/aries_import_go_file.xsd") # Format the header. header: ET.Element = ET.SubElement(go, "header") @@ -103,7 +147,8 @@ def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequen ET.SubElement(header, "journal", code=self.journal_code) ET.SubElement(header, "import-type", id="2") parameters: ET.Element = ET.SubElement(header, "parameters") - ET.SubElement(parameters, "parameter", name="license-code", value="{0}_{1}".format(self.submission_partner_code, self.license_code)) + ET.SubElement(parameters, "parameter", name="license-code", + value="{0}_{1}".format(self.submission_partner_code, self.license_code)) # Begin the filegroup. filegroup: ET.Element = ET.SubElement(go, "filegroup") @@ -119,11 +164,16 @@ def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequen self.go_filepath = os.path.join(self.export_folder, "{0}.go.xml".format(filename)); tree.write(self.go_filepath) - def __fetch_article(self, article_id: str) -> Article: - # TODO: Finish method. - pass + @staticmethod + def __fetch_article(article_id: str) -> Article: + """ + Gets the article object for the given article ID. + :param article_id: The ID of the article. + :return: The article object with the given article ID. + """ + return Article.get_article(article_id) - def __create_metadata_file(self, article: Article) -> File: + def __create_metadata_file(self, article: Article) -> File | None: """ Creates the metadata file based on the given article. :param article: The article to convert to JATS. @@ -131,32 +181,26 @@ def __create_metadata_file(self, article: Article) -> File: """ pass + @staticmethod + def fetch_article_files(article: Article) -> List[File]: + """ + Fetches the manuscript (or content or body) of an article alongside any other files associated with it. + :param article: The article to fetch the manuscript files for. + :return: A list of all files related to the article. + """ -def fetch_article_files(article: Article) -> List[File]: - """ - Fetches the manuscript (or content or body) of an article alongside any other files associated with it. - :param article: The article to fetch the manuscript files for. - :return: A list of all files related to the article. - """ - - files: List[File] = list() - - for manuscript in article.manuscript_files.all(): - files.append(manuscript) - - for data_file in article.data_figure_files.all(): - files.append(data_file) - - for source_file in article.source_files.all(): - files.append(source_file) - - for supplementary_file in article.supplementary_files.all(): - files.append(supplementary_file) - - return files - + files: List[File] = list() + for manuscript in article.manuscript_files.all(): + files.append(manuscript) + for data_file in article.data_figure_files.all(): + files.append(data_file) + for source_file in article.source_files.all(): + files.append(source_file) + for supplementary_file in article.supplementary_files.all(): + files.append(supplementary_file) + return files diff --git a/logger_messages.py b/logger_messages.py index 588862c..3b25d2d 100644 --- a/logger_messages.py +++ b/logger_messages.py @@ -7,6 +7,7 @@ from plugins.editorial_manager_transfer_service.plugin_settings import PLUGIN_NAME, EXPORT_FILE_PATH, IMPORT_FILE_PATH + def plugin_installation_beginning() -> str: """ Gets the log message for a plugin beginning installation. @@ -14,6 +15,7 @@ def plugin_installation_beginning() -> str: """ return '{0} installation beginning...'.format(PLUGIN_NAME) + def plugin_installed() -> str: """ Gets the log message for a plugin being successfully installed. @@ -21,6 +23,7 @@ def plugin_installed() -> str: """ return '{0} installed.'.format(PLUGIN_NAME) + def plugin_already_installed() -> str: """ Gets the log message for a plugin has been previously installed. @@ -28,6 +31,7 @@ def plugin_already_installed() -> str: """ return '{0} is already installed.'.format(PLUGIN_NAME) + def export_folder_creating() -> str: """ Gets the log message for when an export folder is being created. @@ -35,6 +39,7 @@ def export_folder_creating() -> str: """ return '{0} creating export folder (Filepath: \"{1}\")...'.format(PLUGIN_NAME, EXPORT_FILE_PATH) + def export_folder_created() -> str: """ Gets the log message for when an export folder has been created. @@ -42,6 +47,7 @@ def export_folder_created() -> str: """ return '{0} export folder already exists.'.format(PLUGIN_NAME) + def import_folder_creating() -> str: """ Gets the log message for when an import folder is being created. @@ -49,6 +55,7 @@ def import_folder_creating() -> str: """ return '{0} creating import folder (Filepath: \"{1}\")...'.format(PLUGIN_NAME, IMPORT_FILE_PATH) + def import_folder_created() -> str: """ Gets the log message for when an import folder has been created. @@ -56,6 +63,7 @@ def import_folder_created() -> str: """ return '{0} import folder already exists.'.format(PLUGIN_NAME) + def process_fetching_article(article_id: str) -> str: """ Gets the log message for when an article is being fetched from the database. @@ -64,10 +72,29 @@ 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: """ Gets the log message for when an article failed to be fetched. :param: article_id: The ID of the article being fetched. :return: The logger message. """ - return "Fetching article from database (ID: {0})...".format(article_id) \ No newline at end of file + return "Fetching article from database (ID: {0}) failed. Discontinuing export process.".format(article_id) + + +def process_failed_fetching_metadata(article_id): + """ + Gets the log message for when an article failed to be fetched. + :param: article_id: The ID of the article being fetched. + :return: The logger message. + """ + return "Fetching article (ID: {0}) metadata failed. Discontinuing export process.".format(article_id) + + +def process_failed_fetching_article_files(article_id): + """ + Gets the log message for when an article failed to be fetched. + :param: article_id: The ID of the article being fetched. + :return: The logger message. + """ + return "Fetching files for article (ID: {0}) failed. Discontinuing export process.".format(article_id) diff --git a/plugin_settings.py b/plugin_settings.py index 0dcf388..93176ee 100644 --- a/plugin_settings.py +++ b/plugin_settings.py @@ -6,10 +6,12 @@ __maintainer__ = "The Public Library of Science (PLOS)" import os -from utils.logger import get_logger + from django.conf import settings -from utils import plugins + import logger_messages +from utils import plugins +from utils.logger import get_logger PLUGIN_NAME = 'Editorial Manager Transfer Service Plugin' DISPLAY_NAME = 'Editorial Manager Transfer Service' @@ -25,6 +27,7 @@ logger = get_logger(__name__) + class EditorialManagerTransferServicePlugin(plugins.Plugin): """ The plugin class for the Editorial Manager Transfer Service. @@ -39,6 +42,7 @@ class EditorialManagerTransferServicePlugin(plugins.Plugin): version = VERSION janeway_version = JANEWAY_VERSION + def install(): """ Installs the Editorial Manager Transfer Service. From 5d4c7732e429f0f3f4d006eded05413d9ff4ea37 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 27 Aug 2025 13:08:12 -0500 Subject: [PATCH 03/31] Update minor typos. --- file_creation.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/file_creation.py b/file_creation.py index 2e947ee..f76d6a7 100644 --- a/file_creation.py +++ b/file_creation.py @@ -7,7 +7,7 @@ import os import uuid -import xml.etree.cElementTree as ET +import xml.etree.cElementTree as ETree import zipfile from collections.abc import Sequence from typing import List @@ -112,7 +112,7 @@ def __create_export_file(self, article_id: str): return # Attempt to fetch the article files. - article_files: Sequence[File] = fetch_article_files(article) + article_files: Sequence[File] = self.fetch_article_files(article) if len(article_files) <= 0: logger.error(logger_messages.process_failed_fetching_article_files(article_id)) return @@ -136,32 +136,32 @@ def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequen :param article_filenames: The filenames of the article's associated files. :param filename: The name to use for the go.xml file (Must match the name of the zip file). """ - go: ET.Element = ET.Element("GO") + go: ETree.Element = ETree.Element("GO") go.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") go.set("xsi:noNamespaceSchemaLocation", "app://Aries.EditorialManager/Resources/XmlDefineTransformFiles/aries_import_go_file.xsd") # Format the header. - header: ET.Element = ET.SubElement(go, "header") - ET.SubElement(header, "version", number="1.0") - ET.SubElement(header, "journal", code=self.journal_code) - ET.SubElement(header, "import-type", id="2") - parameters: ET.Element = ET.SubElement(header, "parameters") - ET.SubElement(parameters, "parameter", name="license-code", - value="{0}_{1}".format(self.submission_partner_code, self.license_code)) + header: ETree.Element = ETree.SubElement(go, "header") + ETree.SubElement(header, "version", number="1.0") + ETree.SubElement(header, "journal", code=self.journal_code) + ETree.SubElement(header, "import-type", id="2") + parameters: ETree.Element = ETree.SubElement(header, "parameters") + ETree.SubElement(parameters, "parameter", name="license-code", + value="{0}_{1}".format(self.submission_partner_code, self.license_code)) # Begin the filegroup. - filegroup: ET.Element = ET.SubElement(go, "filegroup") + filegroup: ETree.Element = ETree.SubElement(go, "filegroup") # Create the archive and metadata files. - ET.SubElement(filegroup, "archive-file", name="{0}.zip".format(filename)) - ET.SubElement(filegroup, "metadata-file", name=metadata_filename) + ETree.SubElement(filegroup, "archive-file", name="{0}.zip".format(filename)) + ETree.SubElement(filegroup, "metadata-file", name=metadata_filename) for article_filename in article_filenames: - ET.SubElement(filegroup, "file", name=article_filename) + ETree.SubElement(filegroup, "file", name=article_filename) - tree = ET.ElementTree(go) - self.go_filepath = os.path.join(self.export_folder, "{0}.go.xml".format(filename)); + tree = ETree.ElementTree(go) + self.go_filepath = os.path.join(self.export_folder, "{0}.go.xml".format(filename)) tree.write(self.go_filepath) @staticmethod From 0141c613dcd66ae80813c08f5c9f55a8285665c4 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 27 Aug 2025 13:35:54 -0500 Subject: [PATCH 04/31] Rewrites to exclude the need for a journal to be handed over. --- file_creation.py | 67 +++++++++++++----------- logger_messages.py | 13 ++++- management/commands/create_export_zip.py | 27 ++++++++-- 3 files changed, 72 insertions(+), 35 deletions(-) diff --git a/file_creation.py b/file_creation.py index f76d6a7..cf5e8d0 100644 --- a/file_creation.py +++ b/file_creation.py @@ -23,14 +23,14 @@ logger = get_logger(__name__) -def create_export_files(article_id: str, journal: Journal) -> List[str]: +def create_export_files(article_id: str) -> List[str]: """ Returns a list of file paths to the files to be exported. :param article_id: The id of the article. :param journal: The journal where the article is located. :return: A list of file paths to the paths to be exported. """ - file_creator: FileCreation = FileCreation(article_id, journal) + file_creator: FileCreation = FileCreation(article_id) return List([file_creator.get_zip_filepath(), file_creator.get_go_filepath()]) @@ -52,23 +52,38 @@ class FileCreation: A class for managing the export file creation process. """ - def __init__(self, article_id: str, journal: Journal): + def __init__(self, article_id: str): self.zip_filepath: str | None = None self.go_filepath: str | None = None + + # 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(article_id) + except Article.DoesNotExist: + logger.error(logger_messages.process_failed_fetching_article(article_id)) + return + + # Attempt to get the journal. + self.journal: Journal = self.article.journal + if self.journal is None: + logger.error(logger_messages.process_failed_fetching_journal(article_id)) + return + self.license_code: str = setting_handler.get_setting( setting_group_name="plugin:editorial_manager_transfer_service", setting_name="license_code", - journal=journal, ).processed_value + journal=self.journal, ).processed_value self.journal_code: str = setting_handler.get_setting( setting_group_name="plugin:editorial_manager_transfer_service", setting_name="journal_code", - journal=journal, ).processed_value + journal=self.journal, ).processed_value self.submission_partner_code: str = setting_handler.get_setting( setting_group_name="plugin:editorial_manager_transfer_service", setting_name="submission_partner_code", - journal=journal, ).processed_value + journal=self.journal, ).processed_value export_folders: Sequence[str] = get_article_export_folders() self.export_folder: str | None = export_folders[0] if len(export_folders) > 0 else None - self.__create_export_file(article_id) + self.__create_export_file() def get_zip_filepath(self) -> str | None: """ @@ -90,29 +105,21 @@ def get_go_filepath(self) -> str | None: else: return self.go_filepath - def __create_export_file(self, article_id: str): + def __create_export_file(self): """ Creates the export file for - :param article_id: The ID of the article to create an export file for. - :return: The filepath to the created export file. """ - # Get the article based upon the given article ID. - logger.info(logger_messages.process_fetching_article(article_id)) - try: - article: Article = self.__fetch_article(article_id) - except Exception: - logger.error(logger_messages.process_failed_fetching_article(article_id)) - return + article_id: str = self.article.pk # Attempt to create the metadata file. - metadata_file: File | None = self.__create_metadata_file(article) + metadata_file: File | None = self.__create_metadata_file(self.article) if metadata_file is None: logger.error(logger_messages.process_failed_fetching_metadata(article_id)) return # Attempt to fetch the article files. - article_files: Sequence[File] = self.fetch_article_files(article) + article_files: Sequence[File] = self.fetch_article_files(self.article) if len(article_files) <= 0: logger.error(logger_messages.process_failed_fetching_article_files(article_id)) return @@ -121,9 +128,9 @@ def __create_export_file(self, article_id: str): self.zip_filepath: str = os.path.join(self.export_folder, "{0}.zip".format(prefix)) with zipfile.ZipFile(self.zip_filepath, "w") as zipf: - zipf.write(metadata_file.get_file_path(article)) + zipf.write(metadata_file.get_file_path(self.article)) for article_file in article_files: - zipf.write(article_file.get_file_path(article)) + zipf.write(article_file.get_file_path(self.article)) filenames: Sequence[str] = zipf.namelist() zipf.close() @@ -164,15 +171,6 @@ def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequen self.go_filepath = os.path.join(self.export_folder, "{0}.go.xml".format(filename)) tree.write(self.go_filepath) - @staticmethod - def __fetch_article(article_id: str) -> Article: - """ - Gets the article object for the given article ID. - :param article_id: The ID of the article. - :return: The article object with the given article ID. - """ - return Article.get_article(article_id) - def __create_metadata_file(self, article: Article) -> File | None: """ Creates the metadata file based on the given article. @@ -181,6 +179,15 @@ def __create_metadata_file(self, article: Article) -> File | None: """ pass + @staticmethod + def __fetch_article(article_id: str) -> Article: + """ + Gets the article object for the given article ID. + :param article_id: The ID of the article. + :return: The article object with the given article ID. + """ + return Article.get_article(article_id).get_deferred_fields() + @staticmethod def fetch_article_files(article: Article) -> List[File]: """ diff --git a/logger_messages.py b/logger_messages.py index 3b25d2d..b646285 100644 --- a/logger_messages.py +++ b/logger_messages.py @@ -84,7 +84,7 @@ def process_failed_fetching_article(article_id: str) -> str: def process_failed_fetching_metadata(article_id): """ - Gets the log message for when an article failed to be fetched. + Gets the log message for when an article's metadata failed to be fetched. :param: article_id: The ID of the article being fetched. :return: The logger message. """ @@ -93,8 +93,17 @@ def process_failed_fetching_metadata(article_id): def process_failed_fetching_article_files(article_id): """ - Gets the log message for when an article failed to be fetched. + Gets the log message for when an article's files failed to be fetched. :param: article_id: The ID of the article being fetched. :return: The logger message. """ return "Fetching files for article (ID: {0}) failed. Discontinuing export process.".format(article_id) + + +def process_failed_fetching_journal(article_id): + """ + Gets the log message for when an article's journal failed to be fetched. + :param: article_id: The ID of the article. + :return: The logger message. + """ + return "Fetching journal where article (ID: {0}) lives failed. Discontinuing export process.".format(article_id) \ No newline at end of file diff --git a/management/commands/create_export_zip.py b/management/commands/create_export_zip.py index 71ef178..5a98e35 100644 --- a/management/commands/create_export_zip.py +++ b/management/commands/create_export_zip.py @@ -1,4 +1,17 @@ -from django.core.management.base import BaseCommand +""" +Commands for exporting and importing files to/form Aries's Editorial Manager. +""" + +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + +from typing import Sequence + +from django.core.management.base import BaseCommand, CommandError + +import plugins.editorial_manager_transfer_service.file_creation as file_creation + class Command(BaseCommand): """Creates an export ZIP from an article.""" @@ -9,5 +22,13 @@ def add_arguments(self, parser): parser.add_argument('article_id', help="The ID of the article to create a zip file for.") def handle(self, *args, **options): - with open(options["article_id"], "r", encoding="utf-8-sig") as article_id: - print("Beginning bundling process for article...") \ No newline at end of file + article_id: str = open(options["article_id"], "r", encoding="utf-8-sig").read().strip() + + print("Beginning bundling process for article...") + export_files: Sequence[str] = file_creation.create_export_files(article_id) + if not export_files: + raise CommandError("Error while creating export ZIP.") + + print("Export files created.") + + # TODO: Initiate HTTP request to FTP server. From 4a6a413b8b224d95fc6804c9e9532f2fd469a879 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 27 Aug 2025 14:03:40 -0500 Subject: [PATCH 05/31] Updates to create a singleton class service to be utilized easily. --- file_creation.py | 21 +++---- file_transfer_service.py | 80 ++++++++++++++++++++++++ management/commands/create_export_zip.py | 12 ++-- 3 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 file_transfer_service.py diff --git a/file_creation.py b/file_creation.py index cf5e8d0..b568bb1 100644 --- a/file_creation.py +++ b/file_creation.py @@ -23,18 +23,6 @@ logger = get_logger(__name__) -def create_export_files(article_id: str) -> List[str]: - """ - Returns a list of file paths to the files to be exported. - :param article_id: The id of the article. - :param journal: The journal where the article is located. - :return: A list of file paths to the paths to be exported. - """ - file_creator: FileCreation = FileCreation(article_id) - - return List([file_creator.get_zip_filepath(), file_creator.get_go_filepath()]) - - def get_article_export_folders() -> List[str]: """ Gets the filepaths for the folders used for exporting articles. @@ -47,7 +35,7 @@ def get_article_export_folders() -> List[str]: return [] -class FileCreation: +class ExportFileCreation: """ A class for managing the export file creation process. """ @@ -55,6 +43,7 @@ class FileCreation: def __init__(self, article_id: str): self.zip_filepath: str | None = None self.go_filepath: str | None = None + self.in_error_state: bool = False # Get the article based upon the given article ID. logger.info(logger_messages.process_fetching_article(article_id)) @@ -62,12 +51,14 @@ def __init__(self, article_id: str): self.article: Article = self.__fetch_article(article_id) except Article.DoesNotExist: logger.error(logger_messages.process_failed_fetching_article(article_id)) + self.in_error_state = True return # Attempt to get the journal. self.journal: Journal = self.article.journal if self.journal is None: logger.error(logger_messages.process_failed_fetching_journal(article_id)) + self.in_error_state = True return self.license_code: str = setting_handler.get_setting( @@ -91,6 +82,7 @@ def get_zip_filepath(self) -> str | None: :return: The zip file path. """ if self.zip_filepath is None: + self.in_error_state = True return None else: return self.zip_filepath @@ -101,6 +93,7 @@ def get_go_filepath(self) -> str | None: :return: The filepath for the go.xml file or None, if the process failed. """ if self.go_filepath is None: + self.in_error_state = True return None else: return self.go_filepath @@ -116,12 +109,14 @@ def __create_export_file(self): metadata_file: File | None = self.__create_metadata_file(self.article) if metadata_file is None: logger.error(logger_messages.process_failed_fetching_metadata(article_id)) + self.in_error_state = True return # 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(article_id)) + self.in_error_state = True return prefix: str = "{0}_{1}".format(self.submission_partner_code, uuid.uuid4()) diff --git a/file_transfer_service.py b/file_transfer_service.py new file mode 100644 index 0000000..1cb6de9 --- /dev/null +++ b/file_transfer_service.py @@ -0,0 +1,80 @@ +""" +This service is used to manage the transfers to and from Aries's Editorial Manager system. +""" +__author__ = "Rosetta Reatherford" +__license__ = "AGPL v3" +__maintainer__ = "The Public Library of Science (PLOS)" + +from plugins.editorial_manager_transfer_service.file_creation import ExportFileCreation +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class FileTransferService: + """ + Manages the transfers to and from Aries's Editorial Manager system. + """ + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + """ + Constructor. + """ + if not hasattr(self, '_initialized'): # Prevent re-initialization on subsequent calls + self.exports: dict[str, ExportFileCreation] = dict() + self._initialized = True + + def get_export_file_creator(self, article_id: str) -> ExportFileCreation | None: + """ + Gets the export file creator for the given article. + :param article_id: The article id. + :return: The export file creator. + """ + if article_id not in self.exports: + file_creator = ExportFileCreation(article_id) + if file_creator.in_error_state: + return None + self.exports[article_id] = file_creator + return self.exports[article_id] + + def get_export_zip_filepath(self, article_id: str) -> str | None: + """ + Gets the export zip file path for the given article. + :param article_id: The article id. + :return: The export zip file path. + """ + file_export_creator = self.get_export_file_creator(article_id) + return file_export_creator.get_zip_filepath() if file_export_creator else None + + def get_export_go_filepath(self, article_id: str) -> str | None: + """ + Gets the export go file path for the given article. + :param article_id: The article id. + :return: The export go file path. + """ + file_export_creator = self.get_export_file_creator(article_id) + return file_export_creator.get_go_filepath() if file_export_creator else None + + +def get_export_zip_filepath(article_id: str) -> str | None: + """ + Gets the zip file path for a given article. + :param article_id: The article id. + :return: The zip file path. + """ + return FileTransferService().get_export_zip_filepath(article_id) + + +def get_export_go_filepath(article_id: str) -> str | None: + """ + Gets the export file path for a go file created for a given article. + :param article_id: The article id. + :return: The export go file path. + """ + return FileTransferService().get_export_go_filepath(article_id) diff --git a/management/commands/create_export_zip.py b/management/commands/create_export_zip.py index 5a98e35..3878467 100644 --- a/management/commands/create_export_zip.py +++ b/management/commands/create_export_zip.py @@ -6,11 +6,9 @@ __license__ = "AGPL v3" __maintainer__ = "The Public Library of Science (PLOS)" -from typing import Sequence - from django.core.management.base import BaseCommand, CommandError -import plugins.editorial_manager_transfer_service.file_creation as file_creation +import plugins.editorial_manager_transfer_service.file_transfer_service as file_transfer_service class Command(BaseCommand): @@ -25,10 +23,14 @@ def handle(self, *args, **options): article_id: str = open(options["article_id"], "r", encoding="utf-8-sig").read().strip() print("Beginning bundling process for article...") - export_files: Sequence[str] = file_creation.create_export_files(article_id) - if not export_files: + export_zip_file: str = file_transfer_service.get_export_zip_filepath(article_id) + if not export_zip_file: raise CommandError("Error while creating export ZIP.") + export_go_file: str = file_transfer_service.get_export_go_filepath(article_id) + if not export_go_file: + raise CommandError("Error while creating export GO file.") + print("Export files created.") # TODO: Initiate HTTP request to FTP server. From b4a4cff15526c457ed7a586c929d0487db1be2a3 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 27 Aug 2025 20:26:37 -0500 Subject: [PATCH 06/31] Updates to fix build issues during plugin installation. --- file_creation.py | 7 +++---- logger_messages.py | 10 ++++++++-- plugin_settings.py | 13 ++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/file_creation.py b/file_creation.py index b568bb1..5a75061 100644 --- a/file_creation.py +++ b/file_creation.py @@ -12,10 +12,9 @@ from collections.abc import Sequence from typing import List -import logger_messages +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.plugin_settings import EXPORT_FILE_PATH from submission.models import Article from utils import setting_handler from utils.logger import get_logger @@ -29,8 +28,8 @@ def get_article_export_folders() -> List[str]: :return: A list of filepaths for the export folders. """ - if os.path.exists(EXPORT_FILE_PATH): - return os.listdir(EXPORT_FILE_PATH) + if os.path.exists(logger_messages.EXPORT_FILE_PATH): + return os.listdir(logger_messages.EXPORT_FILE_PATH) else: return [] diff --git a/logger_messages.py b/logger_messages.py index b646285..b1f5565 100644 --- a/logger_messages.py +++ b/logger_messages.py @@ -5,7 +5,13 @@ __license__ = "AGPL v3" __maintainer__ = "The Public Library of Science (PLOS)" -from plugins.editorial_manager_transfer_service.plugin_settings import PLUGIN_NAME, EXPORT_FILE_PATH, IMPORT_FILE_PATH +import os + +from django.conf import settings + +PLUGIN_NAME = 'Editorial Manager Transfer Service Plugin' +EXPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'export') +IMPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'import') def plugin_installation_beginning() -> str: @@ -106,4 +112,4 @@ def process_failed_fetching_journal(article_id): :param: article_id: The ID of the article. :return: The logger message. """ - return "Fetching journal where article (ID: {0}) lives failed. Discontinuing export process.".format(article_id) \ No newline at end of file + return "Fetching journal where article (ID: {0}) lives failed. Discontinuing export process.".format(article_id) diff --git a/plugin_settings.py b/plugin_settings.py index 93176ee..06046ae 100644 --- a/plugin_settings.py +++ b/plugin_settings.py @@ -7,13 +7,11 @@ import os -from django.conf import settings - -import logger_messages +import plugins.editorial_manager_transfer_service.logger_messages as logger_messages from utils import plugins from utils.logger import get_logger -PLUGIN_NAME = 'Editorial Manager Transfer Service Plugin' +PLUGIN_NAME = logger_messages.PLUGIN_NAME DISPLAY_NAME = 'Editorial Manager Transfer Service' DESCRIPTION = 'A plugin to provide information for Aries\' Editorial Manager to enable automatic transfers.' AUTHOR = 'PLOS' @@ -22,9 +20,6 @@ MANAGER_URL = 'editorial_manager_transfer_service_manager' JANEWAY_VERSION = "1.8.0" -EXPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'export') -IMPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'import') - logger = get_logger(__name__) @@ -54,7 +49,7 @@ def install(): # Create the export folder. try: logger.info(logger_messages.export_folder_creating()) - os.makedirs(EXPORT_FILE_PATH) + os.makedirs(logger_messages.EXPORT_FILE_PATH) except FileExistsError: logger.info(logger_messages.export_folder_created()) pass @@ -62,7 +57,7 @@ def install(): # Create the import folder. try: logger.info(logger_messages.import_folder_creating()) - os.makedirs(IMPORT_FILE_PATH) + os.makedirs(logger_messages.IMPORT_FILE_PATH) except FileExistsError: logger.info(logger_messages.import_folder_created()) pass From 64790008eb354c7d1884a633ecf92d6e7cc14b58 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Thu, 28 Aug 2025 13:41:56 -0500 Subject: [PATCH 07/31] Cleanup code and add simple testing. --- consts.py | 30 ++++++++ dev-requirements.txt | 3 +- file_creation.py | 142 +++++++++++++++++++++++++++--------- file_transfer_service.py | 38 ++++++++++ logger_messages.py | 30 +++++--- plugin_settings.py | 30 +++----- tests/test_file_creation.py | 107 +++++++++++++++++++++++++++ 7 files changed, 315 insertions(+), 65 deletions(-) create mode 100644 consts.py create mode 100644 tests/test_file_creation.py diff --git a/consts.py b/consts.py new file mode 100644 index 0000000..b12390c --- /dev/null +++ b/consts.py @@ -0,0 +1,30 @@ +import os + +from django.conf import settings + +# Plugin Settings +PLUGIN_NAME = 'Editorial Manager Transfer Service Plugin' +DISPLAY_NAME = 'Editorial Manager Transfer Service' +DESCRIPTION = 'A plugin to provide information for Aries\' Editorial Manager to enable automatic transfers.' +AUTHOR = 'PLOS' +VERSION = '0.1' +SHORT_NAME = 'editorial_manager_transfer_service' +MANAGER_URL = 'editorial_manager_transfer_service_manager' +JANEWAY_VERSION = "1.8.0" + +# Setting Configuration +PLUGIN_SETTINGS_GROUP_NAME = "plugin:editorial_manager_transfer_service" +PLUGIN_SETTINGS_LICENSE_CODE = "license_code" +PLUGIN_SETTINGS_JOURNAL_CODE = "journal_code" +PLUGIN_SETTINGS_SUBMISSION_PARTNER_CODE = "submission_partner_code" + +# Import and export filepaths +EXPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'export') +IMPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'import') + +# XML File +GO_FILE_ELEMENT_TAG_GO = "GO" +GO_FILE_GO_ELEMENT_ATTRIBUTE_XMLNS_XSI_KEY = "xmlns:xsi" +GO_FILE_GO_ELEMENT_ATTRIBUTE_XMLNS_XSI_VALUE = "http://www.w3.org/2001/XMLSchema-instance" +GO_FILE_GO_ELEMENT_ATTRIBUTE_SCHEMA_LOCATION_KEY = "xsi:noNamespaceSchemaLocation" +GO_FILE_GO_ELEMENT_ATTRIBUTE_SCHEMA_LOCATION_VALUE = "app://Aries.EditorialManager/Resources/XmlDefineTransformFiles/aries_import_go_file.xsd" diff --git a/dev-requirements.txt b/dev-requirements.txt index e079f8a..2da04d8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1 +1,2 @@ -pytest +hypothesis==6.138.7 +pytest==8.4.1 \ No newline at end of file diff --git a/file_creation.py b/file_creation.py index 5a75061..07781e6 100644 --- a/file_creation.py +++ b/file_creation.py @@ -12,6 +12,9 @@ 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 journal.models import Journal @@ -22,16 +25,16 @@ logger = get_logger(__name__) -def get_article_export_folders() -> List[str]: +def get_article_export_folders() -> str: """ Gets the filepaths for the folders used for exporting articles. :return: A list of filepaths for the export folders. """ - if os.path.exists(logger_messages.EXPORT_FILE_PATH): - return os.listdir(logger_messages.EXPORT_FILE_PATH) + if os.path.exists(consts.EXPORT_FILE_PATH): + return consts.EXPORT_FILE_PATH else: - return [] + return "" class ExportFileCreation: @@ -43,6 +46,19 @@ def __init__(self, article_id: str): 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: 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 # Get the article based upon the given article ID. logger.info(logger_messages.process_fetching_article(article_id)) @@ -60,19 +76,15 @@ def __init__(self, article_id: str): self.in_error_state = True return - self.license_code: str = setting_handler.get_setting( - setting_group_name="plugin:editorial_manager_transfer_service", setting_name="license_code", - journal=self.journal, ).processed_value - self.journal_code: str = setting_handler.get_setting( - setting_group_name="plugin:editorial_manager_transfer_service", setting_name="journal_code", - journal=self.journal, ).processed_value - self.submission_partner_code: str = setting_handler.get_setting( - setting_group_name="plugin:editorial_manager_transfer_service", setting_name="submission_partner_code", - journal=self.journal, ).processed_value - - export_folders: Sequence[str] = get_article_export_folders() - self.export_folder: str | None = export_folders[0] if len(export_folders) > 0 else None + # 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.in_error_state = True + return + self.export_folder = export_folders + # Start export process self.__create_export_file() def get_zip_filepath(self) -> str | None: @@ -102,33 +114,89 @@ def __create_export_file(self): Creates the export file for """ - article_id: str = self.article.pk - - # Attempt to create the metadata file. - metadata_file: File | None = self.__create_metadata_file(self.article) - if metadata_file is None: - logger.error(logger_messages.process_failed_fetching_metadata(article_id)) + if not self.can_export(): self.in_error_state = True return + # TODO: Attempt to create the metadata file. + # metadata_file: File | None = self.__create_metadata_file(self.article) + # if metadata_file is None: + # logger.error(logger_messages.process_failed_fetching_metadata(self.article_id)) + # self.in_error_state = True + # return + # Attempt to fetch the article files. - article_files: Sequence[File] = self.fetch_article_files(self.article) + article_files: Sequence[File] = self.__fetch_article_files(self.article) if len(article_files) <= 0: - logger.error(logger_messages.process_failed_fetching_article_files(article_id)) + logger.error(logger_messages.process_failed_fetching_article_files(self.article_id)) self.in_error_state = True return - prefix: str = "{0}_{1}".format(self.submission_partner_code, uuid.uuid4()) + prefix: str = "{0}_{1}".format(self.get_submission_partner_code(), uuid.uuid4()) self.zip_filepath: str = os.path.join(self.export_folder, "{0}.zip".format(prefix)) with zipfile.ZipFile(self.zip_filepath, "w") as zipf: - zipf.write(metadata_file.get_file_path(self.article)) + # TODO: zipf.write(metadata_file.get_file_path(self.article)) for article_file in article_files: zipf.write(article_file.get_file_path(self.article)) filenames: Sequence[str] = zipf.namelist() zipf.close() - self.__create_go_xml_file(metadata_file.uuid_filename, filenames, prefix) + # TODO: Remove below and replace with 'self.__create_go_xml_file(metadata_file.uuid_filename, filenames, prefix)' + self.__create_go_xml_file("fake name", filenames, prefix) + + def get_license_code(self) -> str: + """ + Gets the license code for exporting files. + :return: The license code or None, if the process failed. + """ + if not self.__license_code: + self.__license_code: str = self.get_setting(consts.PLUGIN_SETTINGS_LICENSE_CODE) + return self.__license_code + + def get_journal_code(self) -> str: + """ + Gets the journal code for exporting files. + :return: The journal code or None, if the process failed. + """ + if not self.__journal_code: + self.__journal_code: str = self.get_setting(consts.PLUGIN_SETTINGS_JOURNAL_CODE) + return self.__journal_code + + def get_submission_partner_code(self) -> str: + """ + Gets the submission partner code for exporting files. + :return: The submission partner code or None, if the process failed. + """ + if not self.__submission_partner_code: + self.__submission_partner_code: str = self.get_setting(consts.PLUGIN_SETTINGS_SUBMISSION_PARTNER_CODE) + return self.__submission_partner_code + + def can_export(self) -> bool: + """ + Checks if the export file can be created. + :return: True if the export file can be created, False otherwise. + """ + return (not self.in_error_state and + self.article is not None and + self.journal is not None and + self.get_license_code() is not None and + self.get_journal_code() is not None and + self.get_submission_partner_code() is not None) + + def get_setting(self, setting_name: str) -> str: + """ + Gets the setting for the given setting name. + :param setting_name: The name of the setting to get the value for. + :return: The value for the given setting or a blank string, if the process failed. + """ + 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)) + self.in_error_state = True + return "" def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequence[str], filename: str): """ @@ -137,19 +205,23 @@ def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequen :param article_filenames: The filenames of the article's associated files. :param filename: The name to use for the go.xml file (Must match the name of the zip file). """ - go: ETree.Element = ETree.Element("GO") - go.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") - go.set("xsi:noNamespaceSchemaLocation", - "app://Aries.EditorialManager/Resources/XmlDefineTransformFiles/aries_import_go_file.xsd") + if not self.can_export(): + self.in_error_state = True + return + + go: ETree.Element = ETree.Element(consts.GO_FILE_ELEMENT_TAG_GO) + go.set(consts.GO_FILE_GO_ELEMENT_ATTRIBUTE_XMLNS_XSI_KEY, consts.GO_FILE_GO_ELEMENT_ATTRIBUTE_XMLNS_XSI_VALUE) + go.set(consts.GO_FILE_GO_ELEMENT_ATTRIBUTE_SCHEMA_LOCATION_KEY, + consts.GO_FILE_GO_ELEMENT_ATTRIBUTE_SCHEMA_LOCATION_VALUE) # Format the header. header: ETree.Element = ETree.SubElement(go, "header") ETree.SubElement(header, "version", number="1.0") - ETree.SubElement(header, "journal", code=self.journal_code) + ETree.SubElement(header, "journal", code=self.get_journal_code()) ETree.SubElement(header, "import-type", id="2") parameters: ETree.Element = ETree.SubElement(header, "parameters") ETree.SubElement(parameters, "parameter", name="license-code", - value="{0}_{1}".format(self.submission_partner_code, self.license_code)) + value="{0}_{1}".format(self.get_submission_partner_code(), self.get_license_code())) # Begin the filegroup. filegroup: ETree.Element = ETree.SubElement(go, "filegroup") @@ -180,10 +252,10 @@ def __fetch_article(article_id: str) -> Article: :param article_id: The ID of the article. :return: The article object with the given article ID. """ - return Article.get_article(article_id).get_deferred_fields() + return Article.get_article(article_id) @staticmethod - def fetch_article_files(article: Article) -> List[File]: + def __fetch_article_files(article: Article) -> List[File]: """ Fetches the manuscript (or content or body) of an article alongside any other files associated with it. :param article: The article to fetch the manuscript files for. diff --git a/file_transfer_service.py b/file_transfer_service.py index 1cb6de9..920bdb1 100644 --- a/file_transfer_service.py +++ b/file_transfer_service.py @@ -5,6 +5,9 @@ __license__ = "AGPL v3" __maintainer__ = "The Public Library of Science (PLOS)" +import os +from typing import List + from plugins.editorial_manager_transfer_service.file_creation import ExportFileCreation from utils.logger import get_logger @@ -28,6 +31,7 @@ def __init__(self): """ if not hasattr(self, '_initialized'): # Prevent re-initialization on subsequent calls self.exports: dict[str, ExportFileCreation] = dict() + self.files_to_delete: List[str] = list() self._initialized = True def get_export_file_creator(self, article_id: str) -> ExportFileCreation | None: @@ -61,6 +65,32 @@ def get_export_go_filepath(self, article_id: str) -> str | None: file_export_creator = self.get_export_file_creator(article_id) return file_export_creator.get_go_filepath() if file_export_creator else None + def delete_export_files(self, article_id: str) -> None: + if article_id not in self.exports: + return + file_exporter = self.exports.pop(article_id) + + self.files_to_delete.append(file_exporter.get_zip_filepath()) + self.files_to_delete.append(file_exporter.get_go_filepath()) + + del file_exporter + + def __delete_files(self) -> None: + for file in self.files_to_delete: + if self.__delete_file(file): + self.files_to_delete.remove(file) + + @staticmethod + def __delete_file(filepath: str) -> bool: + if not os.path.exists(filepath): + return True + try: + os.remove(filepath) + except OSError: + return False + + return True + def get_export_zip_filepath(article_id: str) -> str | None: """ @@ -78,3 +108,11 @@ def get_export_go_filepath(article_id: str) -> str | None: :return: The export go file path. """ return FileTransferService().get_export_go_filepath(article_id) + + +def export_success_callback(article_id: str) -> None: + FileTransferService().delete_export_files(article_id) + + +def export_failure_callback(article_id: str) -> None: + FileTransferService().delete_export_files(article_id) diff --git a/logger_messages.py b/logger_messages.py index b1f5565..0b0e489 100644 --- a/logger_messages.py +++ b/logger_messages.py @@ -5,13 +5,7 @@ __license__ = "AGPL v3" __maintainer__ = "The Public Library of Science (PLOS)" -import os - -from django.conf import settings - -PLUGIN_NAME = 'Editorial Manager Transfer Service Plugin' -EXPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'export') -IMPORT_FILE_PATH = os.path.join(settings.BASE_DIR, 'files', 'plugins', 'editorial-manager-transfer-service', 'import') +from plugins.editorial_manager_transfer_service.consts import PLUGIN_NAME, EXPORT_FILE_PATH, IMPORT_FILE_PATH def plugin_installation_beginning() -> str: @@ -88,7 +82,7 @@ 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_failed_fetching_metadata(article_id): +def process_failed_fetching_metadata(article_id) -> str: """ Gets the log message for when an article's metadata failed to be fetched. :param: article_id: The ID of the article being fetched. @@ -97,7 +91,7 @@ def process_failed_fetching_metadata(article_id): return "Fetching article (ID: {0}) metadata failed. Discontinuing export process.".format(article_id) -def process_failed_fetching_article_files(article_id): +def process_failed_fetching_article_files(article_id) -> str: """ Gets the log message for when an article's files failed to be fetched. :param: article_id: The ID of the article being fetched. @@ -106,10 +100,26 @@ def process_failed_fetching_article_files(article_id): return "Fetching files for article (ID: {0}) failed. Discontinuing export process.".format(article_id) -def process_failed_fetching_journal(article_id): +def process_failed_fetching_journal(article_id) -> str: """ Gets the log message for when an article's journal failed to be fetched. :param: article_id: The ID of the article. :return: The logger message. """ return "Fetching journal where article (ID: {0}) lives failed. Discontinuing export process.".format(article_id) + + +def process_failed_no_article_id_provided() -> str: + """ + Gets the log message for when an article ID was not provided. + :return: The logger message. + """ + return "No article ID 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." diff --git a/plugin_settings.py b/plugin_settings.py index 06046ae..2c832ff 100644 --- a/plugin_settings.py +++ b/plugin_settings.py @@ -7,19 +7,11 @@ import os +import plugins.editorial_manager_transfer_service.consts as consts import plugins.editorial_manager_transfer_service.logger_messages as logger_messages from utils import plugins from utils.logger import get_logger -PLUGIN_NAME = logger_messages.PLUGIN_NAME -DISPLAY_NAME = 'Editorial Manager Transfer Service' -DESCRIPTION = 'A plugin to provide information for Aries\' Editorial Manager to enable automatic transfers.' -AUTHOR = 'PLOS' -VERSION = '0.1' -SHORT_NAME = 'editorial_manager_transfer_service' -MANAGER_URL = 'editorial_manager_transfer_service_manager' -JANEWAY_VERSION = "1.8.0" - logger = get_logger(__name__) @@ -27,15 +19,15 @@ class EditorialManagerTransferServicePlugin(plugins.Plugin): """ The plugin class for the Editorial Manager Transfer Service. """ - plugin_name = PLUGIN_NAME - display_name = DISPLAY_NAME - description = DESCRIPTION - author = AUTHOR - short_name = SHORT_NAME - manager_url = MANAGER_URL + plugin_name = consts.PLUGIN_NAME + display_name = consts.DISPLAY_NAME + description = consts.DESCRIPTION + author = consts.AUTHOR + short_name = consts.SHORT_NAME + manager_url = consts.MANAGER_URL - version = VERSION - janeway_version = JANEWAY_VERSION + version = consts.VERSION + janeway_version = consts.JANEWAY_VERSION def install(): @@ -49,7 +41,7 @@ def install(): # Create the export folder. try: logger.info(logger_messages.export_folder_creating()) - os.makedirs(logger_messages.EXPORT_FILE_PATH) + os.makedirs(consts.EXPORT_FILE_PATH) except FileExistsError: logger.info(logger_messages.export_folder_created()) pass @@ -57,7 +49,7 @@ def install(): # Create the import folder. try: logger.info(logger_messages.import_folder_creating()) - os.makedirs(logger_messages.IMPORT_FILE_PATH) + os.makedirs(consts.IMPORT_FILE_PATH) except FileExistsError: logger.info(logger_messages.import_folder_created()) pass diff --git a/tests/test_file_creation.py b/tests/test_file_creation.py new file mode 100644 index 0000000..8a40efb --- /dev/null +++ b/tests/test_file_creation.py @@ -0,0 +1,107 @@ +import os +import re +import unittest +from unittest.mock import patch, MagicMock + +from django.conf import settings +from hypothesis import given +from hypothesis import settings as hypothesis_settings +from hypothesis.strategies import from_regex + +import plugins.editorial_manager_transfer_service.consts as consts +import plugins.editorial_manager_transfer_service.file_creation as file_creation +from core.models import File +from journal.models import Journal +from submission.models import Article + +uuid4_regex = re.compile('^([a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12})$') + +EXPORT_FOLDER = os.path.join(settings.BASE_DIR, "collected-static", consts.SHORT_NAME, "export") + + +def _get_setting(self, setting_name: str) -> str: + match setting_name: + case consts.PLUGIN_SETTINGS_LICENSE_CODE: + return "LCODE" + case consts.PLUGIN_SETTINGS_JOURNAL_CODE: + return "JOURNAL" + case consts.PLUGIN_SETTINGS_SUBMISSION_PARTNER_CODE: + return "SUBMISSION_PARTNER" + + +def _get_article_export_folders() -> str: + """ + Gets the filepaths for the folders used for exporting articles. + + :return: A list of filepaths for the export folders. + """ + + if not os.path.exists(EXPORT_FOLDER): + try: + os.makedirs(EXPORT_FOLDER) + print("Created folder {}".format(EXPORT_FOLDER)) + except FileExistsError: + pass + return EXPORT_FOLDER + + +def get_journal() -> Journal: + return MagicMock(Journal) + + +def create_manuscript_file(filename: str) -> File: + 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.close() + except FileExistsError: + pass + + manuscript: File = MagicMock(File) + manuscript.get_file_path.return_value = manuscript_filepath + + return manuscript + + +class TestFileCreation(unittest.TestCase): + @given(article_id=from_regex(uuid4_regex), manuscript_filename=from_regex(uuid4_regex)) + @patch.object(file_creation.ExportFileCreation, 'get_setting', new=_get_setting) + @patch('submission.models.Article.get_article') + @hypothesis_settings(max_examples=5) + def test_something(self, mock_get_article, article_id: str, manuscript_filename: str): + """ + 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. + """ + with patch( + 'plugins.editorial_manager_transfer_service.file_creation.get_article_export_folders') as mock_get_folders: + manuscript: File = create_manuscript_file(manuscript_filename) + + mock_get_folders.return_value = _get_article_export_folders() + article: Article = MagicMock(Article) + article.article_id = article_id + article.journal = get_journal() + + # 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 + + # Set the return + mock_get_article.return_value = article + + exporter = file_creation.ExportFileCreation(article_id) + self.assertTrue(exporter.can_export()) + self.assertEqual(article_id.strip(), exporter.article_id) # add assertion here + + def tearDown(self): + pass + # shutil.rmtree(EXPORT_FOLDER) + + +if __name__ == '__main__': + unittest.main() From 38968b6e0ca7643a4d37a203a4f0949c8f6281c6 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Fri, 29 Aug 2025 09:31:45 -0500 Subject: [PATCH 08/31] Update tests and code cleanup --- consts.py | 18 ++++++++ file_creation.py | 34 +++++++++----- tests/test_file_creation.py | 90 ++++++++++++++++++++++++------------- 3 files changed, 101 insertions(+), 41 deletions(-) diff --git a/consts.py b/consts.py index b12390c..6d84160 100644 --- a/consts.py +++ b/consts.py @@ -28,3 +28,21 @@ GO_FILE_GO_ELEMENT_ATTRIBUTE_XMLNS_XSI_VALUE = "http://www.w3.org/2001/XMLSchema-instance" GO_FILE_GO_ELEMENT_ATTRIBUTE_SCHEMA_LOCATION_KEY = "xsi:noNamespaceSchemaLocation" GO_FILE_GO_ELEMENT_ATTRIBUTE_SCHEMA_LOCATION_VALUE = "app://Aries.EditorialManager/Resources/XmlDefineTransformFiles/aries_import_go_file.xsd" +GO_FILE_ELEMENT_TAG_HEADER = "header" +GO_FILE_ELEMENT_TAG_VERSION = "version" +GO_FILE_VERSION_ELEMENT_ATTRIBUTE_NUMBER_KEY = "number" +GO_FILE_VERSION_ELEMENT_ATTRIBUTE_NUMBER_VALUE = "1.0" +GO_FILE_ELEMENT_TAG_JOURNAL = "journal" +GO_FILE_JOURNAL_ELEMENT_ATTRIBUTE_CODE_KEY = "code" +GO_FILE_ELEMENT_TAG_IMPORT_TYPE = "import-type" +GO_FILE_IMPORT_TYPE_ELEMENT_ATTRIBUTE_ID_KEY = "id" +GO_FILE_IMPORT_TYPE_ELEMENT_ATTRIBUTE_ID_VALUE = "2" +GO_FILE_ELEMENT_TAG_PARAMETERS = "parameters" +GO_FILE_ELEMENT_TAG_PARAMETER = "parameter" +GO_FILE_ATTRIBUTE_ELEMENT_NAME_KEY = "name" +GO_FILE_PARAMETER_ELEMENT_NAME_VALUE = "license-code" +GO_FILE_PARAMETER_ELEMENT_VALUE_KEY = "value" +GO_FILE_ELEMENT_TAG_ARCHIVE_FILE = "archive-file" +GO_FILE_ELEMENT_TAG_FILEGROUP = "filegroup" +GO_FILE_ELEMENT_TAG_FILE = "file" +GO_FILE_ELEMENT_TAG_METADATA_FILE = "metadata-file" diff --git a/file_creation.py b/file_creation.py index 07781e6..b36b952 100644 --- a/file_creation.py +++ b/file_creation.py @@ -142,6 +142,8 @@ def __create_export_file(self): filenames: Sequence[str] = zipf.namelist() zipf.close() + # Remove the manuscript + # TODO: Remove below and replace with 'self.__create_go_xml_file(metadata_file.uuid_filename, filenames, prefix)' self.__create_go_xml_file("fake name", filenames, prefix) @@ -215,23 +217,33 @@ def __create_go_xml_file(self, metadata_filename: str, article_filenames: Sequen consts.GO_FILE_GO_ELEMENT_ATTRIBUTE_SCHEMA_LOCATION_VALUE) # Format the header. - header: ETree.Element = ETree.SubElement(go, "header") - ETree.SubElement(header, "version", number="1.0") - ETree.SubElement(header, "journal", code=self.get_journal_code()) - ETree.SubElement(header, "import-type", id="2") - parameters: ETree.Element = ETree.SubElement(header, "parameters") - ETree.SubElement(parameters, "parameter", name="license-code", - value="{0}_{1}".format(self.get_submission_partner_code(), self.get_license_code())) + header: ETree.Element = ETree.SubElement(go, consts.GO_FILE_ELEMENT_TAG_HEADER) + version: ETree.Element = ETree.SubElement(header, consts.GO_FILE_ELEMENT_TAG_VERSION) + 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()) + 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) + parameters: ETree.Element = ETree.SubElement(header, consts.GO_FILE_ELEMENT_TAG_PARAMETERS) + parameter: ETree.Element = ETree.SubElement(parameters, consts.GO_FILE_ELEMENT_TAG_PARAMETER) + parameter.set(consts.GO_FILE_ATTRIBUTE_ELEMENT_NAME_KEY, consts.GO_FILE_PARAMETER_ELEMENT_NAME_VALUE) + parameter.set(consts.GO_FILE_PARAMETER_ELEMENT_VALUE_KEY, + "{0}_{1}".format(self.get_submission_partner_code(), self.get_license_code())) # Begin the filegroup. - filegroup: ETree.Element = ETree.SubElement(go, "filegroup") + filegroup: ETree.Element = ETree.SubElement(go, consts.GO_FILE_ELEMENT_TAG_FILEGROUP) # Create the archive and metadata files. - ETree.SubElement(filegroup, "archive-file", name="{0}.zip".format(filename)) - ETree.SubElement(filegroup, "metadata-file", name=metadata_filename) + archive_file: ETree.Element = ETree.SubElement(filegroup, consts.GO_FILE_ELEMENT_TAG_ARCHIVE_FILE) + archive_file.set(consts.GO_FILE_ATTRIBUTE_ELEMENT_NAME_KEY, "{0}.zip".format(filename)) + metadata_file: ETree.Element = ETree.SubElement(filegroup, consts.GO_FILE_ELEMENT_TAG_METADATA_FILE) + metadata_file.set(consts.GO_FILE_ATTRIBUTE_ELEMENT_NAME_KEY, metadata_filename) for article_filename in article_filenames: - ETree.SubElement(filegroup, "file", name=article_filename) + file_tree = ETree.SubElement(filegroup, consts.GO_FILE_ELEMENT_TAG_FILE) + file_tree.set(consts.GO_FILE_ATTRIBUTE_ELEMENT_NAME_KEY, article_filename) tree = ETree.ElementTree(go) self.go_filepath = os.path.join(self.export_folder, "{0}.go.xml".format(filename)) diff --git a/tests/test_file_creation.py b/tests/test_file_creation.py index 8a40efb..3a8d3a8 100644 --- a/tests/test_file_creation.py +++ b/tests/test_file_creation.py @@ -1,6 +1,8 @@ import os import re +import shutil import unittest +import xml.etree.ElementTree as ElementTree from unittest.mock import patch, MagicMock from django.conf import settings @@ -35,21 +37,14 @@ def _get_article_export_folders() -> str: :return: A list of filepaths for the export folders. """ - - if not os.path.exists(EXPORT_FOLDER): - try: - os.makedirs(EXPORT_FOLDER) - print("Created folder {}".format(EXPORT_FOLDER)) - except FileExistsError: - pass return EXPORT_FOLDER -def get_journal() -> Journal: +def _get_journal() -> Journal: return MagicMock(Journal) -def create_manuscript_file(filename: str) -> File: +def _create_manuscript_file(filename: str) -> File: 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: @@ -65,42 +60,77 @@ def create_manuscript_file(filename: str) -> File: return manuscript +def _create_article(article_id: str, manuscript_filename: str) -> Article: + manuscript: File = _create_manuscript_file(manuscript_filename) + + article: Article = MagicMock(Article) + article.article_id = article_id + article.journal = _get_journal() + + # 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 + return article + + class TestFileCreation(unittest.TestCase): + def setUp(self): + if not os.path.exists(EXPORT_FOLDER): + try: + os.makedirs(EXPORT_FOLDER) + print("Created folder {}".format(EXPORT_FOLDER)) + except FileExistsError: + pass + + def tearDown(self): + """ + Tears down after each test to ensure each test is unique. + """ + shutil.rmtree(EXPORT_FOLDER) + @given(article_id=from_regex(uuid4_regex), manuscript_filename=from_regex(uuid4_regex)) @patch.object(file_creation.ExportFileCreation, 'get_setting', new=_get_setting) + @patch('plugins.editorial_manager_transfer_service.file_creation.get_article_export_folders', + new=_get_article_export_folders) @patch('submission.models.Article.get_article') @hypothesis_settings(max_examples=5) - def test_something(self, mock_get_article, article_id: str, manuscript_filename: str): + def test_regular_article_creation_process(self, mock_get_article, article_id: str, manuscript_filename: str): """ 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. """ - with patch( - 'plugins.editorial_manager_transfer_service.file_creation.get_article_export_folders') as mock_get_folders: - manuscript: File = create_manuscript_file(manuscript_filename) + # Set the return + mock_get_article.return_value = _create_article(article_id, manuscript_filename) - mock_get_folders.return_value = _get_article_export_folders() - article: Article = MagicMock(Article) - article.article_id = article_id - article.journal = get_journal() + exporter = file_creation.ExportFileCreation(article_id) + self.assertTrue(exporter.can_export()) + self.assertEqual(article_id.strip(), exporter.article_id) # add assertion here - # 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 + self.__check_go_file(exporter.get_go_filepath(), 1) - # Set the return - mock_get_article.return_value = article + 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)) - exporter = file_creation.ExportFileCreation(article_id) - self.assertTrue(exporter.can_export()) - self.assertEqual(article_id.strip(), exporter.article_id) # add assertion here + # Get the XML file. + try: + tree = ElementTree.parse(go_filepath) + except ElementTree.ParseError: + self.fail("Go_filepath {} could not be parsed".format(go_filepath)) - def tearDown(self): - pass - # shutil.rmtree(EXPORT_FOLDER) + root: ElementTree.Element = tree.getroot() + self.assertIsNotNone(root) + self.assertEqual(consts.GO_FILE_ELEMENT_TAG_GO, root.tag) + + filegroup: ElementTree.Element = root.find(consts.GO_FILE_ELEMENT_TAG_FILEGROUP) + self.assertIsNotNone(filegroup) + self.assertEqual(consts.GO_FILE_ELEMENT_TAG_FILEGROUP, filegroup.tag) + + files: list[ElementTree.Element] = filegroup.findall(consts.GO_FILE_ELEMENT_TAG_FILE) + self.assertEqual(number_of_files, len(files)) if __name__ == '__main__': From 59c9654866dcf8f38407a6c1d004386eea5c9419 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Fri, 29 Aug 2025 09:49:56 -0500 Subject: [PATCH 09/31] Added the settings manager page for the plugin. --- forms.py | 9 ++++-- logic.py | 62 +++++++++++++++++++++++++++++++++++++ tests/test_file_creation.py | 1 - views.py | 39 ++++++++++++++++++++++- 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 logic.py diff --git a/forms.py b/forms.py index 6459616..6cb0577 100644 --- a/forms.py +++ b/forms.py @@ -1,5 +1,10 @@ from django import forms -class DummyManagerForm(forms.Form): - dummy_field = forms.CharField() +class EditorialManagerTransferServiceForm(forms.Form): + """ + The form for the Editorial Manager Transfer Service + """ + submission_partner_code = forms.CharField(required=False, help_text="Your organization's Submission Partner Code.") + license_code = forms.CharField(required=False, help_text="The license code for your organization.") + journal_code = forms.CharField(required=False, help_text="The code for the current journal.") diff --git a/logic.py b/logic.py new file mode 100644 index 0000000..47bbd63 --- /dev/null +++ b/logic.py @@ -0,0 +1,62 @@ +import plugins.editorial_manager_transfer_service.consts as consts +from journal.models import Journal +from utils import setting_handler +from utils.logger import get_logger + +logger = get_logger(__name__) + + +def get_plugin_settings(journal: Journal): + """ + Get the plugin settings for the Editorial Manager Transfer Service. + :param journal: the journal + """ + + logger.debug("Fetching journal settings for the following journal: %s", journal.id) + submission_partner_code = setting_handler.get_setting( + setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME, + setting_name="submission_partner_code", + journal=journal, + ).processed_value + license_code = setting_handler.get_setting( + setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME, + setting_name="license_code", + journal=journal, + ).processed_value + journal_code = setting_handler.get_setting( + setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME, + setting_name="journal_code", + journal=journal, + ).processed_value + + return ( + submission_partner_code, + license_code, + journal_code, + ) + + +def save_plugin_settings( + submission_partner_code, + license_code, + journal_code, + request, +): + setting_handler.save_setting( + setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME, + setting_name="submission_partner_code", + journal=request.journal, + value=submission_partner_code, + ) + setting_handler.save_setting( + setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME, + setting_name="license_code", + journal=request.journal, + value=license_code, + ) + setting_handler.save_setting( + setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME, + setting_name="journal_code", + journal=request.journal, + value=journal_code, + ) diff --git a/tests/test_file_creation.py b/tests/test_file_creation.py index 3a8d3a8..e697c3d 100644 --- a/tests/test_file_creation.py +++ b/tests/test_file_creation.py @@ -80,7 +80,6 @@ def setUp(self): if not os.path.exists(EXPORT_FOLDER): try: os.makedirs(EXPORT_FOLDER) - print("Created folder {}".format(EXPORT_FOLDER)) except FileExistsError: pass diff --git a/views.py b/views.py index 6d6d7a5..2e25840 100644 --- a/views.py +++ b/views.py @@ -1,10 +1,47 @@ +from django.contrib.admin.views.decorators import staff_member_required from django.shortcuts import render from plugins.editorial_manager_transfer_service import forms +from plugins.editorial_manager_transfer_service.logic import get_plugin_settings, save_plugin_settings +from security import decorators +@staff_member_required +@decorators.has_journal def manager(request): - form = forms.DummyManagerForm() + """ + The manager view for the Editorial Manager Service. + :param request: the request object + """ + ( + submission_partner_code, + license_code, + journal_code, + ) = get_plugin_settings(request.journal) + + if request.POST: + form = forms.EditorialManagerTransferServiceForm(request.POST) + + if form.is_valid(): + submission_partner_code = form.cleaned_data["submission_partner_code"] + license_code = form.cleaned_data["license_code"] + journal_code = form.cleaned_data["journal_code"] + + save_plugin_settings( + submission_partner_code, + license_code, + journal_code, + request, + ) + + else: + form = forms.EditorialManagerTransferServiceForm( + initial={ + "submission_partner_code": submission_partner_code, + "license_code": license_code, + "journal_code": journal_code, + } + ) template = 'editorial_manager_transfer_service/manager.html' context = { From dce160e734bd729f94de0b6f70034edcbcab2282 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Fri, 29 Aug 2025 10:09:50 -0500 Subject: [PATCH 10/31] Complete all pending work. --- logic.py | 2 +- plugin_settings.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/logic.py b/logic.py index 47bbd63..aa8dc4e 100644 --- a/logic.py +++ b/logic.py @@ -12,7 +12,7 @@ def get_plugin_settings(journal: Journal): :param journal: the journal """ - logger.debug("Fetching journal settings for the following journal: %s", journal.id) + logger.debug("Fetching journal settings for the following journal: %s", journal.code) submission_partner_code = setting_handler.get_setting( setting_group_name=consts.PLUGIN_SETTINGS_GROUP_NAME, setting_name="submission_partner_code", diff --git a/plugin_settings.py b/plugin_settings.py index 2c832ff..e58730f 100644 --- a/plugin_settings.py +++ b/plugin_settings.py @@ -10,10 +10,21 @@ import plugins.editorial_manager_transfer_service.consts as consts import plugins.editorial_manager_transfer_service.logger_messages as logger_messages from utils import plugins +from utils.install import update_settings from utils.logger import get_logger logger = get_logger(__name__) +# Plugin Settings +PLUGIN_NAME = consts.PLUGIN_NAME +DISPLAY_NAME = consts.DISPLAY_NAME +DESCRIPTION = consts.DESCRIPTION +AUTHOR = consts.AUTHOR +VERSION = consts.VERSION +SHORT_NAME = consts.SHORT_NAME +MANAGER_URL = consts.MANAGER_URL +JANEWAY_VERSION = consts.JANEWAY_VERSION + class EditorialManagerTransferServicePlugin(plugins.Plugin): """ @@ -35,6 +46,9 @@ def install(): Installs the Editorial Manager Transfer Service. """ logger.info(logger_messages.plugin_installation_beginning()) + update_settings( + file_path="plugins/editorial_manager_transfer_service/install/settings.json" + ) plugin, created = EditorialManagerTransferServicePlugin.install() if created: From 31681bbf74f829b17396343a9bcb14f54840bdcd Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 06:38:34 -0500 Subject: [PATCH 11/31] Update tests to add more complexity. --- tests/test_file_creation.py | 80 +++++++-------------------- tests/utils/__init__.py | 0 tests/utils/article_creation_utils.py | 63 +++++++++++++++++++++ 3 files changed, 84 insertions(+), 59 deletions(-) create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/article_creation_utils.py diff --git a/tests/test_file_creation.py b/tests/test_file_creation.py index e697c3d..f9800c1 100644 --- a/tests/test_file_creation.py +++ b/tests/test_file_creation.py @@ -3,22 +3,22 @@ import shutil import unittest import xml.etree.ElementTree as ElementTree -from unittest.mock import patch, MagicMock +from typing import Sequence +from unittest.mock import patch -from django.conf import settings from hypothesis import given from hypothesis import settings as hypothesis_settings -from hypothesis.strategies import from_regex +from hypothesis.strategies import from_regex, SearchStrategy, lists import plugins.editorial_manager_transfer_service.consts as consts import plugins.editorial_manager_transfer_service.file_creation as file_creation -from core.models import File -from journal.models import Journal -from submission.models import Article +import plugins.editorial_manager_transfer_service.tests.utils.article_creation_utils as article_utils 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") +valid_filenames: SearchStrategy[list[str]] = lists(from_regex(valid_filename_regex), unique=True, min_size=0, + max_size=20) def _get_setting(self, setting_name: str) -> str: @@ -31,55 +31,14 @@ def _get_setting(self, setting_name: str) -> str: return "SUBMISSION_PARTNER" -def _get_article_export_folders() -> str: - """ - Gets the filepaths for the folders used for exporting articles. - - :return: A list of filepaths for the export folders. - """ - return EXPORT_FOLDER - - -def _get_journal() -> Journal: - return MagicMock(Journal) - - -def _create_manuscript_file(filename: str) -> File: - 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.close() - except FileExistsError: - pass - - manuscript: File = MagicMock(File) - manuscript.get_file_path.return_value = manuscript_filepath - - return manuscript - - -def _create_article(article_id: str, manuscript_filename: str) -> Article: - manuscript: File = _create_manuscript_file(manuscript_filename) - - article: Article = MagicMock(Article) - article.article_id = article_id - article.journal = _get_journal() - - # 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 - return article - - class TestFileCreation(unittest.TestCase): def setUp(self): - if not os.path.exists(EXPORT_FOLDER): + """ + Sets up the export folder structure. + """ + if not os.path.exists(article_utils._get_article_export_folders()): try: - os.makedirs(EXPORT_FOLDER) + os.makedirs(article_utils._get_article_export_folders()) except FileExistsError: pass @@ -87,28 +46,31 @@ def tearDown(self): """ Tears down after each test to ensure each test is unique. """ - shutil.rmtree(EXPORT_FOLDER) + shutil.rmtree(article_utils._get_article_export_folders()) - @given(article_id=from_regex(uuid4_regex), manuscript_filename=from_regex(uuid4_regex)) + @given(article_id=from_regex(uuid4_regex), manuscript_filename=from_regex(valid_filename_regex), + data_figure_filenames=valid_filenames) @patch.object(file_creation.ExportFileCreation, 'get_setting', new=_get_setting) @patch('plugins.editorial_manager_transfer_service.file_creation.get_article_export_folders', - new=_get_article_export_folders) + new=article_utils._get_article_export_folders) @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): + def test_regular_article_creation_process(self, mock_get_article, article_id: str, manuscript_filename: str, + data_figure_filenames: Sequence[str]): """ 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. """ # Set the return - mock_get_article.return_value = _create_article(article_id, manuscript_filename) + mock_get_article.return_value = article_utils._create_article(article_id, manuscript_filename, + data_figure_filenames) exporter = file_creation.ExportFileCreation(article_id) self.assertTrue(exporter.can_export()) self.assertEqual(article_id.strip(), exporter.article_id) # add assertion here - self.__check_go_file(exporter.get_go_filepath(), 1) + self.__check_go_file(exporter.get_go_filepath(), len(data_figure_filenames) + 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/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/article_creation_utils.py b/tests/utils/article_creation_utils.py new file mode 100644 index 0000000..4585ddd --- /dev/null +++ b/tests/utils/article_creation_utils.py @@ -0,0 +1,63 @@ +import os +from typing import Sequence +from unittest.mock import MagicMock + +from core import settings +from core.models import File +from journal.models import Journal +from plugins.editorial_manager_transfer_service import consts +from submission.models import Article + +EXPORT_FOLDER = os.path.join(settings.BASE_DIR, "collected-static", consts.SHORT_NAME, "export") + + +def _get_article_export_folders() -> str: + """ + Gets the filepaths for the folders used for exporting articles. + + :return: A list of filepaths for the export folders. + """ + return EXPORT_FOLDER + + +def _get_journal() -> Journal: + return MagicMock(Journal) + + +def _create_txt_file(filename: str) -> File: + 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.close() + except FileExistsError: + pass + + manuscript: File = MagicMock(File) + manuscript.get_file_path.return_value = manuscript_filepath + + return manuscript + + +def _create_article(article_id: str, manuscript_filename: str, data_figure_filenames: Sequence[str]) -> Article: + manuscript: File = _create_txt_file(manuscript_filename) + + article: Article = MagicMock(Article) + article.article_id = article_id + article.journal = _get_journal() + + # 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 + + # Handle the + 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 7ecfa363763d56d36f7a35db3d75f238080ce13f Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 13:04:07 -0500 Subject: [PATCH 12/31] Update testing for automatic runs. --- .github/workflows/django.yml | 7 ++++--- .gitignore | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 9b28454..2966a74 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -17,20 +17,21 @@ jobs: steps: - name: Fetch Main Janeway Branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: PLOS/janeway ref: develop path: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway - name: Fetch Editorial Manager Transfer Service - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} path: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway + cache: 'pip' - name: Install Dependencies working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway run: | diff --git a/.gitignore b/.gitignore index e1bff6b..daa5a64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.venv/ /editorial_manager_transfer_service.iml /.idea/ +/.hypothesis/ From 31c0aae3a0048e63ed24b169f38e99ad3af617f8 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 13:10:40 -0500 Subject: [PATCH 13/31] Update django.yml Remove path from python setup. --- .github/workflows/django.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 2966a74..b1ad15d 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -30,7 +30,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - path: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway cache: 'pip' - name: Install Dependencies working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway From 408c87453af766d821ce8b0cbed1b1e953d1b64a Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 13:17:01 -0500 Subject: [PATCH 14/31] Update django.yml Update the pytest. --- .github/workflows/django.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index b1ad15d..564183c 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -38,4 +38,4 @@ jobs: sudo find . -name "*requirements.txt" -type f -exec pip3 install -r '{}' ';' - name: Run Tests run: | - pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service/ + python -m pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From f38755563c159608907de0cebed389b5dd8d5c2f Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 13:21:38 -0500 Subject: [PATCH 15/31] Update django.yml Update `pip3` to `pip` so it points to the same pip instance. --- .github/workflows/django.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 564183c..cd11fd4 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -35,7 +35,7 @@ jobs: working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway run: | python -m pip install --upgrade pip - sudo find . -name "*requirements.txt" -type f -exec pip3 install -r '{}' ';' + sudo find . -name "*requirements.txt" -type f -exec python -m pip install -r '{}' ';' - name: Run Tests run: | python -m pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From 565aed54dfa94f4fb18359caf617394b4ba1a2ab Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 13:24:51 -0500 Subject: [PATCH 16/31] Update django.yml --- .github/workflows/django.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index cd11fd4..c3794e6 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -38,4 +38,4 @@ jobs: sudo find . -name "*requirements.txt" -type f -exec python -m pip install -r '{}' ';' - name: Run Tests run: | - python -m pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service + pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From 091571da1b5e0d6d26ccd1dab20d15edaf38e011 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 13:38:59 -0500 Subject: [PATCH 17/31] Update django.yml --- .github/workflows/django.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index c3794e6..b7e3aa3 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -32,10 +32,9 @@ jobs: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Install Dependencies - working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway run: | python -m pip install --upgrade pip sudo find . -name "*requirements.txt" -type f -exec python -m pip install -r '{}' ';' - name: Run Tests run: | - pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service + python -m pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From 8a3c5c5eaea2be1954f06c6053a9b550414211a0 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 13:45:47 -0500 Subject: [PATCH 18/31] Add init. --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From e7a206529f3cdf62d63f2cf8981e54935365fe09 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 13:50:25 -0500 Subject: [PATCH 19/31] Update django.yml Attempt to fix versions of python. --- .github/workflows/django.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index b7e3aa3..ad2e10e 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -34,7 +34,7 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - sudo find . -name "*requirements.txt" -type f -exec python -m pip install -r '{}' ';' + find . -name "*requirements.txt" -type f -exec pip install -r '{}' ';' - name: Run Tests run: | - python -m pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service + pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From 8b58e73d12dd58ebc0977f2801424485a3c2ae9f Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 14:11:12 -0500 Subject: [PATCH 20/31] Add src to python path. Add janeway settings module. --- .github/workflows/django.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index ad2e10e..8feb641 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -35,6 +35,13 @@ jobs: run: | python -m pip install --upgrade pip find . -name "*requirements.txt" -type f -exec pip install -r '{}' ';' + - name: Add `src` to Python Path + run: | + echo "export PYTHONPATH=\"/home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src:$PYTHONPATH\"" >> $GITHUB_ENV + - name: Import the Default Janeway Settings + run: | + cp /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/janeway_global_settings.py /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/settings.py + echo "export JANEWAY_SETTINGS_MODULE=\"core.settings\"" >> $GITHUB_ENV - name: Run Tests run: | pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From 7f4eb39e49360b13f6ec648026d33f8dd234752c Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 14:14:23 -0500 Subject: [PATCH 21/31] Remove export. --- .github/workflows/django.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 8feb641..297eebe 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -37,11 +37,11 @@ jobs: find . -name "*requirements.txt" -type f -exec pip install -r '{}' ';' - name: Add `src` to Python Path run: | - echo "export PYTHONPATH=\"/home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src:$PYTHONPATH\"" >> $GITHUB_ENV + echo "PYTHONPATH=\"/home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src:$PYTHONPATH\"" >> $GITHUB_ENV - name: Import the Default Janeway Settings run: | cp /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/janeway_global_settings.py /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/settings.py - echo "export JANEWAY_SETTINGS_MODULE=\"core.settings\"" >> $GITHUB_ENV + echo "JANEWAY_SETTINGS_MODULE=\"core.settings\"" >> $GITHUB_ENV - name: Run Tests run: | pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From 2e80da1b5519c8f880e33f01f62760f4904904a3 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 14:27:59 -0500 Subject: [PATCH 22/31] Add working directory. --- .github/workflows/django.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 297eebe..3b5c784 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -41,7 +41,7 @@ jobs: - name: Import the Default Janeway Settings run: | cp /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/janeway_global_settings.py /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/settings.py - echo "JANEWAY_SETTINGS_MODULE=\"core.settings\"" >> $GITHUB_ENV - name: Run Tests + working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway run: | pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From 9dec926335ff5af5e8aa58e03a536ab0a2831024 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 14:33:09 -0500 Subject: [PATCH 23/31] Update environment variable. --- .github/workflows/django.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 3b5c784..79d9bcf 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -37,11 +37,13 @@ jobs: find . -name "*requirements.txt" -type f -exec pip install -r '{}' ';' - name: Add `src` to Python Path run: | - echo "PYTHONPATH=\"/home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src:$PYTHONPATH\"" >> $GITHUB_ENV + echo "PYTHONPATH=/home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src" >> $GITHUB_ENV + shell: bash - name: Import the Default Janeway Settings run: | - cp /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/janeway_global_settings.py /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/settings.py + cp /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/dev_settings.py /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/settings.py - name: Run Tests working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway run: | pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service + shell: cmd From a950b9c90811d70ed2ae52b83fe01897b0c5a924 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 14:36:03 -0500 Subject: [PATCH 24/31] Update shell. --- .github/workflows/django.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 79d9bcf..5f53139 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -46,4 +46,3 @@ jobs: working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway run: | pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service - shell: cmd From 938eb8782367c9f26e77bd45877fecabc57f6964 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 14:40:23 -0500 Subject: [PATCH 25/31] Add environment variable. --- .github/workflows/django.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 5f53139..3cc45a7 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -43,6 +43,8 @@ jobs: run: | cp /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/dev_settings.py /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/core/settings.py - name: Run Tests + env: + JANEWAY_SETTINGS_MODULE: core.settings working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway run: | pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From 56a5978d64e3ffc3f75cf5f16824a20f65e181af Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Tue, 2 Sep 2025 14:49:13 -0500 Subject: [PATCH 26/31] Add Django Settings module to environment. --- .github/workflows/django.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 3cc45a7..1fba2cc 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -45,6 +45,7 @@ jobs: - name: Run Tests env: JANEWAY_SETTINGS_MODULE: core.settings + DJANGO_SETTINGS_MODULE: core.settings working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway run: | pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service From 5c1e2a2da783a4dfa7a344c98597c6715fc63a1c Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 3 Sep 2025 09:01:09 -0500 Subject: [PATCH 27/31] Use Django Test Runner Switch from PyTest to Django's test runner. --- .github/workflows/django.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 1fba2cc..4b84a79 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -30,7 +30,13 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip # Path to pip's cache directory + key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} # Unique cache key + restore-keys: | + ${{ runner.os }}-pip- - name: Install Dependencies run: | python -m pip install --upgrade pip @@ -48,4 +54,4 @@ jobs: DJANGO_SETTINGS_MODULE: core.settings working-directory: /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway run: | - pytest /home/runner/work/editorial_manager_transfer_service/editorial_manager_transfer_service/janeway/src/plugins/editorial_manager_transfer_service + python src/manage.py test editorial_manager_transfer_service From 2d18404f979e3a0086ae3e958cec3b0a4bce6c7d Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 3 Sep 2025 09:21:52 -0500 Subject: [PATCH 28/31] Update based on feedback --- file_creation.py => file_exporter.py | 2 ++ file_transfer_service.py | 2 +- models.py | 0 .../manager.html | 28 +++++++++++++------ tests/test_file_creation.py | 2 +- tests/utils/article_creation_utils.py | 2 +- utils.py | 2 -- 7 files changed, 25 insertions(+), 13 deletions(-) rename file_creation.py => file_exporter.py (99%) delete mode 100644 models.py delete mode 100644 utils.py diff --git a/file_creation.py b/file_exporter.py similarity index 99% rename from file_creation.py rename to file_exporter.py index b36b952..385c87f 100644 --- a/file_creation.py +++ b/file_exporter.py @@ -64,6 +64,8 @@ def __init__(self, article_id: str): logger.info(logger_messages.process_fetching_article(article_id)) try: self.article: Article = self.__fetch_article(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 diff --git a/file_transfer_service.py b/file_transfer_service.py index 920bdb1..aa1c13a 100644 --- a/file_transfer_service.py +++ b/file_transfer_service.py @@ -8,7 +8,7 @@ import os from typing import List -from plugins.editorial_manager_transfer_service.file_creation import ExportFileCreation +from plugins.editorial_manager_transfer_service.file_exporter import ExportFileCreation from utils.logger import get_logger logger = get_logger(__name__) diff --git a/models.py b/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/templates/editorial_manager_transfer_service/manager.html b/templates/editorial_manager_transfer_service/manager.html index 22a8d06..5752626 100644 --- a/templates/editorial_manager_transfer_service/manager.html +++ b/templates/editorial_manager_transfer_service/manager.html @@ -1,15 +1,27 @@ {% extends "admin/core/base.html" %} {% load foundation %} -{% block title %}editorial_manager_transfer_service Manager{% endblock %} +{% block title %}Editorial Manager Transfer Service Configuration{% endblock %} {% block body %} -
-
-

Management Form

-
-
- {{ form|foundation }} -
+
+
+ {% csrf_token %} +
+
+

Configuration

+
+
+ {{ form|foundation }} +
+
+
+
+ +
+
+
+
+
{% endblock body %} \ No newline at end of file diff --git a/tests/test_file_creation.py b/tests/test_file_creation.py index f9800c1..1302bf5 100644 --- a/tests/test_file_creation.py +++ b/tests/test_file_creation.py @@ -11,7 +11,7 @@ from hypothesis.strategies import from_regex, SearchStrategy, lists import plugins.editorial_manager_transfer_service.consts as consts -import plugins.editorial_manager_transfer_service.file_creation as file_creation +import plugins.editorial_manager_transfer_service.file_exporter as file_creation import plugins.editorial_manager_transfer_service.tests.utils.article_creation_utils as article_utils uuid4_regex = re.compile('^([a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12})$') diff --git a/tests/utils/article_creation_utils.py b/tests/utils/article_creation_utils.py index 4585ddd..f54c33f 100644 --- a/tests/utils/article_creation_utils.py +++ b/tests/utils/article_creation_utils.py @@ -53,7 +53,7 @@ def _create_article(article_id: str, manuscript_filename: str, data_figure_filen manuscript_files.append(manuscript) article.manuscript_files.all.return_value = manuscript_files - # Handle the + # 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: diff --git a/utils.py b/utils.py deleted file mode 100644 index 1ffb8d2..0000000 --- a/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -from utils.logger import get_logger - From 19d603b8bfad76821131ec2cc517450e833d65e6 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 3 Sep 2025 09:33:27 -0500 Subject: [PATCH 29/31] Update mock. --- tests/test_file_creation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_file_creation.py b/tests/test_file_creation.py index 1302bf5..769580b 100644 --- a/tests/test_file_creation.py +++ b/tests/test_file_creation.py @@ -11,7 +11,7 @@ from hypothesis.strategies import from_regex, SearchStrategy, lists import plugins.editorial_manager_transfer_service.consts as consts -import plugins.editorial_manager_transfer_service.file_exporter as file_creation +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 uuid4_regex = re.compile('^([a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12})$') @@ -50,8 +50,8 @@ def tearDown(self): @given(article_id=from_regex(uuid4_regex), manuscript_filename=from_regex(valid_filename_regex), data_figure_filenames=valid_filenames) - @patch.object(file_creation.ExportFileCreation, 'get_setting', new=_get_setting) - @patch('plugins.editorial_manager_transfer_service.file_creation.get_article_export_folders', + @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('submission.models.Article.get_article') @hypothesis_settings(max_examples=5) @@ -66,7 +66,7 @@ def test_regular_article_creation_process(self, mock_get_article, article_id: st mock_get_article.return_value = article_utils._create_article(article_id, manuscript_filename, data_figure_filenames) - exporter = file_creation.ExportFileCreation(article_id) + exporter = file_exporter.ExportFileCreation(article_id) self.assertTrue(exporter.can_export()) self.assertEqual(article_id.strip(), exporter.article_id) # add assertion here From b38261e7521b322689473893399d396017714475 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Wed, 3 Sep 2025 10:31:44 -0500 Subject: [PATCH 30/31] Add the journal code to calls for finding the article. --- file_exporter.py | 24 ++++----- file_transfer_service.py | 63 ++++++++++++++++-------- management/commands/create_export_zip.py | 6 ++- tests/test_file_creation.py | 8 ++- tests/utils/article_creation_utils.py | 10 ++-- 5 files changed, 72 insertions(+), 39 deletions(-) diff --git a/file_exporter.py b/file_exporter.py index 385c87f..9310934 100644 --- a/file_exporter.py +++ b/file_exporter.py @@ -42,7 +42,7 @@ class ExportFileCreation: A class for managing the export file creation process. """ - def __init__(self, article_id: str): + def __init__(self, journal_code: str, article_id: str): self.zip_filepath: str | None = None self.go_filepath: str | None = None self.in_error_state: bool = False @@ -60,10 +60,18 @@ def __init__(self, article_id: str): 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 + 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(article_id) + self.article: Article = self.__fetch_article(self.journal, article_id) if not self.article: raise Article.DoesNotExist except Article.DoesNotExist: @@ -71,13 +79,6 @@ def __init__(self, article_id: str): self.in_error_state = True return - # Attempt to get the journal. - self.journal: Journal = self.article.journal - if self.journal is None: - logger.error(logger_messages.process_failed_fetching_journal(article_id)) - self.in_error_state = True - return - # Get the export folder. export_folders: str = get_article_export_folders() if len(export_folders) <= 0: @@ -260,13 +261,14 @@ def __create_metadata_file(self, article: Article) -> File | None: pass @staticmethod - def __fetch_article(article_id: str) -> Article: + def __fetch_article(journal: Journal, article_id: str) -> Article: """ 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(article_id) + return Article.get_article(journal, "id", article_id) @staticmethod def __fetch_article_files(article: Article) -> List[File]: diff --git a/file_transfer_service.py b/file_transfer_service.py index aa1c13a..0a883a4 100644 --- a/file_transfer_service.py +++ b/file_transfer_service.py @@ -34,41 +34,52 @@ def __init__(self): self.files_to_delete: List[str] = list() self._initialized = True - def get_export_file_creator(self, article_id: str) -> ExportFileCreation | None: + def get_export_file_creator(self, journal_code: str, article_id: str) -> ExportFileCreation | None: """ Gets the export file creator 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 export file creator. """ - if article_id not in self.exports: - file_creator = ExportFileCreation(article_id) + + dictionary_identifier: str = self.__get_dictionary_identifier(journal_code, article_id) + + if dictionary_identifier not in self.exports: + file_creator = ExportFileCreation(journal_code, article_id) if file_creator.in_error_state: return None - self.exports[article_id] = file_creator - return self.exports[article_id] + self.exports[dictionary_identifier] = file_creator + return self.exports[dictionary_identifier] - def get_export_zip_filepath(self, article_id: str) -> str | None: + @staticmethod + def __get_dictionary_identifier(journal_code: str, article_id: str) -> str: + return f"{journal_code}-{article_id}" + + def get_export_zip_filepath(self, journal_code: str, article_id: str) -> str | None: """ Gets the export zip file path for the given article. + :param journal_code: The journal code of the journal the article lives in. :param article_id: The article id. :return: The export zip file path. """ - file_export_creator = self.get_export_file_creator(article_id) + file_export_creator = self.get_export_file_creator(journal_code, article_id) return file_export_creator.get_zip_filepath() if file_export_creator else None - def get_export_go_filepath(self, article_id: str) -> str | None: + def get_export_go_filepath(self, journal_code: str, article_id: str) -> str | None: """ Gets the export go file path for the given article. + :param journal_code: The journal code of the journal the article lives in. :param article_id: The article id. :return: The export go file path. """ - file_export_creator = self.get_export_file_creator(article_id) + 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 delete_export_files(self, article_id: str) -> None: - if article_id not in self.exports: + def delete_export_files(self, journal_code: str, article_id: str) -> None: + dictionary_identifier: str = self.__get_dictionary_identifier(journal_code, article_id) + if dictionary_identifier not in self.exports: return - file_exporter = self.exports.pop(article_id) + file_exporter = self.exports.pop(dictionary_identifier) self.files_to_delete.append(file_exporter.get_zip_filepath()) self.files_to_delete.append(file_exporter.get_go_filepath()) @@ -92,27 +103,39 @@ def __delete_file(filepath: str) -> bool: return True -def get_export_zip_filepath(article_id: str) -> str | None: +def get_export_zip_filepath(journal_code: str, article_id: str) -> str | None: """ Gets the zip file path for a given article. + :param journal_code: The journal code of the journal the article lives in. :param article_id: The article id. :return: The zip file path. """ - return FileTransferService().get_export_zip_filepath(article_id) + return FileTransferService().get_export_zip_filepath(journal_code, article_id) -def get_export_go_filepath(article_id: str) -> str | None: +def get_export_go_filepath(journal_code: str, article_id: str) -> str | None: """ Gets the export file path for a go file created for a given article. + :param journal_code: The journal code of the journal the article lives in. :param article_id: The article id. :return: The export go file path. """ - return FileTransferService().get_export_go_filepath(article_id) + return FileTransferService().get_export_go_filepath(journal_code, article_id) -def export_success_callback(article_id: str) -> None: - FileTransferService().delete_export_files(article_id) +def export_success_callback(journal_code: str, article_id: str) -> None: + """ + The callback in case of a successful export. + :param journal_code: The journal code of the journal the article lives in. + :param article_id: The article id. + """ + FileTransferService().delete_export_files(journal_code, article_id) -def export_failure_callback(article_id: str) -> None: - FileTransferService().delete_export_files(article_id) +def export_failure_callback(journal_code: str, article_id: str) -> None: + """ + The callback in case of a failed export. + :param journal_code: The journal code of the journal the article lives in. + :param article_id: The article id. + """ + FileTransferService().delete_export_files(journal_code, article_id) diff --git a/management/commands/create_export_zip.py b/management/commands/create_export_zip.py index 3878467..be392cb 100644 --- a/management/commands/create_export_zip.py +++ b/management/commands/create_export_zip.py @@ -18,16 +18,18 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument('article_id', help="The ID of the article to create a zip file for.") + parser.add_argument('journal_code', help="The code of the journal where the article to export lives.") def handle(self, *args, **options): article_id: str = open(options["article_id"], "r", encoding="utf-8-sig").read().strip() + journal_code: str = open(options["journal_code"], "r", encoding="utf-8-sig").read().strip() print("Beginning bundling process for article...") - export_zip_file: str = file_transfer_service.get_export_zip_filepath(article_id) + export_zip_file: str = file_transfer_service.get_export_zip_filepath(journal_code, article_id) if not export_zip_file: raise CommandError("Error while creating export ZIP.") - export_go_file: str = file_transfer_service.get_export_go_filepath(article_id) + export_go_file: str = file_transfer_service.get_export_go_filepath(journal_code, article_id) if not export_go_file: raise CommandError("Error while creating export GO file.") diff --git a/tests/test_file_creation.py b/tests/test_file_creation.py index 769580b..b5efd22 100644 --- a/tests/test_file_creation.py +++ b/tests/test_file_creation.py @@ -13,6 +13,7 @@ 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 uuid4_regex = re.compile('^([a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12})$') valid_filename_regex = re.compile("^[\w\-. ]+$") @@ -53,6 +54,7 @@ def tearDown(self): @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, @@ -62,11 +64,13 @@ def test_regular_article_creation_process(self, mock_get_article, article_id: st :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(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(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 diff --git a/tests/utils/article_creation_utils.py b/tests/utils/article_creation_utils.py index f54c33f..4e30466 100644 --- a/tests/utils/article_creation_utils.py +++ b/tests/utils/article_creation_utils.py @@ -20,8 +20,10 @@ def _get_article_export_folders() -> str: return EXPORT_FOLDER -def _get_journal() -> Journal: - return MagicMock(Journal) +def _get_journal(code: str) -> Journal: + journal: Journal = MagicMock(Journal) + journal.code = code + return journal def _create_txt_file(filename: str) -> File: @@ -40,12 +42,12 @@ def _create_txt_file(filename: str) -> File: return manuscript -def _create_article(article_id: str, manuscript_filename: str, data_figure_filenames: Sequence[str]) -> Article: +def _create_article(journal: Journal, article_id: str, manuscript_filename: str, data_figure_filenames: Sequence[str]) -> Article: manuscript: File = _create_txt_file(manuscript_filename) article: Article = MagicMock(Article) article.article_id = article_id - article.journal = _get_journal() + article.journal = journal # Handle the manuscript files. article.manuscript_files = MagicMock(File.objects) From 96b1f6c316e2f43dc4cb0dc4c9d1f322f69fa239 Mon Sep 17 00:00:00 2001 From: Rose Reatherford Date: Thu, 4 Sep 2025 07:30:24 -0500 Subject: [PATCH 31/31] Update readme to include instructions regarding the `PYTHONPATH` for easier development. --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/README.md b/README.md index e3d5ec9..bb5a8f8 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,83 @@ A plugin to provide information for Aries' Editorial Manager to enable automatic ## Requirements This plugin depends on the Production Transporter plugin in order to work properly. + +## Development Tips +This section contains guidance for developing this plugin. + +For the purposes of this guide we'll refer to the Janeway installation folder as `[workspace]`. + +The plugin's installation folder is assumed to be `[workspace]/src/plugins/editorial_manager_transfer_service`. + +### Adding The Janeway SRC to the `PYTHONPATH` +Adding the Janeway SRC folder to the `PYTHONPATH` can help any type of IDE correctly identify imports while developing plugins. + +#### Instructions for Python Virtual Environment (VENV) +First, open your `activate` file in a text editor. It is located at: +```text +[workspace]/.venv/bin/activate +``` +Once open, scroll to find the following text section: +```bash +VIRTUAL_ENV="[workspace]/.venv" +if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then + VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV") +fi +export VIRTUAL_ENV + + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH +``` +Please note and change the `[worksapce]` variable to your Janeway installation path. + +You will modify this section to become the following: +```bash +_PYTHON_ENV_PKG='[workspace]' +VIRTUAL_ENV="$_PYTHON_ENV_PKG/.venv" +if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then + VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV") +fi +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +_OLD_VIRTUAL_PYTHONPATH="$PYTHONPATH" +PYTHONPATH="$_PYTHON_ENV_PKG:$PYTHONPATH" +export PYTHONPATH +``` +Next you will modify the `deactivate()` script. Scroll until you find this section of script: +```bash +deactivate () { + unset -f pydoc >/dev/null 2>&1 || true + + # reset old environment variables + # ! [ -z ${VAR+_} ] returns true if VAR is declared at all + if ! [ -z "${_OLD_VIRTUAL_PATH:+_}" ] ; then + PATH="$_OLD_VIRTUAL_PATH" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then + PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi +``` +Modify it to add the `PYTHONPATH` deactivation: +```bash + ... + unset _OLD_VIRTUAL_PATH + fi + if ! [ -z "${_OLD_VIRTUAL_PYTHONPATH:+_}" ] ; then + PYTHONPATH="$_OLD_VIRTUAL_PYTHONPATH" + export PYTHONPATH + unset _OLD_VIRTUAL_PYTHONPATH + fi + if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then + ... +``` +After restarting your IDE, you should see it properly detect the SRC folder. \ No newline at end of file