diff --git a/doajtest/fixtures/editor_groups.py b/doajtest/fixtures/editor_groups.py new file mode 100644 index 0000000000..52bf0ef6cc --- /dev/null +++ b/doajtest/fixtures/editor_groups.py @@ -0,0 +1,23 @@ +from portality import models + + +def create_editor_group_en(): + eg = models.EditorGroup() + eg.set_name("English") + eg.set_id('egid') + return eg + + +def create_editor_group_cn(): + eg = models.EditorGroup() + eg.set_name("Chinese") + eg.set_id('egid2') + return eg + + +def create_editor_group_jp(): + eg = models.EditorGroup() + eg.set_name("Japanese") + eg.set_id('egid3') + return eg + diff --git a/doajtest/fixtures/v2/journals.py b/doajtest/fixtures/v2/journals.py index 3b3d1123aa..58b57885c7 100644 --- a/doajtest/fixtures/v2/journals.py +++ b/doajtest/fixtures/v2/journals.py @@ -11,7 +11,7 @@ class JournalFixtureFactory(object): @staticmethod - def make_journal_source(in_doaj=False): + def make_journal_source(in_doaj=False) -> dict: template = deepcopy(JOURNAL_SOURCE) template['admin']['in_doaj'] = in_doaj return template @@ -37,23 +37,23 @@ def make_many_journal_sources(count=2, in_doaj=False) -> Iterable[dict]: return journal_sources @staticmethod - def make_journal_form(): + def make_journal_form() -> dict: return deepcopy(JOURNAL_FORM) @staticmethod - def make_journal_form_info(): + def make_journal_form_info() -> dict: return deepcopy(JOURNAL_FORM_EXPANDED) @staticmethod - def make_bulk_edit_data(): + def make_bulk_edit_data() -> dict: return deepcopy(JOURNAL_BULK_EDIT) @staticmethod - def csv_headers(): + def csv_headers() -> dict: return deepcopy(CSV_HEADERS) @staticmethod - def question_answers(): + def question_answers() -> dict: return deepcopy(JOURNAL_QUESTION_ANSWERS) diff --git a/doajtest/testdrive/todo_editor.py b/doajtest/testdrive/todo_editor.py index 7380b483ba..3cbb87210b 100644 --- a/doajtest/testdrive/todo_editor.py +++ b/doajtest/testdrive/todo_editor.py @@ -68,13 +68,13 @@ def build_application(title, lmu_diff, cd_diff, status, editor=None, editor_grou return ap -def build_applications(un, eg): +def build_applications(un: str, eg: models.Application): w = 7 * 24 * 60 * 60 apps = {} app = build_application(un + " Stalled Application", 6 * w, 7 * w, constants.APPLICATION_STATUS_IN_PROGRESS, - editor_group=eg.name) + editor_group=eg.id) app.save() apps["stalled"] = [{ "id": app.id, @@ -82,7 +82,7 @@ def build_applications(un, eg): }] app = build_application(un + " Old Application", 8 * w, 8 * w, constants.APPLICATION_STATUS_IN_PROGRESS, - editor_group=eg.name) + editor_group=eg.id) app.save() apps["old"] = [{ "id": app.id, @@ -90,7 +90,7 @@ def build_applications(un, eg): }] app = build_application(un + " Completed Application", 1 * w, 1 * w, constants.APPLICATION_STATUS_COMPLETED, - editor_group=eg.name) + editor_group=eg.id) app.save() apps["completed"] = [{ "id": app.id, @@ -98,7 +98,7 @@ def build_applications(un, eg): }] app = build_application(un + " Pending Application", 1 * w, 1 * w, constants.APPLICATION_STATUS_PENDING, - editor_group=eg.name) + editor_group=eg.id) app.remove_editor() app.save() apps["pending"] = [{ @@ -107,4 +107,3 @@ def build_applications(un, eg): }] return apps - diff --git a/doajtest/testdrive/todo_maned_editor_associate.py b/doajtest/testdrive/todo_maned_editor_associate.py index 1fa8ff936e..e5180ba97e 100644 --- a/doajtest/testdrive/todo_maned_editor_associate.py +++ b/doajtest/testdrive/todo_maned_editor_associate.py @@ -13,7 +13,8 @@ class TodoManedEditorAssociate(TestDrive): def setup(self) -> dict: un = self.create_random_str() pw = self.create_random_str() - admin = models.Account.make_account(un + "@example.com", un, "TodoManedEditorAssociate " + un, ["admin", "editor", constants.ROLE_ASSOCIATE_EDITOR]) + admin = models.Account.make_account(un + "@example.com", un, "TodoManedEditorAssociate " + un, + ["admin", "editor", constants.ROLE_ASSOCIATE_EDITOR]) admin.set_password(pw) admin.save() @@ -53,7 +54,6 @@ def setup(self) -> dict: eapps = build_editor_applications(un, eg2) mapps = build_maned_applications(un, eg1, owner.id, eg3) - return { "account": { "username": admin.id, @@ -96,13 +96,15 @@ def teardown(self, params) -> dict: return {"status": "success"} -def build_maned_applications(un, eg, owner, eponymous_group): +def build_maned_applications(un: str, + eg: models.EditorGroup, owner, + eponymous_group: models.EditorGroup): w = 7 * 24 * 60 * 60 apps = {} app = build_application(un + " Maned Stalled Application", 8 * w, 9 * w, constants.APPLICATION_STATUS_IN_PROGRESS, - editor_group=eg.name, owner=owner) + editor_group=eg.id, owner=owner) app.save() apps["stalled"] = [{ "id": app.id, @@ -110,7 +112,7 @@ def build_maned_applications(un, eg, owner, eponymous_group): }] app = build_application(un + " Maned Old Application", 10 * w, 10 * w, constants.APPLICATION_STATUS_IN_PROGRESS, - editor_group=eg.name, owner=owner) + editor_group=eg.id, owner=owner) app.save() apps["old"] = [{ "id": app.id, @@ -118,7 +120,7 @@ def build_maned_applications(un, eg, owner, eponymous_group): }] app = build_application(un + " Maned Ready Application", 1 * w, 1 * w, constants.APPLICATION_STATUS_READY, - editor_group=eg.name, owner=owner) + editor_group=eg.id, owner=owner) app.save() apps["ready"] = [{ "id": app.id, @@ -126,7 +128,7 @@ def build_maned_applications(un, eg, owner, eponymous_group): }] app = build_application(un + " Maned Completed Application", 2 * w, 2 * w, constants.APPLICATION_STATUS_COMPLETED, - editor_group=eg.name, owner=owner) + editor_group=eg.id, owner=owner) app.save() apps["completed"] = [{ "id": app.id, @@ -134,7 +136,7 @@ def build_maned_applications(un, eg, owner, eponymous_group): }] app = build_application(un + " Maned Pending Application", 2 * w, 2 * w, constants.APPLICATION_STATUS_PENDING, - editor_group=eg.name, owner=owner) + editor_group=eg.id, owner=owner) app.remove_editor() app.save() apps["pending"] = [{ @@ -144,7 +146,7 @@ def build_maned_applications(un, eg, owner, eponymous_group): app = build_application(un + " Maned Low Priority Pending Application", 1 * w, 1 * w, constants.APPLICATION_STATUS_PENDING, - editor_group=eponymous_group.name, owner=owner) + editor_group=eponymous_group.id, owner=owner) app.remove_editor() app.save() apps["low_priority_pending"] = [{ @@ -152,12 +154,14 @@ def build_maned_applications(un, eg, owner, eponymous_group): "title": un + " Maned Low Priority Pending Application" }] - lmur = build_application(un + " Last Month Maned Update Request", 5 * w, 5 * w, constants.APPLICATION_STATUS_UPDATE_REQUEST, - editor_group=eponymous_group.name, owner=owner, update_request=True) + lmur = build_application(un + " Last Month Maned Update Request", 5 * w, 5 * w, + constants.APPLICATION_STATUS_UPDATE_REQUEST, + editor_group=eponymous_group.id, owner=owner, update_request=True) lmur.save() - tmur = build_application(un + " This Month Maned Update Request", 0, 0, constants.APPLICATION_STATUS_UPDATE_REQUEST, - editor_group=eponymous_group.name, owner=owner, update_request=True) + tmur = build_application(un + " This Month Maned Update Request", 0, 0, + constants.APPLICATION_STATUS_UPDATE_REQUEST, + editor_group=eponymous_group.id, owner=owner, update_request=True) tmur.save() apps["update_request"] = [ @@ -174,7 +178,8 @@ def build_maned_applications(un, eg, owner, eponymous_group): return apps -def build_application(title, lmu_diff, cd_diff, status, editor=None, editor_group=None, owner=None, update_request=False): +def build_application(title, lmu_diff, cd_diff, status, editor=None, editor_group=None, owner=None, + update_request=False): source = ApplicationFixtureFactory.make_application_source() ap = models.Application(**source) ap.bibjson().title = title diff --git a/doajtest/unit/application_processors/test_editor_journal_review.py b/doajtest/unit/application_processors/test_editor_journal_review.py index 1291cb02c9..2f8401acd3 100644 --- a/doajtest/unit/application_processors/test_editor_journal_review.py +++ b/doajtest/unit/application_processors/test_editor_journal_review.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from doajtest.helpers import DoajTestCase from portality import models @@ -13,7 +15,7 @@ ##################################################################### @classmethod -def editor_group_pull(cls, field, value): +def editor_group_pull(cls, value): eg = models.EditorGroup() eg.set_editor("eddie") eg.set_associates(["associate", "assan"]) @@ -36,25 +38,13 @@ def mock_lookup_code(code): class TestEditorJournalReview(DoajTestCase): - def setUp(self): - super(TestEditorJournalReview, self).setUp() - - self.editor_group_pull = models.EditorGroup.pull_by_key - models.EditorGroup.pull_by_key = editor_group_pull - - self.old_lookup_code = lcc.lookup_code - lcc.lookup_code = mock_lookup_code - - def tearDown(self): - super(TestEditorJournalReview, self).tearDown() - models.EditorGroup.pull_by_key = self.editor_group_pull - lcc.lookup_code = self.old_lookup_code - ########################################################### # Tests on the publisher's re-journal form ########################################################### + @patch('portality.models.EditorGroup.pull', editor_group_pull) + @patch('portality.lcc.lookup_code', mock_lookup_code) def test_01_editor_review_success(self): """Give the editor's journal form a full workout""" diff --git a/doajtest/unit/application_processors/test_maned_journal_review.py b/doajtest/unit/application_processors/test_maned_journal_review.py index 07badcbabe..421865b254 100644 --- a/doajtest/unit/application_processors/test_maned_journal_review.py +++ b/doajtest/unit/application_processors/test_maned_journal_review.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from doajtest.helpers import DoajTestCase from portality import models @@ -17,7 +19,7 @@ ##################################################################### @classmethod -def editor_group_pull(cls, field, value): +def editor_group_pull(cls, value): eg = models.EditorGroup() eg.set_editor("eddie") eg.set_associates(["associate", "assan"]) @@ -32,20 +34,8 @@ def mock_lookup_code(code): class TestManEdJournalReview(DoajTestCase): - def setUp(self): - super(TestManEdJournalReview, self).setUp() - - self.editor_group_pull = models.EditorGroup.pull_by_key - models.EditorGroup.pull_by_key = editor_group_pull - - self.old_lookup_code = lcc.lookup_code - lcc.lookup_code = mock_lookup_code - - def tearDown(self): - super(TestManEdJournalReview, self).tearDown() - models.EditorGroup.pull_by_key = self.editor_group_pull - lcc.lookup_code = self.old_lookup_code - + @patch('portality.models.EditorGroup.pull', editor_group_pull) + @patch('portality.lcc.lookup_code', mock_lookup_code) def test_01_maned_review_success(self): """Give the Managing Editor's journal form a full workout""" diff --git a/doajtest/unit/event_consumers/test_application_assed_inprogress_notify.py b/doajtest/unit/event_consumers/test_application_assed_inprogress_notify.py index 75127191da..4b2a44792b 100644 --- a/doajtest/unit/event_consumers/test_application_assed_inprogress_notify.py +++ b/doajtest/unit/event_consumers/test_application_assed_inprogress_notify.py @@ -42,6 +42,7 @@ def test_consume_success(self): acc.save() eg = models.EditorGroup() + eg.set_id(app.editor_group) eg.set_name(app.editor_group) eg.set_maned(acc.id) eg.save(blocking=True) diff --git a/doajtest/unit/event_consumers/test_application_editor_completed_notify.py b/doajtest/unit/event_consumers/test_application_editor_completed_notify.py index 0748b4a9d4..ed1d2add9a 100644 --- a/doajtest/unit/event_consumers/test_application_editor_completed_notify.py +++ b/doajtest/unit/event_consumers/test_application_editor_completed_notify.py @@ -41,7 +41,8 @@ def test_consume_success(self): acc.save() eg = models.EditorGroup() - eg.set_name(app.editor_group) + eg.set_name("test group") + eg.set_id(app.editor_group) eg.set_editor(acc.id) eg.save(blocking=True) diff --git a/doajtest/unit/event_consumers/test_application_editor_group_assigned_notify.py b/doajtest/unit/event_consumers/test_application_editor_group_assigned_notify.py index 0381c529c4..67b5bb1e35 100644 --- a/doajtest/unit/event_consumers/test_application_editor_group_assigned_notify.py +++ b/doajtest/unit/event_consumers/test_application_editor_group_assigned_notify.py @@ -37,7 +37,8 @@ def test_consume_success(self): acc.save() eg = models.EditorGroup() - eg.set_name(app.editor_group) + eg.set_name("test group") + eg.set_id(app.editor_group) eg.set_editor("editor") eg.save(blocking=True) @@ -67,7 +68,8 @@ def test_consume_fail(self): # app.save(blocking=True) eg = models.EditorGroup() - eg.set_name(app.editor_group) + eg.set_name('test group') + eg.set_id(app.editor_group) eg.save(blocking=True) event = models.Event(constants.EVENT_APPLICATION_EDITOR_GROUP_ASSIGNED, context={"application": app.data}) diff --git a/doajtest/unit/event_consumers/test_application_editor_inprogress_notify.py b/doajtest/unit/event_consumers/test_application_editor_inprogress_notify.py index 3f1ed58bcc..e71a92011c 100644 --- a/doajtest/unit/event_consumers/test_application_editor_inprogress_notify.py +++ b/doajtest/unit/event_consumers/test_application_editor_inprogress_notify.py @@ -45,7 +45,8 @@ def test_consume_success(self): acc.save() eg = models.EditorGroup() - eg.set_name(app.editor_group) + eg.set_name('test group') + eg.set_id(app.editor_group) eg.set_editor(acc.id) eg.save(blocking=True) diff --git a/doajtest/unit/event_consumers/test_application_maned_ready_notify.py b/doajtest/unit/event_consumers/test_application_maned_ready_notify.py index bdb07df500..2207eaeb5c 100644 --- a/doajtest/unit/event_consumers/test_application_maned_ready_notify.py +++ b/doajtest/unit/event_consumers/test_application_maned_ready_notify.py @@ -41,7 +41,8 @@ def test_consume_success(self): acc.save() eg = models.EditorGroup() - eg.set_name(app.editor_group) + eg.set_name("test group") + eg.set_id(app.editor_group) eg.set_maned(acc.id) eg.save(blocking=True) diff --git a/doajtest/unit/event_consumers/test_journal_discontinuing_soon_notify.py b/doajtest/unit/event_consumers/test_journal_discontinuing_soon_notify.py index 9da78fd1c0..5ce05d7030 100644 --- a/doajtest/unit/event_consumers/test_journal_discontinuing_soon_notify.py +++ b/doajtest/unit/event_consumers/test_journal_discontinuing_soon_notify.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from portality import models from portality import constants from portality.bll import exceptions @@ -14,7 +16,7 @@ def pull_application(cls, id): return app @classmethod -def pull_by_key(cls, key, value): +def pull_editor_group(cls, value): ed = models.EditorGroup() acc = models.Account() acc.set_id('testuser') @@ -26,17 +28,6 @@ def pull_by_key(cls, key, value): return ed class TestJournalDiscontinuingSoonNotify(DoajTestCase): - def setUp(self): - super(TestJournalDiscontinuingSoonNotify, self).setUp() - self.pull_application = models.Application.pull - models.Application.pull = pull_application - self.pull_by_key = models.EditorGroup.pull_by_key - models.EditorGroup.pull_by_key = pull_by_key - - def tearDown(self): - super(TestJournalDiscontinuingSoonNotify, self).tearDown() - models.Application.pull = self.pull_application - models.EditorGroup.pull_by_key = self.pull_by_key def test_should_consume(self): @@ -52,6 +43,8 @@ def test_should_consume(self): event = models.Event(constants.EVENT_JOURNAL_DISCONTINUING_SOON, context = {"journal": {"1234"}, "discontinue_date": "2002-22-02"}) assert JournalDiscontinuingSoonNotify.should_consume(event) + @patch('portality.models.EditorGroup.pull', pull_editor_group) + @patch('portality.models.Application.pull', pull_application) def test_consume_success(self): self._make_and_push_test_context("/") diff --git a/doajtest/unit/event_consumers/test_journal_editor_group_assigned_notify.py b/doajtest/unit/event_consumers/test_journal_editor_group_assigned_notify.py index e55d0ea082..0887255ebf 100644 --- a/doajtest/unit/event_consumers/test_journal_editor_group_assigned_notify.py +++ b/doajtest/unit/event_consumers/test_journal_editor_group_assigned_notify.py @@ -37,7 +37,8 @@ def test_consume_success(self): acc.save() eg = models.EditorGroup() - eg.set_name(app.editor_group) + eg.set_name("test group") + eg.set_id(app.editor_group) eg.set_editor("editor") eg.save(blocking=True) @@ -67,7 +68,8 @@ def test_consume_fail(self): # app.save(blocking=True) eg = models.EditorGroup() - eg.set_name(app.editor_group) + eg.set_name("test group") + eg.set_id(app.editor_group) eg.save(blocking=True) event = models.Event(constants.EVENT_JOURNAL_EDITOR_GROUP_ASSIGNED, context={"journal": app.data}) diff --git a/doajtest/unit/test_admin_editor_groups.py b/doajtest/unit/test_admin_editor_groups.py index b5fb728a27..420c04af41 100644 --- a/doajtest/unit/test_admin_editor_groups.py +++ b/doajtest/unit/test_admin_editor_groups.py @@ -38,5 +38,5 @@ def test_editor_group_creation_and_update(self): # give some time for the new record to be indexed time.sleep(1) updated_group = EditorGroup.pull(editor_group_id) - self.assertEquals(updated_group.name, "Test Group") - self.assertNotEquals(updated_group.name, "New Test Group") + self.assertEquals(updated_group.name, "New Test Group") + self.assertNotEquals(updated_group.name, "Test Group") diff --git a/doajtest/unit/test_bll_todo_top_todo_editor.py b/doajtest/unit/test_bll_todo_top_todo_editor.py index f5f6d4a2e7..1b8f53b6e2 100644 --- a/doajtest/unit/test_bll_todo_top_todo_editor.py +++ b/doajtest/unit/test_bll_todo_top_todo_editor.py @@ -56,6 +56,7 @@ def test_top_todo(self, name, kwargs): w = 7 * 24 * 60 * 60 account = None + editor_group_id = None if account_arg == "admin": asource = AccountFixtureFactory.make_managing_editor_source() account = models.Account(**asource) @@ -65,6 +66,7 @@ def test_top_todo(self, name, kwargs): eg_source = EditorGroupFixtureFactory.make_editor_group_source(editor=account.id) eg = models.EditorGroup(**eg_source) eg.save(blocking=True) + editor_group_id = eg.id elif account_arg == "assed": asource = AccountFixtureFactory.make_assed1_source() account = models.Account(**asource) @@ -76,19 +78,23 @@ def test_top_todo(self, name, kwargs): ############################################################ # an application created more than 8 weeks ago - self.build_application("editor_follow_up_old", 2 * w, 9 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps) + self.build_application("editor_follow_up_old", 2 * w, 9 * w, constants.APPLICATION_STATUS_IN_PROGRESS, + apps, editor_group_id=editor_group_id) # an application that was last updated over 6 weeks ago - self.build_application("editor_stalled", 7 * w, 7 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps) + self.build_application("editor_stalled", 7 * w, 7 * w, constants.APPLICATION_STATUS_IN_PROGRESS, + apps, editor_group_id=editor_group_id) # an application that was modifed recently into the completed status - self.build_application("editor_completed", 2 * w, 2 * w, constants.APPLICATION_STATUS_COMPLETED, apps) + self.build_application("editor_completed", 2 * w, 2 * w, constants.APPLICATION_STATUS_COMPLETED, + apps, editor_group_id=editor_group_id) # an application that is pending without an editor assigned def assign_pending(ap): ap.remove_editor() - self.build_application("editor_assign_pending", 2 * w, 2 * w, constants.APPLICATION_STATUS_PENDING, apps, additional_fn=assign_pending) + self.build_application("editor_assign_pending", 2 * w, 2 * w, constants.APPLICATION_STATUS_PENDING, + apps, additional_fn=assign_pending, editor_group_id=editor_group_id) wait_until_no_es_incomplete_tasks() models.Application.refresh() @@ -127,7 +133,8 @@ def assign_pending(ap): else: # the todo item is not positioned at all assert len(positions.get(k, [])) == 0 - def build_application(self, id, lmu_diff, cd_diff, status, app_registry, additional_fn=None): + def build_application(self, id, lmu_diff, cd_diff, status, app_registry, + additional_fn=None, editor_group_id=None): source = ApplicationFixtureFactory.make_application_source() ap = models.Application(**source) ap.set_id(id) @@ -139,5 +146,8 @@ def build_application(self, id, lmu_diff, cd_diff, status, app_registry, additio if additional_fn is not None: additional_fn(ap) + if editor_group_id is not None: + ap.set_editor_group(editor_group_id) + ap.save() app_registry.append(ap) \ No newline at end of file diff --git a/doajtest/unit/test_bll_todo_top_todo_maned.py b/doajtest/unit/test_bll_todo_top_todo_maned.py index 5322a8c9ae..a5d8989b47 100644 --- a/doajtest/unit/test_bll_todo_top_todo_maned.py +++ b/doajtest/unit/test_bll_todo_top_todo_maned.py @@ -12,11 +12,11 @@ def load_cases(): return load_parameter_sets(rel2abs(__file__, "..", "matrices", "bll_todo_maned"), "top_todo_maned", "test_id", - {"test_id" : []}) + {"test_id": []}) EXCEPTIONS = { - "ArgumentException" : exceptions.ArgumentException + "ArgumentException": exceptions.ArgumentException } @@ -45,7 +45,7 @@ def test_top_todo(self, name, kwargs): ] category_args = { - cat : ( + cat: ( int(kwargs.get(cat)), int(kwargs.get(cat + "_order") if kwargs.get(cat + "_order") != "" else -1) ) for cat in categories @@ -58,12 +58,14 @@ def test_top_todo(self, name, kwargs): w = 7 * 24 * 60 * 60 account = None + editor_group_id = None if account_arg == "admin": asource = AccountFixtureFactory.make_managing_editor_source() account = models.Account(**asource) eg_source = EditorGroupFixtureFactory.make_editor_group_source(maned=account.id) eg = models.EditorGroup(**eg_source) eg.save(blocking=True) + editor_group_id = eg.id elif account_arg == "editor": asource = AccountFixtureFactory.make_editor_source() account = models.Account(**asource) @@ -78,27 +80,31 @@ def test_top_todo(self, name, kwargs): ############################################################ # an application stalled for more than 8 weeks (todo_maned_stalled) - self.build_application("maned_stalled", 9 * w, 9 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps) + self.build_application("maned_stalled", 9 * w, 9 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps, + editor_group_id=editor_group_id) # an application that was created over 10 weeks ago (but updated recently) (todo_maned_follow_up_old) - self.build_application("maned_follow_up_old", 2 * w, 11 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps) + self.build_application("maned_follow_up_old", 2 * w, 11 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps, + editor_group_id=editor_group_id) # an application that was modifed recently into the ready status (todo_maned_ready) - self.build_application("maned_ready", 2 * w, 2 * w, constants.APPLICATION_STATUS_READY, apps) + self.build_application("maned_ready", 2 * w, 2 * w, constants.APPLICATION_STATUS_READY, apps, + editor_group_id=editor_group_id) # an application that was modifed recently into the ready status (todo_maned_completed) - self.build_application("maned_completed", 3 * w, 3 * w, constants.APPLICATION_STATUS_COMPLETED, apps) + self.build_application("maned_completed", 3 * w, 3 * w, constants.APPLICATION_STATUS_COMPLETED, apps, + editor_group_id=editor_group_id) # an application that was modifed recently into the ready status (todo_maned_assign_pending) def assign_pending(ap): ap.remove_editor() self.build_application("maned_assign_pending", 4 * w, 4 * w, constants.APPLICATION_STATUS_PENDING, apps, - assign_pending) + assign_pending, editor_group_id=editor_group_id) # an update request self.build_application("maned_update_request", 5 * w, 5 * w, constants.APPLICATION_STATUS_UPDATE_REQUEST, apps, - update_request=True) + update_request=True, editor_group_id=editor_group_id) # Applications that should never be reported ############################################ @@ -109,7 +115,8 @@ def assign_pending(ap): # maned_ready # maned_completed # maned_assign_pending - self.build_application("not_stalled__not_old", 2 * w, 2 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps) + self.build_application("not_stalled__not_old", 2 * w, 2 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps, + editor_group_id=editor_group_id) # an application that is old but rejected # counter to maned_stalled @@ -117,7 +124,8 @@ def assign_pending(ap): # maned_ready # maned_completed # maned_assign_pending - self.build_application("old_rejected", 11 * w, 11 * w, constants.APPLICATION_STATUS_REJECTED, apps) + self.build_application("old_rejected", 11 * w, 11 * w, constants.APPLICATION_STATUS_REJECTED, apps, + editor_group_id=editor_group_id) # an application that was created/modified over 10 weeks ago and is accepted # counter to maned_stalled @@ -125,7 +133,8 @@ def assign_pending(ap): # maned_ready # maned_completed # maned_assign_pending - self.build_application("old_accepted", 12 * w, 12 * w, constants.APPLICATION_STATUS_ACCEPTED, apps) + self.build_application("old_accepted", 12 * w, 12 * w, constants.APPLICATION_STATUS_ACCEPTED, apps, + editor_group_id=editor_group_id) # an application that was recently completed (<2wk) # counter to maned_stalled @@ -133,7 +142,8 @@ def assign_pending(ap): # maned_ready # maned_completed # maned_assign_pending - self.build_application("old_accepted", 1 * w, 1 * w, constants.APPLICATION_STATUS_COMPLETED, apps) + self.build_application("old_accepted", 1 * w, 1 * w, constants.APPLICATION_STATUS_COMPLETED, apps, + editor_group_id=editor_group_id) # pending application with no assed younger than 2 weeks # counter to maned_stalled @@ -142,11 +152,12 @@ def assign_pending(ap): # maned_completed # maned_assign_pending self.build_application("not_assign_pending", 1 * w, 1 * w, constants.APPLICATION_STATUS_PENDING, apps, - assign_pending) + assign_pending, editor_group_id=editor_group_id) # pending application with assed assigned # counter to maned_assign_pending - self.build_application("pending_assed_assigned", 3 * w, 3 * w, constants.APPLICATION_STATUS_PENDING, apps) + self.build_application("pending_assed_assigned", 3 * w, 3 * w, constants.APPLICATION_STATUS_PENDING, apps, + editor_group_id=editor_group_id) # pending application with no editor group assigned # counter to maned_assign_pending @@ -154,17 +165,18 @@ def noeditorgroup(ap): ap.remove_editor_group() self.build_application("pending_assed_assigned", 3 * w, 3 * w, constants.APPLICATION_STATUS_PENDING, apps, - noeditorgroup) + noeditorgroup, editor_group_id=editor_group_id) # application with no assed, but not pending # counter to maned_assign_pending - self.build_application("no_assed", 3 * w, 3 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps, assign_pending) + self.build_application("no_assed", 3 * w, 3 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps, + assign_pending, editor_group_id=editor_group_id) wait_until_no_es_incomplete_tasks() models.Application.refresh() # size = int(size_arg) - size=25 + size = 25 raises = None if raises_arg: @@ -194,10 +206,11 @@ def noeditorgroup(ap): assert actions.get(k, 0) == v[0] if v[1] > -1: assert v[1] in positions.get(k, []) - else: # the todo item is not positioned at all + else: # the todo item is not positioned at all assert len(positions.get(k, [])) == 0 - def build_application(self, id, lmu_diff, cd_diff, status, app_registry, additional_fn=None, update_request=False): + def build_application(self, id, lmu_diff, cd_diff, status, app_registry, additional_fn=None, + update_request=False, editor_group_id=None): source = ApplicationFixtureFactory.make_application_source() ap = models.Application(**source) ap.set_id(id) @@ -215,5 +228,8 @@ def build_application(self, id, lmu_diff, cd_diff, status, app_registry, additio if additional_fn is not None: additional_fn(ap) + if editor_group_id is not None: + ap.set_editor_group(editor_group_id) + ap.save() - app_registry.append(ap) \ No newline at end of file + app_registry.append(ap) diff --git a/doajtest/unit/test_fc_editor_app_review.py b/doajtest/unit/test_fc_editor_app_review.py index 5f7c90e992..ec30e1a0c0 100644 --- a/doajtest/unit/test_fc_editor_app_review.py +++ b/doajtest/unit/test_fc_editor_app_review.py @@ -1,5 +1,6 @@ import time from copy import deepcopy +from unittest.mock import patch from werkzeug.datastructures import MultiDict @@ -18,7 +19,7 @@ ##################################################################### @classmethod -def editor_group_pull(cls, field, value): +def editor_group_pull(cls, value): eg = models.EditorGroup() eg.set_editor("eddie") eg.set_associates(["associate", "assan"]) @@ -58,30 +59,13 @@ def make_application_form(): class TestEditorAppReview(DoajTestCase): - def setUp(self): - super(TestEditorAppReview, self).setUp() - - self.editor_group_pull = models.EditorGroup.pull_by_key - models.EditorGroup.pull_by_key = editor_group_pull - - self.old_lcc_choices = lcc.lcc_choices - lcc.lcc_choices = mock_lcc_choices - - self.old_lookup_code = lcc.lookup_code - lcc.lookup_code = mock_lookup_code - - def tearDown(self): - super(TestEditorAppReview, self).tearDown() - - models.EditorGroup.pull_by_key = self.editor_group_pull - lcc.lcc_choices = self.old_lcc_choices - - lcc.lookup_code = self.old_lookup_code - ########################################################### # Tests on the editor's application form ########################################################### + @patch('portality.models.EditorGroup.pull', editor_group_pull) + @patch('portality.lcc.lcc_choices', mock_lcc_choices) + @patch('portality.lcc.lookup_code', mock_lookup_code) def test_01_editor_review_success(self): """Give the editor's application form a full workout""" acc = models.Account() @@ -167,6 +151,9 @@ def test_01_editor_review_success(self): ctx.pop() + @patch('portality.models.EditorGroup.pull', editor_group_pull) + @patch('portality.lcc.lcc_choices', mock_lcc_choices) + @patch('portality.lcc.lookup_code', mock_lookup_code) def test_02_classification_required(self): # Check we can mark an application 'ready' with a subject classification present in_progress_application = models.Suggestion(**ApplicationFixtureFactory.make_update_request_source()) diff --git a/doajtest/unit/test_fc_maned_app_review.py b/doajtest/unit/test_fc_maned_app_review.py index dae4d8a1a6..284bf0bfa8 100644 --- a/doajtest/unit/test_fc_maned_app_review.py +++ b/doajtest/unit/test_fc_maned_app_review.py @@ -1,5 +1,6 @@ import time from copy import deepcopy +from unittest.mock import patch from werkzeug.datastructures import MultiDict @@ -17,7 +18,7 @@ ##################################################################### @classmethod -def editor_group_pull(cls, field, value): +def editor_group_pull(cls, value): eg = models.EditorGroup() eg.set_editor("eddie") eg.set_associates(["associate", "assan"]) @@ -58,26 +59,9 @@ def make_application_form(): class TestManEdAppReview(DoajTestCase): - def setUp(self): - super(TestManEdAppReview, self).setUp() - - self.editor_group_pull = models.EditorGroup.pull_by_key - models.EditorGroup.pull_by_key = editor_group_pull - - self.old_lcc_choices = lcc.lcc_choices - lcc.lcc_choices = mock_lcc_choices - - self.old_lookup_code = lcc.lookup_code - lcc.lookup_code = mock_lookup_code - - def tearDown(self): - super(TestManEdAppReview, self).tearDown() - - models.EditorGroup.pull_by_key = self.editor_group_pull - lcc.lcc_choices = self.old_lcc_choices - - lcc.lookup_code = self.old_lookup_code - + @patch('portality.models.EditorGroup.pull', editor_group_pull) + @patch('portality.lcc.lcc_choices', mock_lcc_choices) + @patch('portality.lcc.lookup_code', mock_lookup_code) def test_01_maned_review_success(self): """Give the editor's application form a full workout""" acc = models.Account() @@ -203,6 +187,9 @@ def test_02_update_request(self): ctx.pop() + @patch('portality.models.EditorGroup.pull', editor_group_pull) + @patch('portality.lcc.lcc_choices', mock_lcc_choices) + @patch('portality.lcc.lookup_code', mock_lookup_code) def test_03_classification_required(self): acc = models.Account() acc.set_id("steve") diff --git a/doajtest/unit/test_models.py b/doajtest/unit/test_models.py index af07a92859..602363ed47 100644 --- a/doajtest/unit/test_models.py +++ b/doajtest/unit/test_models.py @@ -399,10 +399,10 @@ def test_06_article_deletes(self): # now hit the key methods involved in article deletes query = { - "query" : { - "bool" : { - "must" : [ - {"term" : {"bibjson.title.exact" : "Test Article 0"}} + "query": { + "bool": { + "must": [ + {"term": {"bibjson.title.exact": "Test Article 0"}} ] } } @@ -457,10 +457,10 @@ def test_07_journal_deletes(self): # now hit the key methods involved in journal deletes query = { - "query" : { - "bool" : { - "must" : [ - {"term" : {"bibjson.title.exact" : "Test Journal 1"}} + "query": { + "bool": { + "must": [ + {"term": {"bibjson.title.exact": "Test Journal 1"}} ] } } @@ -479,14 +479,14 @@ def test_07_journal_deletes(self): assert len(self.list_today_article_history_files()) == 1 assert len(models.Journal.all()) == 4 - assert len(self.list_today_journal_history_files()) == 6 # Because all journals are snapshot at create time + assert len(self.list_today_journal_history_files()) == 6 # Because all journals are snapshot at create time @patch_history_dir('JOURNAL_HISTORY_DIR') def test_08_iterate(self): for jsrc in JournalFixtureFactory.make_many_journal_sources(count=99, in_doaj=True): j = models.Journal(**jsrc) j.save() - time.sleep(2) # index all the journals + time.sleep(2) # index all the journals journal_ids = [] theqgen = models.JournalQuery() for j in models.Journal.iterate(q=theqgen.all_in_doaj(), page_size=10): @@ -564,9 +564,11 @@ def test_12_archiving_policy(self): assert b.preservation_summary == ["LOCKSS", "CLOCKSS", "Somewhere else", ["A national library", "Trinity"]] b.add_archiving_policy("SAFE") - assert b.preservation_summary == ["LOCKSS", "CLOCKSS", "Somewhere else", "SAFE", ["A national library", "Trinity"]] + assert b.preservation_summary == ["LOCKSS", "CLOCKSS", "Somewhere else", "SAFE", + ["A national library", "Trinity"]] - assert b.flattened_archiving_policies == ['LOCKSS', 'CLOCKSS', "Somewhere else", 'SAFE', 'A national library: Trinity'], b.flattened_archiving_policies + assert b.flattened_archiving_policies == ['LOCKSS', 'CLOCKSS', "Somewhere else", 'SAFE', + 'A national library: Trinity'], b.flattened_archiving_policies def test_13_generic_bibjson(self): source = BibJSONFixtureFactory.generic_bibjson() @@ -589,7 +591,7 @@ def test_13_generic_bibjson(self): gbj.title = "Updated Title" gbj.add_identifier("doi", "10.1234/7") gbj.add_keyword("test") - gbj.add_keyword("ONE") # make sure keywords are stored in lowercase + gbj.add_keyword("ONE") # make sure keywords are stored in lowercase keyword = None # make sure None keyword doesn't cause error gbj.add_keyword(keyword) gbj.add_url("http://test", "test") @@ -600,11 +602,11 @@ def test_13_generic_bibjson(self): assert gbj.get_one_identifier("doi") == "10.1234/7" assert gbj.keywords == ["word", "key", "test", "one"] assert gbj.get_single_url("test") == "http://test" - assert gbj.subjects()[2] == {"scheme" : "TEST", "term" : "first", "code" : "one"} + assert gbj.subjects()[2] == {"scheme": "TEST", "term": "first", "code": "one"} gbj.remove_identifiers("doi") - gbj.set_keywords("TwO") # make sure keywords are stored in lowercase - gbj.set_subjects({"scheme" : "TEST", "term" : "first", "code" : "one"}) + gbj.set_keywords("TwO") # make sure keywords are stored in lowercase + gbj.set_subjects({"scheme": "TEST", "term": "first", "code": "one"}) assert gbj.keywords == ["two"] keywords = [] @@ -665,7 +667,8 @@ def test_14_journal_like_bibjson(self): assert bj.plagiarism_detection is True assert bj.plagiarism_url == "http://plagiarism.screening" assert bj.preservation is not None - assert bj.preservation_summary == ["LOCKSS", "CLOCKSS", "A safe place", ["A national library", "Trinity"], ["A national library", "Imperial"]] + assert bj.preservation_summary == ["LOCKSS", "CLOCKSS", "A safe place", ["A national library", "Trinity"], + ["A national library", "Imperial"]] assert bj.preservation_url == "http://digital.archiving.policy" assert bj.publisher_name == "The Publisher" assert bj.publisher_country == "US" @@ -787,7 +790,8 @@ def test_14_journal_like_bibjson(self): assert len(bj.apc) == 2 assert bj.deposit_policy == ["Never", "OK"] assert bj.pid_scheme == ["Handle", "PURL"] - assert bj.preservation_summary == ["LOCKSS", "MOUNTAIN", ["A national library", "UCL"], ["A national library", "LSE"]] + assert bj.preservation_summary == ["LOCKSS", "MOUNTAIN", ["A national library", "UCL"], + ["A national library", "LSE"]] # special methods assert bj.issns() == ["1111-111X", "0000-000X"], bj.issns() @@ -820,8 +824,8 @@ def test_14_journal_like_bibjson(self): assert bj.pid_scheme == ["ARK", "PURL"] assert bj.subject == bj.subjects() - bj.set_subjects({"scheme" : "whatever", "term" : "also whatever"}) - assert bj.subject == [{"scheme" : "whatever", "term" : "also whatever"}] + bj.set_subjects({"scheme": "whatever", "term": "also whatever"}) + assert bj.subject == [{"scheme": "whatever", "term": "also whatever"}] bj.remove_subjects() assert len(bj.subject) == 0 @@ -899,7 +903,6 @@ def test_14_journal_like_bibjson(self): assert bj.replaces == [] assert bj.subject == [] - def test_15_continuations(self): journal = models.Journal() bj = journal.bibjson() @@ -990,7 +993,8 @@ def test_16_article_bibjson(self): assert bj.publisher == "IEEE" assert bj.author[0].get("name") == "Test" assert bj.author[0].get("affiliation") == "University of Life" - assert bj.author[0].get("orcid_id") == "https://orcid.org/0000-0001-1234-1234", "received: {}".format(bj.author[0].get("orcid_id")) + assert bj.author[0].get("orcid_id") == "https://orcid.org/0000-0001-1234-1234", "received: {}".format( + bj.author[0].get("orcid_id")) bj.year = "2000" bj.month = "5" @@ -1022,7 +1026,8 @@ def test_16_article_bibjson(self): assert bj.publisher == "Elsevier" assert bj.author[1].get("name") == "Testing" assert bj.author[1].get("affiliation") == "School of Hard Knocks" - assert bj.author[1].get("orcid_id") == "0000-0001-4321-4321", "received: {}".format(bj.author[1].get("orcid_id")) + assert bj.author[1].get("orcid_id") == "0000-0001-4321-4321", "received: {}".format( + bj.author[1].get("orcid_id")) del bj.year del bj.month @@ -1204,6 +1209,25 @@ def test_22_autocomplete(self): res = models.Journal.advanced_autocomplete("index.publisher_ac", "bibjson.publisher.name", "BioMed C") assert len(res) == 1, "autocomplete for 'BioMed C': found {}, expected 2".format(len(res)) + def test_autocomplete_pair(self): + eg = models.EditorGroup() + eg.set_id("id-1") + eg.set_name("eg name 1") + eg.save() + + eg = models.EditorGroup() + eg.set_id("id-2") + eg.set_name("eg name 2") + eg.save(blocking=True) + + res = models.EditorGroup.autocomplete_pair('name', 'eg', 'id', 'name') + assert len(res) == 2 + assert {r['id']: r['text'] for r in res} == {'id-1': 'eg name 1', 'id-2': 'eg name 2'} + + def test_autocomplete_pair__not_found(self): + res = models.EditorGroup.autocomplete_pair('name', 'aksjdlaksjdlkasjdl', 'id', 'name') + assert len(res) == 0 + def test_23_provenance(self): """Read and write properties into the provenance model""" p = models.Provenance() @@ -1231,7 +1255,7 @@ def test_24_save_valid_seamless_or_dataobj(self): s.data["junk"] = "in here" with self.assertRaises(seamless.SeamlessException): s.save() - assert s.id is not None # ID is necessary for duplication check + assert s.id is not None # ID is necessary for duplication check p = models.Provenance() p.type = "suggestion" @@ -1269,7 +1293,7 @@ def test_25_make_provenance(self): eg2 = models.EditorGroup() eg2.set_id("editor") - eg2.set_name("Editor") # note: REQUIRED so that the mapping includes .name, which is needed to find groups_by + eg2.set_name("Editor") # note: REQUIRED so that the mapping includes .name, which is needed to find groups_by eg2.set_editor(acc.id) eg2.save() @@ -1303,7 +1327,7 @@ def test_26_background_job(self): source = BackgroundFixtureFactory.example() source["params"]["ids"] = ["1", "2", "3"] source["params"]["type"] = "suggestion" - source["reference"]["query"] = json.dumps({"query" : {"match_all" : {}}}) + source["reference"]["query"] = json.dumps({"query": {"match_all": {}}}) bj = models.BackgroundJob(**source) bj.save() @@ -1316,8 +1340,8 @@ def test_26a_background_job_active(self): bj = models.BackgroundJob(**source) bj.save(blocking=True) - assert len(models.BackgroundJob.active(source["action"])) == 1, "expected 1 active, got {x}".format(x=len(models.BackgroundJob.active(source["action"]))) - + assert len(models.BackgroundJob.active(source["action"])) == 1, "expected 1 active, got {x}".format( + x=len(models.BackgroundJob.active(source["action"]))) def test_27_article_journal_sync(self): j = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=True)) @@ -1394,25 +1418,29 @@ def test_30_article_stats(self): # make a bunch of articles variably in doaj/not in doaj, for/not for the issn we'll search for i in range(1, 3): article = models.Article( - **ArticleFixtureFactory.make_article_source(eissn="1111-1111", pissn="1111-1111", with_id=False, in_doaj=True) + **ArticleFixtureFactory.make_article_source(eissn="1111-1111", pissn="1111-1111", with_id=False, + in_doaj=True) ) article.set_created("2019-01-0" + str(i) + "T00:00:00Z") articles.append(article) for i in range(3, 5): article = models.Article( - **ArticleFixtureFactory.make_article_source(eissn="1111-1111", pissn="1111-1111", with_id=False, in_doaj=False) + **ArticleFixtureFactory.make_article_source(eissn="1111-1111", pissn="1111-1111", with_id=False, + in_doaj=False) ) article.set_created("2019-01-0" + str(i) + "T00:00:00Z") articles.append(article) for i in range(5, 7): article = models.Article( - **ArticleFixtureFactory.make_article_source(eissn="2222-2222", pissn="2222-2222", with_id=False, in_doaj=True) + **ArticleFixtureFactory.make_article_source(eissn="2222-2222", pissn="2222-2222", with_id=False, + in_doaj=True) ) article.set_created("2019-01-0" + str(i) + "T00:00:00Z") articles.append(article) for i in range(7, 9): article = models.Article( - **ArticleFixtureFactory.make_article_source(eissn="2222-2222", pissn="2222-2222", with_id=False, in_doaj=False) + **ArticleFixtureFactory.make_article_source(eissn="2222-2222", pissn="2222-2222", with_id=False, + in_doaj=False) ) article.set_created("2019-01-0" + str(i) + "T00:00:00Z") articles.append(article) @@ -1429,18 +1457,20 @@ def test_30_article_stats(self): def test_31_cache(self): models.Cache.cache_site_statistics({ - "journals" : 10, - "new_journals" : 20, - "countries" : 30, - "abstracts" : 40, - "no_apc" : 50 + "journals": 10, + "new_journals": 20, + "countries": 30, + "abstracts": 40, + "no_apc": 50 }) models.Cache.cache_csv("/csv/filename.csv") models.Cache.cache_sitemap("sitemap.xml") - models.Cache.cache_public_data_dump("ac", "af", "http://example.com/article", 100, "jc", "jf", "http://example.com/journal", 200) + models.Cache.cache_public_data_dump("ac", "af", "http://example.com/article", + 100, "jc", "jf", + "http://example.com/journal", 200) time.sleep(1) @@ -1534,26 +1564,26 @@ def test_32_journal_like_object_discovery(self): # find_by_journal_url(cls, url, in_doaj=None, max=10) # recent(cls, max=10): -# TODO: reinstate this test when author emails have been disallowed again -# ''' -# def test_33_article_with_author_email(self): -# """Check the system disallows articles with emails in the author field""" -# a_source = ArticleFixtureFactory.make_article_source() -# -# # Creating a model from a source with email is rejected by the DataObj -# a_source['bibjson']['author'][0]['email'] = 'author@example.com' -# with self.assertRaises(dataobj.DataStructureException): -# a = models.Article(**a_source) -# bj = a.bibjson() -# -# # Remove the email address again to create the model -# del a_source['bibjson']['author'][0]['email'] -# a = models.Article(**a_source) -# -# # We can't add an author with an email address any more. -# with self.assertRaises(TypeError): -# a.bibjson().add_author(name='Ms Test', affiliation='School of Rock', email='author@example.com') -# ''' + # TODO: reinstate this test when author emails have been disallowed again + # ''' + # def test_33_article_with_author_email(self): + # """Check the system disallows articles with emails in the author field""" + # a_source = ArticleFixtureFactory.make_article_source() + # + # # Creating a model from a source with email is rejected by the DataObj + # a_source['bibjson']['author'][0]['email'] = 'author@example.com' + # with self.assertRaises(dataobj.DataStructureException): + # a = models.Article(**a_source) + # bj = a.bibjson() + # + # # Remove the email address again to create the model + # del a_source['bibjson']['author'][0]['email'] + # a = models.Article(**a_source) + # + # # We can't add an author with an email address any more. + # with self.assertRaises(TypeError): + # a.bibjson().add_author(name='Ms Test', affiliation='School of Rock', email='author@example.com') + # ''' def test_34_preserve(self): model = models.PreservationState() @@ -1602,7 +1632,7 @@ def test_35_event(self): assert event2.context.get("key2") == "value2" assert event2.when == event.when - event3 = models.Event("ABCD", "another", {"key3" : "value3"}) + event3 = models.Event("ABCD", "another", {"key3": "value3"}) assert event3.id == "ABCD" assert event3.who == "another" assert event3.context.get("key3") == "value3" @@ -1722,9 +1752,9 @@ def test_40_autocheck_retrieves(self): ap2 = models.Autocheck.for_journal("9876") assert ap2.journal == "9876" + class TestAccount(DoajTestCase): def test_get_name_safe(self): - # have name acc = models.Account.make_account(email='user@example.com') acc_name = 'Account Name' @@ -1766,4 +1796,45 @@ def test_11_find_by_issn(self): assert journals[0].id == j1.id journals = models.Journal.find_by_issn_exact(["1111-1111", "3333-3333"], True) - assert len(journals) == 0 \ No newline at end of file + assert len(journals) == 0 + + +class TestJournal(DoajTestCase): + def test_renew_editor_group_name(self): + # preparing data + eg1 = models.EditorGroup(**{ + 'id': 'eg1', + 'name': 'Editor Group 111111', + }) + eg2 = models.EditorGroup(**{ + 'id': 'eg2', + 'name': 'Editor Group 222222', + }) + models.DomainObject.save_all_block_last([eg1, eg2]) + + journals = [models.Journal(**j) for j in JournalFixtureFactory.make_many_journal_sources(count=3, in_doaj=True)] + journals[0].set_editor_group(eg1.id) + journals[1].set_editor_group(eg1.id) + journals[2].set_editor_group(eg2.id) + + models.DomainObject.save_all_block_last(journals) + + def refresh_journals(journals): + return [models.Journal.pull(j.id) for j in journals] + + journals = refresh_journals(journals) + assert journals[0].editor_group_name() == eg1.name + assert journals[1].editor_group_name() == eg1.name + assert journals[2].editor_group_name() == eg2.name + + # start testing + new_name = 'XXCCCCXXX' + models.Journal.renew_editor_group_name( + editor_group_id=eg1.id, + new_name=new_name, + ) + + journals = refresh_journals(journals) + assert journals[0].editor_group_name() == new_name + assert journals[1].editor_group_name() == new_name + assert journals[2].editor_group_name() == eg2.name diff --git a/doajtest/unit/test_query_filters.py b/doajtest/unit/test_query_filters.py index 59ce6dfafe..8987a8fdca 100644 --- a/doajtest/unit/test_query_filters.py +++ b/doajtest/unit/test_query_filters.py @@ -92,7 +92,7 @@ def test_05_editor(self): "track_total_hits": True, 'query': { 'bool': { - 'must': [{'terms': {'admin.editor_group.exact': [eg.name]}}] + 'must': [{'terms': {'admin.editor_group.exact': [eg.id]}}] } } }, newq.as_dict() diff --git a/doajtest/unit/test_task_journal_bulkedit.py b/doajtest/unit/test_task_journal_bulkedit.py index 12facfee39..f059f8e908 100644 --- a/doajtest/unit/test_task_journal_bulkedit.py +++ b/doajtest/unit/test_task_journal_bulkedit.py @@ -61,10 +61,12 @@ def test_01_editor_group_successful_assign(self): new_eg = EditorGroupFixtureFactory.setup_editor_group_with_editors(group_name='Test Editor Group') # test dry run - summary = journal_manage({"query": {"terms": {"_id": [j.id for j in self.journals]}}}, editor_group=new_eg.name, dry_run=True) + summary = journal_manage({"query": {"terms": {"_id": [j.id for j in self.journals]}}}, + editor_group=new_eg.id, dry_run=True) assert summary.as_dict().get("affected", {}).get("journals") == TEST_JOURNAL_COUNT, summary.as_dict() - summary = journal_manage({"query": {"terms": {"_id": [j.id for j in self.journals]}}}, editor_group=new_eg.name, dry_run=False) + summary = journal_manage({"query": {"terms": {"_id": [j.id for j in self.journals]}}}, + editor_group=new_eg.id, dry_run=False) assert summary.as_dict().get("affected", {}).get("journals") == TEST_JOURNAL_COUNT, summary.as_dict() sleep(1) @@ -74,7 +76,7 @@ def test_01_editor_group_successful_assign(self): modified_journals = [j.pull(j.id) for j in self.journals] for ix, j in enumerate(modified_journals): - assert j.editor_group == new_eg.name, \ + assert j.editor_group == new_eg.id, \ "modified_journals[{}].editor_group is {}" \ "\nHere is the BackgroundJob audit log:\n{}"\ .format(ix, j.editor_group, json.dumps(job.audit, indent=2)) diff --git a/doajtest/unit/test_task_suggestion_bulkedit.py b/doajtest/unit/test_task_suggestion_bulkedit.py index 6843db1375..5b1014faaa 100644 --- a/doajtest/unit/test_task_suggestion_bulkedit.py +++ b/doajtest/unit/test_task_suggestion_bulkedit.py @@ -70,10 +70,12 @@ def test_01_editor_group_successful_assign(self): new_eg = EditorGroupFixtureFactory.setup_editor_group_with_editors(group_name='Test Editor Group') # test dry run - summary = suggestion_manage({"query": {"terms": {"_id": [s.id for s in self.suggestions]}}}, editor_group=new_eg.name, dry_run=True) + summary = suggestion_manage({"query": {"terms": {"_id": [s.id for s in self.suggestions]}}}, + editor_group=new_eg.id, dry_run=True) assert summary.as_dict().get("affected", {}).get("applications") == TEST_SUGGESTION_COUNT, summary.as_dict() - summary = suggestion_manage({"query": {"terms": {"_id": [s.id for s in self.suggestions]}}}, editor_group=new_eg.name, dry_run=False) + summary = suggestion_manage({"query": {"terms": {"_id": [s.id for s in self.suggestions]}}}, + editor_group=new_eg.id, dry_run=False) assert summary.as_dict().get("affected", {}).get("applications") == TEST_SUGGESTION_COUNT, summary.as_dict() blocklist = [(s.id, s.last_updated) for s in self.suggestions] @@ -85,7 +87,7 @@ def test_01_editor_group_successful_assign(self): modified_suggestions = [s.pull(s.id) for s in self.suggestions] for ix, s in enumerate(modified_suggestions): - assert s.editor_group == new_eg.name, \ + assert s.editor_group == new_eg.id, \ "modified_suggestions[{}].editor_group is {}\n" \ "Here is the BackgroundJob audit log:\n{}".format( ix, s.editor_group, json.dumps(job.audit, indent=2) diff --git a/doajtest/unit/test_view_doaj.py b/doajtest/unit/test_view_doaj.py new file mode 100644 index 0000000000..b9a5ed10e3 --- /dev/null +++ b/doajtest/unit/test_view_doaj.py @@ -0,0 +1,109 @@ +import time + +from doajtest.fixtures.editor_groups import create_editor_group_en, create_editor_group_cn, create_editor_group_jp +from doajtest.helpers import DoajTestCase +from portality import models +from portality.view.doaj import id_text_mapping + + +class TestViewDoaj(DoajTestCase): + + def test_autocomplete_pair(self): + eg = create_editor_group_en() + eg.save(blocking=True) + + with self.app_test.test_client() as client: + doc_type = 'editor_group' + field_name = 'name' + id_field = 'id' + query = 'eng' + response = client.get(f'/autocomplete/{doc_type}/{field_name}/{id_field}?q={query}', ) + resp_json = response.json + assert response.status_code == 200 + assert len(resp_json['suggestions']) == 1 + assert resp_json['suggestions'][0]['id'] == eg.id + assert resp_json['suggestions'][0]['text'] == eg.name + + def test_autocomplete_pair__not_found(self): + with self.app_test.test_client() as client: + doc_type = 'editor_group' + field_name = 'name' + id_field = 'id' + query = 'alksjdlaksjdalksdjl' + response = client.get(f'/autocomplete/{doc_type}/{field_name}/{id_field}?q={query}', ) + resp_json = response.json + assert response.status_code == 200 + assert len(resp_json['suggestions']) == 0 + + def test_autocomplete_pair__unknown_doc_type(self): + with self.app_test.test_client() as client: + doc_type = 'alskdjalskdjal' + field_name = 'name' + id_field = 'id' + query = 'eng' + response = client.get(f'/autocomplete/{doc_type}/{field_name}/{id_field}?q={query}', ) + resp_json = response.json + assert response.status_code == 200 + assert len(resp_json['suggestions']) == 1 + assert resp_json['suggestions'][0]['id'] == '' + + def test_autocomplete_text(self): + eg = create_editor_group_en() + eg.save(blocking=True) + with self.app_test.test_client() as client: + doc_type = 'editor_group' + field_name = 'name' + id_field = 'id' + response = client.get(f'/autocomplete-text/{doc_type}/{field_name}/{id_field}?id={eg.id}', ) + + assert response.status_code == 200 + assert response.json == {eg.id: eg.name} + + def test_autocomplete_text__not_found(self): + eg = create_editor_group_en() + eg.save(blocking=True) + with self.app_test.test_client() as client: + doc_type = 'editor_group' + field_name = 'name' + id_field = 'id' + id_val = 'qwjeqkwjeq' + response = client.get(f'/autocomplete-text/{doc_type}/{field_name}/{id_field}?id={id_val}', ) + + assert response.status_code == 200 + assert response.json == {} + + def test_autocomplete_text__ids(self): + eg = create_editor_group_en() + eg2 = create_editor_group_cn() + eg3 = create_editor_group_jp() + + models.EditorGroup.save_all_block_last([eg, eg2, eg3]) + + with self.app_test.test_client() as client: + doc_type = 'editor_group' + field_name = 'name' + id_field = 'id' + response = client.get(f'/autocomplete-text/{doc_type}/{field_name}/{id_field}', + json={'ids': [eg.id, eg2.id]}) + + assert response.status_code == 200 + assert response.json == {eg.id: eg.name, eg2.id: eg2.name, } + + def test_autocomplete_text__ids_empty(self): + with self.app_test.test_client() as client: + doc_type = 'editor_group' + field_name = 'name' + id_field = 'id' + response = client.get(f'/autocomplete-text/{doc_type}/{field_name}/{id_field}', + json={'ids': []}) + + assert response.status_code == 200 + assert response.json == {} + + def test_id_text_mapping(self): + models.EditorGroup.save_all_block_last([create_editor_group_en(), create_editor_group_cn(), + create_editor_group_jp()]) + + time.sleep(5) + assert id_text_mapping('editor_group', 'name', 'id', 'egid') == {'egid': 'English'} + assert id_text_mapping('editor_group', 'name', 'id', 'egid2') == {'egid2': 'Chinese'} diff --git a/docs/dev/user-guide/user-guide.md b/docs/dev/user-guide/manage-bgjobs.md similarity index 100% rename from docs/dev/user-guide/user-guide.md rename to docs/dev/user-guide/manage-bgjobs.md diff --git a/portality/bll/services/authorisation.py b/portality/bll/services/authorisation.py index 5be4e65c77..dd6bf52083 100644 --- a/portality/bll/services/authorisation.py +++ b/portality/bll/services/authorisation.py @@ -74,7 +74,7 @@ def can_edit_application(self, account, application): if not application.editor_group: return False - eg = models.EditorGroup.pull_by_key("name", application.editor_group) + eg = models.EditorGroup.pull(application.editor_group) if eg is not None and eg.editor == account.id: return True @@ -129,7 +129,7 @@ def can_edit_journal(self, account: models.Account, journal: models.Journal): passed = True # now check whether the user is the editor of the editor group - eg = models.EditorGroup.pull_by_key("name", journal.editor_group) # ~~->EditorGroup:Model~~ + eg = models.EditorGroup.pull(journal.editor_group) # ~~->EditorGroup:Model~~ if eg is not None and eg.editor == account.id: passed = True diff --git a/portality/bll/services/todo.py b/portality/bll/services/todo.py index fc57f66da7..3b6f02ea84 100644 --- a/portality/bll/services/todo.py +++ b/portality/bll/services/todo.py @@ -1,9 +1,12 @@ -from portality.lib.argvalidate import argvalidate +from typing import Iterable + +from portality import constants from portality import models from portality.bll import exceptions -from portality import constants from portality.lib import dates -from datetime import datetime +from portality.lib.argvalidate import argvalidate +from portality.models import EditorGroup + class TodoService(object): """ @@ -14,18 +17,18 @@ class TodoService(object): def group_stats(self, group_id): # ~~-> EditorGroup:Model~~ eg = models.EditorGroup.pull(group_id) - stats = {"editor_group" : eg.data} + stats = {"editor_group": eg.data} - #~~-> Account:Model ~~ + # ~~-> Account:Model ~~ stats["editors"] = {} editors = [eg.editor] + eg.associates for editor in editors: acc = models.Account.pull(editor) stats["editors"][editor] = { - "email" : None if acc is None else acc.email - } + "email": None if acc is None else acc.email + } - q = GroupStatsQuery(eg.name) + q = GroupStatsQuery(eg.id) resp = models.Application.query(q=q.query()) stats["total"] = {"applications": 0, "update_requests": 0} @@ -42,7 +45,8 @@ def group_stats(self, group_id): stats["by_editor"][bucket["key"]]["update_requests"] = b["doc_count"] stats["total"]["update_requests"] += b["doc_count"] - unassigned_buckets = resp.get("aggregations", {}).get("unassigned", {}).get("application_type", {}).get("buckets", []) + unassigned_buckets = resp.get("aggregations", {}).get("unassigned", {}).get("application_type", {}).get( + "buckets", []) stats["unassigned"] = {"applications": 0, "update_requests": 0} for ub in unassigned_buckets: if ub["key"] == constants.APPLICATION_TYPE_NEW_APPLICATION: @@ -63,7 +67,6 @@ def group_stats(self, group_id): return stats - def top_todo(self, account, size=25, new_applications=True, update_requests=True): """ Returns the top number of todo items for a given user @@ -74,7 +77,7 @@ def top_todo(self, account, size=25, new_applications=True, update_requests=True """ # first validate the incoming arguments to ensure that we've got the right thing argvalidate("top_todo", [ - {"arg" : account, "instance" : models.Account, "allow_none" : False, "arg_name" : "account"} + {"arg": account, "instance": models.Account, "allow_none": False, "arg_name": "account"} ], exceptions.ArgumentException) queries = [] @@ -90,7 +93,7 @@ def top_todo(self, account, size=25, new_applications=True, update_requests=True queries.append(TodoRules.maned_last_month_update_requests(size, maned_of)) queries.append(TodoRules.maned_new_update_requests(size, maned_of)) - if new_applications: # editor and associate editor roles only deal with new applications + if new_applications: # editor and associate editor roles only deal with new applications if account.has_role("editor"): groups = [g for g in models.EditorGroup.groups_by_editor(account.id)] regular_groups = [g for g in groups if g.maned != account.id] @@ -123,15 +126,18 @@ def top_todo(self, account, size=25, new_applications=True, update_requests=True for aid, q, sort, boost in queries: applications = models.Application.object_query(q=q.query()) for ap in applications: - todos.append({ + todo = { "date": ap.last_manual_update_timestamp if sort == "last_manual_update" else ap.date_applied_timestamp, "date_type": sort, - "action_id" : [aid], - "title" : ap.bibjson().title, - "object_id" : ap.id, - "object" : ap, - "boost": boost - }) + "action_id": [aid], + "title": ap.bibjson().title, + "object_id": ap.id, + "object": ap, + "boost": boost, + } + if ap.editor_group: + todo["editor_group_name"] = ap.editor_group_name(default_id=True) + todos.append(todo) todos = self._rationalise_todos(todos, size) @@ -170,7 +176,7 @@ def maned_stalled(cls, size, maned_of): stalled = TodoQuery( musts=[ TodoQuery.lmu_older_than(8), - TodoQuery.editor_group(maned_of), + TodoQuery.editor_groups(maned_of), TodoQuery.is_new_application() ], must_nots=[ @@ -187,7 +193,7 @@ def maned_follow_up_old(cls, size, maned_of): follow_up_old = TodoQuery( musts=[ TodoQuery.cd_older_than(10), - TodoQuery.editor_group(maned_of), + TodoQuery.editor_groups(maned_of), TodoQuery.is_new_application() ], must_nots=[ @@ -204,7 +210,7 @@ def maned_ready(cls, size, maned_of): ready = TodoQuery( musts=[ TodoQuery.status([constants.APPLICATION_STATUS_READY]), - TodoQuery.editor_group(maned_of), + TodoQuery.editor_groups(maned_of), TodoQuery.is_new_application() ], sort=sort_date, @@ -219,7 +225,7 @@ def maned_completed(cls, size, maned_of): musts=[ TodoQuery.status([constants.APPLICATION_STATUS_COMPLETED]), TodoQuery.lmu_older_than(2), - TodoQuery.editor_group(maned_of), + TodoQuery.editor_groups(maned_of), TodoQuery.is_new_application() ], sort=sort_date, @@ -235,7 +241,7 @@ def maned_assign_pending(cls, size, maned_of): TodoQuery.exists("admin.editor_group"), TodoQuery.lmu_older_than(2), TodoQuery.status([constants.APPLICATION_STATUS_PENDING]), - TodoQuery.editor_group(maned_of), + TodoQuery.editor_groups(maned_of), TodoQuery.is_new_application() ], must_nots=[ @@ -258,7 +264,7 @@ def maned_last_month_update_requests(cls, size, maned_of): TodoQuery.exists("admin.editor_group"), TodoQuery.cd_older_than(since_som, unit="s"), # TodoQuery.status([constants.APPLICATION_STATUS_UPDATE_REQUEST]), - TodoQuery.editor_group(maned_of), + TodoQuery.editor_groups(maned_of), TodoQuery.is_update_request() ], must_nots=[ @@ -278,7 +284,7 @@ def maned_new_update_requests(cls, size, maned_of): TodoQuery.exists("admin.editor_group"), # TodoQuery.cd_older_than(4), # TodoQuery.status([constants.APPLICATION_STATUS_UPDATE_REQUEST]), - TodoQuery.editor_group(maned_of), + TodoQuery.editor_groups(maned_of), TodoQuery.is_update_request() ], must_nots=[ @@ -364,7 +370,7 @@ def editor_assign_pending(cls, groups, size): return constants.TODO_EDITOR_ASSIGN_PENDING, assign_pending, sort_date, 1 @classmethod - def associate_stalled(cls, acc_id, size): + def associate_stalled(cls, acc_id, size): sort_field = "created_date" stalled = TodoQuery( musts=[ @@ -386,7 +392,7 @@ def associate_stalled(cls, acc_id, size): return constants.TODO_ASSOCIATE_PROGRESS_STALLED, stalled, sort_field, 0 @classmethod - def associate_follow_up_old(cls, acc_id, size): + def associate_follow_up_old(cls, acc_id, size): sort_field = "created_date" follow_up_old = TodoQuery( musts=[ @@ -448,7 +454,7 @@ class TodoQuery(object): ~~->$Todo:Query~~ ~~^->Elasticsearch:Technology~~ """ - lmu_sort = {"last_manual_update" : {"order" : "asc"}} + lmu_sort = {"last_manual_update": {"order": "asc"}} # cd_sort = {"created_date" : {"order" : "asc"}} # NOTE that admin.date_applied and created_date should be the same for applications, but for some reason this is not always the case # therefore, we take a created_date sort to mean a date_applied sort @@ -463,16 +469,16 @@ def __init__(self, musts=None, must_nots=None, sort="last_manual_update", size=1 def query(self): sort = self.lmu_sort if self._sort == "last_manual_update" else self.cd_sort q = { - "query" : { - "bool" : { + "query": { + "bool": { "must": self._musts, "must_not": self._must_nots } }, - "sort" : [ + "sort": [ sort ], - "size" : self._size + "size": self._size } return q @@ -492,14 +498,6 @@ def is_update_request(cls): } } - @classmethod - def editor_group(cls, groups): - return { - "terms" : { - "admin.editor_group.exact" : [g.name for g in groups] - } - } - @classmethod def lmu_older_than(cls, weeks): return { @@ -523,25 +521,24 @@ def cd_older_than(cls, count, unit="w"): @classmethod def status(cls, statuses): return { - "terms" : { - "admin.application_status.exact" : statuses + "terms": { + "admin.application_status.exact": statuses } } @classmethod def exists(cls, field): return { - "exists" : { - "field" : field + "exists": { + "field": field } } @classmethod - def editor_groups(cls, groups): - gids = [g.name for g in groups] + def editor_groups(cls, groups: Iterable[EditorGroup]): return { "terms": { - "admin.editor_group.exact": gids + "admin.editor_group.exact": [g.id for g in groups] } } @@ -554,31 +551,32 @@ def editor(cls, acc_id): } -class GroupStatsQuery(): +class GroupStatsQuery: """ ~~->$GroupStats:Query~~ ~~^->Elasticsearch:Technology~~ """ - def __init__(self, group_name, editor_count=10): - self.group_name = group_name + + def __init__(self, group_id, editor_count=10): + self.group_id = group_id self.editor_count = editor_count def query(self): return { - "track_total_hits" : True, + "track_total_hits": True, "query": { "bool": { "must": [ { "term": { - "admin.editor_group.exact": self.group_name + "admin.editor_group.exact": self.group_id } } ], - "must_not" : [ + "must_not": [ { - "terms" : { - "admin.application_status.exact" : [ + "terms": { + "admin.application_status.exact": [ constants.APPLICATION_STATUS_ACCEPTED, constants.APPLICATION_STATUS_REJECTED ] @@ -587,26 +585,26 @@ def query(self): ] } }, - "size" : 0, - "aggs" : { - "editor" : { - "terms" : { - "field" : "admin.editor.exact", - "size" : self.editor_count + "size": 0, + "aggs": { + "editor": { + "terms": { + "field": "admin.editor.exact", + "size": self.editor_count }, - "aggs" : { - "application_type" : { - "terms" : { + "aggs": { + "application_type": { + "terms": { "field": "admin.application_type.exact", "size": 2 } } } }, - "status" : { - "terms" : { - "field" : "admin.application_status.exact", - "size" : len(constants.APPLICATION_STATUSES_ALL) + "status": { + "terms": { + "field": "admin.application_status.exact", + "size": len(constants.APPLICATION_STATUSES_ALL) }, "aggs": { "application_type": { @@ -617,13 +615,13 @@ def query(self): } } }, - "unassigned" : { - "missing" : { + "unassigned": { + "missing": { "field": "admin.editor.exact" }, - "aggs" : { - "application_type" : { - "terms" : { + "aggs": { + "application_type": { + "terms": { "field": "admin.application_type.exact", "size": 2 } @@ -631,4 +629,4 @@ def query(self): } } } - } \ No newline at end of file + } diff --git a/portality/core.py b/portality/core.py index d1146d1224..c2aa3fba45 100644 --- a/portality/core.py +++ b/portality/core.py @@ -189,7 +189,11 @@ def create_es_connection(app): # return type -def put_mappings(conn, mappings): +def get_doaj_index_name(name: str) -> str: + return app.config['ELASTIC_SEARCH_DB_PREFIX'] + name + + +def put_mappings(conn: elasticsearch.Elasticsearch, mappings): # get the ES version that we're working with #es_version = app.config.get("ELASTIC_SEARCH_VERSION", "1.7.5") diff --git a/portality/dao.py b/portality/dao.py index 80635d714a..e0c93fae7b 100644 --- a/portality/dao.py +++ b/portality/dao.py @@ -1,20 +1,26 @@ -import time +from __future__ import annotations +import json import re import sys -import uuid -import json -import elasticsearch +import time import urllib.parse - +import uuid from collections import UserDict from copy import deepcopy from datetime import timedelta -from typing import List +from typing import List, Iterable, Union, Dict, TypedDict, Optional +from typing import TYPE_CHECKING + +import elasticsearch from portality.core import app, es_connection as ES from portality.lib import dates from portality.lib.dates import FMT_DATETIME_STD +if TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import Self + # All models in models.py should inherit this DomainObject to know how to save themselves in the index and so on. # You can overwrite and add to the DomainObject functions as required. See models.py for some examples. @@ -22,6 +28,10 @@ ES_MAPPING_MISSING_REGEX = re.compile(r'.*No mapping found for \[[a-zA-Z0-9-_\.]+?\] in order to sort on.*', re.DOTALL) CONTENT_TYPE_JSON = {'Content-Type': 'application/json'} +class IdText(TypedDict): + id: str + text: str + class ElasticSearchWriteException(Exception): pass @@ -380,7 +390,7 @@ def refresh(cls): return ES.indices.refresh(index=cls.index_name()) @classmethod - def pull(cls, id_): + def pull(cls, id_) -> 'Self': """Retrieve object by id.""" if id_ is None or id_ == '': return None @@ -400,7 +410,7 @@ def pull(cls, id_): return cls(**out) @classmethod - def pull_by_key(cls, key, value): + def pull_by_key(cls, key, value) -> 'Self': res = cls.query(q={"query": {"term": {key + app.config['FACET_FIELD']: value}}}) if res.get('hits', {}).get('total', {}).get('value', 0) == 1: return cls.pull(res['hits']['hits'][0]['_source']['id']) @@ -408,7 +418,7 @@ def pull_by_key(cls, key, value): return None @classmethod - def object_query(cls, q=None, **kwargs): + def object_query(cls, q=None, **kwargs) -> List['Self']: result = cls.query(q, **kwargs) return [cls(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])] @@ -569,7 +579,7 @@ def handle_es_raw_response(cls, res, wrap, extra_trace_info=''): @classmethod def iterate(cls, q: dict = None, page_size: int = 1000, limit: int = None, wrap: bool = True, - keepalive: str = '1m'): + keepalive: str = '1m') -> Iterable[Union['Self', Dict]]: """ Provide an iterable of all items in a model, use :param q: The query to scroll results on :param page_size: limited by ElasticSearch, check settings to override @@ -798,7 +808,7 @@ def wildcard_autocomplete_query(cls, field, substring, before=True, after=True, return cls.send_query(q.query()) @classmethod - def advanced_autocomplete(cls, filter_field, facet_field, substring, size=5, prefix_only=True): + def advanced_autocomplete(cls, filter_field, facet_field, substring, size=5, prefix_only=True) -> List[IdText]: analyzed = True if " " in substring: analyzed = False @@ -819,7 +829,7 @@ def advanced_autocomplete(cls, filter_field, facet_field, substring, size=5, pre return result @classmethod - def autocomplete(cls, field, prefix, size=5): + def autocomplete(cls, field, prefix, size=5) -> List[IdText]: res = None # if there is a space in the prefix, the prefix query won't work, so we fall back to a wildcard # we only do this if we have to, because the wildcard query is a little expensive @@ -837,7 +847,35 @@ def autocomplete(cls, field, prefix, size=5): return result @classmethod - def q2obj(cls, **kwargs): + def autocomplete_pair(cls, query_field, query_prefix, id_field, text_field, size=5) -> list[IdText]: + query = AutocompletePairQuery(query_field, query_prefix, + source_fields=[id_field, text_field], + size=size).query() + objs = cls.q2obj(q=query) + return [{"id": getattr(obj, id_field), "text": getattr(obj, text_field)} for obj in objs] + + @classmethod + def get_target_value(cls, query, id_name, field_name) -> str | None: + results = cls.get_target_values(query, id_name, field_name, size=2) + if len(results) > 1: + app.logger.debug("More than one record found for query {q}".format(q=json.dumps(query, indent=2))) + + if len(results) > 0: + return list(results.values())[0] + + return None + + + @classmethod + def get_target_values(cls, query, id_name, field_name, size=1000) -> dict[str]: + query['_source'] = [id_name, field_name] + query['size'] = size + res = cls.query(q=query) + return {hit['_source'].get(id_name): hit['_source'].get(field_name) + for hit in res['hits']['hits']} + + @classmethod + def q2obj(cls, **kwargs) -> List['Self']: extra_trace_info = '' if 'q' in kwargs: extra_trace_info = "\nQuery sent to ES (before manipulation in DomainObject.query):\n{}\n".format( @@ -937,6 +975,13 @@ def save_all(cls, models, blocking=False): if blocking: cls.blockall((m.id, getattr(m, "last_updated", None)) for m in models) + @classmethod + def save_all_block_last(cls, objects): + *objs, last = objects + for obj in objects: + obj.save() + last.save(blocking=True) + def any_pending_tasks(): """ Check if there are any pending tasks in the elasticsearch task queue """ @@ -1061,6 +1106,25 @@ def query(self): } +class AutocompletePairQuery(object): + def __init__(self, query_field, query_value, source_fields=None, size=5): + self.query_field = query_field + self.query_value = query_value + self.source_fields = source_fields + self.size = size + + def query(self): + query = { + "query": { + "prefix": {self.query_field: self.query_value} + }, + "size": self.size + } + if self.source_fields is not None: + query["_source"] = self.source_fields + return query + + ######################################################################### # A query handler that knows how to speak facetview2 ######################################################################### @@ -1108,6 +1172,23 @@ def url_encode_query(query): return urllib.parse.quote(json.dumps(query).replace(' ', '')) +######################################################################### +# id to text Queries +######################################################################### + +class IdTextTermQuery: + def __init__(self, id_field, id_value): + self.id_field = id_field + self.id_value = id_value + + def query(self): + if isinstance(self.id_value, list): + term = {"terms": {self.id_field: self.id_value}} + else: + term = {"term": {self.id_field: self.id_value}} + return {"query": term} + + def patch_model_for_bulk(obj: DomainObject): obj.data['es_type'] = obj.__type__ obj.data['id'] = obj.makeid() diff --git a/portality/events/consumers/application_assed_assigned_notify.py b/portality/events/consumers/application_assed_assigned_notify.py index a403f5ab34..c7f1ae19f3 100644 --- a/portality/events/consumers/application_assed_assigned_notify.py +++ b/portality/events/consumers/application_assed_assigned_notify.py @@ -33,7 +33,7 @@ def consume(cls, event): notification.classification = constants.NOTIFICATION_CLASSIFICATION_ASSIGN notification.long = svc.long_notification(cls.ID).format( journal_title=application.bibjson().title, - group_name=application.editor_group + group_name=application.editor_group_name(default_id=True) ) notification.short = svc.short_notification(cls.ID).format( issns=application.bibjson().issns_as_text() diff --git a/portality/events/consumers/application_editor_completed_notify.py b/portality/events/consumers/application_editor_completed_notify.py index 4c5710e418..f9e2e4cbfc 100644 --- a/portality/events/consumers/application_editor_completed_notify.py +++ b/portality/events/consumers/application_editor_completed_notify.py @@ -40,9 +40,7 @@ def consume(cls, event): associate_editor = event.who # Notification is to the editor in charge of this application's assigned editor group - editor_group_name = application.editor_group - editor_group_id = models.EditorGroup.group_exists_by_name(name=editor_group_name) - eg = models.EditorGroup.pull(editor_group_id) + eg = models.EditorGroup.pull(application.editor_group) group_editor = eg.editor if not group_editor: diff --git a/portality/events/consumers/application_editor_group_assigned_notify.py b/portality/events/consumers/application_editor_group_assigned_notify.py index 54bfdfe1e6..4147387f3b 100644 --- a/portality/events/consumers/application_editor_group_assigned_notify.py +++ b/portality/events/consumers/application_editor_group_assigned_notify.py @@ -24,8 +24,7 @@ def consume(cls, event): if not application.editor_group: return - editor_group = models.EditorGroup.pull_by_key("name", application.editor_group) - + editor_group = models.EditorGroup.pull(application.editor_group) if not editor_group.editor: raise exceptions.NoSuchPropertyException("Editor Group {x} does not have property `editor`".format(x=editor_group.id)) diff --git a/portality/events/consumers/application_editor_inprogress_notify.py b/portality/events/consumers/application_editor_inprogress_notify.py index 38d698683c..40e5371586 100644 --- a/portality/events/consumers/application_editor_inprogress_notify.py +++ b/portality/events/consumers/application_editor_inprogress_notify.py @@ -36,9 +36,7 @@ def consume(cls, event): return # Notification is to the editor in charge of this application's assigned editor group - editor_group_name = application.editor_group - editor_group_id = models.EditorGroup.group_exists_by_name(name=editor_group_name) - eg = models.EditorGroup.pull(editor_group_id) + eg = models.EditorGroup.pull(application.editor_group) group_editor = eg.editor if not group_editor: diff --git a/portality/events/consumers/application_maned_ready_notify.py b/portality/events/consumers/application_maned_ready_notify.py index ac4b70d9c2..482231a6a4 100644 --- a/portality/events/consumers/application_maned_ready_notify.py +++ b/portality/events/consumers/application_maned_ready_notify.py @@ -26,7 +26,7 @@ def consume(cls, event): return - eg = models.EditorGroup.pull_by_key("name", application.editor_group) + eg = models.EditorGroup.pull(application.editor_group) managing_editor = eg.maned if not managing_editor: return diff --git a/portality/events/consumers/journal_discontinuing_soon_notify.py b/portality/events/consumers/journal_discontinuing_soon_notify.py index fc603fcd76..d031b846c5 100644 --- a/portality/events/consumers/journal_discontinuing_soon_notify.py +++ b/portality/events/consumers/journal_discontinuing_soon_notify.py @@ -32,7 +32,7 @@ def consume(cls, event): if not journal.editor_group: return - eg = models.EditorGroup.pull_by_key("name", journal.editor_group) + eg = models.EditorGroup.pull(journal.editor_group) managing_editor = eg.maned if not managing_editor: return diff --git a/portality/events/consumers/journal_editor_group_assigned_notify.py b/portality/events/consumers/journal_editor_group_assigned_notify.py index 0119bcaeb3..1f736bc3d4 100644 --- a/portality/events/consumers/journal_editor_group_assigned_notify.py +++ b/portality/events/consumers/journal_editor_group_assigned_notify.py @@ -29,8 +29,7 @@ def consume(cls, event): if not journal.editor_group: return - editor_group = models.EditorGroup.pull_by_key("name", journal.editor_group) - + editor_group = models.EditorGroup.pull(journal.editor_group) if not editor_group.editor: raise exceptions.NoSuchPropertyException("Editor Group {x} does not have property `editor`".format(x=editor_group.id)) diff --git a/portality/forms/application_forms.py b/portality/forms/application_forms.py index 8d4adcd85d..eb9fb0a446 100644 --- a/portality/forms/application_forms.py +++ b/portality/forms/application_forms.py @@ -1810,18 +1810,18 @@ class FieldDefinitions: "label": "Group", "input": "text", "widgets": [ - {"autocomplete": {"type": "editor_group", "field": "name", "include": False}} - # ~~^-> Autocomplete:FormWidget~~ + {"autocomplete": {"type": "editor_group", "field": "name", + "include": False, "id_field": "id" }} # ~~^-> Autocom dget~~ ], "contexts": { "editor": { "disabled": True }, - "admin": { - "widgets": [ - {"autocomplete": {"type": "editor_group", "field": "name", "include": False}}, - # ~~^-> Autocomplete:FormWidget~~ - {"load_editors": {"field": "editor"}} + "admin" : { + "widgets" : [ + {"autocomplete": {"type": "editor_group", "field": "name", + "include": False, "id_field": "id" }}, # ~~^-> Autocomplete:FormWidget~~ + {"load_editors" : {"field": "editor"}} ] } } @@ -2664,11 +2664,11 @@ def editor_choices(field, formulaic_context): if wtf is None: return [{"display": "", "value": ""}] - editor_group_name = wtf.data - if editor_group_name is None: + editor_group_id = wtf.data + if editor_group_id is None: return [{"display": "", "value": ""}] else: - eg = EditorGroup.pull_by_key("name", editor_group_name) + eg = EditorGroup.pull(editor_group_id) if eg is not None: editors = [eg.editor] editors += eg.associates diff --git a/portality/forms/application_processors.py b/portality/forms/application_processors.py index a7f0271448..1e959b1915 100644 --- a/portality/forms/application_processors.py +++ b/portality/forms/application_processors.py @@ -598,13 +598,6 @@ def finalise(self): # email managing editors if the application was newly set to 'ready' if self.source.application_status != constants.APPLICATION_STATUS_READY and self.target.application_status == constants.APPLICATION_STATUS_READY: - # Tell the ManEds who has made the status change - the editor in charge of the group - # ~~-> EditorGroup:Model~~ - editor_group_name = self.target.editor_group - editor_group_id = models.EditorGroup.group_exists_by_name(name=editor_group_name) - editor_group = models.EditorGroup.pull(editor_group_id) - editor_acc = editor_group.get_editor_account() - # record the event in the provenance tracker # ~~-> Provenance:Model~~ models.Provenance.make(current_user, "status:ready", self.target) diff --git a/portality/forms/validate.py b/portality/forms/validate.py index 363a9cba76..d396d731e8 100644 --- a/portality/forms/validate.py +++ b/portality/forms/validate.py @@ -519,13 +519,13 @@ def __call__(self, form, field): # lifted from from formcontext editor = field.data if editor is not None and editor != "": - editor_group_name = group_field.data - if editor_group_name is not None and editor_group_name != "": - eg = EditorGroup.pull_by_key("name", editor_group_name) + editor_group_id = group_field.data + if editor_group_id is not None and editor_group_id != "": + eg = EditorGroup.pull(editor_group_id) if eg is not None: if eg.is_member(editor): return # success - an editor group was found and our editor was in it - raise validators.ValidationError("Editor '{0}' not found in editor group '{1}'".format(editor, editor_group_name)) + raise validators.ValidationError("Editor '{0}' not found in editor group '{1}'".format(editor, editor_group_id)) else: raise validators.ValidationError("An editor has been assigned without an editor group") diff --git a/portality/lib/query_filters.py b/portality/lib/query_filters.py index 07b1bb7d6f..ae99f5b654 100644 --- a/portality/lib/query_filters.py +++ b/portality/lib/query_filters.py @@ -109,7 +109,7 @@ def editor(q): gnames = [] groups = models.EditorGroup.groups_by_editor(current_user.id) for g in groups: - gnames.append(g.name) + gnames.append(g.id) q.clear_match_all() q.add_must({"terms" : {"admin.editor_group.exact" : gnames}}) return q diff --git a/portality/migrate/3850_link_editor_groups_by_name/README.md b/portality/migrate/3850_link_editor_groups_by_name/README.md new file mode 100644 index 0000000000..166195c57f --- /dev/null +++ b/portality/migrate/3850_link_editor_groups_by_name/README.md @@ -0,0 +1,7 @@ +https://github.com/DOAJ/doajPM/issues/3245 + +The script looks for applications which are rejected and if they have a `related_journal` then it tags them as update requests. + + python portality/upgrade.py -u portality/migrate/3850_link_editor_groups_by_name/migrate.json + python portality/scripts/update_mapping_for_exact.py journal index.editor_group_name + python portality/scripts/update_mapping_for_exact.py application index.editor_group_name diff --git a/portality/migrate/3850_link_editor_groups_by_name/migrate.json b/portality/migrate/3850_link_editor_groups_by_name/migrate.json new file mode 100644 index 0000000000..6c0d23cc2a --- /dev/null +++ b/portality/migrate/3850_link_editor_groups_by_name/migrate.json @@ -0,0 +1,21 @@ +{ + "batch" : 10000, + "types": [ + { + "type" : "application", + "init_with_model" : true, + "keepalive" : "5m", + "functions" : [ + "portality.migrate.3850_link_editor_groups_by_name.operations.name_to_id" + ] + }, + { + "type" : "journal", + "init_with_model" : true, + "keepalive" : "5m", + "functions" : [ + "portality.migrate.3850_link_editor_groups_by_name.operations.name_to_id" + ] + } + ] +} \ No newline at end of file diff --git a/portality/migrate/3850_link_editor_groups_by_name/operations.py b/portality/migrate/3850_link_editor_groups_by_name/operations.py new file mode 100644 index 0000000000..df1e30e520 --- /dev/null +++ b/portality/migrate/3850_link_editor_groups_by_name/operations.py @@ -0,0 +1,42 @@ +import warnings +from typing import Union + +from portality import constants, models +from portality.models import Application, Journal + + +def make_update_request(application): + if application.related_journal and not application.application_type == constants.APPLICATION_TYPE_UPDATE_REQUEST: + application.application_type = constants.APPLICATION_TYPE_UPDATE_REQUEST + return application + + +def name_to_id(model: Union[Application, Journal]): + eg_id = model.editor_group + if eg_id is None: + return model + new_val = models.EditorGroup.group_exists_by_name(eg_id) + if not new_val: + # print(f'editor group not found by name [{eg_id}]') + return model + + + # print(f'editor group [{eg_id}] -> [{new_val}]') + model.set_editor_group(new_val) + return model + + +def id_to_name(model: Union[Application, Journal]): + # for rollback for only + warnings.warn("TOBEREMOVE: rollback for debug only") + eg_id = model.editor_group + if eg_id is None: + return model + eg = models.EditorGroup.pull(eg_id) + if not eg: + # print(f'editor group not found by id [{eg_id}]') + return model + + # print(f'editor group [{eg_id}] -> [{eg.name}]') + model.set_editor_group(eg.name) + return model diff --git a/portality/models/__init__.py b/portality/models/__init__.py index 2570929105..3ecabc2d39 100644 --- a/portality/models/__init__.py +++ b/portality/models/__init__.py @@ -1,4 +1,6 @@ -from typing import Any +from typing import Any, Optional + +from portality.dao import DomainObject # import the versioned objects, so that the current version is the default one from portality.models.v2 import shared_structs from portality.models.v2.bibjson import JournalLikeBibJSON @@ -32,7 +34,7 @@ import sys -def lookup_model(name='', capitalize=True, split_on="_"): +def lookup_model(name='', capitalize=True, split_on="_") -> Optional[DomainObject]: parts = name.split(split_on) if capitalize: parts = [p.capitalize() for p in parts] diff --git a/portality/models/editors.py b/portality/models/editors.py index 8b935553cb..8e91b4b964 100644 --- a/portality/models/editors.py +++ b/portality/models/editors.py @@ -1,6 +1,12 @@ -from portality.dao import DomainObject, ScrollInitialiseException +import sys +from typing import List, Optional + +from portality.dao import DomainObject, ScrollInitialiseException, IdTextTermQuery from portality.models import Account +if sys.version_info >= (3, 11): + from typing import Self + class EditorGroup(DomainObject): """ @@ -9,7 +15,7 @@ class EditorGroup(DomainObject): __type__ = "editor_group" @classmethod - def group_exists_by_name(cls, name): + def group_exists_by_name(cls, name) -> Optional[str]: q = EditorGroupQuery(name) res = cls.query(q=q.query()) ids = [hit.get("_source", {}).get("id") for hit in res.get("hits", {}).get("hits", []) if "_source" in hit] @@ -19,7 +25,7 @@ def group_exists_by_name(cls, name): return ids[0] @classmethod - def _groups_by_x(cls, **kwargs): + def _groups_by_x(cls, **kwargs) -> List['Self']: """ Generalised editor groups by maned / editor / associate """ # ~~-> EditorGroupMember:Query~~ q = EditorGroupMemberQuery(**kwargs) @@ -30,17 +36,22 @@ def _groups_by_x(cls, **kwargs): return [] @classmethod - def groups_by_maned(cls, maned): + def groups_by_maned(cls, maned) -> List['Self']: return cls._groups_by_x(maned=maned) @classmethod - def groups_by_editor(cls, editor): + def groups_by_editor(cls, editor) -> List['Self']: return cls._groups_by_x(editor=editor) @classmethod - def groups_by_associate(cls, associate): + def groups_by_associate(cls, associate) -> List['Self']: return cls._groups_by_x(associate=associate) + @classmethod + def find_name_by_id(cls, editor_group_id) -> Optional[str]: + return cls.get_target_value(IdTextTermQuery('id', editor_group_id).query(), + id_name=editor_group_id, field_name="name") + @property def name(self): return self.data.get("name") @@ -94,6 +105,11 @@ def is_member(self, account_name): all_eds = self.associates + [self.editor] return account_name in all_eds + @classmethod + def find_editor_role_by_id(cls, editor_group_id, user_id) -> str: + eg = cls.pull(editor_group_id) + return "editor" if eg is not None and eg.editor == user_id else "associate_editor" + class EditorGroupQuery(object): def __init__(self, name): @@ -101,7 +117,7 @@ def __init__(self, name): def query(self): q = { - "track_total_hits" : True, + "track_total_hits": True, "query": {"term": {"name.exact": self.name}} } return q @@ -111,6 +127,7 @@ class EditorGroupMemberQuery(object): """ ~~EditorGroupMember:Query->Elasticsearch:Technology~~ """ + def __init__(self, editor=None, associate=None, maned=None): self.editor = editor self.associate = associate @@ -120,7 +137,7 @@ def query(self): q = { "track_total_hits": True, "query": {"bool": {"should": []}}, - "sort": {"name.exact": {"order" : "asc"}} + "sort": {"name.exact": {"order": "asc"}} } if self.editor is not None: et = {"term": {"editor.exact": self.editor}} @@ -129,6 +146,6 @@ def query(self): at = {"term": {"associates.exact": self.associate}} q["query"]["bool"]["should"].append(at) if self.maned is not None: - mt = {"term" : {"maned.exact" : self.maned}} + mt = {"term": {"maned.exact": self.maned}} q["query"]["bool"]["should"].append(mt) return q diff --git a/portality/models/v2/application.py b/portality/models/v2/application.py index c8f42d5c4e..877df6c729 100644 --- a/portality/models/v2/application.py +++ b/portality/models/v2/application.py @@ -95,7 +95,7 @@ def find_all_by_related_journal(cls, journal_id): @classmethod def assignment_to_editor_groups(cls, egs): - q = AssignedEditorGroupsQuery([eg.name for eg in egs]) + q = AssignedEditorGroupsQuery([eg.id for eg in egs]) res = cls.query(q.query()) buckets = res.get("aggregations", {}).get("editor_groups", {}).get("buckets", []) assignments = {} diff --git a/portality/models/v2/journal.py b/portality/models/v2/journal.py index f97f8084f2..8ed18e3b87 100644 --- a/portality/models/v2/journal.py +++ b/portality/models/v2/journal.py @@ -6,8 +6,10 @@ from datetime import datetime, timedelta from typing import Callable, Iterable +from elasticsearch import helpers from unidecode import unidecode +from portality import dao from portality.core import app from portality.dao import DomainObject from portality.lib import es_data_mapping, dates, coerce @@ -50,7 +52,7 @@ "index": { "fields": { "publisher_ac": {"coerce": "unicode"}, - "institution_ac": {"coerce": "unicode"} + "institution_ac": {"coerce": "unicode"}, } } } @@ -129,6 +131,41 @@ def recent(cls, max=10): records = [cls(**r.get("_source")) for r in result.get("hits", {}).get("hits", [])] return records + @classmethod + def renew_editor_group_name(cls, + editor_group_id: str, + new_name: str, + batch_size: int = 10000, + ): + """ + Renew editor_group_name in the index. + for ALL documents with related editor_group_id. + """ + + query = ByEditorGroupIdQuery(editor_group_id).query() + query['_source'] = ['id'] + + def _to_action(_id): + return { + "_op_type": "update", + "_index": cls.index_name(), + "_id": _id, + "script": { + "source": "ctx._source.index.editor_group_name = params.new_value", + "lang": "painless", + "params": { + "new_value": new_name + } + } + } + + ids = [j['id'] for j in cls.iterate(q=query, wrap=False, page_size=batch_size, keepalive='5m')] + app.logger.info(f"Found {len(ids)} {cls.__name__} with editor_group_id: {editor_group_id}") + + id_batches = (ids[i:i + batch_size] for i in range(0, len(ids), batch_size)) + for _sub_ids in id_batches: + helpers.bulk(dao.ES, [_to_action(_id) for _id in _sub_ids]) + ############################################ ## base property methods @@ -232,8 +269,23 @@ def editor_group(self): def set_editor_group(self, eg): self.__seamless__.set_with_struct("admin.editor_group", eg) + def remove_editor_group(self): self.__seamless__.delete("admin.editor_group") + self.__seamless__.delete("index.editor_group_name") + + def editor_group_name(self, index=True, default_id=False) -> str | None: + name = None + if index: + name = self.__seamless__.get_single("index.editor_group_name") + elif self.editor_group: + from portality.models import EditorGroup + name = EditorGroup.find_name_by_id(self.editor_group) + + if default_id: + name = name or self.editor_group + + return name @property def editor(self): @@ -305,7 +357,8 @@ def ordered_notes(self): clusters = {} for note in notes: if "date" not in note: - note["date"] = DEFAULT_TIMESTAMP_VAL # this really means something is broken with note date setting, which needs to be fixed + # this really means something is broken with note date setting, which needs to be fixed + note["date"] = DEFAULT_TIMESTAMP_VAL if note["date"] not in clusters: clusters[note["date"]] = [note] else: @@ -455,6 +508,8 @@ def _generate_index(self): if self.editor is not None: has_editor = "Yes" + editor_group_name = self.editor_group_name(index=False) + # build the index part of the object index = {} @@ -489,6 +544,8 @@ def _generate_index(self): index["schema_code"] = schema_codes if len(schema_codes_tree) > 0: index["schema_codes_tree"] = schema_codes_tree + if editor_group_name: + index["editor_group_name"] = editor_group_name self.__seamless__.set_with_struct("index", index) @@ -1185,3 +1242,22 @@ def query(self): {"created_date": {"order": "desc"}} ] } + + +class ByEditorGroupIdQuery: + + def __init__(self, editor_group_id): + self.editor_group_id = editor_group_id + + def query(self): + return { + 'query': { + 'bool': { + 'filter': { + 'term': { + 'admin.editor_group.exact': self.editor_group_id + } + } + } + }, + } diff --git a/portality/models/v2/shared_structs.py b/portality/models/v2/shared_structs.py index 6c2c031af1..71f55c73b4 100644 --- a/portality/models/v2/shared_structs.py +++ b/portality/models/v2/shared_structs.py @@ -221,7 +221,8 @@ "asciiunpunctitle" : {"coerce" : "unicode"}, "continued" : {"coerce" : "unicode"}, "has_editor_group" : {"coerce" : "unicode"}, - "has_editor" : {"coerce" : "unicode"} + "has_editor" : {"coerce" : "unicode"}, + "editor_group_name" : {'coerce' : 'unicode'}, }, "lists" : { "issn" : {"contains" : "field", "coerce" : "unicode"}, diff --git a/portality/notifications/application_emails.py b/portality/notifications/application_emails.py index 2df06ae8b1..b87decb536 100644 --- a/portality/notifications/application_emails.py +++ b/portality/notifications/application_emails.py @@ -5,8 +5,6 @@ from portality import models, app_email, constants from portality.core import app from portality.dao import Facetview2 -from portality.ui.messages import Messages -from portality.lib import dates from portality.ui import templates @@ -19,9 +17,7 @@ def send_editor_completed_email(application): url_for_application = url_root + url_for("editor.group_suggestions", source=string_id_query) # This is to the editor in charge of this application's assigned editor group - editor_group_name = application.editor_group - editor_group_id = models.EditorGroup.group_exists_by_name(name=editor_group_name) - editor_group = models.EditorGroup.pull(editor_group_id) + editor_group = models.EditorGroup.pull(application.editor_group) editor_acc = editor_group.get_editor_account() editor_id = editor_acc.id diff --git a/portality/scripts/accounts_with_marketing_consent.py b/portality/scripts/accounts_with_marketing_consent.py index e45976e099..9734b7847e 100644 --- a/portality/scripts/accounts_with_marketing_consent.py +++ b/portality/scripts/accounts_with_marketing_consent.py @@ -20,7 +20,7 @@ HEADERS = ["ID", "Name", "Email", "Created", "Last Updated", "Updated Since Create?"] -def output_map(acc): +def output_map(acc: Account): updated_since_create = acc.created_timestamp < acc.last_updated_timestamp return { diff --git a/portality/scripts/update_mapping_for_exact.py b/portality/scripts/update_mapping_for_exact.py new file mode 100644 index 0000000000..34f8ba9eb3 --- /dev/null +++ b/portality/scripts/update_mapping_for_exact.py @@ -0,0 +1,44 @@ +import argparse + +from portality import core +from portality.core import es_connection + + +def main(): + """ + Update mapping for exact field + """ + + parser = argparse.ArgumentParser(description='Update mapping for exact field') + parser.add_argument('index_type', help='Index name') + parser.add_argument('field_name', help='Field name (e.g. index.editor_group_name)') + + args = parser.parse_args() + + index = core.get_doaj_index_name(args.index_type) + field_name = args.field_name + exact_mapping = { + "exact": { + "type": "keyword", + "store": True + } + } + + print(f'Updating mapping for field: [{field_name}] in index: [{index}]') + + conn = es_connection + org_mapping = conn.indices.get_field_mapping(field_name, index=index) + org_field_mapping = org_mapping[index]['mappings'][field_name]['mapping'] + org_field_mapping = next(iter(org_field_mapping.values())) + org_field_mapping['fields'].update(**exact_mapping) + new_mapping = { + 'properties': { + field_name: org_field_mapping + } + } + print(new_mapping) + conn.indices.put_mapping(body=new_mapping, index=index) + + +if __name__ == '__main__': + main() diff --git a/portality/static/js/dashboard.js b/portality/static/js/dashboard.js index 4acc2ae0bd..2b5f698963 100644 --- a/portality/static/js/dashboard.js +++ b/portality/static/js/dashboard.js @@ -72,7 +72,7 @@ doaj.dashboard.renderGroupInfo = function(data) { let appQuerySource = doaj.searchQuerySource({ "term" : [ {"admin.editor.exact" : ed}, - {"admin.editor_group.exact" : data.editor_group.name}, + {"admin.editor_group.exact" : data.editor_group.id}, {"index.application_type.exact" : "new application"} // this is required so we only see open applications, not finished ones ], "sort": [{"admin.date_applied": {"order": "asc"}}] @@ -80,7 +80,7 @@ doaj.dashboard.renderGroupInfo = function(data) { // // ~~-> UpdateRequestsSearch:Page ~~ // let urQuerySource = doaj.searchQuerySource({"term" : [ // {"admin.editor.exact" : ed}, - // {"admin.editor_group.exact" : data.editor_group.name}, + // {"admin.editor_group.exact" : data.editor_group.id}, // {"index.application_type.exact" : "update request"} // this is required so we only see open update requests, not finished ones // ]}) let appCount = 0; @@ -110,9 +110,9 @@ doaj.dashboard.renderGroupInfo = function(data) { } // ~~-> ApplicationSearch:Page~~ - let appUnassignedSource = doaj.searchQuerySource({ + let appUnassignedSource = doaj.searchQuerySource({ "term" : [ - {"admin.editor_group.exact" : data.editor_group.name}, + {"admin.editor_group.exact" : data.editor_group.id}, {"index.has_editor.exact": "No"}, {"index.application_type.exact" : "new application"} // this is required so we only see open applications, not finished ones ], @@ -120,7 +120,7 @@ doaj.dashboard.renderGroupInfo = function(data) { }); // ~~-> UpdateRequestsSearch:Page ~~ // let urUnassignedSource = doaj.searchQuerySource({"term" : [ - // {"admin.editor_group.exact" : data.editor_group.name}, + // {"admin.editor_group.exact" : data.editor_group.id}, // {"index.has_editor.exact": "No"}, // {"index.application_type.exact" : "update request"} // this is required so we only see open update requests, not finished ones // ]}) @@ -138,7 +138,7 @@ doaj.dashboard.renderGroupInfo = function(data) { // ~~-> ApplicationSearch:Page~~ let appStatusSource = doaj.searchQuerySource({ "term": [ - {"admin.editor_group.exact": data.editor_group.name}, + {"admin.editor_group.exact": data.editor_group.id}, {"admin.application_status.exact": status}, {"index.application_type.exact": "new application"} // this is required so we only see open applications, not finished ones ], @@ -155,14 +155,14 @@ doaj.dashboard.renderGroupInfo = function(data) { // ~~-> ApplicationSearch:Page~~ let appGroupSource = doaj.searchQuerySource({ "term" : [ - {"admin.editor_group.exact" : data.editor_group.name}, + {"admin.editor_group.exact" : data.editor_group.id}, {"index.application_type.exact" : "new application"} // this is required so we only see open applications, not finished ones ], "sort": [{"admin.date_applied": {"order": "asc"}}] }); // ~~-> UpdateRequestsSearch:Page ~~ // let urGroupSource = doaj.searchQuerySource({ "term" : [ - // {"admin.editor_group.exact" : data.editor_group.name}, + // {"admin.editor_group.exact" : data.editor_group.id}, // {"index.application_type.exact" : "update request"} // this is required so we only see open applications, not finished ones // ]}) let frag = `
diff --git a/portality/static/js/doaj.fieldrender.edges.js b/portality/static/js/doaj.fieldrender.edges.js index dc3138e4f8..a056142d3b 100644 --- a/portality/static/js/doaj.fieldrender.edges.js +++ b/portality/static/js/doaj.fieldrender.edges.js @@ -125,7 +125,7 @@ $.extend(true, doaj, { return edges.newRefiningANDTermSelector({ id: "editor_group", category: "facet", - field: "admin.editor_group.exact", + field: "index.editor_group_name.exact", display: "Editor group", deactivateThreshold: 1, renderer: edges.bs3.newRefiningANDTermSelectorRenderer({ @@ -133,7 +133,7 @@ $.extend(true, doaj, { open: false, togglable: true, countFormat: doaj.valueMaps.countFormat, - hideInactive: true + hideInactive: true, }) }) }, diff --git a/portality/static/js/edges/admin.applications.edge.js b/portality/static/js/edges/admin.applications.edge.js index c43bf55d5e..a22cc45928 100644 --- a/portality/static/js/edges/admin.applications.edge.js +++ b/portality/static/js/edges/admin.applications.edge.js @@ -133,7 +133,7 @@ $.extend(true, doaj, { [ { "pre" : "Editor group: ", - "field" : "admin.editor_group" + "field" : "index.editor_group_name", } ], [ @@ -211,7 +211,7 @@ $.extend(true, doaj, { 'index.application_type.exact' : 'Application', 'index.has_editor_group.exact' : 'Editor group', 'index.has_editor.exact' : 'Associate Editor', - 'admin.editor_group.exact' : 'Editor group', + 'index.editor_group_name.exact' : 'Editor group', 'admin.editor.exact' : 'Editor', 'index.classification.exact' : 'Classification', 'index.language.exact' : 'Language', @@ -250,7 +250,7 @@ $.extend(true, doaj, { callbacks : { "edges:query-fail" : function() { alert("There was an unexpected error. Please reload the page and try again. If the issue persists please contact an administrator."); - } + }, } }); doaj.adminApplicationsSearch.activeEdges[selector] = e; diff --git a/portality/static/js/edges/admin.journals.edge.js b/portality/static/js/edges/admin.journals.edge.js index e612a2c7f5..e7323ac80f 100644 --- a/portality/static/js/edges/admin.journals.edge.js +++ b/portality/static/js/edges/admin.journals.edge.js @@ -132,7 +132,7 @@ $.extend(true, doaj, { edges.newRefiningANDTermSelector({ id: "editor_group", category: "facet", - field: "admin.editor_group.exact", + field: "index.editor_group_name.exact", display: "Editor group", deactivateThreshold: 1, renderer: edges.bs3.newRefiningANDTermSelectorRenderer({ @@ -140,7 +140,7 @@ $.extend(true, doaj, { open: false, togglable: true, countFormat: countFormat, - hideInactive: true + hideInactive: true, }) }), edges.newRefiningANDTermSelector({ @@ -376,7 +376,7 @@ $.extend(true, doaj, { [ { "pre" : "Editor group: ", - "field" : "admin.editor_group" + "field" : "index.editor_group_name", } ], [ @@ -474,7 +474,7 @@ $.extend(true, doaj, { "admin.owner.exact" : "Owner", "index.has_editor_group.exact" : "Editor group?", "index.has_editor.exact" : "Associate Editor?", - "admin.editor_group.exact" : "Editor group", + "index.editor_group_name.exact" : "Editor group", "admin.editor.exact" : "Associate Editor", "index.license.exact" : "License", "bibjson.publisher.name.exact" : "Publisher", @@ -510,7 +510,7 @@ $.extend(true, doaj, { callbacks : { "edges:query-fail" : function() { alert("There was an unexpected error. Please reload the page and try again. If the issue persists please contact an administrator."); - } + }, } }); doaj.adminJournalsSearch.activeEdges[selector] = e; diff --git a/portality/static/js/edges/admin.update_requests.edge.js b/portality/static/js/edges/admin.update_requests.edge.js index cea56910fe..932e9fcadf 100644 --- a/portality/static/js/edges/admin.update_requests.edge.js +++ b/portality/static/js/edges/admin.update_requests.edge.js @@ -144,7 +144,7 @@ $.extend(true, doaj, { [ { "pre" : "Editor Group: ", - "field" : "admin.editor_group" + "field" : "index.editor_group_name", } ], [ @@ -212,7 +212,7 @@ $.extend(true, doaj, { 'index.application_type.exact' : 'Update Request', 'index.has_editor_group.exact' : 'Editor Group?', 'index.has_editor.exact' : 'Associate Editor?', - 'admin.editor_group.exact' : 'Editor Group', + 'index.editor_group_name.exact' : 'Editor Group', 'admin.editor.exact' : 'Editor', 'index.classification.exact' : 'Classification', 'index.language.exact' : 'Language', @@ -229,7 +229,7 @@ $.extend(true, doaj, { "update request": "Open", "new application": "Open" } - } + }, }) ]; @@ -245,7 +245,7 @@ $.extend(true, doaj, { callbacks : { "edges:query-fail" : function() { alert("There was an unexpected error. Please reload the page and try again. If the issue persists please contact an administrator."); - } + }, } }); doaj.adminApplicationsSearch.activeEdges[selector] = e; diff --git a/portality/static/js/edges/editor.groupapplications.edge.js b/portality/static/js/edges/editor.groupapplications.edge.js index 5e40fd9e22..dbb6a2e1e3 100644 --- a/portality/static/js/edges/editor.groupapplications.edge.js +++ b/portality/static/js/edges/editor.groupapplications.edge.js @@ -75,7 +75,7 @@ $.extend(true, doaj, { edges.newRefiningANDTermSelector({ id: "editor_group", category: "facet", - field: "admin.editor_group.exact", + field: "index.editor_group_name.exact", display: "Editor Group", deactivateThreshold: 1, renderer: edges.bs3.newRefiningANDTermSelectorRenderer({ @@ -83,7 +83,7 @@ $.extend(true, doaj, { open: false, togglable: true, countFormat: countFormat, - hideInactive: true + hideInactive: true, }) }), edges.newRefiningANDTermSelector({ @@ -293,7 +293,7 @@ $.extend(true, doaj, { [ { "pre" : "Editor Group: ", - "field" : "admin.editor_group" + "field" : "index.editor_group_name", } ], [ @@ -373,7 +373,7 @@ $.extend(true, doaj, { 'admin.application_status.exact': 'Application Status', 'index.application_type.exact' : 'Record type', 'index.has_editor.exact' : 'Has Associate Editor?', - 'admin.editor_group.exact' : 'Editor Group', + 'index.editor_group_name.exact' : 'Editor Group', 'admin.editor.exact' : 'Editor', 'index.classification.exact' : 'Classification', 'index.language.exact' : 'Journal language', @@ -382,7 +382,7 @@ $.extend(true, doaj, { 'bibjson.publisher.name.exact' : 'Publisher', 'index.license.exact' : 'Journal license', "index.has_apc.exact" : "Publication charges?" - } + }, }) ]; @@ -398,7 +398,7 @@ $.extend(true, doaj, { callbacks : { "edges:query-fail" : function() { alert("There was an unexpected error. Please reload the page and try again. If the issue persists please contact an administrator."); - } + }, } }); doaj.editorGroupApplicationsSearch.activeEdges[selector] = e; diff --git a/portality/static/js/edges/editor.groupjournals.edge.js b/portality/static/js/edges/editor.groupjournals.edge.js index 9c145ba018..85d1a98494 100644 --- a/portality/static/js/edges/editor.groupjournals.edge.js +++ b/portality/static/js/edges/editor.groupjournals.edge.js @@ -52,7 +52,7 @@ $.extend(true, doaj, { edges.newRefiningANDTermSelector({ id: "editor_group", category: "facet", - field: "admin.editor_group.exact", + field: "index.editor_group_name.exact", display: "Editor Group", deactivateThreshold: 1, renderer: edges.bs3.newRefiningANDTermSelectorRenderer({ @@ -60,7 +60,7 @@ $.extend(true, doaj, { open: false, togglable: true, countFormat: countFormat, - hideInactive: true + hideInactive: true, }) }), edges.newRefiningANDTermSelector({ @@ -274,7 +274,7 @@ $.extend(true, doaj, { [ { "pre" : "Editor Group: ", - "field" : "admin.editor_group" + "field" : "index.editor_group_name", } ], [ @@ -365,7 +365,7 @@ $.extend(true, doaj, { "admin.in_doaj" : "In DOAJ?", "admin.owner.exact" : "Owner", "index.has_editor.exact" : "Associate Editor?", - "admin.editor_group.exact" : "Editor group", + "index.editor_group_name.exact" : "Editor group", "admin.editor.exact" : "Associate Editor", "index.license.exact" : "License", "bibjson.publisher.name.exact" : "Publisher", @@ -381,7 +381,7 @@ $.extend(true, doaj, { true : "True", false : "False" } - } + }, }) ]; @@ -397,7 +397,7 @@ $.extend(true, doaj, { callbacks : { "edges:query-fail" : function() { alert("There was an unexpected error. Please reload the page and try again. If the issue persists please contact an administrator."); - } + }, } }); doaj.editorGroupJournalsSearch.activeEdges[selector] = e; diff --git a/portality/static/js/formulaic.js b/portality/static/js/formulaic.js index 0c5932c3ce..c318470932 100644 --- a/portality/static/js/formulaic.js +++ b/portality/static/js/formulaic.js @@ -2151,51 +2151,66 @@ var formulaic = { let mininput = this.params.min_input === undefined ? 3 : this.params.min_input; let include_input = this.params.include === undefined ? true : this.params.include; let allow_clear = this.params.allow_clear_input === undefined ? true : this.params.allow_clear_input; + const id_field = this.params.id_field; + let url = `${current_scheme}//${current_domain}/autocomplete/${doc_type}/${doc_field}`; + if (id_field) { + url += `/${id_field}`; + } let ajax = { - url: current_scheme + "//" + current_domain + "/autocomplete/" + doc_type + "/" + doc_field, + url: url, dataType: 'json', data: function (term, page) { - return { - q: term - }; + return { q: term }; }, results: function (data, page) { return { results: data["suggestions"] }; } }; - var csc = function(term) {return {"id":term, "text": term};}; var initSel = function (element, callback) { - var data = {id: element.val(), text: element.val()}; - callback(data); + function setIdText(id, text) { + callback({id: id, text: text || id}); + } + const eleVal = element.val(); + + if (!id_field || !eleVal) { + /* + no need to query text value if id_field is not provided (not id text mapping) + or if the element value is empty + */ + setIdText(eleVal, eleVal); + return; + } + + $.ajax({ + type : "GET", + data : {id : eleVal}, + dataType: "json", + url: `${current_scheme}//${current_domain}/autocomplete-text/${doc_type}/${doc_field}/${id_field}`, + success: function(resp) { + setIdText(eleVal, resp[eleVal]); + } + }) }; let selector = "[name='" + this.fieldDef.name + "']"; $(selector).on("focus", formulaic.widgets._select2_shift_focus); + let select2Param = { + minimumInputLength: mininput, + ajax: ajax, + initSelection : initSel, + allowClear: allow_clear, + width: 'resolve' + }; if (include_input) { // apply the create search choice - $(selector).select2({ - minimumInputLength: mininput, - ajax: ajax, - createSearchChoice: csc, - initSelection : initSel, - allowClear: allow_clear, - width: 'resolve' - }); - } else { - // go without the create search choice option - $(selector).select2({ - minimumInputLength: mininput, - ajax: ajax, - initSelection : initSel, - allowClear: allow_clear, - width: 'resolve' - }); + select2Param.createSearchChoice = (term) => ({"id": term, "text": term}); } + $(selector).select2(select2Param); $(selector).on("focus", formulaic.widgets._select2_shift_focus); }; diff --git a/portality/static/js/suggestions_and_journals.js b/portality/static/js/suggestions_and_journals.js deleted file mode 100644 index 723f695091..0000000000 --- a/portality/static/js/suggestions_and_journals.js +++ /dev/null @@ -1,415 +0,0 @@ -jQuery(document).ready(function($) { - - ////// functions for handling edit locks /////////////////////// - - function setLockTimeout() { - var ts = $("#lock_expires").attr("data-timestamp"); - var d = new Date(ts); - var hours = d.getHours(); - var minutes = d.getMinutes(); - if (String(minutes).length == 1) { minutes = "0" + minutes } - var formatted = hours + ":" + minutes; - $("#lock_expires").html(formatted) - } - setLockTimeout(); - - function unlock(params) { - var type = params.type; - var id = params.id; - - function success_callback(data) { - var newWindow = window.open('', '_self', ''); //open the current window - window.close(); - } - - function error_callback(jqXHR, textStatus, errorThrown) { - alert("error releasing lock: " + textStatus + " " + errorThrown) - } - - $.ajax({ - type: "POST", - url: "/service/unlock/" + type + "/" + id, - contentType: "application/json", - dataType: "json", - success : success_callback, - error: error_callback - }) - } - - $("#unlock").click(function(event) { - event.preventDefault(); - var id = $(this).attr("data-id"); - var type = $(this).attr("data-type"); - unlock({type : type, id : id}) - }); - - // NOTE: this does not play well with page reloads, so not using it - //$(window).unload(function() { - // var id = $("#unlock").attr("data-id") - // var type = $("#unlock").attr("data-type") - // unlock({type : type, id : id}) - //}); - - //////////////////////////////////////////////////// - - //////////////////////////////////////////////////// - // functions for diff table - - $(".show_hide_diff_table").click(function(event) { - event.preventDefault(); - - var state = $(this).attr("data-state"); - if (state == "hidden") { - $(".form_diff").slideDown(); - $(this).html("hide").attr("data-state", "shown"); - } else if (state === "shown") { - $(".form_diff").slideUp(); - $(this).html("show").attr("data-state", "hidden"); - } - }); - - /////////////////////////////////////////////////// - - /////////////////////////////////////////////////// - // quick reject - - function showCustomRejectNote() { - $("#custom_reject_reason").show(); - } - - function hideCustomRejectNote() { - $("#custom_reject_reason").hide(); - } - - $("#submit_quick_reject").on("click", function(event) { - if ($("#reject_reason").val() == "" && $("#additional_reject_information").val() == "") { - alert("When selecting 'Other' as a reason for rejection, you must provide additional information"); - event.preventDefault(); - } - }); - - /////////////////////////////////////////////////// - - - // define a new highlight function, letting us highlight any element - // on a page - // adapted from http://stackoverflow.com/a/11589350 - jQuery.fn.highlight = function(color, fade_time) { - // some defaults - var color = color || "#F68B1F"; // the DOAJ color - var fade_time = fade_time || 1500; // milliseconds - - $(this).each(function() { - var el = $(this); - el.before("
"); - el.prev() - .width(el.width()) - .height(el.height()) - .css({ - "position": "absolute", - "background-color": color, - "opacity": ".9" - }) - .fadeOut(fade_time); - }); - }; - - // animated scrolling to an anchor - jQuery.fn.anchorAnimate = function(settings) { - - settings = jQuery.extend({ - speed : 700 - }, settings); - - return this.each(function(){ - var caller = this; - $(caller).click(function (event) { - event.preventDefault(); - var locationHref = window.location.href; - var elementClick = $(caller).attr("href"); - - var destination = $(elementClick).offset().top; - - $("html:not(:animated),body:not(:animated)").animate({ scrollTop: destination}, settings.speed, function() { - window.location.hash = elementClick; // ... but it also needs to be getting set here for the animation itself to work - }); - - setTimeout(function(){ - highlight_target(); - }, settings.speed + 50); - - return false; - }) - }) - }; - - toggle_optional_field('waiver_policy', ['#waiver_policy_url']); - toggle_optional_field('download_statistics', ['#download_statistics_url']); - toggle_optional_field('plagiarism_screening', ['#plagiarism_screening_url']); - toggle_optional_field('publishing_rights', ['#publishing_rights_url'], ["True"]); - toggle_optional_field('copyright', ['#copyright_url'], ["True"]); - toggle_optional_field('license_embedded', ['#license_embedded_url']); - toggle_optional_field('processing_charges', ['#processing_charges_amount', '#processing_charges_currency']); - toggle_optional_field('submission_charges', ['#submission_charges_amount', '#submission_charges_currency']); - toggle_optional_field('license', ['#license_checkbox'], ["Other"]); - - $('#country').select2({allowClear: true}); - $('#processing_charges_currency').select2({allowClear: true}); - $('#submission_charges_currency').select2({allowClear: true}); - - $("#keywords").select2({ - minimumInputLength: 1, - tags: [], - tokenSeparators: [","], - maximumSelectionSize: 6 - }); - - $("#languages").select2({allowClear: true}); - - autocomplete('#publisher', 'bibjson.publisher.name'); - autocomplete('#society_institution', 'bibjson.institution.name'); - autocomplete('#owner', 'id', 'account'); - autocomplete('#editor_group', 'name', 'editor_group', 1, false); - - exclusive_checkbox('digital_archiving_policy', 'No policy in place'); - exclusive_checkbox('article_identifiers', 'None'); - exclusive_checkbox('deposit_policy', 'None'); - - if ("onhashchange" in window) { - window.onhashchange = highlight_target(); - $('a.animated').anchorAnimate(); - } - - setup_subject_tree(); - if (typeof(notes_editable) !== 'undefined' && notes_editable === false) { // set by template - $('#add_note_btn').hide() - } else { - setup_remove_buttons(); - setup_add_buttons(); - } - - $("#editor_group").change(function(event) { - event.preventDefault(); - $("#editor").html("") - }); - - $(".application_journal_form").on("submit", function(event) { - $(".save-record").attr("disabled", "disabled"); - }); -}); - -function setup_subject_tree() { - $(function () { - $('#subject_tree').jstree({ - 'plugins':["checkbox","sort","search"], - 'core' : { - 'data' : lcc_jstree - }, - "checkbox" : { - "three_state" : false - }, - "search" : { - "fuzzy" : false, - "show_only_matches" : true - } - }); - }); - - $('#subject_tree') - .on('ready.jstree', function (e, data) { - var subjects = $('#subject').val() || []; - for (var i = 0; i < subjects.length; i++) { - $('#subject_tree').jstree('select_node', subjects[i]); - } - }); - - $('#subject_tree') - .on('changed.jstree', function (e, data) { - var subjects = $('#subject').val(data.selected); - }); - - - $('#subject_tree_container').prepend('
\ - \ -
\ - \ -

Selecting a subject will not automatically select its sub-categories.

\ -
\ -
'); - - var to = false; - $('#subject_tree_search').keyup(function () { - if(to) { clearTimeout(to); } - to = setTimeout(function () { - var v = $('#subject_tree_search').val(); - $('#subject_tree').jstree(true).search(v); - }, 750); - }); - - // Previously the superseded container was completely hidden, but we want to show its errors, so hide its children. - //$('#subject-container').hide(); - $('#subject-container').find("label").hide(); - $('#subject-container').find("#subject").hide(); - $('#subject-container').css('margin-bottom', 0); -} - -function exclusive_checkbox(field_name, exclusive_val) { - var doit = function() { - if (this.checked) { - $('#' + field_name + ' :checkbox:not([value="' + exclusive_val + '"])').prop('disabled', true); - $('#' + field_name + ' .extra_input_field').prop('disabled', true); - } else { - $('#' + field_name + ' :checkbox:not([value="' + exclusive_val + '"])').prop('disabled', false); - $('#' + field_name + ' .extra_input_field').prop('disabled', false); - } - }; - - $('#' + field_name + ' :checkbox[value="' + exclusive_val + '"]').each(doit); // on page load too - $('#' + field_name + ' :checkbox[value="' + exclusive_val + '"]').change(doit); // when exclusive checkbox ticked -} - -function highlight_target() { - $(window.location.hash).highlight() -} - -function setup_add_buttons() { - var customisations = { - // by container element id - container of the add more button and the fields being added - 'notes-outer-container': {'value': 'Add new note', 'id': 'add_note_btn'} - }; - - $('.addable-field-container').each(function() { - var e = $(this); - var id = e.attr('id'); - var value = customisations[id]['value'] || 'Add'; - - var thebtn = '\ -
\ -
'; - - cur_number_of_notes += 1; // this doesn't get decremented in the remove button because there's no point, WTForms will understand it - // even if the ID-s go 0, 2, 7, 13 etc. - $(this).after(thefield); - if (notes_deletable) { - setup_remove_buttons(); - } - }; - - var button_handlers = { - // by button element id - 'add_note_btn': add_note_btn - }; - for (var button_id in button_handlers) { - $('#' + button_id).unbind('click'); - $('#' + button_id).click(button_handlers[button_id]); - } -} - -function setup_remove_buttons() { - $('.deletable').each(function() { - var e = $(this); - var id = e.attr('id'); - if(e.find('button[id="remove_'+id+'"]').length == 0) { - e.append('
'); - } - setup_remove_button_handler(); - }); -} - -function setup_remove_button_handler() { - $(".remove_button").unbind("click"); - $(".remove_button").click( function(event) { - event.preventDefault(); - var toremove = $(this).attr('target'); - $('#' + toremove).remove(); - }); -} - -/* -Permits Managing Editors to assign editors on the Application and Journal edit forms. - */ -function load_eds_in_group(ed_query_url) { - var ed_group_name = $("#s2id_editor_group").find('span').text(); - $.ajax({ - type : "GET", - data : {egn : ed_group_name}, - dataType: "json", - url: ed_query_url, - success: function(resp) - { - // Get the options for the drop-down from our ajax request - var assoc_options = []; - if (resp != null) - { - assoc_options = [["", "Choose an editor"]]; - - for (var i=0; i").attr("value", assoc_options[j][0]).text(assoc_options[j][1]) - ); - } - } - }) -} diff --git a/portality/static/vendor/edges b/portality/static/vendor/edges index 990f422016..3fd1c4602d 160000 --- a/portality/static/vendor/edges +++ b/portality/static/vendor/edges @@ -1 +1 @@ -Subproject commit 990f4220163a3e18880f0bdc3ad5c80d234d22dd +Subproject commit 3fd1c4602de3214c0958c311ed11460b7276f290 diff --git a/portality/tasks/anon_export.py b/portality/tasks/anon_export.py index 234fc7727f..bb671ceea7 100644 --- a/portality/tasks/anon_export.py +++ b/portality/tasks/anon_export.py @@ -138,7 +138,8 @@ def run_anon_export(tmpStore, mainStore, container, clean=False, limit=None, bat out_rollover_fn = functools.partial(_copy_on_complete, logger_fn=logger_fn, tmpStore=tmpStore, mainStore=mainStore, container=container) _ = model.dump(q=iter_q, limit=limit, transform=transform, out_template=output_file, out_batch_sizes=batch_size, - out_rollover_callback=out_rollover_fn, es_bulk_fields=["_id"], scroll_keepalive=app.config.get('TASKS_ANON_EXPORT_SCROLL_TIMEOUT', '5m')) + out_rollover_callback=out_rollover_fn, es_bulk_fields=["_id"], + scroll_keepalive=app.config.get('TASKS_ANON_EXPORT_SCROLL_TIMEOUT', '5m')) logger_fn((dates.now_str() + " done\n")) @@ -187,12 +188,11 @@ def submit(cls, background_job): @huey_helper.register_schedule def scheduled_anon_export(): - background_helper.submit_by_bg_task_type(AnonExportBackgroundTask, - clean=app.config.get("TASKS_ANON_EXPORT_CLEAN", False), - limit=app.config.get("TASKS_ANON_EXPORT_LIMIT", None), - batch_size=app.config.get("TASKS_ANON_EXPORT_BATCH_SIZE", 100000)) + huey_helper.scheduled_common(clean=app.config.get("TASKS_ANON_EXPORT_CLEAN", False), + limit=app.config.get("TASKS_ANON_EXPORT_LIMIT", None), + batch_size=app.config.get("TASKS_ANON_EXPORT_BATCH_SIZE", 100000)) @huey_helper.register_execute(is_load_config=False) def anon_export(job_id): - background_helper.execute_by_job_id(job_id, AnonExportBackgroundTask) + huey_helper.execute_common(job_id) diff --git a/portality/tasks/article_bulk_create.py b/portality/tasks/article_bulk_create.py index 3066ecaefe..13dbf25b20 100644 --- a/portality/tasks/article_bulk_create.py +++ b/portality/tasks/article_bulk_create.py @@ -83,4 +83,4 @@ def submit(cls, background_job): @huey_helper.register_execute(is_load_config=False) def article_bulk_create(job_id): - background_helper.execute_by_job_id(job_id, ArticleBulkCreateBackgroundTask) \ No newline at end of file + huey_helper.execute_common(job_id) \ No newline at end of file diff --git a/portality/tasks/async_workflow_notifications.py b/portality/tasks/async_workflow_notifications.py index 70de3eb70e..6bf020e0db 100644 --- a/portality/tasks/async_workflow_notifications.py +++ b/portality/tasks/async_workflow_notifications.py @@ -234,21 +234,15 @@ def editor_notifications(emails_dict, limit=None): status_filters = [Facetview2.make_term_filter(term, status) for status in relevant_statuses] # First note - how many applications in editor's group have no associate editor assigned. - ed_app_query = EdAppQuery(status_filters) - ed_url = app.config.get("BASE_URL") + "/editor/group_applications" # Query for editor groups which have items in the required statuses, count their numbers - es = models.Suggestion.query(q=ed_app_query.query()) - group_stats = [(bucket.get("key"), bucket.get("doc_count")) for bucket in es.get("aggregations", {}).get("ed_group_counts", {}).get("buckets", [])] - - if limit is not None and isinstance(limit, int): - group_stats = group_stats[:limit] + group_stats = find_group_stats(EdAppQuery(status_filters).query(), limit) # Get the email addresses for the editor in charge of each group, Add the template to their email - for (group_name, group_count) in group_stats: + for (group_id, group_count) in group_stats: # get editor group object by name - eg = models.EditorGroup.pull_by_key("name", group_name) + eg = models.EditorGroup.pull(group_id) if eg is None: continue @@ -256,7 +250,7 @@ def editor_notifications(emails_dict, limit=None): editor = eg.get_editor_account() ed_email = editor.email - text = render_template(templates.EMAIL_WF_EDITOR_GROUPCOUNT, num=group_count, ed_group=group_name, url=ed_url) + text = render_template(templates.EMAIL_WF_EDITOR_GROUPCOUNT, num=group_count, ed_group=eg.name, url=ed_url) _add_email_paragraph(emails_dict, ed_email, eg.editor, text) # Second note - records within editor group not touched for so long @@ -264,22 +258,16 @@ def editor_notifications(emails_dict, limit=None): newest_date = dates.now() - timedelta(weeks=X_WEEKS) newest_date_stamp = newest_date.strftime(FMT_DATETIME_STD) - ed_age_query = EdAgeQuery(newest_date_stamp, status_filters) - ed_fv_prefix = app.config.get('BASE_URL') + "/editor/group_applications?source=" fv_age = Facetview2.make_query(sort_parameter="last_manual_update") ed_age_url = ed_fv_prefix + Facetview2.url_encode_query(fv_age) - es = models.Suggestion.query(q=ed_age_query.query()) - group_stats = [(bucket.get("key"), bucket.get("doc_count")) for bucket in es.get("aggregations", {}).get("ed_group_counts", {}).get("buckets", [])] - - if limit is not None and isinstance(limit, int): - group_stats = group_stats[:limit] + group_stats = find_group_stats(EdAgeQuery(newest_date_stamp, status_filters).query(), limit) # Get the email addresses for the editor in charge of each group, Add the template to their email - for (group_name, group_count) in group_stats: + for (group_id, group_count) in group_stats: # get editor group object by name - eg = models.EditorGroup.pull_by_key("name", group_name) + eg = models.EditorGroup.pull(group_id) if eg is None: continue @@ -287,10 +275,19 @@ def editor_notifications(emails_dict, limit=None): editor = eg.get_editor_account() ed_email = editor.email - text = render_template(templates.EMAIL_WF_EDITOR_AGE, num=group_count, ed_group=group_name, url=ed_age_url, x_weeks=X_WEEKS) + text = render_template(templates.EMAIL_WF_EDITOR_AGE, num=group_count, ed_group=eg.name, url=ed_age_url, x_weeks=X_WEEKS) _add_email_paragraph(emails_dict, ed_email, eg.editor, text) +def find_group_stats(ed_query, limit): + es = models.Suggestion.query(q=ed_query) + group_stats = [(bucket.get("key"), bucket.get("doc_count")) + for bucket in es.get("aggregations", {}).get("ed_group_counts", {}).get("buckets", [])] + if limit is not None and isinstance(limit, int): + group_stats = group_stats[:limit] + return group_stats + + def associate_editor_notifications(emails_dict, limit=None): """ Notify associates about two things: diff --git a/portality/tasks/journal_bulk_edit.py b/portality/tasks/journal_bulk_edit.py index 3e39d4388e..3f31842e57 100644 --- a/portality/tasks/journal_bulk_edit.py +++ b/portality/tasks/journal_bulk_edit.py @@ -109,7 +109,7 @@ def run(self): # FIXME: this is a bit of a stop-gap, pending a more substantial referential-integrity-like solution # if the editor group is not being changed, validate that the editor is actually in the editor group, # and if not, unset them - eg = models.EditorGroup.pull_by_key("name", j.editor_group) + eg = models.EditorGroup.pull(j.editor_group) if eg is not None: all_eds = eg.associates + [eg.editor] if j.editor not in all_eds: diff --git a/portality/templates-v2/management/base.html b/portality/templates-v2/management/base.html index bde4236224..d02e5eb54c 100644 --- a/portality/templates-v2/management/base.html +++ b/portality/templates-v2/management/base.html @@ -102,7 +102,7 @@

{% for eg in managed_groups|sort(attribute="name") %}
  • {% set app_source = search_query_source(term=[ - {"admin.editor_group.exact" : eg.name}, + {"admin.editor_group.exact" : eg.id}, {"index.application_type.exact" : "new application"} ], sort=[{"admin.date_applied": {"order": "asc"}}] ) %} @@ -122,11 +122,10 @@

    {% for eg in editor_of_groups|sort(attribute="name") %}
  • {% set app_source = search_query_source(term=[ - {"admin.editor_group.exact" : eg.name}, + {"admin.editor_group.exact" : eg.id}, {"index.application_type.exact" : "new application"} ], sort=[{"admin.date_applied": {"order": "asc"}}] ) %} - {{ eg.name }} {% set maned = eg.get_maned_account() %} {{ maned.id }} (Managing Editor) {{ " / " if not loop.last else "" }} diff --git a/portality/templates-v2/management/includes/_todo.html b/portality/templates-v2/management/includes/_todo.html index 14d29431b2..ea8090e441 100644 --- a/portality/templates-v2/management/includes/_todo.html +++ b/portality/templates-v2/management/includes/_todo.html @@ -97,7 +97,7 @@ "link" : url_for('editor.associate_suggestions') } } - %} +%}
    @@ -173,21 +173,27 @@

    - - - - - -

  • - {% endfor %} + + + + + +

  • + {% endfor %} \ No newline at end of file diff --git a/portality/view/admin.py b/portality/view/admin.py index 088c4513fc..bf291b2b63 100644 --- a/portality/view/admin.py +++ b/portality/view/admin.py @@ -563,8 +563,6 @@ def editor_group(group_id=None): eg = models.EditorGroup.pull(group_id) form.group_id.data = eg.id form.name.data = eg.name - # Do not allow the user to edit the name. issue #3859 - form.name.render_kw = {'disabled': True} form.maned.data = eg.maned form.editor.data = eg.editor form.associates.data = ",".join(eg.associates) @@ -620,14 +618,18 @@ def editor_group(group_id=None): ae.add_role("associate_editor") ae.save() - if eg.name is None: - eg.set_name(form.name.data) + is_name_changed = eg.name != form.name.data + eg.set_name(form.name.data) eg.set_maned(form.maned.data) eg.set_editor(form.editor.data) if associates is not None: eg.set_associates(associates) eg.save() + if is_name_changed: + models.Journal.renew_editor_group_name(eg.id, eg.name) + models.Application.renew_editor_group_name(eg.id, eg.name) + flash( "Group was updated - changes may not be reflected below immediately. Reload the page to see the update.", "success") @@ -656,7 +658,7 @@ def user_autocomplete(): @ssl_required def eg_associates_dropdown(): egn = request.values.get("egn") - eg = models.EditorGroup.pull_by_key("name", egn) + eg = models.EditorGroup.pull(egn) if eg is not None: editors = [eg.editor] @@ -722,7 +724,7 @@ def bulk_assign_editor_group(doaj_type): summary = task( selection_query=get_query_from_request(payload, doaj_type), - editor_group=payload['editor_group'], + editor_group=models.EditorGroup.group_exists_by_name(payload['editor_group']) or payload['editor_group'], dry_run=payload.get('dry_run', True) ) @@ -760,6 +762,11 @@ def bulk_edit_journal_metadata(): if not "metadata" in payload: raise BulkAdminEndpointException("key 'metadata' not present in request json") + # replace editor_group name with id + if "editor_group" in payload["metadata"]: + _eg = payload["metadata"]["editor_group"] + payload["metadata"]["editor_group"] = models.EditorGroup.group_exists_by_name(_eg) or _eg + formdata = MultiDict(payload["metadata"]) formulaic_context = JournalFormFactory.context("bulk_edit", extra_param=exparam_editing_user()) fc = formulaic_context.processor(formdata=formdata) diff --git a/portality/view/doaj.py b/portality/view/doaj.py index d4d775e846..fc3d5d5a18 100644 --- a/portality/view/doaj.py +++ b/portality/view/doaj.py @@ -1,3 +1,4 @@ +from __future__ import annotations import json import re import urllib.error @@ -13,6 +14,7 @@ from portality import models from portality import store from portality.core import app +from portality.dao import IdTextTermQuery from portality.decorators import ssl_required, api_key_required from portality.forms.application_forms import JournalFormFactory from portality.lcc import lcc_jstree @@ -218,21 +220,24 @@ def get_from_local_store(container, filename): return send_file(file_handle, mimetype="application/octet-stream", as_attachment=True, attachment_filename=filename) +def _no_suggestions_response(): + return jsonify({'suggestions': [{"id": "", "text": "No results found"}]}) + + @blueprint.route('/autocomplete//', methods=["GET", "POST"]) def autocomplete(doc_type, field_name): prefix = request.args.get('q', '') if not prefix: - return jsonify({'suggestions': [{"id": "", "text": "No results found"}]}) # select2 does not understand 400, which is the correct code here... + return _no_suggestions_response() # select2 does not understand 400, which is the correct code here... m = models.lookup_model(doc_type) if not m: - return jsonify({'suggestions': [{"id": "", "text": "No results found"}]}) # select2 does not understand 404, which is the correct code here... + return _no_suggestions_response() # select2 does not understand 404, which is the correct code here... size = request.args.get('size', 5) filter_field = app.config.get("AUTOCOMPLETE_ADVANCED_FIELD_MAPS", {}).get(field_name) - suggs = [] if filter_field is None: suggs = m.autocomplete(field_name, prefix, size=size) else: @@ -243,6 +248,77 @@ def autocomplete(doc_type, field_name): # http://flask.pocoo.org/docs/security/#json-security +@blueprint.route('/autocomplete///', methods=["GET", "POST"]) +def autocomplete_pair(doc_type, field_name, id_field): + r""" + different from autocomplete, in response json, the values of `id` and `text` are used different field + + Parameters: + `q` -- will be used to search record with value that start with `q` in `field_name` field + + Returns: + Json string with follow format: + {suggestions: [{id: id_field, text: field_name}]} + + `id` used value of `id_field` + `text` used value of `field_name` + """ + + prefix = request.args.get('q', '') + if not prefix: + return _no_suggestions_response() # select2 does not understand 400, which is the correct code here... + + m = models.lookup_model(doc_type) + if not m: + return _no_suggestions_response() # select2 does not understand 404, which is the correct code here... + + size = request.args.get('size', 5) + suggs = m.autocomplete_pair(field_name, prefix.lower(), id_field, field_name, size=size) + return jsonify({'suggestions': suggs}) + + +@blueprint.route('/autocomplete-text///', methods=["GET", "POST"]) +def autocomplete_text_mapping(doc_type, field_name, id_field): + """ + This route is used by the autocomplete widget to get the text value by id + + Json string with follow format: + {id_val: text_val} + """ + + id_value = request.args.get('id') + if id_value is None: + id_value = request.json.get('ids', []) + else: + id_value = id_value.strip() + id_map = id_text_mapping(doc_type, field_name, id_field, id_value) + return jsonify(id_map) + + +def id_text_mapping(doc_type, field_name, id_field, id_value) -> dict: + + query_factory_mapping = { + ('editor_group', 'id', 'name', ): lambda: IdTextTermQuery(id_field, id_value).query(), + } + query_factory = query_factory_mapping.get((doc_type, id_field, field_name)) + if not query_factory: + app.logger.warning(f"Unsupported id_text_mapping for " + f"doc_type[{doc_type}], field_name[{field_name}], id_field[{id_field}]") + return {} + + if not id_value: + return {} + + m = models.lookup_model(doc_type) + if not m: + app.logger.warning(f"model not found for doc_type[{doc_type}]") + return {} + + query = query_factory() + return m.get_target_values(query, id_field, field_name) + + + def find_toc_journal_by_identifier(identifier): if identifier is None: abort(404) diff --git a/portality/view/editor.py b/portality/view/editor.py index 05cb2c53b3..8b7d485036 100644 --- a/portality/view/editor.py +++ b/portality/view/editor.py @@ -138,8 +138,7 @@ def application(application_id): form_diff, current_journal = ApplicationFormXWalk.update_request_diff(ap) # Edit role is either associate_editor or editor, depending whether the user is group leader - eg = models.EditorGroup.pull_by_key("name", ap.editor_group) - role = 'editor' if eg is not None and eg.editor == current_user.id else 'associate_editor' + role = models.EditorGroup.find_editor_role_by_id(ap.editor_group, current_user.id) fc = ApplicationFormFactory.context(role, extra_param=exparam_editing_user()) if request.method == "GET":