diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4ac2d449a8..1f15388af7 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -14,6 +14,7 @@ jobs:
- xpack.security.enabled: false
- transport.host: localhost
- discovery.type: single-node
+ parallelism: 4
steps:
- checkout
@@ -26,7 +27,8 @@ jobs:
echo "ELASTICSEARCH_HOSTS = [{'host': 'localhost', 'port': 9200}]" > test.cfg
python portality/cms/build_fragments.py
python portality/cms/build_sass.py
- pytest -v --color=yes --code-highlight=yes --log-level=DEBUG doajtest/unit
+ TESTS=$(circleci tests glob "doajtest/unit/**/*.py" | circleci tests split)
+ pytest -v --color=yes --code-highlight=yes --log-level=DEBUG $TESTS
working_directory: ~/doaj
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 3a3600a653..8ee9b3549c 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -55,6 +55,10 @@ Instructions for reviewers:
- [ ] Developer
- [ ] Reviewer
+- Urls are constructed with `url_for` not hard-coded
+ - [ ] N/A
+ - [ ] Developer
+ - [ ] Reviewer
### Testing
- Unit tests have been added/modified
diff --git a/cms/assets/img/sponsors/IUCN.svg b/cms/assets/img/sponsors/IUCN.svg
new file mode 100644
index 0000000000..159bf16ca2
--- /dev/null
+++ b/cms/assets/img/sponsors/IUCN.svg
@@ -0,0 +1,19 @@
+
diff --git a/cms/assets/img/sponsors/nerac.jpg b/cms/assets/img/sponsors/nerac.jpg
new file mode 100644
index 0000000000..6f6b28fa4d
Binary files /dev/null and b/cms/assets/img/sponsors/nerac.jpg differ
diff --git a/cms/assets/img/tours/dashboard-ed-assed/card.png b/cms/assets/img/tours/dashboard-ed-assed/card.png
new file mode 100644
index 0000000000..6c3db0e416
Binary files /dev/null and b/cms/assets/img/tours/dashboard-ed-assed/card.png differ
diff --git a/cms/data/notifications.yml b/cms/data/notifications.yml
index 1a67c6c7c2..1e417e9b71 100644
--- a/cms/data/notifications.yml
+++ b/cms/data/notifications.yml
@@ -3,38 +3,38 @@ application:assed:assigned:notify:
long: |
An application, or update request for the journal **{journal_title}** has been assigned to you by the Editor of your group **{group_name}**. Please start work on this within 10 days.
short:
- New application assigned to you
+ New application ({issns}) assigned to you
application:assed:inprogress:notify:
long: |
The application for **{application_title}** has not passed review by an Editor or Managing Editor and has been assigned back to you with questions or changes.
short:
- One of your applications has not passed review
+ One of your applications ({issns}) has not passed review
application:editor:completed:notify:
long: |
**{associate_editor}** has finished a review of the application for **{application_title}**
and marked it as **Completed**. Please review within 5 working days.
short:
- Application marked as completed
+ Application ({issns}) marked as completed
application:editor_group:assigned:notify:
long: |
A new application or an update request for the journal **{journal_name}** has been assigned to your group by a Managing Editor. Please assign this to an Associate Editor within 5 working days.
short:
- New application assigned to your group
+ New application ({issns}) assigned to your group
application:editor:inprogress:notify:
long: |
The application for **{application_title}** has not passed review by a Managing Editor and has been assigned back to your group with questions or changes.
short:
- Application reverted to 'In Progress' by Managing Editor
+ Application ({issns}) reverted to 'In Progress' by Managing Editor
application:maned:ready:notify:
long: |
The application for **{application_title}** has been marked **Ready** by **{editor}**. Please review it as soon as possible.
short:
- Application marked as ready
+ Application ({issns}) marked as ready
application:publisher:accepted:notify:
long: |
@@ -52,13 +52,13 @@ application:publisher:accepted:notify:
We are delighted to welcome this journal into DOAJ. Do not hesitate to contact us at [helpdesk@doaj.org](mailto:helpdesk@doaj.org) if you have any questions.
short:
- Your journal has been accepted
+ Your journal ({issns}) has been accepted
application:publisher:assigned:notify:
long: |
Your application for **{application_title}** submitted on {application_date} has been assigned to an editor for review for inclusion in the DOAJ. Please look out for further communications about the application. These may come from someone who is not using a DOAJ email address: [{volunteers_url}]({volunteers_url})
short:
- Your application has been assigned an editor for review
+ Your application ({issns}) has been assigned to an editor for review
application:publisher:created:notify:
long: |
@@ -72,7 +72,7 @@ application:publisher:created:notify:
If you write to us, check first that there is nothing in your Spam folder from us or one of our volunteers. Our volunteers may not be emailing from a DOAJ email address so you can check that their name is there on this page [{volunteers_url}]({volunteers_url})
short:
- Your application to DOAJ has been received
+ Your application ({issns}) to DOAJ has been received
application:publisher:inprogress:notify:
long: |
@@ -80,7 +80,7 @@ application:publisher:inprogress:notify:
The Associate Editor ([{volunteers}]({volunteers})) may contact you by email with questions. They may not be using a doaj.org email address. These emails can end up in your Spam folder so please check your Spam folder regularly.
short:
- Your submission is under review
+ Your submission ({issns}) is under review
application:publisher:quickreject:notify:
long: |
@@ -90,13 +90,13 @@ application:publisher:quickreject:notify:
You may submit a new application 6 months after the date of this email unless advised otherwise by a member of the DOAJ Editorial Team. Before you apply again, make any necessary changes to ensure your journal adheres to our criteria: ([{doaj_guide_url}]({doaj_guide_url}))
short:
- Your application was rejected
+ Your application ({issns}) was rejected
application:publisher:revision:notify:
long: |
The update which you submitted for **{application_title}** on {date_applied} requires some revisions before it can be accepted. The Managing Editor reviewing your update will contact you to explain the changes that are needed.
short:
- Your update request needs revisions
+ Your update request ({issns}) needs revisions
bg:job_finished:notify:
long: |
@@ -108,13 +108,13 @@ journal:assed:assigned:notify:
long: |
The journal **{journal_name}** has been assigned to you by the Editor of your group **{group_name}**. Please start work on this within 10 days.
short:
- New journal assigned to you
+ New journal ({issns}) assigned to you
journal:editor_group:assigned:notify:
long: |
The journal **{journal_name}** has been assigned to your group by a Managing Editor. Please assign this to an Associate Editor within 5 working days.
short:
- New journal assigned to your group
+ New journal ({issns}) assigned to your group
update_request:publisher:accepted:notify:
long: |
@@ -122,7 +122,7 @@ update_request:publisher:accepted:notify:
Thank you for updating this journal and helping to keep the DOAJ database up-to-date.
short:
- Update request accepted
+ Update request ({issns}) accepted
update_request:publisher:assigned:notify:
long: |
@@ -130,7 +130,7 @@ update_request:publisher:assigned:notify:
Thank you for helping to keep the DOAJ database up-to-date.
short:
- Your update request has been assigned an editor for review
+ Your update request ({issns}) has been assigned to an editor for review
update_request:publisher:rejected:notify:
long: |
@@ -140,7 +140,7 @@ update_request:publisher:rejected:notify:
- We already have one active update in the system. Additional updates are rejected without review.
short:
- Your update request was rejected
+ Your update request ({issns}) was rejected
journal:assed:discontinuing_soon:notify:
long: |
diff --git a/cms/data/sponsors.yml b/cms/data/sponsors.yml
index 157b0defe4..379c51cf69 100644
--- a/cms/data/sponsors.yml
+++ b/cms/data/sponsors.yml
@@ -94,6 +94,10 @@ bronze:
url: https://www.iop.org/
logo: iop.jpg
+- name: International Union for Conservation of Nature
+ url: https://iucn.org/
+ logo: IUCN.svg
+
- name: JMIR Publications
url: https://jmirpublications.com/
logo: jmir.svg
@@ -106,6 +110,10 @@ bronze:
url: https://www.keaipublishing.com/
logo: keai.svg
+- name: NERAC Inc.
+ url: https://www.nerac.com/
+ logo: nerac.jpg
+
- name: OASPA
url: https://oaspa.org/
logo: oaspa.png
diff --git a/cms/data/team.yml b/cms/data/team.yml
index ce175c0667..781c84737e 100644
--- a/cms/data/team.yml
+++ b/cms/data/team.yml
@@ -33,7 +33,7 @@
- name: Dominic Mitchell
role: Operations Manager
photo: dominic.jpg
- bio: 'Dominic has over 25 years experience working with publisher and library communities. He is responsible for operations and development of the DOAJ platform. He acts as Committee chair for the Think. Check. Submit. initiative, of which DOAJ is a founding organisation. He represents DOAJ in Project JASPER, a cross-industry project working to ensure that journals are preserved for the long term. He also sits on the OASPA Board of Directors and serves as Secretary. Outside of work, he is reluctantly becoming an expert in the playparks of Stockholm with his twin sons.'
+ bio: 'Dominic has over 25 years of experience working with publisher and library communities. He is responsible for operations and development of the DOAJ platform. He acts as Committee chair for the Think. Check. Submit. initiative, of which DOAJ is a founding organisation. He represents DOAJ in Project JASPER, a cross-industry project working to ensure that journals are preserved for the long term. He also sits on the OASPA Board of Directors and serves as Secretary. Outside of work, he is reluctantly becoming an expert in the playparks of Stockholm with his twin sons.'
coi:
2016: https://drive.google.com/file/d/0ByRf6PVViI-mWmU0UHZqZm1xcDQ/view?usp=sharing&resourcekey=0-BmQKwWn6Vb9ot73Xie66aA
2018: https://drive.google.com/file/d/13XX_GUrw2xRmXARjRrTxegULPT8Redka/view?usp=sharing
@@ -43,7 +43,7 @@
- name: Gala García Reátegui
role: Managing Editor
photo: gala.jpg
- bio: 'Gala holds a Masters Degree in Information and Documentation from Lyon 3 University in France. Prior to joining DOAJ, she worked for the Digital Strategy and Data Directorate at The French National Research Agency (ANR) and for the Open Archive HAL at the Center for Direct Scientific Communication (CCSD). Gala is Peruvian but lived for more than ten years in France. Today, she is based in Denmark. She loves meeting people from other cultures, trying local dishes or experiences: currently Gala goes winter bathing in the Limfjord, Denmark! She also loves running.'
+ bio: "Gala holds a Master's Degree in Information and Documentation from Lyon 3 University in France. Prior to joining DOAJ, she worked for the Digital Strategy and Data Directorate at The French National Research Agency (ANR) and for the Open Archive HAL at the Center for Direct Scientific Communication (CCSD). Gala is Peruvian but lived for more than ten years in France. Today, she is based in Denmark. She loves meeting people from other cultures, trying local dishes or experiences. Currently Gala goes winter bathing in the Limfjord, Denmark! She also loves running."
coi:
2023: https://drive.google.com/file/d/1R7XquFauefdmtjPIsfGfAWQoAw7NLIci/view?usp=sharing
@@ -58,14 +58,14 @@
- name: Joanna Ball
role: Managing Director
photo: joba.jpg
- bio: 'Joanna has had over 25 years experience of working within research libraries in the UK and Denmark, most recently as Head of Roskilde University Library, before joining DOAJ in 2022. She has also been involved with UKSG as Chair of Insights Editorial Board and a Trustee, and is currently Vice Chair. Joanna lives with her family in Roskilde and enjoys running in her spare time.'
+ bio: 'Joanna has over 25 years of experience working within research libraries in the UK and Denmark, most recently as Head of Roskilde University Library, before joining DOAJ in 2022. She has also been involved with UKSG as Chair of Insights Editorial Board and a Trustee, and is currently Vice Chair. Joanna lives with her family in Roskilde and enjoys running in her spare time.'
coi:
2022: https://drive.google.com/file/d/1-3xzwkHMclREgLhj_XNF5n6Nr4q2_bnw/view?usp=sharing
- name: Judith Barnsby
- role: Senior Managing Editor
+ role: Head of Editorial
photo: judith.jpg
- bio: 'Judith has 25 years experience in the scholarly publishing industry, working for a range of non-profit society publishers and service providers before joining DOAJ. She has a keen interest in publishing standards and protocols, and has served on the board of CLOCKSS and as chair of the PALS (publisher and library solutions) working group in the UK. Judith loves books, especially detective fiction, and volunteers in her local library.'
+ bio: 'Judith has 25 years of experience in the scholarly publishing industry, working for a range of non-profit society publishers and service providers before joining DOAJ. She has a keen interest in publishing standards and protocols, and has served on the board of CLOCKSS and as chair of the PALS (publisher and library solutions) working group in the UK. Judith loves books, especially detective fiction, and volunteers in her local library.'
coi:
2016: https://drive.google.com/file/d/0B0fPCpIPjZlmb3JmVkFYbjN5aTh1OUhLd2lZaEV0ZlFwbTZV/view?usp=sharing&resourcekey=0-o_PXKLk5UFbPk_-4B61jVA
2018: https://drive.google.com/file/d/0ByRf6PVViI-mV2lfMjByQjYxUkpMcXhuc2l5Q3ZDWlpiYUtZ/view?usp=sharing&resourcekey=0-6eiGIRal00eXvgJUTeN_lw
@@ -85,7 +85,7 @@
- name: Katrine Sundsbø
role: Community Manager
photo: katrine.jpeg
- bio: 'Katrine holds a Master’s degree in Cognitive Neuroscience, and has five years of experience in the field of scholarly communications. She has been an advocate for open access and visibility of research through various working groups, projects and through gamification of scholarly communications. Though Katrine is half Danish and half Norwegian, her son is named after a Swedish singer - and her British husband suggested the name!'
+ bio: "Katrine holds a Master’s degree in Cognitive Neuroscience, and has five years of experience in the field of scholarly communications. She has been an advocate for open access and visibility of research through various working groups, projects and through gamification of scholarly communications. Though Katrine is half Danish and half Norwegian, her son is named after a Swedish singer - and her British husband suggested the name!"
coi:
2023: https://drive.google.com/file/d/1yqK-Znq62T_QR_JjtcpQl6W_Ian2Ti4F/view?usp=share_link
diff --git a/cms/pages/about/at-20.md b/cms/pages/about/at-20.md
index b364aa00da..1678adfe2a 100644
--- a/cms/pages/about/at-20.md
+++ b/cms/pages/about/at-20.md
@@ -12,9 +12,11 @@ featuremap: ~~At20:Fragment~~
We are celebrating 20 years of being an important part of open infrastructure with a year-long campaign throughout 2023, and we want to invite you to be a part of our celebrations!
-We are holding three events for our community around the themes: ['Open', 'Global', and 'Trusted'](https://drive.google.com/file/d/1tw0d_Ztl09AQS_6L-VP1CqR3jtZ1iw5h/view?usp=sharing). Details about these events and how you can join them will be available on this page. We will also share interviews with key individuals who have shaped DOAJ into what it is today.
+We are holding three events for our community around the themes: ['Open', 'Global', and 'Trusted'](https://drive.google.com/file/d/1tw0d_Ztl09AQS_6L-VP1CqR3jtZ1iw5h/view?usp=sharing). Full details about these events and how you can join them are available on this page.
-Further down the page is a historical timeline to give you a full overview of DOAJ’s important milestones from 2003 to today.
+Below is a historical timeline providing an overview of DOAJ’s important milestones from 2003 to today.
+
+There is also an opportunity for you to [support DOAJ during its 20th year](/at-20/#support-our-anniversary-campaign) via Paypal.
---
diff --git a/cms/sass/components/_dropdown.scss b/cms/sass/components/_dropdown.scss
index 07524b5101..47ab4dc5e7 100644
--- a/cms/sass/components/_dropdown.scss
+++ b/cms/sass/components/_dropdown.scss
@@ -15,6 +15,10 @@
}
}
+.dropdown--notifications {
+ @extend .dropdown;
+}
+
.dropdown__menu {
display: none;
padding: 0;
diff --git a/cms/sass/components/_form.scss b/cms/sass/components/_form.scss
index b203ff5988..fd97423f9f 100644
--- a/cms/sass/components/_form.scss
+++ b/cms/sass/components/_form.scss
@@ -82,7 +82,7 @@
border-left: 1px solid $sanguine;
}
-.form__long-help {
+.form__long-help, .form__click-to-copy {
cursor: pointer;
&:hover {
diff --git a/cms/sass/components/_tag.scss b/cms/sass/components/_tag.scss
index 1f24ebce92..836e55c7ef 100644
--- a/cms/sass/components/_tag.scss
+++ b/cms/sass/components/_tag.scss
@@ -90,3 +90,8 @@
color: $white;
}
}
+
+.tag--confirmation {
+ background: $dark-green;
+ color: $white;
+}
diff --git a/cms/sass/layout/_editorial-panel.scss b/cms/sass/layout/_editorial-panel.scss
index 4cef1bbef4..a08cb98ca3 100644
--- a/cms/sass/layout/_editorial-panel.scss
+++ b/cms/sass/layout/_editorial-panel.scss
@@ -14,7 +14,7 @@
@media (min-width: 992px) {
position: -webkit-sticky;
position: sticky;
- top: 55px; // sticky header + 5px
+ top: 100px;
&__content {
max-height: 70vh;
diff --git a/cms/tours/dashboard_ed.yml b/cms/tours/dashboard_ed.yml
new file mode 100644
index 0000000000..51d1f46a6f
--- /dev/null
+++ b/cms/tours/dashboard_ed.yml
@@ -0,0 +1,12 @@
+steps:
+ - selector: '#group-tab'
+ title: Your group activity
+ content: This panel allows you to see how many applications your group has and their status, and which Associates need more applications.
+
+ - selector: '#group-tab ul.progress-bar'
+ title: Progress bar
+ content: Click any coloured block to open the list of applications.
+
+ - selector: "#feature_tour_nav"
+ title: Want to see the tour again?
+ content: Take the tour again by selecting it from the Feature Tours menu.
\ No newline at end of file
diff --git a/cms/tours/dashboard_ed_assed.yml b/cms/tours/dashboard_ed_assed.yml
new file mode 100644
index 0000000000..cf62b32362
--- /dev/null
+++ b/cms/tours/dashboard_ed_assed.yml
@@ -0,0 +1,48 @@
+steps:
+ - selector: header h1
+ title: Welcome to your dashboard!
+ content: Your dashboard is the starting point for your work and shows you a prioritised list of the applications that you have to work on.
+
+ - selector: "nav.vertical-nav a[href='/editor/']"
+ title: Navigation
+ content: Use the buttons here on the left to navigate around.
+
+ - selector: nav dl
+ title: Your groups
+ content: The groups that you are a member of and the email address of your Editor or Managing Editor are shown here.
+
+ - selector: "#notifications_nav"
+ title: Notifications
+ content: Notifications that you receive by email can also be found here. Click 'See all notifications' to find old notifications.
+
+ - selector: "header h1"
+ title: Your prioritised list
+ content: Each application is shown as a "card". The cards are arranged in date and priority order, from left to right.
+ They are also categorised by colour. Click a card to open the application so you can start your work.
+
+ - selector: "header h1"
+ title: "Each card contains:"
+ content: |
+
+
+
+
The date the application was submitted.
+
The application's status.
+
What you need to do with the application.
+
The journal's title.
+
Which group the application is assigned to.
+
To who the application is assigned.
+
+
+
+ - selector: "#logout"
+ title: Log out
+ content: "You can now log out or update your account from the bottom of your dashboard."
+
+ - selector: "#doaj_home"
+ title: "DOAJ Home"
+ content: "To get back to the main DOAJ website, click 'DOAJ HOME' in the top right corner"
+
+ - selector: "#feature_tour_nav"
+ title: Want to see the tour again?
+ content: Take the tour again by selecting it from the Feature Tours menu.
\ No newline at end of file
diff --git a/deploy/nginx/doaj b/deploy/nginx/doaj
index b52db3aea0..4e6c3b0576 100644
--- a/deploy/nginx/doaj
+++ b/deploy/nginx/doaj
@@ -36,17 +36,27 @@ map $http_user_agent $block_ua {
~*curl 1;
}
+# the public server (deprecated, use failover)
upstream doaj_apps {
- server 10.131.191.139:5050;
+ server 10.131.191.139:5050; #doaj-public-app-1
}
+
+# Background server runs async tasks
upstream doaj_bg_apps {
- #server 10.131.56.133:5050; #old bg machine
- server 10.131.12.33:5050;
+ server 10.131.12.33:5050; #doaj-background-app-1
+}
+
+# Editor and admin site components
+upstream doaj_ed_failover {
+ server 10.131.56.133:5050; #doaj-editor-app-1
+ server 10.131.12.33:5050 backup; #doaj-background-app-1
}
+
+# For public site components, try all servers
upstream doaj_apps_failover {
- server 10.131.191.139:5050;
- #server 10.131.56.133:5050 backup; #old bg machine
- server 10.131.12.33:5050 backup;
+ server 10.131.191.139:5050; #doaj-public-app-1
+ server 10.131.12.33:5050 backup; #doaj-background-app-1
+ server 10.131.56.133:5050 backup; #doaj-editor-app-1
}
upstream doaj_index {
server 10.131.191.132:9200;
@@ -121,6 +131,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
+
location /search {
if ($block_ua) {return 403;}
limit_req zone=general burst=10 nodelay;
@@ -144,9 +155,7 @@ server {
proxy_buffering off;
}
- # for now we are going to send all login functions to the bg machine
- # technically ONLY the routes that require file upload need to go to the bg machine
- # but we think it is handy to separate them out, and later we could send them to other machines
+ # technically only the routes that require file upload need to go to the bg machine, but separate for consistency
location /account {
limit_req zone=general burst=10 nodelay;
proxy_pass http://doaj_bg_apps;
@@ -157,6 +166,19 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
+
+ # prefer the editor machine for application form work (but application_quick_reject goes to background async)
+ location ~* /admin/application/ {
+ limit_req zone=general burst=10 nodelay;
+ proxy_pass http://doaj_ed_failover;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_buffering off;
+ }
+
location /admin { # there are admin bulk actions that MUST go to bg machine
limit_req zone=general burst=10 nodelay;
proxy_pass http://doaj_bg_apps;
@@ -167,9 +189,10 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
+
location /editor {
limit_req zone=general burst=10 nodelay;
- proxy_pass http://doaj_bg_apps;
+ proxy_pass http://doaj_ed_failover;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -177,9 +200,10 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
+
location /journal/readonly {
limit_req zone=general burst=10 nodelay;
- proxy_pass http://doaj_bg_apps;
+ proxy_pass http://doaj_ed_failover;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@@ -187,7 +211,8 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
- location /publisher { # only /publisher/uploadfile MUST go to bg, and /publisher/uploadFile
+
+ location /publisher { # only /publisher/uploadfile MUST go to background
limit_req zone=general burst=10 nodelay;
proxy_pass http://doaj_bg_apps;
proxy_redirect off;
@@ -197,7 +222,8 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
- location /service {
+
+ location /service { # performs locks etc - handle on the background server
limit_req zone=general burst=10 nodelay;
proxy_pass http://doaj_bg_apps;
proxy_redirect off;
@@ -221,6 +247,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
+
location /csv {
limit_req zone=general burst=10 nodelay;
proxy_pass http://doaj_bg_apps;
@@ -235,6 +262,7 @@ server {
location =/robots.txt {
alias /home/cloo/doaj/src/doaj/deploy/robots-production.txt;
}
+
location /static/ {
alias /home/cloo/doaj/src/doaj/portality/static/;
autoindex off;
diff --git a/doajtest/helpers.py b/doajtest/helpers.py
index 1f716bcf3b..2224ebc4c7 100644
--- a/doajtest/helpers.py
+++ b/doajtest/helpers.py
@@ -181,6 +181,10 @@ def tearDown(self):
pass # could be removed by other thread / process
shutil.rmtree(paths.rel2abs(__file__, "..", "tmp"), ignore_errors=True)
+ self.reset_db_record()
+
+ @staticmethod
+ def reset_db_record():
global CREATED_INDICES
if len(CREATED_INDICES) > 0:
dao.DomainObject.destroy_index()
diff --git a/doajtest/matrices/bll_todo_assed/top_todo_assed.matrix.csv b/doajtest/matrices/bll_todo_assed/top_todo_assed.matrix.csv
new file mode 100644
index 0000000000..b43591df41
--- /dev/null
+++ b/doajtest/matrices/bll_todo_assed/top_todo_assed.matrix.csv
@@ -0,0 +1,4 @@
+test_id,account,raises,todo_associate_follow_up_old,todo_associate_progress_stalled,todo_associate_start_pending,todo_associate_all_applications,todo_associate_follow_up_old_order,todo_associate_progress_stalled_order,todo_associate_start_pending_order,todo_associate_all_applications_order
+1,none,ArgumentException,0,0,0,0,,,,
+2,no_role,,0,0,0,0,,,,
+3,assed,,1,1,1,4,1,2,3,4
diff --git a/doajtest/matrices/bll_todo_assed/top_todo_assed.settings.csv b/doajtest/matrices/bll_todo_assed/top_todo_assed.settings.csv
new file mode 100644
index 0000000000..9780c80dfa
--- /dev/null
+++ b/doajtest/matrices/bll_todo_assed/top_todo_assed.settings.csv
@@ -0,0 +1,26 @@
+field,test_id,account,raises,todo_associate_follow_up_old,todo_associate_progress_stalled,todo_associate_start_pending,todo_associate_all_applications,todo_associate_follow_up_old_order,todo_associate_progress_stalled_order,todo_associate_start_pending_order,todo_associate_all_applications_order
+type,index,generated,conditional,conditional,conditional,conditional,conditional,conditional,conditional,conditional,conditional
+default,,,,,,,,,,,
+,,,,,,,,,,,
+values,,none,ArgumentException,,,,,,,,
+values,,no_role,,,,,,,,,
+values,,assed,,,,,,,,,
+,,,,,,,,,,,
+conditional raises,,none,ArgumentException,,,,,,,,
+,,,,,,,,,,,
+conditional todo_associate_follow_up_old,,assed,,1,,,,,,,
+conditional todo_associate_follow_up_old,,!assed,,0,,,,,,,
+,,,,,,,,,,,
+conditional todo_associate_progress_stalled,,assed,,,1,,,,,,
+conditional todo_associate_progress_stalled,,!assed,,,0,,,,,,
+,,,,,,,,,,,
+conditional todo_associate_start_pending,,assed,,,,1,,,,,
+conditional todo_associate_start_pending,,!assed,,,,0,,,,,
+,,,,,,,,,,,
+conditional todo_associate_all_applications,,assed,,,,,4,,,,
+conditional todo_associate_all_applications,,!assed,,,,,0,,,,
+,,,,,,,,,,,
+conditional todo_associate_follow_up_old_order,,assed,,,,,,1,,,
+conditional todo_associate_progress_stalled_order,,assed,,,,,,,2,,
+conditional todo_associate_start_pending_order,,assed,,,,,,,,3,
+conditional todo_associate_all_applications_order,,assed,,,,,,,,,4
\ No newline at end of file
diff --git a/doajtest/matrices/bll_todo_assed/top_todo_assed.settings.json b/doajtest/matrices/bll_todo_assed/top_todo_assed.settings.json
new file mode 100644
index 0000000000..f80349e252
--- /dev/null
+++ b/doajtest/matrices/bll_todo_assed/top_todo_assed.settings.json
@@ -0,0 +1,223 @@
+{
+ "parameters": [
+ {
+ "name": "test_id",
+ "type": "index"
+ },
+ {
+ "name": "account",
+ "type": "generated",
+ "values": {
+ "none": {},
+ "no_role": {},
+ "assed": {}
+ }
+ },
+ {
+ "name": "raises",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "ArgumentException": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "none"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_associate_follow_up_old",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "1": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ },
+ "0": {
+ "conditions": [
+ {
+ "account": {
+ "nor": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_associate_progress_stalled",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "1": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ },
+ "0": {
+ "conditions": [
+ {
+ "account": {
+ "nor": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_associate_start_pending",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "1": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ },
+ "0": {
+ "conditions": [
+ {
+ "account": {
+ "nor": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_associate_all_applications",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "4": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ },
+ "0": {
+ "conditions": [
+ {
+ "account": {
+ "nor": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_associate_follow_up_old_order",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "1": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_associate_progress_stalled_order",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "2": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_associate_start_pending_order",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "3": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_associate_all_applications_order",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "4": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "assed"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/doajtest/matrices/bll_todo_editor/top_todo_editor.matrix.csv b/doajtest/matrices/bll_todo_editor/top_todo_editor.matrix.csv
new file mode 100644
index 0000000000..65d669d813
--- /dev/null
+++ b/doajtest/matrices/bll_todo_editor/top_todo_editor.matrix.csv
@@ -0,0 +1,5 @@
+test_id,account,raises,todo_editor_follow_up_old,todo_editor_stalled,todo_editor_completed,todo_editor_assign_pending,todo_editor_follow_up_old_order,todo_editor_stalled_order,todo_editor_completed_order,todo_editor_assign_pending_order
+1,none,ArgumentException,0,0,0,0,,,,
+2,no_role,,0,0,0,0,,,,
+3,assed,,0,0,0,0,,,,
+4,editor,,1,1,1,1,3,4,1,2
diff --git a/doajtest/matrices/bll_todo_editor/top_todo_editor.settings.csv b/doajtest/matrices/bll_todo_editor/top_todo_editor.settings.csv
new file mode 100644
index 0000000000..aac9f5a3fc
--- /dev/null
+++ b/doajtest/matrices/bll_todo_editor/top_todo_editor.settings.csv
@@ -0,0 +1,27 @@
+field,test_id,account,raises,todo_editor_follow_up_old,todo_editor_stalled,todo_editor_completed,todo_editor_assign_pending,todo_editor_follow_up_old_order,todo_editor_stalled_order,todo_editor_completed_order,todo_editor_assign_pending_order
+type,index,generated,conditional,conditional,conditional,conditional,conditional,conditional,conditional,conditional,conditional
+default,,,,,,,,,,,
+,,,,,,,,,,,
+values,,none,ArgumentException,,,,,,,,
+values,,no_role,,,,,,,,,
+values,,assed,,,,,,,,,
+values,,editor,,,,,,,,,
+,,,,,,,,,,,
+conditional raises,,none,ArgumentException,,,,,,,,
+,,,,,,,,,,,
+conditional todo_editor_follow_up_old,,editor,,1,,,,,,,
+conditional todo_editor_follow_up_old,,!editor,,0,,,,,,,
+,,,,,,,,,,,
+conditional todo_editor_stalled,,editor,,,1,,,,,,
+conditional todo_editor_stalled,,!editor,,,0,,,,,,
+,,,,,,,,,,,
+conditional todo_editor_completed,,editor,,,,1,,,,,
+conditional todo_editor_completed,,!editor,,,,0,,,,,
+,,,,,,,,,,,
+conditional todo_editor_assign_pending,,editor,,,,,1,,,,
+conditional todo_editor_assign_pending,,!editor,,,,,0,,,,
+,,,,,,,,,,,
+conditional todo_editor_follow_up_old_order,,editor,,,,,,3,,,
+conditional todo_editor_stalled_order,,editor,,,,,,,4,,
+conditional todo_editor_completed_order,,editor,,,,,,,,1,
+conditional todo_editor_assign_pending_order,,editor,,,,,,,,,2
\ No newline at end of file
diff --git a/doajtest/matrices/bll_todo_editor/top_todo_editor.settings.json b/doajtest/matrices/bll_todo_editor/top_todo_editor.settings.json
new file mode 100644
index 0000000000..091801cd29
--- /dev/null
+++ b/doajtest/matrices/bll_todo_editor/top_todo_editor.settings.json
@@ -0,0 +1,224 @@
+{
+ "parameters": [
+ {
+ "name": "test_id",
+ "type": "index"
+ },
+ {
+ "name": "account",
+ "type": "generated",
+ "values": {
+ "none": {},
+ "no_role": {},
+ "assed": {},
+ "editor": {}
+ }
+ },
+ {
+ "name": "raises",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "ArgumentException": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "none"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_editor_follow_up_old",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "1": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ },
+ "0": {
+ "conditions": [
+ {
+ "account": {
+ "nor": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_editor_stalled",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "1": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ },
+ "0": {
+ "conditions": [
+ {
+ "account": {
+ "nor": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_editor_completed",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "1": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ },
+ "0": {
+ "conditions": [
+ {
+ "account": {
+ "nor": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_editor_assign_pending",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "1": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ },
+ "0": {
+ "conditions": [
+ {
+ "account": {
+ "nor": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_editor_follow_up_old_order",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "3": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_editor_stalled_order",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "4": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_editor_completed_order",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "1": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "todo_editor_assign_pending_order",
+ "type": "conditional",
+ "default": "",
+ "values": {
+ "2": {
+ "conditions": [
+ {
+ "account": {
+ "or": [
+ "editor"
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/doajtest/matrices/bll_todo/top_todo.matrix.csv b/doajtest/matrices/bll_todo_maned/top_todo_maned.matrix.csv
similarity index 100%
rename from doajtest/matrices/bll_todo/top_todo.matrix.csv
rename to doajtest/matrices/bll_todo_maned/top_todo_maned.matrix.csv
diff --git a/doajtest/matrices/bll_todo/top_todo.settings.csv b/doajtest/matrices/bll_todo_maned/top_todo_maned.settings.csv
similarity index 100%
rename from doajtest/matrices/bll_todo/top_todo.settings.csv
rename to doajtest/matrices/bll_todo_maned/top_todo_maned.settings.csv
diff --git a/doajtest/matrices/bll_todo/top_todo.settings.json b/doajtest/matrices/bll_todo_maned/top_todo_maned.settings.json
similarity index 100%
rename from doajtest/matrices/bll_todo/top_todo.settings.json
rename to doajtest/matrices/bll_todo_maned/top_todo_maned.settings.json
diff --git a/doajtest/testbook/dashboards/assed_todo.yml b/doajtest/testbook/dashboards/assed_todo.yml
new file mode 100644
index 0000000000..921f36dccb
--- /dev/null
+++ b/doajtest/testbook/dashboards/assed_todo.yml
@@ -0,0 +1,52 @@
+suite: Dashboard
+testset: Associate Editor Todo List
+
+tests:
+ - title: Associate Editor Todo List
+ context:
+ role: associate editor
+ testdrive: todo_associate
+ setup:
+ - Use the todo_associate testdrive to setup for this test, of follow the next steps
+ - You should set up a user account which has only the associate editor role
+ - "The user account should be assigned at least 4 applications which meet the following criteria: one that was created over 6 weeks ago,
+ one that has not been modified for 3 weeks, one which has recently been assigned to the user and is in the pending state, and one
+ recent application"
+ steps:
+ - step: log in as an associate editor
+ - step: Go to the editor's dashboard page
+ path: /editor
+ results:
+ - You can see 4 applications in your priority list
+ - The highest priority application is for an old application
+ - The second priority is for a stalled application
+ - The third priority is for a recently assigned/pending application
+ - The lowest priority is for an open application
+ - step: click on the highest priority application
+ results:
+ - The application opens in a new browser tab/window
+ - step: Edit the application in some minor way and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - Your priority list is unchanged
+ - step: Click on the highest priority application again
+ - step: Change the application status to "Completed" and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You can see 3 applications in your priority list
+ - The application you have just edited as disappeared from your priority list
+ - step: click on the new highest priority application (stalled)
+ - step: Edit the application in some minor way and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You can still see 3 applications in your priority list
+ - The stalled application you modified in the previous steps is no longer your highest priority, it is now
+ listed only as an "open application"
+ - Your new highest priority is a pending application
+ - step: click on the new highest priority application (pending)
+ - step: Change the application status to "In Progress" and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You can still see 3 applications in your priority list
+ - All your applications are now "open applications"
+ - They are ordered by created date, with the oldest first
\ No newline at end of file
diff --git a/doajtest/testbook/dashboards/editor_todo.yml b/doajtest/testbook/dashboards/editor_todo.yml
new file mode 100644
index 0000000000..f52b43da49
--- /dev/null
+++ b/doajtest/testbook/dashboards/editor_todo.yml
@@ -0,0 +1,57 @@
+suite: Dashboard
+testset: Editor Todo List
+
+tests:
+ - title: Editor Todo List
+ context:
+ role: editor
+ testdrive: todo_editor_associate
+ setup:
+ - Use the todo_editor_associate testdrive to setup for this test, of follow the next steps
+ - You should set up a user account which has both the editor and associate editor roles, and is the editor of at least one editorial group
+ - "The user account should be assigned at least 4 applications which meet the following criteria: one that was created over 6 weeks ago,
+ one that has not been modified for 3 weeks, one which has recently been assigned to the user and is in the pending state, and one
+ recent application"
+ - "The user account should be assigned another 4 applications which meet the following criteria: one that is in the completed state and assigned
+ to your editorial group, one that is assigned to your editorial group in the pending state but with no associate editor assigned, one that
+ is in your editorial group which was created less than 8 weeks ago but which hasn't been updated for 6 weeks, and one that is
+ in your editorial group which was created more than 8 weeks ago"
+ steps:
+ - step: log in as an editor
+ - step: Go to the editor's dashboard page
+ path: /editor
+ results:
+ - You can see 8 applications in your priority list
+ - The highest priority is for a recently completed application (in your editorial group)
+ - The second priority is for a recently assigned/pending application (in your editorial group)
+ - The third priority is for an old (+8 weeks) application (in your editorial group)
+ - The fourth priority is for a stalled (+6 weeks) application (in your editorial group)
+ - The fifth priority is for an old application (+6 weeks) (assigned to you as an associate editor)
+ - The sixth priority is for a stalled application (assigned to you as an associate editor)
+ - The seventh priority is for a recently assigned/pending application (assigned to you as an associate editor)
+ - The lowest priority is for an open application (assigned to you as an associate editor)
+ - step: click on the highest priority application
+ - step: Change the application status to "Ready" and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You can see 7 applications in your priority list
+ - The application you have just edited as disappeared from your priority list
+ - step: click on the new highest priority application (pending)
+ - step: assign an associate editor (ideally yourself) to the application and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You can still see 7 applications in your priority list
+ - The pending application you modified in the previous steps is no longer your highest priority, it is now
+ listed as recently assigned/pending, further down the list
+ - Your new highest priority is an old application
+ - step: click on the new highest priority application (old)
+ - step: Modify the item in some minor way and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - Your list of priorities has not changed
+ - step: click on your second highest priority (stalled)
+ - step: Modify the item in some minor way and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You have 6 applications left in your todo list
+ - The stalled application you just edited is no longer visible
\ No newline at end of file
diff --git a/doajtest/testbook/dashboards/editorial_group_status.yml b/doajtest/testbook/dashboards/editorial_group_status.yml
new file mode 100644
index 0000000000..8c595f01bd
--- /dev/null
+++ b/doajtest/testbook/dashboards/editorial_group_status.yml
@@ -0,0 +1,89 @@
+suite: Dashboard
+testset: Editorial Group Status for Editors
+
+tests:
+ - title: Record Counts
+ context:
+ role: editor
+ setup:
+ - You must set up at least 2 editorial groups with multiple members and a good selection of applications assigned to a number of associate editors. You should set yourself as the editor of those groups.
+ steps:
+ - step: Go to your Dashboard page
+ path: /dashboard
+ - step: Scroll to the bottom of the dashbaord page, after your TODO list items.
+ results:
+ - The editorial groups you are editor of are listed under the heading Activity
+ - The first group is already selected and the data shown
+ - step: Click on one of the other group names
+ results:
+ - The editorial group's statistics are presented on the screen
+ - You can see the total number of applications assigned to the group
+ - You can see the number of applications assigned to each associated editor
+ - You can see how many applications have not been assigned
+ - You can see the breakdown of the statuses of all the applications
+ - step: Click on the application count next to the group's name (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 assigned 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 an associated editor's name
+ results:
+ - A mail window opens in your mail client
+ - step: Click on the application count next to an editor's name
+ results:
+ - You are taken to the application search which shows the open applications assigned to the user
+ - The number of search results is the same as shown on the dashboard
+ - step: Go back to the dashboard page
+ - step: Click on the application count next to the "unassigned" label
+ results:
+ - You are taken to the application search which shows the open applications with no assigned editor
+ - 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 under "Applications By Status"
+ 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
+
+ - title: Updating Associated Editors
+ context:
+ role: editor
+ setup:
+ - You must set up at least 2 editorial groups with multiple members and a good selection of applications assigned to a number of associate editors. You should set yourself as the editor of those groups.
+ steps:
+ - step: Go to your ManEd Dashboard page
+ path: /dashboard
+ - step: Select an editorial group to see its status information
+ - step: Take a note of the number of applications assigned to one of your associated editors
+ - step: Click on the count of applications next to the "unassigned" tag
+ - step: Click "Review Application" on the application in the search interface
+ - step: Assign the application to the editor selected above
+ - step: Go back to the dashboard page (you may need to refresh it if you kept the page open)
+ results:
+ - The editor selected now has one more application assigned to them
+ - The number of unassigned applications has reduced by one
+ - step: Click on the count of applications next to the selected editor
+ results:
+ - The application search is shown
+ - The application you assigned to the editor is listed in the search
+
+ - title: Changing status
+ context:
+ role: editor
+ setup:
+ - You must set up at least 2 editorial groups with multiple members and a good selection of applications assigned to a number of associate editors. You should set yourself as the editor of those groups.
+ steps:
+ - step: Go to your ManEd Dashboard page
+ path: /dashboard
+ - step: Select an editorial group to see its status information
+ - step: Take a note of the number of applications assigned to a specific status (e.g. in progress)
+ - step: Click on the link to applications in that status
+ - step: Click "Review Application" on the application in the search interface
+ - step: Change the status to something different
+ - step: Go back to the dashboard page (you may need to refresh it if you kept the page open)
+ results:
+ - The status you selected now has one more application in that state
+ - The previous status has one fewer application in that state
+ - step: Click on the new status link
+ results:
+ - The application search is shown
+ - The application you put into this status is visible
diff --git a/doajtest/testbook/dashboards/maned_todo.yml b/doajtest/testbook/dashboards/maned_todo.yml
new file mode 100644
index 0000000000..8bc3564a6d
--- /dev/null
+++ b/doajtest/testbook/dashboards/maned_todo.yml
@@ -0,0 +1,61 @@
+suite: Dashboard
+testset: ManEd Todo List
+
+tests:
+ - title: ManEd Todo List
+ context:
+ role: admin
+ testdrive: todo_maned_editor_associate
+ setup:
+ - Use the todo_maned_editor_associate testdrive to setup for this test, OR follow the next steps
+ - You should set up a user account which has the admin, editor and associate editor roles, and is the maned of at least one editorial group, and
+ editor of at least one other editorial group
+ - "The user account should be assigned at least 4 applications which meet the following criteria: one that was created over 6 weeks ago,
+ one that has not been modified for 3 weeks, one which has recently been assigned to the user and is in the pending state, and one
+ recent application"
+ - "The user account should be assigned another 4 applications which meet the following criteria: one that is in the completed state and assigned
+ to your editorial group, one that is assigned to your editorial group in the pending state but with no associate editor assigned, one that
+ is in your editorial group which was created less than 8 weeks ago but which hasn't been updated for 6 weeks, and one that is
+ in your editorial group which was created more than 8 weeks ago"
+ - "The user account should be assigned another 5 applications which meet the following criteria: an application in your maned group which is
+ in the ready state, an application in your maned group which is in the completed state, an application in your maned group which has
+ not had an associate editor assigned, an application created over 10 weeks ago in your maned group, an application in your maned
+ group which has not been updated for 8 weeks."
+ steps:
+ - step: log in as a managing editor
+ - step: Go to the maned dashboard page
+ path: /dashboard
+ results:
+ - You can see 13 applications in your priority list
+ - Your priority list contains a mixture of managing editor items (actions related to teams you are the managing editor for),
+ editor items (actions related to teams you are the editor for) and associate items (actions related to applications which
+ are assigned specifically to you for review).
+ - At least one of your priority items is for an application which is older than 10 weeks (it should indicate that it is for your maned group)
+ - At least one of your priority items is for an application which has been inactive (stalled) for more than 8 weeks (it should indicate that it is for your maned group)
+ - At least one of your priority items is for an application in the state ready (it should indicate that it is for your maned group)
+ - At least one of your priority items is for an application in the completed state which has not been updated for more than 2 weeks (it should indicate that it is for your maned group)
+ - At least one of your priority items is for an application in the pending state which has not been updated for more than 2 weeks (it should indicate that it is for your maned group)
+ - step: click on the managing editor's ready application
+ - step: Change the application status to "Accepted" and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You can see 12 applications in your priority list
+ - The application you have just edited has disappeared from your priority list
+ - step: click on the [in progress] stalled managing editor's application
+ - step: make any minor adjustment to the metadata and save
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You can see 11 applications in your priority list
+ - The application you just edited has disappeared from your priority list
+ - step: click on the "completed" maned application
+ - step: Change the application to "ready" status
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You can still see 11 applications in your priority list
+ - The completed application you just moved to ready is now in your priority list as a ready application
+ - step: click on the pending managing editor's application
+ - step: Assign the item to an editor in the selected group (there should be a test editor available to you to select)
+ - step: close the tab, return to the dashboard and reload the page
+ results:
+ - You have 10 applications left in your todo list
+ - The pending application you just edited is no longer visible
\ No newline at end of file
diff --git a/doajtest/testbook/journal_form/associate_form.yml b/doajtest/testbook/journal_form/associate_form.yml
index cd333cbc2d..8a9e68b11c 100644
--- a/doajtest/testbook/journal_form/associate_form.yml
+++ b/doajtest/testbook/journal_form/associate_form.yml
@@ -70,4 +70,10 @@ tests:
- step: Attempt to click the "Remove" button
results:
- You are unable to delete the note
+ - step: Click "copy" button next to one of the fields (eg. Title)
+ results:
+ - Confirmation with fields value is displayed for 3 seconds
+ - step: Attempt to paste the value (use separate editor)
+ results:
+ - Correct value is pasted
diff --git a/doajtest/testbook/journal_form/editor_form.yml b/doajtest/testbook/journal_form/editor_form.yml
index 16b78e2c77..747bd3f81d 100644
--- a/doajtest/testbook/journal_form/editor_form.yml
+++ b/doajtest/testbook/journal_form/editor_form.yml
@@ -80,3 +80,9 @@ tests:
- step: Attempt to click the "Remove" button
results:
- You are unable to delete the note
+ - step: Click "copy" button next to one of the fields (eg. Title)
+ results:
+ - Confirmation with fields value is displayed for 3 seconds
+ - step: Attempt to paste the value (use separate editor)
+ results:
+ - Correct value is pasted
diff --git a/doajtest/testbook/journal_form/maned_form.yml b/doajtest/testbook/journal_form/maned_form.yml
index 7c19aa906b..0a43ffe559 100644
--- a/doajtest/testbook/journal_form/maned_form.yml
+++ b/doajtest/testbook/journal_form/maned_form.yml
@@ -120,6 +120,12 @@ tests:
- step: Attempt to click the "Remove" button
results:
- You are unable to delete the note
+ - step: Click "copy" button next to one of the fields (eg. Title)
+ results:
+ - Confirmation with fields value is displayed for 3 seconds
+ - step: Attempt to paste the value (use separate editor)
+ results:
+ - Correct value is pasted
- title: Withdraw all
depends:
- suite: New Application Form
diff --git a/doajtest/testbook/new_application_form/associate_editor_form.yml b/doajtest/testbook/new_application_form/associate_editor_form.yml
index 366fac92c4..f9f9fd4619 100644
--- a/doajtest/testbook/new_application_form/associate_editor_form.yml
+++ b/doajtest/testbook/new_application_form/associate_editor_form.yml
@@ -63,3 +63,9 @@ tests:
- step: Attempt to click the "Remove" button
results:
- You are unable to delete the note
+ - step: Click "copy" button next to one of the fields (eg. Title)
+ results:
+ - Confirmation with fields value is displayed for 3 seconds
+ - step: Attempt to paste the value (use separate editor)
+ results:
+ - Correct value is pasted
diff --git a/doajtest/testbook/new_application_form/editor_form.yml b/doajtest/testbook/new_application_form/editor_form.yml
index cd9b8edf3d..b9db0066f7 100644
--- a/doajtest/testbook/new_application_form/editor_form.yml
+++ b/doajtest/testbook/new_application_form/editor_form.yml
@@ -64,4 +64,10 @@ tests:
- you are unable to edit the note
- step: Attempt to click the "Remove" button
results:
- - You are unable to delete the note
\ No newline at end of file
+ - You are unable to delete the note
+ - step: Click "copy" button next to one of the fields (eg. Title)
+ results:
+ - Confirmation with fields value is displayed for 3 seconds
+ - step: Attempt to paste the value (use separate editor)
+ results:
+ - Correct value is pasted
\ No newline at end of file
diff --git a/doajtest/testbook/new_application_form/maned_form.yml b/doajtest/testbook/new_application_form/maned_form.yml
index 907791691b..98dc0211f6 100644
--- a/doajtest/testbook/new_application_form/maned_form.yml
+++ b/doajtest/testbook/new_application_form/maned_form.yml
@@ -95,3 +95,9 @@ tests:
- step: Attempt to click the "Remove" button
results:
- You are unable to delete the note
+ - step: Click "copy" button next to one of the fields (eg. Title)
+ results:
+ - Confirmation with fields value is displayed for 3 seconds
+ - step: Attempt to paste the value (use separate editor)
+ results:
+ - Correct value is pasted
diff --git a/doajtest/testdrive/todo_editor.py b/doajtest/testdrive/todo_editor.py
new file mode 100644
index 0000000000..c73939597d
--- /dev/null
+++ b/doajtest/testdrive/todo_editor.py
@@ -0,0 +1,109 @@
+from portality import constants
+from doajtest.testdrive.factory import TestDrive
+from doajtest.fixtures.v2.applications import ApplicationFixtureFactory
+from portality.lib import dates
+from portality import models
+from datetime import datetime
+
+
+class TodoEditor(TestDrive):
+
+ def setup(self) -> dict:
+ un = self.create_random_str()
+ pw = self.create_random_str()
+ acc = models.Account.make_account(un + "@example.com", un, "TodoEditor " + un, ["editor"])
+ acc.set_password(pw)
+ acc.save()
+
+ gn = "TodoEditor Group " + un
+ eg = models.EditorGroup(**{
+ "name": gn
+ })
+ eg.set_editor(acc.id)
+ eg.save()
+
+ apps = build_applications(un, eg)
+
+ return {
+ "account": {
+ "username": acc.id,
+ "password": pw
+ },
+ "editor_group": {
+ "id": eg.id,
+ "name": eg.name
+ },
+ "applications": apps
+ }
+
+ def teardown(self, params) -> dict:
+ models.Account.remove_by_id(params["account"]["username"])
+ models.EditorGroup.remove_by_id(params["editor_group"]["id"])
+ for nature, details in params["applications"].items():
+ for detail in details:
+ models.Application.remove_by_id(detail["id"])
+ return {"status": "success"}
+
+
+def build_application(title, lmu_diff, cd_diff, status, editor=None, editor_group=None):
+ source = ApplicationFixtureFactory.make_application_source()
+ ap = models.Application(**source)
+ ap.bibjson().title = title
+ ap.set_id(ap.makeid())
+ ap.remove_current_journal()
+ ap.remove_related_journal()
+ ap.application_type = constants.APPLICATION_TYPE_NEW_APPLICATION
+ ap.set_last_manual_update(dates.before(datetime.utcnow(), lmu_diff))
+ ap.set_created(dates.before(datetime.utcnow(), cd_diff))
+ ap.set_application_status(status)
+
+ if editor is not None:
+ ap.set_editor(editor)
+
+ if editor_group is not None:
+ ap.set_editor_group(editor_group)
+
+ ap.save()
+ return ap
+
+
+def build_applications(un, eg):
+ 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)
+ app.save()
+ apps["stalled"] = [{
+ "id": app.id,
+ "title": un + " Stalled Application"
+ }]
+
+ app = build_application(un + " Old Application", 8 * w, 8 * w, constants.APPLICATION_STATUS_IN_PROGRESS,
+ editor_group=eg.name)
+ app.save()
+ apps["old"] = [{
+ "id": app.id,
+ "title": un + " Old Application"
+ }]
+
+ app = build_application(un + " Completed Application", 1 * w, 1 * w, constants.APPLICATION_STATUS_COMPLETED,
+ editor_group=eg.name)
+ app.save()
+ apps["completed"] = [{
+ "id": app.id,
+ "title": un + " Completed Application"
+ }]
+
+ app = build_application(un + " Pending Application", 1 * w, 1 * w, constants.APPLICATION_STATUS_PENDING,
+ editor_group=eg.name)
+ app.remove_editor()
+ app.save()
+ apps["pending"] = [{
+ "id": app.id,
+ "title": un + " Pending Application"
+ }]
+
+ return apps
+
diff --git a/doajtest/testdrive/todo_editor_associate.py b/doajtest/testdrive/todo_editor_associate.py
new file mode 100644
index 0000000000..095d9b2539
--- /dev/null
+++ b/doajtest/testdrive/todo_editor_associate.py
@@ -0,0 +1,51 @@
+from portality import constants
+from doajtest.testdrive.factory import TestDrive
+from doajtest.testdrive.todo_associate import build_applications as build_associate_applications
+from doajtest.testdrive.todo_editor import build_applications as build_editor_applications
+from portality import models
+
+
+class TodoEditorAssociate(TestDrive):
+
+ def setup(self) -> dict:
+ un = self.create_random_str()
+ pw = self.create_random_str()
+ acc = models.Account.make_account(un + "@example.com", un, "TodoEditorAssociate " + un, ["editor", constants.ROLE_ASSOCIATE_EDITOR])
+ acc.set_password(pw)
+ acc.save()
+
+ gn = "TodoEditorAssociate Group " + un
+ eg = models.EditorGroup(**{
+ "name": gn
+ })
+ eg.set_editor(acc.id)
+ eg.save()
+
+ aapps = build_associate_applications(un)
+ eapps = build_editor_applications(un, eg)
+
+ return {
+ "account": {
+ "username": acc.id,
+ "password": pw
+ },
+ "editor_group": {
+ "id": eg.id,
+ "name": eg.name
+ },
+ "applications": {
+ "associate": aapps,
+ "editor": eapps
+ }
+ }
+
+ def teardown(self, params) -> dict:
+ models.Account.remove_by_id(params["account"]["username"])
+ models.EditorGroup.remove_by_id(params["editor_group"]["id"])
+ for nature, details in params["applications"]["associate"].items():
+ for detail in details:
+ models.Application.remove_by_id(detail["id"])
+ for nature, details in params["applications"]["editor"].items():
+ for detail in details:
+ models.Application.remove_by_id(detail["id"])
+ return {"status": "success"}
\ No newline at end of file
diff --git a/doajtest/testdrive/todo_maned_editor_associate.py b/doajtest/testdrive/todo_maned_editor_associate.py
new file mode 100644
index 0000000000..5b038f2153
--- /dev/null
+++ b/doajtest/testdrive/todo_maned_editor_associate.py
@@ -0,0 +1,181 @@
+from portality import constants
+from doajtest.testdrive.factory import TestDrive
+from doajtest.fixtures.v2.applications import ApplicationFixtureFactory
+from doajtest.testdrive.todo_associate import build_applications as build_associate_applications
+from doajtest.testdrive.todo_editor import build_applications as build_editor_applications
+from portality import models
+from portality.lib import dates
+from datetime import datetime
+
+
+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.set_password(pw)
+ admin.save()
+
+ oun = self.create_random_str()
+ owner = models.Account.make_account(oun + "@example.com", oun, "Owner " + un, ["publisher"])
+ owner.save()
+
+ eun = self.create_random_str()
+ assed = models.Account.make_account(eun + "@example.com", eun, "Associate Editor " + un, ["associate_editor"])
+ assed.save()
+
+ gn1 = "Maned Group " + un
+ eg1 = models.EditorGroup(**{
+ "name": gn1
+ })
+ eg1.set_maned(admin.id)
+ eg1.add_associate(assed.id)
+ eg1.save()
+
+ gn2 = "Editor Group " + un
+ eg2 = models.EditorGroup(**{
+ "name": gn2
+ })
+ eg2.set_editor(admin.id)
+ eg2.save()
+
+ # the eponymous group
+ eg3 = models.EditorGroup(**{
+ "name": admin.id
+ })
+ eg3.set_maned(admin.id)
+ eg3.set_editor(admin.id)
+ eg3.add_associate(admin.id)
+ eg3.save()
+
+ aapps = build_associate_applications(un)
+ eapps = build_editor_applications(un, eg2)
+ mapps = build_maned_applications(un, eg1, owner.id, eg3)
+
+
+ return {
+ "account": {
+ "username": admin.id,
+ "password": pw
+ },
+ "users": [
+ owner.id,
+ assed.id
+ ],
+ "editor_group": {
+ "id": eg2.id,
+ "name": eg2.name
+ },
+ "maned_group": {
+ "id": eg1.id,
+ "name": eg1.name
+ },
+ "applications": {
+ "associate": aapps,
+ "editor": eapps,
+ "maned": mapps
+ }
+ }
+
+ def teardown(self, params) -> dict:
+ models.Account.remove_by_id(params["account"]["username"])
+ for user in params["users"]:
+ models.Account.remove_by_id(user)
+ models.EditorGroup.remove_by_id(params["editor_group"]["id"])
+ models.EditorGroup.remove_by_id(params["maned_group"]["id"])
+ for nature, details in params["applications"]["associate"].items():
+ for detail in details:
+ models.Application.remove_by_id(detail["id"])
+ for nature, details in params["applications"]["editor"].items():
+ for detail in details:
+ models.Application.remove_by_id(detail["id"])
+ for nature, details in params["applications"]["maned"].items():
+ for detail in details:
+ models.Application.remove_by_id(detail["id"])
+ return {"status": "success"}
+
+
+def build_maned_applications(un, eg, owner, eponymous_group):
+ 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)
+ app.save()
+ apps["stalled"] = [{
+ "id": app.id,
+ "title": un + " Maned Stalled Application"
+ }]
+
+ app = build_application(un + " Maned Old Application", 10 * w, 10 * w, constants.APPLICATION_STATUS_IN_PROGRESS,
+ editor_group=eg.name, owner=owner)
+ app.save()
+ apps["old"] = [{
+ "id": app.id,
+ "title": un + " Maned Old Application"
+ }]
+
+ app = build_application(un + " Maned Ready Application", 1 * w, 1 * w, constants.APPLICATION_STATUS_READY,
+ editor_group=eg.name, owner=owner)
+ app.save()
+ apps["ready"] = [{
+ "id": app.id,
+ "title": un + " Maned Completed Application"
+ }]
+
+ app = build_application(un + " Maned Completed Application", 2 * w, 2 * w, constants.APPLICATION_STATUS_COMPLETED,
+ editor_group=eg.name, owner=owner)
+ app.save()
+ apps["completed"] = [{
+ "id": app.id,
+ "title": un + " Maned Completed Application"
+ }]
+
+ app = build_application(un + " Maned Pending Application", 2 * w, 2 * w, constants.APPLICATION_STATUS_PENDING,
+ editor_group=eg.name, owner=owner)
+ app.remove_editor()
+ app.save()
+ apps["pending"] = [{
+ "id": app.id,
+ "title": un + " Maned Pending Application"
+ }]
+
+ app = build_application(un + " Maned Low Priority Pending Application", 1 * w, 1 * w,
+ constants.APPLICATION_STATUS_PENDING,
+ editor_group=eponymous_group.name, owner=owner)
+ app.remove_editor()
+ app.save()
+ apps["low_priority_pending"] = [{
+ "id": app.id,
+ "title": un + " Maned Low Priority Pending Application"
+ }]
+
+ return apps
+
+
+def build_application(title, lmu_diff, cd_diff, status, editor=None, editor_group=None, owner=None):
+ source = ApplicationFixtureFactory.make_application_source()
+ ap = models.Application(**source)
+ ap.bibjson().title = title
+ ap.set_id(ap.makeid())
+ ap.remove_current_journal()
+ ap.remove_related_journal()
+ del ap.bibjson().discontinued_date
+ ap.application_type = constants.APPLICATION_TYPE_NEW_APPLICATION
+ ap.set_last_manual_update(dates.before(datetime.utcnow(), lmu_diff))
+ ap.set_created(dates.before(datetime.utcnow(), cd_diff))
+ ap.set_application_status(status)
+
+ if editor is not None:
+ ap.set_editor(editor)
+
+ if editor_group is not None:
+ ap.set_editor_group(editor_group)
+
+ if owner is not None:
+ ap.set_owner(owner)
+
+ ap.save()
+ return ap
diff --git a/doajtest/unit/application_processors/test_application_processor_emails.py b/doajtest/unit/application_processors/test_application_processor_emails.py
index 44630b518c..036c86c68a 100644
--- a/doajtest/unit/application_processors/test_application_processor_emails.py
+++ b/doajtest/unit/application_processors/test_application_processor_emails.py
@@ -65,13 +65,14 @@ def editor_account_pull(self, _id):
ACTUAL_ACCOUNT_PULL = models.Account.pull
# A regex string for searching the log entries
-email_log_regex = 'template.*%s.*to:\[u{0,1}\'%s.*subject:.*%s'
+email_log_regex = r'template.*%s.*to:\[u{0,1}\'%s.*subject:.*%s'
# A string present in each email log entry (for counting them)
email_count_string = 'Email template'
NOTIFICATIONS_INTERCEPT = []
+
class TestPublicApplicationEmails(DoajTestCase):
def setUp(self):
super(TestPublicApplicationEmails, self).setUp()
@@ -120,11 +121,11 @@ def test_01_public_application_email(self):
# * to the applicant, informing them the application was received
public_template = re.escape('notification_email.jinja2')
public_to = re.escape(account.email)
- public_subject = "Directory of Open Access Journals - Your application to DOAJ has been received"
+ public_subject = re.escape("Directory of Open Access Journals - Your application (" + ", ".join(issn for issn in processor.source.bibjson().issns()) + ") to DOAJ has been received")
public_email_matched = re.search(email_log_regex % (public_template, public_to, public_subject),
info_stream_contents,
re.DOTALL)
- assert bool(public_email_matched)
+ assert bool(public_email_matched), info_stream_contents
assert len(re.findall(email_count_string, info_stream_contents)) == 1
@@ -220,7 +221,7 @@ def test_01_maned_review_emails(self):
editor_template = re.escape('notification_email.jinja2')
editor_to = re.escape('eddie@example.com')
- editor_subject = "Application reverted to 'In Progress' by Managing Editor"
+ editor_subject = re.escape("Application (" + ", ".join(issn for issn in processor.source.bibjson().issns()) + ") reverted to 'In Progress' by Managing Editor\n")
editor_email_matched = re.search(email_log_regex % (editor_template, editor_to, editor_subject),
info_stream_contents,
re.DOTALL)
@@ -236,7 +237,7 @@ def test_01_maned_review_emails(self):
assoc_editor_template = re.escape('email/notification_email.jinja2')
assoc_editor_to = re.escape('associate@example.com')
- assoc_editor_subject = self.svc.short_notification(ApplicationAssedInprogressNotify.ID)# "an application assigned to you has not passed review."
+ assoc_editor_subject = re.escape(self.svc.short_notification(ApplicationAssedInprogressNotify.ID).replace("{issns}", ", ".join(issn for issn in processor.source.bibjson().issns())) + "\n")# "an application assigned to you has not passed review."
assoc_editor_email_matched = re.search(email_log_regex % (assoc_editor_template, assoc_editor_to, assoc_editor_subject),
info_stream_contents,
re.DOTALL)
@@ -288,7 +289,7 @@ def test_01_maned_review_emails(self):
editor_template = re.escape('email/notification_email.jinja2')
editor_to = re.escape('eddie@example.com')
- editor_subject = "Directory of Open Access Journals - Application reverted to 'In Progress' by Managing Editor"
+ editor_subject = re.escape("Directory of Open Access Journals - Application ({}) reverted to 'In Progress' by Managing Editor".format(', '.join(issn for issn in processor.source.bibjson().issns())))
editor_email_matched = re.search(email_log_regex % (editor_template, editor_to, editor_subject),
info_stream_contents,
re.DOTALL)
@@ -304,7 +305,7 @@ def test_01_maned_review_emails(self):
assoc_editor_template = re.escape('email/notification_email.jinja2')
assoc_editor_to = re.escape('associate@example.com')
- assoc_editor_subject = self.svc.short_notification(ApplicationAssedInprogressNotify.ID) # "an application assigned to you has not passed review."
+ assoc_editor_subject = re.escape(self.svc.short_notification(ApplicationAssedInprogressNotify.ID).replace("{issns}", ", ".join(issn for issn in processor.source.bibjson().issns())) + "\n") # "an application assigned to you has not passed review."
assoc_editor_email_matched = re.search(
email_log_regex % (assoc_editor_template, assoc_editor_to, assoc_editor_subject),
info_stream_contents,
@@ -345,7 +346,7 @@ def test_01_maned_review_emails(self):
# * and to the publisher informing them there's an editor assigned.
assEd_template = re.escape('email/notification_email.jinja2')
assEd_to = re.escape(models.Account.pull('associate_3').email)
- assEd_subject = 'Directory of Open Access Journals - New application assigned to you'
+ assEd_subject = re.escape('Directory of Open Access Journals - New application ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
@@ -354,7 +355,7 @@ def test_01_maned_review_emails(self):
publisher_template = re.escape('email/notification_email.jinja2')
publisher_to = re.escape(owner.email)
- publisher_subject = 'Directory of Open Access Journals - Your application has been assigned an editor for review'
+ publisher_subject = re.escape('Directory of Open Access Journals - Your application ({}) has been assigned to an editor for review'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
publisher_email_matched = re.search(email_log_regex % (publisher_template, publisher_to, publisher_subject),
info_stream_contents,
@@ -386,7 +387,7 @@ def test_01_maned_review_emails(self):
# * to the AssEd who's been assigned
editor_template = re.escape('email/notification_email.jinja2')
editor_to = re.escape('eddie@example.com')
- editor_subject = 'Directory of Open Access Journals - New application assigned to your group'
+ editor_subject = re.escape('Directory of Open Access Journals - New application ({}) assigned to your group'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
editor_email_matched = re.search(email_log_regex % (editor_template, editor_to, editor_subject),
info_stream_contents,
@@ -395,8 +396,7 @@ def test_01_maned_review_emails(self):
assEd_template = re.escape('email/notification_email.jinja2')
assEd_to = re.escape(models.Account.pull('associate_3').email)
- assEd_subject = 'Directory of Open Access Journals - New application assigned to you'
-
+ assEd_subject = re.escape('Directory of Open Access Journals - New application ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
re.DOTALL)
@@ -435,7 +435,7 @@ def test_01_maned_review_emails(self):
# * to the ManEd in charge of the assigned Editor Group, saying an application is ready
manEd_template = re.escape('email/notification_email.jinja2')
manEd_to = re.escape(acc.email)
- manEd_subject = 'Directory of Open Access Journals - Application marked as ready'
+ manEd_subject = re.escape('Directory of Open Access Journals - Application ({}) marked as ready'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
manEd_email_matched = re.search(email_log_regex % (manEd_template, manEd_to, manEd_subject),
info_stream_contents,
@@ -460,7 +460,7 @@ def test_01_maned_review_emails(self):
# * to the publisher, informing them of the journal's acceptance
publisher_template = re.escape('email/notification_email.jinja2')
publisher_to = re.escape(owner.email)
- publisher_subject = 'Directory of Open Access Journals - Your journal has been accepted'
+ publisher_subject = re.escape('Directory of Open Access Journals - Your journal ({}) has been accepted'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
publisher_email_matched = re.search(email_log_regex % (publisher_template, publisher_to, publisher_subject),
info_stream_contents,
@@ -511,7 +511,7 @@ def test_02_ed_review_emails(self):
# * to the ManEds, saying an application is ready
manEd_template = 'email/notification_email.jinja2'
manEd_to = re.escape("maned@example.com")
- manEd_subject = 'Application marked as ready'
+ manEd_subject = re.escape('Application ({}) marked as ready'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
manEd_email_matched = re.search(email_log_regex % (manEd_template, manEd_to, manEd_subject),
info_stream_contents,
@@ -546,7 +546,7 @@ def test_02_ed_review_emails(self):
# * and to the publisher informing them there's an editor assigned.
assEd_template = 'email/notification_email.jinja2'
assEd_to = re.escape(models.Account.pull('associate_3').email)
- assEd_subject = 'New application assigned to you'
+ assEd_subject = re.escape('New application ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
@@ -555,7 +555,7 @@ def test_02_ed_review_emails(self):
publisher_template = 'email/notification_email.jinja2'
publisher_to = re.escape(owner.email)
- publisher_subject = 'Your update request has been assigned an editor for review'
+ publisher_subject = re.escape('Your update request ({}) has been assigned to an editor for review'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
publisher_email_matched = re.search(email_log_regex % (publisher_template, publisher_to, publisher_subject),
info_stream_contents,
@@ -584,7 +584,7 @@ def test_02_ed_review_emails(self):
# * to the AssEd who's been assigned,
assEd_template = 'email/notification_email.jinja2'
assEd_to = re.escape(models.Account.pull('associate_2').email)
- assEd_subject = 'New application assigned to you'
+ assEd_subject = re.escape('New application ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
@@ -626,7 +626,7 @@ def test_02_ed_review_emails(self):
# * to the editor telling them an application has reverted to in progress
assoc_editor_template = re.escape('email/notification_email.jinja2')
assoc_editor_to = re.escape('associate@example.com')
- assoc_editor_subject = "One of your applications has not passed review"
+ assoc_editor_subject = re.escape('One of your applications ({}) has not passed review'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assoc_editor_email_matched = re.search(
email_log_regex % (assoc_editor_template, assoc_editor_to, assoc_editor_subject),
info_stream_contents,
@@ -668,7 +668,7 @@ def test_03_assoc_ed_review_emails(self):
# * to the publisher, notifying that an editor is viewing their application
publisher_template = re.escape('email/notification_email.jinja2')
publisher_to = re.escape(owner.email)
- publisher_subject = 'Directory of Open Access Journals - Your submission is under review'
+ publisher_subject = re.escape('Directory of Open Access Journals - Your submission ({}) is under review'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
publisher_email_matched = re.search(email_log_regex % (publisher_template, publisher_to, publisher_subject),
info_stream_contents,
@@ -689,7 +689,7 @@ def test_03_assoc_ed_review_emails(self):
# * to the editor, informing them an application has been completed by an Associate Editor
editor_template = re.escape('notification_email.jinja2')
editor_to = re.escape('eddie@example.com')
- editor_subject = 'Directory of Open Access Journals - Application marked as completed'
+ editor_subject = re.escape('Directory of Open Access Journals - Application ({}) marked as completed'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
editor_email_matched = re.search(email_log_regex % (editor_template, editor_to, editor_subject),
info_stream_contents,
re.DOTALL)
@@ -805,7 +805,7 @@ def test_01_maned_review_emails(self):
editor_template = re.escape('email/notification_email.jinja2')
editor_to = re.escape('eddie@example.com')
- editor_subject = "Application reverted to 'In Progress' by Managing Editor"
+ editor_subject = re.escape("Application ({}) reverted to 'In Progress' by Managing Editor".format(', '.join(issn for issn in processor.source.bibjson().issns())))
editor_email_matched = re.search(email_log_regex % (editor_template, editor_to, editor_subject),
info_stream_contents,
re.DOTALL)
@@ -822,8 +822,8 @@ def test_01_maned_review_emails(self):
assoc_editor_template = re.escape('email/notification_email.jinja2')
assoc_editor_to = re.escape('associate@example.com')
- assoc_editor_subject = self.svc.short_notification(
- ApplicationAssedInprogressNotify.ID) # "an application assigned to you has not passed review."
+ assoc_editor_subject = re.escape(self.svc.short_notification(
+ ApplicationAssedInprogressNotify.ID).replace("{issns}", ", ".join(issn for issn in processor.target.bibjson().issns())) + "\n") # "an application assigned to you has not passed review."
assoc_editor_email_matched = re.search(
email_log_regex % (assoc_editor_template, assoc_editor_to, assoc_editor_subject),
info_stream_contents,
@@ -876,7 +876,7 @@ def test_01_maned_review_emails(self):
editor_template = re.escape('email/notification_email.jinja2')
editor_to = re.escape('eddie@example.com')
- editor_subject = "Application reverted to 'In Progress' by Managing Editor"
+ editor_subject = re.escape("Application ({}) reverted to 'In Progress' by Managing Editor".format(', '.join(issn for issn in processor.source.bibjson().issns())))
editor_email_matched = re.search(email_log_regex % (editor_template, editor_to, editor_subject),
info_stream_contents,
re.DOTALL)
@@ -893,8 +893,8 @@ def test_01_maned_review_emails(self):
assoc_editor_template = re.escape('email/notification_email.jinja2')
assoc_editor_to = re.escape('associate@example.com')
- assoc_editor_subject = self.svc.short_notification(
- ApplicationAssedInprogressNotify.ID) # "an application assigned to you has not passed review."
+ assoc_editor_subject = re.escape(self.svc.short_notification(
+ ApplicationAssedInprogressNotify.ID).replace("{issns}", ", ".join(issn for issn in processor.source.bibjson().issns())) + "\n") # "an application assigned to you has not passed review."
assoc_editor_email_matched = re.search(
email_log_regex % (assoc_editor_template, assoc_editor_to, assoc_editor_subject),
info_stream_contents,
@@ -929,7 +929,7 @@ def test_01_maned_review_emails(self):
# * and to the publisher informing them there's an editor assigned.
assEd_template = 'email/notification_email.jinja2'
assEd_to = re.escape(models.Account.pull('associate_3').email)
- assEd_subject = 'New application assigned to you'
+ assEd_subject = re.escape('New application ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
@@ -938,7 +938,7 @@ def test_01_maned_review_emails(self):
publisher_template = 'email/notification_email.jinja2'
publisher_to = re.escape(owner.email)
- publisher_subject = 'Your update request has been assigned an editor for review'
+ publisher_subject = re.escape('Your update request ({}) has been assigned to an editor for review'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
publisher_email_matched = re.search(email_log_regex % (publisher_template, publisher_to, publisher_subject),
info_stream_contents,
@@ -970,7 +970,7 @@ def test_01_maned_review_emails(self):
# * to the AssEd who's been assigned
editor_template = re.escape('email/notification_email.jinja2')
editor_to = re.escape('eddie@example.com')
- editor_subject = 'New application assigned to your group'
+ editor_subject = re.escape('New application ({}) assigned to your group'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
editor_email_matched = re.search(email_log_regex % (editor_template, editor_to, editor_subject),
info_stream_contents,
@@ -979,7 +979,7 @@ def test_01_maned_review_emails(self):
assEd_template = 'email/notification_email.jinja2'
assEd_to = re.escape(models.Account.pull('associate_3').email)
- assEd_subject = 'New application assigned to you'
+ assEd_subject = re.escape('New application ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
@@ -1007,7 +1007,7 @@ def test_01_maned_review_emails(self):
# * to the ManEds, saying an application is ready
manEd_template = 'email/notification_email.jinja2'
manEd_to = re.escape("maned@example.com")
- manEd_subject = 'Application marked as ready'
+ manEd_subject = re.escape('Application ({}) marked as ready'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
manEd_email_matched = re.search(email_log_regex % (manEd_template, manEd_to, manEd_subject),
info_stream_contents,
@@ -1034,7 +1034,7 @@ def test_01_maned_review_emails(self):
# * to the journal contact, informing them of the journal's acceptance
publisher_template = 'email/notification_email.jinja2'
publisher_to = re.escape(owner.email)
- publisher_subject = 'Update request accepted'
+ publisher_subject = re.escape('Update request ({}) accepted'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
publisher_email_matched = re.search(email_log_regex % (publisher_template, publisher_to, publisher_subject),
info_stream_contents,
@@ -1084,7 +1084,7 @@ def test_02_ed_review_emails(self):
# * to the ManEds, saying an application is ready
manEd_template = 'email/notification_email.jinja2'
manEd_to = re.escape("maned@example.com")
- manEd_subject = 'Application marked as ready'
+ manEd_subject = re.escape('Application ({}) marked as ready'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
manEd_email_matched = re.search(email_log_regex % (manEd_template, manEd_to, manEd_subject),
info_stream_contents,
@@ -1118,7 +1118,7 @@ def test_02_ed_review_emails(self):
# * and to the publisher informing them there's an editor assigned.
assEd_template = 'email/notification_email.jinja2'
assEd_to = re.escape(models.Account.pull('associate_3').email)
- assEd_subject = 'New application assigned to you'
+ assEd_subject = re.escape('New application ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
@@ -1127,7 +1127,7 @@ def test_02_ed_review_emails(self):
publisher_template = 'email/notification_email.jinja2'
publisher_to = re.escape(owner.email)
- publisher_subject = 'Your update request has been assigned an editor for review'
+ publisher_subject = re.escape('Your update request ({}) has been assigned to an editor for review'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
publisher_email_matched = re.search(email_log_regex % (publisher_template, publisher_to, publisher_subject),
info_stream_contents,
@@ -1156,7 +1156,7 @@ def test_02_ed_review_emails(self):
# * to the AssEd who's been assigned,
assEd_template = 'email/notification_email.jinja2'
assEd_to = re.escape(models.Account.pull('associate_2').email)
- assEd_subject = 'New application assigned to you'
+ assEd_subject = re.escape('New application ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
@@ -1197,7 +1197,7 @@ def test_02_ed_review_emails(self):
# * to the associate editor, informing them the application has been bounced back to in progress.
assoc_editor_template = re.escape('email/notification_email.jinja2')
assoc_editor_to = re.escape('associate@example.com')
- assoc_editor_subject = "One of your applications has not passed review"
+ assoc_editor_subject = re.escape('One of your applications ({}) has not passed review'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assoc_editor_email_matched = re.search(
email_log_regex % (assoc_editor_template, assoc_editor_to, assoc_editor_subject),
info_stream_contents,
@@ -1243,7 +1243,7 @@ def test_03_assoc_ed_review_emails(self):
# * to the publisher, notifying that an editor is viewing their application
publisher_template = re.escape('email/notification_email.jinja2')
publisher_to = re.escape(owner.email)
- publisher_subject = 'Your submission is under review'
+ publisher_subject = re.escape('Your submission ({}) is under review'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
publisher_email_matched = re.search(email_log_regex % (publisher_template, publisher_to, publisher_subject),
info_stream_contents,
@@ -1264,7 +1264,7 @@ def test_03_assoc_ed_review_emails(self):
# * to the editor, informing them an application has been completed by an Associate Editor
editor_template = re.escape('email/notification_email.jinja2')
editor_to = re.escape('eddie@example.com')
- editor_subject = "Application marked as completed"
+ editor_subject = re.escape("Application ({}) marked as completed".format(', '.join(issn for issn in processor.source.bibjson().issns())))
editor_email_matched = re.search(email_log_regex % (editor_template, editor_to, editor_subject),
info_stream_contents,
re.DOTALL)
@@ -1332,7 +1332,7 @@ def test_01_maned_review_emails(self):
# * to the AssEd who's been assigned,
editor_template = re.escape('email/notification_email.jinja2')
editor_to = re.escape('eddie@example.com')
- editor_subject = 'Directory of Open Access Journals - New journal assigned to your group'
+ editor_subject = re.escape('Directory of Open Access Journals - New journal ({}) assigned to your group'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
editor_email_matched = re.search(email_log_regex % (editor_template, editor_to, editor_subject),
info_stream_contents,
@@ -1341,7 +1341,7 @@ def test_01_maned_review_emails(self):
assEd_template = re.escape('email/notification_email.jinja2')
assEd_to = re.escape(models.Account.pull('associate_3').email)
- assEd_subject = 'Directory of Open Access Journals - New journal assigned to you'
+ assEd_subject = re.escape('Directory of Open Access Journals - New journal ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
@@ -1372,7 +1372,7 @@ def test_02_ed_review_emails(self):
# * to the AssEd who's been assigned
assEd_template = re.escape('email/notification_email.jinja2')
assEd_to = re.escape(models.Account.pull('associate_2').email)
- assEd_subject = 'Directory of Open Access Journals - New journal assigned to you'
+ assEd_subject = re.escape('Directory of Open Access Journals - New journal ({}) assigned to you'.format(', '.join(issn for issn in processor.source.bibjson().issns())))
assEd_email_matched = re.search(email_log_regex % (assEd_template, assEd_to, assEd_subject),
info_stream_contents,
diff --git a/doajtest/unit/test_bll_todo_top_todo_assed.py b/doajtest/unit/test_bll_todo_top_todo_assed.py
new file mode 100644
index 0000000000..99f9462393
--- /dev/null
+++ b/doajtest/unit/test_bll_todo_top_todo_assed.py
@@ -0,0 +1,140 @@
+from parameterized import parameterized
+from combinatrix.testintegration import load_parameter_sets
+
+from doajtest.fixtures import ApplicationFixtureFactory, AccountFixtureFactory, EditorGroupFixtureFactory
+from doajtest.helpers import DoajTestCase
+from portality import constants
+from portality import models
+from portality.bll import DOAJ
+from portality.bll import exceptions
+from portality.lib.paths import rel2abs
+from portality.lib import dates
+
+
+def load_cases():
+ return load_parameter_sets(rel2abs(__file__, "..", "matrices", "bll_todo_assed"), "top_todo_assed", "test_id",
+ {"test_id" : []})
+
+
+EXCEPTIONS = {
+ "ArgumentException" : exceptions.ArgumentException
+}
+
+
+class TestBLLTopTodoAssed(DoajTestCase):
+
+ def setUp(self):
+ super(TestBLLTopTodoAssed, self).setUp()
+ self.svc = DOAJ.todoService()
+
+ def tearDown(self):
+ super(TestBLLTopTodoAssed, self).tearDown()
+
+ @parameterized.expand(load_cases)
+ def test_top_todo(self, name, kwargs):
+
+ account_arg = kwargs.get("account")
+ raises_arg = kwargs.get("raises")
+
+ categories = [
+ "todo_associate_follow_up_old",
+ "todo_associate_progress_stalled",
+ "todo_associate_start_pending",
+ "todo_associate_all_applications"
+ ]
+
+ category_args = {
+ cat : (
+ int(kwargs.get(cat)),
+ int(kwargs.get(cat + "_order") if kwargs.get(cat + "_order") != "" else -1)
+ ) for cat in categories
+ }
+
+ ###############################################
+ ## set up
+
+ apps = []
+ w = 7 * 24 * 60 * 60
+
+ account = 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)
+ elif account_arg == "editor":
+ asource = AccountFixtureFactory.make_editor_source()
+ account = models.Account(**asource)
+ elif account_arg == "assed":
+ asource = AccountFixtureFactory.make_assed1_source()
+ account = models.Account(**asource)
+ elif account_arg == "no_role":
+ asource = AccountFixtureFactory.make_publisher_source()
+ account = models.Account(**asource)
+
+ # Applications that we expect to see reported for some tests
+ ############################################################
+
+ # an application created more than 6 weeks ago
+ self.build_application("assed_follow_up_old", 2 * w, 7 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps)
+
+ # an application that was last updated over 3 weeks ago
+ self.build_application("assed_stalled", 4 * w, 4 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps)
+
+ # an application that was modifed recently into the pending status
+ self.build_application("assed_start_pending", 2 * w, 2 * w, constants.APPLICATION_STATUS_PENDING, apps)
+
+ # an application that is otherwise normal
+ self.build_application("assed_all_applications", 2 * w, 2 * w, constants.APPLICATION_STATUS_IN_PROGRESS, apps)
+
+ models.Application.blockall([(ap.id, ap.last_updated) for ap in apps])
+
+ # size = int(size_arg)
+ size=25
+
+ raises = None
+ if raises_arg:
+ raises = EXCEPTIONS[raises_arg]
+
+ ###########################################################
+ # Execution
+
+ if raises is not None:
+ with self.assertRaises(raises):
+ self.svc.top_todo(account, size)
+ else:
+ todos = self.svc.top_todo(account, size)
+
+ actions = {}
+ positions = {}
+ for i, todo in enumerate(todos):
+ for aid in todo["action_id"]:
+ if aid not in actions:
+ actions[aid] = 0
+ actions[aid] += 1
+ if aid not in positions:
+ positions[aid] = []
+ positions[aid].append(i + 1)
+
+ for k, v in category_args.items():
+ 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
+ assert len(positions.get(k, [])) == 0
+
+ def build_application(self, id, lmu_diff, cd_diff, status, app_registry, additional_fn=None):
+ source = ApplicationFixtureFactory.make_application_source()
+ ap = models.Application(**source)
+ ap.set_id(id)
+ ap.set_last_manual_update(dates.before_now(lmu_diff))
+ ap.set_created(dates.before_now(cd_diff))
+ ap.set_application_status(status)
+ ap.application_type = constants.APPLICATION_TYPE_NEW_APPLICATION
+
+ if additional_fn is not None:
+ additional_fn(ap)
+
+ ap.save()
+ app_registry.append(ap)
\ No newline at end of file
diff --git a/doajtest/unit/test_bll_todo_top_todo_editor.py b/doajtest/unit/test_bll_todo_top_todo_editor.py
new file mode 100644
index 0000000000..d952a8709d
--- /dev/null
+++ b/doajtest/unit/test_bll_todo_top_todo_editor.py
@@ -0,0 +1,143 @@
+from parameterized import parameterized
+from combinatrix.testintegration import load_parameter_sets
+
+from doajtest.fixtures import ApplicationFixtureFactory, AccountFixtureFactory, EditorGroupFixtureFactory
+from doajtest.helpers import DoajTestCase
+from portality import constants
+from portality import models
+from portality.bll import DOAJ
+from portality.bll import exceptions
+from portality.lib.paths import rel2abs
+from portality.lib import dates
+
+
+def load_cases():
+ return load_parameter_sets(rel2abs(__file__, "..", "matrices", "bll_todo_editor"), "top_todo_editor", "test_id",
+ {"test_id" : []})
+
+
+EXCEPTIONS = {
+ "ArgumentException" : exceptions.ArgumentException
+}
+
+
+class TestBLLTopTodoEditor(DoajTestCase):
+
+ def setUp(self):
+ super(TestBLLTopTodoEditor, self).setUp()
+ self.svc = DOAJ.todoService()
+
+ def tearDown(self):
+ super(TestBLLTopTodoEditor, self).tearDown()
+
+ @parameterized.expand(load_cases)
+ def test_top_todo(self, name, kwargs):
+
+ account_arg = kwargs.get("account")
+ raises_arg = kwargs.get("raises")
+
+ categories = [
+ "todo_editor_follow_up_old",
+ "todo_editor_stalled",
+ "todo_editor_completed",
+ "todo_editor_assign_pending"
+ ]
+
+ category_args = {
+ cat : (
+ int(kwargs.get(cat)),
+ int(kwargs.get(cat + "_order") if kwargs.get(cat + "_order") != "" else -1)
+ ) for cat in categories
+ }
+
+ ###############################################
+ ## set up
+
+ apps = []
+ w = 7 * 24 * 60 * 60
+
+ account = None
+ if account_arg == "admin":
+ asource = AccountFixtureFactory.make_managing_editor_source()
+ account = models.Account(**asource)
+ elif account_arg == "editor":
+ asource = AccountFixtureFactory.make_editor_source()
+ account = models.Account(**asource)
+ eg_source = EditorGroupFixtureFactory.make_editor_group_source(editor=account.id)
+ eg = models.EditorGroup(**eg_source)
+ eg.save(blocking=True)
+ elif account_arg == "assed":
+ asource = AccountFixtureFactory.make_assed1_source()
+ account = models.Account(**asource)
+ elif account_arg == "no_role":
+ asource = AccountFixtureFactory.make_publisher_source()
+ account = models.Account(**asource)
+
+ # Applications that we expect to see reported for some tests
+ ############################################################
+
+ # 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)
+
+ # 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)
+
+ # an application that was modifed recently into the completed status
+ self.build_application("editor_completed", 2 * w, 2 * w, constants.APPLICATION_STATUS_COMPLETED, apps)
+
+ # 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)
+
+ models.Application.blockall([(ap.id, ap.last_updated) for ap in apps])
+
+ # size = int(size_arg)
+ size=25
+
+ raises = None
+ if raises_arg:
+ raises = EXCEPTIONS[raises_arg]
+
+ ###########################################################
+ # Execution
+
+ if raises is not None:
+ with self.assertRaises(raises):
+ self.svc.top_todo(account, size)
+ else:
+ todos = self.svc.top_todo(account, size)
+
+ actions = {}
+ positions = {}
+ for i, todo in enumerate(todos):
+ for aid in todo["action_id"]:
+ if aid not in actions:
+ actions[aid] = 0
+ actions[aid] += 1
+ if aid not in positions:
+ positions[aid] = []
+ positions[aid].append(i + 1)
+
+ for k, v in category_args.items():
+ 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
+ assert len(positions.get(k, [])) == 0
+
+ def build_application(self, id, lmu_diff, cd_diff, status, app_registry, additional_fn=None):
+ source = ApplicationFixtureFactory.make_application_source()
+ ap = models.Application(**source)
+ ap.set_id(id)
+ ap.set_last_manual_update(dates.before_now(lmu_diff))
+ ap.set_created(dates.before_now(cd_diff))
+ ap.set_application_status(status)
+ ap.application_type = constants.APPLICATION_TYPE_NEW_APPLICATION
+
+ if additional_fn is not None:
+ additional_fn(ap)
+
+ ap.save()
+ app_registry.append(ap)
\ No newline at end of file
diff --git a/doajtest/unit/test_bll_todo_top_todo.py b/doajtest/unit/test_bll_todo_top_todo_maned.py
similarity index 85%
rename from doajtest/unit/test_bll_todo_top_todo.py
rename to doajtest/unit/test_bll_todo_top_todo_maned.py
index e9139a7709..d2800bb464 100644
--- a/doajtest/unit/test_bll_todo_top_todo.py
+++ b/doajtest/unit/test_bll_todo_top_todo_maned.py
@@ -3,16 +3,16 @@
from doajtest.fixtures import ApplicationFixtureFactory, AccountFixtureFactory, EditorGroupFixtureFactory
from doajtest.helpers import DoajTestCase
+from portality import constants
+from portality import models
from portality.bll import DOAJ
from portality.bll import exceptions
-from portality import models
-from portality import constants
from portality.lib.paths import rel2abs
from portality.lib import dates
def load_cases():
- return load_parameter_sets(rel2abs(__file__, "..", "matrices", "bll_todo"), "top_todo", "test_id",
+ return load_parameter_sets(rel2abs(__file__, "..", "matrices", "bll_todo_maned"), "top_todo_maned", "test_id",
{"test_id" : []})
@@ -21,14 +21,14 @@ def load_cases():
}
-class TestBLLTopTodo(DoajTestCase):
+class TestBLLTopTodoManed(DoajTestCase):
def setUp(self):
- super(TestBLLTopTodo, self).setUp()
+ super(TestBLLTopTodoManed, self).setUp()
self.svc = DOAJ.todoService()
def tearDown(self):
- super(TestBLLTopTodo, self).tearDown()
+ super(TestBLLTopTodoManed, self).tearDown()
@parameterized.expand(load_cases)
def test_top_todo(self, name, kwargs):
@@ -38,7 +38,10 @@ def test_top_todo(self, name, kwargs):
categories = [
"todo_maned_stalled",
- "todo_maned_follow_up_old"
+ "todo_maned_follow_up_old",
+ "todo_maned_ready",
+ "todo_maned_completed",
+ "todo_maned_assign_pending"
]
category_args = {
@@ -52,7 +55,7 @@ def test_top_todo(self, name, kwargs):
## set up
apps = []
- w = 7*24*60*60
+ w = 7 * 24 * 60 * 60
account = None
if account_arg == "admin":
@@ -75,7 +78,7 @@ 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)
# 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)
@@ -87,8 +90,11 @@ def test_top_todo(self, name, kwargs):
self.build_application("maned_completed", 3 * w, 3 * w, constants.APPLICATION_STATUS_COMPLETED, apps)
# 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)
+ def assign_pending(ap):
+ ap.remove_editor()
+
+ self.build_application("maned_assign_pending", 4 * w, 4 * w, constants.APPLICATION_STATUS_PENDING, apps,
+ assign_pending)
# Applications that should never be reported
############################################
@@ -99,7 +105,7 @@ def assign_pending(ap): ap.remove_editor()
# 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)
# an application that is old but rejected
# counter to maned_stalled
@@ -131,7 +137,8 @@ def assign_pending(ap): ap.remove_editor()
# maned_ready
# maned_completed
# maned_assign_pending
- self.build_application("not_assign_pending", 1 * w, 1 * w, constants.APPLICATION_STATUS_PENDING, apps, assign_pending)
+ self.build_application("not_assign_pending", 1 * w, 1 * w, constants.APPLICATION_STATUS_PENDING, apps,
+ assign_pending)
# pending application with assed assigned
# counter to maned_assign_pending
@@ -139,8 +146,11 @@ def assign_pending(ap): ap.remove_editor()
# pending application with no editor group assigned
# counter to maned_assign_pending
- def noeditorgroup(ap): ap.remove_editor_group()
- self.build_application("pending_assed_assigned", 3 * w, 3 * w, constants.APPLICATION_STATUS_PENDING, apps, noeditorgroup)
+ def noeditorgroup(ap):
+ ap.remove_editor_group()
+
+ self.build_application("pending_assed_assigned", 3 * w, 3 * w, constants.APPLICATION_STATUS_PENDING, apps,
+ noeditorgroup)
# application with no assed, but not pending
# counter to maned_assign_pending
@@ -148,6 +158,9 @@ def noeditorgroup(ap): ap.remove_editor_group()
models.Application.blockall([(ap.id, ap.last_updated) for ap in apps])
+ # size = int(size_arg)
+ size=25
+
raises = None
if raises_arg:
raises = EXCEPTIONS[raises_arg]
@@ -157,9 +170,9 @@ def noeditorgroup(ap): ap.remove_editor_group()
if raises is not None:
with self.assertRaises(raises):
- todos = self.svc.top_todo(account, 25)
+ self.svc.top_todo(account, size)
else:
- todos = self.svc.top_todo(account, 25)
+ todos = self.svc.top_todo(account, size)
actions = {}
positions = {}
@@ -172,8 +185,6 @@ def noeditorgroup(ap): ap.remove_editor_group()
positions[aid] = []
positions[aid].append(i + 1)
- # this is where we look through all the categories, look for the expectations on the
- # action counts and positions in the result set and test them
for k, v in category_args.items():
assert actions.get(k, 0) == v[0]
if v[1] > -1:
@@ -188,7 +199,9 @@ def build_application(self, id, lmu_diff, cd_diff, status, app_registry, additio
ap.set_last_manual_update(dates.before_now(lmu_diff))
ap.set_created(dates.before_now(cd_diff))
ap.set_application_status(status)
+
if additional_fn is not None:
additional_fn(ap)
+
ap.save()
app_registry.append(ap)
\ No newline at end of file
diff --git a/doajtest/unit/test_formrender.py b/doajtest/unit/test_formrender.py
index 1d655420af..f57ee83a61 100644
--- a/doajtest/unit/test_formrender.py
+++ b/doajtest/unit/test_formrender.py
@@ -6,11 +6,16 @@
# Form context for basic test
################################################################
+
class TestForm(Form):
+ __test__ = False # Prevent collection by PyTest
one = StringField("One")
two = StringField("Two")
+
class TestRenderer(Renderer):
+ __test__ = False # Prevent collection by PyTest
+
def __init__(self):
super(TestRenderer, self).__init__()
self.FIELD_GROUPS = {
@@ -20,7 +25,10 @@ def __init__(self):
]
}
+
class TestContext(FormContext):
+ __test__ = False # Prevent collection by PyTest
+
def data2form(self):
self.form = TestForm(formdata=self.form_data)
diff --git a/doajtest/unit/test_oaipmh.py b/doajtest/unit/test_oaipmh.py
index b65d319bd0..bab8102499 100644
--- a/doajtest/unit/test_oaipmh.py
+++ b/doajtest/unit/test_oaipmh.py
@@ -245,7 +245,7 @@ def test_06_identify(self):
records = t.xpath('/oai:OAI-PMH/oai:Identify', namespaces=self.oai_ns)
assert len(records) == 1
assert records[0].xpath('//oai:repositoryName', namespaces=self.oai_ns)[0].text == 'Directory of Open Access Journals'
- assert records[0].xpath('//oai:adminEmail', namespaces=self.oai_ns)[0].text == 'sysadmin@cottagelabs.com'
+ assert records[0].xpath('//oai:adminEmail', namespaces=self.oai_ns)[0].text == 'helpdesk+oai@doaj.org'
assert records[0].xpath('//oai:granularity', namespaces=self.oai_ns)[0].text == 'YYYY-MM-DDThh:mm:ssZ'
def test_07_bad_verb(self):
diff --git a/portality/app.py b/portality/app.py
index 9f6b4d2466..ad2b58a5af 100644
--- a/portality/app.py
+++ b/portality/app.py
@@ -46,6 +46,7 @@
from portality.view.status import blueprint as status
from portality.lib.normalise import normalise_doi
from portality.view.dashboard import blueprint as dashboard
+from portality.view.tours import blueprint as tours
if app.config.get("DEBUG", False) and app.config.get("TESTDRIVE_ENABLED", False):
from portality.view.testdrive import blueprint as testdrive
@@ -73,6 +74,7 @@
app.register_blueprint(apply, url_prefix='/apply') # ~~-> Apply:Blueprint~~
app.register_blueprint(jct, url_prefix="/jct") # ~~-> JCT:Blueprint~~
app.register_blueprint(dashboard, url_prefix="/dashboard") #~~-> Dashboard:Blueprint~~
+app.register_blueprint(tours, url_prefix="/tours") # ~~-> Tours:Blueprint~~
app.register_blueprint(oaipmh) # ~~-> OAIPMH:Blueprint~~
app.register_blueprint(openurl) # ~~-> OpenURL:Blueprint~~
@@ -313,6 +315,32 @@ def maned_of():
return dict(maned_of=maned_of)
+@app.context_processor
+def editor_of_wrapper():
+ def editor_of():
+ # ~~-> EditorGroup:Model ~~
+ egs = []
+ assignments = {}
+ if current_user.has_role("editor"):
+ egs = models.EditorGroup.groups_by_editor(current_user.id)
+ if len(egs) > 0:
+ assignments = models.Application.assignment_to_editor_groups(egs)
+ return egs, assignments
+ return dict(editor_of=editor_of)
+
+@app.context_processor
+def associate_of_wrapper():
+ def associate_of():
+ # ~~-> EditorGroup:Model ~~
+ egs = []
+ assignments = {}
+ if current_user.has_role("associate_editor"):
+ egs = models.EditorGroup.groups_by_associate(current_user.id)
+ if len(egs) > 0:
+ assignments = models.Application.assignment_to_editor_groups(egs)
+ return egs, assignments
+ return dict(associate_of=associate_of)
+
# ~~-> Account:Model~~
# ~~-> AuthNZ:Feature~~
@app.before_request
diff --git a/portality/bll/doaj.py b/portality/bll/doaj.py
index 4d80815395..584daa9b8f 100644
--- a/portality/bll/doaj.py
+++ b/portality/bll/doaj.py
@@ -117,3 +117,12 @@ def backgroundTaskStatusService(cls):
"""
from portality.bll.services import background_task_status
return background_task_status.BackgroundTaskStatusService()
+
+ @classmethod
+ def tourService(cls):
+ """
+ Obtain an instance of the tour service ~~->Tour:Service~~
+ :return: SiteService
+ """
+ from portality.bll.services import tour
+ return tour.TourService()
diff --git a/portality/bll/services/authorisation.py b/portality/bll/services/authorisation.py
index 0f1bef51d0..75d0a2b607 100644
--- a/portality/bll/services/authorisation.py
+++ b/portality/bll/services/authorisation.py
@@ -71,6 +71,9 @@ def can_edit_application(self, account, application):
return True
# now check whether the user is the editor of the editor group
+ if not application.editor_group:
+ return False
+
eg = models.EditorGroup.pull_by_key("name", application.editor_group)
if eg is not None and eg.editor == account.id:
return True
diff --git a/portality/bll/services/background_task_status.py b/portality/bll/services/background_task_status.py
index 486fdb1d84..ae0c6b7908 100644
--- a/portality/bll/services/background_task_status.py
+++ b/portality/bll/services/background_task_status.py
@@ -95,7 +95,7 @@ def create_queues_status(self, queue_name) -> dict:
# prepare for err_msgs
limited_sec = app.config.get('BG_MONITOR_LAST_COMPLETED', {}).get(queue_name)
if limited_sec is None:
- app.logger.warn(f'BG_MONITOR_LAST_COMPLETED for {queue_name} not found ')
+ app.logger.warning(f'BG_MONITOR_LAST_COMPLETED for {queue_name} not found ')
err_msgs = []
if limited_sec is not None and last_completed_date:
diff --git a/portality/bll/services/todo.py b/portality/bll/services/todo.py
index 375d4a3986..8a0a54b51c 100644
--- a/portality/bll/services/todo.py
+++ b/portality/bll/services/todo.py
@@ -66,6 +66,7 @@ def top_todo(self, account, size=25):
Returns the top number of todo items for a given user
:param account:
+ :param size:
:return:
"""
# first validate the incoming arguments to ensure that we've got the right thing
@@ -73,16 +74,43 @@ def top_todo(self, account, size=25):
{"arg" : account, "instance" : models.Account, "allow_none" : False, "arg_name" : "account"}
], exceptions.ArgumentException)
-
queries = []
if account.has_role("admin"):
maned_of = models.EditorGroup.groups_by_maned(account.id)
- queries.append(TodoRules.maned_stalled(size, maned_of))
queries.append(TodoRules.maned_follow_up_old(size, maned_of))
+ queries.append(TodoRules.maned_stalled(size, maned_of))
queries.append(TodoRules.maned_ready(size, maned_of))
queries.append(TodoRules.maned_completed(size, maned_of))
queries.append(TodoRules.maned_assign_pending(size, maned_of))
+ 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]
+ maned_groups = [g for g in groups if g.maned == account.id]
+ if len(groups) > 0:
+ queries.append(TodoRules.editor_follow_up_old(groups, size))
+ queries.append(TodoRules.editor_stalled(groups, size))
+ queries.append(TodoRules.editor_completed(groups, size))
+
+ # for groups where the user is not the maned for a group, given them the assign
+ # pending todos at the regular priority
+ if len(regular_groups) > 0:
+ queries.append(TodoRules.editor_assign_pending(regular_groups, size))
+
+ # for groups where the user IS the maned for a group, give them the assign
+ # pending todos at a lower priority
+ if len(maned_groups) > 0:
+ qi = TodoRules.editor_assign_pending(maned_groups, size)
+ queries.append((constants.TODO_EDITOR_ASSIGN_PENDING_LOW_PRIORITY, qi[1], qi[2], -2))
+
+ if account.has_role(constants.ROLE_ASSOCIATE_EDITOR):
+ queries.extend([
+ TodoRules.associate_follow_up_old(account.id, size),
+ TodoRules.associate_stalled(account.id, size),
+ TodoRules.associate_start_pending(account.id, size),
+ TodoRules.associate_all_applications(account.id, size)
+ ])
+
todos = []
for aid, q, sort, boost in queries:
applications = models.Application.object_query(q=q.query())
@@ -102,10 +130,12 @@ def top_todo(self, account, size=25):
return todos
def _rationalise_todos(self, todos, size):
- boosted = list(filter(lambda x: x["boost"], todos))
- unboosted = list(filter(lambda x: not x["boost"], todos))
+ boost_groups = sorted(list(set([x["boost"] for x in todos])), reverse=True)
- stds = sorted(boosted, key=lambda x: x['date']) + sorted(unboosted, key=lambda x: x['date'])
+ stds = []
+ for bg in boost_groups:
+ group = list(filter(lambda x: x["boost"] == bg, todos))
+ stds += sorted(group, key=lambda x: x['date'])
id_map = {}
removals = []
@@ -139,7 +169,7 @@ def maned_stalled(cls, size, maned_of):
sort="last_manual_update",
size=size
)
- return constants.TODO_MANED_STALLED, stalled, "last_manual_update", False
+ return constants.TODO_MANED_STALLED, stalled, "last_manual_update", 0
@classmethod
def maned_follow_up_old(cls, size, maned_of):
@@ -154,7 +184,7 @@ def maned_follow_up_old(cls, size, maned_of):
sort="created_date",
size=size
)
- return constants.TODO_MANED_FOLLOW_UP_OLD, follow_up_old, "created_date", False
+ return constants.TODO_MANED_FOLLOW_UP_OLD, follow_up_old, "created_date", 0
@classmethod
def maned_ready(cls, size, maned_of):
@@ -166,7 +196,7 @@ def maned_ready(cls, size, maned_of):
sort="last_manual_update",
size=size
)
- return constants.TODO_MANED_READY, ready, "last_manual_update", True
+ return constants.TODO_MANED_READY, ready, "created_date", 1
@classmethod
def maned_completed(cls, size, maned_of):
@@ -179,7 +209,7 @@ def maned_completed(cls, size, maned_of):
sort="last_manual_update",
size=size
)
- return constants.TODO_MANED_COMPLETED, completed, "last_manual_update", False
+ return constants.TODO_MANED_COMPLETED, completed, "last_manual_update", 0
@classmethod
def maned_assign_pending(cls, size, maned_of):
@@ -196,7 +226,155 @@ def maned_assign_pending(cls, size, maned_of):
sort="created_date",
size=size
)
- return constants.TODO_MANED_ASSIGN_PENDING, assign_pending, "last_manual_update", False
+ return constants.TODO_MANED_ASSIGN_PENDING, assign_pending, "last_manual_update", 0
+
+ @classmethod
+ def editor_stalled(cls, groups, size):
+ stalled = TodoQuery(
+ musts=[
+ TodoQuery.lmu_older_than(6),
+ TodoQuery.editor_groups(groups),
+ TodoQuery.is_new_application()
+ ],
+ must_nots=[
+ TodoQuery.status([
+ constants.APPLICATION_STATUS_ACCEPTED,
+ constants.APPLICATION_STATUS_REJECTED,
+ constants.APPLICATION_STATUS_READY
+ ])
+ ],
+ sort="last_manual_update",
+ size=size
+ )
+ return constants.TODO_EDITOR_STALLED, stalled, "last_manual_update", 0
+
+ @classmethod
+ def editor_follow_up_old(cls, groups, size):
+ follow_up_old = TodoQuery(
+ musts=[
+ TodoQuery.cd_older_than(8),
+ TodoQuery.editor_groups(groups),
+ TodoQuery.is_new_application()
+ ],
+ must_nots=[
+ TodoQuery.status([
+ constants.APPLICATION_STATUS_ACCEPTED,
+ constants.APPLICATION_STATUS_REJECTED,
+ constants.APPLICATION_STATUS_READY
+ ])
+ ],
+ sort="created_date",
+ size=size
+ )
+ return constants.TODO_EDITOR_FOLLOW_UP_OLD, follow_up_old, "created_date", 0
+
+ @classmethod
+ def editor_completed(cls, groups, size):
+ completed = TodoQuery(
+ musts=[
+ TodoQuery.status([constants.APPLICATION_STATUS_COMPLETED]),
+ TodoQuery.editor_groups(groups),
+ TodoQuery.is_new_application()
+ ],
+ sort="last_manual_update",
+ size=size
+ )
+ return constants.TODO_EDITOR_COMPLETED, completed, "last_manual_update", 1
+
+ @classmethod
+ def editor_assign_pending(cls, groups, size):
+ assign_pending = TodoQuery(
+ musts=[
+ TodoQuery.editor_groups(groups),
+ TodoQuery.status([constants.APPLICATION_STATUS_PENDING]),
+ TodoQuery.is_new_application()
+ ],
+ must_nots=[
+ TodoQuery.exists("admin.editor")
+ ],
+ sort="created_date",
+ size=size
+ )
+ return constants.TODO_EDITOR_ASSIGN_PENDING, assign_pending, "created_date", 1
+
+ @classmethod
+ def associate_stalled(cls, acc_id, size):
+ sort_field = "last_manual_update"
+ stalled = TodoQuery(
+ musts=[
+ TodoQuery.lmu_older_than(3),
+ TodoQuery.editor(acc_id),
+ TodoQuery.is_new_application()
+ ],
+ must_nots=[
+ TodoQuery.status([
+ constants.APPLICATION_STATUS_ACCEPTED,
+ constants.APPLICATION_STATUS_REJECTED,
+ constants.APPLICATION_STATUS_READY,
+ constants.APPLICATION_STATUS_COMPLETED
+ ])
+ ],
+ sort=sort_field,
+ size=size
+ )
+ return constants.TODO_ASSOCIATE_PROGRESS_STALLED, stalled, sort_field, 0
+
+ @classmethod
+ def associate_follow_up_old(cls, acc_id, size):
+ sort_field = "created_date"
+ follow_up_old = TodoQuery(
+ musts=[
+ TodoQuery.cd_older_than(6),
+ TodoQuery.editor(acc_id),
+ TodoQuery.is_new_application()
+ ],
+ must_nots=[
+ TodoQuery.status([
+ constants.APPLICATION_STATUS_ACCEPTED,
+ constants.APPLICATION_STATUS_REJECTED,
+ constants.APPLICATION_STATUS_READY,
+ constants.APPLICATION_STATUS_COMPLETED
+ ])
+ ],
+ sort=sort_field,
+ size=size
+ )
+ return constants.TODO_ASSOCIATE_FOLLOW_UP_OLD, follow_up_old, sort_field, 0
+
+ @classmethod
+ def associate_start_pending(cls, acc_id, size):
+ sort_field = "created_date"
+ assign_pending = TodoQuery(
+ musts=[
+ TodoQuery.editor(acc_id),
+ TodoQuery.status([constants.APPLICATION_STATUS_PENDING]),
+ TodoQuery.is_new_application()
+ ],
+ sort=sort_field,
+ size=size
+ )
+ return constants.TODO_ASSOCIATE_START_PENDING, assign_pending, sort_field, 0
+
+ @classmethod
+ def associate_all_applications(cls, acc_id, size):
+ sort_field = "created_date"
+ all = TodoQuery(
+ musts=[
+ TodoQuery.editor(acc_id),
+ TodoQuery.is_new_application()
+ ],
+ must_nots=[
+ TodoQuery.status([
+ constants.APPLICATION_STATUS_ACCEPTED,
+ constants.APPLICATION_STATUS_REJECTED,
+ constants.APPLICATION_STATUS_READY,
+ constants.APPLICATION_STATUS_COMPLETED
+ ])
+ ],
+ sort=sort_field,
+ size=size
+ )
+ return constants.TODO_ASSOCIATE_ALL_APPLICATIONS, all, sort_field, -1
class TodoQuery(object):
@@ -229,6 +407,14 @@ def query(self):
}
return q
+ @classmethod
+ def is_new_application(cls):
+ return {
+ "term": {
+ "admin.application_type.exact": constants.APPLICATION_TYPE_NEW_APPLICATION
+ }
+ }
+
@classmethod
def editor_group(cls, groups):
return {
@@ -273,6 +459,23 @@ def exists(cls, field):
}
}
+ @classmethod
+ def editor_groups(cls, groups):
+ gids = [g.name for g in groups]
+ return {
+ "terms": {
+ "admin.editor_group.exact": gids
+ }
+ }
+
+ @classmethod
+ def editor(cls, acc_id):
+ return {
+ "terms": {
+ "admin.editor.exact": [acc_id],
+ }
+ }
+
class GroupStatsQuery():
"""
diff --git a/portality/bll/services/tour.py b/portality/bll/services/tour.py
new file mode 100644
index 0000000000..31dae4767b
--- /dev/null
+++ b/portality/bll/services/tour.py
@@ -0,0 +1,27 @@
+from portality.core import app
+
+class TourService(object):
+ def activeTours(self, path, user):
+ tours = app.config.get("TOURS", {})
+ active_tours = []
+ for k, v in tours.items():
+ if path == k:
+ for tour in v:
+ if "roles" in tour:
+ if user is None:
+ continue
+ for r in tour.get("roles"):
+ if user.has_role(r):
+ active_tours.append(tour)
+ break
+ else:
+ active_tours.append(tour)
+ return active_tours
+
+ def validateContentId(self, content_id):
+ tours = app.config.get("TOURS", {})
+ for k, v in tours.items():
+ for tour in v:
+ if tour.get("content_id") == content_id:
+ return True
+ return False
\ No newline at end of file
diff --git a/portality/constants.py b/portality/constants.py
index ce7ed5e406..2f2bb566b6 100644
--- a/portality/constants.py
+++ b/portality/constants.py
@@ -44,6 +44,18 @@
TODO_MANED_READY = "todo_maned_ready"
TODO_MANED_COMPLETED = "todo_maned_completed"
TODO_MANED_ASSIGN_PENDING = "todo_maned_assign_pending"
+TODO_EDITOR_STALLED = "todo_editor_stalled"
+TODO_EDITOR_FOLLOW_UP_OLD = "todo_editor_follow_up_old"
+TODO_EDITOR_COMPLETED = "todo_editor_completed"
+TODO_EDITOR_ASSIGN_PENDING = "todo_editor_assign_pending"
+TODO_EDITOR_ASSIGN_PENDING_LOW_PRIORITY = "todo_editor_assign_pending_low_priority"
+TODO_ASSOCIATE_PROGRESS_STALLED = "todo_associate_progress_stalled"
+TODO_ASSOCIATE_FOLLOW_UP_OLD = "todo_associate_follow_up_old"
+TODO_ASSOCIATE_START_PENDING = "todo_associate_start_pending"
+TODO_ASSOCIATE_ALL_APPLICATIONS = "todo_associate_all_applications"
+
+# Roles
+ROLE_ASSOCIATE_EDITOR = 'associate_editor'
EVENT_ACCOUNT_CREATED = "account:created"
EVENT_ACCOUNT_PASSWORD_RESET = "account:password_reset"
diff --git a/portality/core.py b/portality/core.py
index 0a09313632..c74493a9a0 100644
--- a/portality/core.py
+++ b/portality/core.py
@@ -9,7 +9,7 @@
from lxml import etree
from portality import settings, constants, datasets
-from portality.bll import exceptions
+from portality.bll import exceptions, DOAJ
from portality.error_handler import setup_error_logging
from portality.lib import es_data_mapping, dates, paths
from portality.ui.debug_toolbar import DoajDebugToolbar
@@ -227,11 +227,11 @@ def initialise_index(app, conn, only_mappings=None):
:return:
"""
if not app.config['INITIALISE_INDEX']:
- app.logger.warn('INITIALISE_INDEX config var is not True, initialise_index command cannot run')
+ app.logger.warning('INITIALISE_INDEX config var is not True, initialise_index command cannot run')
return
if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False):
- app.logger.warn("System is in READ-ONLY mode, initialise_index command cannot run")
+ app.logger.warning("System is in READ-ONLY mode, initialise_index command cannot run")
return
# get the app mappings
@@ -294,6 +294,8 @@ def setup_jinja(app):
app.jinja_env.globals['dates'] = dates
#~~->Datasets:Data~~
app.jinja_env.globals['datasets'] = datasets
+ # ~~->DOAJ:Service~~
+ app.jinja_env.globals['services'] = DOAJ
_load_data(app)
#~~->CMS:DataStore~~
app.jinja_env.loader = FileSystemLoader([app.config['BASE_FILE_PATH'] + '/templates',
diff --git a/portality/crosswalks/application_form.py b/portality/crosswalks/application_form.py
index c92e41ae18..812ad089af 100644
--- a/portality/crosswalks/application_form.py
+++ b/portality/crosswalks/application_form.py
@@ -40,7 +40,7 @@ class ApplicationFormXWalk(JournalGenericXWalk):
"oa_statement_url" : "bibjson.ref.oa_statement",
"journal_url" : "bibjson.ref.journal",
"aims_scope_url" : "bibjson.ref.aims_scope",
- "editorial_board_url" : "bibjon.editorial.board_url",
+ "editorial_board_url" : "bibjson.editorial.board_url",
"author_instructions_url" : "bibjson.ref.author_instructions",
"waiver_url" : "bibjson.waiver.url",
"persistent_identifiers" : "bibjson.pid_scheme.scheme",
diff --git a/portality/dao.py b/portality/dao.py
index 14c5ad125f..40f14adc7c 100644
--- a/portality/dao.py
+++ b/portality/dao.py
@@ -136,7 +136,7 @@ def save(self, retries=0, back_off_factor=1, differentiate=False, blocking=False
:return:
"""
if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False):
- app.logger.warn("System is in READ-ONLY mode, save command cannot run")
+ app.logger.warning("System is in READ-ONLY mode, save command cannot run")
return
if retries > app.config.get("ES_RETRY_HARD_LIMIT", 1000): # an arbitrary large number
@@ -220,7 +220,7 @@ def save(self, retries=0, back_off_factor=1, differentiate=False, blocking=False
def delete(self):
if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False):
- app.logger.warn("System is in READ-ONLY mode, delete command cannot run")
+ app.logger.warning("System is in READ-ONLY mode, delete command cannot run")
return
# r = requests.delete(self.target() + self.id)
@@ -313,7 +313,7 @@ def bulk(cls, documents: List[dict], idkey='id', refresh=False, action='index',
"""
# ~~->ReadOnlyMode:Feature~~
if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False):
- app.logger.warn("System is in READ-ONLY mode, bulk command cannot run")
+ app.logger.warning("System is in READ-ONLY mode, bulk command cannot run")
return
if action not in ['index', 'update', 'delete']:
@@ -363,7 +363,7 @@ def refresh(cls):
:return:
"""
if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False):
- app.logger.warn("System is in READ-ONLY mode, refresh command cannot run")
+ app.logger.warning("System is in READ-ONLY mode, refresh command cannot run")
return
# r = requests.post(cls.target() + '_refresh', headers=CONTENT_TYPE_JSON)
@@ -449,7 +449,7 @@ def send_query(cls, qobj, retry=50, **kwargs):
@classmethod
def remove_by_id(cls, id):
if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False):
- app.logger.warn("System is in READ-ONLY mode, delete_by_id command cannot run")
+ app.logger.warning("System is in READ-ONLY mode, delete_by_id command cannot run")
return
# r = requests.delete(cls.target() + id)
@@ -461,7 +461,7 @@ def remove_by_id(cls, id):
@classmethod
def delete_by_query(cls, query):
if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False):
- app.logger.warn("System is in READ-ONLY mode, delete_by_query command cannot run")
+ app.logger.warning("System is in READ-ONLY mode, delete_by_query command cannot run")
return
#r = requests.delete(cls.target() + "_query", data=json.dumps(query))
@@ -472,7 +472,7 @@ def delete_by_query(cls, query):
@classmethod
def destroy_index(cls):
if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False):
- app.logger.warn("System is in READ-ONLY mode, destroy_index command cannot run")
+ app.logger.warning("System is in READ-ONLY mode, destroy_index command cannot run")
return
# if app.config['ELASTIC_SEARCH_INDEX_PER_TYPE']:
diff --git a/portality/events/consumers/application_assed_assigned_notify.py b/portality/events/consumers/application_assed_assigned_notify.py
index 54b5c72c9b..954a359832 100644
--- a/portality/events/consumers/application_assed_assigned_notify.py
+++ b/portality/events/consumers/application_assed_assigned_notify.py
@@ -38,7 +38,9 @@ def consume(cls, event):
journal_title=application.bibjson().title,
group_name=application.editor_group
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
notification.action = url_for("editor.application", application_id=application.id)
svc.notify(notification)
diff --git a/portality/events/consumers/application_assed_inprogress_notify.py b/portality/events/consumers/application_assed_inprogress_notify.py
index 060b1d0034..652dc60f77 100644
--- a/portality/events/consumers/application_assed_inprogress_notify.py
+++ b/portality/events/consumers/application_assed_inprogress_notify.py
@@ -36,7 +36,9 @@ def consume(cls, event):
notification.created_by = cls.ID
notification.classification = constants.NOTIFICATION_CLASSIFICATION_STATUS_CHANGE
notification.long = svc.long_notification(cls.ID).format(application_title=application.bibjson().title)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
notification.action = url_for("editor.application", application_id=application.id)
svc.notify(notification)
diff --git a/portality/events/consumers/application_editor_completed_notify.py b/portality/events/consumers/application_editor_completed_notify.py
index 23c42b87c3..cd90c8bb51 100644
--- a/portality/events/consumers/application_editor_completed_notify.py
+++ b/portality/events/consumers/application_editor_completed_notify.py
@@ -58,7 +58,9 @@ def consume(cls, event):
application_title=application.bibjson().title,
associate_editor=associate_editor
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
notification.action = url_for("editor.application", application_id=application.id)
svc.notify(notification)
diff --git a/portality/events/consumers/application_editor_group_assigned_notify.py b/portality/events/consumers/application_editor_group_assigned_notify.py
index e496337c13..22b277d283 100644
--- a/portality/events/consumers/application_editor_group_assigned_notify.py
+++ b/portality/events/consumers/application_editor_group_assigned_notify.py
@@ -43,7 +43,9 @@ def consume(cls, event):
notification.long = svc.long_notification(cls.ID).format(
journal_name=application.bibjson().title
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
notification.action = url_for("editor.application", application_id=application.id)
diff --git a/portality/events/consumers/application_editor_inprogress_notify.py b/portality/events/consumers/application_editor_inprogress_notify.py
index 1fdbe029d8..0c929e3631 100644
--- a/portality/events/consumers/application_editor_inprogress_notify.py
+++ b/portality/events/consumers/application_editor_inprogress_notify.py
@@ -53,7 +53,9 @@ def consume(cls, event):
notification.long = svc.long_notification(cls.ID).format(
application_title=application.bibjson().title
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
notification.action = url_for("editor.application", application_id=application.id)
svc.notify(notification)
diff --git a/portality/events/consumers/application_maned_ready_notify.py b/portality/events/consumers/application_maned_ready_notify.py
index 798f204627..58b22384b8 100644
--- a/portality/events/consumers/application_maned_ready_notify.py
+++ b/portality/events/consumers/application_maned_ready_notify.py
@@ -49,7 +49,9 @@ def consume(cls, event):
application_title=application.bibjson().title,
editor=editor
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
notification.action = url_for("admin.application", application_id=application.id)
svc.notify(notification)
diff --git a/portality/events/consumers/application_publisher_accepted_notify.py b/portality/events/consumers/application_publisher_accepted_notify.py
index bf6ecca6f9..9bd34769c2 100644
--- a/portality/events/consumers/application_publisher_accepted_notify.py
+++ b/portality/events/consumers/application_publisher_accepted_notify.py
@@ -67,7 +67,9 @@ def consume(cls, event):
publisher_dashboard_url=app.config.get("BASE_URL") + url_for("publisher.journals"),
faq_url=app.config.get("BASE_URL") + url_for("doaj.faq")
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
notification.action = url_for("publisher.journals")
diff --git a/portality/events/consumers/application_publisher_assigned_notify.py b/portality/events/consumers/application_publisher_assigned_notify.py
index 22acf66d02..2349dca60c 100644
--- a/portality/events/consumers/application_publisher_assigned_notify.py
+++ b/portality/events/consumers/application_publisher_assigned_notify.py
@@ -64,7 +64,9 @@ def consume(cls, event):
application_date=dates.human_date(application.date_applied),
volunteers_url=app.config.get('BASE_URL', "https://doaj.org") + url_for("doaj.volunteers"),
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
# note that there is no action url
svc.notify(notification)
diff --git a/portality/events/consumers/application_publisher_created_notify.py b/portality/events/consumers/application_publisher_created_notify.py
index 743b67cbf4..14640a18e0 100644
--- a/portality/events/consumers/application_publisher_created_notify.py
+++ b/portality/events/consumers/application_publisher_created_notify.py
@@ -41,6 +41,8 @@ def consume(cls, event):
journal_url=application.bibjson().journal_url,
application_date=dates.human_date(application.date_applied),
volunteers_url=url_for("doaj.volunteers"))
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
svc.notify(notification)
diff --git a/portality/events/consumers/application_publisher_inprogress_notify.py b/portality/events/consumers/application_publisher_inprogress_notify.py
index f6391f96f5..659d5d31c2 100644
--- a/portality/events/consumers/application_publisher_inprogress_notify.py
+++ b/portality/events/consumers/application_publisher_inprogress_notify.py
@@ -46,6 +46,8 @@ def consume(cls, event):
date_applied=date_applied,
volunteers=volunteers
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
svc.notify(notification)
diff --git a/portality/events/consumers/application_publisher_quickreject_notify.py b/portality/events/consumers/application_publisher_quickreject_notify.py
index 0548b6d581..7608d18cbf 100644
--- a/portality/events/consumers/application_publisher_quickreject_notify.py
+++ b/portality/events/consumers/application_publisher_quickreject_notify.py
@@ -51,7 +51,9 @@ def consume(cls, event):
note=note if note is not None else "",
doaj_guide_url=app.config.get('BASE_URL', "https://doaj.org") + url_for("doaj.guide")
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
# there is no action url for this notification
diff --git a/portality/events/consumers/application_publisher_revision_notify.py b/portality/events/consumers/application_publisher_revision_notify.py
index d710fcb96d..143e0c2ccb 100644
--- a/portality/events/consumers/application_publisher_revision_notify.py
+++ b/portality/events/consumers/application_publisher_revision_notify.py
@@ -43,6 +43,8 @@ def consume(cls, event):
application_title=application.bibjson().title,
date_applied=date_applied
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
svc.notify(notification)
diff --git a/portality/events/consumers/journal_assed_assigned_notify.py b/portality/events/consumers/journal_assed_assigned_notify.py
index 00d76a39ba..c57d3d07d0 100644
--- a/portality/events/consumers/journal_assed_assigned_notify.py
+++ b/portality/events/consumers/journal_assed_assigned_notify.py
@@ -39,7 +39,9 @@ def consume(cls, event):
journal_name=journal.bibjson().title,
group_name=journal.editor_group
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in journal.bibjson().issns())
+ )
notification.action = url_for("editor.journal_page", journal_id=journal.id)
svc.notify(notification)
diff --git a/portality/events/consumers/journal_editor_group_assigned_notify.py b/portality/events/consumers/journal_editor_group_assigned_notify.py
index ac696375f4..217a404b80 100644
--- a/portality/events/consumers/journal_editor_group_assigned_notify.py
+++ b/portality/events/consumers/journal_editor_group_assigned_notify.py
@@ -44,7 +44,9 @@ def consume(cls, event):
notification.long = svc.long_notification(cls.ID).format(
journal_name=journal.bibjson().title
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in journal.bibjson().issns())
+ )
notification.action = url_for("editor.journal_page", journal_id=journal.id)
svc.notify(notification)
diff --git a/portality/events/consumers/update_request_publisher_accepted_notify.py b/portality/events/consumers/update_request_publisher_accepted_notify.py
index d9cd05d102..3484617232 100644
--- a/portality/events/consumers/update_request_publisher_accepted_notify.py
+++ b/portality/events/consumers/update_request_publisher_accepted_notify.py
@@ -67,7 +67,9 @@ def consume(cls, event):
application_date=dates.human_date(application.date_applied),
publisher_dashboard_url=app.config.get('BASE_URL', "https://doaj.org") + url_for("publisher.journals")
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
notification.action = url_for("publisher.journals")
diff --git a/portality/events/consumers/update_request_publisher_assigned_notify.py b/portality/events/consumers/update_request_publisher_assigned_notify.py
index 7d47eb3002..63254fbbc2 100644
--- a/portality/events/consumers/update_request_publisher_assigned_notify.py
+++ b/portality/events/consumers/update_request_publisher_assigned_notify.py
@@ -62,7 +62,9 @@ def consume(cls, event):
application_title=application.bibjson().title,
application_date=dates.human_date(application.date_applied)
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
# note that there is no action url
svc.notify(notification)
diff --git a/portality/events/consumers/update_request_publisher_rejected_notify.py b/portality/events/consumers/update_request_publisher_rejected_notify.py
index 6db89fc39b..8885891a96 100644
--- a/portality/events/consumers/update_request_publisher_rejected_notify.py
+++ b/portality/events/consumers/update_request_publisher_rejected_notify.py
@@ -65,7 +65,9 @@ def consume(cls, event):
title=application.bibjson().title,
date_applied=date_applied,
)
- notification.short = svc.short_notification(cls.ID)
+ notification.short = svc.short_notification(cls.ID).format(
+ issns=", ".join(issn for issn in application.bibjson().issns())
+ )
# there is no action url associated with this notification
diff --git a/portality/forms/application_forms.py b/portality/forms/application_forms.py
index 0065b765fc..d1f8a44d20 100644
--- a/portality/forms/application_forms.py
+++ b/portality/forms/application_forms.py
@@ -167,11 +167,22 @@ class FieldDefinitions:
"full_contents" # ~~^->FullContents:FormWidget~~
],
"contexts": {
+ "admin": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
+ },
"editor": {
- "disabled": True
+ "disabled": True,
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
},
"associate_editor": {
- "disabled": True
+ "disabled": True,
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
},
"update_request": {
"disabled": True
@@ -198,6 +209,21 @@ class FieldDefinitions:
"contexts": {
"update_request": {
"disabled": True
+ },
+ "admin": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
+ },
+ "associate_editor": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
+ },
+ "editor": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
}
}
}
@@ -457,6 +483,21 @@ class FieldDefinitions:
"contexts" : {
"bulk_edit" : {
"validate" : []
+ },
+ "admin": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
+ },
+ "associate_editor": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
+ },
+ "editor": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
}
}
}
@@ -505,6 +546,23 @@ class FieldDefinitions:
"a society or other type of institution, enter that here."],
"placeholder": "Type or select the society or institution’s name"
},
+ "contexts" : {
+ "admin": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
+ },
+ "associate_editor": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
+ },
+ "editor": {
+ "widgets": [
+ "click_to_copy", # ~~^-> ClickToCopy:FormWidget~~
+ ]
+ }
+ },
"widgets": [
"trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~
{"autocomplete": {"type" : "journal", "field": "bibjson.institution.name.exact"}},
@@ -770,7 +828,7 @@ class FieldDefinitions:
}
],
"widgets" : [
- "trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~
+ "trim_whitespace" # ~~^-> TrimWhitespace:FormWidget~~
],
"asynchronous_warning": [
{"warn_on_value": {"value": "None"}}
@@ -1376,7 +1434,7 @@ class FieldDefinitions:
],
"widgets": [
"trim_whitespace", # ~~^-> TrimWhitespace:FormWidget~~
- "clickable_url" # ~~^-> ClickableURL:FormWidget~~
+ "clickable_url", # ~~^-> ClickableURL:FormWidget~~
],
"contexts" : {
"public" : {
@@ -2936,6 +2994,7 @@ def wtforms(field, settings):
JAVASCRIPT_FUNCTIONS = {
"clickable_url": "formulaic.widgets.newClickableUrl", # ~~-> ClickableURL:FormWidget~~
+ "click_to_copy": "formulaic.widgets.newClickToCopy", # ~~-> ClickToCopy:FormWidget~~
"clickable_owner": "formulaic.widgets.newClickableOwner", # ~~-> ClickableOwner:FormWidget~~
"select": "formulaic.widgets.newSelect", # ~~-> SelectBox:FormWidget~~
"taglist": "formulaic.widgets.newTagList", # ~~-> TagList:FormWidget~~
diff --git a/portality/scripts/230609_find_articles_with_invalid_issns.py b/portality/scripts/230609_find_articles_with_invalid_issns.py
new file mode 100644
index 0000000000..8b857faa01
--- /dev/null
+++ b/portality/scripts/230609_find_articles_with_invalid_issns.py
@@ -0,0 +1,56 @@
+from portality import models
+from portality.bll.services import article as articlesvc
+from portality.bll import exceptions
+import csv
+
+IN_DOAJ = {
+ "query": {
+ "bool": {
+ "must": [
+ {"term": {"admin.in_doaj": True}}
+ ]
+ }
+ }
+}
+
+
+if __name__ == "__main__":
+
+ import argparse
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-o", "--out", help="output file path", required=True)
+ args = parser.parse_args()
+
+ with open(args.out, "w", encoding="utf-8") as f:
+ writer = csv.writer(f)
+ writer.writerow(["ID", "PISSN", "EISSN", "Journals found with article's PISSN", "In doaj?", "Journals found with article's EISSN", "In doaj?", "Error"])
+
+ for a in models.Article.iterate(q=IN_DOAJ, page_size=100, keepalive='5m'):
+ article = models.Article(_source=a)
+ bibjson = article.bibjson()
+ try:
+ articlesvc.ArticleService._validate_issns(bibjson)
+ except exceptions.ArticleNotAcceptable as e:
+ id = article.id
+ pissn = bibjson.get_identifiers("pissn")
+ eissn = bibjson.get_identifiers("eissn")
+ j_p = [j["id"] for j in models.Journal.find_by_issn(pissn)]
+ j_p_in_doaj = []
+ if (j_p):
+ for j in j_p:
+ jobj = models.Journal.pull(j)
+ if (jobj):
+ j_p_in_doaj.append(jobj.is_in_doaj())
+ else:
+ j_p_in_doaj.append("n/a")
+ j_e = [j["id"] for j in models.Journal.find_by_issn(eissn)]
+ j_e_in_doaj = []
+ if (j_e):
+ for j in j_e:
+ jobj = models.Journal.pull(j)
+ if (jobj):
+ j_e_in_doaj.append(jobj.is_in_doaj())
+ else:
+ j_e_in_doaj.append("n/a")
+ writer.writerow([id, pissn, eissn, j_p, j_p_in_doaj, j_e, j_e_in_doaj, str(e)])
diff --git a/portality/scripts/application_status_report.py b/portality/scripts/application_status_report.py
index e94192c471..385c0e5b83 100644
--- a/portality/scripts/application_status_report.py
+++ b/portality/scripts/application_status_report.py
@@ -1,11 +1,22 @@
from portality import models
+from portality.lib import dates
import csv
+import os
"""This script generates a report of the status of applications in the DOAJ. The output is a CSV file with number
of applications in each status(new, accepted, rejected) for each year."""
+# constants
+SUBMITTED = "submitted"
+ACCEPTED = "accepted"
+REJECTED = "rejected"
+STATUS_ACCEPTED = "status:accepted"
+STATUS_REJECTED = "status:rejected"
-def date_applied_query(date_year):
+DATA = dict()
+DEFAULT_COUNTERS = [(SUBMITTED, 0), (ACCEPTED, 0), (REJECTED, 0)]
+
+def date_applied_query(date_year_from, date_year_to,):
return {
"query": {
"bool": {
@@ -18,68 +29,103 @@ def date_applied_query(date_year):
{
"range": {
"admin.date_applied": {
- "gte": str(date_year) + "-01-01",
- "lte": str(date_year) + "-12-31"
+ "gte": str(date_year_from) + "-01-01",
+ "lte": str(date_year_to) + "-12-31"
}
}
}
]
}
- }
+ },
+ "track_total_hits": True
}
-def status_query(date_year, status):
+def status_query(resource_id):
return {
"query": {
"bool": {
"must": [
- {
+ {
+ "terms": {
+ "action": [STATUS_ACCEPTED, STATUS_REJECTED]
+ }
+ },
+ {
"term": {
- "action": "status:" + status
- }
- },
- {
- "range": {
- "created_date": {
- "gte": str(date_year) + "-01-01",
- "lte": str(date_year) + "-12-31"
- }
+ "resource_id": resource_id
}
}
]
}
- }
+ },
+ "sort": [
+ {
+ "last_updated": {
+ "order": "desc"
+ }
+ }
+ ],
+ "track_total_hits": True
}
+def update_submission_counter(year):
+ global DATA
+ submission_data = DATA.setdefault(str(year),dict(DEFAULT_COUNTERS))
+ submission_data[SUBMITTED] += 1
+
+def update_accepted_counter(year):
+ global DATA
+ submission_data = DATA.setdefault(str(year), dict(DEFAULT_COUNTERS))
+ submission_data[ACCEPTED] += 1
+
+def update_rejected_counter(year):
+ global DATA
+ submission_data = DATA.setdefault(str(year), dict(DEFAULT_COUNTERS))
+ submission_data[REJECTED] += 1
+
+def write_applications_count_to_file(output_dir, from_year, to_year):
+ global DATA
+ with open(os.path.join(output_dir,"application_status_report" + from_year + "_" + to_year + ".csv"), "w",
+ encoding="utf-8") as f:
+ writer = csv.writer(f)
+
+ for year, app_data in sorted(DATA.items()):
+ writer.writerow(["Year", year])
+ writer.writerow([])
+ writer.writerow(["Submitted", app_data[SUBMITTED]])
+ writer.writerow(["Accepted", app_data[ACCEPTED]])
+ writer.writerow(["Rejected", app_data[REJECTED]])
+ writer.writerow([])
+ writer.writerow([])
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
- parser.add_argument("-o", "--out", help="output file path")
- parser.add_argument("-y", "--year", help="year to filter by")
+ parser.add_argument("-o", "--out", help="output file directory path", required=True)
+ parser.add_argument("-fy", "--from_year", help="from-year to filter by", required=True)
+ parser.add_argument("-ty", "--to_year", help="to-year to filter by", required=True)
args = parser.parse_args()
- if not args.out:
- print("Please specify an output file path with the -o option")
- parser.print_help()
- exit()
- if not args.year:
- print("Please specify a year to filter the applications with the -y option")
- parser.print_help()
- exit()
-
- with open(args.out, "w", encoding="utf-8") as f:
- writer = csv.writer(f)
+ # Application id is saved as resource_id in Provenance model
+ # Iterate through all the applications and query Provenance with application id and status to retrieve the
+ # status of the applications
+ for record in models.Application.iterate(q=date_applied_query(args.from_year, args.to_year), page_size=20):
+ application_year = dates.parse(record["created_date"], dates.FMT_DATETIME_MS_STD).year
+ update_submission_counter(application_year)
- res = models.Application.query(q=date_applied_query(args.year), size=0)
- writer.writerow(["Submitted", res.get("hits", {}).get("total", {}).get("value", 0)])
+ provenance_res = models.Provenance.query(q=status_query(record["id"]), size=2)
+ if provenance_res["hits"]["total"]["value"] > 0:
+ provenance_record = provenance_res["hits"]["hits"][0]["_source"]
+ provenance_year = dates.parse(provenance_record["last_updated"], dates.FMT_DATETIME_MS_STD).year
+ action_status = provenance_record["action"]
- res = models.Provenance.query(q=status_query(args.year, "accepted"), size=0)
- writer.writerow(["Accepted", res.get("hits", {}).get("total", {}).get("value", 0)])
+ if action_status == STATUS_ACCEPTED:
+ update_accepted_counter(provenance_year)
+ elif action_status == STATUS_REJECTED:
+ update_rejected_counter(provenance_year)
- res = models.Provenance.query(q=status_query(args.year, "rejected"), size=0)
- writer.writerow(["Rejected", res.get("hits", {}).get("total", {}).get("value", 0)])
+ write_applications_count_to_file(args.out, args.from_year, args.to_year)
diff --git a/portality/scripts/applications_rejected_data.py b/portality/scripts/applications_rejected_data.py
new file mode 100644
index 0000000000..8100c19a96
--- /dev/null
+++ b/portality/scripts/applications_rejected_data.py
@@ -0,0 +1,254 @@
+import csv
+import os
+from portality import models, constants
+from portality.lib import dates
+from portality.crosswalks.application_form import ApplicationFormXWalk
+from portality.forms.application_forms import FieldDefinitions as field
+
+
+"""This script generates files with the detailed information of applications which are rejected."""
+
+APP_FILE_HANDLES = {}
+UR_FILE_HANDLES = {}
+APP_WRITERS = {}
+UR_WRITERS = {}
+APP_RECORD_COUNTER = {}
+UR_RECORD_COUNTER = {}
+BASE_DIR_PATH = None
+FIELDS = [
+ field.TITLE,
+ {"name": "application_type", "label": "Type", "input": "text"},
+ {"name": "last_updated", "label": "Date", "input": "text"},
+ {"name": "notes", "label": "Notes", "input": "text"},
+ field.BOAI, field.OA_STATEMENT_URL, field.ALTERNATIVE_TITLE, field.JOURNAL_URL, field.PISSN, field.EISSN,
+ field.KEYWORDS, field.LANGUAGE, field.PUBLISHER_NAME, field.PUBLISHER_COUNTRY, field.INSTITUTION_NAME,
+ field.INSTITUTION_COUNTRY, field.LICENSE, field.LICENSE_TERMS_URL, field.LICENSE_DISPLAY,
+ field.LICENSE_DISPLAY_EXAMPLE_URL,
+ field.COPYRIGHT_AUTHOR_RETAINS, field.COPYRIGHT_URL, field.REVIEW_PROCESS, field.REVIEW_URL,
+ field.PLAGIARISM_DETECTION, field.PLAGIARISM_URL, field.AIMS_SCOPE_URL, field.EDITORIAL_BOARD_URL,
+ field.AUTHOR_INSTRUCTIONS_URL, field.PUBLICATION_TIME_WEEKS, field.APC, field.APC_CHARGES,
+ field.APC_URL, field.HAS_WAIVER, field.WAIVER_URL, field.HAS_OTHER_CHARGES, field.OTHER_CHARGES_URL,
+ field.PRESERVATION_SERVICE, field.PRESERVATION_SERVICE_URL, field.DEPOSIT_POLICY, field.DEPOSIT_POLICY_OTHER,
+ field.DEPOSIT_POLICY_URL, field.PERSISTENT_IDENTIFIERS, field.ORCID_IDS, field.OPEN_CITATIONS,
+ {"name": "id", "label": "Id", "input": "text"},
+]
+FIELD_NAMES = {"application_type":"admin.application_type", "last_updated":"last_updated", "notes":"admin.notes",
+ "publisher_name":"bibjson.publisher.name", "publisher_country": "bibjson.publisher.country",
+ "institution_name" : "bibjson.institution.name", "institution_country" : "bibjson.institution.country",
+ "other_charges_url": "bibjson.other_charges.url", "deposit_policy" : "bibjson.deposit_policy.has_policy"
+ }
+
+COLUMN_HEADINGS = [""]
+
+def application_query(date_year_from, date_year_to):
+ return {
+ "query": {
+ "bool": {
+ "must": [
+ {
+ "terms": {
+ "admin.application_type.exact": [constants.APPLICATION_TYPE_NEW_APPLICATION,
+ constants.APPLICATION_TYPE_UPDATE_REQUEST]
+ }
+ },
+ {
+ "range": {
+ "admin.date_applied": {
+ "gte": str(date_year_from) + "-01-01",
+ "lte": str(date_year_to) + "-12-31"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "track_total_hits": True
+ }
+
+
+def provenance_query(resource_id):
+ return {
+ "query": {
+ "bool": {
+ "must": [
+ {
+ "term": {
+ "action": constants.PROVENANCE_STATUS_REJECTED
+ }
+ },
+ {
+ "term": {
+ "resource_id": resource_id
+ }
+ }
+ ]
+ }
+ },
+ "sort": [
+ {
+ "last_updated": {
+ "order": "desc"
+ }
+ }
+ ],
+ "track_total_hits": True
+ }
+
+
+def get_file_handler(year, application_type):
+ global APP_FILE_HANDLES
+ global UR_FILE_HANDLES
+ global APP_WRITERS
+ global UR_WRITERS
+ # applications file handle
+ if application_type == constants.APPLICATION_TYPE_NEW_APPLICATION:
+ if year not in APP_WRITERS:
+ filename = os.path.join(BASE_DIR_PATH, "rejected_applications_" + str(year) + ".csv")
+ file_handle = open(filename, 'w')
+ APP_FILE_HANDLES[year] = file_handle
+ csv_writer = csv.writer(file_handle)
+ csv_writer.writerow(COLUMN_HEADINGS)
+ APP_WRITERS[year] = csv_writer
+ return APP_WRITERS[year]
+ else:
+ if year not in UR_WRITERS:
+ filename = os.path.join(BASE_DIR_PATH, "rejected_update_requests_" + str(year) + ".csv")
+ file_handle = open(filename, 'w')
+ UR_FILE_HANDLES[year] = file_handle
+ csv_writer = csv.writer(file_handle)
+ csv_writer.writerow(COLUMN_HEADINGS)
+ UR_WRITERS[year] = csv_writer
+ return UR_WRITERS[year]
+
+def get_record_counter(year, application_type):
+ global APP_RECORD_COUNTER
+ global UR_RECORD_COUNTER
+ # applications counter
+ if application_type == constants.APPLICATION_TYPE_NEW_APPLICATION:
+ record_number = APP_RECORD_COUNTER.setdefault(year, 0)
+ record_number += 1
+ APP_RECORD_COUNTER[year] = record_number
+ return record_number
+ # Update requests counter
+ else:
+ record_number = UR_RECORD_COUNTER.setdefault(year, 0)
+ record_number += 1
+ UR_RECORD_COUNTER[year] = record_number
+ return record_number
+
+def get_param_obj(obj, param):
+ if isinstance(obj, list):
+ value = ""
+ for k in obj:
+ if param in k:
+ value += str(k[param]) + ","
+ return value
+ elif param in obj:
+ return obj[param]
+ else:
+ return None
+
+def get_value(obj, key_field):
+
+ param_with_sub_fields = ""
+ if isinstance(key_field, list):
+ for k in key_field:
+ param_value = obj
+ params = k.split(".")
+ for param in params:
+ param_value = get_param_obj(param_value, param)
+ if param_value is None:
+ param_value = ""
+ param_with_sub_fields += str(param_value) + " "
+ return param_with_sub_fields
+ else:
+ param_value = obj
+ params = key_field.split(".")
+ for param in params:
+ param_value = get_param_obj(param_value, param)
+ if param_value is None:
+ return ""
+
+ return param_value
+
+def get_notes(record):
+ admin = record["admin"]
+ notes = ""
+ if "notes" in admin:
+ for note in admin["notes"]:
+ notes += note["date"] + " : " + note["note"] + "\n"
+ return notes
+
+def get_field_value(record, field_obj):
+ if "name" in field_obj:
+ name = field_obj["name"]
+ sub_field_names = []
+ if "subfields" in field_obj:
+ sub_field_names = field_obj["subfields"]
+ sub_fields = []
+ if name == "notes":
+ return get_notes(record)
+ if name in FIELD_NAMES:
+ field_name = FIELD_NAMES[name]
+ else:
+ field_name = ApplicationFormXWalk.formField2objectField(name)
+ for sub_field in sub_field_names:
+ sub_fields.append(ApplicationFormXWalk.formField2objectField(name+"."+sub_field))
+ key_field = sub_fields if len(sub_fields) > 0 else field_name
+ field_value = get_value(record, key_field)
+ if isinstance(field_value, bool):
+ return "Yes" if field_value else "No"
+ return field_value
+ raise Exception("Field name not found")
+
+def create_header():
+ for f in FIELDS:
+ COLUMN_HEADINGS.append(f["label"])
+
+def write_applications_data_to_file(year, record):
+
+ application_type = record["admin"]["application_type"]
+ writer = get_file_handler(year, application_type)
+ record_number = get_record_counter(year, application_type)
+
+ row = [record_number]
+ for f in FIELDS:
+ row.append(get_field_value(record, f))
+ writer.writerow(row)
+
+def execute(args):
+ try:
+ global BASE_DIR_PATH
+ BASE_DIR_PATH = args.out
+
+ # create file header
+ create_header()
+
+ # Application id is saved as resource_id in Provenance model
+ # Iterate through all the applications and query Provenance with application id and status
+ # to get rejected applications
+ for record in models.Application.iterate(q=application_query(args.from_year, args.to_year), page_size=20):
+ application_year = dates.parse(record["created_date"], dates.FMT_DATETIME_MS_STD).year
+
+ provenance_res = models.Provenance.query(q=provenance_query(record["id"]), size=2)
+ if provenance_res["hits"]["total"]["value"] > 0:
+ write_applications_data_to_file(application_year, record)
+
+ finally:
+ for file_handle in APP_FILE_HANDLES.values():
+ file_handle.close()
+ for file_handle in UR_FILE_HANDLES.values():
+ file_handle.close()
+
+
+if __name__ == "__main__":
+
+ import argparse
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-o", "--out", help="output file directory path", required=True)
+ parser.add_argument("-fy", "--from_year", help="from-year to filter by", required=True)
+ parser.add_argument("-ty", "--to_year", help="to-year to filter by", required=True)
+ args = parser.parse_args()
+
+ execute(args)
diff --git a/portality/settings.py b/portality/settings.py
index b50039200f..2bffdbab8a 100644
--- a/portality/settings.py
+++ b/portality/settings.py
@@ -9,7 +9,7 @@
# Application Version information
# ~~->API:Feature~~
-DOAJ_VERSION = "6.3.12"
+DOAJ_VERSION = "6.3.16"
API_VERSION = "3.0.1"
######################################
@@ -947,6 +947,8 @@
# OAI-PMH SETTINGS
# ~~->OAIPMH:Feature~~
+OAI_ADMIN_EMAIL = 'helpdesk+oai@doaj.org'
+
# ~~->OAIAriticleXML:Crosswalk~~
# ~~->OAIJournalXML:Crosswalk~~
OAI_DC_METADATA_FORMAT = {
@@ -1388,4 +1390,27 @@
PRESERVATION_PAGE_UNDER_MAINTENANCE = False
# report journals that discontinue in ... days (eg. 1 = tomorrow)
-DISCONTINUED_DATE_DELTA = 0
\ No newline at end of file
+DISCONTINUED_DATE_DELTA = 0
+
+##################################################
+# Feature tours currently active
+
+TOUR_COOKIE_PREFIX = "doaj_tour_"
+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"
+ }
+ ]
+}
diff --git a/portality/static/js/dashboard.js b/portality/static/js/dashboard.js
index e2eb7ad48e..4acc2ae0bd 100644
--- a/portality/static/js/dashboard.js
+++ b/portality/static/js/dashboard.js
@@ -13,7 +13,8 @@ doaj.dashboard = {
]
};
-doaj.dashboard.init = function() {
+doaj.dashboard.init = function(context) {
+ doaj.dashboard.context = context;
$(".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
@@ -53,9 +54,13 @@ doaj.dashboard.renderGroupInfo = function(data) {
// ~~-> EditorGroup:Model~~
// first remove the editor from the associates list if they are there
- let edInAssEd = data.editor_group.associates.indexOf(data.editor_group.editor)
- if (edInAssEd > -1) {
- data.editor_group.associates.splice(edInAssEd, 1);
+ 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 = []; // just to avoid having to keep checking it below
}
let allEditors = [data.editor_group.editor].concat(data.editor_group.associates);
@@ -85,20 +90,23 @@ doaj.dashboard.renderGroupInfo = function(data) {
urCount = data.by_editor[ed].update_requests || 0;
}
- let isEd = "";
- if (i === 0) { // first one in the list is always the editor
- isEd = " (Ed.)"
- }
- editorListFrag += `
`
- if (data.editors[ed].email) {
- editorListFrag += `${ed}${isEd}`
- }
- else {
- editorListFrag += `${ed}${isEd} (no email)`
- }
+ if (data.editors[ed]) {
+ let isEd = "";
+ if (i === 0) { // first one in the list is always the editor
+ isEd = " (Editor)"
+ }
- editorListFrag += `${appCount}applications
-
+ {% endif %}
{% endif %}
\ No newline at end of file
diff --git a/portality/templates/application_form/_field.html b/portality/templates/application_form/_field.html
index 8ebbaa9fe9..2e6ce601a5 100644
--- a/portality/templates/application_form/_field.html
+++ b/portality/templates/application_form/_field.html
@@ -8,6 +8,10 @@
{% if f.help("long_help") %}
More help
{% endif %}
+ {% if f.has_widget("click_to_copy") %}
+ Copy value
+ Copied!
+ {% endif %}
{% if f.optional %}(Optional){% endif %}
{% endset %}
diff --git a/portality/templates/application_form/_list.html b/portality/templates/application_form/_list.html
index bd75ea5795..02567676cc 100644
--- a/portality/templates/application_form/_list.html
+++ b/portality/templates/application_form/_list.html
@@ -7,6 +7,10 @@
{% if f.help("long_help") %}
More help
{% endif %}
+ {% if f.has_widget("click_to_copy") %}
+ Copy value
+ Copied!
+ {% endif %}
{% if f.optional %}(Optional){% endif %}
{% if f.get("hint") %}
{{ f.hint | safe }}
{% endif %}
diff --git a/portality/templates/application_form/editorial_form_body.html b/portality/templates/application_form/editorial_form_body.html
index 7912037689..53be1f5437 100644
--- a/portality/templates/application_form/editorial_form_body.html
+++ b/portality/templates/application_form/editorial_form_body.html
@@ -1,6 +1,5 @@
{% include "application_form/_edit_status.html" %}
{% include "application_form/_backend_validation.html" %}
-{% include "application_form/_contact.html" %}
{% import "application_form/_application_warning_msg.html" as _msg %}
{{ _msg.build_journal_withdrawn_deleted_msg(obj) }}
diff --git a/portality/templates/application_form/editorial_side_panel.html b/portality/templates/application_form/editorial_side_panel.html
index cde46632fc..34db9ba93e 100644
--- a/portality/templates/application_form/editorial_side_panel.html
+++ b/portality/templates/application_form/editorial_side_panel.html
@@ -1,3 +1,4 @@
+{% include "application_form/_contact.html" %}
{% if obj %}
LOCKED FOR EDITING UNTIL {{lock.expire_formatted()}}
{% if obj.application_status != constants.APPLICATION_STATUS_ACCEPTED %}
@@ -17,7 +18,6 @@
LOCKED FOR EDITING UNTIL
-
{% endif %}
- {% if current_user.has_role("editor") or current_user.has_role("associate_editor") %}
+ {% if (current_user.has_role("editor") or current_user.has_role("associate_editor")) and not current_user.has_role("admin") %}
+{% endif %}
\ No newline at end of file
diff --git a/portality/templates/layouts/dashboard_base.html b/portality/templates/layouts/dashboard_base.html
index d6366735b4..355463e4e2 100644
--- a/portality/templates/layouts/dashboard_base.html
+++ b/portality/templates/layouts/dashboard_base.html
@@ -4,6 +4,8 @@
{# we're potentially going need this in a few places in inherited files, so lets just put it here #}
{# ~~-> EditorGroup:Model ~~ #}
{% set managed_groups, maned_assignments = maned_of() %}
+{% set editor_of_groups, editor_of_assignments = editor_of() %}
+{% set associate_of_groups, associate_of_assignments = associate_of() %}
{# ~~Dashboard:Template~~ #}
{% block base_content %}
@@ -15,94 +17,163 @@
{% endif %}
-
DOAJ Dashboard
+
DOAJ Dashboard
- {% include 'dashboard/nav.html' %}
+ {% block nav %}
+ {% include 'dashboard/nav.html' %}
+ {% endblock %}
{% block extra_header %}{% endblock %}
{% block main_panel %}
- {% if request.path == "/dashboard/" %}