diff --git a/cms/data/motivational_banners.yml b/cms/data/motivational_banners.yml
new file mode 100644
index 0000000000..e13d52bfe7
--- /dev/null
+++ b/cms/data/motivational_banners.yml
@@ -0,0 +1,14 @@
+banners:
+ zero_count:
+ - "You haven't completed any applications this year yet. Start by choosing one from
+ the list below."
+ positive_count:
+ - "Whoa! {{ COUNT }} applications already? You’re crushing it!"
+ - "{{ COUNT }} applications down already? You’re on fire! Keep it up, superstar!"
+ - "{{ COUNT }} applications down, many more to go! Keep rocking!"
+ - "{{ COUNT }} applications completed? You’re making it look easy!"
+ - "{{ COUNT }} applications down and still going strong!"
+ - "Wait, you’ve completed {{ COUNT }} applications this year already? You’re on fire!"
+ - "Impressive! You’ve already completed {{ COUNT }} applications this year!"
+ - "{{ COUNT }} applications in the bag this year! You’re smashing it!"
+ - "Congratulations, you have completed {{ COUNT }} applications this year so far! Keep it up!"
\ No newline at end of file
diff --git a/cms/sass/components/_alert.scss b/cms/sass/components/_alert.scss
index 47f841bdb5..84992cad36 100644
--- a/cms/sass/components/_alert.scss
+++ b/cms/sass/components/_alert.scss
@@ -26,6 +26,14 @@
border: 2px solid $grapefruit;
}
+ &--success {
+ border: 2px solid $dark-green;
+ }
+
+ &--info {
+ border: 2px solid $dark-grey;
+ }
+
code {
background-color: rgba($dark-grey, 0.1);
}
diff --git a/cms/sass/components/_motivational-banner.scss b/cms/sass/components/_motivational-banner.scss
new file mode 100644
index 0000000000..583a10021e
--- /dev/null
+++ b/cms/sass/components/_motivational-banner.scss
@@ -0,0 +1,13 @@
+.motivational-banner {
+ width: 100%;
+
+ h3 {
+ text-align: center;
+ margin-bottom: 0;
+
+ .tag {
+ margin: 0 $spacing-02;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/cms/sass/components/_progress-bar.scss b/cms/sass/components/_progress-bar.scss
index fb1e4823a8..c6aa9195bc 100644
--- a/cms/sass/components/_progress-bar.scss
+++ b/cms/sass/components/_progress-bar.scss
@@ -10,39 +10,10 @@
align-items: center;
padding: 0 $spacing-03;
height: 50px;
- text-decoration: none;
- color: $warm-black;
- // truncate text if it’s too long — user will need to hover to see full info
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ color: inherit;
+ text-decoration: inherit;
}
-}
-
-.progress-bar__bar--pending {
- background-color: $yellow;
-}
-
-.progress-bar__bar--in-progress {
- background-color: $light-green;
-}
-
-.progress-bar__bar--completed {
- background-color: $mid-green;
-
- .progress-bar__link {
- color: $white;
- }
-}
-
-.progress-bar__bar--ready {
- background-color: $dark-green;
-
- .progress-bar__link {
- color: $white;
- }
-}
-
-.progress-bar__bar--on-hold {
- background-color: $light-grey;
}
\ No newline at end of file
diff --git a/cms/sass/main.scss b/cms/sass/main.scss
index 0f72c8eac8..cfadfb6d72 100644
--- a/cms/sass/main.scss
+++ b/cms/sass/main.scss
@@ -43,6 +43,7 @@
"components/loading",
"components/logo",
"components/modal",
+ "components/motivational-banner",
"components/notifications",
"components/numbered-table",
"components/pager-buttons",
@@ -72,5 +73,6 @@
"themes/dashboard",
"themes/editorial-form",
- "themes/timeline"
+ "themes/timeline",
+ "themes/activity-section"
;
diff --git a/cms/sass/themes/_activity-section.scss b/cms/sass/themes/_activity-section.scss
new file mode 100644
index 0000000000..9cd3a26159
--- /dev/null
+++ b/cms/sass/themes/_activity-section.scss
@@ -0,0 +1,54 @@
+
+.activity-section {
+
+ .status {
+
+ text-decoration: none;
+
+ &--link {
+ &:hover,
+ &:focus,
+ &:active {
+ color: $grapefruit;
+ text-decoration: none;
+ }
+ }
+
+ &--pending {
+ background-color: $yellow;
+ color: $warm-black;
+ }
+
+ &--in-progress {
+ background-color: $light-green;
+ color: $warm-black;
+ }
+
+ &--completed {
+ background-color: $mid-green;
+ color: $warm-black;
+ }
+
+ &--ready {
+ background-color: $dark-green;
+ color: $white;
+ }
+
+ &--on-hold {
+ background-color: $light-grey;
+ color: $warm-black;
+ }
+ }
+
+ .color-legend {
+ font-size: 18px;
+
+ li:not(:first-child) {
+ margin-left: $spacing-01;
+ }
+
+ a {
+ padding: $spacing-02;
+ }
+ }
+}
\ No newline at end of file
diff --git a/cms/tours/application_by_status.yml b/cms/tours/application_by_status.yml
new file mode 100644
index 0000000000..88671b7504
--- /dev/null
+++ b/cms/tours/application_by_status.yml
@@ -0,0 +1,13 @@
+steps:
+ - selector: '#color-legend'
+ title: Colour legend links
+ content:
+
+ Each label in the colour legend now works as a link!
+ Clicking a label will take you to the search page, filtered by the currently viewed group
+ and the specific application status.
+
+
+ - selector: '#feature_tour_nav'
+ title: Want to see this tip again?
+ content: Take the tour again by selecting it from the Feature Tours menu.
\ No newline at end of file
diff --git a/doajtest/testbook/dashboard/editorial_group_status.yml b/doajtest/testbook/dashboard/editorial_group_status.yml
index 4cf4b49a7b..d1853a44d0 100644
--- a/doajtest/testbook/dashboard/editorial_group_status.yml
+++ b/doajtest/testbook/dashboard/editorial_group_status.yml
@@ -28,6 +28,13 @@ tests:
to this group.
- The number of search results is the same as shown on the dashboard
- step: Go back to the dashboard page
+ - step: Click on one of the statuses in the colour legend under "Open Applications" header (you may want to
+ right click to keep the dashboard tab open during this test)
+ results:
+ - You are taken to the application search which shows the open applications in
+ that status
+ - The number of search results is the same as shown on the dashboard
+ - step: Go back to the dashboard page
- step: Click on an editor's name
results:
- A mail window opens in your mail client
diff --git a/doajtest/testbook/dashboard/historical_stats.yml b/doajtest/testbook/dashboard/historical_stats.yml
new file mode 100644
index 0000000000..603aab4ee2
--- /dev/null
+++ b/doajtest/testbook/dashboard/historical_stats.yml
@@ -0,0 +1,94 @@
+suite: Dashboard
+testset: Historical Statistics
+tests:
+ - title: Historical Statistics Display - Managing Editor with 3 groups
+ context:
+ role: admin
+ testdrive: statistics
+ steps:
+ - step: Navigate to "/testdrive/statistics" and wait for the page to load slowly. This should be done once at the beginning of the test session and not refreshed between individual tests.
+ - step: Log in with provided managing_editor credentials.
+ results:
+ - User dashboard is displayed.
+ - step: Verify historical statistics in the Activity section at the bottom.
+ results:
+ - Three groups are displayed.
+ - Only associate editors are shown in the Activity section for each group.
+ - Each group has the correct statistics for the current year.
+ - The number of READY/COMPLETED applications matches the "additional information" table.
+ - step: Log out.
+ - step: (Optional) Click the link at the bottom of the testdrive page to free resources after finishing this test session.
+ - title: Historical Statistics Display - Editor with 2 groups
+ context:
+ role: editor
+ testdrive: statistics
+ steps:
+ - step: Navigate to "/testdrive/statistics" and wait for the page to load slowly. This should be done once at the beginning of the test session and not refreshed between individual tests.
+ - step: Log in with provided editor_1 credentials.
+ results:
+ - User dashboard is displayed.
+ - step: Verify the motivational banner at the top of the dashboard (green) and check number of finished applications.
+ results:
+ - The motivational banner is GREEN and shows the correct sum of applications finished in both groups.
+ - step: Verify historical statistics in the Activity section at the bottom.
+ results:
+ - Two groups are displayed.
+ - Only associate editors are shown in the Activity section for each group.
+ - Each group has the correct statistics for the current year.
+ - The number of READY/COMPLETED applications matches the "additional information" table.
+ - step: Log out.
+ - step: (Optional) Click the link at the bottom of the testdrive page to free resources after finishing this test session.
+ - title: Historical Statistics Display - Editor with 1 group
+ context:
+ role: editor
+ testdrive: statistics
+ steps:
+ - step: Navigate to "/testdrive/statistics" and wait for the page to load slowly. This should be done once at the beginning of the test session and not refreshed between individual tests.
+ - step: Log in with provided editor_2 credentials.
+ results:
+ - User dashboard is displayed.
+ - step: Verify the motivational banner at the top of the dashboard (green) and check number of finished applications.
+ results:
+ - The motivational banner is GREEN and shows the correct sum of applications finished in the group.
+ - step: Verify historical statistics in the Activity section at the bottom.
+ results:
+ - One group is displayed.
+ - Only associate editors are shown in the Activity section for the group.
+ - The group has the correct statistics for the current year.
+ - The number of READY/COMPLETED applications matches the "additional information" table.
+ - step: Log out.
+ - step: (Optional) Click the link at the bottom of the testdrive page to free resources after finishing this test session.
+ - title: Historical Statistics Display - Associate Editors with 0 finished applications
+ context:
+ role: associate editor
+ testdrive: statistics
+ steps:
+ - step: Navigate to "/testdrive/statistics" and wait for the page to load slowly. This should be done once at the beginning of the test session and not refreshed between individual tests.
+ - step: Log in with provided associate_editor_1 credentials.
+ results:
+ - User dashboard is displayed.
+ - step: Verify the motivational banner at the top of the dashboard (grey).
+ results:
+ - The motivational banner is GREY with appropriate information about no finished applications.
+ - step: Verify no statistics are displayed at the bottom of the page.
+ results:
+ - No statistics are displayed in the Activity section.
+ - step: Log out.
+ - step:
+ - title: Historical Statistics Display - Associate Editor 2
+ context:
+ role: associate editor
+ testdrive: statistics
+ steps:
+ - step: Navigate to "/testdrive/statistics" and wait for the page to load slowly. This should be done once at the beginning of the test session and not refreshed between individual tests.
+ - step: Log in with provided associate_editor_2 credentials.
+ results:
+ - User dashboard is displayed.
+ - step: Verify the motivational banner at the top of the dashboard (green).
+ results:
+ - The motivational banner is GREEN.
+ - step: Verify no statistics are displayed at the bottom of the page.
+ results:
+ - No statistics are displayed in the Activity section.
+ - step: Log out.
+ - step: (Optional) Click the link at the bottom of the testdrive page to free resources after finishing this test session.
diff --git a/doajtest/testdrive/statistics.py b/doajtest/testdrive/statistics.py
new file mode 100644
index 0000000000..bc78f440cf
--- /dev/null
+++ b/doajtest/testdrive/statistics.py
@@ -0,0 +1,201 @@
+from doajtest.testdrive.factory import TestDrive
+from portality import constants
+from portality import models
+from doajtest.fixtures import EditorGroupFixtureFactory
+import string
+import random
+import json
+
+class Statistics(TestDrive):
+ """
+ Creates a group structure for statistics testdrive:
+ 3 groups, each with the same Man Ed, 2 groups with the same editor, each with different number of AssEds
+ """
+
+ NUMBER_OF_GROUPS = 3
+ NUMBER_OF_EDITORS = 2
+ NUMBER_OF_ASSEDITORS = 8
+
+ FINISHED_APPLICATIONS = [
+ {'role': 'editor', 'group': 0, 'index': 0, 'count': 2},
+ {'role': 'editor', 'group': 1, 'index': 0, 'count': 3},
+ {'role': 'editor', 'group': 2, 'index': 1, 'count': 3},
+ {'role': 'assed', 'group': 0, 'index': 0, 'count': 0},
+ {'role': 'assed', 'group': 0, 'index': 1, 'count': 4},
+ {'role': 'assed', 'group': 1, 'index': 2, 'count': 0},
+ {'role': 'assed', 'group': 1, 'index': 3, 'count': 5},
+ {'role': 'assed', 'group': 1, 'index': 4, 'count': 3},
+ {'role': 'assed', 'group': 2, 'index': 5, 'count': 7},
+ {'role': 'assed', 'group': 2, 'index': 6, 'count': 4},
+ {'role': 'assed', 'group': 2, 'index': 7, 'count': 2},
+ ]
+
+ # Define editor groups and associate editors' associations
+ EDITOR_GROUPS = {
+ 'eg0': {'editor_index': 0, 'start_assed_index': 0, 'end_assed_index': 2},
+ 'eg1': {'editor_index': 0, 'start_assed_index': 2, 'end_assed_index': 5},
+ 'eg2': {'editor_index': 1, 'start_assed_index': 5, 'end_assed_index': 8},
+ }
+
+ def setup(self) -> dict:
+ self.createAccounts()
+ self.createGroups()
+ self.createProvenanceData()
+ return {
+ "admin": {
+ "username": self.admin,
+ "password": self.admin_pass
+ },
+ "editor_1": {
+ "username": self.editors[0]["id"],
+ "password": self.editors[0]["pass"]
+ },
+ "editor_2": {
+ "username": self.editors[1]["id"],
+ "password": self.editors[1]["pass"]
+ },
+ "associate_editor_1": {
+ "username": self.asseds[0]["id"],
+ "password": self.asseds[0]["pass"]
+ },
+ "associate_editor_2": {
+ "username": self.asseds[1]["id"],
+ "password": self.asseds[1]["pass"]
+ },
+ "finished_applications": self.finished_by_user,
+ "non_renderable": {
+ "asseds": [assed['id'] for assed in
+ self.asseds[2:]],
+ "groups": self.groups,
+ "provenance": self.provenance_data
+ }
+ }
+
+ def create_random_str(self, n_char=10):
+ s = string.ascii_letters + string.digits
+ return ''.join(random.choices(s, k=n_char))
+
+ def createAccounts(self):
+ un = self.create_random_str()
+ pw1 = self.create_random_str()
+ admin = models.Account.make_account(un + "@example.com", un, "Admin " + un, [constants.ROLE_ADMIN])
+ admin.set_password(pw1)
+ admin.save()
+ self.admin = admin.id
+ self.admin_pass = pw1
+
+ # Create editors accounts
+ self.editors = []
+ for i in range(self.NUMBER_OF_EDITORS):
+ us = self.create_random_str()
+ pw = self.create_random_str()
+ editor = models.Account.make_account(us + "@example.com", us, "Editor" + str(i+1) + " " + us,
+ [constants.ROLE_EDITOR])
+ editor.set_password(pw)
+ editor.save()
+ self.editors.append({"id": editor.id, "pass": pw})
+
+ # Create associate editors accounts
+ self.asseds = []
+ for i in range(self.NUMBER_OF_ASSEDITORS):
+ us = self.create_random_str()
+ pw = self.create_random_str()
+ assed = models.Account.make_account(us + "@example.com", us, "AssEd" + str(i+1) + " " + us,
+ [constants.ROLE_ASSOCIATE_EDITOR])
+ assed.set_password(pw)
+ assed.save()
+ self.asseds.append({"id": assed.id, "pass": pw})
+
+ def createGroups(self):
+
+ self.groups = []
+ for group_key, group_info in self.EDITOR_GROUPS.items():
+ eg_source = EditorGroupFixtureFactory.make_editor_group_source(group_name=group_key, maned=self.admin,
+ editor=
+ self.editors[group_info['editor_index']][
+ "id"])
+ del eg_source["associates"] # these will be added in a moment
+ editor_group = models.EditorGroup(**eg_source)
+ editor_group.set_id(group_key)
+ ids_to_set = [assed['id'] for assed in
+ self.asseds[group_info['start_assed_index']:group_info['end_assed_index']]]
+ editor_group.set_associates(ids_to_set)
+ editor_group.save()
+ self.groups.append(editor_group.id)
+
+ def createProvenanceData(self):
+
+ self.finished_by_user = {}
+ self.provenance_data = []
+ role_mapping = {
+ "editor": {
+ "array": self.editors,
+ "status": constants.APPLICATION_STATUS_READY
+ },
+ "assed": {
+ "array": self.asseds,
+ "status": constants.APPLICATION_STATUS_COMPLETED
+ }
+ }
+
+ for entry in self.FINISHED_APPLICATIONS:
+ role = entry["role"]
+ idx = entry['index']
+ count = entry['count']
+ group_key = "eg" + str(entry['group'])
+
+ users = role_mapping[role]["array"]
+ app_status = role_mapping[role]["status"]
+
+ status = "status:" + app_status
+ user_id = users[idx]["id"]
+ user_name_with_eg = user_id + " (" + role + " in " + group_key + ")"
+
+ self.finished_by_user[user_name_with_eg] = str(count)
+
+ for i in range(count):
+ p = self.add_provenance_record(status, role, user_id, group_key)
+ self.provenance_data.append(p.id)
+
+ def add_provenance_record(self, status, role, user, editor_group):
+ data = {
+ "user": user,
+ "roles": [role],
+ "type": "suggestion",
+ "action": status,
+ "editor_group": [editor_group]
+ }
+ p = models.Provenance(**data)
+ p.save()
+ print(p.id)
+ return p
+
+ def teardown(self, params):
+ for key, obj in params.items():
+ print(key)
+ if key != "non_renderable" and key != "finished_applications":
+ models.Account.remove_by_id(obj["username"])
+
+ non_renderable = params.get("non_renderable", {})
+
+ for assed in non_renderable.get("asseds", []):
+ models.Account.remove_by_id(assed)
+
+ for provenance_id in non_renderable.get("provenance", []):
+ models.Provenance.remove_by_id(provenance_id)
+
+ for group in non_renderable.get("groups", []):
+ models.Provenance.remove_by_id(group)
+
+ return {"success": True}
+
+
+if __name__ == "__main__":
+ structure = Statistics()
+ params = structure.setup()
+ print(structure.admin)
+ print(structure.editors)
+ print(structure.asseds)
+ print(structure.groups)
+ print(structure.provenance_data)
+ structure.teardown(params)
diff --git a/doajtest/unit/test_bll_todo_top_todo_maned.py b/doajtest/unit/test_bll_todo_top_todo_maned.py
index 1c5ae4457d..2c0dd07fc5 100644
--- a/doajtest/unit/test_bll_todo_top_todo_maned.py
+++ b/doajtest/unit/test_bll_todo_top_todo_maned.py
@@ -220,4 +220,60 @@ def build_application(self, id, lmu_diff, cd_diff, status, app_registry, additio
additional_fn(ap)
ap.save()
- app_registry.append(ap)
\ No newline at end of file
+ app_registry.append(ap)
+
+ def test_historical_count(self):
+ EDITOR_GROUP_SOURCE = EditorGroupFixtureFactory.make_editor_group_source()
+ eg = models.EditorGroup(**EDITOR_GROUP_SOURCE)
+ maned = models.Account(**AccountFixtureFactory.make_managing_editor_source())
+
+ EDITOR_SOURCE = AccountFixtureFactory.make_editor_source()
+ ASSED1_SOURCE = AccountFixtureFactory.make_assed1_source()
+ ASSED2_SOURCE = AccountFixtureFactory.make_assed2_source()
+ ASSED3_SOURCE = AccountFixtureFactory.make_assed3_source()
+ editor = models.Account(**EDITOR_SOURCE)
+ assed1 = models.Account(**ASSED1_SOURCE)
+ assed2 = models.Account(**ASSED2_SOURCE)
+ assed3 = models.Account(**ASSED3_SOURCE)
+ editor.save(blocking=True)
+ assed1.save(blocking=True)
+ assed2.save(blocking=True)
+ assed3.save(blocking=True)
+ eg.set_maned(maned.id)
+ eg.set_editor(editor.id)
+ eg.set_associates([assed1.id, assed2.id, assed3.id])
+ eg.save(blocking=True)
+
+ self.add_provenance_record("status:" + constants.APPLICATION_STATUS_READY, "editor", editor.id, eg)
+ self.add_provenance_record("status:" + constants.APPLICATION_STATUS_COMPLETED, "associate_editor", assed1.id, eg)
+ self.add_provenance_record("status:" + constants.APPLICATION_STATUS_COMPLETED, "associate_editor", assed2.id, eg)
+ self.add_provenance_record("status:" + constants.APPLICATION_STATUS_COMPLETED, "associate_editor", assed3.id, eg)
+
+ stats = self.svc.group_finished_historical_counts(eg)
+
+ self.assertEqual(stats["year"], dates.now_str(dates.FMT_YEAR))
+ self.assertEqual(stats["editor"]["id"], editor.id)
+ self.assertEqual(stats["editor"]["count"], 1)
+
+ associate_editors = [assed1.id, assed2.id, assed3.id]
+
+ for assed in stats["associate_editors"]:
+ self.assertTrue(assed["id"] in associate_editors)
+ self.assertEqual(assed["count"], 1)
+
+ editor_count = self.svc.user_finished_historical_counts(editor)
+ self.assertEqual(editor_count, 1)
+ assed_count = self.svc.user_finished_historical_counts(assed1)
+ self.assertEqual(assed_count, 1)
+
+
+ def add_provenance_record(self, status, role, user, editor_group):
+ data = {
+ "user": user,
+ "roles": [role],
+ "type": "suggestion",
+ "action": status,
+ "editor_group": [editor_group.id]
+ }
+ p1 = models.Provenance(**data)
+ p1.save(blocking=True)
\ No newline at end of file
diff --git a/portality/bll/services/todo.py b/portality/bll/services/todo.py
index 25a62a2c71..bf11ce6835 100644
--- a/portality/bll/services/todo.py
+++ b/portality/bll/services/todo.py
@@ -15,7 +15,7 @@ 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 ~~
stats["editors"] = {}
@@ -23,8 +23,8 @@ def group_stats(self, group_id):
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)
resp = models.Application.query(q=q.query())
@@ -62,8 +62,67 @@ def group_stats(self, group_id):
elif b["key"] == constants.APPLICATION_TYPE_UPDATE_REQUEST:
stats["by_status"][bucket["key"]]["update_requests"] = b["doc_count"]
+ stats["historical_numbers"] = self.group_finished_historical_counts(eg)
+
return stats
+ def group_finished_historical_counts(self, editor_group: models.EditorGroup, year=None):
+ """
+ Get the count of applications in an editor group where
+ Associate Editors set to Completed when they have done their review
+ Editors set them to Ready
+ in a given year (current by default)
+ :param editor_group
+ :param year
+ :return: historical for editor and associate editor in dict
+ """
+ year_for_query = dates.now_str(dates.FMT_YEAR) if year is None else year
+ editor_status = "status:" + constants.APPLICATION_STATUS_READY
+ associate_status = "status:" + constants.APPLICATION_STATUS_COMPLETED
+
+ stats = {"year": year_for_query}
+
+ hs = HistoricalNumbersQuery(editor_group.editor, editor_status, editor_group.id)
+ # ~~-> Provenance:Model ~~
+ editor_count = models.Provenance.count(query=hs.query())
+
+ # ~~-> Account:Model ~~
+ acc = models.Account.pull(editor_group.editor)
+ stats["editor"] = {"id": acc.id, "count": editor_count}
+
+ stats["associate_editors"] = []
+ for associate in editor_group.associates:
+ hs = HistoricalNumbersQuery(associate, associate_status, editor_group.id)
+ associate_count = models.Provenance.count(query=hs.query())
+ acc = models.Account.pull(associate)
+ stats["associate_editors"].append({"id": acc.id, "name": acc.name, "count": associate_count})
+
+ return stats
+
+ def user_finished_historical_counts(self, account, year=None):
+ """
+ Get the count of overall applications
+ Associate Editors set to Completed
+ Editors set them to Ready
+ in a given year (current by default)
+ :param account
+ :param year
+ :return:
+ """
+ hs = None
+
+ if account.has_role("editor"):
+ hs = HistoricalNumbersQuery(account.id, "status:" + constants.APPLICATION_STATUS_READY, year)
+ elif account.has_role("associate_editor"):
+ hs = HistoricalNumbersQuery(account.id, "status:" + constants.APPLICATION_STATUS_COMPLETED, year)
+
+ if hs:
+ count = models.Provenance.count(query=hs.query())
+ else:
+ count = None
+
+ return count
+
def top_todo(self, account, size=25, new_applications=True, update_requests=True, on_hold=True):
"""
Returns the top number of todo items for a given user
@@ -74,7 +133,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 = []
@@ -92,7 +151,7 @@ def top_todo(self, account, size=25, new_applications=True, update_requests=True
if on_hold:
queries.append(TodoRules.maned_on_hold(size, account.id, 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]
@@ -128,10 +187,10 @@ def top_todo(self, account, size=25, new_applications=True, update_requests=True
todos.append({
"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,
+ "action_id": [aid],
+ "title": ap.bibjson().title,
+ "object_id": ap.id,
+ "object": ap,
"boost": boost
})
@@ -399,7 +458,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=[
@@ -422,7 +481,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=[
@@ -486,7 +545,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
@@ -502,13 +561,16 @@ def __init__(self, musts=None, must_nots=None, ors=None, sort="last_manual_updat
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
}
if len(self._musts) > 0:
@@ -540,8 +602,8 @@ def is_update_request(cls):
@classmethod
def editor_group(cls, groups):
return {
- "terms" : {
- "admin.editor_group.exact" : [g.name for g in groups]
+ "terms": {
+ "admin.editor_group.exact": [g.name for g in groups]
}
}
@@ -568,16 +630,16 @@ 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
}
}
@@ -604,13 +666,14 @@ class GroupStatsQuery():
~~->$GroupStats:Query~~
~~^->Elasticsearch:Technology~~
"""
+
def __init__(self, group_name, editor_count=10):
self.group_name = group_name
self.editor_count = editor_count
def query(self):
return {
- "track_total_hits" : True,
+ "track_total_hits": True,
"query": {
"bool": {
"must": [
@@ -620,10 +683,10 @@ def query(self):
}
}
],
- "must_not" : [
+ "must_not": [
{
- "terms" : {
- "admin.application_status.exact" : [
+ "terms": {
+ "admin.application_status.exact": [
constants.APPLICATION_STATUS_ACCEPTED,
constants.APPLICATION_STATUS_REJECTED
]
@@ -632,26 +695,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": {
@@ -662,13 +725,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
}
@@ -676,4 +739,42 @@ def query(self):
}
}
}
- }
\ No newline at end of file
+ }
+
+
+class HistoricalNumbersQuery:
+ """
+ ~~->$HistoricalNumbers:Query~~
+ ~~^->Elasticsearch:Technology~~
+ """
+
+ def __init__(self, editor, application_status, editor_group=None, year=None):
+ self.editor_group = editor_group
+ self.editor = editor
+ self.application_status = application_status
+ self.year = year
+
+ def query(self):
+ if self.year is None:
+ date_range = {"gte": "now/y", "lte": "now"}
+ else:
+ date_range = {
+ "gte": f"{self.year}-01-01",
+ "lte": f"{self.year}-12-31"
+ }
+ must_terms = [{"range": {"last_updated": date_range}},
+ {"term": {"type": "suggestion"}},
+ {"term": {"user.exact": self.editor}},
+ {"term": {"action": self.application_status}}
+ ]
+
+ if self.editor_group:
+ must_terms.append({"term": {"editor_group": self.editor_group}})
+
+ return {
+ "query": {
+ "bool": {
+ "must": must_terms
+ }
+ }
+ }
diff --git a/portality/constants.py b/portality/constants.py
index 974ab3f49c..ba6edf42e6 100644
--- a/portality/constants.py
+++ b/portality/constants.py
@@ -82,7 +82,7 @@
PROCESS__QUICK_REJECT = "quick_reject"
-# Role
+# Roles
ROLE_ADMIN = "admin"
ROLE_PUBLISHER = "publisher"
ROLE_EDITOR = "editor"
diff --git a/portality/dao.py b/portality/dao.py
index 80635d714a..4a5e2e2e5f 100644
--- a/portality/dao.py
+++ b/portality/dao.py
@@ -855,8 +855,8 @@ def all(cls, size=10000, **kwargs):
return cls.q2obj(size=size, **kwargs)
@classmethod
- def count(cls):
- res = ES.count(index=cls.index_name(), doc_type=cls.doc_type())
+ def count(cls, query=None):
+ res = ES.count(index=cls.index_name(), doc_type=cls.doc_type(), body=query)
return res.get("count")
# return requests.get(cls.target() + '_count').json()['count']
diff --git a/portality/settings.py b/portality/settings.py
index e6bd2ae57e..2a5123df04 100644
--- a/portality/settings.py
+++ b/portality/settings.py
@@ -22,8 +22,8 @@
VALID_ENVIRONMENTS = ['dev', 'test', 'staging', 'production', 'harvester']
CMS_BUILD_ASSETS_ON_STARTUP = False
# Cookies security
-SESSION_COOKIE_SAMESITE='Strict'
-SESSION_COOKIE_SECURE=True
+SESSION_COOKIE_SAMESITE = 'Strict'
+SESSION_COOKIE_SECURE = True
REMEMBER_COOKIE_SECURE = True
####################################
@@ -51,7 +51,7 @@
# ~~->Elasticsearch:Technology
# elasticsearch settings # TODO: changing from single host / esprit to multi host on ES & correct the default
-ELASTIC_SEARCH_HOST = os.getenv('ELASTIC_SEARCH_HOST', 'http://localhost:9200') # remember the http:// or https://
+ELASTIC_SEARCH_HOST = os.getenv('ELASTIC_SEARCH_HOST', 'http://localhost:9200') # remember the http:// or https://
ELASTICSEARCH_HOSTS = [{'host': 'localhost', 'port': 9200}, {'host': 'localhost', 'port': 9201}]
ELASTIC_SEARCH_VERIFY_CERTS = True # Verify the SSL certificate of the ES host. Set to False in dev.cfg to avoid having to configure your local certificates
@@ -62,8 +62,8 @@
# e.g. host:port/type/doc/id
ELASTIC_SEARCH_INDEX_PER_TYPE = True
-INDEX_PER_TYPE_SUBSTITUTE = '_doc' # Migrated from esprit
-ELASTIC_SEARCH_DB_PREFIX = "doaj-" # note: include the separator
+INDEX_PER_TYPE_SUBSTITUTE = '_doc' # Migrated from esprit
+ELASTIC_SEARCH_DB_PREFIX = "doaj-" # note: include the separator
ELASTIC_SEARCH_TEST_DB_PREFIX = "doajtest-"
INITIALISE_INDEX = True # whether or not to try creating the index and required index types on startup
@@ -82,15 +82,15 @@
ENABLE_APM = False
ELASTIC_APM = {
- # Set required service name. Allowed characters:
- # a-z, A-Z, 0-9, -, _, and space
- 'SERVICE_NAME': '',
+ # Set required service name. Allowed characters:
+ # a-z, A-Z, 0-9, -, _, and space
+ 'SERVICE_NAME': '',
- # Use if APM Server requires a token
- 'SECRET_TOKEN': '',
+ # Use if APM Server requires a token
+ 'SECRET_TOKEN': '',
- # Set custom APM Server URL (default: http://localhost:8200)
- 'SERVER_URL': '',
+ # Set custom APM Server URL (default: http://localhost:8200)
+ 'SERVER_URL': '',
}
###########################################
@@ -111,7 +111,6 @@
# This puts the cron jobs into READ_ONLY mode
SCRIPTS_READ_ONLY_MODE = False
-
###########################################
# Feature Toggles
@@ -175,8 +174,8 @@
STORE_IMPL = "portality.store.StoreLocal"
STORE_SCOPE_IMPL = {
-# Enable this by scope in order to have different scopes store via different storage implementations
-# constants.STORE__SCOPE__PUBLIC_DATA_DUMP: "portality.store.StoreS3"
+ # Enable this by scope in order to have different scopes store via different storage implementations
+ # constants.STORE__SCOPE__PUBLIC_DATA_DUMP: "portality.store.StoreS3"
}
STORE_TMP_IMPL = "portality.store.TempStore"
@@ -198,27 +197,27 @@
# S3 credentials for relevant scopes
# ~~->S3:Technology~~
STORE_S3_SCOPES = {
- "anon_data" : {
- "aws_access_key_id" : "put this in your dev/test/production.cfg",
- "aws_secret_access_key" : "put this in your dev/test/production.cfg"
+ "anon_data": {
+ "aws_access_key_id": "put this in your dev/test/production.cfg",
+ "aws_secret_access_key": "put this in your dev/test/production.cfg"
},
- "cache" : {
- "aws_access_key_id" : "put this in your dev/test/production.cfg",
- "aws_secret_access_key" : "put this in your dev/test/production.cfg"
+ "cache": {
+ "aws_access_key_id": "put this in your dev/test/production.cfg",
+ "aws_secret_access_key": "put this in your dev/test/production.cfg"
},
# Used by the api_export script to dump data from the api
- constants.STORE__SCOPE__PUBLIC_DATA_DUMP : {
- "aws_access_key_id" : "put this in your dev/test/production.cfg",
- "aws_secret_access_key" : "put this in your dev/test/production.cfg"
+ constants.STORE__SCOPE__PUBLIC_DATA_DUMP: {
+ "aws_access_key_id": "put this in your dev/test/production.cfg",
+ "aws_secret_access_key": "put this in your dev/test/production.cfg"
},
# Used to store harvester run logs to S3
- "harvester" : {
- "aws_access_key_id" : "put this in your dev/test/production.cfg",
- "aws_secret_access_key" : "put this in your dev/test/production.cfg"
+ "harvester": {
+ "aws_access_key_id": "put this in your dev/test/production.cfg",
+ "aws_secret_access_key": "put this in your dev/test/production.cfg"
}
}
-STORE_S3_MULTIPART_THRESHOLD = 5 * 1024**3 # 5GB
+STORE_S3_MULTIPART_THRESHOLD = 5 * 1024 ** 3 # 5GB
####################################
# CMS configuration
@@ -256,7 +255,6 @@
# ~~->Cookies:Feature~~
SECRET_KEY = "default-key"
-
# Consent Cookie and other Top-Level dismissable notes
# ~~->ConsentCookie:Feature~~
CONSENT_COOKIE_KEY = "doaj-cookie-consent"
@@ -265,7 +263,7 @@
# ~~-> SiteNote:Feature~~
SITE_NOTE_ACTIVE = False
SITE_NOTE_KEY = "doaj-site-note"
-SITE_NOTE_SLEEP = 259200 # every 3 days
+SITE_NOTE_SLEEP = 259200 # every 3 days
SITE_NOTE_COOKIE_VALUE = "You have seen our most recent site wide announcement"
####################################
@@ -299,10 +297,10 @@
ROLE_MAP = {
"editor": [
- "associate_editor", # note, these don't cascade, so we still need to list all the low-level roles
+ "associate_editor", # note, these don't cascade, so we still need to list all the low-level roles
"edit_journal",
"edit_suggestion",
- "edit_application", # todo: switchover from suggestion to application
+ "edit_application", # todo: switchover from suggestion to application
"editor_area",
"assign_to_associate",
"list_group_journals",
@@ -340,16 +338,16 @@
# ~~->Email:ExternalService
# Settings for Flask-Mail. Set in app.cfg
-MAIL_SERVER = None # default localhost
-MAIL_PORT = 25 # default 25
-#MAIL_USE_TLS # default False
-#MAIL_USE_SSL # default False
-#MAIL_DEBUG # default app.debug
-#MAIL_USERNAME # default None
-#MAIL_PASSWORD # default None
-#MAIL_DEFAULT_SENDER # default None
-#MAIL_MAX_EMAILS # default None
-#MAIL_SUPPRESS_SEND # default app.testing
+MAIL_SERVER = None # default localhost
+MAIL_PORT = 25 # default 25
+# MAIL_USE_TLS # default False
+# MAIL_USE_SSL # default False
+# MAIL_DEBUG # default app.debug
+# MAIL_USERNAME # default None
+# MAIL_PASSWORD # default None
+# MAIL_DEFAULT_SENDER # default None
+# MAIL_MAX_EMAILS # default None
+# MAIL_SUPPRESS_SEND # default app.testing
ENABLE_EMAIL = True
ENABLE_PUBLISHER_EMAIL = True
@@ -379,8 +377,8 @@
# workflow email notification settings
# ~~->WorkflowNotifications:Feature~~
-MAN_ED_IDLE_WEEKS = 4 # weeks before an application is considered reminder-worthy
-ED_IDLE_WEEKS = 3 # weeks before the editor is warned about idle applications in their group
+MAN_ED_IDLE_WEEKS = 4 # weeks before an application is considered reminder-worthy
+ED_IDLE_WEEKS = 3 # weeks before the editor is warned about idle applications in their group
ASSOC_ED_IDLE_DAYS = 10
ASSOC_ED_IDLE_WEEKS = 3
@@ -406,7 +404,7 @@
# ~~->StatusEndpoint:Feature~~
# /status endpoint connection to all app machines
-APP_MACHINES_INTERNAL_IPS = [HOST + ':' + str(PORT)] # This should be set in production.cfg (or dev.cfg etc)
+APP_MACHINES_INTERNAL_IPS = [HOST + ':' + str(PORT)] # This should be set in production.cfg (or dev.cfg etc)
###########################################
# Background Jobs settings
@@ -478,12 +476,12 @@
# an array of DAO classes from which to retrieve the type-specific ES mappings
# to be loaded into the index during initialisation.
ELASTIC_SEARCH_MAPPINGS = [
- "portality.models.Journal", # ~~->Journal:Model~~
- "portality.models.Application", # ~~->Application:Model~~
- "portality.models.DraftApplication", # ~~-> DraftApplication:Model~~
- "portality.models.harvester.HarvestState", # ~~->HarvestState:Model~~
- "portality.models.background.BackgroundJob", # ~~-> BackgroundJob:Model~~
- "portality.models.autocheck.Autocheck" # ~~-> Autocheck:Model~~
+ "portality.models.Journal", # ~~->Journal:Model~~
+ "portality.models.Application", # ~~->Application:Model~~
+ "portality.models.DraftApplication", # ~~-> DraftApplication:Model~~
+ "portality.models.harvester.HarvestState", # ~~->HarvestState:Model~~
+ "portality.models.background.BackgroundJob", # ~~-> BackgroundJob:Model~~
+ "portality.models.autocheck.Autocheck" # ~~-> Autocheck:Model~~
]
# Map from dataobj coercion declarations to ES mappings
@@ -495,7 +493,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -505,7 +503,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -515,7 +513,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -525,7 +523,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -535,7 +533,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -545,7 +543,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -555,7 +553,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -565,7 +563,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -575,7 +573,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -585,7 +583,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -595,7 +593,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -605,7 +603,7 @@
"fields": {
"exact": {
"type": "keyword",
-# "index": False,
+ # "index": False,
"store": True
}
}
@@ -654,7 +652,6 @@
'number_of_replicas': 1
}
-
DEFAULT_DYNAMIC_MAPPING = {
'dynamic_templates': [
{
@@ -665,7 +662,7 @@
"fields": {
"exact": {
"type": "keyword",
- #"normalizer": "lowercase"
+ # "normalizer": "lowercase"
}
}
}
@@ -678,7 +675,7 @@
# a dict of the ES mappings. identify by name, and include name as first object name
# and identifier for how non-analyzed fields for faceting are differentiated in the mappings
MAPPINGS = {
- 'account': { #~~->Account:Model~~
+ 'account': { # ~~->Account:Model~~
# 'aliases': {
# 'account': {}
# },
@@ -687,18 +684,18 @@
}
}
-MAPPINGS['article'] = MAPPINGS["account"] #~~->Article:Model~~
-MAPPINGS['upload'] = MAPPINGS["account"] #~~->Upload:Model~~
-MAPPINGS['bulk_articles'] = MAPPINGS["account"] #~~->BulkArticles:Model~~
-MAPPINGS['cache'] = MAPPINGS["account"] #~~->Cache:Model~~
-MAPPINGS['lcc'] = MAPPINGS["account"] #~~->LCC:Model~~
-MAPPINGS['editor_group'] = MAPPINGS["account"] #~~->EditorGroup:Model~~
-MAPPINGS['news'] = MAPPINGS["account"] #~~->News:Model~~
-MAPPINGS['lock'] = MAPPINGS["account"] #~~->Lock:Model~~
-MAPPINGS['provenance'] = MAPPINGS["account"] #~~->Provenance:Model~~
-MAPPINGS['preserve'] = MAPPINGS["account"] #~~->Preservation:Model~~
-MAPPINGS['notification'] = MAPPINGS["account"] #~~->Notification:Model~~
-MAPPINGS['article_tombstone'] = MAPPINGS["account"] #~~->ArticleTombstone:Model~~
+MAPPINGS['article'] = MAPPINGS["account"] # ~~->Article:Model~~
+MAPPINGS['upload'] = MAPPINGS["account"] # ~~->Upload:Model~~
+MAPPINGS['bulk_articles'] = MAPPINGS["account"] # ~~->BulkArticles:Model~~
+MAPPINGS['cache'] = MAPPINGS["account"] # ~~->Cache:Model~~
+MAPPINGS['lcc'] = MAPPINGS["account"] # ~~->LCC:Model~~
+MAPPINGS['editor_group'] = MAPPINGS["account"] # ~~->EditorGroup:Model~~
+MAPPINGS['news'] = MAPPINGS["account"] # ~~->News:Model~~
+MAPPINGS['lock'] = MAPPINGS["account"] # ~~->Lock:Model~~
+MAPPINGS['provenance'] = MAPPINGS["account"] # ~~->Provenance:Model~~
+MAPPINGS['preserve'] = MAPPINGS["account"] # ~~->Preservation:Model~~
+MAPPINGS['notification'] = MAPPINGS["account"] # ~~->Notification:Model~~
+MAPPINGS['article_tombstone'] = MAPPINGS["account"] # ~~->ArticleTombstone:Model~~
#########################################
# Query Routes
@@ -949,31 +946,31 @@
ADMIN_NOTES_SEARCH_MAPPING = {
"admin.notes.id": {
- "type": "text",
- "fields": {
- "exact": {
- "type": "keyword",
- "store": True
- }
+ "type": "text",
+ "fields": {
+ "exact": {
+ "type": "keyword",
+ "store": True
}
+ }
},
"admin.notes.note": {
- "type": "text",
- "fields": {
- "exact": {
- "type": "keyword",
- "store": True
- }
+ "type": "text",
+ "fields": {
+ "exact": {
+ "type": "keyword",
+ "store": True
}
+ }
},
"admin.notes.author_id": {
- "type": "text",
- "fields": {
- "exact": {
- "type": "keyword",
- "store": True
- }
+ "type": "text",
+ "fields": {
+ "exact": {
+ "type": "keyword",
+ "store": True
}
+ }
}
}
@@ -993,7 +990,6 @@
# save the public application form as a draft every 60 seconds
PUBLIC_FORM_AUTOSAVE = 60000
-
############################################
# Atom Feed
# ~~->AtomFeed:Feature~~
@@ -1018,7 +1014,6 @@
# ~~->Favicon:Content~~
FEED_LOGO = "https://doaj.org/static/doaj/images/favicon.ico"
-
###########################################
# OAI-PMH SETTINGS
# ~~->OAIPMH:Feature~~
@@ -1054,7 +1049,6 @@
OAIPMH_RESUMPTION_TOKEN_EXPIRY = 86400
-
##########################################
# Article XML configuration
@@ -1103,7 +1097,6 @@
ARTICLE_HISTORY_DIR = os.path.join(ROOT_DIR, "history", "article")
JOURNAL_HISTORY_DIR = os.path.join(ROOT_DIR, "history", "journal")
-
#################################################
# Sitemap settings
# ~~->Sitemap:Feature~~
@@ -1111,7 +1104,6 @@
# approximate rate of change of the Table of Contents for journals
TOC_CHANGEFREQ = "monthly"
-
##################################################
# News feed settings
# ~~->News:Feature~~
@@ -1124,7 +1116,6 @@
NEWS_PAGE_NEWS_ITEMS = 20
-
##################################################
# Edit Lock settings
# ~~->Lock:Feature~~
@@ -1135,17 +1126,16 @@
# amount of time a background task can lock a resource for, in seconds
BACKGROUND_TASK_LOCK_TIMEOUT = 3600
-
###############################################
# Bit.ly configuration
# ~~->Bitly:ExternalService~~
# bit,ly api shortening service
-#BITLY_SHORTENING_API_URL = "https://api-ssl.bitly.com/v4/shorten"
+# BITLY_SHORTENING_API_URL = "https://api-ssl.bitly.com/v4/shorten"
# bitly oauth token
# ENTER YOUR OWN TOKEN IN APPROPRIATE .cfg FILE
-#BITLY_OAUTH_TOKEN = ""
+# BITLY_OAUTH_TOKEN = ""
#################################################
@@ -1203,7 +1193,6 @@
DISCOVERY_BULK_PAGE_SIZE = 1000
DISCOVERY_RECORDS_PER_FILE = 100000
-
######################################################
# Hotjar configuration
# ~~->Hotjar:ExternalService~~
@@ -1211,7 +1200,6 @@
# hotjar id - only activate this in production
HOTJAR_ID = ""
-
######################################################
# Analytics configuration
# specify in environment .cfg file - avoids sending live analytics events from test and dev environments
@@ -1228,7 +1216,7 @@
# Analytics custom dimensions. These are configured in the interface. #fixme: are these still configured since the move from GA?
ANALYTICS_DIMENSIONS = {
- 'oai_res_id': 'dimension1', # In analytics as OAI:Record
+ 'oai_res_id': 'dimension1', # In analytics as OAI:Record
}
# Plausible for OAI-PMH
@@ -1277,7 +1265,6 @@
'bulk_article_delete': 'Bulk article delete'
}
-
# Plausible for fixed query widget
# ~~->FixedQueryWidget:Feature~~
ANALYTICS_CATEGORY_FQW = 'FQW'
@@ -1315,7 +1302,6 @@
MINIMAL_OA_START_DATE = 1900
-
#############################################
# Harvester Configuration
# ~~->Harvester:Feature~~
@@ -1325,7 +1311,7 @@
# EPMC Client configuration
# ~~-> EPMC:ExternalService~~
EPMC_REST_API = "https://www.ebi.ac.uk/europepmc/webservices/rest/"
-EPMC_TARGET_VERSION = "6.9" # doc here: https://europepmc.org/docs/Europe_PMC_RESTful_Release_Notes.pdf
+EPMC_TARGET_VERSION = "6.9" # doc here: https://europepmc.org/docs/Europe_PMC_RESTful_Release_Notes.pdf
EPMC_HARVESTER_THROTTLE = 0.2
# General harvester configuration
@@ -1347,7 +1333,7 @@
# ReCAPTCHA configuration
# ~~->ReCAPTCHA:ExternalService
-#Recaptcha test keys, should be overridden in dev.cfg by the keys obtained from Google ReCaptcha v2
+# Recaptcha test keys, should be overridden in dev.cfg by the keys obtained from Google ReCaptcha v2
RECAPTCHA_ENABLE = True
RECAPTCHA_SITE_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'
RECAPTCHA_SECRET_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
@@ -1360,7 +1346,6 @@
PRESERVATION_PASSWD = "password"
PRESERVATION_COLLECTION = {}
-
#########################################################
# Background tasks --- anon export
TASKS_ANON_EXPORT_CLEAN = False
@@ -1371,21 +1356,19 @@
#########################################################
# Background tasks --- old_data_cleanup
TASK_DATA_RETENTION_DAYS = {
- "notification": 180, # ~~-> Notifications:Feature ~~
- "background_job": 180, # ~~-> BackgroundJobs:Feature ~~
+ "notification": 180, # ~~-> Notifications:Feature ~~
+ "background_job": 180, # ~~-> BackgroundJobs:Feature ~~
}
########################################
# Editorial Dashboard - set to-do list size
TODO_LIST_SIZE = 48
-
#########################################################
# Background tasks --- monitor_bgjobs
-TASKS_MONITOR_BGJOBS_TO = ["helpdesk@doaj.org",]
+TASKS_MONITOR_BGJOBS_TO = ["helpdesk@doaj.org", ]
TASKS_MONITOR_BGJOBS_FROM = "helpdesk@doaj.org"
-
##################################
# Background monitor
# ~~->BackgroundMonitor:Feature~~
@@ -1393,7 +1376,7 @@
# Configures the age of the last completed job on the queue before the queue is marked as unstable
# (in seconds)
BG_MONITOR_LAST_COMPLETED = {
- 'main_queue': 7200, # 2 hours
+ 'main_queue': 7200, # 2 hours
'long_running': 93600, # 26 hours
}
@@ -1433,7 +1416,7 @@
# Main queue
'journal_csv': {
'total': 2,
- 'oldest': 1200, # 20 mins
+ 'oldest': 1200, # 20 mins
},
'ingest_articles': {
'total': 250,
@@ -1472,20 +1455,6 @@
TOUR_COOKIE_MAX_AGE = 31536000
TOURS = {
- "/editor/": [
- {
- "roles": ["editor", "associate_editor"],
- "content_id": "dashboard_ed_assed",
- "name": "Welcome to your dashboard!",
- "description": "The new dashboard gives you a way to see all your priority work, take a look at what's new.",
- },
- {
- "roles": ["editor"],
- "content_id": "dashboard_ed",
- "name": "Your group activity",
- "description": "Your dashboard shows you who is working on what, and the status of your group's applications"
- }
- ],
"/admin/journal/*": [
{
"roles": ["admin"],
@@ -1494,10 +1463,25 @@
"name": "Autochecks",
"description": "Autochecks are available on some journals, and can help you to identify potential problems with the journal's metadata."
}
+ ],
+ "/editor/": [
+ {
+ "roles": ["editor"],
+ "content_id": "application_by_status",
+ "name": "New Links in the Colour Legend",
+ "description": "Discover how the colour legend labels now serve as links to quickly filter and view applications by group and status."
+ }
+ ],
+ "/dashboard/": [
+ {
+ "roles": ["admin"],
+ "content_id": "application_by_status",
+ "name": "New Links in the Colour Legend",
+ "description": "Discover how the colour legend labels now serve as links to quickly filter and view applications by group and status."
+ }
]
}
-
#######################################################
# Selenium test environment
@@ -1516,7 +1500,6 @@
UR_CONCURRENCY_TIMEOUT = 10
-
#############################################
# Google Sheet
# ~~->GoogleSheet:ExternalService~~
@@ -1525,7 +1508,6 @@
# value should be key file path of json, empty string means disabled
GOOGLE_KEY_PATH = ''
-
#############################################
# Datalog
# ~~->Datalog:Feature~~
@@ -1545,8 +1527,7 @@
AUTOCHECK_INCOMING = False
AUTOCHECK_RESOURCE_ISSN_ORG_TIMEOUT = 10
-AUTOCHECK_RESOURCE_ISSN_ORG_THROTTLE = 1 # seconds between requests
-
+AUTOCHECK_RESOURCE_ISSN_ORG_THROTTLE = 1 # seconds between requests
##################################################
# Background jobs Management settings
diff --git a/portality/static/js/dashboard.js b/portality/static/js/dashboard.js
index 4acc2ae0bd..2ee2e7ce77 100644
--- a/portality/static/js/dashboard.js
+++ b/portality/static/js/dashboard.js
@@ -1,6 +1,6 @@
// ~~Dashboard:Feature~~
doaj.dashboard = {
- statusOrder : [
+ statusOrder: [
"pending",
"in progress",
"completed",
@@ -10,11 +10,20 @@ doaj.dashboard = {
"ready",
"rejected",
"accepted"
+ ],
+ visibleStatusFilters: [
+ "pending",
+ "in progress",
+ "completed",
+ "on hold",
+ "ready",
]
};
-doaj.dashboard.init = function(context) {
+doaj.dashboard.init = function (context) {
doaj.dashboard.context = context;
+ doaj.dashboard.motivationalBanners = motivational_banners;
+ doaj.dashboard.context.historical_count = historical_count;
$(".js-group-tab").on("click", doaj.dashboard.groupTabClick);
// trigger a click on the first one, so there is something for the user to look at
@@ -22,15 +31,39 @@ doaj.dashboard.init = function(context) {
if (first) {
first.trigger("click");
}
+ this.generateMotivationalBanner();
+
}
-doaj.dashboard.groupTabClick = function(event) {
+doaj.dashboard.generateMotivationalBanner = function () {
+
+ _addNumberToBanner = function (text) {
+ var number = `` + doaj.dashboard.context.historical_count + ``;
+ var bannerTextWithNumber = text.replace(/{{ COUNT }}/g, number);
+ return bannerTextWithNumber;
+ }
+ _addTextToBanner = function (text) {
+ $("#banner_text_placeholder").html(text);
+ }
+
+ if (doaj.dashboard.context.historical_count == 0) {
+ bannerText = doaj.dashboard.motivationalBanners["banners"]["zero_count"][0];
+ } else {
+ var available_texts = doaj.dashboard.motivationalBanners["banners"]["positive_count"]
+ var randomIndex = Math.floor(Math.random() * available_texts.length);
+ var randomBannerText = available_texts[randomIndex];
+ bannerText = _addNumberToBanner(randomBannerText);
+ }
+ _addTextToBanner(bannerText);
+}
+
+doaj.dashboard.groupTabClick = function (event) {
let groupId = $(event.target).attr("data-group-id");
doaj.dashboard.loadGroupTab(groupId);
}
// ~~->GroupStats:Endpoint~~
-doaj.dashboard.loadGroupTab = function(groupId) {
+doaj.dashboard.loadGroupTab = function (groupId) {
$.ajax({
type: "GET",
contentType: "application/json",
@@ -41,157 +74,224 @@ doaj.dashboard.loadGroupTab = function(groupId) {
});
}
-doaj.dashboard.groupLoaded = function(data) {
+doaj.dashboard.groupLoaded = function (data) {
let container = $("#group-tab");
container.html(doaj.dashboard.renderGroupInfo(data));
}
-doaj.dashboard.groupLoadError = function(data) {
+doaj.dashboard.groupLoadError = function (data) {
alert("Unable to determine group status at this time");
}
-doaj.dashboard.renderGroupInfo = function(data) {
- // ~~-> EditorGroup:Model~~
-
- // first remove the editor from the associates list if they are there
- if (data.editor_group.associates && data.editor_group.associates.length > 0) {
- let edInAssEd = data.editor_group.associates.indexOf(data.editor_group.editor)
- if (edInAssEd > -1) {
- data.editor_group.associates.splice(edInAssEd, 1);
+doaj.dashboard.renderGroupInfo = function (data) {
+ // Remove the editor from the associates list if they are there
+ _removeEditorFromAssociates = function (data) {
+ if (data.editor_group.associates && data.editor_group.associates.length > 0) {
+ let edInAssEd = data.editor_group.associates.indexOf(data.editor_group.editor);
+ if (edInAssEd > -1) {
+ data.editor_group.associates.splice(edInAssEd, 1);
+ }
+ } else {
+ data.editor_group.associates = []; // to avoid having to keep checking it below
}
- } else {
- data.editor_group.associates = []; // just to avoid having to keep checking it below
}
- let allEditors = [data.editor_group.editor].concat(data.editor_group.associates);
-
- let editorListFrag = "";
- for (let i = 0; i < allEditors.length; i++) {
- let ed = allEditors[i];
- // ~~-> ApplicationSearch:Page~~
- let appQuerySource = doaj.searchQuerySource({
- "term" : [
- {"admin.editor.exact" : ed},
- {"admin.editor_group.exact" : data.editor_group.name},
- {"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 urQuerySource = doaj.searchQuerySource({"term" : [
- // {"admin.editor.exact" : ed},
- // {"admin.editor_group.exact" : data.editor_group.name},
- // {"index.application_type.exact" : "update request"} // this is required so we only see open update requests, not finished ones
- // ]})
- let appCount = 0;
- let urCount = 0;
- if (data.by_editor[ed]) {
- appCount = data.by_editor[ed].applications || 0;
- urCount = data.by_editor[ed].update_requests || 0;
- }
+ // Generate the search query source
+ _generateSearchQuerySource = function (term, sort) {
+ return doaj.searchQuerySource({
+ "term": term,
+ "sort": sort
+ });
+ }
- if (data.editors[ed]) {
- let isEd = "";
- if (i === 0) { // first one in the list is always the editor
- isEd = " (Editor)"
- }
+ _generateStatusLinks = function (data) {
+ const statusLinks = {};
- editorListFrag += `
+ ${doaj.dashboard.visibleStatusFilters.map(status => {
+ // Use the statusLink for each status
+ const link = statusLinks[status] || '#'; // Fallback to # if no link is found
+
+ return `
`;
+ }
+
+
+ // Generate the editor list fragment
+ _generateEditorListFragment = function (data, allEditors) {
+ let editorListFrag = "";
+ let unassignedFragment = _generateUnassignedApplicationsFragment(data);
+ for (let i = 0; i < allEditors.length; i++) {
+ let ed = allEditors[i];
+ let appQuerySource = _generateSearchQuerySource([
+ {"admin.editor.exact": ed},
+ {"admin.editor_group.exact": data.editor_group.name},
+ {"index.application_type.exact": "new application"} // only see open applications, not finished ones
+ ], [{"admin.date_applied": {"order": "asc"}}]);
- editorListFrag += `${appCount}applications
-
`;
+ let appCount = data.by_editor[ed]?.applications || 0;
+ let urCount = data.by_editor[ed]?.update_requests || 0;
+
+ if (data.editors[ed]) {
+ let isEd = i === 0 ? " (Editor)" : "";
+ editorListFrag += `
`;
+ {"index.application_type.exact": "new application"} // only see open applications, not finished ones
+ ], [{"admin.date_applied": {"order": "asc"}}]);
+
+ return `
`;
+ }
+
+ // Generate the status progress bar
+ _generateStatusProgressBar = function (data) {
+
+
+ let appStatusProgressBar = "";
+
+ for (let status of doaj.dashboard.statusOrder) {
+ if (data.by_status[status]?.applications > 0) {
+ let url = statusLinks[status]; // Get the URL from the precomputed status links
+
+ appStatusProgressBar += `
`;
}
}
+
+ return appStatusProgressBar;
}
- // ~~-> ApplicationSearch:Page~~
- let appGroupSource = doaj.searchQuerySource({
- "term" : [
- {"admin.editor_group.exact" : data.editor_group.name},
- {"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},
- // {"index.application_type.exact" : "update request"} // this is required so we only see open applications, not finished ones
- // ]})
- let frag = `
+
+ _generateStatisticsFragment = function (data) {
+ let statisticsFrag = "";
+ let historicalNumbers = data.historical_numbers;
+
+ if (historicalNumbers) {
+ statisticsFrag += ``;
+
+ if (current_user.role.includes("admin") || historicalNumbers.associate_editors.length > 0) {
+ statisticsFrag += `
Statistics for the current year (${historicalNumbers.year})
`;
+ }
+
+ if (current_user.role.includes("admin")) {
+ // Ready applications by editor
+ statisticsFrag += `
{% for k, v in params.items() %}
- {% if k != "teardown" %}
+ {% if k != "teardown" and k != "non_renderable" %}
{{ k }}
{% if v is mapping %}
diff --git a/portality/templates-v2/includes/_js_includes.html b/portality/templates-v2/includes/_js_includes.html
index d18034e523..d6de913520 100644
--- a/portality/templates-v2/includes/_js_includes.html
+++ b/portality/templates-v2/includes/_js_includes.html
@@ -29,7 +29,6 @@
-
diff --git a/portality/templates-v2/management/admin/dashboard.html b/portality/templates-v2/management/admin/dashboard.html
index b70e51e874..414b95daf4 100644
--- a/portality/templates-v2/management/admin/dashboard.html
+++ b/portality/templates-v2/management/admin/dashboard.html
@@ -36,43 +36,9 @@
{% endif %}
{% include "management/includes/_todo.html" %}
-
- {# ~~->$GroupStatus:Feature~~ #}
-
Activity
-
-
-
- {# TODO: there’s a bit of a11y work to be done here; we need to indicate which tabs are hidden and which
- aren’t using ARIA attributes. #}
- {# TODO: the first tab content needs to be shown by default, without a "click to see" message. #}
-
-
-
-
-
+ {% set groups = managed_groups %}
+ {% set person_of_assignments = maned_assignments %}
+ {% include "management/includes/_activity.html" %}
{% endblock %}
{% block admin_js %}
diff --git a/portality/templates-v2/management/base.html b/portality/templates-v2/management/base.html
index bde4236224..f352e0a39e 100644
--- a/portality/templates-v2/management/base.html
+++ b/portality/templates-v2/management/base.html
@@ -184,6 +184,16 @@
+ {% include "management/includes/_motivational_banner.html" %}
{% include "management/includes/_todo.html" %}
-
- {# ~~->$GroupStatus:Feature~~ #}
- {% if editor_of_groups | length != 0 %}
-
Activity
-
-
- {% endif %}
-
- {# TODO: there’s a bit of a11y work to be done here; we need to indicate which tabs are hidden and which
- aren’t using ARIA attributes. #}
- {# TODO: the first tab content needs to be shown by default, without a "click to see" message. #}
-
-
-
-
-
+ {% set groups = editor_of_groups %}
+ {% set person_of_assignments = editor_of_assignments %}
+ {% include "management/includes/_activity.html" %}
{% endblock %}
diff --git a/portality/templates-v2/management/includes/_activity.html b/portality/templates-v2/management/includes/_activity.html
new file mode 100644
index 0000000000..331e7b1565
--- /dev/null
+++ b/portality/templates-v2/management/includes/_activity.html
@@ -0,0 +1,38 @@
+
+ {# ~~->$GroupStatus:Feature~~ #}
+ {% if groups | length != 0 %}
+
Activity
+
+
+ {% endif %}
+
+ {# TODO: there’s a bit of a11y work to be done here; we need to indicate which tabs are hidden and which
+ aren’t using ARIA attributes. #}
+ {# TODO: the first tab content needs to be shown by default, without a "click to see" message. #}
+
+
+
+
+
\ No newline at end of file
diff --git a/portality/templates-v2/management/includes/_motivational_banner.html b/portality/templates-v2/management/includes/_motivational_banner.html
new file mode 100644
index 0000000000..624afae0ca
--- /dev/null
+++ b/portality/templates-v2/management/includes/_motivational_banner.html
@@ -0,0 +1,14 @@
+{% if historical_count %}
+