From 828e846ead887be9465b7cdc0a13bfcbee2ca215 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Apr 2024 11:50:34 +0100 Subject: [PATCH 01/46] add RisEntry --- doajtest/unit/test_ris.py | 74 ++++++++++++++ portality/lib/ris.py | 205 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 doajtest/unit/test_ris.py create mode 100644 portality/lib/ris.py diff --git a/doajtest/unit/test_ris.py b/doajtest/unit/test_ris.py new file mode 100644 index 000000000..7e2ef085b --- /dev/null +++ b/doajtest/unit/test_ris.py @@ -0,0 +1,74 @@ +from unittest import TestCase + +from portality.lib.ris import RisEntry + + +class TestRisEntry(TestCase): + + def test_get_set_item__basic(self): + test_value = 'value_a' + entry = RisEntry() + entry['primary_author'] = test_value + assert entry['primary_author'] == test_value + + def test_get_set_item__alias(self): + test_value = 'value_a' + entry = RisEntry() + entry['primary_author'] = test_value + assert entry['A1'] == test_value + + def test_getitem__valid_undefined(self): + entry = RisEntry() + assert entry['primary_author'] is None + + def test_setitem__raise_field_not_found(self): + entry = RisEntry() + with self.assertRaises(ValueError): + entry['qoidjqowijdkncoiqw'] = 'value_a' + + def test_getitem__raise_field_not_found(self): + entry = RisEntry() + with self.assertRaises(ValueError): + print(entry['qoidjqowijdkncoiqw']) + + def test_to_dict(self): + entry = RisEntry() + entry['primary_author'] = 'value_a' + entry['secondary_author'] = 'value_b' + entry['tertiary_author'] = 'value_c' + assert entry.to_dict() == { + 'A1': 'value_a', + 'A2': 'value_b', + 'A3': 'value_c' + } + + def test_to_text(self): + entry = RisEntry() + entry['primary_author'] = 'value_a' + entry['secondary_author'] = 'value_b' + entry['TY'] = 'JOUR' + + expected = """ +TY - JOUR +A1 - value_a +A2 - value_b +ER - + """.strip() + ' \n' + + assert entry.to_text() == expected + + def test_from_text(self): + expected = """ + TY - JOUR + A1 - value_a + A2 - value_b + ER - + """.strip() + ' \n' + + entry = RisEntry.from_text(expected) + assert entry['TY'] == 'JOUR' + assert dict(entry.to_dict()) == { + 'TY': 'JOUR', + 'A1': 'value_a', + 'A2': 'value_b' + } diff --git a/portality/lib/ris.py b/portality/lib/ris.py new file mode 100644 index 000000000..f1ba0083d --- /dev/null +++ b/portality/lib/ris.py @@ -0,0 +1,205 @@ +""" +very simple library for RIS format + +file format references: https://en.wikipedia.org/wiki/RIS_(file_format) +""" +import logging +from collections import OrderedDict +from typing import Dict, Optional + +log = logging.getLogger(__name__) + +RTAG_TYPE = 'TY' +RTAG_END = 'ER' +RIS_ALIAS = [ + ['A1', 'primary_author'], + ['A2', 'secondary_author'], + ['A3', 'tertiary_author'], + ['A4', 'quaternary_author'], + ['A5', 'quinary_author_compiler'], + ['A6', 'website_editor'], + ['AB', 'abstract_synopsis'], + ['AD', 'author_editor_address'], + ['AN', 'accession_number'], + ['AU', 'author_editor_translator'], + ['AV', 'availability_location'], + ['BT', 'primary_secondary_title'], + ['C1', 'custom1'], + ['C2', 'custom2'], + ['C3', 'custom3'], + ['C4', 'custom4'], + ['C5', 'custom5'], + ['C6', 'custom6'], + ['C7', 'custom7'], + ['C8', 'custom8'], + ['CA', 'caption'], + ['CL', 'classification'], + ['CN', 'call_number'], + ['CP', 'city_place_publication'], + ['CR', 'cited_references'], + ['CT', 'caption_primary_title'], + ['CY', 'place_published'], + ['DA', 'date'], + ['DB', 'name_of_database'], + ['DI', 'digital_object_identifier', 'di', ], + ['DO', 'digital_object_identifier2', 'do', ], + ['DOI', 'digital_object_identifier3', 'doi', ], + ['DP', 'database_provider'], + ['DS', 'data_source'], + ['ED', 'secondary_author'], + ['EP', 'end_page'], + ['ET', 'edition'], + ['FD', 'free_form_publication_data'], + ['H1', 'location_library'], + ['H2', 'location_call_number'], + ['ID', 'reference_identifier'], + ['IP', 'identifying_phrase'], + ['IS', 'number_volumes'], + ['J1', 'journal_abbreviation_1'], + ['J2', 'alternate_title'], + ['JA', 'journal_standard_abbreviation'], + ['JF', 'journal_full_name'], + ['JO', 'journal_abbreviation'], + ['K1', 'keyword1'], + ['KW', 'keyword_phrase'], + ['L1', 'file_attachments'], + ['L2', 'url_link'], + ['L3', 'doi_link'], + ['L4', 'figure_image_link'], + ['LA', 'language'], + ['LB', 'label'], + ['LK', 'links'], + ['LL', 'sponsoring_library_location'], + ['M1', 'miscellaneous1'], + ['M2', 'miscellaneous2'], + ['M3', 'type_of_work'], + ['N1', 'notes1'], + ['N2', 'abstract_notes'], + ['NO', 'notes'], + ['NV', 'number_of_volumes'], + ['OL', 'output_language'], + ['OP', 'original_publication'], + ['PA', 'personal_notes'], + ['PB', 'publisher'], + ['PMCID', 'pmcid'], + ['PMID', 'pmid'], + ['PP', 'place_of_publication'], + ['PY', 'publication_year'], + ['RD', 'retrieved_date'], + ['RI', 'reviewed_item'], + ['RN', 'research_notes'], + ['RP', 'reprint_status'], + ['RT', 'reference_type'], + ['SE', 'section'], + ['SF', 'subfile_database'], + ['SL', 'sponsoring_library'], + ['SN', 'issn_isbn'], + ['SP', 'start_pages'], + ['SR', 'source_type'], + ['ST', 'short_title'], + ['SV', 'series_volume'], + ['T1', 'primary_title'], + ['T2', 'secondary_title'], + ['T3', 'tertiary_title'], + ['TA', 'translated_author'], + ['TI', 'title'], + ['TT', 'translated_title'], + [RTAG_TYPE, 'type_of_reference'], + ['U1', 'user_definable1'], + ['U2', 'user_definable2'], + ['U3', 'user_definable3'], + ['U4', 'user_definable4'], + ['U5', 'user_definable5'], + ['U6', 'user_definable6'], + ['U7', 'user_definable7'], + ['U8', 'user_definable8'], + ['U9', 'user_definable9'], + ['U10', 'user_definable10'], + ['U11', 'user_definable11'], + ['U12', 'user_definable12'], + ['U13', 'user_definable13'], + ['U14', 'user_definable14'], + ['U15', 'user_definable15'], + ['UR', 'web_url'], + ['VL', 'volume'], + ['VO', 'volume_published_standard'], + ['WP', 'date_of_electronic_publication'], + ['WT', 'website_title'], + ['WV', 'website_version'], + ['Y1', 'year_date'], + ['Y2', 'access_date_secondary_date'], + ['YR', 'publication_year_ref'] +] + + +def find_tag(field_name) -> Optional[str]: + for alias in RIS_ALIAS: + if field_name in alias: + return alias[0] + raise ValueError(f'Field not found: {field_name}') + + +class RisEntry: + + def __init__(self): + self.data: Dict[str, str] = OrderedDict() + + def __setitem__(self, field_name, value): + tag = find_tag(field_name) + self.data[tag] = value + + def __getitem__(self, field_name) -> str: + tag = find_tag(field_name) + return self.data.get(tag) + + @classmethod + def from_dict(cls, d: dict): + instance = cls() + for k, v in d.items(): + setattr(instance, k, v) + return instance + + def to_dict(self) -> dict: + return self.data.copy() + + @classmethod + def from_text(cls, text: str): + def _to_tag_value(line: str): + tag, value = line.split('-', 1) + tag = tag.strip() + value = value.lstrip() + value = value.replace('\\n', '\n') + return tag, value + + text = text.strip() + lines = text.splitlines() + entry = RisEntry() + for line in lines: + tag, val = _to_tag_value(line) + if tag == RTAG_END: + break + entry[tag] = val + return entry + + def to_text(self) -> str: + tags = list(self.data.keys()) + if RTAG_TYPE in tags: + tags.remove(RTAG_TYPE) + tags.insert(0, RTAG_TYPE) + + if RTAG_END in tags: + tags.remove(RTAG_END) + + def _to_line(tag, value): + if '\n' in value: + value = value.replace('\n', '\\n') + if value is None: + value = '' + return f'{tag} - {value}\n' + + text = '' + for tag in tags: + text += _to_line(tag, self.data[tag]) + + text += _to_line(RTAG_END, '') + return text From 4848dff4d45b5a114fb9964f91a52727c843729f Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Apr 2024 12:50:54 +0100 Subject: [PATCH 02/46] replace RIS_ALIAS --- doajtest/unit/test_ris.py | 18 +-- portality/lib/ris.py | 250 ++++++++++++++++++++------------------ 2 files changed, 138 insertions(+), 130 deletions(-) diff --git a/doajtest/unit/test_ris.py b/doajtest/unit/test_ris.py index 7e2ef085b..e49f5eea0 100644 --- a/doajtest/unit/test_ris.py +++ b/doajtest/unit/test_ris.py @@ -8,18 +8,18 @@ class TestRisEntry(TestCase): def test_get_set_item__basic(self): test_value = 'value_a' entry = RisEntry() - entry['primary_author'] = test_value - assert entry['primary_author'] == test_value + entry['A1'] = test_value + assert entry['A1'] == test_value def test_get_set_item__alias(self): test_value = 'value_a' entry = RisEntry() - entry['primary_author'] = test_value + entry['A1'] = test_value assert entry['A1'] == test_value def test_getitem__valid_undefined(self): entry = RisEntry() - assert entry['primary_author'] is None + assert entry['A1'] is None def test_setitem__raise_field_not_found(self): entry = RisEntry() @@ -33,9 +33,9 @@ def test_getitem__raise_field_not_found(self): def test_to_dict(self): entry = RisEntry() - entry['primary_author'] = 'value_a' - entry['secondary_author'] = 'value_b' - entry['tertiary_author'] = 'value_c' + entry['A1'] = 'value_a' + entry['A2'] = 'value_b' + entry['A3'] = 'value_c' assert entry.to_dict() == { 'A1': 'value_a', 'A2': 'value_b', @@ -44,8 +44,8 @@ def test_to_dict(self): def test_to_text(self): entry = RisEntry() - entry['primary_author'] = 'value_a' - entry['secondary_author'] = 'value_b' + entry['A1'] = 'value_a' + entry['A2'] = 'value_b' entry['TY'] = 'JOUR' expected = """ diff --git a/portality/lib/ris.py b/portality/lib/ris.py index f1ba0083d..3c16c8379 100644 --- a/portality/lib/ris.py +++ b/portality/lib/ris.py @@ -11,131 +11,131 @@ RTAG_TYPE = 'TY' RTAG_END = 'ER' -RIS_ALIAS = [ - ['A1', 'primary_author'], - ['A2', 'secondary_author'], - ['A3', 'tertiary_author'], - ['A4', 'quaternary_author'], - ['A5', 'quinary_author_compiler'], - ['A6', 'website_editor'], - ['AB', 'abstract_synopsis'], - ['AD', 'author_editor_address'], - ['AN', 'accession_number'], - ['AU', 'author_editor_translator'], - ['AV', 'availability_location'], - ['BT', 'primary_secondary_title'], - ['C1', 'custom1'], - ['C2', 'custom2'], - ['C3', 'custom3'], - ['C4', 'custom4'], - ['C5', 'custom5'], - ['C6', 'custom6'], - ['C7', 'custom7'], - ['C8', 'custom8'], - ['CA', 'caption'], - ['CL', 'classification'], - ['CN', 'call_number'], - ['CP', 'city_place_publication'], - ['CR', 'cited_references'], - ['CT', 'caption_primary_title'], - ['CY', 'place_published'], - ['DA', 'date'], - ['DB', 'name_of_database'], - ['DI', 'digital_object_identifier', 'di', ], - ['DO', 'digital_object_identifier2', 'do', ], - ['DOI', 'digital_object_identifier3', 'doi', ], - ['DP', 'database_provider'], - ['DS', 'data_source'], - ['ED', 'secondary_author'], - ['EP', 'end_page'], - ['ET', 'edition'], - ['FD', 'free_form_publication_data'], - ['H1', 'location_library'], - ['H2', 'location_call_number'], - ['ID', 'reference_identifier'], - ['IP', 'identifying_phrase'], - ['IS', 'number_volumes'], - ['J1', 'journal_abbreviation_1'], - ['J2', 'alternate_title'], - ['JA', 'journal_standard_abbreviation'], - ['JF', 'journal_full_name'], - ['JO', 'journal_abbreviation'], - ['K1', 'keyword1'], - ['KW', 'keyword_phrase'], - ['L1', 'file_attachments'], - ['L2', 'url_link'], - ['L3', 'doi_link'], - ['L4', 'figure_image_link'], - ['LA', 'language'], - ['LB', 'label'], - ['LK', 'links'], - ['LL', 'sponsoring_library_location'], - ['M1', 'miscellaneous1'], - ['M2', 'miscellaneous2'], - ['M3', 'type_of_work'], - ['N1', 'notes1'], - ['N2', 'abstract_notes'], - ['NO', 'notes'], - ['NV', 'number_of_volumes'], - ['OL', 'output_language'], - ['OP', 'original_publication'], - ['PA', 'personal_notes'], - ['PB', 'publisher'], - ['PMCID', 'pmcid'], - ['PMID', 'pmid'], - ['PP', 'place_of_publication'], - ['PY', 'publication_year'], - ['RD', 'retrieved_date'], - ['RI', 'reviewed_item'], - ['RN', 'research_notes'], - ['RP', 'reprint_status'], - ['RT', 'reference_type'], - ['SE', 'section'], - ['SF', 'subfile_database'], - ['SL', 'sponsoring_library'], - ['SN', 'issn_isbn'], - ['SP', 'start_pages'], - ['SR', 'source_type'], - ['ST', 'short_title'], - ['SV', 'series_volume'], - ['T1', 'primary_title'], - ['T2', 'secondary_title'], - ['T3', 'tertiary_title'], - ['TA', 'translated_author'], - ['TI', 'title'], - ['TT', 'translated_title'], - [RTAG_TYPE, 'type_of_reference'], - ['U1', 'user_definable1'], - ['U2', 'user_definable2'], - ['U3', 'user_definable3'], - ['U4', 'user_definable4'], - ['U5', 'user_definable5'], - ['U6', 'user_definable6'], - ['U7', 'user_definable7'], - ['U8', 'user_definable8'], - ['U9', 'user_definable9'], - ['U10', 'user_definable10'], - ['U11', 'user_definable11'], - ['U12', 'user_definable12'], - ['U13', 'user_definable13'], - ['U14', 'user_definable14'], - ['U15', 'user_definable15'], - ['UR', 'web_url'], - ['VL', 'volume'], - ['VO', 'volume_published_standard'], - ['WP', 'date_of_electronic_publication'], - ['WT', 'website_title'], - ['WV', 'website_version'], - ['Y1', 'year_date'], - ['Y2', 'access_date_secondary_date'], - ['YR', 'publication_year_ref'] +RIS_TAGS = [ + 'A1', # primary_author + 'A2', # secondary_author + 'A3', # tertiary_author + 'A4', # quaternary_author + 'A5', # quinary_author_compiler + 'A6', # website_editor + 'AB', # abstract_synopsis + 'AD', # author_editor_address + 'AN', # accession_number + 'AU', # author_editor_translator + 'AV', # availability_location + 'BT', # primary_secondary_title + 'C1', # custom1 + 'C2', # custom2 + 'C3', # custom3 + 'C4', # custom4 + 'C5', # custom5 + 'C6', # custom6 + 'C7', # custom7 + 'C8', # custom8 + 'CA', # caption + 'CL', # classification + 'CN', # call_number + 'CP', # city_place_publication + 'CR', # cited_references + 'CT', # caption_primary_title + 'CY', # place_published + 'DA', # date + 'DB', # name_of_database + 'DI', # digital_object_identifier + 'DO', # digital_object_identifier2 + 'DOI', # digital_object_identifier3 + 'DP', # database_provider + 'DS', # data_source + 'ED', # secondary_author + 'EP', # end_page + 'ET', # edition + 'FD', # free_form_publication_data + 'H1', # location_library + 'H2', # location_call_number + 'ID', # reference_identifier + 'IP', # identifying_phrase + 'IS', # number_volumes + 'J1', # journal_abbreviation_1 + 'J2', # alternate_title + 'JA', # journal_standard_abbreviation + 'JF', # journal_full_name + 'JO', # journal_abbreviation + 'K1', # keyword1 + 'KW', # keyword_phrase + 'L1', # file_attachments + 'L2', # url_link + 'L3', # doi_link + 'L4', # figure_image_link + 'LA', # language + 'LB', # label + 'LK', # links + 'LL', # sponsoring_library_location + 'M1', # miscellaneous1 + 'M2', # miscellaneous2 + 'M3', # type_of_work + 'N1', # notes1 + 'N2', # abstract_notes + 'NO', # notes + 'NV', # number_of_volumes + 'OL', # output_language + 'OP', # original_publication + 'PA', # personal_notes + 'PB', # publisher + 'PMCID', # pmcid + 'PMID', # pmid + 'PP', # place_of_publication + 'PY', # publication_year + 'RD', # retrieved_date + 'RI', # reviewed_item + 'RN', # research_notes + 'RP', # reprint_status + 'RT', # reference_type + 'SE', # section + 'SF', # subfile_database + 'SL', # sponsoring_library + 'SN', # issn_isbn + 'SP', # start_pages + 'SR', # source_type + 'ST', # short_title + 'SV', # series_volume + 'T1', # primary_title + 'T2', # secondary_title + 'T3', # tertiary_title + 'TA', # translated_author + 'TI', # title + 'TT', # translated_title + RTAG_TYPE, # 'type_of_reference' + 'U1', # user_definable1 + 'U2', # user_definable2 + 'U3', # user_definable3 + 'U4', # user_definable4 + 'U5', # user_definable5 + 'U6', # user_definable6 + 'U7', # user_definable7 + 'U8', # user_definable8 + 'U9', # user_definable9 + 'U10', # user_definable10 + 'U11', # user_definable11 + 'U12', # user_definable12 + 'U13', # user_definable13 + 'U14', # user_definable14 + 'U15', # user_definable15 + 'UR', # web_url + 'VL', # volume + 'VO', # volume_published_standard + 'WP', # date_of_electronic_publication + 'WT', # website_title + 'WV', # website_version + 'Y1', # year_date + 'Y2', # access_date_secondary_date + 'YR', # publication_year_ref ] def find_tag(field_name) -> Optional[str]: - for alias in RIS_ALIAS: - if field_name in alias: - return alias[0] + field_name = field_name.upper() + if field_name in RIS_TAGS: + return field_name raise ValueError(f'Field not found: {field_name}') @@ -152,6 +152,14 @@ def __getitem__(self, field_name) -> str: tag = find_tag(field_name) return self.data.get(tag) + @property + def type(self): + return self[RTAG_TYPE] + + @type.setter + def type(self, value): + self[RTAG_TYPE] = value + @classmethod def from_dict(cls, d: dict): instance = cls() From cd9b42abfb3d4eeadf1f7e2261b8ab2849b213f6 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Apr 2024 13:13:42 +0100 Subject: [PATCH 03/46] support values list --- doajtest/unit/test_ris.py | 38 +++++++++++++++----------------------- portality/lib/ris.py | 32 +++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/doajtest/unit/test_ris.py b/doajtest/unit/test_ris.py index e49f5eea0..346181c79 100644 --- a/doajtest/unit/test_ris.py +++ b/doajtest/unit/test_ris.py @@ -5,21 +5,24 @@ class TestRisEntry(TestCase): - def test_get_set_item__basic(self): + def test_get_set_item(self): test_value = 'value_a' entry = RisEntry() entry['A1'] = test_value - assert entry['A1'] == test_value + assert entry['A1'] == [test_value] - def test_get_set_item__alias(self): - test_value = 'value_a' + def test_append(self): entry = RisEntry() - entry['A1'] = test_value - assert entry['A1'] == test_value + entry.append('A1', '1') + entry['A1'].append('2') + assert entry['A1'] == ['1', '2'] + + entry['A1'] = '9' + assert entry['A1'] == ['9'] def test_getitem__valid_undefined(self): entry = RisEntry() - assert entry['A1'] is None + assert entry['A1'] == [] def test_setitem__raise_field_not_found(self): entry = RisEntry() @@ -31,17 +34,6 @@ def test_getitem__raise_field_not_found(self): with self.assertRaises(ValueError): print(entry['qoidjqowijdkncoiqw']) - def test_to_dict(self): - entry = RisEntry() - entry['A1'] = 'value_a' - entry['A2'] = 'value_b' - entry['A3'] = 'value_c' - assert entry.to_dict() == { - 'A1': 'value_a', - 'A2': 'value_b', - 'A3': 'value_c' - } - def test_to_text(self): entry = RisEntry() entry['A1'] = 'value_a' @@ -66,9 +58,9 @@ def test_from_text(self): """.strip() + ' \n' entry = RisEntry.from_text(expected) - assert entry['TY'] == 'JOUR' - assert dict(entry.to_dict()) == { - 'TY': 'JOUR', - 'A1': 'value_a', - 'A2': 'value_b' + assert entry.type == 'JOUR' + assert dict(entry.data) == { + 'TY': ['JOUR'], + 'A1': ['value_a'], + 'A2': ['value_b'], } diff --git a/portality/lib/ris.py b/portality/lib/ris.py index 3c16c8379..3e012b51d 100644 --- a/portality/lib/ris.py +++ b/portality/lib/ris.py @@ -3,6 +3,7 @@ file format references: https://en.wikipedia.org/wiki/RIS_(file_format) """ +import collections import logging from collections import OrderedDict from typing import Dict, Optional @@ -142,19 +143,24 @@ def find_tag(field_name) -> Optional[str]: class RisEntry: def __init__(self): - self.data: Dict[str, str] = OrderedDict() + self.data: collections.defaultdict[str, list] = collections.defaultdict(list) def __setitem__(self, field_name, value): tag = find_tag(field_name) - self.data[tag] = value + self.data[tag] = [value] - def __getitem__(self, field_name) -> str: + def append(self, tag, value) -> list: + tag = find_tag(tag) + self[tag].append(value) + return self[tag] + + def __getitem__(self, field_name) -> list: tag = find_tag(field_name) - return self.data.get(tag) + return self.data[tag] @property def type(self): - return self[RTAG_TYPE] + return self[RTAG_TYPE] and self[RTAG_TYPE][0] @type.setter def type(self, value): @@ -164,11 +170,13 @@ def type(self, value): def from_dict(cls, d: dict): instance = cls() for k, v in d.items(): - setattr(instance, k, v) - return instance + if isinstance(v, list): + for vv in v: + instance[k].append(vv) + else: + instance[k].append(v) - def to_dict(self) -> dict: - return self.data.copy() + return instance @classmethod def from_text(cls, text: str): @@ -186,7 +194,7 @@ def _to_tag_value(line: str): tag, val = _to_tag_value(line) if tag == RTAG_END: break - entry[tag] = val + entry[tag].append(val) return entry def to_text(self) -> str: @@ -207,7 +215,9 @@ def _to_line(tag, value): text = '' for tag in tags: - text += _to_line(tag, self.data[tag]) + values = self.data[tag] + for v in values: + text += _to_line(tag, v) text += _to_line(RTAG_END, '') return text From 5d07e34802119278b30000fcf8e6ad62be89300c Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Apr 2024 14:22:15 +0100 Subject: [PATCH 04/46] add type_of_reference --- portality/lib/ris.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/portality/lib/ris.py b/portality/lib/ris.py index 3e012b51d..d2b741595 100644 --- a/portality/lib/ris.py +++ b/portality/lib/ris.py @@ -142,8 +142,9 @@ def find_tag(field_name) -> Optional[str]: class RisEntry: - def __init__(self): + def __init__(self, type_of_reference: str = None): self.data: collections.defaultdict[str, list] = collections.defaultdict(list) + self.type = type_of_reference def __setitem__(self, field_name, value): tag = find_tag(field_name) From 2a8c3ebcf04791badb3ea9503667809420447155 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Apr 2024 14:22:39 +0100 Subject: [PATCH 05/46] add jsonpath_utils.py --- portality/lib/jsonpath_utils.py | 7 +++++++ setup.py | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 portality/lib/jsonpath_utils.py diff --git a/portality/lib/jsonpath_utils.py b/portality/lib/jsonpath_utils.py new file mode 100644 index 000000000..7201c9bfe --- /dev/null +++ b/portality/lib/jsonpath_utils.py @@ -0,0 +1,7 @@ +from typing import Iterable + +import jsonpath_ng.ext + + +def find_values(query: str, data: dict) -> Iterable: + return (m.value for m in jsonpath_ng.ext.parse(query).find(data)) diff --git a/setup.py b/setup.py index eccafbada..a9a44680b 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,9 @@ 'pandas~=2.0.1', # pandas lets us generate URLs for linkcheck 'gspread-dataframe~=3.3.1', 'gspread-formatting~=1.1.2', + + 'jsonpath-ng~=1.6', + ] + (["setproctitle==1.1.10"] if "linux" in sys.platform else []), extras_require={ # prevent backtracking through all versions From aa25ee3319d2a113f79e76f214491ee6e7bdf95c Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Apr 2024 14:22:59 +0100 Subject: [PATCH 06/46] add ArticleRisXWalk --- doajtest/unit/test_crosswalks_article_ris.py | 39 ++++++++++++++++++++ portality/crosswalks/article_ris.py | 35 ++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 doajtest/unit/test_crosswalks_article_ris.py create mode 100644 portality/crosswalks/article_ris.py diff --git a/doajtest/unit/test_crosswalks_article_ris.py b/doajtest/unit/test_crosswalks_article_ris.py new file mode 100644 index 000000000..b4b499f59 --- /dev/null +++ b/doajtest/unit/test_crosswalks_article_ris.py @@ -0,0 +1,39 @@ +import unittest + +from doajtest.fixtures import ArticleFixtureFactory +from portality import models +from portality.crosswalks.article_ris import ArticleRisXWalk + + +class TestArticleRisXWalk(unittest.TestCase): + def test_article2ris(self): + article = ArticleFixtureFactory.make_article_source() + article = models.Article(**article) + article.bibjson().abstract = "abstract" + ris = ArticleRisXWalk.article2ris(article) + assert ris.type == 'JOUR' + assert ris['T1'] == [article.data['bibjson']['title']] + assert ris.to_text().split() == """ +TY - JOUR +T1 - Article Title +AU - The Author +PY - 1991 +JO - The Title +VL - 1 +SP - 3 +EP - 21 +UR - http://www.example.com/article +AB - abstract +KW - word +KW - key +DOI - 10.0000/SOME.IDENTIFIER +ER - + """.split() + + def test_article2ris__only_title(self): + ris = ArticleRisXWalk.article2ris({"bibjson": {"title": "Article Title"}}) + assert ris.to_text().split() == """ +TY - JOUR +T1 - Article Title +ER - + """.split() diff --git a/portality/crosswalks/article_ris.py b/portality/crosswalks/article_ris.py new file mode 100644 index 000000000..11006ab34 --- /dev/null +++ b/portality/crosswalks/article_ris.py @@ -0,0 +1,35 @@ +from typing import Union + +from portality import models +from portality.lib import jsonpath_utils +from portality.lib.ris import RisEntry + +RIS_ARTICLE_MAPPING = { + 'T1': '$.bibjson.title', + 'AU': '$.bibjson.author[*].name', + 'PY': '$.bibjson.year', + 'JO': '$.bibjson.journal.title', + 'VL': '$.bibjson.journal.volume', + 'SP': '$.bibjson.start_page', + 'EP': '$.bibjson.end_page', + 'UR': '$.bibjson.link[*].url', + 'AB': '$.bibjson.abstract', + 'KW': '$.bibjson.keywords[*]', + 'DOI': '$.bibjson.identifier[?(@.type == "doi")].id', + 'SN': '$.bibjson.journal.issns[*]', +} + + +class ArticleRisXWalk: + + @classmethod + def article2ris(cls, article: Union[models.Article, dict]) -> RisEntry: + if isinstance(article, models.Article): + article = article.data + + entry = RisEntry(type_of_reference='JOUR') + for tag, query in RIS_ARTICLE_MAPPING.items(): + for v in jsonpath_utils.find_values(query, article): + entry[tag].append(v) + + return entry From 6dc56c697ecb59599036274775ba2cb47cf0b528 Mon Sep 17 00:00:00 2001 From: philip Date: Tue, 9 Apr 2024 10:32:57 +0100 Subject: [PATCH 07/46] fix type None --- portality/lib/ris.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/portality/lib/ris.py b/portality/lib/ris.py index d2b741595..36bb83ab9 100644 --- a/portality/lib/ris.py +++ b/portality/lib/ris.py @@ -144,7 +144,8 @@ class RisEntry: def __init__(self, type_of_reference: str = None): self.data: collections.defaultdict[str, list] = collections.defaultdict(list) - self.type = type_of_reference + if type_of_reference: + self.type = type_of_reference def __setitem__(self, field_name, value): tag = find_tag(field_name) From 50024ca3200ed6deda01be804db252a7be62fe4b Mon Sep 17 00:00:00 2001 From: philip Date: Tue, 9 Apr 2024 10:36:11 +0100 Subject: [PATCH 08/46] add export ris for each article result --- portality/static/js/doaj.fieldrender.edges.js | 5 +++ portality/view/doajservices.py | 33 +++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/portality/static/js/doaj.fieldrender.edges.js b/portality/static/js/doaj.fieldrender.edges.js index b82822cfb..6b670ca48 100644 --- a/portality/static/js/doaj.fieldrender.edges.js +++ b/portality/static/js/doaj.fieldrender.edges.js @@ -2872,6 +2872,8 @@ $.extend(true, doaj, { published = 'Published ' + name; } + const export_url = this.doaj_url + '/service/export/article/' + resultobj.id; + var frag = '
  • \
    \
    \ @@ -2905,6 +2907,9 @@ $.extend(true, doaj, {
  • \ About the journal\
  • \ +
  • \ + Export RIS\ +
  • \
  • \ ' + published + '\
  • \ diff --git a/portality/view/doajservices.py b/portality/view/doajservices.py index 48c9c6500..a46fd1461 100644 --- a/portality/view/doajservices.py +++ b/portality/view/doajservices.py @@ -1,13 +1,14 @@ -import json, urllib.request, urllib.parse, urllib.error, requests +import json +from io import BytesIO -from flask import Blueprint, make_response, request, abort, render_template +from flask import Blueprint, make_response, abort, render_template, send_file from flask_login import current_user, login_required -from portality.core import app -from portality.decorators import ssl_required, write_required, restrict_to_role -from portality.util import jsonp from portality import lock, models from portality.bll import DOAJ +from portality.crosswalks.article_ris import ArticleRisXWalk +from portality.decorators import ssl_required, write_required +from portality.util import jsonp blueprint = Blueprint('doajservices', __name__) @@ -40,7 +41,7 @@ def unlock(object_type, object_id): abort(400) # otherwise, return success - resp = make_response(json.dumps({"result" : "success"})) + resp = make_response(json.dumps({"result": "success"})) resp.mimetype = "application/json" return resp @@ -107,7 +108,8 @@ def group_status(group_id): :param group_id: :return: """ - if (not (current_user.has_role("editor") and models.EditorGroup.pull(group_id).editor == current_user.id)) and (not current_user.has_role("admin")): + if (not (current_user.has_role("editor") and models.EditorGroup.pull(group_id).editor == current_user.id)) and ( + not current_user.has_role("admin")): abort(404) svc = DOAJ.todoService() stats = svc.group_stats(group_id) @@ -126,6 +128,7 @@ def dismiss_autocheck(autocheck_set_id, autocheck_id): abort(404) return make_response(json.dumps({"status": "success"})) + @blueprint.route("/autocheck/undismiss//", methods=["GET", "POST"]) @jsonp @login_required @@ -138,3 +141,19 @@ def undismiss_autocheck(autocheck_set_id, autocheck_id): abort(404) return make_response(json.dumps({"status": "success"})) + +@blueprint.route('/export/article/') +def export_article_ris(article_id): + article = models.Article.pull(article_id) + if not article: + abort(404) + + byte_stream = BytesIO() + ris = ArticleRisXWalk.article2ris(article) + byte_stream.write(ris.to_text().encode('utf-8', errors='ignore')) + byte_stream.seek(0) + + filename = f'article-{article_id[:10]}.ris' + + resp = make_response(send_file(byte_stream, as_attachment=True, attachment_filename=filename)) + return resp From a839f54bc3373c94983b06b10dc14b6e73abde72 Mon Sep 17 00:00:00 2001 From: philip Date: Tue, 9 Apr 2024 12:49:20 +0100 Subject: [PATCH 09/46] default autocheck disabled for test cases --- doajtest/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doajtest/helpers.py b/doajtest/helpers.py index a8f33d52d..713d91989 100644 --- a/doajtest/helpers.py +++ b/doajtest/helpers.py @@ -129,6 +129,7 @@ class DoajTestCase(TestCase): @classmethod def create_app_patch(cls): return { + 'AUTOCHECK_INCOMING': False, # old test cases design and depend on work flow of autocheck disabled "STORE_IMPL": "portality.store.StoreLocal", "STORE_LOCAL_DIR": paths.rel2abs(__file__, "..", "tmp", "store", "main", cls.__name__.lower()), "STORE_TMP_DIR": paths.rel2abs(__file__, "..", "tmp", "store", "tmp", cls.__name__.lower()), From 866c52c0ceef1f43a9b53929741371748baecf55 Mon Sep 17 00:00:00 2001 From: philip Date: Tue, 9 Apr 2024 12:52:59 +0100 Subject: [PATCH 10/46] avoid duplicate author names --- portality/crosswalks/article_ris.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/portality/crosswalks/article_ris.py b/portality/crosswalks/article_ris.py index 11006ab34..2b57e2e03 100644 --- a/portality/crosswalks/article_ris.py +++ b/portality/crosswalks/article_ris.py @@ -4,9 +4,16 @@ from portality.lib import jsonpath_utils from portality.lib.ris import RisEntry + +def extra_author_names(article) -> list: + query = '$.bibjson.author[*].name' + values = jsonpath_utils.find_values(query, article) + return sorted(set(values)) + + RIS_ARTICLE_MAPPING = { 'T1': '$.bibjson.title', - 'AU': '$.bibjson.author[*].name', + 'AU': extra_author_names, 'PY': '$.bibjson.year', 'JO': '$.bibjson.journal.title', 'VL': '$.bibjson.journal.volume', @@ -29,7 +36,12 @@ def article2ris(cls, article: Union[models.Article, dict]) -> RisEntry: entry = RisEntry(type_of_reference='JOUR') for tag, query in RIS_ARTICLE_MAPPING.items(): - for v in jsonpath_utils.find_values(query, article): + if callable(query): + values = query(article) + else: + values = jsonpath_utils.find_values(query, article) + + for v in values: entry[tag].append(v) return entry From 8f4fe0737e0034c2b363473e239ca8e52109b5fb Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 10 Apr 2024 10:39:53 +0100 Subject: [PATCH 11/46] add test_view_doajservices.py --- doajtest/unit/test_view_doajservices.py | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 doajtest/unit/test_view_doajservices.py diff --git a/doajtest/unit/test_view_doajservices.py b/doajtest/unit/test_view_doajservices.py new file mode 100644 index 000000000..3b41f39db --- /dev/null +++ b/doajtest/unit/test_view_doajservices.py @@ -0,0 +1,28 @@ +from doajtest.fixtures import ArticleFixtureFactory +from doajtest.helpers import DoajTestCase +from portality.crosswalks.article_ris import ArticleRisXWalk +from portality.models import Article +from portality.util import url_for + + +class TestDoajservices(DoajTestCase): + + def test_export_article_ris(self): + article = Article(**ArticleFixtureFactory.make_article_source()) + article.save(blocking=True) + Article.refresh() + + ris = ArticleRisXWalk.article2ris(article).to_text() + + with self.app_test.test_client() as t_client: + url = url_for('doajservices.export_article_ris', article_id=article.id) + response = t_client.get(url) + assert response.status_code == 200 + assert response.get_data(as_text=True) == ris + + def test_export_article_ris__not_found(self): + with self.app_test.test_client() as t_client: + url = url_for('doajservices.export_article_ris', + article_id='article_id_that_does_not_exist') + response = t_client.get(url) + assert response.status_code == 404 From b761ad92aaddbe6d13bb56d53847a862c013f709 Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 10 Apr 2024 11:19:04 +0100 Subject: [PATCH 12/46] improve RIS mapping --- portality/crosswalks/article_ris.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/portality/crosswalks/article_ris.py b/portality/crosswalks/article_ris.py index 2b57e2e03..214720084 100644 --- a/portality/crosswalks/article_ris.py +++ b/portality/crosswalks/article_ris.py @@ -15,8 +15,10 @@ def extra_author_names(article) -> list: 'T1': '$.bibjson.title', 'AU': extra_author_names, 'PY': '$.bibjson.year', - 'JO': '$.bibjson.journal.title', + 'JF': '$.bibjson.journal.title', + 'PB': '$.bibjson.journal.publisher', 'VL': '$.bibjson.journal.volume', + 'IS': '$.bibjson.journal.number', 'SP': '$.bibjson.start_page', 'EP': '$.bibjson.end_page', 'UR': '$.bibjson.link[*].url', @@ -24,6 +26,7 @@ def extra_author_names(article) -> list: 'KW': '$.bibjson.keywords[*]', 'DOI': '$.bibjson.identifier[?(@.type == "doi")].id', 'SN': '$.bibjson.journal.issns[*]', + 'LA': '$.language.language', } From 9ada58ee895617fe5e6c34a92f634cd22d805c0f Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 10 Apr 2024 11:24:23 +0100 Subject: [PATCH 13/46] add Export article in RIS format --- doajtest/testbook/public_site/public_search.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doajtest/testbook/public_site/public_search.yml b/doajtest/testbook/public_site/public_search.yml index 6ff91b38d..166b02c25 100644 --- a/doajtest/testbook/public_site/public_search.yml +++ b/doajtest/testbook/public_site/public_search.yml @@ -186,4 +186,13 @@ tests: - step: click spacebar to check the filter results: - filter is applied - +- title: Export article in RIS format + context: + role: anonymous + steps: + - step: Go to the DOAJ search page at /search/articles + results: + - Only articles are shown in the results + - step: Click on 'Export RIS' of any article + results: + - A RIS file is downloaded From 3c138f1738e6fc6c3e67b730221664c131c9f14e Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 10 Apr 2024 18:32:52 +0100 Subject: [PATCH 14/46] fix testcases --- doajtest/unit/test_crosswalks_article_ris.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doajtest/unit/test_crosswalks_article_ris.py b/doajtest/unit/test_crosswalks_article_ris.py index b4b499f59..5d58655f4 100644 --- a/doajtest/unit/test_crosswalks_article_ris.py +++ b/doajtest/unit/test_crosswalks_article_ris.py @@ -18,8 +18,10 @@ def test_article2ris(self): T1 - Article Title AU - The Author PY - 1991 -JO - The Title +JF - The Title +PB - The Publisher VL - 1 +IS - 99 SP - 3 EP - 21 UR - http://www.example.com/article From b0dfc6bca65c57a3cb8fda7993fa1391fdabe13b Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 15 Apr 2024 11:46:35 +0100 Subject: [PATCH 15/46] draft articleinfo --- portality/forms/application_forms.py | 6 ++++-- portality/static/js/formulaic.js | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/portality/forms/application_forms.py b/portality/forms/application_forms.py index b370d2905..490269ad5 100644 --- a/portality/forms/application_forms.py +++ b/portality/forms/application_forms.py @@ -1881,7 +1881,8 @@ class FieldDefinitions: "entry_template": "application_form/_entry_group.html", "widgets": [ {"infinite_repeat" : {"enable_on_repeat" : ["textarea"]}}, - "note_modal" + "note_modal", + "article_info", ], "merge_disabled" : "merge_disabled_notes", } @@ -3042,7 +3043,8 @@ def wtforms(field, settings): "trim_whitespace" : "formulaic.widgets.newTrimWhitespace", # ~~-> TrimWhitespace:FormWidget~~ "note_modal" : "formulaic.widgets.newNoteModal", # ~~-> NoteModal:FormWidget~~ "autocheck": "formulaic.widgets.newAutocheck", # ~~-> Autocheck:FormWidget~~ - "issn_link" : "formulaic.widgets.newIssnLink" # ~~-> IssnLink:FormWidget~~, + "issn_link" : "formulaic.widgets.newIssnLink", # ~~-> IssnLink:FormWidget~~, + "article_info": "formulaic.widgets.newArticleInfo", # ~~-> ArticleInfo:FormWidget~~ } diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 0c5932c3c..7ae33d0a9 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -1155,14 +1155,14 @@ var formulaic = { this._renderAutocheck = function(autocheck) { let frag = "
  • "; - + if (autocheck.checked_by && doaj.autocheckers && doaj.autocheckers.registry.hasOwnProperty(autocheck.checked_by)) { frag += (new doaj.autocheckers.registry[autocheck.checked_by]()).draw(autocheck) } else { frag += this._defaultRender(autocheck); } - + frag += `
  • `; return frag; } @@ -2252,5 +2252,20 @@ var formulaic = { this.init(); }, + + newArticleInfo : (params) => edges.instantiate(formulaic.widgets.ArticleInfo, params), + ArticleInfo: function ({formulaic, fieldDef, args}) { + const init = () => { + console.log() + console.log(this.fieldDef); + debugger + }; + + init(); + }, + + + + } }; From edcd98adb884e40eab5f4c86eeddea9054e9f402 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 15 Apr 2024 13:03:14 +0100 Subject: [PATCH 16/46] implement ArticleInfo --- portality/static/js/formulaic.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 7ae33d0a9..ec438f321 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -2256,9 +2256,15 @@ var formulaic = { newArticleInfo : (params) => edges.instantiate(formulaic.widgets.ArticleInfo, params), ArticleInfo: function ({formulaic, fieldDef, args}) { const init = () => { - console.log() - console.log(this.fieldDef); - debugger + const paths = window.location.pathname.split('/') + const journalId = paths[paths.length - 1] + fetch(`/admin/journal/${journalId}/article-info`) + .then(response => response.json()) + .then(data => { + const $p = $('.doaj_seal__container').prev('p'); + const text = $p.text() + $p.text(text + `This journal has ${data.n_articles} articles in DOAJ.`) + }) }; init(); From c90cda05b647c8d1a042482aeee35b5570c9232c Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 15 Apr 2024 13:06:24 +0100 Subject: [PATCH 17/46] update dictionary --- docs/dictionary.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/dictionary.md b/docs/dictionary.md index 49ba798b8..fcbee273d 100644 --- a/docs/dictionary.md +++ b/docs/dictionary.md @@ -1,10 +1,11 @@ -| Short | Description | -|---------|------------------------------| -| bgjob | background job | -| noti | notification | -| noqa | NO-QA (NO Quality Assurance) | -| inst | instance | -| fmt | format | -| exparam | extra parameter | -| maned | Managing Editor | -| gsheet | Google Sheet | \ No newline at end of file +| Short | Description | +|----------|------------------------------| +| bgjob | background job | +| noti | notification | +| noqa | NO-QA (NO Quality Assurance) | +| inst | instance | +| fmt | format | +| exparam | extra parameter | +| maned | Managing Editor | +| gsheet | Google Sheet | +| svc,serv | service | \ No newline at end of file From dcd962af3b2c966f2a91d69e5495f80d9b03f47b Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 15 Apr 2024 13:06:53 +0100 Subject: [PATCH 18/46] setup article_info widgets --- portality/forms/application_forms.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/portality/forms/application_forms.py b/portality/forms/application_forms.py index 490269ad5..71dde9389 100644 --- a/portality/forms/application_forms.py +++ b/portality/forms/application_forms.py @@ -1603,7 +1603,7 @@ class FieldDefinitions: # ~~->$ DOAJSeal:FormField~~ DOAJ_SEAL = { "name": "doaj_seal", - "label": "The journal has fulfilled all the criteria for the Seal. Award the Seal?", + "label": "Award the Seal?", "input": "checkbox", "validate": [ { @@ -1626,7 +1626,10 @@ class FieldDefinitions: "the journal must use a persistent identifier" } } - ] + ], + "widgets": [ + "article_info", + ], } # FIXME: this probably shouldn't be in the admin form fieldsets, rather its own separate form @@ -1882,7 +1885,6 @@ class FieldDefinitions: "widgets": [ {"infinite_repeat" : {"enable_on_repeat" : ["textarea"]}}, "note_modal", - "article_info", ], "merge_disabled" : "merge_disabled_notes", } From 969c687d7d3eff26fcb6b2174f0358ea11a41c32 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 15 Apr 2024 13:44:21 +0100 Subject: [PATCH 19/46] implement query of journal_article_info --- portality/view/admin.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/portality/view/admin.py b/portality/view/admin.py index 484441a02..187aee04e 100644 --- a/portality/view/admin.py +++ b/portality/view/admin.py @@ -7,13 +7,14 @@ from flask_login import current_user, login_required from werkzeug.datastructures import MultiDict -from portality import dao import portality.models as models from portality import constants +from portality import dao from portality import lock from portality.background import BackgroundSummary from portality.bll import DOAJ, exceptions from portality.bll.exceptions import ArticleMergeConflict, DuplicateArticleException +from portality.bll.services.query import Query from portality.core import app from portality.crosswalks.application_form import ApplicationFormXWalk from portality.decorators import ssl_required, restrict_to_role, write_required @@ -28,8 +29,6 @@ from portality.ui.messages import Messages from portality.util import flash_with_url, jsonp, make_json_resp, get_web_json_payload, validate_json from portality.view.forms import EditorGroupForm, MakeContinuation - -from portality.bll.services.query import Query from portality.view.view_helper import exparam_editing_user # ~~Admin:Blueprint~~ @@ -322,6 +321,15 @@ def journals_bulk_reinstate(): # ##################################################################### +@blueprint.route("/journal//article-info/", methods=["GET"]) +def journal_article_info(journal_id): + j = models.Journal.pull(journal_id) + if j is None: + abort(404) + + return {'n_articles': models.Article.count_by_issns(j.bibjson().issns())} + + @blueprint.route("/journal//continue", methods=["GET", "POST"]) @login_required @ssl_required @@ -432,7 +440,8 @@ def application(application_id): flash(str(e)) return redirect(url_for("admin.application", application_id=ap.id, _anchor='cannot_edit')) else: - return fc.render_template(obj=ap, lock=lockinfo, form_diff=form_diff, current_journal=current_journal, lcc_tree=lcc_jstree, autochecks=autochecks) + return fc.render_template(obj=ap, lock=lockinfo, form_diff=form_diff, current_journal=current_journal, + lcc_tree=lcc_jstree, autochecks=autochecks) @blueprint.route("/application_quick_reject/", methods=["POST"]) From 3dcc38ddc02e60684958383ed990a5ec4cff0803 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 15 Apr 2024 13:49:12 +0100 Subject: [PATCH 20/46] add checking before init --- portality/static/js/formulaic.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index ec438f321..07d986f98 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -2255,19 +2255,26 @@ var formulaic = { newArticleInfo : (params) => edges.instantiate(formulaic.widgets.ArticleInfo, params), ArticleInfo: function ({formulaic, fieldDef, args}) { + const sealSelector = '.doaj_seal__container' + const init = () => { const paths = window.location.pathname.split('/') const journalId = paths[paths.length - 1] fetch(`/admin/journal/${journalId}/article-info`) .then(response => response.json()) .then(data => { - const $p = $('.doaj_seal__container').prev('p'); + const $p = $(sealSelector).prev('p'); const text = $p.text() $p.text(text + `This journal has ${data.n_articles} articles in DOAJ.`) }) }; - init(); + + if ($(sealSelector).length) { + init(); + } else { + console.log('skip ArticleInfo, seal section not found') + } }, From 96ac2eb3f8aa8fb26f05f6297b8d7f703590e0a2 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 15 Apr 2024 16:33:55 +0100 Subject: [PATCH 21/46] add test cases --- doajtest/fixtures/accounts.py | 6 ++- doajtest/helpers.py | 4 +- .../api_tests/test_api_crud_returnvalues.py | 5 +-- doajtest/unit/test_view_admin.py | 37 +++++++++++++++++++ portality/view/admin.py | 1 + 5 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 doajtest/unit/test_view_admin.py diff --git a/doajtest/fixtures/accounts.py b/doajtest/fixtures/accounts.py index a36dd60c6..4b974490f 100644 --- a/doajtest/fixtures/accounts.py +++ b/doajtest/fixtures/accounts.py @@ -83,7 +83,11 @@ def create_publisher_a(): return publisher -def create_maned_a(): +def create_maned_a(is_save=False): from portality import models maned = models.Account(**AccountFixtureFactory.make_managing_editor_source()) + maned.set_password("password") + if is_save: + maned.save(blocking=True) return maned + diff --git a/doajtest/helpers.py b/doajtest/helpers.py index a8f33d52d..0cd4fb76f 100644 --- a/doajtest/helpers.py +++ b/doajtest/helpers.py @@ -410,9 +410,9 @@ def assert_expected_dict(test_case: TestCase, target, expected: dict): test_case.assertDictEqual(actual, expected) -def login(app_client, username, password, follow_redirects=True): +def login(app_client, email, password, follow_redirects=True): return app_client.post(url_for('account.login'), - data=dict(user=username, password=password), + data=dict(user=email, password=password), follow_redirects=follow_redirects) diff --git a/doajtest/unit/api_tests/test_api_crud_returnvalues.py b/doajtest/unit/api_tests/test_api_crud_returnvalues.py index 1d708b422..b398d0d23 100644 --- a/doajtest/unit/api_tests/test_api_crud_returnvalues.py +++ b/doajtest/unit/api_tests/test_api_crud_returnvalues.py @@ -1,3 +1,4 @@ +from doajtest import helpers from doajtest.helpers import DoajTestCase, with_es from portality import models from doajtest.fixtures import ApplicationFixtureFactory, ArticleFixtureFactory, JournalFixtureFactory @@ -205,9 +206,7 @@ def test_04_article_structure_exceptions(self): @staticmethod def login(app, username, password): - return app.post('/account/login', - data=dict(username=username, password=password), - follow_redirects=True) + return helpers.login(app, username, password) @staticmethod def logout(app): diff --git a/doajtest/unit/test_view_admin.py b/doajtest/unit/test_view_admin.py new file mode 100644 index 000000000..2f111bb2d --- /dev/null +++ b/doajtest/unit/test_view_admin.py @@ -0,0 +1,37 @@ +import json + +from doajtest import helpers +from doajtest.fixtures import JournalFixtureFactory +from doajtest.fixtures.accounts import create_maned_a +from doajtest.helpers import DoajTestCase +from portality import models +from portality.util import url_for + + +class TestViewAdmin(DoajTestCase): + + def setUp(self): + super().setUp() + self.acc = create_maned_a(is_save=True) + + def test_journal_article_info(self): + journal = models.Journal( + **JournalFixtureFactory.make_journal_source() + ) + journal.save(blocking=True) + models.Journal.refresh() + + with self.app_test.test_client() as client: + resp = helpers.login(client, self.acc.email, 'password') + assert resp.status_code == 200 + + resp = client.get(url_for("admin.journal_article_info", journal_id=journal.id)) + assert resp.status_code == 200 + assert json.loads(resp.data) == {'n_articles': 0} + + def test_journal_article_info__not_found(self): + with self.app_test.test_client() as client: + helpers.login(client, self.acc.email, 'password') + + resp = client.get(url_for("admin.journal_article_info", journal_id='aksjdlaksjdlkajsdlkajsdlk')) + assert resp.status_code == 404 diff --git a/portality/view/admin.py b/portality/view/admin.py index 187aee04e..ba440cea7 100644 --- a/portality/view/admin.py +++ b/portality/view/admin.py @@ -322,6 +322,7 @@ def journals_bulk_reinstate(): ##################################################################### @blueprint.route("/journal//article-info/", methods=["GET"]) +@login_required def journal_article_info(journal_id): j = models.Journal.pull(journal_id) if j is None: From d2f234d6676e8f1f5a7033900046e565c0bdd756 Mon Sep 17 00:00:00 2001 From: philip Date: Fri, 19 Apr 2024 12:07:45 +0100 Subject: [PATCH 22/46] wording --- portality/templates/application_form/editorial_form_fields.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/templates/application_form/editorial_form_fields.html b/portality/templates/application_form/editorial_form_fields.html index 0b91ff624..f7d28bc3a 100644 --- a/portality/templates/application_form/editorial_form_fields.html +++ b/portality/templates/application_form/editorial_form_fields.html @@ -56,7 +56,7 @@

    {{ fs.label }}

    {% set fs = formulaic_context.fieldset("seal") %} {% if fs %}

    {{ fs.label }}

    -

    The journal has fulfilled all the criteria for the Seal.

    +

    The journal may have fulfilled all the criteria for the Seal.

    {% for f in fs.fields() %} {% set field_template = f.template %} {% include field_template %} From 2ba0038dbea8822f4aca02cdb3b8631ff1b3986b Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 29 Apr 2024 14:02:19 +0100 Subject: [PATCH 23/46] add fmt --- doajtest/unit/test_view_doajservices.py | 4 ++-- portality/static/js/doaj.fieldrender.edges.js | 2 +- portality/static/vendor/edges | 2 +- portality/view/doajservices.py | 8 ++++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/doajtest/unit/test_view_doajservices.py b/doajtest/unit/test_view_doajservices.py index 3b41f39db..30e41ba17 100644 --- a/doajtest/unit/test_view_doajservices.py +++ b/doajtest/unit/test_view_doajservices.py @@ -15,7 +15,7 @@ def test_export_article_ris(self): ris = ArticleRisXWalk.article2ris(article).to_text() with self.app_test.test_client() as t_client: - url = url_for('doajservices.export_article_ris', article_id=article.id) + url = url_for('doajservices.export_article_ris', article_id=article.id, fmt='ris') response = t_client.get(url) assert response.status_code == 200 assert response.get_data(as_text=True) == ris @@ -23,6 +23,6 @@ def test_export_article_ris(self): def test_export_article_ris__not_found(self): with self.app_test.test_client() as t_client: url = url_for('doajservices.export_article_ris', - article_id='article_id_that_does_not_exist') + article_id='article_id_that_does_not_exist', fmt='ris') response = t_client.get(url) assert response.status_code == 404 diff --git a/portality/static/js/doaj.fieldrender.edges.js b/portality/static/js/doaj.fieldrender.edges.js index 6b670ca48..3cd2d3559 100644 --- a/portality/static/js/doaj.fieldrender.edges.js +++ b/portality/static/js/doaj.fieldrender.edges.js @@ -2872,7 +2872,7 @@ $.extend(true, doaj, { published = 'Published ' + name; } - const export_url = this.doaj_url + '/service/export/article/' + resultobj.id; + const export_url = this.doaj_url + '/service/export/article/' + resultobj.id + '/ris'; var frag = '
  • \
    \ diff --git a/portality/static/vendor/edges b/portality/static/vendor/edges index 990f42201..9639b871a 160000 --- a/portality/static/vendor/edges +++ b/portality/static/vendor/edges @@ -1 +1 @@ -Subproject commit 990f4220163a3e18880f0bdc3ad5c80d234d22dd +Subproject commit 9639b871acb1f6590ee78e236f2c9333479c9fe8 diff --git a/portality/view/doajservices.py b/portality/view/doajservices.py index a46fd1461..251730d1a 100644 --- a/portality/view/doajservices.py +++ b/portality/view/doajservices.py @@ -142,12 +142,16 @@ def undismiss_autocheck(autocheck_set_id, autocheck_id): return make_response(json.dumps({"status": "success"})) -@blueprint.route('/export/article/') -def export_article_ris(article_id): +@blueprint.route('/export/article//') +def export_article_ris(article_id, fmt): article = models.Article.pull(article_id) if not article: abort(404) + if fmt != 'ris': + # only support ris for now + abort(404) + byte_stream = BytesIO() ris = ArticleRisXWalk.article2ris(article) byte_stream.write(ris.to_text().encode('utf-8', errors='ignore')) From 38fe7104cb99b8658414724389dc99aebc22de71 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 29 Apr 2024 14:09:23 +0100 Subject: [PATCH 24/46] add download icon --- .../static/doaj/images/feather-icons/download.svg | 1 + portality/static/js/doaj.fieldrender.edges.js | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 portality/static/doaj/images/feather-icons/download.svg diff --git a/portality/static/doaj/images/feather-icons/download.svg b/portality/static/doaj/images/feather-icons/download.svg new file mode 100644 index 000000000..76767a924 --- /dev/null +++ b/portality/static/doaj/images/feather-icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/portality/static/js/doaj.fieldrender.edges.js b/portality/static/js/doaj.fieldrender.edges.js index 3cd2d3559..276bb08c7 100644 --- a/portality/static/js/doaj.fieldrender.edges.js +++ b/portality/static/js/doaj.fieldrender.edges.js @@ -2899,8 +2899,7 @@ $.extend(true, doaj, { Read online ' if (this.widget){ frag += 'external-link icon' - } - else { + } else { frag += '' } frag += '
  • \ @@ -2908,7 +2907,14 @@ $.extend(true, doaj, { About the journal\
  • \
  • \ - Export RIS\ + \ + Export RIS ' + if (this.widget){ + frag += 'external-link icon' + } else { + frag += '' + } + frag += '\
  • \
  • \ ' + published + '\ From ee0a321e8a0b100b5467ec7787696f2730030f6c Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 1 May 2024 12:42:41 +0100 Subject: [PATCH 25/46] link order --- portality/static/js/doaj.fieldrender.edges.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/portality/static/js/doaj.fieldrender.edges.js b/portality/static/js/doaj.fieldrender.edges.js index 276bb08c7..fe135729c 100644 --- a/portality/static/js/doaj.fieldrender.edges.js +++ b/portality/static/js/doaj.fieldrender.edges.js @@ -2903,10 +2903,7 @@ $.extend(true, doaj, { frag += '' } frag += '
  • \ -
  • \ - About the journal\ -
  • \ -
  • \ +
  • \ \ Export RIS ' if (this.widget){ @@ -2916,6 +2913,9 @@ $.extend(true, doaj, { } frag += '\
  • \ +
  • \ + About the journal\ +
  • \
  • \ ' + published + '\
  • \ From 9532667c6405867abb01477f4119a6edcbc9c8d4 Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 1 May 2024 13:15:01 +0100 Subject: [PATCH 26/46] naming --- doajtest/fixtures/accounts.py | 4 ++-- doajtest/unit/test_view_admin.py | 2 +- portality/static/vendor/edges | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doajtest/fixtures/accounts.py b/doajtest/fixtures/accounts.py index 4b974490f..231c02ce3 100644 --- a/doajtest/fixtures/accounts.py +++ b/doajtest/fixtures/accounts.py @@ -83,11 +83,11 @@ def create_publisher_a(): return publisher -def create_maned_a(is_save=False): +def create_maned_a(save=False): from portality import models maned = models.Account(**AccountFixtureFactory.make_managing_editor_source()) maned.set_password("password") - if is_save: + if save: maned.save(blocking=True) return maned diff --git a/doajtest/unit/test_view_admin.py b/doajtest/unit/test_view_admin.py index 2f111bb2d..84eead555 100644 --- a/doajtest/unit/test_view_admin.py +++ b/doajtest/unit/test_view_admin.py @@ -12,7 +12,7 @@ class TestViewAdmin(DoajTestCase): def setUp(self): super().setUp() - self.acc = create_maned_a(is_save=True) + self.acc = create_maned_a(save=True) def test_journal_article_info(self): journal = models.Journal( diff --git a/portality/static/vendor/edges b/portality/static/vendor/edges index 990f42201..9639b871a 160000 --- a/portality/static/vendor/edges +++ b/portality/static/vendor/edges @@ -1 +1 @@ -Subproject commit 990f4220163a3e18880f0bdc3ad5c80d234d22dd +Subproject commit 9639b871acb1f6590ee78e236f2c9333479c9fe8 From e52ca45d8e4937d78e7ba1fc1c14a06fafe509c6 Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 1 May 2024 13:59:18 +0100 Subject: [PATCH 27/46] use multiple checkbox for styles only --- portality/crosswalks/journal_form.py | 4 ++-- portality/forms/application_forms.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/portality/crosswalks/journal_form.py b/portality/crosswalks/journal_form.py index ecdc20c61..91ad74a13 100644 --- a/portality/crosswalks/journal_form.py +++ b/portality/crosswalks/journal_form.py @@ -289,7 +289,7 @@ def form2admin(cls, form, obj): obj.set_editor(editor) if getattr(form, "doaj_seal", None): - obj.set_seal(form.doaj_seal.data) + obj.set_seal('y' in form.doaj_seal.data) @classmethod def bibjson2form(cls, bibjson, forminfo): @@ -457,7 +457,7 @@ def admin2form(cls, obj, forminfo): if obj.editor is not None: forminfo['editor'] = obj.editor - forminfo['doaj_seal'] = obj.has_seal() + forminfo['doaj_seal'] = ['y'] if obj.has_seal() else [] class JournalFormXWalk(JournalGenericXWalk): diff --git a/portality/forms/application_forms.py b/portality/forms/application_forms.py index d083a459f..93ee44e21 100644 --- a/portality/forms/application_forms.py +++ b/portality/forms/application_forms.py @@ -1603,8 +1603,13 @@ class FieldDefinitions: # ~~->$ DOAJSeal:FormField~~ DOAJ_SEAL = { "name": "doaj_seal", - "label": "Award the Seal?", + "label": "The journal may have fulfilled all the criteria for the Seal.", + "multiple": True, "input": "checkbox", + "options": [ + {"display": "Award the Seal?", "value": 'y'}, + ], + "validate": [ { "only_if" : { From f415303a7e0b57a012a653c3f45fc788a186815b Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 1 May 2024 14:10:46 +0100 Subject: [PATCH 28/46] update checkbox title --- portality/static/js/formulaic.js | 7 +++---- .../templates/application_form/editorial_form_fields.html | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 07d986f98..04a6b85b8 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -2255,7 +2255,7 @@ var formulaic = { newArticleInfo : (params) => edges.instantiate(formulaic.widgets.ArticleInfo, params), ArticleInfo: function ({formulaic, fieldDef, args}) { - const sealSelector = '.doaj_seal__container' + const sealSelector = 'label[for=doaj_seal]'; const init = () => { const paths = window.location.pathname.split('/') @@ -2263,9 +2263,8 @@ var formulaic = { fetch(`/admin/journal/${journalId}/article-info`) .then(response => response.json()) .then(data => { - const $p = $(sealSelector).prev('p'); - const text = $p.text() - $p.text(text + `This journal has ${data.n_articles} articles in DOAJ.`) + const $ele = $(sealSelector); + $ele.text($ele.text() + `This journal has ${data.n_articles} articles in DOAJ.`) }) }; diff --git a/portality/templates/application_form/editorial_form_fields.html b/portality/templates/application_form/editorial_form_fields.html index ba8f74cab..e56769093 100644 --- a/portality/templates/application_form/editorial_form_fields.html +++ b/portality/templates/application_form/editorial_form_fields.html @@ -56,7 +56,6 @@

    {{ fs.label }}

    {% set fs = formulaic_context.fieldset("seal") %} {% if fs %}

    {{ fs.label }}

    -

    The journal may have fulfilled all the criteria for the Seal.

    {% for f in fs.fields() %} {% set field_template = f.template %} {% include field_template %} From ee1b64226e2a5e8b32031150c30e7905cfef3e05 Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 19 Jun 2024 11:09:35 +0100 Subject: [PATCH 29/46] fix for seal form field --- doajtest/fixtures/v2/common.py | 2 +- .../unit/application_processors/test_maned_journal_review.py | 4 ++-- portality/tasks/journal_bulk_edit.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doajtest/fixtures/v2/common.py b/doajtest/fixtures/v2/common.py index efa5f7367..c550437c9 100644 --- a/doajtest/fixtures/v2/common.py +++ b/doajtest/fixtures/v2/common.py @@ -25,7 +25,7 @@ } SEAL_FORM_EXPANDED = { - "doaj_seal": False, + "doaj_seal": [], } JOURNAL_LIKE_BIBJSON = { diff --git a/doajtest/unit/application_processors/test_maned_journal_review.py b/doajtest/unit/application_processors/test_maned_journal_review.py index 07badcbab..b62fbb146 100644 --- a/doajtest/unit/application_processors/test_maned_journal_review.py +++ b/doajtest/unit/application_processors/test_maned_journal_review.py @@ -148,7 +148,7 @@ def test_04_maned_review_doaj_seal(self): ) # set the seal to False using the form - fc.form.doaj_seal.data = False + fc.form.doaj_seal.data = [] # run the crosswalk, don't test it at all in this test fc.form2target() @@ -162,7 +162,7 @@ def test_04_maned_review_doaj_seal(self): fc.source.set_seal(True) fc.source2form() - assert fc.form.doaj_seal.data is True + assert 'y' in fc.form.doaj_seal.data def test_05_maned_review_continuations(self): # construct it from form data (with a known source) diff --git a/portality/tasks/journal_bulk_edit.py b/portality/tasks/journal_bulk_edit.py index 3e39d4388..0b4242adb 100644 --- a/portality/tasks/journal_bulk_edit.py +++ b/portality/tasks/journal_bulk_edit.py @@ -123,8 +123,8 @@ def run(self): job.add_audit_message("Setting {f} to {x} for journal {y}".format(f=k, x=v, y=journal_id)) fc.form[k].data = v else: - if v: - fc.form.doaj_seal.data = v + if v or (isinstance(v, str) and v.lower() == 'y'): + fc.form.doaj_seal.data = ['y'] updated = True if note: From 56a56ea473374c0242cf48f2cd5bac7ed38b9bbe Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Jul 2024 12:54:36 +0100 Subject: [PATCH 30/46] avoid send request by invalid id --- portality/static/js/formulaic.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 04a6b85b8..5da5dca98 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -2255,25 +2255,24 @@ var formulaic = { newArticleInfo : (params) => edges.instantiate(formulaic.widgets.ArticleInfo, params), ArticleInfo: function ({formulaic, fieldDef, args}) { - const sealSelector = 'label[for=doaj_seal]'; - - const init = () => { - const paths = window.location.pathname.split('/') - const journalId = paths[paths.length - 1] - fetch(`/admin/journal/${journalId}/article-info`) - .then(response => response.json()) - .then(data => { - const $ele = $(sealSelector); - $ele.text($ele.text() + `This journal has ${data.n_articles} articles in DOAJ.`) - }) - }; + const $sealEle = $('label[for=doaj_seal]'); - - if ($(sealSelector).length) { - init(); - } else { + if (!$sealEle.length) { console.log('skip ArticleInfo, seal section not found') + return; + } + + const idResult = window.location.pathname.match('/journal/([a-f0-9]+)') + if (!idResult) { + console.log('skip ArticleInfo, journal id not found') + return } + const journalId = idResult[1] + fetch(`/admin/journal/${journalId}/article-info`) + .then(response => response.json()) + .then(data => { + $sealEle.text($sealEle.text() + `This journal has ${data.n_articles} articles in DOAJ.`) + }) }, From f768d1b069653ecf6ccfd8ad8d9abac742e3a843 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Jul 2024 13:04:30 +0100 Subject: [PATCH 31/46] support in_doaj for count_by_issns query --- portality/models/article.py | 10 +++++++--- portality/view/admin.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/portality/models/article.py b/portality/models/article.py index d431bb5ae..392a16851 100644 --- a/portality/models/article.py +++ b/portality/models/article.py @@ -85,8 +85,8 @@ def find_by_issns(cls, issns): return articles @classmethod - def count_by_issns(cls, issns): - q = ArticleQuery(issns=issns) + def count_by_issns(cls, issns, in_doaj=None): + q = ArticleQuery(issns=issns, in_doaj=in_doaj) return cls.hit_count(q.query()) @classmethod @@ -866,9 +866,10 @@ class ArticleQuery(object): _issn_terms = { "terms" : {"index.issn.exact" : [""]} } _volume_term = { "term" : {"bibjson.journal.volume.exact" : ""} } - def __init__(self, issns=None, volume=None): + def __init__(self, issns=None, volume=None, in_doaj=None): self.issns = issns self.volume = volume + self.in_doaj = in_doaj def query(self): q = deepcopy(self.base_query) @@ -883,6 +884,9 @@ def query(self): vq["term"]["bibjson.journal.volume.exact"] = self.volume q["query"]["bool"]["must"].append(vq) + if self.in_doaj is not None: + q["query"]["bool"]["must"].append({"term": {"admin.in_doaj": self.in_doaj}}) + return q class ArticleVolumesQuery(object): diff --git a/portality/view/admin.py b/portality/view/admin.py index ba440cea7..72c4fb6a6 100644 --- a/portality/view/admin.py +++ b/portality/view/admin.py @@ -328,7 +328,7 @@ def journal_article_info(journal_id): if j is None: abort(404) - return {'n_articles': models.Article.count_by_issns(j.bibjson().issns())} + return {'n_articles': models.Article.count_by_issns(j.bibjson().issns(), in_doaj=True)} @blueprint.route("/journal//continue", methods=["GET", "POST"]) From 5e9f946e232d33ef89b245874985893c5ca79bf3 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Jul 2024 13:08:23 +0100 Subject: [PATCH 32/46] change ArticleInfo result layout --- portality/static/js/formulaic.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 5da5dca98..2068abe12 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -2255,7 +2255,7 @@ var formulaic = { newArticleInfo : (params) => edges.instantiate(formulaic.widgets.ArticleInfo, params), ArticleInfo: function ({formulaic, fieldDef, args}) { - const $sealEle = $('label[for=doaj_seal]'); + const $sealEle = $('label[for=doaj_seal-0]'); if (!$sealEle.length) { console.log('skip ArticleInfo, seal section not found') @@ -2271,7 +2271,7 @@ var formulaic = { fetch(`/admin/journal/${journalId}/article-info`) .then(response => response.json()) .then(data => { - $sealEle.text($sealEle.text() + `This journal has ${data.n_articles} articles in DOAJ.`) + $sealEle.text($sealEle.text() + ` (This journal has ${data.n_articles} articles in DOAJ)`) }) }, From c8f3e734e6d27721184514241b253ef83d932401 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 8 Jul 2024 13:53:09 +0100 Subject: [PATCH 33/46] support admin_site_search redirection --- portality/static/js/formulaic.js | 7 ++++++- portality/view/admin.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 2068abe12..66d918887 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -2271,7 +2271,12 @@ var formulaic = { fetch(`/admin/journal/${journalId}/article-info`) .then(response => response.json()) .then(data => { - $sealEle.text($sealEle.text() + ` (This journal has ${data.n_articles} articles in DOAJ)`) + let articleText = `(This journal has ${data.n_articles} articles in DOAJ)` + if (data.n_articles > 0) { + const articlesUrl = `/admin/journal/${journalId}/article-info/admin-site-search` + articleText = `${articleText}` + } + $sealEle.html($sealEle.text() + ` ${articleText}`) }) }, diff --git a/portality/view/admin.py b/portality/view/admin.py index 72c4fb6a6..00d1edaec 100644 --- a/portality/view/admin.py +++ b/portality/view/admin.py @@ -331,6 +331,21 @@ def journal_article_info(journal_id): return {'n_articles': models.Article.count_by_issns(j.bibjson().issns(), in_doaj=True)} +@blueprint.route("/journal//article-info/admin-site-search", methods=["GET"]) +@login_required +def journal_article_info_admin_site_search(journal_id): + j = models.Journal.pull(journal_id) + if j is None: + abort(404) + + issns = j.bibjson().issns() + if not issns: + abort(404) + + target_url = '/admin/admin_site_search?source={"query":{"bool":{"must":[{"term":{"admin.in_doaj":true}},{"term":{"es_type.exact":"article"}},{"query_string":{"query":"%s","default_operator":"AND","default_field":"index.issn.exact"}}]}},"track_total_hits":true}' + return redirect(target_url % issns[0].replace('-', r'\\-')) + + @blueprint.route("/journal//continue", methods=["GET", "POST"]) @login_required @ssl_required From fd3f5eb972a52e5ffde9a036f145e7179c7b45cb Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Mon, 8 Jul 2024 15:59:34 +0100 Subject: [PATCH 34/46] update export text --- portality/static/js/doaj.fieldrender.edges.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/static/js/doaj.fieldrender.edges.js b/portality/static/js/doaj.fieldrender.edges.js index fe135729c..b667decf5 100644 --- a/portality/static/js/doaj.fieldrender.edges.js +++ b/portality/static/js/doaj.fieldrender.edges.js @@ -2905,7 +2905,7 @@ $.extend(true, doaj, { frag += '\
  • \ \ - Export RIS ' + Export Citation (RIS) ' if (this.widget){ frag += 'external-link icon' } else { From a644d0c861ed6241e11bba8fe74c0e7e00894fd8 Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Mon, 8 Jul 2024 16:36:42 +0100 Subject: [PATCH 35/46] fix language crosswalk mapping for ris --- portality/crosswalks/article_ris.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/crosswalks/article_ris.py b/portality/crosswalks/article_ris.py index 214720084..b9ee310ea 100644 --- a/portality/crosswalks/article_ris.py +++ b/portality/crosswalks/article_ris.py @@ -26,7 +26,7 @@ def extra_author_names(article) -> list: 'KW': '$.bibjson.keywords[*]', 'DOI': '$.bibjson.identifier[?(@.type == "doi")].id', 'SN': '$.bibjson.journal.issns[*]', - 'LA': '$.language.language', + 'LA': '$.bibjson.journal.language[*]', } From ad0c19c54b0ab230997c2158ca6c9e212453a8c6 Mon Sep 17 00:00:00 2001 From: philip Date: Tue, 30 Jul 2024 15:10:11 +0100 Subject: [PATCH 36/46] Reverted submodule edges to develop_edges1 --- portality/static/vendor/edges | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/static/vendor/edges b/portality/static/vendor/edges index 9639b871a..2ca0da93e 160000 --- a/portality/static/vendor/edges +++ b/portality/static/vendor/edges @@ -1 +1 @@ -Subproject commit 9639b871acb1f6590ee78e236f2c9333479c9fe8 +Subproject commit 2ca0da93e7bf345a7e529d1ed056c8dd0a328b6a From c12c2ab5366d4bf09c2e3a88d571dbf50fe9f27f Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 31 Jul 2024 10:09:38 +0100 Subject: [PATCH 37/46] change edges HEAD to same as develop 990f4220163a3e18880f0bdc3ad5c80d234d22dd --- portality/static/vendor/edges | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/static/vendor/edges b/portality/static/vendor/edges index 2ca0da93e..990f42201 160000 --- a/portality/static/vendor/edges +++ b/portality/static/vendor/edges @@ -1 +1 @@ -Subproject commit 2ca0da93e7bf345a7e529d1ed056c8dd0a328b6a +Subproject commit 990f4220163a3e18880f0bdc3ad5c80d234d22dd From 6485715ac1e50f6bda62571e44e44c910cdeda3e Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 28 Aug 2024 10:03:57 +0100 Subject: [PATCH 38/46] handle NoSuchObjectException (DOAJ/doajPM#3838) --- doajtest/unit/test_view_publisher.py | 21 +++++++++++++++++++++ portality/view/publisher.py | 8 ++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 doajtest/unit/test_view_publisher.py diff --git a/doajtest/unit/test_view_publisher.py b/doajtest/unit/test_view_publisher.py new file mode 100644 index 000000000..6ea8f1ec2 --- /dev/null +++ b/doajtest/unit/test_view_publisher.py @@ -0,0 +1,21 @@ +from doajtest import helpers +from doajtest.helpers import DoajTestCase +from portality import models, constants +from portality.util import url_for + + +class TestViewPublisher(DoajTestCase): + + def test_delete_application__no_such_object(self): + pwd = 'password' + un = 'publisher_a' + acc = models.Account.make_account(un + "@example.com", un, "Publisher " + un, [constants.ROLE_PUBLISHER]) + acc.set_password(pwd) + acc.save(blocking=True) + + with self.app_test.test_client() as t_client: + resp = helpers.login(t_client, acc.email, pwd) + assert resp.status_code == 200 + + resp = t_client.get(url_for("publisher.delete_application", application_id='no_such_id')) + assert resp.status_code == 400 diff --git a/portality/view/publisher.py b/portality/view/publisher.py index a3e328b28..fe92f542a 100644 --- a/portality/view/publisher.py +++ b/portality/view/publisher.py @@ -4,7 +4,8 @@ from portality.app_email import EmailException from portality import models, constants -from portality.bll.exceptions import AuthoriseException, ArticleMergeConflict, DuplicateArticleException, ArticleNotAcceptable +from portality.bll.exceptions import AuthoriseException, ArticleMergeConflict, DuplicateArticleException, \ + ArticleNotAcceptable, NoSuchObjectException from portality.decorators import ssl_required, restrict_to_role, write_required from portality.dao import ESMappingMissingError from portality.forms.application_forms import ApplicationFormFactory @@ -54,7 +55,10 @@ def delete_application(application_id): # otherwise delegate to the application service to sort this out appService = DOAJ.applicationService() - appService.delete_application(application_id, current_user._get_current_object()) + try: + appService.delete_application(application_id, current_user._get_current_object()) + except NoSuchObjectException: + abort(400) return redirect(url_for("publisher.deleted_thanks")) From e4f03ffc3fad1aadacc57ce2684fc946b534468e Mon Sep 17 00:00:00 2001 From: philip Date: Thu, 29 Aug 2024 13:25:25 +0100 Subject: [PATCH 39/46] 404 --- doajtest/unit/test_view_publisher.py | 2 +- portality/view/publisher.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doajtest/unit/test_view_publisher.py b/doajtest/unit/test_view_publisher.py index 6ea8f1ec2..fbdf77054 100644 --- a/doajtest/unit/test_view_publisher.py +++ b/doajtest/unit/test_view_publisher.py @@ -18,4 +18,4 @@ def test_delete_application__no_such_object(self): assert resp.status_code == 200 resp = t_client.get(url_for("publisher.delete_application", application_id='no_such_id')) - assert resp.status_code == 400 + assert resp.status_code == 404 diff --git a/portality/view/publisher.py b/portality/view/publisher.py index fe92f542a..902be3960 100644 --- a/portality/view/publisher.py +++ b/portality/view/publisher.py @@ -58,7 +58,7 @@ def delete_application(application_id): try: appService.delete_application(application_id, current_user._get_current_object()) except NoSuchObjectException: - abort(400) + abort(404) return redirect(url_for("publisher.deleted_thanks")) From 8956178f8703b7a4a4746f2a6d0c72110bb63470 Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Tue, 3 Sep 2024 15:47:46 +0100 Subject: [PATCH 40/46] put new dependency in alphabetical order --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3fca37d02..870d178cb 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ "feedparser==6.0.8", "itsdangerous==2.0.1", # fixme: unpinned dependency of flask, 2.1.0 is causing an import error 'json' "jinja2<3.1.0", # fixme: unpinned dependency of flask, import error on 'escape' + "jsonpath-ng~=1.6", "Flask~=2.1.2", "Flask-Cors==3.0.8", "Flask-DebugToolbar==0.13.1", @@ -64,8 +65,6 @@ 'gspread-dataframe~=3.3.1', 'gspread-formatting~=1.1.2', - 'jsonpath-ng~=1.6', - ] + (["setproctitle==1.1.10"] if "linux" in sys.platform else []), extras_require={ # prevent backtracking through all versions From b437581e5928dd0aebc1317a1a0b58240a5ac2c6 Mon Sep 17 00:00:00 2001 From: philip Date: Fri, 4 Oct 2024 12:04:35 +0100 Subject: [PATCH 41/46] convert DOI to DO --- doajtest/unit/test_crosswalks_article_ris.py | 2 +- portality/crosswalks/article_ris.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doajtest/unit/test_crosswalks_article_ris.py b/doajtest/unit/test_crosswalks_article_ris.py index 5d58655f4..90a4e3e6d 100644 --- a/doajtest/unit/test_crosswalks_article_ris.py +++ b/doajtest/unit/test_crosswalks_article_ris.py @@ -28,7 +28,7 @@ def test_article2ris(self): AB - abstract KW - word KW - key -DOI - 10.0000/SOME.IDENTIFIER +DO - 10.0000/SOME.IDENTIFIER ER - """.split() diff --git a/portality/crosswalks/article_ris.py b/portality/crosswalks/article_ris.py index 214720084..c15cfcffa 100644 --- a/portality/crosswalks/article_ris.py +++ b/portality/crosswalks/article_ris.py @@ -24,7 +24,7 @@ def extra_author_names(article) -> list: 'UR': '$.bibjson.link[*].url', 'AB': '$.bibjson.abstract', 'KW': '$.bibjson.keywords[*]', - 'DOI': '$.bibjson.identifier[?(@.type == "doi")].id', + 'DO': '$.bibjson.identifier[?(@.type == "doi")].id', 'SN': '$.bibjson.journal.issns[*]', 'LA': '$.language.language', } From bdac4cc640774d452e3f245151d26988723c02ee Mon Sep 17 00:00:00 2001 From: philip Date: Fri, 4 Oct 2024 12:50:48 +0100 Subject: [PATCH 42/46] update test cases for languages --- doajtest/unit/test_crosswalks_article_ris.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doajtest/unit/test_crosswalks_article_ris.py b/doajtest/unit/test_crosswalks_article_ris.py index 90a4e3e6d..760d819e1 100644 --- a/doajtest/unit/test_crosswalks_article_ris.py +++ b/doajtest/unit/test_crosswalks_article_ris.py @@ -29,6 +29,8 @@ def test_article2ris(self): KW - word KW - key DO - 10.0000/SOME.IDENTIFIER +LA - EN +LA - FR ER - """.split() From 270a655a7a2a30cb6292bad6262860ce7fef20f0 Mon Sep 17 00:00:00 2001 From: Steven Eardley Date: Tue, 5 Nov 2024 20:49:17 +0000 Subject: [PATCH 43/46] add CI badge to readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index bb04445ec..7b21709af 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ This repository provides the software which drives the DOAJ website and the DOAJ directory. +## CI Status + +**develop** [![CircleCI](https://dl.circleci.com/status-badge/img/gh/DOAJ/doaj/tree/develop.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/DOAJ/doaj/tree/develop) +**master** [![CircleCI](https://dl.circleci.com/status-badge/img/gh/DOAJ/doaj/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/DOAJ/doaj/tree/master) + ## Reporting issues Please feel free to use the issue tracker on https://github.com/DOAJ/doaj/issues for any bug From 74ee3a4c0b5f2488bfd0644bf14fbee0c89c521a Mon Sep 17 00:00:00 2001 From: Steven Eardley Date: Tue, 5 Nov 2024 20:51:54 +0000 Subject: [PATCH 44/46] A bit for formatting for README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b21709af..e8c8c4877 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ directory. ## CI Status -**develop** [![CircleCI](https://dl.circleci.com/status-badge/img/gh/DOAJ/doaj/tree/develop.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/DOAJ/doaj/tree/develop) -**master** [![CircleCI](https://dl.circleci.com/status-badge/img/gh/DOAJ/doaj/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/DOAJ/doaj/tree/master) +**develop**   [![CircleCI](https://dl.circleci.com/status-badge/img/gh/DOAJ/doaj/tree/develop.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/DOAJ/doaj/tree/develop) + +**master**   [![CircleCI](https://dl.circleci.com/status-badge/img/gh/DOAJ/doaj/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/DOAJ/doaj/tree/master) ## Reporting issues From 5ea6b51becb976ebd6ebedabd868af0c7f0afcb5 Mon Sep 17 00:00:00 2001 From: Steven Eardley Date: Tue, 12 Nov 2024 16:02:34 +0000 Subject: [PATCH 45/46] correct the edges version in develop --- portality/static/vendor/edges | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portality/static/vendor/edges b/portality/static/vendor/edges index 9639b871a..990f42201 160000 --- a/portality/static/vendor/edges +++ b/portality/static/vendor/edges @@ -1 +1 @@ -Subproject commit 9639b871acb1f6590ee78e236f2c9333479c9fe8 +Subproject commit 990f4220163a3e18880f0bdc3ad5c80d234d22dd From bc293fff5b8dc0b5da6c65e41087e5511dfbf0ff Mon Sep 17 00:00:00 2001 From: Steven Eardley Date: Tue, 12 Nov 2024 16:06:25 +0000 Subject: [PATCH 46/46] version bump for release --- portality/settings.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/portality/settings.py b/portality/settings.py index 213961706..dbeddc63e 100644 --- a/portality/settings.py +++ b/portality/settings.py @@ -9,7 +9,7 @@ # Application Version information # ~~->API:Feature~~ -DOAJ_VERSION = "7.0.1" +DOAJ_VERSION = "7.0.2" API_VERSION = "4.0.0" ###################################### diff --git a/setup.py b/setup.py index ddc27e972..bfebfeed0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='doaj', - version='7.0.1', + version='7.0.2', packages=find_packages(), install_requires=[ "awscli==1.20.50",