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 7b677ba74a..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
@@ -72,6 +76,11 @@ Instructions for reviewers:
- [ ] Developer
- [ ] Reviewer
+- Have CSS/style changes been implemented? If they are of a global scope (e.g. on base HTML elements) have the downstream impacts of the change in other areas of the system been considered?
+ - [ ] N/A
+ - [ ] Developer
+ - [ ] Reviewer
+
### Documentation
- FeatureMap annotations have been added
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 6aca840e49..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,5 +140,10 @@ 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: |
+ Journal "{title}" (id: {id}) will discontinue in {days} days.
+ short:
+ Journal discontinuing
\ No newline at end of file
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 3fb9f40049..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
@@ -55,28 +55,17 @@
2020: https://drive.google.com/file/d/1-4wTgvwCu_tvDv5NIoJhZi0QpQl-fpdB/view?usp=sharing
2022: https://drive.google.com/file/d/1xEUUxhqSE0OnKd_x8z1oLLRRrJrKAwXA/view?usp=sharing
-- name: Ilaria Fava
- role: Managing Editor
- photo: Ilaria.jpg
- bio: 'Ilaria is a librarian with several years of experience within the Open Access community in her home country of Italy, where she has dealt with Open Access issues at both national and international level. She also serves the Göttingen State and University Library working on Open Science projects.
-Based in Rome and Göttingen, Ilaria loves baking cakes; she speaks Italian, English, some Spanish and a little German.'
- coi:
- 2016: https://drive.google.com/file/d/0ByRf6PVViI-mY2dRZTR5eTFjQkk/view?usp=sharing&resourcekey=0-fPa6ce_HjfoVQqKGqWxLNw
- 2018: https://drive.google.com/file/d/1AMi0uIWHgEiaqmJLM7f_SFsiLEJENfjF/view?usp=sharing
- 2020: https://drive.google.com/file/d/1jWZKc6xjp3yfo6qjQWp6Yp3y-71ZHLth/view?usp=sharing
- 2022: https://drive.google.com/file/d/1_au6llN2ALPnkNTgUrzLrSTw8gundldJ/view?usp=sharing
-
- 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
@@ -96,7 +85,7 @@ Based in Rome and Göttingen, Ilaria loves baking cakes; she speaks Italian, Eng
- 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
@@ -120,14 +109,6 @@ Based in Rome and Göttingen, Ilaria loves baking cakes; she speaks Italian, Eng
2020: https://drive.google.com/file/d/1zU-lLB5W54E_QUm5uto5tqB6cZl83TAJ/view?usp=sharing
2022: https://drive.google.com/file/d/19rw-naMJqHkI5T7aDIDPUkwPutBdDpDm/view?usp=sharing
-- name: Louise Stoddard
- role: Communications Manager
- photo: louise.jpg
- bio: "Louise has over 15 years experience in communications and public relations for non-profit and international organisations. She holds a Masters in Journalism and a Bachelor of Science and Economics in International Development from The University of Wales, Swansea. Louise has worked for the United Nations for 8 years, also with academic and non-profit organisations promoting access to knowledge and information. In 2021 Louise joined DOAJ as the focal point for public relations and communications. Outside of work Louise can usually be found in her vegetable garden."
- coi:
- 2021: https://drive.google.com/file/d/1DmDsIkv-orjF7QGEVwqFDAl0EQx3qRXg/view?usp=sharing
- 2022: https://drive.google.com/file/d/1wOeX97BZGEX50orKo6TwlWmGsdbLFfBD/view?usp=sharing
-
- name: Luis Montilla
role: Managing Editor
photo: luis.jpeg
diff --git a/cms/data/volunteers.yml b/cms/data/volunteers.yml
index 01dbfe24a1..9e09159ad7 100644
--- a/cms/data/volunteers.yml
+++ b/cms/data/volunteers.yml
@@ -467,8 +467,8 @@ ass_ed:
- name: Kadri Kıran
area: Systematic Entomology
year_since:
- city: Tekirdag
- country: Turkey
+ city: Edirne
+ country: Türkiye
language: English, Turkish, German
photo: "Kadri Kiran.jpg"
diff --git a/cms/pages/about/at-20.md b/cms/pages/about/at-20.md
index e3dde2fd1f..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.
---
@@ -23,16 +25,17 @@ Further down the page is a historical timeline to give you a full overview of DO
[//]: # (NB. adding whitespace around the titles will break styling)
{.events .unstyled-list}
- {% include "includes/svg/at-20/theme_open.svg" %}
- - **[Registration is open](https://us02web.zoom.us/webinar/register/WN_-b000to3RZKexuFsJGJw1g#/registration)**
+ - **[Recording is available](https://www.youtube.com/watch?v=qnpSdX3eusk)**
- Name: _DOAJ at 20: Open_
- Date: 15th June 2023
- - Event Time: 13:00 UTC ([Check the event time](https://www.timeanddate.com/worldclock/fixedtime.html?iso=20230615T13&ah=1&am=30) where you are.)
+ - Event Time: 13:00 UTC
- Duration: 90 mins
- {% include "includes/svg/at-20/theme_global.svg" %}
+ - **[Registration is open](https://us02web.zoom.us/webinar/register/WN_fu42oi59S7GZ366rjyAUGg#/registration)**
- Name: _DOAJ at 20: Global_
- Date: _28th September 2023_
- - Event Time: to be confirmed
- - Duration: 90 mins
+ - Event Time: 13:00 UTC ([Check the event time](https://www.timeanddate.com/worldclock/fixedtime.html?iso=20230928T13&ah=1&am=30) where you are.)
+ - Duration: 2 hours
- {% include "includes/svg/at-20/theme_trusted.svg" %}
- Name: _DOAJ at 20: Trusted_
- Date: _7th December 2023_
@@ -41,27 +44,15 @@ Further down the page is a historical timeline to give you a full overview of DO
## Open
-Join us for the first of three events marking our 20th anniversary as a key open infrastructure. 'DOAJ at 20: Open' is free and open to researchers, librarians, research support staff, publishers, and anyone interested in open access!
-
-The event will build around the theme ‘open’, where our moderator (Abeni Wickham) will be chatting with our three guests: Lars Bjørnshauge, Mikael Laakso, and Nadine Buckland. The discussion will focus on their thoughts and aspirations on open scholarship. They will also explore the obstacles and challenges in adopting immediate open access.
-
-The event will last 90 minutes.
-
-**Abeni Wickham**
-
-Abeni was born in Guyana, South America and holds a PhD in Molecular Physics from Linkoping University. She left academia in 2018 to create SciFree, a software company with a mission to make research open to the public for free. SciFree currently serves 45 University Library customers in Sweden, Denmark, the United Kingdom and the USA. Besides building new tech platforms for university infrastructure, Abeni volunteers on the NASIG Digital Preservation committee, helps PhDs transition in their careers and enjoys surfing both actual waves and the Open Access wave worldwide.
-
-**Lars Bjørnshauge**
-
-Lars Bjørnshauge is the Director of Infrastructure Services for Open Access C.I.C (www.is4oa.org). A true open access champion, Lars is DOAJ’s founder and worked as the Managing Director until 2022. Previously, he has been the Deputy Director and Acting Director for the Technical Information Center of Denmark at the Technical University of Denmark. Lars has also been the Director of Libraries at Lund University in Sweden, and the Director of SPARC Europe. In addition to founding DOAJ, he has also co-founded OpenDOAR (the Directory of Open Access Repositories, DOAB (Directory of Open Access Books), and Think.Check.Submit. Lars was on the OASPA Board from 2012-2019.
+Our first of three events marking our 20th anniversary took place on the 15th June 2023. The event was built around the theme ‘open’, where our moderator (Abeni Wickham) had a coversation with our three guests: Lars Bjørnshauge, Mikael Laakso, and Nadine Buckland. The discussion focused on development and changes over the last 20 years, with reflections from all speakers on what the next years will bring. A recording of the event is [available on YouTube](https://www.youtube.com/watch?v=qnpSdX3eusk).
-**Mikael Laakso**
+## Global
-Mikael Laakso is an Associate Professor in Information Systems Science at Hanken School of Economics in Helsinki. He has been researching the changing landscape towards openness in scholarly publishing by studying combinations of bibliometrics, web metrics, business models, science policy, and author behaviour. Since the start of his research in this domain around 2009, DOAJ data has been instrumental to most of his research projects. In addition to research, Mikael has also been active in national and international working groups furthering various dimensions of open science.
+Our second event will be around the theme Global, where we will have eight lighting talks from speakers from around the world. Our moderator and DOAJ Ambassador, Ivonne Lujano, will introduce speakers and manage two Q&As, where the audience can ask our speakers questions. More information about the event and all the speakers can be found on the [registration page](https://us02web.zoom.us/webinar/register/WN_fu42oi59S7GZ366rjyAUGg#/registration).
-**Nadine D. Tulloch-Buckland**
+## Trusted
-Nadine D. Tulloch-Buckland is the former General Manager of the UWI Press, Senior Lecturer of the University of the West Indies and Director of Spoizer Content Agency Limited. She has over twenty years’ experience in scholarly publishing with specific emphasis on finance and business model development geared towards sustainability in scholarly publishing in the Caribbean. Nadine is the current Treasurer of ALPSP and former Treasurer/Director of AUPresses. She is an advocate for Sustainable Open Access Publishing.
+Our third and last DOAJ at 20 event will be around the theme Trusted. More information about this event will be available later in the year.
## Timeline
diff --git a/cms/pages/about/team.md b/cms/pages/about/team.md
index 1a5c81ee40..68a3200a48 100644
--- a/cms/pages/about/team.md
+++ b/cms/pages/about/team.md
@@ -8,4 +8,3 @@ featuremap: ~~Team:Fragment->TeamData:Template~~
---
-
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/fixtures/urls.py b/doajtest/fixtures/urls.py
index 0e44633849..0fa1f98302 100644
--- a/doajtest/fixtures/urls.py
+++ b/doajtest/fixtures/urls.py
@@ -3,11 +3,16 @@
"http://www.moonlight.com",
"https://www.cosmos.com#galaxy",
"https://www.cosmos.com/galaxy",
- "https://www.cosmos.com/galaxy#peanut"
+ "https://www.cosmos.com/galaxy#peanut",
+ "http://ftp.example.com/file%20name.txt"
]
INVALID_URL_LISTS = [
"ht:www",
"nonexistent.com",
- "https://www.doaj.org and https://www.reddit.com"
+ "https://www.doaj.org and https://www.reddit.com",
+ "http://www.doaj.org and www.doaj.org",
+"http://www.doaj.org, www.doaj.org",
+"http://www.doaj.org, https://www.doaj.org",
+"http://ftp.example.com/file name.txt"
]
\ No newline at end of file
diff --git a/doajtest/helpers.py b/doajtest/helpers.py
index f65c11f241..2224ebc4c7 100644
--- a/doajtest/helpers.py
+++ b/doajtest/helpers.py
@@ -120,6 +120,7 @@ class DoajTestCase(TestCase):
@classmethod
def setUpClass(cls) -> None:
+ import portality.app # noqa, needed to registing routes
cls.originals = patch_config(app, {
"STORE_IMPL": "portality.store.StoreLocal",
"STORE_LOCAL_DIR": paths.rel2abs(__file__, "..", "tmp", "store", "main", cls.__name__.lower()),
@@ -180,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 0a05f70530..935fe4504f 100644
--- a/doajtest/testbook/journal_form/maned_form.yml
+++ b/doajtest/testbook/journal_form/maned_form.yml
@@ -120,3 +120,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/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/testbook/public_site/ToC.yml b/doajtest/testbook/public_site/ToC.yml
new file mode 100644
index 0000000000..b481085cf9
--- /dev/null
+++ b/doajtest/testbook/public_site/ToC.yml
@@ -0,0 +1,15 @@
+suite: Public Site
+testset: ToC
+tests:
+- title: Test Correctly Displayed Discontinued Date
+ context:
+ role: anonymous
+ steps:
+ - step: To prepare to do this test make sure there are 3 journals publically available in DOAJ
+ one with discontinued date in the past
+ one with discontinued date in the future
+ one with discontinued date today
+ - step: Search for every journal from the list above
+ results:
+ - On the ToC of the journal with discontinued date in the past or today - the discontinued date is displayed
+ - On the ToC of the journal with discontinued date in the future - the discontinued date is not displayed
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/api_tests/test_apiv3_discovery.py b/doajtest/unit/api_tests/test_apiv3_discovery.py
index b847c48b5b..ffc94feacc 100644
--- a/doajtest/unit/api_tests/test_apiv3_discovery.py
+++ b/doajtest/unit/api_tests/test_apiv3_discovery.py
@@ -241,7 +241,7 @@ def test_03_applications(self):
for i in range(5):
a = models.Suggestion()
a.set_owner("owner")
- a.set_created(dates.format(dates.after(now, i)))
+ a.set_created(dates.format(dates.seconds_after(now, i)))
bj = a.bibjson()
bj.title = "Test Suggestion {x}".format(x=i)
bj.add_identifier(bj.P_ISSN, "{x}000-0000".format(x=i))
@@ -256,7 +256,7 @@ def test_03_applications(self):
for i in range(5):
a = models.Suggestion()
a.set_owner("stranger")
- a.set_created(dates.format(dates.after(now, i + 5)))
+ a.set_created(dates.format(dates.seconds_after(now, i + 5)))
bj = a.bibjson()
bj.title = "Test Suggestion {x}".format(x=i)
bj.add_identifier(bj.P_ISSN, "{x}000-0000".format(x=i))
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/event_consumers/test_journal_discontinuing_soon_notify.py b/doajtest/unit/event_consumers/test_journal_discontinuing_soon_notify.py
new file mode 100644
index 0000000000..f4f70f2f78
--- /dev/null
+++ b/doajtest/unit/event_consumers/test_journal_discontinuing_soon_notify.py
@@ -0,0 +1,86 @@
+from portality import models
+from portality import constants
+from portality.bll import exceptions
+from doajtest.helpers import DoajTestCase
+from doajtest.fixtures import JournalFixtureFactory, ApplicationFixtureFactory
+from portality.events.consumers.journal_discontinuing_soon_notify import JournalDiscontinuingSoonNotify
+from doajtest.fixtures import BackgroundFixtureFactory
+import time
+
+# Mock required to make application lookup work
+@classmethod
+def pull_application(cls, id):
+ app = models.Application(**ApplicationFixtureFactory.make_application_source())
+ return app
+
+@classmethod
+def pull_by_key(cls, key, value):
+ ed = models.EditorGroup()
+ acc = models.Account()
+ acc.set_id('testuser')
+ acc.set_email("test@example.com")
+ acc.save(blocking=True)
+ ed.set_maned(acc.id)
+ ed.save(blocking=True)
+
+ return ed
+
+class TestJournalDiscontinuingSoonNotify(DoajTestCase):
+ def setUp(self):
+ super(TestJournalDiscontinuingSoonNotify, self).setUp()
+ self.pull_application = models.Application.pull
+ models.Application.pull = pull_application
+ self.pull_by_key = models.EditorGroup.pull_by_key
+ models.EditorGroup.pull_by_key = pull_by_key
+
+ def tearDown(self):
+ super(TestJournalDiscontinuingSoonNotify, self).tearDown()
+ models.Application.pull = self.pull_application
+ models.EditorGroup.pull_by_key = self.pull_by_key
+
+ def test_consumes(self):
+
+ event = models.Event("test:event", context={"data" : {"1234"}})
+ assert not JournalDiscontinuingSoonNotify.consumes(event)
+
+ event = models.Event("test:event", context={"data": {}})
+ assert not JournalDiscontinuingSoonNotify.consumes(event)
+
+ event = models.Event(constants.EVENT_JOURNAL_DISCONTINUING_SOON)
+ assert not JournalDiscontinuingSoonNotify.consumes(event)
+
+ event = models.Event(constants.EVENT_JOURNAL_DISCONTINUING_SOON, context = {"journal": {"1234"}, "discontinue_date": "2002-22-02"})
+ assert JournalDiscontinuingSoonNotify.consumes(event)
+
+ def test_consume_success(self):
+ self._make_and_push_test_context("/")
+
+ source = BackgroundFixtureFactory.example()
+ bj = models.BackgroundJob(**source)
+ # bj.save(blocking=True)
+
+ acc = models.Account()
+ acc.set_id('testuser')
+ acc.set_email("test@example.com")
+ acc.add_role('admin')
+ acc.save(blocking=True)
+
+ source = JournalFixtureFactory.make_journal_source()
+ journal = models.Journal(**source)
+ journal.save(blocking=True)
+
+ event = models.Event(constants.BACKGROUND_JOB_FINISHED, context={"job" : bj.data, "journal" : journal.id})
+ JournalDiscontinuingSoonNotify.consume(event)
+
+ time.sleep(2)
+ ns = models.Notification.all()
+ assert len(ns) == 1
+
+ n = ns[0]
+ assert n.who == acc.id
+ assert n.created_by == JournalDiscontinuingSoonNotify.ID
+ assert n.classification == constants.NOTIFICATION_CLASSIFICATION_STATUS
+ assert n.long is not None
+ assert n.short is not None
+ assert n.action is not None
+ assert not n.is_seen()
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/doajtest/unit/test_task_discontinued_soon.py b/doajtest/unit/test_task_discontinued_soon.py
new file mode 100644
index 0000000000..3716151294
--- /dev/null
+++ b/doajtest/unit/test_task_discontinued_soon.py
@@ -0,0 +1,93 @@
+import unittest
+import datetime
+
+from doajtest.helpers import DoajTestCase, patch_config
+
+from portality import models
+from portality.tasks import find_discontinued_soon
+from portality.ui.messages import Messages
+from doajtest.fixtures import JournalFixtureFactory
+
+# Expect a notification for journals discontinuing in 1 days time (tomorrow)
+DELTA = 1
+
+
+class TestDiscontinuedSoon(DoajTestCase):
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ super().setUpClass()
+ cls.orig_config = patch_config(cls.app_test, {
+ 'DISCONTINUED_DATE_DELTA': DELTA
+ })
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ super().tearDownClass()
+ patch_config(cls.app_test, cls.orig_config)
+
+ @staticmethod
+ def _date_to_find():
+ return (datetime.datetime.today() + datetime.timedelta(days=DELTA)).strftime('%Y-%m-%d')
+
+ @staticmethod
+ def _date_too_late():
+ return (datetime.datetime.today() + datetime.timedelta(days=DELTA+1)).strftime('%Y-%m-%d')
+
+ def test_discontinued_soon_found(self):
+
+ # Both these should be found
+ journal_discontinued_to_found_1 = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=True))
+ journal_discontinued_to_found_1.set_id("1")
+ jbib = journal_discontinued_to_found_1.bibjson()
+ jbib.title = "Discontinued Tomorrow 1"
+ jbib.discontinued_date = self._date_to_find()
+ journal_discontinued_to_found_1.save(blocking=True)
+
+ journal_discontinued_to_found_2 = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=True))
+ journal_discontinued_to_found_2.set_id("2")
+ jbib = journal_discontinued_to_found_2.bibjson()
+ jbib.title = "Discontinued Tomorrow 2"
+ jbib.discontinued_date = self._date_to_find()
+ journal_discontinued_to_found_2.save(blocking=True)
+
+ # that shouldn't be found
+ journal_discontinued_too_late = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=True))
+ journal_discontinued_too_late.set_id("3")
+ jbib = journal_discontinued_too_late.bibjson()
+ jbib.title = "Discontinued In 2 days"
+ jbib.discontinued_date = self._date_too_late()
+ journal_discontinued_too_late.save(blocking=True)
+
+ job = find_discontinued_soon.FindDiscontinuedSoonBackgroundTask.prepare("system")
+ task = find_discontinued_soon.FindDiscontinuedSoonBackgroundTask(job)
+ task.run()
+
+ assert len(job.audit) == 3 # Journals 1 & 2, and a message to say notification is sent
+ assert job.audit[0]["message"] == Messages.DISCONTINUED_JOURNAL_FOUND_LOG.format(id="1")
+ assert job.audit[1]["message"] == Messages.DISCONTINUED_JOURNAL_FOUND_LOG.format(id="2")
+ assert job.audit[2]["message"] == Messages.DISCONTINUED_JOURNALS_FOUND_NOTIFICATION_SENT_LOG
+
+ def test_discontinued_soon_not_found(self):
+
+ # None of these should be found - this one discontinues in 2 days
+ journal_discontinued_too_late = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=True))
+ journal_discontinued_too_late.set_id("1")
+ jbib = journal_discontinued_too_late.bibjson()
+ jbib.title = "Discontinued In 2 days"
+ jbib.discontinued_date = self._date_too_late()
+ journal_discontinued_too_late.save(blocking=True)
+
+ # this one is not in doaj
+ journal_not_in_doaj = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=False))
+ journal_not_in_doaj.set_id("2")
+ jbib = journal_not_in_doaj.bibjson()
+ jbib.discontinued_date = self._date_to_find()
+ journal_not_in_doaj.save(blocking=True)
+
+ job = find_discontinued_soon.FindDiscontinuedSoonBackgroundTask.prepare("system")
+ task = find_discontinued_soon.FindDiscontinuedSoonBackgroundTask(job)
+ task.run()
+
+ assert len(job.audit) == 1
+ assert job.audit[0]["message"] == Messages.NO_DISCONTINUED_JOURNALS_FOUND_LOG
diff --git a/portality/app.py b/portality/app.py
index 4d1bf787c8..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~~
@@ -285,6 +287,10 @@ def form_diff_table_subject_expand(val):
return ", ".join(results)
+@app.template_filter("is_in_the_past")
+def is_in_the_past(dttm):
+ return dates.is_before(dttm, dates.today())
+
#######################################################
@@ -309,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/events.py b/portality/bll/services/events.py
index 2b27b85beb..3c5e96473c 100644
--- a/portality/bll/services/events.py
+++ b/portality/bll/services/events.py
@@ -21,6 +21,7 @@
from portality.events.consumers.journal_editor_group_assigned_notify import JournalEditorGroupAssignedNotify
from portality.events.consumers.application_publisher_inprogress_notify import ApplicationPublisherInprogressNotify
from portality.events.consumers.update_request_publisher_rejected_notify import UpdateRequestPublisherRejectedNotify
+from portality.events.consumers.journal_discontinuing_soon_notify import JournalDiscontinuingSoonNotify
class EventsService(object):
@@ -44,7 +45,8 @@ class EventsService(object):
JournalEditorGroupAssignedNotify,
UpdateRequestPublisherAcceptedNotify,
UpdateRequestPublisherAssignedNotify,
- UpdateRequestPublisherRejectedNotify
+ UpdateRequestPublisherRejectedNotify,
+ JournalDiscontinuingSoonNotify
]
def __init__(self):
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 ec908a4e82..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"
@@ -53,7 +65,9 @@
EVENT_APPLICATION_EDITOR_GROUP_ASSIGNED = "application:editor_group:assigned"
EVENT_JOURNAL_ASSED_ASSIGNED = "journal:assed:assigned"
EVENT_JOURNAL_EDITOR_GROUP_ASSIGNED = "journal:editor_group:assigned"
+EVENT_JOURNAL_DISCONTINUING_SOON = "journal:discontinuing_soon"
+NOTIFICATION_CLASSIFICATION_STATUS = "alert"
NOTIFICATION_CLASSIFICATION_STATUS_CHANGE = "status_change"
NOTIFICATION_CLASSIFICATION_ASSIGN = "assign"
NOTIFICATION_CLASSIFICATION_CREATE = "create"
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/combined.py b/portality/events/combined.py
new file mode 100644
index 0000000000..869d63ab88
--- /dev/null
+++ b/portality/events/combined.py
@@ -0,0 +1,11 @@
+from portality.events.shortcircuit import send_event as shortcircuit_send_event
+from portality.core import app
+
+
+def send_event(event):
+ try:
+ from portality.events.kafka_producer import send_event as kafka_send_event
+ kafka_send_event(event)
+ except Exception as e:
+ app.logger.exception("Failed to send event to Kafka. " + str(e))
+ shortcircuit_send_event(event)
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_discontinuing_soon_notify.py b/portality/events/consumers/journal_discontinuing_soon_notify.py
new file mode 100644
index 0000000000..11da31bb96
--- /dev/null
+++ b/portality/events/consumers/journal_discontinuing_soon_notify.py
@@ -0,0 +1,55 @@
+# ~~JournalDiscontinuingSoonNotify:Consumer~~
+import json
+import urllib.parse
+
+from portality.util import url_for
+from portality.events.consumer import EventConsumer
+from portality.core import app
+from portality import constants
+from portality import models
+from portality.bll import DOAJ, exceptions
+from portality.lib import edges
+from portality import dao
+
+class JournalDiscontinuingSoonNotify(EventConsumer):
+ ID = "journal:assed:discontinuing_soon:notify"
+
+ @classmethod
+ def consumes(cls, event):
+ return event.id == constants.EVENT_JOURNAL_DISCONTINUING_SOON and \
+ event.context.get("journal") is not None and \
+ event.context.get("discontinue_date") is not None
+
+ @classmethod
+ def consume(cls, event):
+ journal_id = event.context.get("journal")
+ discontinued_date = event.context.get("discontinue_date")
+
+ journal = models.Journal.pull(journal_id)
+ if journal is None:
+ return
+
+ if not journal.editor_group:
+ return
+
+ eg = models.EditorGroup.pull_by_key("name", journal.editor_group)
+ managing_editor = eg.maned
+ if not managing_editor:
+ return
+
+ # ~~-> Notifications:Service ~~
+ svc = DOAJ.notificationsService()
+
+ notification = models.Notification()
+ notification.who = managing_editor
+ notification.created_by = cls.ID
+ notification.classification = constants.NOTIFICATION_CLASSIFICATION_STATUS
+ notification.long = svc.long_notification(cls.ID).format(
+ days=app.config.get('DISCONTINUED_DATE_DELTA',0),
+ title=journal.bibjson().title,
+ id=journal.id
+ )
+ notification.short = svc.short_notification(cls.ID)
+ notification.action = url_for("admin.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/events/kafka_consumer.py b/portality/events/kafka_consumer.py
index 77c812b6e2..0ce1e1120e 100644
--- a/portality/events/kafka_consumer.py
+++ b/portality/events/kafka_consumer.py
@@ -11,13 +11,19 @@
app = faust.App('events', broker=broker, value_serializer='json')
topic = app.topic(topic_name)
+event_counter = 0
+
@app.agent(topic)
async def handle_event(stream):
+ global event_counter
with doajapp.test_request_context("/"):
svc = DOAJ.eventsService()
async for event in stream:
- svc.consume(Event(raw=json.loads(event)))
+ event_counter += 1
+ doajapp.logger.info(f"Kafka event count {event_counter}")
+ # TODO uncomment the following line once the Event model is fixed to Kafka
+ # svc.consume(Event(raw=json.loads(event)))
if __name__ == '__main__':
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/lib/csv_utils.py b/portality/lib/csv_utils.py
new file mode 100644
index 0000000000..c5a46f37fd
--- /dev/null
+++ b/portality/lib/csv_utils.py
@@ -0,0 +1,9 @@
+import csv
+from typing import Iterable, Union
+
+
+def read_all(csv_path, as_dict=False) -> Iterable[Union[list, dict]]:
+ reader = csv.DictReader if as_dict else csv.reader
+ with open(csv_path, 'r') as f:
+ for row in reader(f):
+ yield row
diff --git a/portality/lib/dates.py b/portality/lib/dates.py
index a1af162b0b..52f6b0a809 100644
--- a/portality/lib/dates.py
+++ b/portality/lib/dates.py
@@ -119,15 +119,27 @@ def before_now(seconds: int) -> datetime:
return before(now(), seconds)
-def after(timestamp, seconds) -> datetime:
+def seconds_after(timestamp, seconds) -> datetime:
return timestamp + timedelta(seconds=seconds)
+def seconds_after_now(seconds: int):
+ return seconds_after(datetime.utcnow(), seconds)
+
+
+def days_after(timestamp, days):
+ return timestamp + timedelta(days=days)
+
+
+def days_after_now(days: int):
+ return days_after(datetime.utcnow(), days)
+
+
def eta(since, sofar, total) -> str:
td = (now() - since).total_seconds()
spr = float(td) / float(sofar)
alltime = int(math.ceil(total * spr))
- fin = after(since, alltime)
+ fin = seconds_after(since, alltime)
return format(fin)
@@ -163,3 +175,13 @@ def day_ranges(fro: datetime, to: datetime) -> 'list[str]':
def human_date(stamp, string_format=FMT_DATE_HUMAN) -> str:
return reformat(stamp, out_format=string_format)
+
+def is_before(mydate, comparison=None):
+ if comparison is None:
+ comparison = datetime.utcnow()
+ if isinstance(mydate, str):
+ mydate = parse(mydate)
+ if isinstance(comparison, str):
+ comparison = parse(comparison)
+ return mydate < comparison
+
diff --git a/portality/migrate/20180106_1463_ongoing_updates/sync_journals_applications.py b/portality/migrate/20180106_1463_ongoing_updates/sync_journals_applications.py
index adb85410aa..4315618d51 100644
--- a/portality/migrate/20180106_1463_ongoing_updates/sync_journals_applications.py
+++ b/portality/migrate/20180106_1463_ongoing_updates/sync_journals_applications.py
@@ -52,7 +52,7 @@
app_created = application.created_timestamp
for journal in related_journals:
almu = application.last_manual_update_timestamp
- almu_adjusted = dates.after(almu, 3600)
+ almu_adjusted = dates.seconds_after(almu, 3600)
# do a load of reporting prep
jc_ac_diff = int((journal.created_timestamp - app_created).total_seconds())
diff --git a/portality/migrate/903_remove_blanks/README.md b/portality/migrate/903_remove_blanks/README.md
new file mode 100644
index 0000000000..833c2c102f
--- /dev/null
+++ b/portality/migrate/903_remove_blanks/README.md
@@ -0,0 +1,13 @@
+# Remove Blank
+
+remove blank from start or end of string in Journal and Application
+
+### Run
+```
+python portality/upgrade.py -u portality/migrate/903_remove_blanks/migrate.json
+```
+
+### verify
+```
+python -m portality.scripts.blank_field_finder
+```
\ No newline at end of file
diff --git a/portality/migrate/903_remove_blanks/__init__.py b/portality/migrate/903_remove_blanks/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/portality/migrate/903_remove_blanks/functions.py b/portality/migrate/903_remove_blanks/functions.py
new file mode 100644
index 0000000000..f36c435314
--- /dev/null
+++ b/portality/migrate/903_remove_blanks/functions.py
@@ -0,0 +1,21 @@
+def remove_blanks(obj) -> dict:
+ if not isinstance(obj, dict):
+ return obj
+
+ for k, v in obj.items():
+ if isinstance(v, dict):
+ obj[k] = remove_blanks(v)
+
+ elif isinstance(v, list):
+ if not v:
+ continue
+ if isinstance(v[0], dict):
+ obj[k] = [remove_blanks(item) for item in v]
+ elif isinstance(v[0], str):
+ obj[k] = [item.strip() for item in v]
+
+ elif isinstance(v, str) and v != v.strip():
+ print(f'remove blanks: {k} = [{v}]')
+ obj[k] = v.strip()
+
+ return obj
diff --git a/portality/migrate/903_remove_blanks/migrate.json b/portality/migrate/903_remove_blanks/migrate.json
new file mode 100644
index 0000000000..64d4a31842
--- /dev/null
+++ b/portality/migrate/903_remove_blanks/migrate.json
@@ -0,0 +1,13 @@
+{
+ "batch" : 10000,
+ "types": [
+ {
+ "type" : "journal",
+ "init_with_model" : false,
+ "keepalive" : "10m",
+ "functions" : [
+ "portality.migrate.903_remove_blanks.functions.remove_blanks"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/portality/models/v2/journal.py b/portality/models/v2/journal.py
index 41c50a7ce9..ac1ce42585 100644
--- a/portality/models/v2/journal.py
+++ b/portality/models/v2/journal.py
@@ -1131,4 +1131,4 @@ def query(self):
"sort" : [
{"created_date" : {"order" : "desc"}}
]
- }
\ No newline at end of file
+ }
diff --git a/portality/regex.py b/portality/regex.py
index 7c5773855d..a298a4731f 100644
--- a/portality/regex.py
+++ b/portality/regex.py
@@ -17,7 +17,15 @@
BIG_END_DATE_COMPILED = re.compile(BIG_END_DATE)
#~~URL:Regex~~
-HTTP_URL = r'^https?://([^/:]+\.[a-z]{2,63}|([0-9]{1,3}\.){3}[0-9]{1,3})(:[0-9]+)?(\/.*)?(#.*)?$'
+HTTP_URL = (
+ r'^(?:https?)://' # Scheme: http(s) or ftp
+ r'(?:[\w-]+\.)*[\w-]+' # Domain name (optional subdomains)
+ r'(?:\.[a-z]{2,})' # Top-level domain (e.g., .com, .org)
+ r'(?:\/[^\/\s]*)*' # Path (optional)
+ r'(?:\?[^\/\s]*)?' # Query string (optional)
+ r'(?:#[^\/\s]*)?$' # Fragment (optional)
+)
+
HTTP_URL_COMPILED = re.compile(HTTP_URL, re.IGNORECASE)
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
new file mode 100644
index 0000000000..385c0e5b83
--- /dev/null
+++ b/portality/scripts/application_status_report.py
@@ -0,0 +1,131 @@
+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"
+
+DATA = dict()
+DEFAULT_COUNTERS = [(SUBMITTED, 0), (ACCEPTED, 0), (REJECTED, 0)]
+
+def date_applied_query(date_year_from, date_year_to,):
+ return {
+ "query": {
+ "bool": {
+ "must": [
+ {
+ "term": {
+ "admin.application_type.exact": "new_application"
+ }
+ },
+ {
+ "range": {
+ "admin.date_applied": {
+ "gte": str(date_year_from) + "-01-01",
+ "lte": str(date_year_to) + "-12-31"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "track_total_hits": True
+ }
+
+
+def status_query(resource_id):
+ return {
+ "query": {
+ "bool": {
+ "must": [
+ {
+ "terms": {
+ "action": [STATUS_ACCEPTED, STATUS_REJECTED]
+ }
+ },
+ {
+ "term": {
+ "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 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()
+
+
+ # 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)
+
+ 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"]
+
+ if action_status == STATUS_ACCEPTED:
+ update_accepted_counter(provenance_year)
+ elif action_status == STATUS_REJECTED:
+ update_rejected_counter(provenance_year)
+
+ 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/scripts/blank_field_finder.py b/portality/scripts/blank_field_finder.py
new file mode 100644
index 0000000000..028332b1a6
--- /dev/null
+++ b/portality/scripts/blank_field_finder.py
@@ -0,0 +1,87 @@
+import argparse
+from pathlib import Path
+from typing import Any, Iterable
+
+from portality.bll.services.journal import JournalService
+from portality.lib import csv_utils
+from portality.models import Application, Journal
+
+
+def to_k_v(item: Any, prefix: list = None):
+ if prefix is None:
+ prefix = []
+
+ if isinstance(item, dict):
+ for k, v in item.items():
+ yield from to_k_v(v, prefix=prefix + [k])
+
+ elif isinstance(item, list):
+ for k, v in enumerate(item):
+ yield from to_k_v(v, prefix=prefix + [k])
+ else:
+ yield '.'.join(map(str, prefix)), str(item)
+
+
+def tee(txt: str, out_file):
+ print(txt)
+ out_file.write(txt + '\n')
+
+
+def write_bad_data_domain_object(domain_object_class: Any, out_path):
+ with open(out_path, 'w') as f:
+ items = iter(domain_object_class.iterall())
+ while True:
+ try:
+ j = next(items, None)
+ except:
+ continue
+
+ if j is None:
+ break
+
+ for k, v in filter_bad_only(to_k_v(j.data)):
+ tee(f'{j.id} {k} [{v}]', f)
+
+
+def main2():
+ with open('/tmp/journals.csv', 'w') as f:
+ JournalService._make_journals_csv(f)
+
+
+def is_bad_str(v: str):
+ return isinstance(v, str) and v != v.strip()
+
+
+def filter_bad_only(row: Iterable):
+ return (i for i in row if is_bad_str(i[1]))
+
+
+def write_bad_data_journals_csv(csv_path, out_path):
+ with open(out_path, 'w') as out_file:
+ for row in csv_utils.read_all(csv_path, as_dict=True):
+ for k, v in filter_bad_only(row.items()):
+ tee(f'{k} [{v}]', out_file)
+
+
+def write_results(journal_csv_path, out_dir):
+ # out_dir = Path('/tmp')
+ # journal_csv_path = '/home/kk/tmp/journals.csv'
+ out_dir = Path(out_dir)
+ write_bad_data_domain_object(Application, out_dir / 'bad_app.txt')
+ write_bad_data_domain_object(Journal, out_dir / 'bad_journals.txt')
+ if journal_csv_path:
+ write_bad_data_journals_csv(journal_csv_path, out_dir / 'bad_journals_csv.txt')
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Output file with bad data')
+ parser.add_argument('-i', '--input', help='Path of input CSV file', type=str, default=None)
+ parser.add_argument('-o', '--output', help='Output directory', type=str, default='.')
+ args = parser.parse_args(
+ # ['-i', '/home/kk/tmp/journals.csv', '-o', '/tmp']
+ )
+ write_results(args.input, args.output)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/portality/scripts/priorities.csv b/portality/scripts/priorities.csv
index 08e56c10b5..75c95cf037 100644
--- a/portality/scripts/priorities.csv
+++ b/portality/scripts/priorities.csv
@@ -1,6 +1,7 @@
id,labels,columns
HP/DaR,"Priority: High, Type: Data at Risk",
HP/bug,"Priority: High, bug",
+Deadline,Priority: Deadline,
HP/PfL,"Prioroty: High, Workflow: Pending for Live",Review
HP/sup,"Priority: High, Origin: Support",
Test1,Workflow: On Test,Review
diff --git a/portality/settings.py b/portality/settings.py
index 152e3ec2d4..2bffdbab8a 100644
--- a/portality/settings.py
+++ b/portality/settings.py
@@ -9,7 +9,7 @@
# Application Version information
# ~~->API:Feature~~
-DOAJ_VERSION = "6.3.10"
+DOAJ_VERSION = "6.3.16"
API_VERSION = "3.0.1"
######################################
@@ -439,6 +439,7 @@
"anon_export": {"month": "*", "day": "10", "day_of_week": "*", "hour": "6", "minute": "30"},
"old_data_cleanup": {"month": "*", "day": "12", "day_of_week": "*", "hour": "6", "minute": "30"},
"monitor_bgjobs": {"month": "*", "day": "*/6", "day_of_week": "*", "hour": "10", "minute": "0"},
+ "find_discontinued_soon": {"month": "*", "day": "*", "day_of_week": "*", "hour": "0", "minute": "3"}
}
HUEY_TASKS = {
@@ -946,6 +947,8 @@
# OAI-PMH SETTINGS
# ~~->OAIPMH:Feature~~
+OAI_ADMIN_EMAIL = 'helpdesk+oai@doaj.org'
+
# ~~->OAIAriticleXML:Crosswalk~~
# ~~->OAIJournalXML:Crosswalk~~
OAI_DC_METADATA_FORMAT = {
@@ -1385,3 +1388,29 @@
# Pages under maintenance
PRESERVATION_PAGE_UNDER_MAINTENANCE = False
+
+# report journals that discontinue in ... days (eg. 1 = tomorrow)
+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
-